mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-16 07:18:54 +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"
|
||||
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)",
|
||||
"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)",
|
||||
"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)",
|
||||
"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)",
|
||||
"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]]
|
||||
|
@ -1697,10 +1716,24 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"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)",
|
||||
"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)",
|
||||
"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)",
|
||||
"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]
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
[workspace]
|
||||
|
||||
members = [
|
||||
"youmubot-prelude",
|
||||
"youmubot-db",
|
||||
"youmubot-core",
|
||||
"youmubot-osu",
|
||||
"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::{
|
||||
framework::standard::{
|
||||
macros::{command, group},
|
||||
|
@ -11,6 +10,7 @@ use serenity::{
|
|||
};
|
||||
use soft_ban::{SOFT_BAN_COMMAND, SOFT_BAN_INIT_COMMAND};
|
||||
use std::{thread::sleep, time::Duration};
|
||||
use youmubot_prelude::*;
|
||||
|
||||
mod soft_ban;
|
||||
pub use soft_ban::watch_soft_bans;
|
|
@ -1,9 +1,5 @@
|
|||
use crate::{
|
||||
commands::args,
|
||||
db::{DBWriteGuard, ServerSoftBans, SoftBans},
|
||||
};
|
||||
use crate::db::{ServerSoftBans, SoftBans};
|
||||
use chrono::offset::Utc;
|
||||
use serenity::prelude::*;
|
||||
use serenity::{
|
||||
framework::standard::{macros::command, Args, CommandError as Error, CommandResult},
|
||||
model::{
|
||||
|
@ -12,6 +8,7 @@ use serenity::{
|
|||
},
|
||||
};
|
||||
use std::cmp::max;
|
||||
use youmubot_prelude::*;
|
||||
|
||||
#[command]
|
||||
#[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 mut data = ctx.data.write();
|
||||
let mut data = data
|
||||
.get_mut::<SoftBans>()
|
||||
.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 {
|
||||
let db = SoftBans::open(&*ctx.data.read());
|
||||
let mut db = db.borrow_mut()?;
|
||||
let mut server_ban = db.get_mut(&guild).and_then(|v| match v {
|
||||
ServerSoftBans::Unimplemented => None,
|
||||
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
|
||||
let mut data = ctx.data.write();
|
||||
let mut db: DBWriteGuard<_> = data
|
||||
.get_mut::<SoftBans>()
|
||||
.ok_or(Error::from("DB uninitialized"))?
|
||||
.into();
|
||||
let db = SoftBans::open(&*ctx.data.read());
|
||||
let mut db = db.borrow_mut()?;
|
||||
let server = db
|
||||
.get_mut(&guild.id)
|
||||
.get(&guild.id)
|
||||
.map(|v| match v {
|
||||
ServerSoftBans::Unimplemented => false,
|
||||
_ => true,
|
||||
|
@ -122,7 +111,7 @@ pub fn soft_ban_init(ctx: &mut Context, msg: &Message, mut args: Args) -> Comman
|
|||
}
|
||||
|
||||
// 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 = client.cache_and_http.clone();
|
||||
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
|
||||
{
|
||||
// Poll the data for any changes.
|
||||
let mut data = data.write();
|
||||
let mut db: DBWriteGuard<_> = data
|
||||
.get_mut::<SoftBans>()
|
||||
.expect("DB wrongly initialized")
|
||||
.into();
|
||||
let mut db = db.borrow_mut().expect("cannot unpack DB");
|
||||
let db = data.read();
|
||||
let db = SoftBans::open(&*db);
|
||||
let mut db = db.borrow_mut().expect("Borrowable");
|
||||
let now = Utc::now();
|
||||
for (server_id, soft_bans) in db.iter_mut() {
|
||||
let server_name: String = match server_id.to_partial_guild(cache_http) {
|
|
@ -2,7 +2,6 @@ use rand::{
|
|||
distributions::{Distribution, Uniform},
|
||||
thread_rng,
|
||||
};
|
||||
use serenity::prelude::*;
|
||||
use serenity::{
|
||||
framework::standard::{
|
||||
macros::{command, group},
|
||||
|
@ -14,6 +13,7 @@ use serenity::{
|
|||
},
|
||||
utils::MessageBuilder,
|
||||
};
|
||||
use youmubot_prelude::*;
|
||||
|
||||
mod votes;
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
use crate::commands::args::Duration as ParseDuration;
|
||||
use serenity::framework::standard::CommandError as Error;
|
||||
use serenity::prelude::*;
|
||||
use serenity::{
|
||||
framework::standard::{macros::command, Args, CommandResult},
|
||||
model::{
|
||||
|
@ -12,6 +10,7 @@ use serenity::{
|
|||
use std::collections::HashMap as Map;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use youmubot_prelude::{Duration as ParseDuration, *};
|
||||
|
||||
#[command]
|
||||
#[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 serenity::framework::standard::CommandError as Error;
|
||||
use serenity::prelude::*;
|
||||
use serenity::{
|
||||
framework::standard::{
|
||||
macros::{check, command},
|
||||
|
@ -11,6 +8,7 @@ use serenity::{
|
|||
model::channel::{Channel, Message},
|
||||
};
|
||||
use std::string::ToString;
|
||||
use youmubot_prelude::*;
|
||||
|
||||
#[command]
|
||||
#[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 {
|
||||
let tags = args.remains().unwrap_or("touhou");
|
||||
let http = ctx.data.read();
|
||||
let http = http.get::<HTTP>().unwrap();
|
||||
let image = get_image(http, rating, tags)?;
|
||||
let http = ctx.data.get_cloned::<HTTPClient>();
|
||||
let image = get_image(&http, rating, tags)?;
|
||||
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."),
|
||||
Some(url) => msg.reply(
|
||||
|
@ -59,7 +56,11 @@ fn message_command(ctx: &mut Context, msg: &Message, args: Args, rating: Rating)
|
|||
}
|
||||
|
||||
// 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 +
|
||||
let tags = tags.split_whitespace().collect::<Vec<_>>().join("_");
|
||||
let req = client
|
|
@ -2,7 +2,6 @@ use rand::{
|
|||
distributions::{Distribution, Uniform},
|
||||
thread_rng,
|
||||
};
|
||||
use serenity::prelude::*;
|
||||
use serenity::{
|
||||
framework::standard::{
|
||||
macros::{command, group},
|
||||
|
@ -11,6 +10,7 @@ use serenity::{
|
|||
model::{channel::Message, id::UserId},
|
||||
utils::MessageBuilder,
|
||||
};
|
||||
use youmubot_prelude::*;
|
||||
|
||||
mod images;
|
||||
mod names;
|
|
@ -1,4 +1,3 @@
|
|||
use serenity::prelude::*;
|
||||
use serenity::{
|
||||
framework::standard::{
|
||||
help_commands, macros::help, Args, CommandGroup, CommandResult, HelpOptions,
|
||||
|
@ -6,21 +5,30 @@ use serenity::{
|
|||
model::{channel::Message, id::UserId},
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
|
||||
mod announcer;
|
||||
mod args;
|
||||
use youmubot_prelude::*;
|
||||
|
||||
pub mod admin;
|
||||
pub mod community;
|
||||
mod db;
|
||||
pub mod fun;
|
||||
pub mod osu;
|
||||
|
||||
pub use admin::ADMIN_GROUP;
|
||||
pub use community::COMMUNITY_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
|
||||
#[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"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
bitflags = "1"
|
||||
rayon = "1.1"
|
||||
lazy_static = "1"
|
||||
regex = "1"
|
||||
|
||||
youmubot-db = { path = "../youmubot-db" }
|
||||
youmubot-prelude = { path = "../youmubot-prelude" }
|
||||
|
||||
[dev-dependencies]
|
||||
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::{
|
||||
commands::announcer::Announcer,
|
||||
db::{OsuSavedUsers, OsuUser},
|
||||
http::{Osu, HTTP},
|
||||
models::{Mode, Score},
|
||||
request::{BeatmapRequestKind, UserID},
|
||||
Client as Osu,
|
||||
};
|
||||
use rayon::prelude::*;
|
||||
use reqwest::blocking::Client as HTTPClient;
|
||||
use serenity::{
|
||||
framework::standard::{CommandError as Error, CommandResult},
|
||||
http::Http,
|
||||
model::{
|
||||
id::{ChannelId, UserId},
|
||||
misc::Mentionable,
|
||||
},
|
||||
prelude::ShareMap,
|
||||
};
|
||||
use youmubot_osu::{
|
||||
models::{Mode, Score},
|
||||
request::{BeatmapRequestKind, UserID},
|
||||
Client as OsuClient,
|
||||
model::id::{ChannelId, UserId},
|
||||
};
|
||||
use youmubot_prelude::*;
|
||||
|
||||
/// Announce osu! top scores.
|
||||
pub struct OsuAnnouncer;
|
||||
|
@ -30,33 +22,36 @@ impl Announcer for OsuAnnouncer {
|
|||
}
|
||||
fn send_messages(
|
||||
c: &Http,
|
||||
d: &mut ShareMap,
|
||||
d: AppData,
|
||||
channels: impl Fn(UserId) -> Vec<ChannelId> + Sync,
|
||||
) -> CommandResult {
|
||||
let http = d.get::<HTTP>().expect("HTTP");
|
||||
let osu = d.get::<Osu>().expect("osu!client");
|
||||
let osu = d.get_cloned::<OsuClient>();
|
||||
// For each user...
|
||||
let mut data = d
|
||||
.get::<OsuSavedUsers>()
|
||||
.expect("DB initialized")
|
||||
.read(|f| f.clone())?;
|
||||
let mut data = OsuSavedUsers::open(&*d.read()).borrow()?.clone();
|
||||
for (user_id, osu_user) in data.iter_mut() {
|
||||
let mut user = None;
|
||||
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() {
|
||||
continue;
|
||||
}
|
||||
let user = user.get_or_insert_with(|| {
|
||||
osu.user(http, UserID::ID(osu_user.id), |f| f)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
});
|
||||
let user = {
|
||||
let user = &mut user;
|
||||
if let None = user {
|
||||
match osu.user(UserID::ID(osu_user.id), |f| f.mode(*mode)) {
|
||||
Ok(u) => {
|
||||
*user = u;
|
||||
}
|
||||
Err(_) => continue,
|
||||
}
|
||||
};
|
||||
user.as_ref().unwrap()
|
||||
};
|
||||
scores
|
||||
.into_par_iter()
|
||||
.filter_map(|(rank, score)| {
|
||||
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));
|
||||
let channels = channels(*user_id);
|
||||
match beatmap {
|
||||
|
@ -81,21 +76,14 @@ impl Announcer for OsuAnnouncer {
|
|||
osu_user.last_update = chrono::Utc::now();
|
||||
}
|
||||
// Update users
|
||||
let f = d.get_mut::<OsuSavedUsers>().expect("DB initialized");
|
||||
f.write(|f| *f = data)?;
|
||||
f.save()?;
|
||||
*OsuSavedUsers::open(&*d.read()).borrow_mut()? = data;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl OsuAnnouncer {
|
||||
fn scan_user(
|
||||
http: &HTTPClient,
|
||||
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))?;
|
||||
fn scan_user(osu: &Osu, u: &OsuUser, mode: Mode) -> Result<Vec<(u8, Score)>, Error> {
|
||||
let scores = osu.user_best(UserID::ID(u.id), |f| f.mode(mode).limit(25))?;
|
||||
let scores = scores
|
||||
.into_iter()
|
||||
.filter(|s: &Score| s.date >= u.last_update)
|
|
@ -1,5 +1,5 @@
|
|||
use super::db::OsuLastBeatmap;
|
||||
use super::BeatmapWithMode;
|
||||
use crate::db::{DBWriteGuard, OsuLastBeatmap};
|
||||
use serenity::{
|
||||
framework::standard::{CommandError as Error, CommandResult},
|
||||
model::id::ChannelId,
|
||||
|
@ -8,14 +8,11 @@ use serenity::{
|
|||
|
||||
/// Save the beatmap into the server data storage.
|
||||
pub(crate) fn save_beatmap(
|
||||
data: &mut ShareMap,
|
||||
data: &ShareMap,
|
||||
channel_id: ChannelId,
|
||||
bm: &BeatmapWithMode,
|
||||
) -> CommandResult {
|
||||
let mut db: DBWriteGuard<_> = data
|
||||
.get_mut::<OsuLastBeatmap>()
|
||||
.expect("DB is implemented")
|
||||
.into();
|
||||
let db = OsuLastBeatmap::open(data);
|
||||
let mut db = db.borrow_mut()?;
|
||||
|
||||
db.insert(channel_id, (bm.0.clone(), bm.mode()));
|
||||
|
@ -28,8 +25,8 @@ pub(crate) fn get_beatmap(
|
|||
data: &ShareMap,
|
||||
channel_id: ChannelId,
|
||||
) -> Result<Option<BeatmapWithMode>, Error> {
|
||||
let db = data.get::<OsuLastBeatmap>().expect("DB is implemented");
|
||||
let db = db.borrow_data()?;
|
||||
let db = OsuLastBeatmap::open(data);
|
||||
let db = db.borrow()?;
|
||||
|
||||
Ok(db
|
||||
.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 crate::commands::args::Duration;
|
||||
use crate::models::{Beatmap, Mode, Rank, Score, User};
|
||||
use chrono::Utc;
|
||||
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 {
|
||||
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 regex::Regex;
|
||||
use serenity::{
|
||||
builder::CreateMessage,
|
||||
framework::standard::{CommandError as Error, CommandResult},
|
||||
model::channel::Message,
|
||||
prelude::*,
|
||||
utils::MessageBuilder,
|
||||
};
|
||||
use youmubot_osu::{
|
||||
models::{Beatmap, Mode},
|
||||
request::BeatmapRequestKind,
|
||||
};
|
||||
use youmubot_prelude::*;
|
||||
|
||||
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.
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -71,9 +71,7 @@ struct ToPrint<'a> {
|
|||
}
|
||||
|
||||
fn handle_old_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPrint<'a>>, Error> {
|
||||
let data = ctx.data.write();
|
||||
let reqwest = data.get::<http::HTTP>().unwrap();
|
||||
let osu = data.get::<http::Osu>().unwrap();
|
||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||
let mut to_prints: Vec<ToPrint<'a>> = Vec::new();
|
||||
for capture in OLD_LINK_REGEX.captures_iter(content) {
|
||||
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,
|
||||
})
|
||||
});
|
||||
let beatmaps = osu.beatmaps(reqwest, req, |v| match mode {
|
||||
let beatmaps = osu.beatmaps(req, |v| match mode {
|
||||
Some(m) => v.mode(m, true),
|
||||
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> {
|
||||
let data = ctx.data.write();
|
||||
let reqwest = data.get::<http::HTTP>().unwrap();
|
||||
let osu = data.get::<http::Osu>().unwrap();
|
||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||
let mut to_prints: Vec<ToPrint<'a>> = Vec::new();
|
||||
for capture in NEW_LINK_REGEX.captures_iter(content) {
|
||||
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()?)
|
||||
}
|
||||
};
|
||||
let beatmaps = osu.beatmaps(reqwest, req, |v| match mode {
|
||||
let beatmaps = osu.beatmaps(req, |v| match mode {
|
||||
Some(m) => v.mode(m, true),
|
||||
None => v,
|
||||
})?;
|
|
@ -1,30 +1,70 @@
|
|||
use crate::db::{DBWriteGuard, OsuSavedUsers, OsuUser};
|
||||
use crate::http;
|
||||
use crate::{
|
||||
models::{Beatmap, Mode, User},
|
||||
request::{BeatmapRequestKind, UserID},
|
||||
Client as OsuHttpClient,
|
||||
};
|
||||
use serenity::{
|
||||
framework::standard::{
|
||||
macros::{command, group},
|
||||
Args, CommandError as Error, CommandResult,
|
||||
},
|
||||
model::{channel::Message, id::UserId},
|
||||
prelude::*,
|
||||
utils::MessageBuilder,
|
||||
};
|
||||
use std::str::FromStr;
|
||||
use youmubot_osu::{
|
||||
models::{Beatmap, Mode, User},
|
||||
request::{BeatmapRequestKind, UserID},
|
||||
Client as OsuClient,
|
||||
};
|
||||
use youmubot_prelude::*;
|
||||
|
||||
mod announcer;
|
||||
mod cache;
|
||||
mod db;
|
||||
pub(crate) mod embeds;
|
||||
mod hook;
|
||||
|
||||
pub use announcer::OsuAnnouncer;
|
||||
use db::OsuUser;
|
||||
use db::{OsuLastBeatmap, OsuSavedUsers};
|
||||
use embeds::{beatmap_embed, score_embed, user_embed};
|
||||
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]
|
||||
#[prefix = "osu"]
|
||||
#[description = "osu! related commands."]
|
||||
|
@ -91,18 +131,13 @@ impl AsRef<Beatmap> for BeatmapWithMode {
|
|||
#[usage = "[username or user_id]"]
|
||||
#[num_args(1)]
|
||||
pub fn save(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let mut data = ctx.data.write();
|
||||
let reqwest = data.get::<http::HTTP>().unwrap();
|
||||
let osu = data.get::<http::Osu>().unwrap();
|
||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||
|
||||
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 {
|
||||
Some(u) => {
|
||||
let mut db: DBWriteGuard<_> = data
|
||||
.get_mut::<OsuSavedUsers>()
|
||||
.ok_or(Error::from("DB uninitialized"))?
|
||||
.into();
|
||||
let db = OsuSavedUsers::open(&*ctx.data.read());
|
||||
let mut db = db.borrow_mut()?;
|
||||
|
||||
db.insert(
|
||||
|
@ -148,20 +183,14 @@ enum UsernameArg {
|
|||
}
|
||||
|
||||
impl UsernameArg {
|
||||
fn to_user_id_query(
|
||||
s: Option<Self>,
|
||||
data: &mut ShareMap,
|
||||
msg: &Message,
|
||||
) -> Result<UserID, Error> {
|
||||
fn to_user_id_query(s: Option<Self>, data: &ShareMap, msg: &Message) -> Result<UserID, Error> {
|
||||
let id = match s {
|
||||
Some(UsernameArg::Raw(s)) => return Ok(UserID::Auto(s)),
|
||||
Some(UsernameArg::Tagged(r)) => r,
|
||||
None => msg.author.id,
|
||||
};
|
||||
let db: DBWriteGuard<_> = data
|
||||
.get_mut::<OsuSavedUsers>()
|
||||
.ok_or(Error::from("DB uninitialized"))?
|
||||
.into();
|
||||
|
||||
let db = OsuSavedUsers::open(data);
|
||||
let db = db.borrow()?;
|
||||
db.get(&id)
|
||||
.cloned()
|
||||
|
@ -201,28 +230,24 @@ impl FromStr for Nth {
|
|||
#[example = "#1 / taiko / natsukagami"]
|
||||
#[max_args(3)]
|
||||
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 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: &OsuClient = data.get::<http::Osu>().unwrap();
|
||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||
let user = osu
|
||||
.user(reqwest, user, |f| f.mode(mode))?
|
||||
.user(user, |f| f.mode(mode))?
|
||||
.ok_or(Error::from("User not found"))?;
|
||||
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()
|
||||
.last()
|
||||
.ok_or(Error::from("No such play"))?;
|
||||
let beatmap = osu
|
||||
.beatmaps(
|
||||
reqwest,
|
||||
BeatmapRequestKind::Beatmap(recent_play.beatmap_id),
|
||||
|f| f.mode(mode, true),
|
||||
)?
|
||||
.beatmaps(BeatmapRequestKind::Beatmap(recent_play.beatmap_id), |f| {
|
||||
f.mode(mode, true)
|
||||
})?
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|v| BeatmapWithMode(v, mode))
|
||||
|
@ -237,7 +262,7 @@ pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
})?;
|
||||
|
||||
// Save the beatmap...
|
||||
cache::save_beatmap(&mut *data, msg.channel_id, &beatmap)?;
|
||||
cache::save_beatmap(&*ctx.data.read(), msg.channel_id, &beatmap)?;
|
||||
|
||||
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."]
|
||||
#[num_args(0)]
|
||||
pub fn last(ctx: &mut Context, msg: &Message, _: Args) -> CommandResult {
|
||||
let mut data = ctx.data.write();
|
||||
|
||||
let b = cache::get_beatmap(&mut *data, msg.channel_id)?;
|
||||
let b = cache::get_beatmap(&*ctx.data.read(), msg.channel_id)?;
|
||||
|
||||
match b {
|
||||
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."]
|
||||
#[max_args(1)]
|
||||
pub fn check(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let mut data = ctx.data.write();
|
||||
|
||||
let bm = cache::get_beatmap(&mut *data, msg.channel_id)?;
|
||||
let bm = cache::get_beatmap(&*ctx.data.read(), msg.channel_id)?;
|
||||
|
||||
match bm {
|
||||
None => {
|
||||
|
@ -284,18 +305,18 @@ pub fn check(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
Some(bm) => {
|
||||
let b = &bm.0;
|
||||
let m = bm.1;
|
||||
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 = data.get::<http::Osu>().unwrap();
|
||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||
|
||||
let user = osu
|
||||
.user(reqwest, user, |f| f)?
|
||||
.user(user, |f| f)?
|
||||
.ok_or(Error::from("User not found"))?;
|
||||
let scores = osu.scores(reqwest, b.beatmap_id, |f| {
|
||||
f.user(UserID::ID(user.id)).mode(m)
|
||||
})?;
|
||||
let scores = osu.scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(m))?;
|
||||
|
||||
if scores.is_empty() {
|
||||
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)
|
||||
.unwrap_or(Mode::Std);
|
||||
|
||||
let mut data = ctx.data.write();
|
||||
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: &OsuClient = data.get::<http::Osu>().unwrap();
|
||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||
let user = osu
|
||||
.user(reqwest, user, |f| f.mode(mode))?
|
||||
.user(user, |f| f.mode(mode))?
|
||||
.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;
|
||||
|
||||
|
@ -341,11 +361,9 @@ pub fn top(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
|||
.last()
|
||||
.ok_or(Error::from("No such play"))?;
|
||||
let beatmap = osu
|
||||
.beatmaps(
|
||||
reqwest,
|
||||
BeatmapRequestKind::Beatmap(top_play.beatmap_id),
|
||||
|f| f.mode(mode, true),
|
||||
)?
|
||||
.beatmaps(BeatmapRequestKind::Beatmap(top_play.beatmap_id), |f| {
|
||||
f.mode(mode, true)
|
||||
})?
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|v| BeatmapWithMode(v, mode))
|
||||
|
@ -360,25 +378,24 @@ pub fn top(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
|||
})?;
|
||||
|
||||
// Save the beatmap...
|
||||
cache::save_beatmap(&mut *data, msg.channel_id, &beatmap)?;
|
||||
cache::save_beatmap(&*ctx.data.read(), msg.channel_id, &beatmap)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_user(ctx: &mut Context, msg: &Message, mut args: Args, mode: Mode) -> CommandResult {
|
||||
let mut data = ctx.data.write();
|
||||
let user = UsernameArg::to_user_id_query(args.single::<UsernameArg>().ok(), &mut *data, msg)?;
|
||||
let reqwest = data.get::<http::HTTP>().unwrap();
|
||||
let osu = data.get::<http::Osu>().unwrap();
|
||||
let user = osu.user(reqwest, user, |f| f.mode(mode))?;
|
||||
let user =
|
||||
UsernameArg::to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||
let user = osu.user(user, |f| f.mode(mode))?;
|
||||
match user {
|
||||
Some(u) => {
|
||||
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()
|
||||
.next()
|
||||
.map(|m| {
|
||||
osu.beatmaps(reqwest, BeatmapRequestKind::Beatmap(m.beatmap_id), |f| {
|
||||
osu.beatmaps(BeatmapRequestKind::Beatmap(m.beatmap_id), |f| {
|
||||
f.mode(mode, true)
|
||||
})
|
||||
.map(|map| (m, BeatmapWithMode(map.into_iter().next().unwrap(), mode)))
|
|
@ -1,5 +1,5 @@
|
|||
pub mod discord;
|
||||
pub mod models;
|
||||
|
||||
pub mod request;
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -10,11 +10,14 @@ use request::builders::*;
|
|||
use request::*;
|
||||
use reqwest::blocking::{Client as HTTPClient, RequestBuilder, Response};
|
||||
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.
|
||||
/// It's cheap to clone, so do it.
|
||||
#[derive(Clone)]
|
||||
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> {
|
||||
|
@ -29,52 +32,50 @@ fn vec_try_into<U, T: std::convert::TryFrom<U>>(v: Vec<U>) -> Result<Vec<T>, T::
|
|||
|
||||
impl Client {
|
||||
/// 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 {
|
||||
key: key.as_ref().to_string(),
|
||||
key: Arc::new(key),
|
||||
client: http_client,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_request(&self, c: &HTTPClient, r: RequestBuilder) -> Result<Response, Error> {
|
||||
let v = r.query(&[("k", &self.key)]).build()?;
|
||||
fn build_request(&self, r: RequestBuilder) -> Result<Response, Error> {
|
||||
let v = r.query(&[("k", &*self.key)]).build()?;
|
||||
dbg!(v.url());
|
||||
Ok(c.execute(v)?)
|
||||
Ok(self.client.execute(v)?)
|
||||
}
|
||||
|
||||
pub fn beatmaps(
|
||||
&self,
|
||||
client: &HTTPClient,
|
||||
kind: BeatmapRequestKind,
|
||||
f: impl FnOnce(&mut BeatmapRequestBuilder) -> &mut BeatmapRequestBuilder,
|
||||
) -> Result<Vec<Beatmap>, Error> {
|
||||
let mut r = BeatmapRequestBuilder::new(kind);
|
||||
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)?)
|
||||
}
|
||||
|
||||
pub fn user(
|
||||
&self,
|
||||
client: &HTTPClient,
|
||||
user: UserID,
|
||||
f: impl FnOnce(&mut UserRequestBuilder) -> &mut UserRequestBuilder,
|
||||
) -> Result<Option<User>, Error> {
|
||||
let mut r = UserRequestBuilder::new(user);
|
||||
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)?;
|
||||
Ok(res.into_iter().next())
|
||||
}
|
||||
|
||||
pub fn scores(
|
||||
&self,
|
||||
client: &HTTPClient,
|
||||
beatmap_id: u64,
|
||||
f: impl FnOnce(&mut ScoreRequestBuilder) -> &mut ScoreRequestBuilder,
|
||||
) -> Result<Vec<Score>, Error> {
|
||||
let mut r = ScoreRequestBuilder::new(beatmap_id);
|
||||
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)?;
|
||||
|
||||
// with a scores request you need to fill the beatmap ids yourself
|
||||
|
@ -86,32 +87,29 @@ impl Client {
|
|||
|
||||
pub fn user_best(
|
||||
&self,
|
||||
client: &HTTPClient,
|
||||
user: UserID,
|
||||
f: impl FnOnce(&mut UserScoreRequestBuilder) -> &mut UserScoreRequestBuilder,
|
||||
) -> Result<Vec<Score>, Error> {
|
||||
self.user_scores(UserScoreType::Best, client, user, f)
|
||||
self.user_scores(UserScoreType::Best, user, f)
|
||||
}
|
||||
|
||||
pub fn user_recent(
|
||||
&self,
|
||||
client: &HTTPClient,
|
||||
user: UserID,
|
||||
f: impl FnOnce(&mut UserScoreRequestBuilder) -> &mut UserScoreRequestBuilder,
|
||||
) -> Result<Vec<Score>, Error> {
|
||||
self.user_scores(UserScoreType::Recent, client, user, f)
|
||||
self.user_scores(UserScoreType::Recent, user, f)
|
||||
}
|
||||
|
||||
fn user_scores(
|
||||
&self,
|
||||
u: UserScoreType,
|
||||
client: &HTTPClient,
|
||||
user: UserID,
|
||||
f: impl FnOnce(&mut UserScoreRequestBuilder) -> &mut UserScoreRequestBuilder,
|
||||
) -> Result<Vec<Score>, Error> {
|
||||
let mut r = UserScoreRequestBuilder::new(u, user);
|
||||
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)?;
|
||||
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::{
|
||||
framework::standard::{CommandError as Error, CommandResult},
|
||||
http::{CacheHttp, Http},
|
||||
model::id::{ChannelId, GuildId, UserId},
|
||||
prelude::ShareMap,
|
||||
};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
collections::{HashMap, HashSet},
|
||||
thread::{spawn, JoinHandle},
|
||||
};
|
||||
use youmubot_db::DB;
|
||||
|
||||
pub(crate) type AnnouncerChannels = DB<HashMap<String, HashMap<GuildId, ChannelId>>>;
|
||||
|
||||
pub trait Announcer {
|
||||
fn announcer_key() -> &'static str;
|
||||
fn send_messages(
|
||||
c: &Http,
|
||||
d: &mut ShareMap,
|
||||
d: AppData,
|
||||
channels: impl Fn(UserId) -> Vec<ChannelId> + Sync,
|
||||
) -> CommandResult;
|
||||
|
||||
fn set_channel(d: &mut ShareMap, guild: GuildId, channel: ChannelId) -> CommandResult {
|
||||
let mut data: DBWriteGuard<_> = d
|
||||
.get_mut::<AnnouncerChannels>()
|
||||
.expect("DB initialized")
|
||||
.into();
|
||||
let mut data = data.borrow_mut()?;
|
||||
data.entry(Self::announcer_key().to_owned())
|
||||
fn set_channel(d: AppData, guild: GuildId, channel: ChannelId) -> CommandResult {
|
||||
AnnouncerChannels::open(&*d.read())
|
||||
.borrow_mut()?
|
||||
.entry(Self::announcer_key().to_owned())
|
||||
.or_default()
|
||||
.insert(guild, channel);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_guilds(d: &mut ShareMap) -> Result<Vec<(GuildId, ChannelId)>, Error> {
|
||||
let data = d
|
||||
.get::<AnnouncerChannels>()
|
||||
.expect("DB initialized")
|
||||
.read(|v| {
|
||||
v.get(Self::announcer_key())
|
||||
fn get_guilds(d: AppData) -> Result<Vec<(GuildId, ChannelId)>, Error> {
|
||||
let data = AnnouncerChannels::open(&*d.read())
|
||||
.borrow()?
|
||||
.get(Self::announcer_key())
|
||||
.map(|m| m.iter().map(|(a, b)| (*a, *b)).collect())
|
||||
.unwrap_or_else(|| vec![])
|
||||
})?;
|
||||
.unwrap_or_else(|| vec![]);
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn announce(c: &Http, d: &mut ShareMap) -> CommandResult {
|
||||
let guilds: Vec<_> = Self::get_guilds(d)?;
|
||||
fn announce(c: impl AsRef<Http>, d: AppData) -> CommandResult {
|
||||
let guilds: Vec<_> = Self::get_guilds(d.clone())?;
|
||||
let member_sets = {
|
||||
let mut v = Vec::with_capacity(guilds.len());
|
||||
for (guild, channel) in guilds.into_iter() {
|
||||
|
@ -75,7 +71,7 @@ pub trait Announcer {
|
|||
let c = client.cache_and_http.clone();
|
||||
let data = client.data.clone();
|
||||
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);
|
||||
}
|
||||
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]
|
||||
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"
|
||||
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::var;
|
||||
use reqwest;
|
||||
use serenity::{
|
||||
framework::standard::{DispatchError, StandardFramework},
|
||||
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;
|
||||
mod db;
|
||||
mod http;
|
||||
|
||||
use commands::osu::OsuAnnouncer;
|
||||
use commands::Announcer;
|
||||
|
||||
const MESSAGE_HOOKS: [fn(&mut Context, &Message) -> (); 1] = [commands::osu::hook];
|
||||
const MESSAGE_HOOKS: [fn(&mut Context, &Message) -> (); 1] = [youmubot_osu::discord::hook];
|
||||
|
||||
struct Handler;
|
||||
|
||||
|
@ -40,26 +32,28 @@ fn main() {
|
|||
// Collect the token
|
||||
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
|
||||
setup_framework(Client::new(token, Handler).expect("Cannot connect..."))
|
||||
Client::new(token, Handler).expect("Cannot connect")
|
||||
};
|
||||
|
||||
// Setup initial data
|
||||
db::setup_db(&mut client).expect("Setup db should succeed");
|
||||
// Setup shared instances of things
|
||||
// Set up base framework
|
||||
let mut fw = setup_framework(&client);
|
||||
|
||||
// Setup each package starting from the prelude.
|
||||
{
|
||||
let mut data = client.data.write();
|
||||
data.insert::<http::HTTP>(reqwest::blocking::Client::new());
|
||||
data.insert::<http::Osu>(OsuClient::new(
|
||||
var("OSU_API_KEY").expect("Please set OSU_API_KEY as osu! api key."),
|
||||
));
|
||||
let db_path = var("DBPATH")
|
||||
.map(|v| std::path::PathBuf::from(v))
|
||||
.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...");
|
||||
if let Err(v) = client.start() {
|
||||
panic!(v)
|
||||
|
@ -69,7 +63,7 @@ fn main() {
|
|||
}
|
||||
|
||||
// Sets up a framework for a client
|
||||
fn setup_framework(mut client: Client) -> Client {
|
||||
fn setup_framework(client: &Client) -> StandardFramework {
|
||||
// Collect owners
|
||||
let owner = client
|
||||
.cache_and_http
|
||||
|
@ -78,7 +72,6 @@ fn setup_framework(mut client: Client) -> Client {
|
|||
.expect("Should be able to get app info")
|
||||
.owner;
|
||||
|
||||
client.with_framework(
|
||||
StandardFramework::new()
|
||||
.configure(|c| {
|
||||
c.with_whitespace(false)
|
||||
|
@ -86,7 +79,7 @@ fn setup_framework(mut client: Client) -> Client {
|
|||
.delimiters(vec![" / ", "/ ", " /", "/"])
|
||||
.owners([owner.id].iter().cloned().collect())
|
||||
})
|
||||
.help(&commands::HELP)
|
||||
.help(&youmubot_core::HELP)
|
||||
.before(|_, msg, command_name| {
|
||||
println!(
|
||||
"Got command '{}' by user '{}'",
|
||||
|
@ -135,10 +128,8 @@ fn setup_framework(mut client: Client) -> Client {
|
|||
c.delay(30).time_span(30).limit(1)
|
||||
})
|
||||
// groups here
|
||||
.group(&commands::ADMIN_GROUP)
|
||||
.group(&commands::FUN_GROUP)
|
||||
.group(&commands::COMMUNITY_GROUP)
|
||||
.group(&commands::OSU_GROUP)
|
||||
);
|
||||
client
|
||||
.group(&youmubot_core::ADMIN_GROUP)
|
||||
.group(&youmubot_core::FUN_GROUP)
|
||||
.group(&youmubot_core::COMMUNITY_GROUP)
|
||||
.group(&OSU_GROUP)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue