osu: Introduce Env and propagate it to other functions (#40)

This commit is contained in:
huynd2001 2024-03-17 12:21:13 -04:00 committed by GitHub
parent 94dc225b86
commit 1066f249b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 598 additions and 433 deletions

View file

@ -13,6 +13,7 @@ use serenity::{
use db::{CfSavedUsers, CfUser}; use db::{CfSavedUsers, CfUser};
pub use hook::InfoHook; pub use hook::InfoHook;
use youmubot_prelude::announcer::AnnouncerHandler;
use youmubot_prelude::table_format::table_formatting_unsafe; use youmubot_prelude::table_format::table_formatting_unsafe;
use youmubot_prelude::table_format::Align::{Left, Right}; use youmubot_prelude::table_format::Align::{Left, Right};
use youmubot_prelude::{ use youmubot_prelude::{

View file

@ -1,10 +1,15 @@
use std::collections::HashSet;
use serenity::{ use serenity::{
framework::standard::{ framework::standard::{
help_commands, macros::help, Args, CommandGroup, CommandResult, HelpOptions, help_commands, macros::help, Args, CommandGroup, CommandResult, HelpOptions,
}, },
model::{channel::Message, id::UserId}, model::{channel::Message, id::UserId},
}; };
use std::collections::HashSet;
pub use admin::ADMIN_GROUP;
pub use community::COMMUNITY_GROUP;
pub use fun::FUN_GROUP;
use youmubot_prelude::{announcer::CacheAndHttp, *}; use youmubot_prelude::{announcer::CacheAndHttp, *};
pub mod admin; pub mod admin;
@ -12,14 +17,9 @@ pub mod community;
mod db; mod db;
pub mod fun; pub mod fun;
pub use admin::ADMIN_GROUP;
pub use community::COMMUNITY_GROUP;
pub use fun::FUN_GROUP;
/// Sets up all databases in the client. /// Sets up all databases in the client.
pub fn setup( pub fn setup(
path: &std::path::Path, path: &std::path::Path,
client: &serenity::client::Client,
data: &mut TypeMap, data: &mut TypeMap,
) -> serenity::framework::standard::CommandResult { ) -> serenity::framework::standard::CommandResult {
db::SoftBans::insert_into(&mut *data, &path.join("soft_bans.yaml"))?; db::SoftBans::insert_into(&mut *data, &path.join("soft_bans.yaml"))?;
@ -29,18 +29,21 @@ pub fn setup(
&path.join("roles.yaml"), &path.join("roles.yaml"),
)?; )?;
// Create handler threads
tokio::spawn(admin::watch_soft_bans(
CacheAndHttp::from_client(client),
client.data.clone(),
));
// Start reaction handlers // Start reaction handlers
data.insert::<community::ReactionWatchers>(community::ReactionWatchers::new(&*data)?); data.insert::<community::ReactionWatchers>(community::ReactionWatchers::new(&*data)?);
Ok(()) Ok(())
} }
pub fn ready_hook(ctx: &Context) -> CommandResult {
// Create handler threads
tokio::spawn(admin::watch_soft_bans(
CacheAndHttp::from_context(ctx),
ctx.data.clone(),
));
Ok(())
}
// A help command // A help command
#[help] #[help]
pub async fn help( pub async fn help(

View file

@ -15,16 +15,15 @@ use youmubot_prelude::stream::TryStreamExt;
use youmubot_prelude::*; use youmubot_prelude::*;
use crate::{ use crate::{
discord::beatmap_cache::BeatmapMetaCache,
discord::cache::save_beatmap, discord::cache::save_beatmap,
discord::oppai_cache::{BeatmapCache, BeatmapContent}, discord::oppai_cache::BeatmapContent,
models::{Mode, Score, User, UserEventRank}, models::{Mode, Score, User, UserEventRank},
request::UserID, request::UserID,
Client as Osu, Client as Osu,
}; };
use super::db::{OsuSavedUsers, OsuUser}; use super::db::{OsuSavedUsers, OsuUser};
use super::{calculate_weighted_map_length, OsuClient}; use super::{calculate_weighted_map_length, OsuEnv};
use super::{embeds::score_embed, BeatmapWithMode}; use super::{embeds::score_embed, BeatmapWithMode};
/// osu! announcer's unique announcer key. /// osu! announcer's unique announcer key.
@ -51,9 +50,8 @@ impl youmubot_prelude::Announcer for Announcer {
) -> Result<()> { ) -> Result<()> {
// For each user... // For each user...
let users = { let users = {
let data = d.read().await; let env = d.read().await.get::<OsuEnv>().unwrap().clone();
let data = data.get::<OsuSavedUsers>().unwrap(); env.saved_users.all().await?
data.all().await?
}; };
let now = chrono::Utc::now(); let now = chrono::Utc::now();
users users
@ -198,13 +196,12 @@ impl Announcer {
} }
async fn std_weighted_map_length(ctx: &Context, u: &OsuUser) -> Result<f64> { async fn std_weighted_map_length(ctx: &Context, u: &OsuUser) -> Result<f64> {
let data = ctx.data.read().await; let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let client = data.get::<OsuClient>().unwrap().clone(); let scores = env
let cache = data.get::<BeatmapMetaCache>().unwrap(); .client
let scores = client
.user_best(UserID::ID(u.id), |f| f.mode(Mode::Std).limit(100)) .user_best(UserID::ID(u.id), |f| f.mode(Mode::Std).limit(100))
.await?; .await?;
calculate_weighted_map_length(&scores, cache, Mode::Std).await calculate_weighted_map_length(&scores, &env.beatmaps, Mode::Std).await
} }
} }
@ -282,11 +279,12 @@ impl<'a> CollectedScore<'a> {
} }
async fn get_beatmap(&self, ctx: &Context) -> Result<(BeatmapWithMode, BeatmapContent)> { async fn get_beatmap(&self, ctx: &Context) -> Result<(BeatmapWithMode, BeatmapContent)> {
let data = ctx.data.read().await; let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let cache = data.get::<BeatmapMetaCache>().unwrap(); let beatmap = env
let oppai = data.get::<BeatmapCache>().unwrap(); .beatmaps
let beatmap = cache.get_beatmap_default(self.score.beatmap_id).await?; .get_beatmap_default(self.score.beatmap_id)
let content = oppai.get_beatmap(beatmap.beatmap_id).await?; .await?;
let content = env.oppai.get_beatmap(beatmap.beatmap_id).await?;
Ok((BeatmapWithMode(beatmap, self.mode), content)) Ok((BeatmapWithMode(beatmap, self.mode), content))
} }
@ -341,9 +339,10 @@ impl<'a> CollectedScore<'a> {
}), }),
) )
.await?; .await?;
save_beatmap(&*ctx.data.read().await, channel, bm)
.await let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
.pls_ok();
save_beatmap(&env, channel, bm).await.pls_ok();
Ok(m) Ok(m)
} }
} }

View file

@ -1,18 +1,27 @@
use std::sync::Arc;
use youmubot_db_sql::{models::osu as models, Pool};
use youmubot_prelude::*;
use crate::{ use crate::{
models::{ApprovalStatus, Beatmap, Mode}, models::{ApprovalStatus, Beatmap, Mode},
Client, Client,
}; };
use std::sync::Arc;
use youmubot_db_sql::{models::osu as models, Pool};
use youmubot_prelude::*;
/// BeatmapMetaCache intercepts beatmap-by-id requests and caches them for later recalling. /// BeatmapMetaCache intercepts beatmap-by-id requests and caches them for later recalling.
/// Does not cache non-Ranked beatmaps. /// Does not cache non-Ranked beatmaps.
#[derive(Clone)]
pub struct BeatmapMetaCache { pub struct BeatmapMetaCache {
client: Arc<Client>, client: Arc<Client>,
pool: Pool, pool: Pool,
} }
impl std::fmt::Debug for BeatmapMetaCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<BeatmapMetaCache>")
}
}
impl TypeMapKey for BeatmapMetaCache { impl TypeMapKey for BeatmapMetaCache {
type Value = BeatmapMetaCache; type Value = BeatmapMetaCache;
} }

View file

@ -1,29 +1,26 @@
use super::db::OsuLastBeatmap;
use super::BeatmapWithMode;
use serenity::model::id::ChannelId; use serenity::model::id::ChannelId;
use youmubot_prelude::*; use youmubot_prelude::*;
use super::{BeatmapWithMode, OsuEnv};
/// Save the beatmap into the server data storage. /// Save the beatmap into the server data storage.
pub(crate) async fn save_beatmap( pub(crate) async fn save_beatmap(
data: &TypeMap, env: &OsuEnv,
channel_id: ChannelId, channel_id: ChannelId,
bm: &BeatmapWithMode, bm: &BeatmapWithMode,
) -> Result<()> { ) -> Result<()> {
data.get::<OsuLastBeatmap>() env.last_beatmaps.save(channel_id, &bm.0, bm.1).await?;
.unwrap()
.save(channel_id, &bm.0, bm.1)
.await?;
Ok(()) Ok(())
} }
/// Get the last beatmap requested from this channel. /// Get the last beatmap requested from this channel.
pub(crate) async fn get_beatmap( pub(crate) async fn get_beatmap(
data: &TypeMap, env: &OsuEnv,
channel_id: ChannelId, channel_id: ChannelId,
) -> Result<Option<BeatmapWithMode>> { ) -> Result<Option<BeatmapWithMode>> {
data.get::<OsuLastBeatmap>() env.last_beatmaps
.unwrap()
.by_channel(channel_id) .by_channel(channel_id)
.await .await
.map(|v| v.map(|(bm, mode)| BeatmapWithMode(bm, mode))) .map(|v| v.map(|(bm, mode)| BeatmapWithMode(bm, mode)))

View file

@ -1,14 +1,16 @@
use std::borrow::Cow; use std::borrow::Cow;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use youmubot_db_sql::{models::osu as models, models::osu_user as model, Pool};
use crate::models::{Beatmap, Mode, Score};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serenity::model::id::{ChannelId, UserId}; use serenity::model::id::{ChannelId, UserId};
use youmubot_db_sql::{models::osu as models, models::osu_user as model, Pool};
use youmubot_prelude::*; use youmubot_prelude::*;
use crate::models::{Beatmap, Mode, Score};
/// Save the user IDs. /// Save the user IDs.
#[derive(Debug, Clone)]
pub struct OsuSavedUsers { pub struct OsuSavedUsers {
pool: Pool, pool: Pool,
} }
@ -60,6 +62,7 @@ impl OsuSavedUsers {
} }
/// Save each channel's last requested beatmap. /// Save each channel's last requested beatmap.
#[derive(Debug, Clone)]
pub struct OsuLastBeatmap(Pool); pub struct OsuLastBeatmap(Pool);
impl TypeMapKey for OsuLastBeatmap { impl TypeMapKey for OsuLastBeatmap {
@ -99,6 +102,7 @@ impl OsuLastBeatmap {
} }
/// Save each channel's last requested beatmap. /// Save each channel's last requested beatmap.
#[derive(Debug, Clone)]
pub struct OsuUserBests(Pool); pub struct OsuUserBests(Pool);
impl TypeMapKey for OsuUserBests { impl TypeMapKey for OsuUserBests {
@ -188,14 +192,16 @@ impl From<model::OsuUser> for OsuUser {
#[allow(dead_code)] #[allow(dead_code)]
mod legacy { mod legacy {
use chrono::{DateTime, Utc}; use std::collections::HashMap;
use crate::models::{Beatmap, Mode, Score}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serenity::model::id::{ChannelId, UserId}; use serenity::model::id::{ChannelId, UserId};
use std::collections::HashMap;
use youmubot_db::DB; use youmubot_db::DB;
use crate::models::{Beatmap, Mode, Score};
pub type OsuSavedUsers = DB<HashMap<UserId, OsuUser>>; pub type OsuSavedUsers = DB<HashMap<UserId, OsuUser>>;
/// An osu! saved user. /// An osu! saved user.

View file

@ -54,9 +54,7 @@ mod scores {
use youmubot_prelude::*; use youmubot_prelude::*;
use crate::discord::{ use crate::discord::{cache::save_beatmap, BeatmapWithMode, OsuEnv};
cache::save_beatmap, BeatmapCache, BeatmapMetaCache, BeatmapWithMode,
};
use crate::models::{Mode, Score}; use crate::models::{Mode, Score};
pub async fn display_scores_grid<'a>( pub async fn display_scores_grid<'a>(
@ -88,19 +86,17 @@ mod scores {
#[async_trait] #[async_trait]
impl pagination::Paginate for Paginate { impl pagination::Paginate for Paginate {
async fn render(&mut self, page: u8, ctx: &Context, msg: &mut Message) -> Result<bool> { async fn render(&mut self, page: u8, ctx: &Context, msg: &mut Message) -> Result<bool> {
let data = ctx.data.read().await; let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let client = data.get::<crate::discord::OsuClient>().unwrap();
let osu = data.get::<BeatmapMetaCache>().unwrap();
let beatmap_cache = data.get::<BeatmapCache>().unwrap();
let page = page as usize; let page = page as usize;
let score = &self.scores[page]; let score = &self.scores[page];
let hourglass = msg.react(ctx, '⌛').await?; let hourglass = msg.react(ctx, '⌛').await?;
let mode = self.mode; let mode = self.mode;
let beatmap = osu.get_beatmap(score.beatmap_id, mode).await?; let beatmap = env.beatmaps.get_beatmap(score.beatmap_id, mode).await?;
let content = beatmap_cache.get_beatmap(beatmap.beatmap_id).await?; let content = env.oppai.get_beatmap(beatmap.beatmap_id).await?;
let bm = BeatmapWithMode(beatmap, mode); let bm = BeatmapWithMode(beatmap, mode);
let user = client let user = env
.client
.user(crate::request::UserID::ID(score.user_id), |f| f) .user(crate::request::UserID::ID(score.user_id), |f| f)
.await? .await?
.ok_or_else(|| Error::msg("user not found"))?; .ok_or_else(|| Error::msg("user not found"))?;
@ -114,7 +110,7 @@ mod scores {
}), }),
) )
.await?; .await?;
save_beatmap(&*ctx.data.read().await, msg.channel_id, &bm).await?; save_beatmap(&env, msg.channel_id, &bm).await?;
// End // End
hourglass.delete(ctx).await?; hourglass.delete(ctx).await?;
@ -138,7 +134,7 @@ mod scores {
use youmubot_prelude::*; use youmubot_prelude::*;
use crate::discord::oppai_cache::Accuracy; use crate::discord::oppai_cache::Accuracy;
use crate::discord::{Beatmap, BeatmapCache, BeatmapInfo, BeatmapMetaCache}; use crate::discord::{Beatmap, BeatmapInfo, OsuEnv};
use crate::models::{Mode, Score}; use crate::models::{Mode, Score};
pub async fn display_scores_table<'a>( pub async fn display_scores_table<'a>(
@ -178,9 +174,10 @@ mod scores {
#[async_trait] #[async_trait]
impl pagination::Paginate for Paginate { impl pagination::Paginate for Paginate {
async fn render(&mut self, page: u8, ctx: &Context, msg: &mut Message) -> Result<bool> { async fn render(&mut self, page: u8, ctx: &Context, msg: &mut Message) -> Result<bool> {
let data = ctx.data.read().await; let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let osu = data.get::<BeatmapMetaCache>().unwrap();
let beatmap_cache = data.get::<BeatmapCache>().unwrap(); let meta_cache = &env.beatmaps;
let oppai = &env.oppai;
let page = page as usize; let page = page as usize;
let start = page * ITEMS_PER_PAGE; let start = page * ITEMS_PER_PAGE;
let end = self.scores.len().min(start + ITEMS_PER_PAGE); let end = self.scores.len().min(start + ITEMS_PER_PAGE);
@ -194,9 +191,9 @@ mod scores {
let beatmaps = plays let beatmaps = plays
.iter() .iter()
.map(|play| async move { .map(|play| async move {
let beatmap = osu.get_beatmap(play.beatmap_id, mode).await?; let beatmap = meta_cache.get_beatmap(play.beatmap_id, mode).await?;
let info = { let info = {
let b = beatmap_cache.get_beatmap(beatmap.beatmap_id).await?; let b = oppai.get_beatmap(beatmap.beatmap_id).await?;
b.get_info_with(mode, play.mods).ok() b.get_info_with(mode, play.mods).ok()
}; };
Ok((beatmap, info)) as Result<(Beatmap, Option<BeatmapInfo>)> Ok((beatmap, info)) as Result<(Beatmap, Option<BeatmapInfo>)>
@ -211,7 +208,7 @@ mod scores {
match p.pp.map(|pp| format!("{:.2}", pp)) { match p.pp.map(|pp| format!("{:.2}", pp)) {
Some(v) => Ok(v), Some(v) => Ok(v),
None => { None => {
let b = beatmap_cache.get_beatmap(p.beatmap_id).await?; let b = oppai.get_beatmap(p.beatmap_id).await?;
let r: Result<_> = Ok({ let r: Result<_> = Ok({
b.get_pp_from( b.get_pp_from(
mode, mode,
@ -335,10 +332,9 @@ mod beatmapset {
use youmubot_prelude::*; use youmubot_prelude::*;
use crate::discord::OsuEnv;
use crate::{ use crate::{
discord::{ discord::{cache::save_beatmap, oppai_cache::BeatmapInfoWithPP, BeatmapWithMode},
cache::save_beatmap, oppai_cache::BeatmapInfoWithPP, BeatmapCache, BeatmapWithMode,
},
models::{Beatmap, Mode, Mods}, models::{Beatmap, Mode, Mods},
}; };
@ -386,9 +382,9 @@ mod beatmapset {
impl Paginate { impl Paginate {
async fn get_beatmap_info(&self, ctx: &Context, b: &Beatmap) -> Result<BeatmapInfoWithPP> { async fn get_beatmap_info(&self, ctx: &Context, b: &Beatmap) -> Result<BeatmapInfoWithPP> {
let data = ctx.data.read().await; let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let cache = data.get::<BeatmapCache>().unwrap();
cache env.oppai
.get_beatmap(b.beatmap_id) .get_beatmap(b.beatmap_id)
.await .await
.and_then(move |v| v.get_possible_pp_with(self.mode.unwrap_or(b.mode), self.mods)) .and_then(move |v| v.get_possible_pp_with(self.mode.unwrap_or(b.mode), self.mods))
@ -401,15 +397,10 @@ mod beatmapset {
Some(self.maps.len()) Some(self.maps.len())
} }
async fn render( async fn render(&mut self, page: u8, ctx: &Context, msg: &mut Message) -> Result<bool> {
&mut self,
page: u8,
ctx: &Context,
m: &mut serenity::model::channel::Message,
) -> Result<bool> {
let page = page as usize; let page = page as usize;
if page == self.maps.len() { if page == self.maps.len() {
m.edit( msg.edit(
ctx, ctx,
EditMessage::new().embed(crate::discord::embeds::beatmapset_embed( EditMessage::new().embed(crate::discord::embeds::beatmapset_embed(
&self.maps[..], &self.maps[..],
@ -432,8 +423,8 @@ mod beatmapset {
info info
} }
}; };
m.edit(ctx, msg.edit(ctx,
EditMessage::new().content(self.message.as_str()).embed( EditMessage::new().content(self.message.as_str()).embed(
crate::discord::embeds::beatmap_embed( crate::discord::embeds::beatmap_embed(
map, map,
self.mode.unwrap_or(map.mode), self.mode.unwrap_or(map.mode),
@ -451,9 +442,10 @@ mod beatmapset {
), ),
) )
.await?; .await?;
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
save_beatmap( save_beatmap(
&*ctx.data.read().await, &env,
m.channel_id, msg.channel_id,
&BeatmapWithMode(map.clone(), self.mode.unwrap_or(map.mode)), &BeatmapWithMode(map.clone(), self.mode.unwrap_or(map.mode)),
) )
.await .await

View file

@ -1,14 +1,17 @@
use crate::{ use std::str::FromStr;
discord::beatmap_cache::BeatmapMetaCache,
discord::oppai_cache::{BeatmapCache, BeatmapInfoWithPP},
models::{Beatmap, Mode, Mods},
};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
use serenity::{builder::CreateMessage, model::channel::Message, utils::MessageBuilder}; use serenity::{builder::CreateMessage, model::channel::Message, utils::MessageBuilder};
use std::str::FromStr;
use youmubot_prelude::*; use youmubot_prelude::*;
use crate::discord::OsuEnv;
use crate::{
discord::oppai_cache::BeatmapInfoWithPP,
models::{Beatmap, Mode, Mods},
};
use super::embeds::beatmap_embed; use super::embeds::beatmap_embed;
lazy_static! { lazy_static! {
@ -43,9 +46,9 @@ pub fn dot_osu_hook<'a>(
let url = attachment.url.clone(); let url = attachment.url.clone();
async move { async move {
let data = ctx.data.read().await; let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let oppai = data.get::<BeatmapCache>().unwrap();
let (beatmap, _) = oppai.download_beatmap_from_url(&url).await.ok()?; let (beatmap, _) = env.oppai.download_beatmap_from_url(&url).await.ok()?;
crate::discord::embeds::beatmap_offline_embed( crate::discord::embeds::beatmap_offline_embed(
&beatmap, &beatmap,
Mode::from(beatmap.content.mode as u8), /*For now*/ Mode::from(beatmap.content.mode as u8), /*For now*/
@ -68,9 +71,9 @@ pub fn dot_osu_hook<'a>(
.map(|attachment| { .map(|attachment| {
let url = attachment.url.clone(); let url = attachment.url.clone();
async move { async move {
let data = ctx.data.read().await; let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let oppai = data.get::<BeatmapCache>().unwrap();
let beatmaps = oppai.download_osz_from_url(&url).await.pls_ok()?; let beatmaps = env.oppai.download_osz_from_url(&url).await.pls_ok()?;
Some( Some(
beatmaps beatmaps
.into_iter() .into_iter()
@ -133,13 +136,12 @@ pub fn hook<'a>(
.pls_ok(); .pls_ok();
let mode = l.mode.unwrap_or(b.mode); let mode = l.mode.unwrap_or(b.mode);
let bm = super::BeatmapWithMode(*b, mode); let bm = super::BeatmapWithMode(*b, mode);
crate::discord::cache::save_beatmap(
&*ctx.data.read().await, let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
msg.channel_id,
&bm, crate::discord::cache::save_beatmap(&env, msg.channel_id, &bm)
) .await
.await .pls_ok();
.pls_ok();
} }
EmbedType::Beatmapset(b) => { EmbedType::Beatmapset(b) => {
handle_beatmapset(ctx, b, l.link, l.mode, msg) handle_beatmapset(ctx, b, l.link, l.mode, msg)
@ -174,8 +176,7 @@ fn handle_old_links<'a>(
.captures_iter(content) .captures_iter(content)
.map(move |capture| async move { .map(move |capture| async move {
let data = ctx.data.read().await; let data = ctx.data.read().await;
let cache = data.get::<BeatmapCache>().unwrap(); let env = data.get::<OsuEnv>().unwrap();
let osu = data.get::<BeatmapMetaCache>().unwrap();
let req_type = capture.name("link_type").unwrap().as_str(); let req_type = capture.name("link_type").unwrap().as_str();
let mode = capture let mode = capture
.name("mode") .name("mode")
@ -192,10 +193,18 @@ fn handle_old_links<'a>(
}); });
let beatmaps = match req_type { let beatmaps = match req_type {
"b" => vec![match mode { "b" => vec![match mode {
Some(mode) => osu.get_beatmap(capture["id"].parse()?, mode).await?, Some(mode) => {
None => osu.get_beatmap_default(capture["id"].parse()?).await?, env.beatmaps
.get_beatmap(capture["id"].parse()?, mode)
.await?
}
None => {
env.beatmaps
.get_beatmap_default(capture["id"].parse()?)
.await?
}
}], }],
"s" => osu.get_beatmapset(capture["id"].parse()?).await?, "s" => env.beatmaps.get_beatmapset(capture["id"].parse()?).await?,
_ => unreachable!(), _ => unreachable!(),
}; };
if beatmaps.is_empty() { if beatmaps.is_empty() {
@ -211,7 +220,7 @@ fn handle_old_links<'a>(
.unwrap_or(Mods::NOMOD); .unwrap_or(Mods::NOMOD);
let info = { let info = {
let mode = mode.unwrap_or(b.mode); let mode = mode.unwrap_or(b.mode);
cache env.oppai
.get_beatmap(b.beatmap_id) .get_beatmap(b.beatmap_id)
.await .await
.and_then(|b| b.get_possible_pp_with(mode, mods))? .and_then(|b| b.get_possible_pp_with(mode, mods))?
@ -233,13 +242,10 @@ fn handle_old_links<'a>(
}) })
.collect::<stream::FuturesUnordered<_>>() .collect::<stream::FuturesUnordered<_>>()
.filter_map(|v| { .filter_map(|v| {
future::ready(match v { future::ready(v.unwrap_or_else(|e| {
Ok(v) => v, eprintln!("{}", e);
Err(e) => { None
eprintln!("{}", e); }))
None
}
})
}) })
} }
@ -250,20 +256,23 @@ fn handle_new_links<'a>(
NEW_LINK_REGEX NEW_LINK_REGEX
.captures_iter(content) .captures_iter(content)
.map(|capture| async move { .map(|capture| async move {
let data = ctx.data.read().await; let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let osu = data.get::<BeatmapMetaCache>().unwrap();
let cache = data.get::<BeatmapCache>().unwrap();
let mode = capture let mode = capture
.name("mode") .name("mode")
.and_then(|v| Mode::parse_from_new_site(v.as_str())); .and_then(|v| Mode::parse_from_new_site(v.as_str()));
let link = capture.get(0).unwrap().as_str(); let link = capture.get(0).unwrap().as_str();
let beatmaps = match capture.name("beatmap_id") { let beatmaps = match capture.name("beatmap_id") {
Some(ref v) => vec![match mode { Some(ref v) => vec![match mode {
Some(mode) => osu.get_beatmap(v.as_str().parse()?, mode).await?, Some(mode) => env.beatmaps.get_beatmap(v.as_str().parse()?, mode).await?,
None => osu.get_beatmap_default(v.as_str().parse()?).await?, None => {
env.beatmaps
.get_beatmap_default(v.as_str().parse()?)
.await?
}
}], }],
None => { None => {
osu.get_beatmapset(capture.name("set_id").unwrap().as_str().parse()?) env.beatmaps
.get_beatmapset(capture.name("set_id").unwrap().as_str().parse()?)
.await? .await?
} }
}; };
@ -280,7 +289,7 @@ fn handle_new_links<'a>(
.unwrap_or(Mods::NOMOD); .unwrap_or(Mods::NOMOD);
let info = { let info = {
let mode = mode.unwrap_or(beatmap.mode); let mode = mode.unwrap_or(beatmap.mode);
cache env.oppai
.get_beatmap(beatmap.beatmap_id) .get_beatmap(beatmap.beatmap_id)
.await .await
.and_then(|b| b.get_possible_pp_with(mode, mods))? .and_then(|b| b.get_possible_pp_with(mode, mods))?
@ -328,16 +337,14 @@ fn handle_short_links<'a>(
return Err(Error::msg("not in server announcer channel")); return Err(Error::msg("not in server announcer channel"));
} }
} }
let data = ctx.data.read().await; let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let osu = data.get::<BeatmapMetaCache>().unwrap();
let cache = data.get::<BeatmapCache>().unwrap();
let mode = capture let mode = capture
.name("mode") .name("mode")
.and_then(|v| Mode::parse_from_new_site(v.as_str())); .and_then(|v| Mode::parse_from_new_site(v.as_str()));
let id: u64 = capture.name("id").unwrap().as_str().parse()?; let id: u64 = capture.name("id").unwrap().as_str().parse()?;
let beatmap = match mode { let beatmap = match mode {
Some(mode) => osu.get_beatmap(id, mode).await, Some(mode) => env.beatmaps.get_beatmap(id, mode).await,
None => osu.get_beatmap_default(id).await, None => env.beatmaps.get_beatmap_default(id).await,
}?; }?;
let mods = capture let mods = capture
.name("mods") .name("mods")
@ -345,7 +352,7 @@ fn handle_short_links<'a>(
.unwrap_or(Mods::NOMOD); .unwrap_or(Mods::NOMOD);
let info = { let info = {
let mode = mode.unwrap_or(beatmap.mode); let mode = mode.unwrap_or(beatmap.mode);
cache env.oppai
.get_beatmap(beatmap.beatmap_id) .get_beatmap(beatmap.beatmap_id)
.await .await
.and_then(|b| b.get_possible_pp_with(mode, mods))? .and_then(|b| b.get_possible_pp_with(mode, mods))?

View file

@ -1,11 +1,5 @@
use crate::{ use std::{str::FromStr, sync::Arc};
discord::beatmap_cache::BeatmapMetaCache,
discord::display::ScoreListStyle,
discord::oppai_cache::{BeatmapCache, BeatmapInfo},
models::{self, Beatmap, Mode, Mods, Score, User},
request::{BeatmapRequestKind, UserID},
Client as OsuHttpClient,
};
use rand::seq::IteratorRandom; use rand::seq::IteratorRandom;
use serenity::{ use serenity::{
builder::{CreateMessage, EditMessage}, builder::{CreateMessage, EditMessage},
@ -17,9 +11,24 @@ use serenity::{
model::channel::Message, model::channel::Message,
utils::MessageBuilder, utils::MessageBuilder,
}; };
use std::{str::FromStr, sync::Arc};
use db::{OsuLastBeatmap, OsuSavedUsers, OsuUser, OsuUserBests};
use embeds::{beatmap_embed, score_embed, user_embed};
use hook::SHORT_LINK_REGEX;
pub use hook::{dot_osu_hook, hook};
use server_rank::{SERVER_RANK_COMMAND, SHOW_LEADERBOARD_COMMAND};
use youmubot_prelude::announcer::AnnouncerHandler;
use youmubot_prelude::{stream::FuturesUnordered, *}; use youmubot_prelude::{stream::FuturesUnordered, *};
use crate::{
discord::beatmap_cache::BeatmapMetaCache,
discord::display::ScoreListStyle,
discord::oppai_cache::{BeatmapCache, BeatmapInfo},
models::{Beatmap, Mode, Mods, Score, User},
request::{BeatmapRequestKind, UserID},
Client as OsuHttpClient,
};
mod announcer; mod announcer;
pub(crate) mod beatmap_cache; pub(crate) mod beatmap_cache;
mod cache; mod cache;
@ -30,12 +39,6 @@ mod hook;
pub(crate) mod oppai_cache; pub(crate) mod oppai_cache;
mod server_rank; mod server_rank;
use db::{OsuLastBeatmap, OsuSavedUsers, OsuUser, OsuUserBests};
use embeds::{beatmap_embed, score_embed, user_embed};
use hook::SHORT_LINK_REGEX;
pub use hook::{dot_osu_hook, hook};
use server_rank::{SERVER_RANK_COMMAND, SHOW_LEADERBOARD_COMMAND};
/// The osu! client. /// The osu! client.
pub(crate) struct OsuClient; pub(crate) struct OsuClient;
@ -43,6 +46,30 @@ impl TypeMapKey for OsuClient {
type Value = Arc<OsuHttpClient>; type Value = Arc<OsuHttpClient>;
} }
/// The environment for osu! app commands.
#[derive(Clone)]
pub struct OsuEnv {
pub(crate) prelude: Env,
// databases
pub(crate) saved_users: OsuSavedUsers,
pub(crate) last_beatmaps: OsuLastBeatmap,
pub(crate) user_bests: OsuUserBests,
// clients
pub(crate) client: Arc<OsuHttpClient>,
pub(crate) oppai: BeatmapCache,
pub(crate) beatmaps: BeatmapMetaCache,
}
impl std::fmt::Debug for OsuEnv {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<osu::Env>")
}
}
impl TypeMapKey for OsuEnv {
type Value = OsuEnv;
}
/// Sets up the osu! command handling section. /// Sets up the osu! command handling section.
/// ///
/// This automatically enables: /// This automatically enables:
@ -55,50 +82,58 @@ impl TypeMapKey for OsuClient {
/// - Hooks. Hooks are completely opt-in. /// - Hooks. Hooks are completely opt-in.
/// ///
pub async fn setup( pub async fn setup(
_path: &std::path::Path,
data: &mut TypeMap, data: &mut TypeMap,
prelude: youmubot_prelude::Env,
announcers: &mut AnnouncerHandler, announcers: &mut AnnouncerHandler,
) -> CommandResult { ) -> Result<OsuEnv> {
let sql_client = data.get::<SQLClient>().unwrap().clone();
// Databases // Databases
data.insert::<OsuSavedUsers>(OsuSavedUsers::new(sql_client.clone())); let saved_users = OsuSavedUsers::new(prelude.sql.clone());
data.insert::<OsuLastBeatmap>(OsuLastBeatmap::new(sql_client.clone())); let last_beatmaps = OsuLastBeatmap::new(prelude.sql.clone());
data.insert::<OsuUserBests>(OsuUserBests::new(sql_client.clone())); let user_bests = OsuUserBests::new(prelude.sql.clone());
// API client // API client
let http_client = data.get::<HTTPClient>().unwrap().clone(); let osu_client = Arc::new(
let mk_osu_client = || async { OsuHttpClient::new(
Arc::new( std::env::var("OSU_API_CLIENT_ID")
OsuHttpClient::new( .expect("Please set OSU_API_CLIENT_ID as osu! api v2 client ID.")
std::env::var("OSU_API_CLIENT_ID") .parse()
.expect("Please set OSU_API_CLIENT_ID as osu! api v2 client ID.") .expect("client_id should be u64"),
.parse() std::env::var("OSU_API_CLIENT_SECRET")
.expect("client_id should be u64"), .expect("Please set OSU_API_CLIENT_SECRET as osu! api v2 client secret."),
std::env::var("OSU_API_CLIENT_SECRET")
.expect("Please set OSU_API_CLIENT_SECRET as osu! api v2 client secret."),
)
.await
.expect("osu! should be initialized"),
) )
}; .await
let osu_client = mk_osu_client().await; .expect("osu! should be initialized"),
data.insert::<OsuClient>(osu_client.clone()); );
data.insert::<oppai_cache::BeatmapCache>(oppai_cache::BeatmapCache::new( let oppai_cache = BeatmapCache::new(prelude.http.clone(), prelude.sql.clone());
http_client.clone(), let beatmap_cache = BeatmapMetaCache::new(osu_client.clone(), prelude.sql.clone());
sql_client.clone(),
));
data.insert::<beatmap_cache::BeatmapMetaCache>(beatmap_cache::BeatmapMetaCache::new(
osu_client.clone(),
sql_client,
));
// Announcer // Announcer
let osu_client = mk_osu_client().await;
announcers.add( announcers.add(
announcer::ANNOUNCER_KEY, announcer::ANNOUNCER_KEY,
announcer::Announcer::new(osu_client), announcer::Announcer::new(osu_client.clone()),
); );
Ok(())
// Legacy data
data.insert::<OsuLastBeatmap>(last_beatmaps.clone());
data.insert::<OsuSavedUsers>(saved_users.clone());
data.insert::<OsuUserBests>(user_bests.clone());
data.insert::<OsuClient>(osu_client.clone());
data.insert::<BeatmapCache>(oppai_cache.clone());
data.insert::<BeatmapMetaCache>(beatmap_cache.clone());
let env = OsuEnv {
prelude,
saved_users,
last_beatmaps,
user_bests,
client: osu_client,
oppai: oppai_cache,
beatmaps: beatmap_cache,
};
data.insert::<OsuEnv>(env.clone());
Ok(env)
} }
#[group] #[group]
@ -128,7 +163,8 @@ struct Osu;
#[usage = "[username or user_id = your saved username]"] #[usage = "[username or user_id = your saved username]"]
#[max_args(1)] #[max_args(1)]
pub async fn std(ctx: &Context, msg: &Message, args: Args) -> CommandResult { pub async fn std(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
get_user(ctx, msg, args, Mode::Std).await let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
get_user(ctx, &env, msg, args, Mode::Std).await
} }
#[command] #[command]
@ -137,7 +173,8 @@ pub async fn std(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
#[usage = "[username or user_id = your saved username]"] #[usage = "[username or user_id = your saved username]"]
#[max_args(1)] #[max_args(1)]
pub async fn taiko(ctx: &Context, msg: &Message, args: Args) -> CommandResult { pub async fn taiko(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
get_user(ctx, msg, args, Mode::Taiko).await let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
get_user(ctx, &env, msg, args, Mode::Taiko).await
} }
#[command] #[command]
@ -146,7 +183,8 @@ pub async fn taiko(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
#[usage = "[username or user_id = your saved username]"] #[usage = "[username or user_id = your saved username]"]
#[max_args(1)] #[max_args(1)]
pub async fn catch(ctx: &Context, msg: &Message, args: Args) -> CommandResult { pub async fn catch(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
get_user(ctx, msg, args, Mode::Catch).await let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
get_user(ctx, &env, msg, args, Mode::Catch).await
} }
#[command] #[command]
@ -155,7 +193,8 @@ pub async fn catch(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
#[usage = "[username or user_id = your saved username]"] #[usage = "[username or user_id = your saved username]"]
#[max_args(1)] #[max_args(1)]
pub async fn mania(ctx: &Context, msg: &Message, args: Args) -> CommandResult { pub async fn mania(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
get_user(ctx, msg, args, Mode::Mania).await let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
get_user(ctx, &env, msg, args, Mode::Mania).await
} }
pub(crate) struct BeatmapWithMode(pub Beatmap, pub Mode); pub(crate) struct BeatmapWithMode(pub Beatmap, pub Mode);
@ -177,18 +216,18 @@ impl AsRef<Beatmap> for BeatmapWithMode {
#[usage = "[username or user_id]"] #[usage = "[username or user_id]"]
#[num_args(1)] #[num_args(1)]
pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let data = ctx.data.read().await; let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let osu = data.get::<OsuClient>().unwrap(); let osu_client = &env.client;
let user = args.single::<String>()?; let user = args.single::<String>()?;
let u = match osu.user(UserID::from_string(user), |f| f).await? { let u = match osu_client.user(UserID::from_string(user), |f| f).await? {
Some(u) => u, Some(u) => u,
None => { None => {
msg.reply(&ctx, "user not found...").await?; msg.reply(&ctx, "user not found...").await?;
return Ok(()); return Ok(());
} }
}; };
async fn find_score(client: &OsuHttpClient, u: &User) -> Result<Option<(models::Score, Mode)>> { async fn find_score(client: &OsuHttpClient, u: &User) -> Result<Option<(Score, Mode)>> {
for mode in &[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania] { for mode in &[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania] {
let scores = client let scores = client
.user_best(UserID::ID(u.id), |f| f.mode(*mode)) .user_best(UserID::ID(u.id), |f| f.mode(*mode))
@ -199,7 +238,7 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
} }
Ok(None) Ok(None)
} }
let (score, mode) = match find_score(osu, &u).await? { let (score, mode) = match find_score(osu_client, &u).await? {
Some(v) => v, Some(v) => v,
None => { None => {
msg.reply( msg.reply(
@ -220,19 +259,27 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
.any(|s| s.beatmap_id == map_id)) .any(|s| s.beatmap_id == map_id))
} }
let reply = msg.reply(&ctx, format!("To set your osu username, please make your most recent play be the following map: `/b/{}` in **{}** mode! It does **not** have to be a pass, and **NF** can be used! React to this message with 👌 within 5 minutes when you're done!", score.beatmap_id, mode.as_str_new_site())); let reply = msg.reply(
let beatmap = osu &ctx,
.beatmaps( format!(
crate::request::BeatmapRequestKind::Beatmap(score.beatmap_id), "To set your osu username, please make your most recent play \
|f| f.mode(mode, true), be the following map: `/b/{}` in **{}** mode! \
) It does **not** have to be a pass, and **NF** can be used! \
React to this message with 👌 within 5 minutes when you're done!",
score.beatmap_id,
mode.as_str_new_site()
),
);
let beatmap = osu_client
.beatmaps(BeatmapRequestKind::Beatmap(score.beatmap_id), |f| {
f.mode(mode, true)
})
.await? .await?
.into_iter() .into_iter()
.next() .next()
.unwrap(); .unwrap();
let info = data let info = env
.get::<BeatmapCache>() .oppai
.unwrap()
.get_beatmap(beatmap.beatmap_id) .get_beatmap(beatmap.beatmap_id)
.await? .await?
.get_possible_pp_with(mode, Mods::NOMOD)?; .get_possible_pp_with(mode, Mods::NOMOD)?;
@ -254,7 +301,7 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
.next() .next()
.await; .await;
if let Some(ur) = user_reaction { if let Some(ur) = user_reaction {
if check(osu, &u, score.beatmap_id).await? { if check(osu_client, &u, score.beatmap_id).await? {
break true; break true;
} }
ur.delete(&ctx).await?; ur.delete(&ctx).await?;
@ -268,7 +315,7 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
} }
let username = u.username.clone(); let username = u.username.clone();
add_user(msg.author.id, u, &data).await?; add_user(msg.author.id, u, &env).await?;
msg.reply( msg.reply(
&ctx, &ctx,
MessageBuilder::new() MessageBuilder::new()
@ -287,17 +334,19 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
#[delimiters(" ")] #[delimiters(" ")]
#[num_args(2)] #[num_args(2)]
pub async fn forcesave(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { pub async fn forcesave(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let data = ctx.data.read().await; let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let osu = data.get::<OsuClient>().unwrap();
let osu_client = &env.client;
let target = args.single::<UserId>()?.0; let target = args.single::<UserId>()?.0;
let username = args.quoted().trimmed().single::<String>()?; let username = args.quoted().trimmed().single::<String>()?;
let user: Option<User> = osu let user: Option<User> = osu_client
.user(UserID::from_string(username.clone()), |f| f) .user(UserID::from_string(username.clone()), |f| f)
.await?; .await?;
match user { match user {
Some(u) => { Some(u) => {
add_user(target, u, &data).await?; add_user(target, u, &env).await?;
msg.reply( msg.reply(
&ctx, &ctx,
MessageBuilder::new() MessageBuilder::new()
@ -314,11 +363,7 @@ pub async fn forcesave(ctx: &Context, msg: &Message, mut args: Args) -> CommandR
Ok(()) Ok(())
} }
async fn add_user( async fn add_user(target: serenity::model::id::UserId, user: User, env: &OsuEnv) -> Result<()> {
target: serenity::model::id::UserId,
user: models::User,
data: &TypeMap,
) -> Result<()> {
let u = OsuUser { let u = OsuUser {
user_id: target, user_id: target,
username: user.username.into(), username: user.username.into(),
@ -328,7 +373,7 @@ async fn add_user(
pp: [None, None, None, None], pp: [None, None, None, None],
std_weighted_map_length: None, std_weighted_map_length: None,
}; };
data.get::<OsuSavedUsers>().unwrap().new_user(u).await?; env.saved_users.new_user(u).await?;
Ok(()) Ok(())
} }
@ -349,7 +394,7 @@ impl FromStr for ModeArg {
async fn to_user_id_query( async fn to_user_id_query(
s: Option<UsernameArg>, s: Option<UsernameArg>,
data: &TypeMap, env: &OsuEnv,
msg: &Message, msg: &Message,
) -> Result<UserID, Error> { ) -> Result<UserID, Error> {
let id = match s { let id = match s {
@ -358,8 +403,7 @@ async fn to_user_id_query(
None => msg.author.id, None => msg.author.id,
}; };
data.get::<OsuSavedUsers>() env.saved_users
.unwrap()
.by_user_id(id) .by_user_id(id)
.await? .await?
.map(|u| UserID::ID(u.id)) .map(|u| UserID::ID(u.id))
@ -393,34 +437,37 @@ impl FromStr for Nth {
#[delimiters("/", " ")] #[delimiters("/", " ")]
#[max_args(4)] #[max_args(4)]
pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let data = ctx.data.read().await; let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let nth = args.single::<Nth>().unwrap_or(Nth::All); let nth = args.single::<Nth>().unwrap_or(Nth::All);
let style = args.single::<ScoreListStyle>().unwrap_or_default(); let style = args.single::<ScoreListStyle>().unwrap_or_default();
let mode = args.single::<ModeArg>().unwrap_or(ModeArg(Mode::Std)).0; let mode = args.single::<ModeArg>().unwrap_or(ModeArg(Mode::Std)).0;
let user = to_user_id_query( let user = to_user_id_query(
args.quoted().trimmed().single::<UsernameArg>().ok(), args.quoted().trimmed().single::<UsernameArg>().ok(),
&data, &env,
msg, msg,
) )
.await?; .await?;
let osu = data.get::<OsuClient>().unwrap(); let osu_client = &env.client;
let meta_cache = data.get::<BeatmapMetaCache>().unwrap();
let oppai = data.get::<BeatmapCache>().unwrap(); let user = osu_client
let user = osu
.user(user, |f| f.mode(mode)) .user(user, |f| f.mode(mode))
.await? .await?
.ok_or_else(|| Error::msg("User not found"))?; .ok_or_else(|| Error::msg("User not found"))?;
match nth { match nth {
Nth::Nth(nth) => { Nth::Nth(nth) => {
let recent_play = osu let recent_play = osu_client
.user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(nth)) .user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(nth))
.await? .await?
.into_iter() .into_iter()
.last() .last()
.ok_or_else(|| Error::msg("No such play"))?; .ok_or_else(|| Error::msg("No such play"))?;
let beatmap = meta_cache.get_beatmap(recent_play.beatmap_id, mode).await?; let beatmap = env
let content = oppai.get_beatmap(beatmap.beatmap_id).await?; .beatmaps
.get_beatmap(recent_play.beatmap_id, mode)
.await?;
let content = env.oppai.get_beatmap(beatmap.beatmap_id).await?;
let beatmap_mode = BeatmapWithMode(beatmap, mode); let beatmap_mode = BeatmapWithMode(beatmap, mode);
msg.channel_id msg.channel_id
@ -434,10 +481,10 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
.await?; .await?;
// Save the beatmap... // Save the beatmap...
cache::save_beatmap(&data, msg.channel_id, &beatmap_mode).await?; cache::save_beatmap(&env, msg.channel_id, &beatmap_mode).await?;
} }
Nth::All => { Nth::All => {
let plays = osu let plays = osu_client
.user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50)) .user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50))
.await?; .await?;
style.display_scores(plays, mode, ctx, msg).await?; style.display_scores(plays, mode, ctx, msg).await?;
@ -447,9 +494,9 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
} }
/// Get beatmapset. /// Get beatmapset.
struct OptBeatmapset; struct OptBeatmapSet;
impl FromStr for OptBeatmapset { impl FromStr for OptBeatmapSet {
type Err = Error; type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
@ -462,11 +509,9 @@ impl FromStr for OptBeatmapset {
/// Load the mentioned beatmap from the given message. /// Load the mentioned beatmap from the given message.
pub(crate) async fn load_beatmap( pub(crate) async fn load_beatmap(
ctx: &Context, env: &OsuEnv,
msg: &Message, msg: &Message,
) -> Option<(BeatmapWithMode, Option<Mods>)> { ) -> Option<(BeatmapWithMode, Option<Mods>)> {
let data = ctx.data.read().await;
if let Some(replied) = &msg.referenced_message { if let Some(replied) = &msg.referenced_message {
// Try to look for a mention of the replied message. // Try to look for a mention of the replied message.
let beatmap_id = SHORT_LINK_REGEX.captures(&replied.content).or_else(|| { let beatmap_id = SHORT_LINK_REGEX.captures(&replied.content).or_else(|| {
@ -489,8 +534,8 @@ pub(crate) async fn load_beatmap(
let mods = caps let mods = caps
.name("mods") .name("mods")
.and_then(|m| m.as_str().parse::<Mods>().ok()); .and_then(|m| m.as_str().parse::<Mods>().ok());
let osu = data.get::<OsuClient>().unwrap(); let osu_client = &env.client;
let bms = osu let bms = osu_client
.beatmaps(BeatmapRequestKind::Beatmap(id), |f| f.maybe_mode(mode)) .beatmaps(BeatmapRequestKind::Beatmap(id), |f| f.maybe_mode(mode))
.await .await
.ok() .ok()
@ -499,7 +544,7 @@ pub(crate) async fn load_beatmap(
let bm_mode = beatmap.mode; let bm_mode = beatmap.mode;
let bm = BeatmapWithMode(beatmap, mode.unwrap_or(bm_mode)); let bm = BeatmapWithMode(beatmap, mode.unwrap_or(bm_mode));
// Store the beatmap in history // Store the beatmap in history
cache::save_beatmap(&data, msg.channel_id, &bm) cache::save_beatmap(&env, msg.channel_id, &bm)
.await .await
.pls_ok(); .pls_ok();
@ -508,7 +553,7 @@ pub(crate) async fn load_beatmap(
} }
} }
let b = cache::get_beatmap(&data, msg.channel_id) let b = cache::get_beatmap(&env, msg.channel_id)
.await .await
.ok() .ok()
.flatten(); .flatten();
@ -522,16 +567,16 @@ pub(crate) async fn load_beatmap(
#[delimiters(" ")] #[delimiters(" ")]
#[max_args(2)] #[max_args(2)]
pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let data = ctx.data.read().await; let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let b = load_beatmap(ctx, msg).await;
let beatmapset = args.find::<OptBeatmapset>().is_ok(); let b = load_beatmap(&env, msg).await;
let beatmapset = args.find::<OptBeatmapSet>().is_ok();
match b { match b {
Some((BeatmapWithMode(b, m), mods_def)) => { Some((BeatmapWithMode(b, m), mods_def)) => {
let mods = args.find::<Mods>().ok().or(mods_def).unwrap_or(Mods::NOMOD); let mods = args.find::<Mods>().ok().or(mods_def).unwrap_or(Mods::NOMOD);
if beatmapset { if beatmapset {
let beatmap_cache = data.get::<BeatmapMetaCache>().unwrap(); let beatmapset = env.beatmaps.get_beatmapset(b.beatmapset_id).await?;
let beatmapset = beatmap_cache.get_beatmapset(b.beatmapset_id).await?;
display::display_beatmapset( display::display_beatmapset(
ctx, ctx,
beatmapset, beatmapset,
@ -543,9 +588,8 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
.await?; .await?;
return Ok(()); return Ok(());
} }
let info = data let info = env
.get::<BeatmapCache>() .oppai
.unwrap()
.get_beatmap(b.beatmap_id) .get_beatmap(b.beatmap_id)
.await? .await?
.get_possible_pp_with(m, mods)?; .get_possible_pp_with(m, mods)?;
@ -574,8 +618,8 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
#[description = "Check your own or someone else's best record on the last beatmap. Also stores the result if possible."] #[description = "Check your own or someone else's best record on the last beatmap. Also stores the result if possible."]
#[max_args(3)] #[max_args(3)]
pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let data = ctx.data.read().await; let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let bm = load_beatmap(ctx, msg).await; let bm = load_beatmap(&env, msg).await;
let bm = match bm { let bm = match bm {
Some((bm, _)) => bm, Some((bm, _)) => bm,
@ -598,15 +642,15 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
None => Some(msg.author.id), None => Some(msg.author.id),
_ => None, _ => None,
}; };
let user = to_user_id_query(username_arg, &data, msg).await?; let user = to_user_id_query(username_arg, &env, msg).await?;
let osu = data.get::<OsuClient>().unwrap(); let osu_client = env.client;
let user = osu let user = osu_client
.user(user, |f| f) .user(user, |f| f)
.await? .await?
.ok_or_else(|| Error::msg("User not found"))?; .ok_or_else(|| Error::msg("User not found"))?;
let mut scores = osu let mut scores = osu_client
.scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(m)) .scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(m))
.await? .await?
.into_iter() .into_iter()
@ -625,8 +669,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
if let Some(user_id) = user_id { if let Some(user_id) = user_id {
// Save to database // Save to database
data.get::<OsuUserBests>() env.user_bests
.unwrap()
.save(user_id, m, scores.clone()) .save(user_id, m, scores.clone())
.await .await
.pls_ok(); .pls_ok();
@ -644,7 +687,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
#[example = "#2 / taiko / natsukagami"] #[example = "#2 / taiko / natsukagami"]
#[max_args(4)] #[max_args(4)]
pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let data = ctx.data.read().await; let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let nth = args.single::<Nth>().unwrap_or(Nth::All); let nth = args.single::<Nth>().unwrap_or(Nth::All);
let style = args.single::<ScoreListStyle>().unwrap_or_default(); let style = args.single::<ScoreListStyle>().unwrap_or_default();
let mode = args let mode = args
@ -652,19 +695,16 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
.map(|ModeArg(t)| t) .map(|ModeArg(t)| t)
.unwrap_or(Mode::Std); .unwrap_or(Mode::Std);
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &data, msg).await?; let user = to_user_id_query(args.single::<UsernameArg>().ok(), &env, msg).await?;
let meta_cache = data.get::<BeatmapMetaCache>().unwrap(); let osu_client = &env.client;
let osu = data.get::<OsuClient>().unwrap(); let user = osu_client
let oppai = data.get::<BeatmapCache>().unwrap();
let user = osu
.user(user, |f| f.mode(mode)) .user(user, |f| f.mode(mode))
.await? .await?
.ok_or_else(|| Error::msg("User not found"))?; .ok_or_else(|| Error::msg("User not found"))?;
match nth { match nth {
Nth::Nth(nth) => { Nth::Nth(nth) => {
let top_play = osu let top_play = osu_client
.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(nth)) .user_best(UserID::ID(user.id), |f| f.mode(mode).limit(nth))
.await?; .await?;
@ -674,8 +714,8 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
.into_iter() .into_iter()
.last() .last()
.ok_or_else(|| Error::msg("No such play"))?; .ok_or_else(|| Error::msg("No such play"))?;
let beatmap = meta_cache.get_beatmap(top_play.beatmap_id, mode).await?; let beatmap = env.beatmaps.get_beatmap(top_play.beatmap_id, mode).await?;
let content = oppai.get_beatmap(beatmap.beatmap_id).await?; let content = env.oppai.get_beatmap(beatmap.beatmap_id).await?;
let beatmap = BeatmapWithMode(beatmap, mode); let beatmap = BeatmapWithMode(beatmap, mode);
msg.channel_id msg.channel_id
@ -694,10 +734,10 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
.await?; .await?;
// Save the beatmap... // Save the beatmap...
cache::save_beatmap(&data, msg.channel_id, &beatmap).await?; cache::save_beatmap(&env, msg.channel_id, &beatmap).await?;
} }
Nth::All => { Nth::All => {
let plays = osu let plays = osu_client
.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100)) .user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100))
.await?; .await?;
style.display_scores(plays, mode, ctx, msg).await?; style.display_scores(plays, mode, ctx, msg).await?;
@ -712,34 +752,39 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
#[usage = "[--oppai to clear oppai cache as well]"] #[usage = "[--oppai to clear oppai cache as well]"]
#[max_args(1)] #[max_args(1)]
pub async fn clean_cache(ctx: &Context, msg: &Message, args: Args) -> CommandResult { pub async fn clean_cache(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let data = ctx.data.read().await; let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let meta_cache = data.get::<BeatmapMetaCache>().unwrap(); env.beatmaps.clear().await?;
meta_cache.clear().await?;
if args.remains() == Some("--oppai") { if args.remains() == Some("--oppai") {
let oppai = data.get::<BeatmapCache>().unwrap(); env.oppai.clear().await?;
oppai.clear().await?;
} }
msg.reply_ping(ctx, "Beatmap cache cleared!").await?; msg.reply_ping(ctx, "Beatmap cache cleared!").await?;
Ok(()) Ok(())
} }
async fn get_user(ctx: &Context, msg: &Message, mut args: Args, mode: Mode) -> CommandResult { async fn get_user(
let data = ctx.data.read().await; ctx: &Context,
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &data, msg).await?; env: &OsuEnv,
let osu = data.get::<OsuClient>().unwrap(); msg: &Message,
let cache = data.get::<BeatmapMetaCache>().unwrap(); mut args: Args,
let user = osu.user(user, |f| f.mode(mode)).await?; mode: Mode,
let oppai = data.get::<BeatmapCache>().unwrap(); ) -> CommandResult {
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &env, msg).await?;
let osu_client = &env.client;
let meta_cache = &env.beatmaps;
let user = osu_client.user(user, |f| f.mode(mode)).await?;
match user { match user {
Some(u) => { Some(u) => {
let bests = osu let bests = osu_client
.user_best(UserID::ID(u.id), |f| f.limit(100).mode(mode)) .user_best(UserID::ID(u.id), |f| f.limit(100).mode(mode))
.await?; .await?;
let map_length = calculate_weighted_map_length(&bests, cache, mode).await?; let map_length = calculate_weighted_map_length(&bests, meta_cache, mode).await?;
let best = match bests.into_iter().next() { let best = match bests.into_iter().next() {
Some(m) => { Some(m) => {
let beatmap = cache.get_beatmap(m.beatmap_id, mode).await?; let beatmap = meta_cache.get_beatmap(m.beatmap_id, mode).await?;
let info = oppai let info = env
.oppai
.get_beatmap(m.beatmap_id) .get_beatmap(m.beatmap_id)
.await? .await?
.get_info_with(mode, m.mods)?; .get_info_with(mode, m.mods)?;

View file

@ -1,15 +1,18 @@
use crate::{models::Mode, mods::Mods}; use std::io::Read;
use std::sync::Arc;
use osuparse::MetadataSection; use osuparse::MetadataSection;
use rosu_pp::catch::CatchDifficultyAttributes; use rosu_pp::catch::CatchDifficultyAttributes;
use rosu_pp::mania::ManiaDifficultyAttributes; use rosu_pp::mania::ManiaDifficultyAttributes;
use rosu_pp::osu::OsuDifficultyAttributes; use rosu_pp::osu::OsuDifficultyAttributes;
use rosu_pp::taiko::TaikoDifficultyAttributes; use rosu_pp::taiko::TaikoDifficultyAttributes;
use rosu_pp::{AttributeProvider, Beatmap, CatchPP, DifficultyAttributes, ManiaPP, OsuPP, TaikoPP}; use rosu_pp::{AttributeProvider, Beatmap, CatchPP, DifficultyAttributes, ManiaPP, OsuPP, TaikoPP};
use std::io::Read;
use std::sync::Arc;
use youmubot_db_sql::{models::osu as models, Pool}; use youmubot_db_sql::{models::osu as models, Pool};
use youmubot_prelude::*; use youmubot_prelude::*;
use crate::{models::Mode, mods::Mods};
/// the information collected from a download/Oppai request. /// the information collected from a download/Oppai request.
#[derive(Debug)] #[derive(Debug)]
pub struct BeatmapContent { pub struct BeatmapContent {
@ -37,7 +40,8 @@ impl BeatmapInfo {
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum Accuracy { pub enum Accuracy {
ByCount(u64, u64, u64, u64), // 300 / 100 / 50 / misses ByCount(u64, u64, u64, u64),
// 300 / 100 / 50 / misses
#[allow(dead_code)] #[allow(dead_code)]
ByValue(f64, u64), ByValue(f64, u64),
} }
@ -159,6 +163,7 @@ impl<'a> PPCalc<'a> for OsuPP<'a> {
self.calculate().difficulty self.calculate().difficulty
} }
} }
impl<'a> PPCalc<'a> for TaikoPP<'a> { impl<'a> PPCalc<'a> for TaikoPP<'a> {
type Attrs = TaikoDifficultyAttributes; type Attrs = TaikoDifficultyAttributes;
@ -193,6 +198,7 @@ impl<'a> PPCalc<'a> for TaikoPP<'a> {
self.calculate().difficulty self.calculate().difficulty
} }
} }
impl<'a> PPCalc<'a> for CatchPP<'a> { impl<'a> PPCalc<'a> for CatchPP<'a> {
type Attrs = CatchDifficultyAttributes; type Attrs = CatchDifficultyAttributes;
@ -227,6 +233,7 @@ impl<'a> PPCalc<'a> for CatchPP<'a> {
self.calculate().difficulty self.calculate().difficulty
} }
} }
impl<'a> PPCalc<'a> for ManiaPP<'a> { impl<'a> PPCalc<'a> for ManiaPP<'a> {
type Attrs = ManiaDifficultyAttributes; type Attrs = ManiaDifficultyAttributes;
@ -304,6 +311,7 @@ impl BeatmapContent {
} }
/// A central cache for the beatmaps. /// A central cache for the beatmaps.
#[derive(Debug, Clone)]
pub struct BeatmapCache { pub struct BeatmapCache {
client: ratelimit::Ratelimit<reqwest::Client>, client: ratelimit::Ratelimit<reqwest::Client>,
pool: Pool, pool: Pool,

View file

@ -15,15 +15,12 @@ use youmubot_prelude::{
}; };
use crate::{ use crate::{
discord::{ discord::{display::ScoreListStyle, oppai_cache::Accuracy},
display::ScoreListStyle,
oppai_cache::{Accuracy, BeatmapCache},
},
models::{Mode, Mods}, models::{Mode, Mods},
request::UserID, request::UserID,
}; };
use super::{db::OsuSavedUsers, ModeArg, OsuClient}; use super::{ModeArg, OsuEnv};
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
enum RankQuery { enum RankQuery {
@ -50,21 +47,23 @@ impl FromStr for RankQuery {
#[max_args(1)] #[max_args(1)]
#[only_in(guilds)] #[only_in(guilds)]
pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandResult {
let data = ctx.data.read().await; let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let mode = args let mode = args
.single::<RankQuery>() .single::<RankQuery>()
.unwrap_or(RankQuery::Mode(Mode::Std)); .unwrap_or(RankQuery::Mode(Mode::Std));
let guild = m.guild_id.expect("Guild-only command"); let guild = m.guild_id.expect("Guild-only command");
let member_cache = data.get::<MemberCache>().unwrap();
let osu_users = data let osu_users = env
.get::<OsuSavedUsers>() .saved_users
.unwrap()
.all() .all()
.await? .await?
.into_iter() .into_iter()
.map(|v| (v.user_id, v)) .map(|v| (v.user_id, v))
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
let users = member_cache
let users = env
.prelude
.members
.query_members(&ctx, guild) .query_members(&ctx, guild)
.await? .await?
.iter() .iter()
@ -102,7 +101,7 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR
return Ok(()); return Ok(());
} }
let users = std::sync::Arc::new(users); let users = Arc::new(users);
let last_update = last_update.unwrap(); let last_update = last_update.unwrap();
paginate_reply_fn( paginate_reply_fn(
move |page: u8, ctx: &Context, m: &mut Message| { move |page: u8, ctx: &Context, m: &mut Message| {
@ -197,7 +196,7 @@ impl Default for OrderBy {
} }
} }
impl std::str::FromStr for OrderBy { impl FromStr for OrderBy {
type Err = Error; type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
@ -215,55 +214,57 @@ impl std::str::FromStr for OrderBy {
#[description = "See the server's ranks on the last seen beatmap"] #[description = "See the server's ranks on the last seen beatmap"]
#[max_args(2)] #[max_args(2)]
#[only_in(guilds)] #[only_in(guilds)]
pub async fn show_leaderboard(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { pub async fn show_leaderboard(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let order = args.single::<OrderBy>().unwrap_or_default(); let order = args.single::<OrderBy>().unwrap_or_default();
let style = args.single::<ScoreListStyle>().unwrap_or_default(); let style = args.single::<ScoreListStyle>().unwrap_or_default();
let data = ctx.data.read().await; let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let member_cache = data.get::<MemberCache>().unwrap();
let (bm, _) = match super::load_beatmap(ctx, m).await { let (bm, _) = match super::load_beatmap(&env, msg).await {
Some((bm, mods_def)) => { Some((bm, mods_def)) => {
let mods = args.find::<Mods>().ok().or(mods_def).unwrap_or(Mods::NOMOD); let mods = args.find::<Mods>().ok().or(mods_def).unwrap_or(Mods::NOMOD);
(bm, mods) (bm, mods)
} }
None => { None => {
m.reply(&ctx, "No beatmap queried on this channel.").await?; msg.reply(&ctx, "No beatmap queried on this channel.")
.await?;
return Ok(()); return Ok(());
} }
}; };
let osu = data.get::<OsuClient>().unwrap().clone(); let osu_client = env.client.clone();
// Get oppai map. // Get oppai map.
let mode = bm.1; let mode = bm.1;
let oppai = data.get::<BeatmapCache>().unwrap(); let oppai = env.oppai;
let oppai_map = oppai.get_beatmap(bm.0.beatmap_id).await?; let oppai_map = oppai.get_beatmap(bm.0.beatmap_id).await?;
let guild = m.guild_id.expect("Guild-only command"); let guild = msg.guild_id.expect("Guild-only command");
let scores = { let scores = {
const NO_SCORES: &str = "No scores have been recorded for this beatmap."; const NO_SCORES: &str = "No scores have been recorded for this beatmap.";
// Signal that we are running. // Signal that we are running.
let running_reaction = m.react(&ctx, '⌛').await?; let running_reaction = msg.react(&ctx, '⌛').await?;
let osu_users = data let osu_users = env
.get::<OsuSavedUsers>() .saved_users
.unwrap()
.all() .all()
.await? .await?
.into_iter() .into_iter()
.map(|v| (v.user_id, v)) .map(|v| (v.user_id, v))
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
let mut scores = member_cache let mut scores = env
.prelude
.members
.query_members(&ctx, guild) .query_members(&ctx, guild)
.await? .await?
.iter() .iter()
.filter_map(|m| osu_users.get(&m.user.id).map(|ou| (m.distinct(), ou.id))) .filter_map(|m| osu_users.get(&m.user.id).map(|ou| (m.distinct(), ou.id)))
.map(|(mem, osu_id)| { .map(|(mem, osu_id)| {
osu.scores(bm.0.beatmap_id, move |f| { osu_client
f.user(UserID::ID(osu_id)).mode(bm.1) .scores(bm.0.beatmap_id, move |f| {
}) f.user(UserID::ID(osu_id)).mode(bm.1)
.map(|r| Some((mem, r.ok()?))) })
.map(|r| Some((mem, r.ok()?)))
}) })
.collect::<FuturesUnordered<_>>() .collect::<FuturesUnordered<_>>()
.filter_map(future::ready) .filter_map(future::ready)
@ -300,7 +301,7 @@ pub async fn show_leaderboard(ctx: &Context, m: &Message, mut args: Args) -> Com
running_reaction.delete(&ctx).await?; running_reaction.delete(&ctx).await?;
if scores.is_empty() { if scores.is_empty() {
m.reply(&ctx, NO_SCORES).await?; msg.reply(&ctx, NO_SCORES).await?;
return Ok(()); return Ok(());
} }
match order { match order {
@ -315,7 +316,7 @@ pub async fn show_leaderboard(ctx: &Context, m: &Message, mut args: Args) -> Com
}; };
if scores.is_empty() { if scores.is_empty() {
m.reply( msg.reply(
&ctx, &ctx,
"No scores have been recorded for this beatmap. Run `osu check` to scan for yours!", "No scores have been recorded for this beatmap. Run `osu check` to scan for yours!",
) )
@ -329,7 +330,7 @@ pub async fn show_leaderboard(ctx: &Context, m: &Message, mut args: Args) -> Com
scores.into_iter().map(|(_, _, a)| a).collect(), scores.into_iter().map(|(_, _, a)| a).collect(),
mode, mode,
ctx, ctx,
m, msg,
) )
.await?; .await?;
return Ok(()); return Ok(());
@ -409,7 +410,7 @@ pub async fn show_leaderboard(ctx: &Context, m: &Message, mut args: Args) -> Com
}) })
}, },
ctx, ctx,
m, msg,
std::time::Duration::from_secs(60), std::time::Duration::from_secs(60),
) )
.await?; .await?;

View file

@ -1,16 +1,19 @@
pub mod discord; use std::convert::TryInto;
pub mod models; use std::sync::Arc;
pub mod request;
use models::*; use models::*;
use request::builders::*; use request::builders::*;
use request::*; use request::*;
use std::convert::TryInto;
use youmubot_prelude::*; use youmubot_prelude::*;
pub mod discord;
pub mod models;
pub mod request;
/// 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.
#[derive(Clone)]
pub struct Client { pub struct Client {
rosu: rosu_v2::Osu, rosu: Arc<rosu_v2::Osu>,
} }
pub fn vec_try_into<U, T: std::convert::TryFrom<U>>(v: Vec<U>) -> Result<Vec<T>, T::Error> { pub fn vec_try_into<U, T: std::convert::TryFrom<U>>(v: Vec<U>) -> Result<Vec<T>, T::Error> {
@ -31,7 +34,9 @@ impl Client {
.client_secret(client_secret) .client_secret(client_secret)
.build() .build()
.await?; .await?;
Ok(Client { rosu }) Ok(Client {
rosu: Arc::new(rosu),
})
} }
pub async fn beatmaps( pub async fn beatmaps(

View file

@ -1,4 +1,5 @@
use crate::{AppData, MemberCache, Result}; use std::{collections::HashMap, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use futures_util::{ use futures_util::{
future::{join_all, ready, FutureExt}, future::{join_all, ready, FutureExt},
@ -18,9 +19,11 @@ use serenity::{
prelude::*, prelude::*,
utils::MessageBuilder, utils::MessageBuilder,
}; };
use std::{collections::HashMap, sync::Arc};
use youmubot_db::DB; use youmubot_db::DB;
use crate::{AppData, MemberCache, Result};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CacheAndHttp(Arc<Cache>, Arc<Http>); pub struct CacheAndHttp(Arc<Cache>, Arc<Http>);
@ -28,15 +31,19 @@ impl CacheAndHttp {
pub fn from_client(client: &Client) -> Self { pub fn from_client(client: &Client) -> Self {
Self(client.cache.clone(), client.http.clone()) Self(client.cache.clone(), client.http.clone())
} }
pub fn from_context(context: &Context) -> Self {
Self(context.cache.clone(), context.http.clone())
}
} }
impl CacheHttp for CacheAndHttp { impl CacheHttp for CacheAndHttp {
fn cache(&self) -> Option<&Arc<Cache>> {
Some(&self.0)
}
fn http(&self) -> &Http { fn http(&self) -> &Http {
&self.1 &self.1
} }
fn cache(&self) -> Option<&Arc<Cache>> {
Some(&self.0)
}
} }
/// A list of assigned channels for an announcer. /// A list of assigned channels for an announcer.
@ -94,23 +101,14 @@ impl MemberToChannels {
/// ///
/// This struct manages the list of all Announcers, firing them in a certain interval. /// This struct manages the list of all Announcers, firing them in a certain interval.
pub struct AnnouncerHandler { pub struct AnnouncerHandler {
cache_http: CacheAndHttp,
data: AppData,
announcers: HashMap<&'static str, RwLock<Box<dyn Announcer + Send + Sync>>>, announcers: HashMap<&'static str, RwLock<Box<dyn Announcer + Send + Sync>>>,
} }
// Querying for the AnnouncerHandler in the internal data returns a vec of keys.
impl TypeMapKey for AnnouncerHandler {
type Value = Vec<&'static str>;
}
/// Announcer-managing related. /// Announcer-managing related.
impl AnnouncerHandler { impl AnnouncerHandler {
/// Create a new instance of the handler. /// Create a new instance of the handler.
pub fn new(client: &serenity::Client) -> Self { pub fn new() -> Self {
Self { Self {
cache_http: CacheAndHttp(client.cache.clone(), client.http.clone()),
data: client.data.clone(),
announcers: HashMap::new(), announcers: HashMap::new(),
} }
} }
@ -136,10 +134,30 @@ impl AnnouncerHandler {
self self
} }
} }
pub fn run(self, client: &Client) -> AnnouncerRunner {
let runner = AnnouncerRunner {
cache_http: CacheAndHttp::from_client(client),
data: client.data.clone(),
announcers: self.announcers,
};
runner
}
}
pub struct AnnouncerRunner {
cache_http: CacheAndHttp,
data: AppData,
announcers: HashMap<&'static str, RwLock<Box<dyn Announcer + Send + Sync>>>,
}
// Querying for the AnnouncerRunner in the internal data returns a vec of keys.
impl TypeMapKey for AnnouncerRunner {
type Value = Vec<&'static str>;
} }
/// Execution-related. /// Execution-related.
impl AnnouncerHandler { impl AnnouncerRunner {
/// Collect the list of guilds and their respective channels, by the key of the announcer. /// Collect the list of guilds and their respective channels, by the key of the announcer.
async fn get_guilds(data: &AppData, key: &'static str) -> Result<Vec<(GuildId, ChannelId)>> { async fn get_guilds(data: &AppData, key: &'static str) -> Result<Vec<(GuildId, ChannelId)>> {
let data = AnnouncerChannels::open(&*data.read().await) let data = AnnouncerChannels::open(&*data.read().await)
@ -214,7 +232,7 @@ pub async fn list_announcers(ctx: &Context, m: &Message, _: Args) -> CommandResu
let guild_id = m.guild_id.unwrap(); let guild_id = m.guild_id.unwrap();
let data = &*ctx.data.read().await; let data = &*ctx.data.read().await;
let announcers = AnnouncerChannels::open(data); let announcers = AnnouncerChannels::open(data);
let channels = data.get::<AnnouncerHandler>().unwrap(); let channels = data.get::<AnnouncerRunner>().unwrap();
let channels = channels let channels = channels
.iter() .iter()
.filter_map(|&key| { .filter_map(|&key| {
@ -249,7 +267,7 @@ pub async fn list_announcers(ctx: &Context, m: &Message, _: Args) -> CommandResu
pub async fn register_announcer(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { pub async fn register_announcer(ctx: &Context, m: &Message, mut args: Args) -> CommandResult {
let data = ctx.data.read().await; let data = ctx.data.read().await;
let key = args.single::<String>()?; let key = args.single::<String>()?;
let keys = data.get::<AnnouncerHandler>().unwrap(); let keys = data.get::<AnnouncerRunner>().unwrap();
if !keys.contains(&&key[..]) { if !keys.contains(&&key[..]) {
m.reply( m.reply(
&ctx, &ctx,
@ -296,7 +314,7 @@ pub async fn register_announcer(ctx: &Context, m: &Message, mut args: Args) -> C
pub async fn remove_announcer(ctx: &Context, m: &Message, mut args: Args) -> CommandResult { pub async fn remove_announcer(ctx: &Context, m: &Message, mut args: Args) -> CommandResult {
let data = ctx.data.read().await; let data = ctx.data.read().await;
let key = args.single::<String>()?; let key = args.single::<String>()?;
let keys = data.get::<AnnouncerHandler>().unwrap(); let keys = data.get::<AnnouncerRunner>().unwrap();
if !keys.contains(&key.as_str()) { if !keys.contains(&key.as_str()) {
m.reply( m.reply(
&ctx, &ctx,

View file

@ -1,7 +1,24 @@
use std::sync::Arc;
/// Re-export the anyhow errors
pub use anyhow::{anyhow as error, bail, Error, Result};
/// Re-exporting async_trait helps with implementing Announcer.
pub use async_trait::async_trait;
/// Re-export useful future and stream utils
pub use futures_util::{future, stream, FutureExt, StreamExt, TryFutureExt, TryStreamExt};
/// Module `prelude` provides a sane set of default imports that can be used inside /// Module `prelude` provides a sane set of default imports that can be used inside
/// a Youmubot source file. /// a Youmubot source file.
pub use serenity::prelude::*; pub use serenity::prelude::*;
use std::sync::Arc; /// Re-export the spawn function
pub use tokio::spawn as spawn_future;
pub use announcer::{Announcer, AnnouncerRunner};
pub use args::{ChannelId, Duration, RoleId, UserId, UsernameArg};
pub use debugging_ok::OkPrint;
pub use flags::Flags;
pub use hook::Hook;
pub use member_cache::MemberCache;
pub use pagination::{paginate, paginate_fn, paginate_reply, paginate_reply_fn, Paginate};
pub mod announcer; pub mod announcer;
pub mod args; pub mod args;
@ -13,26 +30,6 @@ pub mod ratelimit;
pub mod setup; pub mod setup;
pub mod table_format; pub mod table_format;
pub use announcer::{Announcer, AnnouncerHandler};
pub use args::{ChannelId, Duration, RoleId, UserId, UsernameArg};
pub use flags::Flags;
pub use hook::Hook;
pub use member_cache::MemberCache;
pub use pagination::{paginate, paginate_fn, paginate_reply, paginate_reply_fn, Paginate};
/// Re-exporting async_trait helps with implementing Announcer.
pub use async_trait::async_trait;
/// Re-export the anyhow errors
pub use anyhow::{anyhow as error, bail, Error, Result};
pub use debugging_ok::OkPrint;
/// Re-export useful future and stream utils
pub use futures_util::{future, stream, FutureExt, StreamExt, TryFutureExt, TryStreamExt};
/// Re-export the spawn function
pub use tokio::spawn as spawn_future;
/// The global app data. /// The global app data.
pub type AppData = Arc<RwLock<TypeMap>>; pub type AppData = Arc<RwLock<TypeMap>>;
@ -50,8 +47,18 @@ impl TypeMapKey for SQLClient {
type Value = youmubot_db_sql::Pool; type Value = youmubot_db_sql::Pool;
} }
/// The created base environment.
#[derive(Debug, Clone)]
pub struct Env {
// clients
pub http: reqwest::Client,
pub sql: youmubot_db_sql::Pool,
pub members: Arc<MemberCache>,
// databases
// pub(crate) announcer_channels: announcer::AnnouncerChannels,
}
pub mod prelude_commands { pub mod prelude_commands {
use crate::announcer::ANNOUNCERCOMMANDS_GROUP;
use serenity::{ use serenity::{
framework::standard::{ framework::standard::{
macros::{command, group}, macros::{command, group},
@ -61,6 +68,8 @@ pub mod prelude_commands {
prelude::Context, prelude::Context,
}; };
use crate::announcer::ANNOUNCERCOMMANDS_GROUP;
#[group("Prelude")] #[group("Prelude")]
#[description = "All the commands that makes the base of Youmu"] #[description = "All the commands that makes the base of Youmu"]
#[commands(ping)] #[commands(ping)]

View file

@ -1,12 +1,14 @@
use std::ops::Deref;
/// Provides a simple ratelimit lock (that only works in tokio) /// Provides a simple ratelimit lock (that only works in tokio)
// use tokio::time:: // use tokio::time::
use std::time::Duration; use std::time::Duration;
use crate::Result;
use flume::{bounded as channel, Receiver, Sender}; use flume::{bounded as channel, Receiver, Sender};
use std::ops::Deref;
use crate::Result;
/// Holds the underlying `T` in a rate-limited way. /// Holds the underlying `T` in a rate-limited way.
#[derive(Debug, Clone)]
pub struct Ratelimit<T> { pub struct Ratelimit<T> {
inner: T, inner: T,
recv: Receiver<()>, recv: Receiver<()>,

View file

@ -1,6 +1,9 @@
use serenity::prelude::*;
use std::{path::Path, time::Duration}; use std::{path::Path, time::Duration};
use serenity::prelude::*;
use crate::Env;
/// Set up the prelude libraries. /// Set up the prelude libraries.
/// ///
/// Panics on failure: Youmubot should *NOT* attempt to continue when this function fails. /// Panics on failure: Youmubot should *NOT* attempt to continue when this function fails.
@ -8,8 +11,8 @@ pub async fn setup_prelude(
db_path: impl AsRef<Path>, db_path: impl AsRef<Path>,
sql_path: impl AsRef<Path>, sql_path: impl AsRef<Path>,
data: &mut TypeMap, data: &mut TypeMap,
) { ) -> Env {
// Setup the announcer DB. // Set up the announcer DB.
crate::announcer::AnnouncerChannels::insert_into( crate::announcer::AnnouncerChannels::insert_into(
data, data,
db_path.as_ref().join("announcers.yaml"), db_path.as_ref().join("announcers.yaml"),
@ -22,17 +25,25 @@ pub async fn setup_prelude(
.expect("SQL database set up"); .expect("SQL database set up");
// Set up the HTTP client. // Set up the HTTP client.
data.insert::<crate::HTTPClient>( let http_client = reqwest::ClientBuilder::new()
reqwest::ClientBuilder::new() .connect_timeout(Duration::from_secs(5))
.connect_timeout(Duration::from_secs(5)) .timeout(Duration::from_secs(60))
.timeout(Duration::from_secs(60)) .build()
.build() .expect("Build be able to build HTTP client");
.expect("Build be able to build HTTP client"), data.insert::<crate::HTTPClient>(http_client.clone());
);
// Set up the member cache. // Set up the member cache.
data.insert::<crate::MemberCache>(std::sync::Arc::new(crate::MemberCache::default())); let member_cache = std::sync::Arc::new(crate::MemberCache::default());
data.insert::<crate::MemberCache>(member_cache.clone());
// Set up the SQL client. // Set up the SQL client.
data.insert::<crate::SQLClient>(sql_pool); data.insert::<crate::SQLClient>(sql_pool.clone());
let env = Env {
http: http_client,
sql: sql_pool,
members: member_cache,
};
env
} }

View file

@ -9,37 +9,58 @@ use serenity::{
permissions::Permissions, permissions::Permissions,
}, },
}; };
use youmubot_prelude::announcer::AnnouncerHandler;
use youmubot_prelude::*; use youmubot_prelude::*;
struct Handler { struct Handler {
hooks: Vec<RwLock<Box<dyn Hook>>>, hooks: Vec<RwLock<Box<dyn Hook>>>,
ready_hooks: Vec<fn(&Context) -> CommandResult>,
} }
impl Handler { impl Handler {
fn new() -> Handler { fn new() -> Handler {
Handler { hooks: vec![] } Handler {
hooks: vec![],
ready_hooks: vec![],
}
} }
fn push_hook<T: Hook + 'static>(&mut self, f: T) { fn push_hook<T: Hook + 'static>(&mut self, f: T) {
self.hooks.push(RwLock::new(Box::new(f))); self.hooks.push(RwLock::new(Box::new(f)));
} }
fn push_ready_hook(&mut self, f: fn(&Context) -> CommandResult) {
self.ready_hooks.push(f);
}
}
/// Environment to be passed into the framework
#[derive(Debug, Clone)]
struct Env {
prelude: youmubot_prelude::Env,
#[cfg(feature = "osu")]
osu: youmubot_osu::discord::OsuEnv,
}
impl AsRef<youmubot_prelude::Env> for Env {
fn as_ref(&self) -> &youmubot_prelude::Env {
&self.prelude
}
}
impl AsRef<youmubot_osu::discord::OsuEnv> for Env {
fn as_ref(&self) -> &youmubot_osu::discord::OsuEnv {
&self.osu
}
}
impl TypeMapKey for Env {
type Value = Env;
} }
#[async_trait] #[async_trait]
impl EventHandler for Handler { impl EventHandler for Handler {
async fn ready(&self, ctx: Context, ready: gateway::Ready) {
// Start ReactionWatchers for community.
#[cfg(feature = "core")]
ctx.data
.read()
.await
.get::<youmubot_core::community::ReactionWatchers>()
.unwrap()
.init(&ctx)
.await;
println!("{} is connected!", ready.user.name);
}
async fn message(&self, ctx: Context, message: Message) { async fn message(&self, ctx: Context, message: Message) {
self.hooks self.hooks
.iter() .iter()
@ -57,6 +78,23 @@ impl EventHandler for Handler {
}) })
.await; .await;
} }
async fn ready(&self, ctx: Context, ready: gateway::Ready) {
// Start ReactionWatchers for community.
#[cfg(feature = "core")]
ctx.data
.read()
.await
.get::<youmubot_core::community::ReactionWatchers>()
.unwrap()
.init(&ctx)
.await;
println!("{} is connected!", ready.user.name);
for f in &self.ready_hooks {
f(&ctx).pls_ok();
}
}
} }
/// Returns whether the user has "MANAGE_MESSAGES" permission in the channel. /// Returns whether the user has "MANAGE_MESSAGES" permission in the channel.
@ -79,16 +117,70 @@ async fn main() {
} }
let mut handler = Handler::new(); let mut handler = Handler::new();
#[cfg(feature = "core")]
handler.push_ready_hook(youmubot_core::ready_hook);
// Set up hooks // Set up hooks
#[cfg(feature = "osu")] #[cfg(feature = "osu")]
handler.push_hook(youmubot_osu::discord::hook); {
#[cfg(feature = "osu")] handler.push_hook(youmubot_osu::discord::hook);
handler.push_hook(youmubot_osu::discord::dot_osu_hook); handler.push_hook(youmubot_osu::discord::dot_osu_hook);
}
#[cfg(feature = "codeforces")] #[cfg(feature = "codeforces")]
handler.push_hook(youmubot_cf::InfoHook); handler.push_hook(youmubot_cf::InfoHook);
// 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.");
// Data to be put into context
let mut data = TypeMap::new();
// Set up announcer handler
let mut announcers = AnnouncerHandler::new();
// Setup each package starting from the prelude.
let env = {
let db_path = var("DBPATH")
.map(std::path::PathBuf::from)
.unwrap_or_else(|e| {
println!("No DBPATH set up ({:?}), using `/data`", e);
std::path::PathBuf::from("/data")
});
let sql_path = var("SQLPATH")
.map(std::path::PathBuf::from)
.unwrap_or_else(|e| {
let res = db_path.join("youmubot.db");
println!("No SQLPATH set up ({:?}), using `{:?}`", e, res);
res
});
let prelude = setup::setup_prelude(&db_path, sql_path, &mut data).await;
// Setup core
#[cfg(feature = "core")]
youmubot_core::setup(&db_path, &mut data).expect("Setup db should succeed");
// osu!
#[cfg(feature = "osu")]
let osu = youmubot_osu::discord::setup(&mut data, prelude.clone(), &mut announcers)
.await
.expect("osu! is initialized");
// codeforces
#[cfg(feature = "codeforces")]
youmubot_cf::setup(&db_path, &mut data, &mut announcers).await;
Env {
prelude,
#[cfg(feature = "osu")]
osu,
}
};
data.insert::<Env>(env);
#[cfg(feature = "core")]
println!("Core enabled.");
#[cfg(feature = "osu")]
println!("osu! enabled.");
#[cfg(feature = "codeforces")]
println!("codeforces enabled.");
// Set up base framework // Set up base framework
let fw = setup_framework(&token[..]).await; let fw = setup_framework(&token[..]).await;
@ -105,60 +197,20 @@ async fn main() {
| GatewayIntents::DIRECT_MESSAGES | GatewayIntents::DIRECT_MESSAGES
| GatewayIntents::DIRECT_MESSAGE_REACTIONS; | GatewayIntents::DIRECT_MESSAGE_REACTIONS;
Client::builder(token, intents) Client::builder(token, intents)
.type_map(data)
.framework(fw) .framework(fw)
.event_handler(handler) .event_handler(handler)
.await .await
.unwrap() .unwrap()
}; };
// Set up announcer handler let announcers = announcers.run(&client);
let mut announcers = AnnouncerHandler::new(&client);
// Setup each package starting from the prelude.
{
let mut data = client.data.write().await;
let db_path = var("DBPATH")
.map(std::path::PathBuf::from)
.unwrap_or_else(|e| {
println!("No DBPATH set up ({:?}), using `/data`", e);
std::path::PathBuf::from("/data")
});
let sql_path = var("SQLPATH")
.map(std::path::PathBuf::from)
.unwrap_or_else(|e| {
let res = db_path.join("youmubot.db");
println!("No SQLPATH set up ({:?}), using `{:?}`", e, res);
res
});
youmubot_prelude::setup::setup_prelude(&db_path, sql_path, &mut data).await;
// Setup core
#[cfg(feature = "core")]
youmubot_core::setup(&db_path, &client, &mut data).expect("Setup db should succeed");
// osu!
#[cfg(feature = "osu")]
youmubot_osu::discord::setup(&db_path, &mut data, &mut announcers)
.await
.expect("osu! is initialized");
// codeforces
#[cfg(feature = "codeforces")]
youmubot_cf::setup(&db_path, &mut data, &mut announcers).await;
}
#[cfg(feature = "core")]
println!("Core enabled.");
#[cfg(feature = "osu")]
println!("osu! enabled.");
#[cfg(feature = "codeforces")]
println!("codeforces enabled.");
tokio::spawn(announcers.scan(std::time::Duration::from_secs(300))); tokio::spawn(announcers.scan(std::time::Duration::from_secs(300)));
println!("Starting..."); println!("Starting...");
if let Err(v) = client.start().await { if let Err(v) = client.start().await {
panic!("{}", v) panic!("{}", v)
} }
println!("Hello, world!");
} }
// Sets up a framework for a client // Sets up a framework for a client