diff --git a/.gitignore b/.gitignore index 9bdf0ca..7242b97 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ target .env -*.ron +*.toml cargo-remote diff --git a/youmubot/src/commands/mod.rs b/youmubot/src/commands/mod.rs index 6c3b173..0160e99 100644 --- a/youmubot/src/commands/mod.rs +++ b/youmubot/src/commands/mod.rs @@ -20,6 +20,8 @@ pub use community::COMMUNITY_GROUP; pub use fun::FUN_GROUP; pub use osu::OSU_GROUP; +pub use announcer::Announcer; + // A help command #[help] pub fn help( diff --git a/youmubot/src/commands/osu/announcer.rs b/youmubot/src/commands/osu/announcer.rs new file mode 100644 index 0000000..d789d99 --- /dev/null +++ b/youmubot/src/commands/osu/announcer.rs @@ -0,0 +1,96 @@ +use super::{embeds::score_embed, BeatmapWithMode}; +use crate::{ + commands::announcer::Announcer, + db::{OsuSavedUsers, OsuUser}, + http::{Osu, HTTP}, +}; +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, +}; + +/// Announce osu! top scores. +pub struct OsuAnnouncer; + +impl Announcer for OsuAnnouncer { + fn announcer_key() -> &'static str { + "osu" + } + fn send_messages( + c: &Http, + d: &mut ShareMap, + channels: impl Fn(UserId) -> Vec, + ) -> CommandResult { + let http = d.get::().expect("HTTP"); + let osu = d.get::().expect("osu!client"); + // For each user... + let mut data = d + .get::() + .expect("DB initialized") + .read(|f| f.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)?; + if scores.is_empty() { + continue; + } + let user = user.get_or_insert_with(|| { + osu.user(http, UserID::ID(osu_user.id), |f| f) + .unwrap() + .unwrap() + }); + for (rank, score) in scores { + let beatmap = BeatmapWithMode( + osu.beatmaps(http, BeatmapRequestKind::Beatmap(score.beatmap_id), |f| f)? + .into_iter() + .next() + .unwrap(), + *mode, + ); + for channel in channels(*user_id) { + channel.send_message(c, |c| { + c.content(format!("New top record from {}!", user_id.mention())) + .embed(|e| score_embed(&score, &beatmap, &user, Some(rank), e)) + })?; + } + } + } + osu_user.last_update = chrono::Utc::now(); + } + // Update users + let f = d.get_mut::().expect("DB initialized"); + f.write(|f| *f = data)?; + f.save()?; + Ok(()) + } +} + +impl OsuAnnouncer { + fn scan_user( + http: &HTTPClient, + osu: &OsuClient, + u: &OsuUser, + mode: Mode, + ) -> Result, Error> { + let scores = osu.user_best(http, UserID::ID(u.id), |f| f.mode(mode).limit(25))?; + let scores = scores + .into_iter() + .filter(|s: &Score| s.date >= u.last_update) + .enumerate() + .map(|(i, v)| (i as u8, v)) + .collect(); + Ok(scores) + } +} diff --git a/youmubot/src/commands/osu/mod.rs b/youmubot/src/commands/osu/mod.rs index c0e67ce..24a11af 100644 --- a/youmubot/src/commands/osu/mod.rs +++ b/youmubot/src/commands/osu/mod.rs @@ -21,6 +21,7 @@ mod cache; pub(crate) mod embeds; mod hook; +pub use announcer::OsuAnnouncer; use embeds::{beatmap_embed, score_embed, user_embed}; pub use hook::hook; diff --git a/youmubot/src/db/mod.rs b/youmubot/src/db/mod.rs index e115383..0922b9f 100644 --- a/youmubot/src/db/mod.rs +++ b/youmubot/src/db/mod.rs @@ -33,6 +33,9 @@ where } } +/// A map from announcer keys to guild IDs and to channels. +pub type AnnouncerChannels = DB>>; + /// A list of SoftBans for all servers. pub type SoftBans = DB>; @@ -49,9 +52,10 @@ pub fn setup_db(client: &mut Client) -> Result<(), Error> { PathBuf::from("data") }); let mut data = client.data.write(); - SoftBans::insert_into(&mut *data, &path.join("soft_bans.ron"))?; - OsuSavedUsers::insert_into(&mut *data, &path.join("osu_saved_users.ron"))?; - OsuLastBeatmap::insert_into(&mut *data, &path.join("last_beatmaps.ron"))?; + SoftBans::insert_into(&mut *data, &path.join("soft_bans.toml"))?; + OsuSavedUsers::insert_into(&mut *data, &path.join("osu_saved_users.toml"))?; + OsuLastBeatmap::insert_into(&mut *data, &path.join("last_beatmaps.toml"))?; + AnnouncerChannels::insert_into(&mut *data, &path.join("announcers.toml"))?; Ok(()) } diff --git a/youmubot/src/main.rs b/youmubot/src/main.rs index 5295dfe..6f5d556 100644 --- a/youmubot/src/main.rs +++ b/youmubot/src/main.rs @@ -12,6 +12,9 @@ mod commands; mod db; mod http; +use commands::osu::OsuAnnouncer; +use commands::Announcer; + const MESSAGE_HOOKS: [fn(&mut Context, &Message) -> (); 1] = [commands::osu::hook]; struct Handler; @@ -54,6 +57,9 @@ fn main() { // Create handler threads std::thread::spawn(commands::admin::watch_soft_bans(&mut client)); + // Announcers + OsuAnnouncer::scan(&client, std::time::Duration::from_secs(60)); + println!("Starting..."); if let Err(v) = client.start() { panic!(v)