mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-18 16:28:55 +00:00
Merge branch 'split-packages'
This commit is contained in:
commit
aea5c5dfcb
32 changed files with 543 additions and 423 deletions
47
Cargo.lock
generated
47
Cargo.lock
generated
|
@ -1677,18 +1677,37 @@ dependencies = [
|
||||||
name = "youmubot"
|
name = "youmubot"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
|
||||||
"dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serenity 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"youmubot-core 0.1.0",
|
||||||
|
"youmubot-db 0.1.0",
|
||||||
|
"youmubot-osu 0.1.0",
|
||||||
|
"youmubot-prelude 0.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "youmubot-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
|
||||||
"regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
|
||||||
"reqwest 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
|
||||||
"rustbreak 2.0.0-rc3 (registry+https://github.com/rust-lang/crates.io-index)",
|
|
||||||
"serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serenity 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serenity 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"static_assertions 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"static_assertions 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"youmubot-osu 0.1.0",
|
"youmubot-db 0.1.0",
|
||||||
|
"youmubot-prelude 0.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "youmubot-db"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"rustbreak 2.0.0-rc3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"serenity 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1697,10 +1716,24 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"regex 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"reqwest 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"reqwest 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serenity 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serenity 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"youmubot-db 0.1.0",
|
||||||
|
"youmubot-prelude 0.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "youmubot-prelude"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"reqwest 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"serenity 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"youmubot-db 0.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
|
|
||||||
members = [
|
members = [
|
||||||
|
"youmubot-prelude",
|
||||||
|
"youmubot-db",
|
||||||
|
"youmubot-core",
|
||||||
"youmubot-osu",
|
"youmubot-osu",
|
||||||
"youmubot",
|
"youmubot",
|
||||||
]
|
]
|
||||||
|
|
17
README.md
Normal file
17
README.md
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# youmubot
|
||||||
|
|
||||||
|
A Discord bot made specifically for server "Dự tuyển Tổng Hợp". Written in Rust.
|
||||||
|
|
||||||
|
All PRs welcome.
|
||||||
|
|
||||||
|
## Project structure
|
||||||
|
|
||||||
|
- `youmubot`: The main command. Collect configurations and dispatch commands.
|
||||||
|
- `youmubot-prelude`: Base structures and handy functions for command parsing / service handling.
|
||||||
|
- `youmubot-db`: Base database structures.
|
||||||
|
- `youmubot-core`: Core commands: admin, fun, community
|
||||||
|
- `youmubot-osu`: osu!-related commands.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Basically MIT.
|
17
youmubot-core/Cargo.toml
Normal file
17
youmubot-core/Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[package]
|
||||||
|
name = "youmubot-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Natsu Kagami <natsukagami@gmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serenity = "0.8"
|
||||||
|
rand = "0.7"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
chrono = "0.4"
|
||||||
|
static_assertions = "1.1"
|
||||||
|
|
||||||
|
youmubot-db = { path = "../youmubot-db" }
|
||||||
|
youmubot-prelude = { path = "../youmubot-prelude" }
|
|
@ -1,4 +1,3 @@
|
||||||
use serenity::prelude::*;
|
|
||||||
use serenity::{
|
use serenity::{
|
||||||
framework::standard::{
|
framework::standard::{
|
||||||
macros::{command, group},
|
macros::{command, group},
|
||||||
|
@ -11,6 +10,7 @@ use serenity::{
|
||||||
};
|
};
|
||||||
use soft_ban::{SOFT_BAN_COMMAND, SOFT_BAN_INIT_COMMAND};
|
use soft_ban::{SOFT_BAN_COMMAND, SOFT_BAN_INIT_COMMAND};
|
||||||
use std::{thread::sleep, time::Duration};
|
use std::{thread::sleep, time::Duration};
|
||||||
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
mod soft_ban;
|
mod soft_ban;
|
||||||
pub use soft_ban::watch_soft_bans;
|
pub use soft_ban::watch_soft_bans;
|
|
@ -1,9 +1,5 @@
|
||||||
use crate::{
|
use crate::db::{ServerSoftBans, SoftBans};
|
||||||
commands::args,
|
|
||||||
db::{DBWriteGuard, ServerSoftBans, SoftBans},
|
|
||||||
};
|
|
||||||
use chrono::offset::Utc;
|
use chrono::offset::Utc;
|
||||||
use serenity::prelude::*;
|
|
||||||
use serenity::{
|
use serenity::{
|
||||||
framework::standard::{macros::command, Args, CommandError as Error, CommandResult},
|
framework::standard::{macros::command, Args, CommandError as Error, CommandResult},
|
||||||
model::{
|
model::{
|
||||||
|
@ -12,6 +8,7 @@ use serenity::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
#[required_permissions(ADMINISTRATOR)]
|
#[required_permissions(ADMINISTRATOR)]
|
||||||
|
@ -33,13 +30,9 @@ pub fn soft_ban(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResu
|
||||||
};
|
};
|
||||||
let guild = msg.guild_id.ok_or(Error::from("Command is guild only"))?;
|
let guild = msg.guild_id.ok_or(Error::from("Command is guild only"))?;
|
||||||
|
|
||||||
let mut data = ctx.data.write();
|
let db = SoftBans::open(&*ctx.data.read());
|
||||||
let mut data = data
|
let mut db = db.borrow_mut()?;
|
||||||
.get_mut::<SoftBans>()
|
let mut server_ban = db.get_mut(&guild).and_then(|v| match v {
|
||||||
.ok_or(Error::from("DB initialized"))
|
|
||||||
.map(|v| DBWriteGuard::from(v))?;
|
|
||||||
let mut data = data.borrow_mut()?;
|
|
||||||
let mut server_ban = data.get_mut(&guild).and_then(|v| match v {
|
|
||||||
ServerSoftBans::Unimplemented => None,
|
ServerSoftBans::Unimplemented => None,
|
||||||
ServerSoftBans::Implemented(ref mut v) => Some(v),
|
ServerSoftBans::Implemented(ref mut v) => Some(v),
|
||||||
});
|
});
|
||||||
|
@ -98,14 +91,10 @@ pub fn soft_ban_init(ctx: &mut Context, msg: &Message, mut args: Args) -> Comman
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
// Check if we already set up
|
// Check if we already set up
|
||||||
let mut data = ctx.data.write();
|
let db = SoftBans::open(&*ctx.data.read());
|
||||||
let mut db: DBWriteGuard<_> = data
|
|
||||||
.get_mut::<SoftBans>()
|
|
||||||
.ok_or(Error::from("DB uninitialized"))?
|
|
||||||
.into();
|
|
||||||
let mut db = db.borrow_mut()?;
|
let mut db = db.borrow_mut()?;
|
||||||
let server = db
|
let server = db
|
||||||
.get_mut(&guild.id)
|
.get(&guild.id)
|
||||||
.map(|v| match v {
|
.map(|v| match v {
|
||||||
ServerSoftBans::Unimplemented => false,
|
ServerSoftBans::Unimplemented => false,
|
||||||
_ => true,
|
_ => true,
|
||||||
|
@ -122,7 +111,7 @@ pub fn soft_ban_init(ctx: &mut Context, msg: &Message, mut args: Args) -> Comman
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch the soft bans.
|
// Watch the soft bans.
|
||||||
pub fn watch_soft_bans(client: &mut serenity::Client) -> impl FnOnce() -> () + 'static {
|
pub fn watch_soft_bans(client: &serenity::Client) -> impl FnOnce() -> () + 'static {
|
||||||
let cache_http = {
|
let cache_http = {
|
||||||
let cache_http = client.cache_and_http.clone();
|
let cache_http = client.cache_and_http.clone();
|
||||||
let cache: serenity::cache::CacheRwLock = cache_http.cache.clone().into();
|
let cache: serenity::cache::CacheRwLock = cache_http.cache.clone().into();
|
||||||
|
@ -135,12 +124,9 @@ pub fn watch_soft_bans(client: &mut serenity::Client) -> impl FnOnce() -> () + '
|
||||||
// Scope so that locks are released
|
// Scope so that locks are released
|
||||||
{
|
{
|
||||||
// Poll the data for any changes.
|
// Poll the data for any changes.
|
||||||
let mut data = data.write();
|
let db = data.read();
|
||||||
let mut db: DBWriteGuard<_> = data
|
let db = SoftBans::open(&*db);
|
||||||
.get_mut::<SoftBans>()
|
let mut db = db.borrow_mut().expect("Borrowable");
|
||||||
.expect("DB wrongly initialized")
|
|
||||||
.into();
|
|
||||||
let mut db = db.borrow_mut().expect("cannot unpack DB");
|
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
for (server_id, soft_bans) in db.iter_mut() {
|
for (server_id, soft_bans) in db.iter_mut() {
|
||||||
let server_name: String = match server_id.to_partial_guild(cache_http) {
|
let server_name: String = match server_id.to_partial_guild(cache_http) {
|
|
@ -2,7 +2,6 @@ use rand::{
|
||||||
distributions::{Distribution, Uniform},
|
distributions::{Distribution, Uniform},
|
||||||
thread_rng,
|
thread_rng,
|
||||||
};
|
};
|
||||||
use serenity::prelude::*;
|
|
||||||
use serenity::{
|
use serenity::{
|
||||||
framework::standard::{
|
framework::standard::{
|
||||||
macros::{command, group},
|
macros::{command, group},
|
||||||
|
@ -14,6 +13,7 @@ use serenity::{
|
||||||
},
|
},
|
||||||
utils::MessageBuilder,
|
utils::MessageBuilder,
|
||||||
};
|
};
|
||||||
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
mod votes;
|
mod votes;
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
use crate::commands::args::Duration as ParseDuration;
|
|
||||||
use serenity::framework::standard::CommandError as Error;
|
use serenity::framework::standard::CommandError as Error;
|
||||||
use serenity::prelude::*;
|
|
||||||
use serenity::{
|
use serenity::{
|
||||||
framework::standard::{macros::command, Args, CommandResult},
|
framework::standard::{macros::command, Args, CommandResult},
|
||||||
model::{
|
model::{
|
||||||
|
@ -12,6 +10,7 @@ use serenity::{
|
||||||
use std::collections::HashMap as Map;
|
use std::collections::HashMap as Map;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use youmubot_prelude::{Duration as ParseDuration, *};
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
#[description = "🎌 Cast a poll upon everyone and ask them for opinions!"]
|
#[description = "🎌 Cast a poll upon everyone and ask them for opinions!"]
|
38
youmubot-core/src/db.rs
Normal file
38
youmubot-core/src/db.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serenity::{
|
||||||
|
model::id::{RoleId, UserId},
|
||||||
|
};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use youmubot_db::{GuildMap, DB};
|
||||||
|
|
||||||
|
/// A list of SoftBans for all servers.
|
||||||
|
pub type SoftBans = DB<GuildMap<ServerSoftBans>>;
|
||||||
|
|
||||||
|
/// For the admin commands:
|
||||||
|
/// - Each server might have a `soft ban` role implemented.
|
||||||
|
/// - We allow periodical `soft ban` applications.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub enum ServerSoftBans {
|
||||||
|
Implemented(ImplementedSoftBans),
|
||||||
|
Unimplemented,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerSoftBans {
|
||||||
|
// Create a new, implemented role.
|
||||||
|
pub fn new_implemented(role: RoleId) -> ServerSoftBans {
|
||||||
|
ServerSoftBans::Implemented(ImplementedSoftBans {
|
||||||
|
role,
|
||||||
|
periodical_bans: HashMap::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct ImplementedSoftBans {
|
||||||
|
/// The soft-ban role.
|
||||||
|
pub role: RoleId,
|
||||||
|
/// List of all to-unban people.
|
||||||
|
pub periodical_bans: HashMap<UserId, DateTime<Utc>>,
|
||||||
|
}
|
|
@ -1,8 +1,5 @@
|
||||||
use crate::http::HTTP;
|
|
||||||
use reqwest::blocking::Client as HTTPClient;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serenity::framework::standard::CommandError as Error;
|
use serenity::framework::standard::CommandError as Error;
|
||||||
use serenity::prelude::*;
|
|
||||||
use serenity::{
|
use serenity::{
|
||||||
framework::standard::{
|
framework::standard::{
|
||||||
macros::{check, command},
|
macros::{check, command},
|
||||||
|
@ -11,6 +8,7 @@ use serenity::{
|
||||||
model::channel::{Channel, Message},
|
model::channel::{Channel, Message},
|
||||||
};
|
};
|
||||||
use std::string::ToString;
|
use std::string::ToString;
|
||||||
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
#[checks(nsfw)]
|
#[checks(nsfw)]
|
||||||
|
@ -45,9 +43,8 @@ fn nsfw_check(ctx: &mut Context, msg: &Message, _: &mut Args, _: &CommandOptions
|
||||||
|
|
||||||
fn message_command(ctx: &mut Context, msg: &Message, args: Args, rating: Rating) -> CommandResult {
|
fn message_command(ctx: &mut Context, msg: &Message, args: Args, rating: Rating) -> CommandResult {
|
||||||
let tags = args.remains().unwrap_or("touhou");
|
let tags = args.remains().unwrap_or("touhou");
|
||||||
let http = ctx.data.read();
|
let http = ctx.data.get_cloned::<HTTPClient>();
|
||||||
let http = http.get::<HTTP>().unwrap();
|
let image = get_image(&http, rating, tags)?;
|
||||||
let image = get_image(http, rating, tags)?;
|
|
||||||
match image {
|
match image {
|
||||||
None => msg.reply(&ctx, "🖼️ No image found...\n💡 Tip: In danbooru, character names follow Japanese standards (last name before first name), so **Hakurei Reimu** might give you an image while **Reimu Hakurei** won't."),
|
None => msg.reply(&ctx, "🖼️ No image found...\n💡 Tip: In danbooru, character names follow Japanese standards (last name before first name), so **Hakurei Reimu** might give you an image while **Reimu Hakurei** won't."),
|
||||||
Some(url) => msg.reply(
|
Some(url) => msg.reply(
|
||||||
|
@ -59,7 +56,11 @@ fn message_command(ctx: &mut Context, msg: &Message, args: Args, rating: Rating)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets an image URL.
|
// Gets an image URL.
|
||||||
fn get_image(client: &HTTPClient, rating: Rating, tags: &str) -> Result<Option<String>, Error> {
|
fn get_image(
|
||||||
|
client: &<HTTPClient as TypeMapKey>::Value,
|
||||||
|
rating: Rating,
|
||||||
|
tags: &str,
|
||||||
|
) -> Result<Option<String>, Error> {
|
||||||
// Fix the tags: change whitespaces to +
|
// Fix the tags: change whitespaces to +
|
||||||
let tags = tags.split_whitespace().collect::<Vec<_>>().join("_");
|
let tags = tags.split_whitespace().collect::<Vec<_>>().join("_");
|
||||||
let req = client
|
let req = client
|
|
@ -2,7 +2,6 @@ use rand::{
|
||||||
distributions::{Distribution, Uniform},
|
distributions::{Distribution, Uniform},
|
||||||
thread_rng,
|
thread_rng,
|
||||||
};
|
};
|
||||||
use serenity::prelude::*;
|
|
||||||
use serenity::{
|
use serenity::{
|
||||||
framework::standard::{
|
framework::standard::{
|
||||||
macros::{command, group},
|
macros::{command, group},
|
||||||
|
@ -11,6 +10,7 @@ use serenity::{
|
||||||
model::{channel::Message, id::UserId},
|
model::{channel::Message, id::UserId},
|
||||||
utils::MessageBuilder,
|
utils::MessageBuilder,
|
||||||
};
|
};
|
||||||
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
mod images;
|
mod images;
|
||||||
mod names;
|
mod names;
|
|
@ -1,4 +1,3 @@
|
||||||
use serenity::prelude::*;
|
|
||||||
use serenity::{
|
use serenity::{
|
||||||
framework::standard::{
|
framework::standard::{
|
||||||
help_commands, macros::help, Args, CommandGroup, CommandResult, HelpOptions,
|
help_commands, macros::help, Args, CommandGroup, CommandResult, HelpOptions,
|
||||||
|
@ -6,21 +5,30 @@ use serenity::{
|
||||||
model::{channel::Message, id::UserId},
|
model::{channel::Message, id::UserId},
|
||||||
};
|
};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use youmubot_prelude::*;
|
||||||
mod announcer;
|
|
||||||
mod args;
|
|
||||||
|
|
||||||
pub mod admin;
|
pub mod admin;
|
||||||
pub mod community;
|
pub mod community;
|
||||||
|
mod db;
|
||||||
pub mod fun;
|
pub mod fun;
|
||||||
pub mod osu;
|
|
||||||
|
|
||||||
pub use admin::ADMIN_GROUP;
|
pub use admin::ADMIN_GROUP;
|
||||||
pub use community::COMMUNITY_GROUP;
|
pub use community::COMMUNITY_GROUP;
|
||||||
pub use fun::FUN_GROUP;
|
pub use fun::FUN_GROUP;
|
||||||
pub use osu::OSU_GROUP;
|
|
||||||
|
|
||||||
pub use announcer::Announcer;
|
/// Sets up all databases in the client.
|
||||||
|
pub fn setup(
|
||||||
|
path: &std::path::Path,
|
||||||
|
client: &serenity::client::Client,
|
||||||
|
data: &mut youmubot_prelude::ShareMap,
|
||||||
|
) -> serenity::framework::standard::CommandResult {
|
||||||
|
db::SoftBans::insert_into(&mut *data, &path.join("soft_bans.yaml"))?;
|
||||||
|
|
||||||
|
// Create handler threads
|
||||||
|
std::thread::spawn(admin::watch_soft_bans(client));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// A help command
|
// A help command
|
||||||
#[help]
|
#[help]
|
24
youmubot-db/Cargo.toml
Normal file
24
youmubot-db/Cargo.toml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
[package]
|
||||||
|
name = "youmubot-db"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Natsu Kagami <natsukagami@gmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serenity = "0.8"
|
||||||
|
dotenv = "0.15"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
chrono = "0.4.9"
|
||||||
|
# rand = "0.7.2"
|
||||||
|
# static_assertions = "1.1.0"
|
||||||
|
# reqwest = "0.10.1"
|
||||||
|
# regex = "1"
|
||||||
|
# lazy_static = "1"
|
||||||
|
# youmubot-osu = { path = "../youmubot-osu" }
|
||||||
|
rayon = "1.1"
|
||||||
|
|
||||||
|
[dependencies.rustbreak]
|
||||||
|
version = "2.0.0-rc3"
|
||||||
|
features = ["yaml_enc"]
|
66
youmubot-db/src/lib.rs
Normal file
66
youmubot-db/src/lib.rs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
use rustbreak::{deser::Yaml as Ron, FileDatabase};
|
||||||
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
|
use serenity::{framework::standard::CommandError as Error, model::id::GuildId, prelude::*};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// GuildMap defines the guild-map type.
|
||||||
|
/// It is basically a HashMap from a GuildId to a data structure.
|
||||||
|
pub type GuildMap<V> = HashMap<GuildId, V>;
|
||||||
|
/// The generic DB type we will be using.
|
||||||
|
pub struct DB<T>(std::marker::PhantomData<T>);
|
||||||
|
impl<T: std::any::Any> serenity::prelude::TypeMapKey for DB<T> {
|
||||||
|
type Value = Arc<FileDatabase<T, Ron>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: std::any::Any + Default + Send + Sync + Clone + Serialize + std::fmt::Debug> DB<T>
|
||||||
|
where
|
||||||
|
for<'de> T: Deserialize<'de>,
|
||||||
|
{
|
||||||
|
/// Insert into a ShareMap.
|
||||||
|
pub fn insert_into(data: &mut ShareMap, path: impl AsRef<Path>) -> Result<(), Error> {
|
||||||
|
let db = FileDatabase::<T, Ron>::from_path(path, T::default())?;
|
||||||
|
db.load().or_else(|e| {
|
||||||
|
dbg!(e);
|
||||||
|
db.save()
|
||||||
|
})?;
|
||||||
|
data.insert::<DB<T>>(Arc::new(db));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open a previously inserted DB.
|
||||||
|
pub fn open(data: &ShareMap) -> DBWriteGuard<T> {
|
||||||
|
data.get::<Self>().expect("DB initialized").clone().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The write guard for our FileDatabase.
|
||||||
|
/// It wraps the FileDatabase in a write-on-drop lock.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DBWriteGuard<T>(Arc<FileDatabase<T, Ron>>)
|
||||||
|
where
|
||||||
|
T: Send + Sync + Clone + std::fmt::Debug + Serialize + DeserializeOwned;
|
||||||
|
|
||||||
|
impl<T> From<Arc<FileDatabase<T, Ron>>> for DBWriteGuard<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + Clone + std::fmt::Debug + Serialize + DeserializeOwned,
|
||||||
|
{
|
||||||
|
fn from(v: Arc<FileDatabase<T, Ron>>) -> Self {
|
||||||
|
DBWriteGuard(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DBWriteGuard<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + Clone + std::fmt::Debug + Serialize + DeserializeOwned,
|
||||||
|
{
|
||||||
|
/// Borrows the FileDatabase.
|
||||||
|
pub fn borrow(&self) -> Result<std::sync::RwLockReadGuard<T>, rustbreak::RustbreakError> {
|
||||||
|
(*self).0.borrow_data()
|
||||||
|
}
|
||||||
|
/// Borrows the FileDatabase for writing.
|
||||||
|
pub fn borrow_mut(&self) -> Result<std::sync::RwLockWriteGuard<T>, rustbreak::RustbreakError> {
|
||||||
|
(*self).0.borrow_data_mut()
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,12 @@ chrono = "0.4.10"
|
||||||
reqwest = "0.10.1"
|
reqwest = "0.10.1"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
bitflags = "1"
|
bitflags = "1"
|
||||||
|
rayon = "1.1"
|
||||||
|
lazy_static = "1"
|
||||||
|
regex = "1"
|
||||||
|
|
||||||
|
youmubot-db = { path = "../youmubot-db" }
|
||||||
|
youmubot-prelude = { path = "../youmubot-prelude" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
|
@ -1,25 +1,17 @@
|
||||||
use super::{embeds::score_embed, BeatmapWithMode};
|
use super::db::{OsuSavedUsers, OsuUser};
|
||||||
|
use super::{embeds::score_embed, BeatmapWithMode, OsuClient};
|
||||||
use crate::{
|
use crate::{
|
||||||
commands::announcer::Announcer,
|
models::{Mode, Score},
|
||||||
db::{OsuSavedUsers, OsuUser},
|
request::{BeatmapRequestKind, UserID},
|
||||||
http::{Osu, HTTP},
|
Client as Osu,
|
||||||
};
|
};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use reqwest::blocking::Client as HTTPClient;
|
|
||||||
use serenity::{
|
use serenity::{
|
||||||
framework::standard::{CommandError as Error, CommandResult},
|
framework::standard::{CommandError as Error, CommandResult},
|
||||||
http::Http,
|
http::Http,
|
||||||
model::{
|
model::id::{ChannelId, UserId},
|
||||||
id::{ChannelId, UserId},
|
|
||||||
misc::Mentionable,
|
|
||||||
},
|
|
||||||
prelude::ShareMap,
|
|
||||||
};
|
|
||||||
use youmubot_osu::{
|
|
||||||
models::{Mode, Score},
|
|
||||||
request::{BeatmapRequestKind, UserID},
|
|
||||||
Client as OsuClient,
|
|
||||||
};
|
};
|
||||||
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
/// Announce osu! top scores.
|
/// Announce osu! top scores.
|
||||||
pub struct OsuAnnouncer;
|
pub struct OsuAnnouncer;
|
||||||
|
@ -30,33 +22,36 @@ impl Announcer for OsuAnnouncer {
|
||||||
}
|
}
|
||||||
fn send_messages(
|
fn send_messages(
|
||||||
c: &Http,
|
c: &Http,
|
||||||
d: &mut ShareMap,
|
d: AppData,
|
||||||
channels: impl Fn(UserId) -> Vec<ChannelId> + Sync,
|
channels: impl Fn(UserId) -> Vec<ChannelId> + Sync,
|
||||||
) -> CommandResult {
|
) -> CommandResult {
|
||||||
let http = d.get::<HTTP>().expect("HTTP");
|
let osu = d.get_cloned::<OsuClient>();
|
||||||
let osu = d.get::<Osu>().expect("osu!client");
|
|
||||||
// For each user...
|
// For each user...
|
||||||
let mut data = d
|
let mut data = OsuSavedUsers::open(&*d.read()).borrow()?.clone();
|
||||||
.get::<OsuSavedUsers>()
|
|
||||||
.expect("DB initialized")
|
|
||||||
.read(|f| f.clone())?;
|
|
||||||
for (user_id, osu_user) in data.iter_mut() {
|
for (user_id, osu_user) in data.iter_mut() {
|
||||||
let mut user = None;
|
let mut user = None;
|
||||||
for mode in &[Mode::Std, Mode::Taiko, Mode::Mania, Mode::Catch] {
|
for mode in &[Mode::Std, Mode::Taiko, Mode::Mania, Mode::Catch] {
|
||||||
let scores = OsuAnnouncer::scan_user(http, osu, osu_user, *mode)?;
|
let scores = OsuAnnouncer::scan_user(&osu, osu_user, *mode)?;
|
||||||
if scores.is_empty() {
|
if scores.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let user = user.get_or_insert_with(|| {
|
let user = {
|
||||||
osu.user(http, UserID::ID(osu_user.id), |f| f)
|
let user = &mut user;
|
||||||
.unwrap()
|
if let None = user {
|
||||||
.unwrap()
|
match osu.user(UserID::ID(osu_user.id), |f| f.mode(*mode)) {
|
||||||
});
|
Ok(u) => {
|
||||||
|
*user = u;
|
||||||
|
}
|
||||||
|
Err(_) => continue,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
user.as_ref().unwrap()
|
||||||
|
};
|
||||||
scores
|
scores
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.filter_map(|(rank, score)| {
|
.filter_map(|(rank, score)| {
|
||||||
let beatmap = osu
|
let beatmap = osu
|
||||||
.beatmaps(http, BeatmapRequestKind::Beatmap(score.beatmap_id), |f| f)
|
.beatmaps(BeatmapRequestKind::Beatmap(score.beatmap_id), |f| f)
|
||||||
.map(|v| BeatmapWithMode(v.into_iter().next().unwrap(), *mode));
|
.map(|v| BeatmapWithMode(v.into_iter().next().unwrap(), *mode));
|
||||||
let channels = channels(*user_id);
|
let channels = channels(*user_id);
|
||||||
match beatmap {
|
match beatmap {
|
||||||
|
@ -81,21 +76,14 @@ impl Announcer for OsuAnnouncer {
|
||||||
osu_user.last_update = chrono::Utc::now();
|
osu_user.last_update = chrono::Utc::now();
|
||||||
}
|
}
|
||||||
// Update users
|
// Update users
|
||||||
let f = d.get_mut::<OsuSavedUsers>().expect("DB initialized");
|
*OsuSavedUsers::open(&*d.read()).borrow_mut()? = data;
|
||||||
f.write(|f| *f = data)?;
|
|
||||||
f.save()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OsuAnnouncer {
|
impl OsuAnnouncer {
|
||||||
fn scan_user(
|
fn scan_user(osu: &Osu, u: &OsuUser, mode: Mode) -> Result<Vec<(u8, Score)>, Error> {
|
||||||
http: &HTTPClient,
|
let scores = osu.user_best(UserID::ID(u.id), |f| f.mode(mode).limit(25))?;
|
||||||
osu: &OsuClient,
|
|
||||||
u: &OsuUser,
|
|
||||||
mode: Mode,
|
|
||||||
) -> Result<Vec<(u8, Score)>, Error> {
|
|
||||||
let scores = osu.user_best(http, UserID::ID(u.id), |f| f.mode(mode).limit(25))?;
|
|
||||||
let scores = scores
|
let scores = scores
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|s: &Score| s.date >= u.last_update)
|
.filter(|s: &Score| s.date >= u.last_update)
|
|
@ -1,5 +1,5 @@
|
||||||
|
use super::db::OsuLastBeatmap;
|
||||||
use super::BeatmapWithMode;
|
use super::BeatmapWithMode;
|
||||||
use crate::db::{DBWriteGuard, OsuLastBeatmap};
|
|
||||||
use serenity::{
|
use serenity::{
|
||||||
framework::standard::{CommandError as Error, CommandResult},
|
framework::standard::{CommandError as Error, CommandResult},
|
||||||
model::id::ChannelId,
|
model::id::ChannelId,
|
||||||
|
@ -8,14 +8,11 @@ use serenity::{
|
||||||
|
|
||||||
/// Save the beatmap into the server data storage.
|
/// Save the beatmap into the server data storage.
|
||||||
pub(crate) fn save_beatmap(
|
pub(crate) fn save_beatmap(
|
||||||
data: &mut ShareMap,
|
data: &ShareMap,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
bm: &BeatmapWithMode,
|
bm: &BeatmapWithMode,
|
||||||
) -> CommandResult {
|
) -> CommandResult {
|
||||||
let mut db: DBWriteGuard<_> = data
|
let db = OsuLastBeatmap::open(data);
|
||||||
.get_mut::<OsuLastBeatmap>()
|
|
||||||
.expect("DB is implemented")
|
|
||||||
.into();
|
|
||||||
let mut db = db.borrow_mut()?;
|
let mut db = db.borrow_mut()?;
|
||||||
|
|
||||||
db.insert(channel_id, (bm.0.clone(), bm.mode()));
|
db.insert(channel_id, (bm.0.clone(), bm.mode()));
|
||||||
|
@ -28,8 +25,8 @@ pub(crate) fn get_beatmap(
|
||||||
data: &ShareMap,
|
data: &ShareMap,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
) -> Result<Option<BeatmapWithMode>, Error> {
|
) -> Result<Option<BeatmapWithMode>, Error> {
|
||||||
let db = data.get::<OsuLastBeatmap>().expect("DB is implemented");
|
let db = OsuLastBeatmap::open(data);
|
||||||
let db = db.borrow_data()?;
|
let db = db.borrow()?;
|
||||||
|
|
||||||
Ok(db
|
Ok(db
|
||||||
.get(&channel_id)
|
.get(&channel_id)
|
22
youmubot-osu/src/discord/db.rs
Normal file
22
youmubot-osu/src/discord/db.rs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serenity::{
|
||||||
|
model::id::{ChannelId, UserId},
|
||||||
|
};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use youmubot_db::{DB};
|
||||||
|
use crate::models::{Beatmap, Mode};
|
||||||
|
|
||||||
|
/// Save the user IDs.
|
||||||
|
pub type OsuSavedUsers = DB<HashMap<UserId, OsuUser>>;
|
||||||
|
|
||||||
|
/// Save each channel's last requested beatmap.
|
||||||
|
pub type OsuLastBeatmap = DB<HashMap<ChannelId, (Beatmap, Mode)>>;
|
||||||
|
|
||||||
|
/// An osu! saved user.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct OsuUser {
|
||||||
|
pub id: u64,
|
||||||
|
pub last_update: DateTime<Utc>,
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
use super::BeatmapWithMode;
|
use super::BeatmapWithMode;
|
||||||
use crate::commands::args::Duration;
|
use crate::models::{Beatmap, Mode, Rank, Score, User};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use serenity::{builder::CreateEmbed, utils::MessageBuilder};
|
use serenity::{builder::CreateEmbed, utils::MessageBuilder};
|
||||||
use youmubot_osu::models::{Beatmap, Mode, Rank, Score, User};
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
fn format_mode(actual: Mode, original: Mode) -> String {
|
fn format_mode(actual: Mode, original: Mode) -> String {
|
||||||
if actual == original {
|
if actual == original {
|
|
@ -1,17 +1,17 @@
|
||||||
use crate::http;
|
use super::OsuClient;
|
||||||
|
use crate::{
|
||||||
|
models::{Beatmap, Mode},
|
||||||
|
request::BeatmapRequestKind,
|
||||||
|
};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
builder::CreateMessage,
|
builder::CreateMessage,
|
||||||
framework::standard::{CommandError as Error, CommandResult},
|
framework::standard::{CommandError as Error, CommandResult},
|
||||||
model::channel::Message,
|
model::channel::Message,
|
||||||
prelude::*,
|
|
||||||
utils::MessageBuilder,
|
utils::MessageBuilder,
|
||||||
};
|
};
|
||||||
use youmubot_osu::{
|
use youmubot_prelude::*;
|
||||||
models::{Beatmap, Mode},
|
|
||||||
request::BeatmapRequestKind,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::embeds::{beatmap_embed, beatmapset_embed};
|
use super::embeds::{beatmap_embed, beatmapset_embed};
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ pub fn hook(ctx: &mut Context, msg: &Message) -> () {
|
||||||
}
|
}
|
||||||
// Save the beatmap for query later.
|
// Save the beatmap for query later.
|
||||||
if let Some(t) = last_beatmap {
|
if let Some(t) = last_beatmap {
|
||||||
if let Err(v) = super::cache::save_beatmap(&mut *ctx.data.write(), msg.channel_id, &t) {
|
if let Err(v) = super::cache::save_beatmap(&*ctx.data.read(), msg.channel_id, &t) {
|
||||||
dbg!(v);
|
dbg!(v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,9 +71,7 @@ struct ToPrint<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_old_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPrint<'a>>, Error> {
|
fn handle_old_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPrint<'a>>, Error> {
|
||||||
let data = ctx.data.write();
|
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||||
let reqwest = data.get::<http::HTTP>().unwrap();
|
|
||||||
let osu = data.get::<http::Osu>().unwrap();
|
|
||||||
let mut to_prints: Vec<ToPrint<'a>> = Vec::new();
|
let mut to_prints: Vec<ToPrint<'a>> = Vec::new();
|
||||||
for capture in OLD_LINK_REGEX.captures_iter(content) {
|
for capture in OLD_LINK_REGEX.captures_iter(content) {
|
||||||
let req_type = capture.name("link_type").unwrap().as_str();
|
let req_type = capture.name("link_type").unwrap().as_str();
|
||||||
|
@ -95,7 +93,7 @@ fn handle_old_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPri
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
let beatmaps = osu.beatmaps(reqwest, req, |v| match mode {
|
let beatmaps = osu.beatmaps(req, |v| match mode {
|
||||||
Some(m) => v.mode(m, true),
|
Some(m) => v.mode(m, true),
|
||||||
None => v,
|
None => v,
|
||||||
})?;
|
})?;
|
||||||
|
@ -123,9 +121,7 @@ fn handle_old_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPri
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_new_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPrint<'a>>, Error> {
|
fn handle_new_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPrint<'a>>, Error> {
|
||||||
let data = ctx.data.write();
|
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||||
let reqwest = data.get::<http::HTTP>().unwrap();
|
|
||||||
let osu = data.get::<http::Osu>().unwrap();
|
|
||||||
let mut to_prints: Vec<ToPrint<'a>> = Vec::new();
|
let mut to_prints: Vec<ToPrint<'a>> = Vec::new();
|
||||||
for capture in NEW_LINK_REGEX.captures_iter(content) {
|
for capture in NEW_LINK_REGEX.captures_iter(content) {
|
||||||
let mode = capture.name("mode").and_then(|v| {
|
let mode = capture.name("mode").and_then(|v| {
|
||||||
|
@ -145,7 +141,7 @@ fn handle_new_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPri
|
||||||
BeatmapRequestKind::Beatmapset(capture.name("set_id").unwrap().as_str().parse()?)
|
BeatmapRequestKind::Beatmapset(capture.name("set_id").unwrap().as_str().parse()?)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let beatmaps = osu.beatmaps(reqwest, req, |v| match mode {
|
let beatmaps = osu.beatmaps(req, |v| match mode {
|
||||||
Some(m) => v.mode(m, true),
|
Some(m) => v.mode(m, true),
|
||||||
None => v,
|
None => v,
|
||||||
})?;
|
})?;
|
|
@ -1,30 +1,70 @@
|
||||||
use crate::db::{DBWriteGuard, OsuSavedUsers, OsuUser};
|
use crate::{
|
||||||
use crate::http;
|
models::{Beatmap, Mode, User},
|
||||||
|
request::{BeatmapRequestKind, UserID},
|
||||||
|
Client as OsuHttpClient,
|
||||||
|
};
|
||||||
use serenity::{
|
use serenity::{
|
||||||
framework::standard::{
|
framework::standard::{
|
||||||
macros::{command, group},
|
macros::{command, group},
|
||||||
Args, CommandError as Error, CommandResult,
|
Args, CommandError as Error, CommandResult,
|
||||||
},
|
},
|
||||||
model::{channel::Message, id::UserId},
|
model::{channel::Message, id::UserId},
|
||||||
prelude::*,
|
|
||||||
utils::MessageBuilder,
|
utils::MessageBuilder,
|
||||||
};
|
};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use youmubot_osu::{
|
use youmubot_prelude::*;
|
||||||
models::{Beatmap, Mode, User},
|
|
||||||
request::{BeatmapRequestKind, UserID},
|
|
||||||
Client as OsuClient,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod announcer;
|
mod announcer;
|
||||||
mod cache;
|
mod cache;
|
||||||
|
mod db;
|
||||||
pub(crate) mod embeds;
|
pub(crate) mod embeds;
|
||||||
mod hook;
|
mod hook;
|
||||||
|
|
||||||
pub use announcer::OsuAnnouncer;
|
pub use announcer::OsuAnnouncer;
|
||||||
|
use db::OsuUser;
|
||||||
|
use db::{OsuLastBeatmap, OsuSavedUsers};
|
||||||
use embeds::{beatmap_embed, score_embed, user_embed};
|
use embeds::{beatmap_embed, score_embed, user_embed};
|
||||||
pub use hook::hook;
|
pub use hook::hook;
|
||||||
|
|
||||||
|
/// The osu! client.
|
||||||
|
pub(crate) struct OsuClient;
|
||||||
|
|
||||||
|
impl TypeMapKey for OsuClient {
|
||||||
|
type Value = OsuHttpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets up the osu! command handling section.
|
||||||
|
///
|
||||||
|
/// This automatically enables:
|
||||||
|
/// - Related databases
|
||||||
|
/// - An announcer system (that will eventually be revamped)
|
||||||
|
/// - The osu! API client.
|
||||||
|
///
|
||||||
|
/// This does NOT automatically enable:
|
||||||
|
/// - Commands on the "osu" prefix
|
||||||
|
/// - Hooks. Hooks are completely opt-in.
|
||||||
|
///
|
||||||
|
pub fn setup(
|
||||||
|
path: &std::path::Path,
|
||||||
|
client: &serenity::client::Client,
|
||||||
|
data: &mut ShareMap,
|
||||||
|
) -> CommandResult {
|
||||||
|
// Databases
|
||||||
|
OsuSavedUsers::insert_into(&mut *data, &path.join("osu_saved_users.yaml"))?;
|
||||||
|
OsuLastBeatmap::insert_into(&mut *data, &path.join("last_beatmaps.yaml"))?;
|
||||||
|
|
||||||
|
// API client
|
||||||
|
let http_client = data.get_cloned::<HTTPClient>();
|
||||||
|
data.insert::<OsuClient>(OsuHttpClient::new(
|
||||||
|
http_client,
|
||||||
|
std::env::var("OSU_API_KEY").expect("Please set OSU_API_KEY as osu! api key."),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Announcer
|
||||||
|
OsuAnnouncer::scan(&client, std::time::Duration::from_secs(300));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[group]
|
#[group]
|
||||||
#[prefix = "osu"]
|
#[prefix = "osu"]
|
||||||
#[description = "osu! related commands."]
|
#[description = "osu! related commands."]
|
||||||
|
@ -91,18 +131,13 @@ impl AsRef<Beatmap> for BeatmapWithMode {
|
||||||
#[usage = "[username or user_id]"]
|
#[usage = "[username or user_id]"]
|
||||||
#[num_args(1)]
|
#[num_args(1)]
|
||||||
pub fn save(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
pub fn save(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
let mut data = ctx.data.write();
|
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||||
let reqwest = data.get::<http::HTTP>().unwrap();
|
|
||||||
let osu = data.get::<http::Osu>().unwrap();
|
|
||||||
|
|
||||||
let user = args.single::<String>()?;
|
let user = args.single::<String>()?;
|
||||||
let user: Option<User> = osu.user(reqwest, UserID::Auto(user), |f| f)?;
|
let user: Option<User> = osu.user(UserID::Auto(user), |f| f)?;
|
||||||
match user {
|
match user {
|
||||||
Some(u) => {
|
Some(u) => {
|
||||||
let mut db: DBWriteGuard<_> = data
|
let db = OsuSavedUsers::open(&*ctx.data.read());
|
||||||
.get_mut::<OsuSavedUsers>()
|
|
||||||
.ok_or(Error::from("DB uninitialized"))?
|
|
||||||
.into();
|
|
||||||
let mut db = db.borrow_mut()?;
|
let mut db = db.borrow_mut()?;
|
||||||
|
|
||||||
db.insert(
|
db.insert(
|
||||||
|
@ -148,20 +183,14 @@ enum UsernameArg {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UsernameArg {
|
impl UsernameArg {
|
||||||
fn to_user_id_query(
|
fn to_user_id_query(s: Option<Self>, data: &ShareMap, msg: &Message) -> Result<UserID, Error> {
|
||||||
s: Option<Self>,
|
|
||||||
data: &mut ShareMap,
|
|
||||||
msg: &Message,
|
|
||||||
) -> Result<UserID, Error> {
|
|
||||||
let id = match s {
|
let id = match s {
|
||||||
Some(UsernameArg::Raw(s)) => return Ok(UserID::Auto(s)),
|
Some(UsernameArg::Raw(s)) => return Ok(UserID::Auto(s)),
|
||||||
Some(UsernameArg::Tagged(r)) => r,
|
Some(UsernameArg::Tagged(r)) => r,
|
||||||
None => msg.author.id,
|
None => msg.author.id,
|
||||||
};
|
};
|
||||||
let db: DBWriteGuard<_> = data
|
|
||||||
.get_mut::<OsuSavedUsers>()
|
let db = OsuSavedUsers::open(data);
|
||||||
.ok_or(Error::from("DB uninitialized"))?
|
|
||||||
.into();
|
|
||||||
let db = db.borrow()?;
|
let db = db.borrow()?;
|
||||||
db.get(&id)
|
db.get(&id)
|
||||||
.cloned()
|
.cloned()
|
||||||
|
@ -201,28 +230,24 @@ impl FromStr for Nth {
|
||||||
#[example = "#1 / taiko / natsukagami"]
|
#[example = "#1 / taiko / natsukagami"]
|
||||||
#[max_args(3)]
|
#[max_args(3)]
|
||||||
pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
let mut data = ctx.data.write();
|
|
||||||
|
|
||||||
let nth = args.single::<Nth>().unwrap_or(Nth(1)).0.min(50).max(1);
|
let nth = args.single::<Nth>().unwrap_or(Nth(1)).0.min(50).max(1);
|
||||||
let mode = args.single::<ModeArg>().unwrap_or(ModeArg(Mode::Std)).0;
|
let mode = args.single::<ModeArg>().unwrap_or(ModeArg(Mode::Std)).0;
|
||||||
let user = UsernameArg::to_user_id_query(args.single::<UsernameArg>().ok(), &mut *data, msg)?;
|
let user =
|
||||||
|
UsernameArg::to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
||||||
|
|
||||||
let reqwest = data.get::<http::HTTP>().unwrap();
|
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||||
let osu: &OsuClient = data.get::<http::Osu>().unwrap();
|
|
||||||
let user = osu
|
let user = osu
|
||||||
.user(reqwest, user, |f| f.mode(mode))?
|
.user(user, |f| f.mode(mode))?
|
||||||
.ok_or(Error::from("User not found"))?;
|
.ok_or(Error::from("User not found"))?;
|
||||||
let recent_play = osu
|
let recent_play = osu
|
||||||
.user_recent(reqwest, UserID::ID(user.id), |f| f.mode(mode).limit(nth))?
|
.user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(nth))?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.last()
|
.last()
|
||||||
.ok_or(Error::from("No such play"))?;
|
.ok_or(Error::from("No such play"))?;
|
||||||
let beatmap = osu
|
let beatmap = osu
|
||||||
.beatmaps(
|
.beatmaps(BeatmapRequestKind::Beatmap(recent_play.beatmap_id), |f| {
|
||||||
reqwest,
|
f.mode(mode, true)
|
||||||
BeatmapRequestKind::Beatmap(recent_play.beatmap_id),
|
})?
|
||||||
|f| f.mode(mode, true),
|
|
||||||
)?
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.next()
|
.next()
|
||||||
.map(|v| BeatmapWithMode(v, mode))
|
.map(|v| BeatmapWithMode(v, mode))
|
||||||
|
@ -237,7 +262,7 @@ pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Save the beatmap...
|
// Save the beatmap...
|
||||||
cache::save_beatmap(&mut *data, msg.channel_id, &beatmap)?;
|
cache::save_beatmap(&*ctx.data.read(), msg.channel_id, &beatmap)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -246,9 +271,7 @@ pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
#[description = "Show information from the last queried beatmap."]
|
#[description = "Show information from the last queried beatmap."]
|
||||||
#[num_args(0)]
|
#[num_args(0)]
|
||||||
pub fn last(ctx: &mut Context, msg: &Message, _: Args) -> CommandResult {
|
pub fn last(ctx: &mut Context, msg: &Message, _: Args) -> CommandResult {
|
||||||
let mut data = ctx.data.write();
|
let b = cache::get_beatmap(&*ctx.data.read(), msg.channel_id)?;
|
||||||
|
|
||||||
let b = cache::get_beatmap(&mut *data, msg.channel_id)?;
|
|
||||||
|
|
||||||
match b {
|
match b {
|
||||||
Some(BeatmapWithMode(b, m)) => {
|
Some(BeatmapWithMode(b, m)) => {
|
||||||
|
@ -273,9 +296,7 @@ pub fn last(ctx: &mut Context, msg: &Message, _: Args) -> CommandResult {
|
||||||
#[description = "Check your own or someone else's best record on the last beatmap."]
|
#[description = "Check your own or someone else's best record on the last beatmap."]
|
||||||
#[max_args(1)]
|
#[max_args(1)]
|
||||||
pub fn check(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
pub fn check(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
let mut data = ctx.data.write();
|
let bm = cache::get_beatmap(&*ctx.data.read(), msg.channel_id)?;
|
||||||
|
|
||||||
let bm = cache::get_beatmap(&mut *data, msg.channel_id)?;
|
|
||||||
|
|
||||||
match bm {
|
match bm {
|
||||||
None => {
|
None => {
|
||||||
|
@ -284,18 +305,18 @@ pub fn check(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
Some(bm) => {
|
Some(bm) => {
|
||||||
let b = &bm.0;
|
let b = &bm.0;
|
||||||
let m = bm.1;
|
let m = bm.1;
|
||||||
let user =
|
let user = UsernameArg::to_user_id_query(
|
||||||
UsernameArg::to_user_id_query(args.single::<UsernameArg>().ok(), &mut *data, msg)?;
|
args.single::<UsernameArg>().ok(),
|
||||||
|
&*ctx.data.read(),
|
||||||
|
msg,
|
||||||
|
)?;
|
||||||
|
|
||||||
let reqwest = data.get::<http::HTTP>().unwrap();
|
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||||
let osu = data.get::<http::Osu>().unwrap();
|
|
||||||
|
|
||||||
let user = osu
|
let user = osu
|
||||||
.user(reqwest, user, |f| f)?
|
.user(user, |f| f)?
|
||||||
.ok_or(Error::from("User not found"))?;
|
.ok_or(Error::from("User not found"))?;
|
||||||
let scores = osu.scores(reqwest, b.beatmap_id, |f| {
|
let scores = osu.scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(m))?;
|
||||||
f.user(UserID::ID(user.id)).mode(m)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if scores.is_empty() {
|
if scores.is_empty() {
|
||||||
msg.reply(&ctx, "No scores found")?;
|
msg.reply(&ctx, "No scores found")?;
|
||||||
|
@ -324,15 +345,14 @@ pub fn top(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
.map(|ModeArg(t)| t)
|
.map(|ModeArg(t)| t)
|
||||||
.unwrap_or(Mode::Std);
|
.unwrap_or(Mode::Std);
|
||||||
|
|
||||||
let mut data = ctx.data.write();
|
let user =
|
||||||
let user = UsernameArg::to_user_id_query(args.single::<UsernameArg>().ok(), &mut *data, msg)?;
|
UsernameArg::to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
||||||
|
|
||||||
let reqwest = data.get::<http::HTTP>().unwrap();
|
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||||
let osu: &OsuClient = data.get::<http::Osu>().unwrap();
|
|
||||||
let user = osu
|
let user = osu
|
||||||
.user(reqwest, user, |f| f.mode(mode))?
|
.user(user, |f| f.mode(mode))?
|
||||||
.ok_or(Error::from("User not found"))?;
|
.ok_or(Error::from("User not found"))?;
|
||||||
let top_play = osu.user_best(reqwest, UserID::ID(user.id), |f| f.mode(mode).limit(nth))?;
|
let top_play = osu.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(nth))?;
|
||||||
|
|
||||||
let rank = top_play.len() as u8;
|
let rank = top_play.len() as u8;
|
||||||
|
|
||||||
|
@ -341,11 +361,9 @@ pub fn top(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
.last()
|
.last()
|
||||||
.ok_or(Error::from("No such play"))?;
|
.ok_or(Error::from("No such play"))?;
|
||||||
let beatmap = osu
|
let beatmap = osu
|
||||||
.beatmaps(
|
.beatmaps(BeatmapRequestKind::Beatmap(top_play.beatmap_id), |f| {
|
||||||
reqwest,
|
f.mode(mode, true)
|
||||||
BeatmapRequestKind::Beatmap(top_play.beatmap_id),
|
})?
|
||||||
|f| f.mode(mode, true),
|
|
||||||
)?
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.next()
|
.next()
|
||||||
.map(|v| BeatmapWithMode(v, mode))
|
.map(|v| BeatmapWithMode(v, mode))
|
||||||
|
@ -360,25 +378,24 @@ pub fn top(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Save the beatmap...
|
// Save the beatmap...
|
||||||
cache::save_beatmap(&mut *data, msg.channel_id, &beatmap)?;
|
cache::save_beatmap(&*ctx.data.read(), msg.channel_id, &beatmap)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_user(ctx: &mut Context, msg: &Message, mut args: Args, mode: Mode) -> CommandResult {
|
fn get_user(ctx: &mut Context, msg: &Message, mut args: Args, mode: Mode) -> CommandResult {
|
||||||
let mut data = ctx.data.write();
|
let user =
|
||||||
let user = UsernameArg::to_user_id_query(args.single::<UsernameArg>().ok(), &mut *data, msg)?;
|
UsernameArg::to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
||||||
let reqwest = data.get::<http::HTTP>().unwrap();
|
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||||
let osu = data.get::<http::Osu>().unwrap();
|
let user = osu.user(user, |f| f.mode(mode))?;
|
||||||
let user = osu.user(reqwest, user, |f| f.mode(mode))?;
|
|
||||||
match user {
|
match user {
|
||||||
Some(u) => {
|
Some(u) => {
|
||||||
let best = osu
|
let best = osu
|
||||||
.user_best(reqwest, UserID::ID(u.id), |f| f.limit(1).mode(mode))?
|
.user_best(UserID::ID(u.id), |f| f.limit(1).mode(mode))?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.next()
|
.next()
|
||||||
.map(|m| {
|
.map(|m| {
|
||||||
osu.beatmaps(reqwest, BeatmapRequestKind::Beatmap(m.beatmap_id), |f| {
|
osu.beatmaps(BeatmapRequestKind::Beatmap(m.beatmap_id), |f| {
|
||||||
f.mode(mode, true)
|
f.mode(mode, true)
|
||||||
})
|
})
|
||||||
.map(|map| (m, BeatmapWithMode(map.into_iter().next().unwrap(), mode)))
|
.map(|map| (m, BeatmapWithMode(map.into_iter().next().unwrap(), mode)))
|
|
@ -1,5 +1,5 @@
|
||||||
|
pub mod discord;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
|
|
||||||
pub mod request;
|
pub mod request;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -10,11 +10,14 @@ use request::builders::*;
|
||||||
use request::*;
|
use request::*;
|
||||||
use reqwest::blocking::{Client as HTTPClient, RequestBuilder, Response};
|
use reqwest::blocking::{Client as HTTPClient, RequestBuilder, Response};
|
||||||
use serenity::framework::standard::CommandError as Error;
|
use serenity::framework::standard::CommandError as Error;
|
||||||
use std::convert::TryInto;
|
use std::{convert::TryInto, sync::Arc};
|
||||||
|
|
||||||
/// Client is the client that will perform calls to the osu! api server.
|
/// Client is the client that will perform calls to the osu! api server.
|
||||||
|
/// It's cheap to clone, so do it.
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
key: String,
|
key: Arc<String>,
|
||||||
|
client: HTTPClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn vec_try_into<U, T: std::convert::TryFrom<U>>(v: Vec<U>) -> Result<Vec<T>, T::Error> {
|
fn vec_try_into<U, T: std::convert::TryFrom<U>>(v: Vec<U>) -> Result<Vec<T>, T::Error> {
|
||||||
|
@ -29,52 +32,50 @@ fn vec_try_into<U, T: std::convert::TryFrom<U>>(v: Vec<U>) -> Result<Vec<T>, T::
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
/// Create a new client from the given API key.
|
/// Create a new client from the given API key.
|
||||||
pub fn new(key: impl AsRef<str>) -> Client {
|
pub fn new(http_client: HTTPClient, key: String) -> Client {
|
||||||
Client {
|
Client {
|
||||||
key: key.as_ref().to_string(),
|
key: Arc::new(key),
|
||||||
|
client: http_client,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_request(&self, c: &HTTPClient, r: RequestBuilder) -> Result<Response, Error> {
|
fn build_request(&self, r: RequestBuilder) -> Result<Response, Error> {
|
||||||
let v = r.query(&[("k", &self.key)]).build()?;
|
let v = r.query(&[("k", &*self.key)]).build()?;
|
||||||
dbg!(v.url());
|
dbg!(v.url());
|
||||||
Ok(c.execute(v)?)
|
Ok(self.client.execute(v)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn beatmaps(
|
pub fn beatmaps(
|
||||||
&self,
|
&self,
|
||||||
client: &HTTPClient,
|
|
||||||
kind: BeatmapRequestKind,
|
kind: BeatmapRequestKind,
|
||||||
f: impl FnOnce(&mut BeatmapRequestBuilder) -> &mut BeatmapRequestBuilder,
|
f: impl FnOnce(&mut BeatmapRequestBuilder) -> &mut BeatmapRequestBuilder,
|
||||||
) -> Result<Vec<Beatmap>, Error> {
|
) -> Result<Vec<Beatmap>, Error> {
|
||||||
let mut r = BeatmapRequestBuilder::new(kind);
|
let mut r = BeatmapRequestBuilder::new(kind);
|
||||||
f(&mut r);
|
f(&mut r);
|
||||||
let res: Vec<raw::Beatmap> = self.build_request(client, r.build(client))?.json()?;
|
let res: Vec<raw::Beatmap> = self.build_request(r.build(&self.client))?.json()?;
|
||||||
Ok(vec_try_into(res)?)
|
Ok(vec_try_into(res)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn user(
|
pub fn user(
|
||||||
&self,
|
&self,
|
||||||
client: &HTTPClient,
|
|
||||||
user: UserID,
|
user: UserID,
|
||||||
f: impl FnOnce(&mut UserRequestBuilder) -> &mut UserRequestBuilder,
|
f: impl FnOnce(&mut UserRequestBuilder) -> &mut UserRequestBuilder,
|
||||||
) -> Result<Option<User>, Error> {
|
) -> Result<Option<User>, Error> {
|
||||||
let mut r = UserRequestBuilder::new(user);
|
let mut r = UserRequestBuilder::new(user);
|
||||||
f(&mut r);
|
f(&mut r);
|
||||||
let res: Vec<raw::User> = self.build_request(client, r.build(client))?.json()?;
|
let res: Vec<raw::User> = self.build_request(r.build(&self.client))?.json()?;
|
||||||
let res = vec_try_into(res)?;
|
let res = vec_try_into(res)?;
|
||||||
Ok(res.into_iter().next())
|
Ok(res.into_iter().next())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scores(
|
pub fn scores(
|
||||||
&self,
|
&self,
|
||||||
client: &HTTPClient,
|
|
||||||
beatmap_id: u64,
|
beatmap_id: u64,
|
||||||
f: impl FnOnce(&mut ScoreRequestBuilder) -> &mut ScoreRequestBuilder,
|
f: impl FnOnce(&mut ScoreRequestBuilder) -> &mut ScoreRequestBuilder,
|
||||||
) -> Result<Vec<Score>, Error> {
|
) -> Result<Vec<Score>, Error> {
|
||||||
let mut r = ScoreRequestBuilder::new(beatmap_id);
|
let mut r = ScoreRequestBuilder::new(beatmap_id);
|
||||||
f(&mut r);
|
f(&mut r);
|
||||||
let res: Vec<raw::Score> = self.build_request(client, r.build(client))?.json()?;
|
let res: Vec<raw::Score> = self.build_request(r.build(&self.client))?.json()?;
|
||||||
let mut res: Vec<Score> = vec_try_into(res)?;
|
let mut res: Vec<Score> = vec_try_into(res)?;
|
||||||
|
|
||||||
// with a scores request you need to fill the beatmap ids yourself
|
// with a scores request you need to fill the beatmap ids yourself
|
||||||
|
@ -86,32 +87,29 @@ impl Client {
|
||||||
|
|
||||||
pub fn user_best(
|
pub fn user_best(
|
||||||
&self,
|
&self,
|
||||||
client: &HTTPClient,
|
|
||||||
user: UserID,
|
user: UserID,
|
||||||
f: impl FnOnce(&mut UserScoreRequestBuilder) -> &mut UserScoreRequestBuilder,
|
f: impl FnOnce(&mut UserScoreRequestBuilder) -> &mut UserScoreRequestBuilder,
|
||||||
) -> Result<Vec<Score>, Error> {
|
) -> Result<Vec<Score>, Error> {
|
||||||
self.user_scores(UserScoreType::Best, client, user, f)
|
self.user_scores(UserScoreType::Best, user, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn user_recent(
|
pub fn user_recent(
|
||||||
&self,
|
&self,
|
||||||
client: &HTTPClient,
|
|
||||||
user: UserID,
|
user: UserID,
|
||||||
f: impl FnOnce(&mut UserScoreRequestBuilder) -> &mut UserScoreRequestBuilder,
|
f: impl FnOnce(&mut UserScoreRequestBuilder) -> &mut UserScoreRequestBuilder,
|
||||||
) -> Result<Vec<Score>, Error> {
|
) -> Result<Vec<Score>, Error> {
|
||||||
self.user_scores(UserScoreType::Recent, client, user, f)
|
self.user_scores(UserScoreType::Recent, user, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn user_scores(
|
fn user_scores(
|
||||||
&self,
|
&self,
|
||||||
u: UserScoreType,
|
u: UserScoreType,
|
||||||
client: &HTTPClient,
|
|
||||||
user: UserID,
|
user: UserID,
|
||||||
f: impl FnOnce(&mut UserScoreRequestBuilder) -> &mut UserScoreRequestBuilder,
|
f: impl FnOnce(&mut UserScoreRequestBuilder) -> &mut UserScoreRequestBuilder,
|
||||||
) -> Result<Vec<Score>, Error> {
|
) -> Result<Vec<Score>, Error> {
|
||||||
let mut r = UserScoreRequestBuilder::new(u, user);
|
let mut r = UserScoreRequestBuilder::new(u, user);
|
||||||
f(&mut r);
|
f(&mut r);
|
||||||
let res: Vec<raw::Score> = self.build_request(client, r.build(client))?.json()?;
|
let res: Vec<raw::Score> = self.build_request(r.build(&self.client))?.json()?;
|
||||||
let res = vec_try_into(res)?;
|
let res = vec_try_into(res)?;
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
12
youmubot-prelude/Cargo.toml
Normal file
12
youmubot-prelude/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "youmubot-prelude"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Natsu Kagami <natsukagami@gmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serenity = "0.8"
|
||||||
|
youmubot-db = { path = "../youmubot-db" }
|
||||||
|
reqwest = "0.10"
|
|
@ -1,49 +1,45 @@
|
||||||
use crate::db::{AnnouncerChannels, DBWriteGuard};
|
use crate::AppData;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
framework::standard::{CommandError as Error, CommandResult},
|
framework::standard::{CommandError as Error, CommandResult},
|
||||||
http::{CacheHttp, Http},
|
http::{CacheHttp, Http},
|
||||||
model::id::{ChannelId, GuildId, UserId},
|
model::id::{ChannelId, GuildId, UserId},
|
||||||
prelude::ShareMap,
|
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::{HashMap, HashSet},
|
||||||
thread::{spawn, JoinHandle},
|
thread::{spawn, JoinHandle},
|
||||||
};
|
};
|
||||||
|
use youmubot_db::DB;
|
||||||
|
|
||||||
|
pub(crate) type AnnouncerChannels = DB<HashMap<String, HashMap<GuildId, ChannelId>>>;
|
||||||
|
|
||||||
pub trait Announcer {
|
pub trait Announcer {
|
||||||
fn announcer_key() -> &'static str;
|
fn announcer_key() -> &'static str;
|
||||||
fn send_messages(
|
fn send_messages(
|
||||||
c: &Http,
|
c: &Http,
|
||||||
d: &mut ShareMap,
|
d: AppData,
|
||||||
channels: impl Fn(UserId) -> Vec<ChannelId> + Sync,
|
channels: impl Fn(UserId) -> Vec<ChannelId> + Sync,
|
||||||
) -> CommandResult;
|
) -> CommandResult;
|
||||||
|
|
||||||
fn set_channel(d: &mut ShareMap, guild: GuildId, channel: ChannelId) -> CommandResult {
|
fn set_channel(d: AppData, guild: GuildId, channel: ChannelId) -> CommandResult {
|
||||||
let mut data: DBWriteGuard<_> = d
|
AnnouncerChannels::open(&*d.read())
|
||||||
.get_mut::<AnnouncerChannels>()
|
.borrow_mut()?
|
||||||
.expect("DB initialized")
|
.entry(Self::announcer_key().to_owned())
|
||||||
.into();
|
|
||||||
let mut data = data.borrow_mut()?;
|
|
||||||
data.entry(Self::announcer_key().to_owned())
|
|
||||||
.or_default()
|
.or_default()
|
||||||
.insert(guild, channel);
|
.insert(guild, channel);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_guilds(d: &mut ShareMap) -> Result<Vec<(GuildId, ChannelId)>, Error> {
|
fn get_guilds(d: AppData) -> Result<Vec<(GuildId, ChannelId)>, Error> {
|
||||||
let data = d
|
let data = AnnouncerChannels::open(&*d.read())
|
||||||
.get::<AnnouncerChannels>()
|
.borrow()?
|
||||||
.expect("DB initialized")
|
.get(Self::announcer_key())
|
||||||
.read(|v| {
|
|
||||||
v.get(Self::announcer_key())
|
|
||||||
.map(|m| m.iter().map(|(a, b)| (*a, *b)).collect())
|
.map(|m| m.iter().map(|(a, b)| (*a, *b)).collect())
|
||||||
.unwrap_or_else(|| vec![])
|
.unwrap_or_else(|| vec![]);
|
||||||
})?;
|
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn announce(c: &Http, d: &mut ShareMap) -> CommandResult {
|
fn announce(c: impl AsRef<Http>, d: AppData) -> CommandResult {
|
||||||
let guilds: Vec<_> = Self::get_guilds(d)?;
|
let guilds: Vec<_> = Self::get_guilds(d.clone())?;
|
||||||
let member_sets = {
|
let member_sets = {
|
||||||
let mut v = Vec::with_capacity(guilds.len());
|
let mut v = Vec::with_capacity(guilds.len());
|
||||||
for (guild, channel) in guilds.into_iter() {
|
for (guild, channel) in guilds.into_iter() {
|
||||||
|
@ -75,7 +71,7 @@ pub trait Announcer {
|
||||||
let c = client.cache_and_http.clone();
|
let c = client.cache_and_http.clone();
|
||||||
let data = client.data.clone();
|
let data = client.data.clone();
|
||||||
spawn(move || loop {
|
spawn(move || loop {
|
||||||
if let Err(e) = Self::announce(c.http(), &mut *data.write()) {
|
if let Err(e) = Self::announce(c.http(), data.clone()) {
|
||||||
dbg!(e);
|
dbg!(e);
|
||||||
}
|
}
|
||||||
std::thread::sleep(cooldown);
|
std::thread::sleep(cooldown);
|
48
youmubot-prelude/src/lib.rs
Normal file
48
youmubot-prelude/src/lib.rs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
pub use serenity::prelude::*;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub mod announcer;
|
||||||
|
pub mod args;
|
||||||
|
pub mod setup;
|
||||||
|
|
||||||
|
pub use announcer::Announcer;
|
||||||
|
pub use args::Duration;
|
||||||
|
|
||||||
|
/// The global app data.
|
||||||
|
pub type AppData = Arc<RwLock<ShareMap>>;
|
||||||
|
|
||||||
|
/// The HTTP client.
|
||||||
|
pub struct HTTPClient;
|
||||||
|
|
||||||
|
impl TypeMapKey for HTTPClient {
|
||||||
|
type Value = reqwest::blocking::Client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The TypeMap trait that allows TypeMaps to quickly get a clonable item.
|
||||||
|
pub trait GetCloned {
|
||||||
|
/// Gets an item from the store, cloned.
|
||||||
|
fn get_cloned<T>(&self) -> T::Value
|
||||||
|
where
|
||||||
|
T: TypeMapKey,
|
||||||
|
T::Value: Clone + Send + Sync;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetCloned for ShareMap {
|
||||||
|
fn get_cloned<T>(&self) -> T::Value
|
||||||
|
where
|
||||||
|
T: TypeMapKey,
|
||||||
|
T::Value: Clone + Send + Sync,
|
||||||
|
{
|
||||||
|
self.get::<T>().cloned().expect("Should be there")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetCloned for AppData {
|
||||||
|
fn get_cloned<T>(&self) -> T::Value
|
||||||
|
where
|
||||||
|
T: TypeMapKey,
|
||||||
|
T::Value: Clone + Send + Sync,
|
||||||
|
{
|
||||||
|
self.read().get::<T>().cloned().expect("Should be there")
|
||||||
|
}
|
||||||
|
}
|
13
youmubot-prelude/src/setup.rs
Normal file
13
youmubot-prelude/src/setup.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
use serenity::{framework::standard::StandardFramework, prelude::*};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Set up the prelude libraries.
|
||||||
|
///
|
||||||
|
/// Panics on failure: Youmubot should *NOT* attempt to continue when this function fails.
|
||||||
|
pub fn setup_prelude(db_path: &Path, data: &mut ShareMap, _: &mut StandardFramework) {
|
||||||
|
// Setup the announcer DB.
|
||||||
|
crate::announcer::AnnouncerChannels::insert_into(data, db_path.join("announcers.yaml"))
|
||||||
|
.expect("Announcers DB set up");
|
||||||
|
|
||||||
|
data.insert::<crate::HTTPClient>(reqwest::blocking::Client::new());
|
||||||
|
}
|
|
@ -9,16 +9,8 @@ edition = "2018"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serenity = "0.8"
|
serenity = "0.8"
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
chrono = "0.4.9"
|
|
||||||
rand = "0.7.2"
|
|
||||||
static_assertions = "1.1.0"
|
|
||||||
reqwest = "0.10.1"
|
|
||||||
regex = "1"
|
|
||||||
lazy_static = "1"
|
|
||||||
youmubot-osu = { path = "../youmubot-osu" }
|
youmubot-osu = { path = "../youmubot-osu" }
|
||||||
rayon = "1.1"
|
youmubot-db = { path = "../youmubot-db" }
|
||||||
|
youmubot-prelude = { path = "../youmubot-prelude" }
|
||||||
|
youmubot-core = { path = "../youmubot-core" }
|
||||||
|
|
||||||
[dependencies.rustbreak]
|
|
||||||
version = "2.0.0-rc3"
|
|
||||||
features = ["yaml_enc"]
|
|
||||||
|
|
|
@ -1,134 +0,0 @@
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use dotenv::var;
|
|
||||||
use rustbreak::{deser::Yaml as Ron, FileDatabase};
|
|
||||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
|
||||||
use serenity::{
|
|
||||||
client::Client,
|
|
||||||
framework::standard::CommandError as Error,
|
|
||||||
model::id::{ChannelId, GuildId, RoleId, UserId},
|
|
||||||
prelude::*,
|
|
||||||
};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use youmubot_osu::models::{Beatmap, Mode};
|
|
||||||
|
|
||||||
/// GuildMap defines the guild-map type.
|
|
||||||
/// It is basically a HashMap from a GuildId to a data structure.
|
|
||||||
pub type GuildMap<V> = HashMap<GuildId, V>;
|
|
||||||
/// The generic DB type we will be using.
|
|
||||||
pub struct DB<T>(std::marker::PhantomData<T>);
|
|
||||||
impl<T: std::any::Any> serenity::prelude::TypeMapKey for DB<T> {
|
|
||||||
type Value = FileDatabase<T, Ron>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: std::any::Any + Default + Send + Sync + Clone + Serialize + std::fmt::Debug> DB<T>
|
|
||||||
where
|
|
||||||
for<'de> T: Deserialize<'de>,
|
|
||||||
{
|
|
||||||
fn insert_into(data: &mut ShareMap, path: impl AsRef<Path>) -> Result<(), Error> {
|
|
||||||
let db = FileDatabase::<T, Ron>::from_path(path, T::default())?;
|
|
||||||
db.load().or_else(|e| {
|
|
||||||
dbg!(e);
|
|
||||||
db.save()
|
|
||||||
})?;
|
|
||||||
data.insert::<DB<T>>(db);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A map from announcer keys to guild IDs and to channels.
|
|
||||||
pub type AnnouncerChannels = DB<HashMap<String, GuildMap<ChannelId>>>;
|
|
||||||
|
|
||||||
/// A list of SoftBans for all servers.
|
|
||||||
pub type SoftBans = DB<GuildMap<ServerSoftBans>>;
|
|
||||||
|
|
||||||
/// Save the user IDs.
|
|
||||||
pub type OsuSavedUsers = DB<HashMap<UserId, OsuUser>>;
|
|
||||||
|
|
||||||
/// Save each channel's last requested beatmap.
|
|
||||||
pub type OsuLastBeatmap = DB<HashMap<ChannelId, (Beatmap, Mode)>>;
|
|
||||||
|
|
||||||
/// Sets up all databases in the client.
|
|
||||||
pub fn setup_db(client: &mut Client) -> Result<(), Error> {
|
|
||||||
let path: PathBuf = var("DBPATH").map(|v| PathBuf::from(v)).unwrap_or_else(|e| {
|
|
||||||
println!("No DBPATH set up ({:?}), using `/data`", e);
|
|
||||||
PathBuf::from("data")
|
|
||||||
});
|
|
||||||
let mut data = client.data.write();
|
|
||||||
SoftBans::insert_into(&mut *data, &path.join("soft_bans.yaml"))?;
|
|
||||||
OsuSavedUsers::insert_into(&mut *data, &path.join("osu_saved_users.yaml"))?;
|
|
||||||
OsuLastBeatmap::insert_into(&mut *data, &path.join("last_beatmaps.yaml"))?;
|
|
||||||
AnnouncerChannels::insert_into(&mut *data, &path.join("announcers.yaml"))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DBWriteGuard<'a, T>(&'a mut FileDatabase<T, Ron>)
|
|
||||||
where
|
|
||||||
T: Send + Sync + Clone + std::fmt::Debug + Serialize + DeserializeOwned;
|
|
||||||
|
|
||||||
impl<'a, T> From<&'a mut FileDatabase<T, Ron>> for DBWriteGuard<'a, T>
|
|
||||||
where
|
|
||||||
T: Send + Sync + Clone + std::fmt::Debug + Serialize + DeserializeOwned,
|
|
||||||
{
|
|
||||||
fn from(v: &'a mut FileDatabase<T, Ron>) -> Self {
|
|
||||||
DBWriteGuard(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T> DBWriteGuard<'a, T>
|
|
||||||
where
|
|
||||||
T: Send + Sync + Clone + std::fmt::Debug + Serialize + DeserializeOwned,
|
|
||||||
{
|
|
||||||
pub fn borrow(&self) -> Result<std::sync::RwLockReadGuard<T>, rustbreak::RustbreakError> {
|
|
||||||
(*self).0.borrow_data()
|
|
||||||
}
|
|
||||||
pub fn borrow_mut(
|
|
||||||
&mut self,
|
|
||||||
) -> Result<std::sync::RwLockWriteGuard<T>, rustbreak::RustbreakError> {
|
|
||||||
(*self).0.borrow_data_mut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, T> Drop for DBWriteGuard<'a, T>
|
|
||||||
where
|
|
||||||
T: Send + Sync + Clone + std::fmt::Debug + Serialize + DeserializeOwned,
|
|
||||||
{
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.0.save().expect("Save succeed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// For the admin commands:
|
|
||||||
/// - Each server might have a `soft ban` role implemented.
|
|
||||||
/// - We allow periodical `soft ban` applications.
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
pub enum ServerSoftBans {
|
|
||||||
Implemented(ImplementedSoftBans),
|
|
||||||
Unimplemented,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServerSoftBans {
|
|
||||||
// Create a new, implemented role.
|
|
||||||
pub fn new_implemented(role: RoleId) -> ServerSoftBans {
|
|
||||||
ServerSoftBans::Implemented(ImplementedSoftBans {
|
|
||||||
role: role,
|
|
||||||
periodical_bans: HashMap::new(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
pub struct ImplementedSoftBans {
|
|
||||||
/// The soft-ban role.
|
|
||||||
pub role: RoleId,
|
|
||||||
/// List of all to-unban people.
|
|
||||||
pub periodical_bans: HashMap<UserId, DateTime<Utc>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An osu! saved user.
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
pub struct OsuUser {
|
|
||||||
pub id: u64,
|
|
||||||
pub last_update: DateTime<Utc>,
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
use serenity::prelude::TypeMapKey;
|
|
||||||
use youmubot_osu::Client as OsuClient;
|
|
||||||
|
|
||||||
pub(crate) struct HTTP;
|
|
||||||
|
|
||||||
impl TypeMapKey for HTTP {
|
|
||||||
type Value = reqwest::blocking::Client;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct Osu;
|
|
||||||
|
|
||||||
impl TypeMapKey for Osu {
|
|
||||||
type Value = OsuClient;
|
|
||||||
}
|
|
|
@ -1,21 +1,13 @@
|
||||||
use dotenv;
|
use dotenv;
|
||||||
use dotenv::var;
|
use dotenv::var;
|
||||||
use reqwest;
|
|
||||||
use serenity::{
|
use serenity::{
|
||||||
framework::standard::{DispatchError, StandardFramework},
|
framework::standard::{DispatchError, StandardFramework},
|
||||||
model::{channel::Message, gateway},
|
model::{channel::Message, gateway},
|
||||||
prelude::*,
|
|
||||||
};
|
};
|
||||||
use youmubot_osu::Client as OsuClient;
|
use youmubot_osu::discord::{setup as setup_osu, OSU_GROUP};
|
||||||
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
mod commands;
|
const MESSAGE_HOOKS: [fn(&mut Context, &Message) -> (); 1] = [youmubot_osu::discord::hook];
|
||||||
mod db;
|
|
||||||
mod http;
|
|
||||||
|
|
||||||
use commands::osu::OsuAnnouncer;
|
|
||||||
use commands::Announcer;
|
|
||||||
|
|
||||||
const MESSAGE_HOOKS: [fn(&mut Context, &Message) -> (); 1] = [commands::osu::hook];
|
|
||||||
|
|
||||||
struct Handler;
|
struct Handler;
|
||||||
|
|
||||||
|
@ -40,26 +32,28 @@ fn main() {
|
||||||
// Collect the token
|
// Collect the token
|
||||||
let token = var("TOKEN").expect("Please set TOKEN as the Discord Bot's token to be used.");
|
let token = var("TOKEN").expect("Please set TOKEN as the Discord Bot's token to be used.");
|
||||||
// Attempt to connect and set up a framework
|
// Attempt to connect and set up a framework
|
||||||
setup_framework(Client::new(token, Handler).expect("Cannot connect..."))
|
Client::new(token, Handler).expect("Cannot connect")
|
||||||
};
|
};
|
||||||
|
|
||||||
// Setup initial data
|
// Set up base framework
|
||||||
db::setup_db(&mut client).expect("Setup db should succeed");
|
let mut fw = setup_framework(&client);
|
||||||
// Setup shared instances of things
|
|
||||||
|
// Setup each package starting from the prelude.
|
||||||
{
|
{
|
||||||
let mut data = client.data.write();
|
let mut data = client.data.write();
|
||||||
data.insert::<http::HTTP>(reqwest::blocking::Client::new());
|
let db_path = var("DBPATH")
|
||||||
data.insert::<http::Osu>(OsuClient::new(
|
.map(|v| std::path::PathBuf::from(v))
|
||||||
var("OSU_API_KEY").expect("Please set OSU_API_KEY as osu! api key."),
|
.unwrap_or_else(|e| {
|
||||||
));
|
println!("No DBPATH set up ({:?}), using `/data`", e);
|
||||||
|
std::path::PathBuf::from("data")
|
||||||
|
});
|
||||||
|
youmubot_prelude::setup::setup_prelude(&db_path, &mut data, &mut fw);
|
||||||
|
// Setup core
|
||||||
|
youmubot_core::setup(&db_path, &client, &mut data).expect("Setup db should succeed");
|
||||||
|
// osu!
|
||||||
|
setup_osu(&db_path, &client, &mut data).expect("osu! is initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create handler threads
|
|
||||||
std::thread::spawn(commands::admin::watch_soft_bans(&mut client));
|
|
||||||
|
|
||||||
// Announcers
|
|
||||||
OsuAnnouncer::scan(&client, std::time::Duration::from_secs(300));
|
|
||||||
|
|
||||||
println!("Starting...");
|
println!("Starting...");
|
||||||
if let Err(v) = client.start() {
|
if let Err(v) = client.start() {
|
||||||
panic!(v)
|
panic!(v)
|
||||||
|
@ -69,7 +63,7 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets up a framework for a client
|
// Sets up a framework for a client
|
||||||
fn setup_framework(mut client: Client) -> Client {
|
fn setup_framework(client: &Client) -> StandardFramework {
|
||||||
// Collect owners
|
// Collect owners
|
||||||
let owner = client
|
let owner = client
|
||||||
.cache_and_http
|
.cache_and_http
|
||||||
|
@ -78,7 +72,6 @@ fn setup_framework(mut client: Client) -> Client {
|
||||||
.expect("Should be able to get app info")
|
.expect("Should be able to get app info")
|
||||||
.owner;
|
.owner;
|
||||||
|
|
||||||
client.with_framework(
|
|
||||||
StandardFramework::new()
|
StandardFramework::new()
|
||||||
.configure(|c| {
|
.configure(|c| {
|
||||||
c.with_whitespace(false)
|
c.with_whitespace(false)
|
||||||
|
@ -86,7 +79,7 @@ fn setup_framework(mut client: Client) -> Client {
|
||||||
.delimiters(vec![" / ", "/ ", " /", "/"])
|
.delimiters(vec![" / ", "/ ", " /", "/"])
|
||||||
.owners([owner.id].iter().cloned().collect())
|
.owners([owner.id].iter().cloned().collect())
|
||||||
})
|
})
|
||||||
.help(&commands::HELP)
|
.help(&youmubot_core::HELP)
|
||||||
.before(|_, msg, command_name| {
|
.before(|_, msg, command_name| {
|
||||||
println!(
|
println!(
|
||||||
"Got command '{}' by user '{}'",
|
"Got command '{}' by user '{}'",
|
||||||
|
@ -135,10 +128,8 @@ fn setup_framework(mut client: Client) -> Client {
|
||||||
c.delay(30).time_span(30).limit(1)
|
c.delay(30).time_span(30).limit(1)
|
||||||
})
|
})
|
||||||
// groups here
|
// groups here
|
||||||
.group(&commands::ADMIN_GROUP)
|
.group(&youmubot_core::ADMIN_GROUP)
|
||||||
.group(&commands::FUN_GROUP)
|
.group(&youmubot_core::FUN_GROUP)
|
||||||
.group(&commands::COMMUNITY_GROUP)
|
.group(&youmubot_core::COMMUNITY_GROUP)
|
||||||
.group(&commands::OSU_GROUP)
|
.group(&OSU_GROUP)
|
||||||
);
|
|
||||||
client
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue