mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-05-24 01:00:49 +00:00
osu: Introduce Env
and propagate it to other functions (#40)
This commit is contained in:
parent
94dc225b86
commit
1066f249b0
17 changed files with 598 additions and 433 deletions
|
@ -13,6 +13,7 @@ use serenity::{
|
|||
|
||||
use db::{CfSavedUsers, CfUser};
|
||||
pub use hook::InfoHook;
|
||||
use youmubot_prelude::announcer::AnnouncerHandler;
|
||||
use youmubot_prelude::table_format::table_formatting_unsafe;
|
||||
use youmubot_prelude::table_format::Align::{Left, Right};
|
||||
use youmubot_prelude::{
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
use serenity::{
|
||||
framework::standard::{
|
||||
help_commands, macros::help, Args, CommandGroup, CommandResult, HelpOptions,
|
||||
},
|
||||
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, *};
|
||||
|
||||
pub mod admin;
|
||||
|
@ -12,14 +17,9 @@ pub mod community;
|
|||
mod db;
|
||||
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.
|
||||
pub fn setup(
|
||||
path: &std::path::Path,
|
||||
client: &serenity::client::Client,
|
||||
data: &mut TypeMap,
|
||||
) -> serenity::framework::standard::CommandResult {
|
||||
db::SoftBans::insert_into(&mut *data, &path.join("soft_bans.yaml"))?;
|
||||
|
@ -29,18 +29,21 @@ pub fn setup(
|
|||
&path.join("roles.yaml"),
|
||||
)?;
|
||||
|
||||
// Create handler threads
|
||||
tokio::spawn(admin::watch_soft_bans(
|
||||
CacheAndHttp::from_client(client),
|
||||
client.data.clone(),
|
||||
));
|
||||
|
||||
// Start reaction handlers
|
||||
data.insert::<community::ReactionWatchers>(community::ReactionWatchers::new(&*data)?);
|
||||
|
||||
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
|
||||
#[help]
|
||||
pub async fn help(
|
||||
|
|
|
@ -15,16 +15,15 @@ use youmubot_prelude::stream::TryStreamExt;
|
|||
use youmubot_prelude::*;
|
||||
|
||||
use crate::{
|
||||
discord::beatmap_cache::BeatmapMetaCache,
|
||||
discord::cache::save_beatmap,
|
||||
discord::oppai_cache::{BeatmapCache, BeatmapContent},
|
||||
discord::oppai_cache::BeatmapContent,
|
||||
models::{Mode, Score, User, UserEventRank},
|
||||
request::UserID,
|
||||
Client as Osu,
|
||||
};
|
||||
|
||||
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};
|
||||
|
||||
/// osu! announcer's unique announcer key.
|
||||
|
@ -51,9 +50,8 @@ impl youmubot_prelude::Announcer for Announcer {
|
|||
) -> Result<()> {
|
||||
// For each user...
|
||||
let users = {
|
||||
let data = d.read().await;
|
||||
let data = data.get::<OsuSavedUsers>().unwrap();
|
||||
data.all().await?
|
||||
let env = d.read().await.get::<OsuEnv>().unwrap().clone();
|
||||
env.saved_users.all().await?
|
||||
};
|
||||
let now = chrono::Utc::now();
|
||||
users
|
||||
|
@ -198,13 +196,12 @@ impl Announcer {
|
|||
}
|
||||
|
||||
async fn std_weighted_map_length(ctx: &Context, u: &OsuUser) -> Result<f64> {
|
||||
let data = ctx.data.read().await;
|
||||
let client = data.get::<OsuClient>().unwrap().clone();
|
||||
let cache = data.get::<BeatmapMetaCache>().unwrap();
|
||||
let scores = client
|
||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||
let scores = env
|
||||
.client
|
||||
.user_best(UserID::ID(u.id), |f| f.mode(Mode::Std).limit(100))
|
||||
.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)> {
|
||||
let data = ctx.data.read().await;
|
||||
let cache = data.get::<BeatmapMetaCache>().unwrap();
|
||||
let oppai = data.get::<BeatmapCache>().unwrap();
|
||||
let beatmap = cache.get_beatmap_default(self.score.beatmap_id).await?;
|
||||
let content = oppai.get_beatmap(beatmap.beatmap_id).await?;
|
||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||
let beatmap = env
|
||||
.beatmaps
|
||||
.get_beatmap_default(self.score.beatmap_id)
|
||||
.await?;
|
||||
let content = env.oppai.get_beatmap(beatmap.beatmap_id).await?;
|
||||
Ok((BeatmapWithMode(beatmap, self.mode), content))
|
||||
}
|
||||
|
||||
|
@ -341,9 +339,10 @@ impl<'a> CollectedScore<'a> {
|
|||
}),
|
||||
)
|
||||
.await?;
|
||||
save_beatmap(&*ctx.data.read().await, channel, bm)
|
||||
.await
|
||||
.pls_ok();
|
||||
|
||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||
|
||||
save_beatmap(&env, channel, bm).await.pls_ok();
|
||||
Ok(m)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use youmubot_db_sql::{models::osu as models, Pool};
|
||||
use youmubot_prelude::*;
|
||||
|
||||
use crate::{
|
||||
models::{ApprovalStatus, Beatmap, Mode},
|
||||
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.
|
||||
/// Does not cache non-Ranked beatmaps.
|
||||
#[derive(Clone)]
|
||||
pub struct BeatmapMetaCache {
|
||||
client: Arc<Client>,
|
||||
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 {
|
||||
type Value = BeatmapMetaCache;
|
||||
}
|
||||
|
|
|
@ -1,29 +1,26 @@
|
|||
use super::db::OsuLastBeatmap;
|
||||
use super::BeatmapWithMode;
|
||||
use serenity::model::id::ChannelId;
|
||||
|
||||
use youmubot_prelude::*;
|
||||
|
||||
use super::{BeatmapWithMode, OsuEnv};
|
||||
|
||||
/// Save the beatmap into the server data storage.
|
||||
pub(crate) async fn save_beatmap(
|
||||
data: &TypeMap,
|
||||
env: &OsuEnv,
|
||||
channel_id: ChannelId,
|
||||
bm: &BeatmapWithMode,
|
||||
) -> Result<()> {
|
||||
data.get::<OsuLastBeatmap>()
|
||||
.unwrap()
|
||||
.save(channel_id, &bm.0, bm.1)
|
||||
.await?;
|
||||
env.last_beatmaps.save(channel_id, &bm.0, bm.1).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the last beatmap requested from this channel.
|
||||
pub(crate) async fn get_beatmap(
|
||||
data: &TypeMap,
|
||||
env: &OsuEnv,
|
||||
channel_id: ChannelId,
|
||||
) -> Result<Option<BeatmapWithMode>> {
|
||||
data.get::<OsuLastBeatmap>()
|
||||
.unwrap()
|
||||
env.last_beatmaps
|
||||
.by_channel(channel_id)
|
||||
.await
|
||||
.map(|v| v.map(|(bm, mode)| BeatmapWithMode(bm, mode)))
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
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 serenity::model::id::{ChannelId, UserId};
|
||||
|
||||
use youmubot_db_sql::{models::osu as models, models::osu_user as model, Pool};
|
||||
use youmubot_prelude::*;
|
||||
|
||||
use crate::models::{Beatmap, Mode, Score};
|
||||
|
||||
/// Save the user IDs.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OsuSavedUsers {
|
||||
pool: Pool,
|
||||
}
|
||||
|
@ -60,6 +62,7 @@ impl OsuSavedUsers {
|
|||
}
|
||||
|
||||
/// Save each channel's last requested beatmap.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OsuLastBeatmap(Pool);
|
||||
|
||||
impl TypeMapKey for OsuLastBeatmap {
|
||||
|
@ -99,6 +102,7 @@ impl OsuLastBeatmap {
|
|||
}
|
||||
|
||||
/// Save each channel's last requested beatmap.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OsuUserBests(Pool);
|
||||
|
||||
impl TypeMapKey for OsuUserBests {
|
||||
|
@ -188,14 +192,16 @@ impl From<model::OsuUser> for OsuUser {
|
|||
|
||||
#[allow(dead_code)]
|
||||
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 serenity::model::id::{ChannelId, UserId};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use youmubot_db::DB;
|
||||
|
||||
use crate::models::{Beatmap, Mode, Score};
|
||||
|
||||
pub type OsuSavedUsers = DB<HashMap<UserId, OsuUser>>;
|
||||
|
||||
/// An osu! saved user.
|
||||
|
|
|
@ -54,9 +54,7 @@ mod scores {
|
|||
|
||||
use youmubot_prelude::*;
|
||||
|
||||
use crate::discord::{
|
||||
cache::save_beatmap, BeatmapCache, BeatmapMetaCache, BeatmapWithMode,
|
||||
};
|
||||
use crate::discord::{cache::save_beatmap, BeatmapWithMode, OsuEnv};
|
||||
use crate::models::{Mode, Score};
|
||||
|
||||
pub async fn display_scores_grid<'a>(
|
||||
|
@ -88,19 +86,17 @@ mod scores {
|
|||
#[async_trait]
|
||||
impl pagination::Paginate for Paginate {
|
||||
async fn render(&mut self, page: u8, ctx: &Context, msg: &mut Message) -> Result<bool> {
|
||||
let data = ctx.data.read().await;
|
||||
let client = data.get::<crate::discord::OsuClient>().unwrap();
|
||||
let osu = data.get::<BeatmapMetaCache>().unwrap();
|
||||
let beatmap_cache = data.get::<BeatmapCache>().unwrap();
|
||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||
let page = page as usize;
|
||||
let score = &self.scores[page];
|
||||
|
||||
let hourglass = msg.react(ctx, '⌛').await?;
|
||||
let mode = self.mode;
|
||||
let beatmap = osu.get_beatmap(score.beatmap_id, mode).await?;
|
||||
let content = beatmap_cache.get_beatmap(beatmap.beatmap_id).await?;
|
||||
let beatmap = env.beatmaps.get_beatmap(score.beatmap_id, mode).await?;
|
||||
let content = env.oppai.get_beatmap(beatmap.beatmap_id).await?;
|
||||
let bm = BeatmapWithMode(beatmap, mode);
|
||||
let user = client
|
||||
let user = env
|
||||
.client
|
||||
.user(crate::request::UserID::ID(score.user_id), |f| f)
|
||||
.await?
|
||||
.ok_or_else(|| Error::msg("user not found"))?;
|
||||
|
@ -114,7 +110,7 @@ mod scores {
|
|||
}),
|
||||
)
|
||||
.await?;
|
||||
save_beatmap(&*ctx.data.read().await, msg.channel_id, &bm).await?;
|
||||
save_beatmap(&env, msg.channel_id, &bm).await?;
|
||||
|
||||
// End
|
||||
hourglass.delete(ctx).await?;
|
||||
|
@ -138,7 +134,7 @@ mod scores {
|
|||
use youmubot_prelude::*;
|
||||
|
||||
use crate::discord::oppai_cache::Accuracy;
|
||||
use crate::discord::{Beatmap, BeatmapCache, BeatmapInfo, BeatmapMetaCache};
|
||||
use crate::discord::{Beatmap, BeatmapInfo, OsuEnv};
|
||||
use crate::models::{Mode, Score};
|
||||
|
||||
pub async fn display_scores_table<'a>(
|
||||
|
@ -178,9 +174,10 @@ mod scores {
|
|||
#[async_trait]
|
||||
impl pagination::Paginate for Paginate {
|
||||
async fn render(&mut self, page: u8, ctx: &Context, msg: &mut Message) -> Result<bool> {
|
||||
let data = ctx.data.read().await;
|
||||
let osu = data.get::<BeatmapMetaCache>().unwrap();
|
||||
let beatmap_cache = data.get::<BeatmapCache>().unwrap();
|
||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||
|
||||
let meta_cache = &env.beatmaps;
|
||||
let oppai = &env.oppai;
|
||||
let page = page as usize;
|
||||
let start = page * ITEMS_PER_PAGE;
|
||||
let end = self.scores.len().min(start + ITEMS_PER_PAGE);
|
||||
|
@ -194,9 +191,9 @@ mod scores {
|
|||
let beatmaps = plays
|
||||
.iter()
|
||||
.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 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()
|
||||
};
|
||||
Ok((beatmap, info)) as Result<(Beatmap, Option<BeatmapInfo>)>
|
||||
|
@ -211,7 +208,7 @@ mod scores {
|
|||
match p.pp.map(|pp| format!("{:.2}", pp)) {
|
||||
Some(v) => Ok(v),
|
||||
None => {
|
||||
let b = beatmap_cache.get_beatmap(p.beatmap_id).await?;
|
||||
let b = oppai.get_beatmap(p.beatmap_id).await?;
|
||||
let r: Result<_> = Ok({
|
||||
b.get_pp_from(
|
||||
mode,
|
||||
|
@ -335,10 +332,9 @@ mod beatmapset {
|
|||
|
||||
use youmubot_prelude::*;
|
||||
|
||||
use crate::discord::OsuEnv;
|
||||
use crate::{
|
||||
discord::{
|
||||
cache::save_beatmap, oppai_cache::BeatmapInfoWithPP, BeatmapCache, BeatmapWithMode,
|
||||
},
|
||||
discord::{cache::save_beatmap, oppai_cache::BeatmapInfoWithPP, BeatmapWithMode},
|
||||
models::{Beatmap, Mode, Mods},
|
||||
};
|
||||
|
||||
|
@ -386,9 +382,9 @@ mod beatmapset {
|
|||
|
||||
impl Paginate {
|
||||
async fn get_beatmap_info(&self, ctx: &Context, b: &Beatmap) -> Result<BeatmapInfoWithPP> {
|
||||
let data = ctx.data.read().await;
|
||||
let cache = data.get::<BeatmapCache>().unwrap();
|
||||
cache
|
||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||
|
||||
env.oppai
|
||||
.get_beatmap(b.beatmap_id)
|
||||
.await
|
||||
.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())
|
||||
}
|
||||
|
||||
async fn render(
|
||||
&mut self,
|
||||
page: u8,
|
||||
ctx: &Context,
|
||||
m: &mut serenity::model::channel::Message,
|
||||
) -> Result<bool> {
|
||||
async fn render(&mut self, page: u8, ctx: &Context, msg: &mut Message) -> Result<bool> {
|
||||
let page = page as usize;
|
||||
if page == self.maps.len() {
|
||||
m.edit(
|
||||
msg.edit(
|
||||
ctx,
|
||||
EditMessage::new().embed(crate::discord::embeds::beatmapset_embed(
|
||||
&self.maps[..],
|
||||
|
@ -432,7 +423,7 @@ mod beatmapset {
|
|||
info
|
||||
}
|
||||
};
|
||||
m.edit(ctx,
|
||||
msg.edit(ctx,
|
||||
EditMessage::new().content(self.message.as_str()).embed(
|
||||
crate::discord::embeds::beatmap_embed(
|
||||
map,
|
||||
|
@ -451,9 +442,10 @@ mod beatmapset {
|
|||
),
|
||||
)
|
||||
.await?;
|
||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||
save_beatmap(
|
||||
&*ctx.data.read().await,
|
||||
m.channel_id,
|
||||
&env,
|
||||
msg.channel_id,
|
||||
&BeatmapWithMode(map.clone(), self.mode.unwrap_or(map.mode)),
|
||||
)
|
||||
.await
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
use crate::{
|
||||
discord::beatmap_cache::BeatmapMetaCache,
|
||||
discord::oppai_cache::{BeatmapCache, BeatmapInfoWithPP},
|
||||
models::{Beatmap, Mode, Mods},
|
||||
};
|
||||
use std::str::FromStr;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use serenity::{builder::CreateMessage, model::channel::Message, utils::MessageBuilder};
|
||||
use std::str::FromStr;
|
||||
|
||||
use youmubot_prelude::*;
|
||||
|
||||
use crate::discord::OsuEnv;
|
||||
use crate::{
|
||||
discord::oppai_cache::BeatmapInfoWithPP,
|
||||
models::{Beatmap, Mode, Mods},
|
||||
};
|
||||
|
||||
use super::embeds::beatmap_embed;
|
||||
|
||||
lazy_static! {
|
||||
|
@ -43,9 +46,9 @@ pub fn dot_osu_hook<'a>(
|
|||
let url = attachment.url.clone();
|
||||
|
||||
async move {
|
||||
let data = ctx.data.read().await;
|
||||
let oppai = data.get::<BeatmapCache>().unwrap();
|
||||
let (beatmap, _) = oppai.download_beatmap_from_url(&url).await.ok()?;
|
||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||
|
||||
let (beatmap, _) = env.oppai.download_beatmap_from_url(&url).await.ok()?;
|
||||
crate::discord::embeds::beatmap_offline_embed(
|
||||
&beatmap,
|
||||
Mode::from(beatmap.content.mode as u8), /*For now*/
|
||||
|
@ -68,9 +71,9 @@ pub fn dot_osu_hook<'a>(
|
|||
.map(|attachment| {
|
||||
let url = attachment.url.clone();
|
||||
async move {
|
||||
let data = ctx.data.read().await;
|
||||
let oppai = data.get::<BeatmapCache>().unwrap();
|
||||
let beatmaps = oppai.download_osz_from_url(&url).await.pls_ok()?;
|
||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||
|
||||
let beatmaps = env.oppai.download_osz_from_url(&url).await.pls_ok()?;
|
||||
Some(
|
||||
beatmaps
|
||||
.into_iter()
|
||||
|
@ -133,11 +136,10 @@ pub fn hook<'a>(
|
|||
.pls_ok();
|
||||
let mode = l.mode.unwrap_or(b.mode);
|
||||
let bm = super::BeatmapWithMode(*b, mode);
|
||||
crate::discord::cache::save_beatmap(
|
||||
&*ctx.data.read().await,
|
||||
msg.channel_id,
|
||||
&bm,
|
||||
)
|
||||
|
||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||
|
||||
crate::discord::cache::save_beatmap(&env, msg.channel_id, &bm)
|
||||
.await
|
||||
.pls_ok();
|
||||
}
|
||||
|
@ -174,8 +176,7 @@ fn handle_old_links<'a>(
|
|||
.captures_iter(content)
|
||||
.map(move |capture| async move {
|
||||
let data = ctx.data.read().await;
|
||||
let cache = data.get::<BeatmapCache>().unwrap();
|
||||
let osu = data.get::<BeatmapMetaCache>().unwrap();
|
||||
let env = data.get::<OsuEnv>().unwrap();
|
||||
let req_type = capture.name("link_type").unwrap().as_str();
|
||||
let mode = capture
|
||||
.name("mode")
|
||||
|
@ -192,10 +193,18 @@ fn handle_old_links<'a>(
|
|||
});
|
||||
let beatmaps = match req_type {
|
||||
"b" => vec![match mode {
|
||||
Some(mode) => osu.get_beatmap(capture["id"].parse()?, mode).await?,
|
||||
None => osu.get_beatmap_default(capture["id"].parse()?).await?,
|
||||
Some(mode) => {
|
||||
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!(),
|
||||
};
|
||||
if beatmaps.is_empty() {
|
||||
|
@ -211,7 +220,7 @@ fn handle_old_links<'a>(
|
|||
.unwrap_or(Mods::NOMOD);
|
||||
let info = {
|
||||
let mode = mode.unwrap_or(b.mode);
|
||||
cache
|
||||
env.oppai
|
||||
.get_beatmap(b.beatmap_id)
|
||||
.await
|
||||
.and_then(|b| b.get_possible_pp_with(mode, mods))?
|
||||
|
@ -233,13 +242,10 @@ fn handle_old_links<'a>(
|
|||
})
|
||||
.collect::<stream::FuturesUnordered<_>>()
|
||||
.filter_map(|v| {
|
||||
future::ready(match v {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
future::ready(v.unwrap_or_else(|e| {
|
||||
eprintln!("{}", e);
|
||||
None
|
||||
}
|
||||
})
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -250,20 +256,23 @@ fn handle_new_links<'a>(
|
|||
NEW_LINK_REGEX
|
||||
.captures_iter(content)
|
||||
.map(|capture| async move {
|
||||
let data = ctx.data.read().await;
|
||||
let osu = data.get::<BeatmapMetaCache>().unwrap();
|
||||
let cache = data.get::<BeatmapCache>().unwrap();
|
||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||
let mode = capture
|
||||
.name("mode")
|
||||
.and_then(|v| Mode::parse_from_new_site(v.as_str()));
|
||||
let link = capture.get(0).unwrap().as_str();
|
||||
let beatmaps = match capture.name("beatmap_id") {
|
||||
Some(ref v) => vec![match mode {
|
||||
Some(mode) => osu.get_beatmap(v.as_str().parse()?, mode).await?,
|
||||
None => osu.get_beatmap_default(v.as_str().parse()?).await?,
|
||||
Some(mode) => env.beatmaps.get_beatmap(v.as_str().parse()?, mode).await?,
|
||||
None => {
|
||||
env.beatmaps
|
||||
.get_beatmap_default(v.as_str().parse()?)
|
||||
.await?
|
||||
}
|
||||
}],
|
||||
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?
|
||||
}
|
||||
};
|
||||
|
@ -280,7 +289,7 @@ fn handle_new_links<'a>(
|
|||
.unwrap_or(Mods::NOMOD);
|
||||
let info = {
|
||||
let mode = mode.unwrap_or(beatmap.mode);
|
||||
cache
|
||||
env.oppai
|
||||
.get_beatmap(beatmap.beatmap_id)
|
||||
.await
|
||||
.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"));
|
||||
}
|
||||
}
|
||||
let data = ctx.data.read().await;
|
||||
let osu = data.get::<BeatmapMetaCache>().unwrap();
|
||||
let cache = data.get::<BeatmapCache>().unwrap();
|
||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||
let mode = capture
|
||||
.name("mode")
|
||||
.and_then(|v| Mode::parse_from_new_site(v.as_str()));
|
||||
let id: u64 = capture.name("id").unwrap().as_str().parse()?;
|
||||
let beatmap = match mode {
|
||||
Some(mode) => osu.get_beatmap(id, mode).await,
|
||||
None => osu.get_beatmap_default(id).await,
|
||||
Some(mode) => env.beatmaps.get_beatmap(id, mode).await,
|
||||
None => env.beatmaps.get_beatmap_default(id).await,
|
||||
}?;
|
||||
let mods = capture
|
||||
.name("mods")
|
||||
|
@ -345,7 +352,7 @@ fn handle_short_links<'a>(
|
|||
.unwrap_or(Mods::NOMOD);
|
||||
let info = {
|
||||
let mode = mode.unwrap_or(beatmap.mode);
|
||||
cache
|
||||
env.oppai
|
||||
.get_beatmap(beatmap.beatmap_id)
|
||||
.await
|
||||
.and_then(|b| b.get_possible_pp_with(mode, mods))?
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
use crate::{
|
||||
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 std::{str::FromStr, sync::Arc};
|
||||
|
||||
use rand::seq::IteratorRandom;
|
||||
use serenity::{
|
||||
builder::{CreateMessage, EditMessage},
|
||||
|
@ -17,9 +11,24 @@ use serenity::{
|
|||
model::channel::Message,
|
||||
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 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;
|
||||
pub(crate) mod beatmap_cache;
|
||||
mod cache;
|
||||
|
@ -30,12 +39,6 @@ mod hook;
|
|||
pub(crate) mod oppai_cache;
|
||||
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.
|
||||
pub(crate) struct OsuClient;
|
||||
|
||||
|
@ -43,6 +46,30 @@ impl TypeMapKey for OsuClient {
|
|||
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.
|
||||
///
|
||||
/// This automatically enables:
|
||||
|
@ -55,20 +82,17 @@ impl TypeMapKey for OsuClient {
|
|||
/// - Hooks. Hooks are completely opt-in.
|
||||
///
|
||||
pub async fn setup(
|
||||
_path: &std::path::Path,
|
||||
data: &mut TypeMap,
|
||||
prelude: youmubot_prelude::Env,
|
||||
announcers: &mut AnnouncerHandler,
|
||||
) -> CommandResult {
|
||||
let sql_client = data.get::<SQLClient>().unwrap().clone();
|
||||
) -> Result<OsuEnv> {
|
||||
// Databases
|
||||
data.insert::<OsuSavedUsers>(OsuSavedUsers::new(sql_client.clone()));
|
||||
data.insert::<OsuLastBeatmap>(OsuLastBeatmap::new(sql_client.clone()));
|
||||
data.insert::<OsuUserBests>(OsuUserBests::new(sql_client.clone()));
|
||||
let saved_users = OsuSavedUsers::new(prelude.sql.clone());
|
||||
let last_beatmaps = OsuLastBeatmap::new(prelude.sql.clone());
|
||||
let user_bests = OsuUserBests::new(prelude.sql.clone());
|
||||
|
||||
// API client
|
||||
let http_client = data.get::<HTTPClient>().unwrap().clone();
|
||||
let mk_osu_client = || async {
|
||||
Arc::new(
|
||||
let osu_client = Arc::new(
|
||||
OsuHttpClient::new(
|
||||
std::env::var("OSU_API_CLIENT_ID")
|
||||
.expect("Please set OSU_API_CLIENT_ID as osu! api v2 client ID.")
|
||||
|
@ -79,26 +103,37 @@ pub async fn setup(
|
|||
)
|
||||
.await
|
||||
.expect("osu! should be initialized"),
|
||||
)
|
||||
};
|
||||
let osu_client = mk_osu_client().await;
|
||||
data.insert::<OsuClient>(osu_client.clone());
|
||||
data.insert::<oppai_cache::BeatmapCache>(oppai_cache::BeatmapCache::new(
|
||||
http_client.clone(),
|
||||
sql_client.clone(),
|
||||
));
|
||||
data.insert::<beatmap_cache::BeatmapMetaCache>(beatmap_cache::BeatmapMetaCache::new(
|
||||
osu_client.clone(),
|
||||
sql_client,
|
||||
));
|
||||
);
|
||||
let oppai_cache = BeatmapCache::new(prelude.http.clone(), prelude.sql.clone());
|
||||
let beatmap_cache = BeatmapMetaCache::new(osu_client.clone(), prelude.sql.clone());
|
||||
|
||||
// Announcer
|
||||
let osu_client = mk_osu_client().await;
|
||||
announcers.add(
|
||||
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]
|
||||
|
@ -128,7 +163,8 @@ struct Osu;
|
|||
#[usage = "[username or user_id = your saved username]"]
|
||||
#[max_args(1)]
|
||||
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]
|
||||
|
@ -137,7 +173,8 @@ pub async fn std(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|||
#[usage = "[username or user_id = your saved username]"]
|
||||
#[max_args(1)]
|
||||
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]
|
||||
|
@ -146,7 +183,8 @@ pub async fn taiko(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|||
#[usage = "[username or user_id = your saved username]"]
|
||||
#[max_args(1)]
|
||||
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]
|
||||
|
@ -155,7 +193,8 @@ pub async fn catch(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|||
#[usage = "[username or user_id = your saved username]"]
|
||||
#[max_args(1)]
|
||||
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);
|
||||
|
@ -177,18 +216,18 @@ impl AsRef<Beatmap> for BeatmapWithMode {
|
|||
#[usage = "[username or user_id]"]
|
||||
#[num_args(1)]
|
||||
pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let data = ctx.data.read().await;
|
||||
let osu = data.get::<OsuClient>().unwrap();
|
||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||
let osu_client = &env.client;
|
||||
|
||||
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,
|
||||
None => {
|
||||
msg.reply(&ctx, "user not found...").await?;
|
||||
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] {
|
||||
let scores = client
|
||||
.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)
|
||||
}
|
||||
let (score, mode) = match find_score(osu, &u).await? {
|
||||
let (score, mode) = match find_score(osu_client, &u).await? {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
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))
|
||||
}
|
||||
|
||||
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 beatmap = osu
|
||||
.beatmaps(
|
||||
crate::request::BeatmapRequestKind::Beatmap(score.beatmap_id),
|
||||
|f| f.mode(mode, true),
|
||||
)
|
||||
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 beatmap = osu_client
|
||||
.beatmaps(BeatmapRequestKind::Beatmap(score.beatmap_id), |f| {
|
||||
f.mode(mode, true)
|
||||
})
|
||||
.await?
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
let info = data
|
||||
.get::<BeatmapCache>()
|
||||
.unwrap()
|
||||
let info = env
|
||||
.oppai
|
||||
.get_beatmap(beatmap.beatmap_id)
|
||||
.await?
|
||||
.get_possible_pp_with(mode, Mods::NOMOD)?;
|
||||
|
@ -254,7 +301,7 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
.next()
|
||||
.await;
|
||||
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;
|
||||
}
|
||||
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();
|
||||
add_user(msg.author.id, u, &data).await?;
|
||||
add_user(msg.author.id, u, &env).await?;
|
||||
msg.reply(
|
||||
&ctx,
|
||||
MessageBuilder::new()
|
||||
|
@ -287,17 +334,19 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
#[delimiters(" ")]
|
||||
#[num_args(2)]
|
||||
pub async fn forcesave(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let data = ctx.data.read().await;
|
||||
let osu = data.get::<OsuClient>().unwrap();
|
||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||
|
||||
let osu_client = &env.client;
|
||||
|
||||
let target = args.single::<UserId>()?.0;
|
||||
|
||||
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)
|
||||
.await?;
|
||||
match user {
|
||||
Some(u) => {
|
||||
add_user(target, u, &data).await?;
|
||||
add_user(target, u, &env).await?;
|
||||
msg.reply(
|
||||
&ctx,
|
||||
MessageBuilder::new()
|
||||
|
@ -314,11 +363,7 @@ pub async fn forcesave(ctx: &Context, msg: &Message, mut args: Args) -> CommandR
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_user(
|
||||
target: serenity::model::id::UserId,
|
||||
user: models::User,
|
||||
data: &TypeMap,
|
||||
) -> Result<()> {
|
||||
async fn add_user(target: serenity::model::id::UserId, user: User, env: &OsuEnv) -> Result<()> {
|
||||
let u = OsuUser {
|
||||
user_id: target,
|
||||
username: user.username.into(),
|
||||
|
@ -328,7 +373,7 @@ async fn add_user(
|
|||
pp: [None, None, None, None],
|
||||
std_weighted_map_length: None,
|
||||
};
|
||||
data.get::<OsuSavedUsers>().unwrap().new_user(u).await?;
|
||||
env.saved_users.new_user(u).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -349,7 +394,7 @@ impl FromStr for ModeArg {
|
|||
|
||||
async fn to_user_id_query(
|
||||
s: Option<UsernameArg>,
|
||||
data: &TypeMap,
|
||||
env: &OsuEnv,
|
||||
msg: &Message,
|
||||
) -> Result<UserID, Error> {
|
||||
let id = match s {
|
||||
|
@ -358,8 +403,7 @@ async fn to_user_id_query(
|
|||
None => msg.author.id,
|
||||
};
|
||||
|
||||
data.get::<OsuSavedUsers>()
|
||||
.unwrap()
|
||||
env.saved_users
|
||||
.by_user_id(id)
|
||||
.await?
|
||||
.map(|u| UserID::ID(u.id))
|
||||
|
@ -393,34 +437,37 @@ impl FromStr for Nth {
|
|||
#[delimiters("/", " ")]
|
||||
#[max_args(4)]
|
||||
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 style = args.single::<ScoreListStyle>().unwrap_or_default();
|
||||
let mode = args.single::<ModeArg>().unwrap_or(ModeArg(Mode::Std)).0;
|
||||
let user = to_user_id_query(
|
||||
args.quoted().trimmed().single::<UsernameArg>().ok(),
|
||||
&data,
|
||||
&env,
|
||||
msg,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let osu = data.get::<OsuClient>().unwrap();
|
||||
let meta_cache = data.get::<BeatmapMetaCache>().unwrap();
|
||||
let oppai = data.get::<BeatmapCache>().unwrap();
|
||||
let user = osu
|
||||
let osu_client = &env.client;
|
||||
|
||||
let user = osu_client
|
||||
.user(user, |f| f.mode(mode))
|
||||
.await?
|
||||
.ok_or_else(|| Error::msg("User not found"))?;
|
||||
match 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))
|
||||
.await?
|
||||
.into_iter()
|
||||
.last()
|
||||
.ok_or_else(|| Error::msg("No such play"))?;
|
||||
let beatmap = meta_cache.get_beatmap(recent_play.beatmap_id, mode).await?;
|
||||
let content = oppai.get_beatmap(beatmap.beatmap_id).await?;
|
||||
let beatmap = env
|
||||
.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);
|
||||
|
||||
msg.channel_id
|
||||
|
@ -434,10 +481,10 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
|
|||
.await?;
|
||||
|
||||
// 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 => {
|
||||
let plays = osu
|
||||
let plays = osu_client
|
||||
.user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50))
|
||||
.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.
|
||||
struct OptBeatmapset;
|
||||
struct OptBeatmapSet;
|
||||
|
||||
impl FromStr for OptBeatmapset {
|
||||
impl FromStr for OptBeatmapSet {
|
||||
type Err = Error;
|
||||
|
||||
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.
|
||||
pub(crate) async fn load_beatmap(
|
||||
ctx: &Context,
|
||||
env: &OsuEnv,
|
||||
msg: &Message,
|
||||
) -> Option<(BeatmapWithMode, Option<Mods>)> {
|
||||
let data = ctx.data.read().await;
|
||||
|
||||
if let Some(replied) = &msg.referenced_message {
|
||||
// Try to look for a mention of the replied message.
|
||||
let beatmap_id = SHORT_LINK_REGEX.captures(&replied.content).or_else(|| {
|
||||
|
@ -489,8 +534,8 @@ pub(crate) async fn load_beatmap(
|
|||
let mods = caps
|
||||
.name("mods")
|
||||
.and_then(|m| m.as_str().parse::<Mods>().ok());
|
||||
let osu = data.get::<OsuClient>().unwrap();
|
||||
let bms = osu
|
||||
let osu_client = &env.client;
|
||||
let bms = osu_client
|
||||
.beatmaps(BeatmapRequestKind::Beatmap(id), |f| f.maybe_mode(mode))
|
||||
.await
|
||||
.ok()
|
||||
|
@ -499,7 +544,7 @@ pub(crate) async fn load_beatmap(
|
|||
let bm_mode = beatmap.mode;
|
||||
let bm = BeatmapWithMode(beatmap, mode.unwrap_or(bm_mode));
|
||||
// Store the beatmap in history
|
||||
cache::save_beatmap(&data, msg.channel_id, &bm)
|
||||
cache::save_beatmap(&env, msg.channel_id, &bm)
|
||||
.await
|
||||
.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
|
||||
.ok()
|
||||
.flatten();
|
||||
|
@ -522,16 +567,16 @@ pub(crate) async fn load_beatmap(
|
|||
#[delimiters(" ")]
|
||||
#[max_args(2)]
|
||||
pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let data = ctx.data.read().await;
|
||||
let b = load_beatmap(ctx, msg).await;
|
||||
let beatmapset = args.find::<OptBeatmapset>().is_ok();
|
||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||
|
||||
let b = load_beatmap(&env, msg).await;
|
||||
let beatmapset = args.find::<OptBeatmapSet>().is_ok();
|
||||
|
||||
match b {
|
||||
Some((BeatmapWithMode(b, m), mods_def)) => {
|
||||
let mods = args.find::<Mods>().ok().or(mods_def).unwrap_or(Mods::NOMOD);
|
||||
if beatmapset {
|
||||
let beatmap_cache = data.get::<BeatmapMetaCache>().unwrap();
|
||||
let beatmapset = beatmap_cache.get_beatmapset(b.beatmapset_id).await?;
|
||||
let beatmapset = env.beatmaps.get_beatmapset(b.beatmapset_id).await?;
|
||||
display::display_beatmapset(
|
||||
ctx,
|
||||
beatmapset,
|
||||
|
@ -543,9 +588,8 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
let info = data
|
||||
.get::<BeatmapCache>()
|
||||
.unwrap()
|
||||
let info = env
|
||||
.oppai
|
||||
.get_beatmap(b.beatmap_id)
|
||||
.await?
|
||||
.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."]
|
||||
#[max_args(3)]
|
||||
pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let data = ctx.data.read().await;
|
||||
let bm = load_beatmap(ctx, msg).await;
|
||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||
let bm = load_beatmap(&env, msg).await;
|
||||
|
||||
let bm = match 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,
|
||||
};
|
||||
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)
|
||||
.await?
|
||||
.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))
|
||||
.await?
|
||||
.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 {
|
||||
// Save to database
|
||||
data.get::<OsuUserBests>()
|
||||
.unwrap()
|
||||
env.user_bests
|
||||
.save(user_id, m, scores.clone())
|
||||
.await
|
||||
.pls_ok();
|
||||
|
@ -644,7 +687,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
|
|||
#[example = "#2 / taiko / natsukagami"]
|
||||
#[max_args(4)]
|
||||
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 style = args.single::<ScoreListStyle>().unwrap_or_default();
|
||||
let mode = args
|
||||
|
@ -652,19 +695,16 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
.map(|ModeArg(t)| t)
|
||||
.unwrap_or(Mode::Std);
|
||||
|
||||
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &data, msg).await?;
|
||||
let meta_cache = data.get::<BeatmapMetaCache>().unwrap();
|
||||
let osu = data.get::<OsuClient>().unwrap();
|
||||
|
||||
let oppai = data.get::<BeatmapCache>().unwrap();
|
||||
let user = osu
|
||||
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &env, msg).await?;
|
||||
let osu_client = &env.client;
|
||||
let user = osu_client
|
||||
.user(user, |f| f.mode(mode))
|
||||
.await?
|
||||
.ok_or_else(|| Error::msg("User not found"))?;
|
||||
|
||||
match 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))
|
||||
.await?;
|
||||
|
||||
|
@ -674,8 +714,8 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
.into_iter()
|
||||
.last()
|
||||
.ok_or_else(|| Error::msg("No such play"))?;
|
||||
let beatmap = meta_cache.get_beatmap(top_play.beatmap_id, mode).await?;
|
||||
let content = oppai.get_beatmap(beatmap.beatmap_id).await?;
|
||||
let beatmap = env.beatmaps.get_beatmap(top_play.beatmap_id, mode).await?;
|
||||
let content = env.oppai.get_beatmap(beatmap.beatmap_id).await?;
|
||||
let beatmap = BeatmapWithMode(beatmap, mode);
|
||||
|
||||
msg.channel_id
|
||||
|
@ -694,10 +734,10 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
.await?;
|
||||
|
||||
// Save the beatmap...
|
||||
cache::save_beatmap(&data, msg.channel_id, &beatmap).await?;
|
||||
cache::save_beatmap(&env, msg.channel_id, &beatmap).await?;
|
||||
}
|
||||
Nth::All => {
|
||||
let plays = osu
|
||||
let plays = osu_client
|
||||
.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100))
|
||||
.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]"]
|
||||
#[max_args(1)]
|
||||
pub async fn clean_cache(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||
let data = ctx.data.read().await;
|
||||
let meta_cache = data.get::<BeatmapMetaCache>().unwrap();
|
||||
meta_cache.clear().await?;
|
||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||
env.beatmaps.clear().await?;
|
||||
|
||||
if args.remains() == Some("--oppai") {
|
||||
let oppai = data.get::<BeatmapCache>().unwrap();
|
||||
oppai.clear().await?;
|
||||
env.oppai.clear().await?;
|
||||
}
|
||||
msg.reply_ping(ctx, "Beatmap cache cleared!").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_user(ctx: &Context, msg: &Message, mut args: Args, mode: Mode) -> CommandResult {
|
||||
let data = ctx.data.read().await;
|
||||
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &data, msg).await?;
|
||||
let osu = data.get::<OsuClient>().unwrap();
|
||||
let cache = data.get::<BeatmapMetaCache>().unwrap();
|
||||
let user = osu.user(user, |f| f.mode(mode)).await?;
|
||||
let oppai = data.get::<BeatmapCache>().unwrap();
|
||||
async fn get_user(
|
||||
ctx: &Context,
|
||||
env: &OsuEnv,
|
||||
msg: &Message,
|
||||
mut args: Args,
|
||||
mode: Mode,
|
||||
) -> 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 {
|
||||
Some(u) => {
|
||||
let bests = osu
|
||||
let bests = osu_client
|
||||
.user_best(UserID::ID(u.id), |f| f.limit(100).mode(mode))
|
||||
.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() {
|
||||
Some(m) => {
|
||||
let beatmap = cache.get_beatmap(m.beatmap_id, mode).await?;
|
||||
let info = oppai
|
||||
let beatmap = meta_cache.get_beatmap(m.beatmap_id, mode).await?;
|
||||
let info = env
|
||||
.oppai
|
||||
.get_beatmap(m.beatmap_id)
|
||||
.await?
|
||||
.get_info_with(mode, m.mods)?;
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
use crate::{models::Mode, mods::Mods};
|
||||
use std::io::Read;
|
||||
use std::sync::Arc;
|
||||
|
||||
use osuparse::MetadataSection;
|
||||
use rosu_pp::catch::CatchDifficultyAttributes;
|
||||
use rosu_pp::mania::ManiaDifficultyAttributes;
|
||||
use rosu_pp::osu::OsuDifficultyAttributes;
|
||||
use rosu_pp::taiko::TaikoDifficultyAttributes;
|
||||
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_prelude::*;
|
||||
|
||||
use crate::{models::Mode, mods::Mods};
|
||||
|
||||
/// the information collected from a download/Oppai request.
|
||||
#[derive(Debug)]
|
||||
pub struct BeatmapContent {
|
||||
|
@ -37,7 +40,8 @@ impl BeatmapInfo {
|
|||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Accuracy {
|
||||
ByCount(u64, u64, u64, u64), // 300 / 100 / 50 / misses
|
||||
ByCount(u64, u64, u64, u64),
|
||||
// 300 / 100 / 50 / misses
|
||||
#[allow(dead_code)]
|
||||
ByValue(f64, u64),
|
||||
}
|
||||
|
@ -159,6 +163,7 @@ impl<'a> PPCalc<'a> for OsuPP<'a> {
|
|||
self.calculate().difficulty
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PPCalc<'a> for TaikoPP<'a> {
|
||||
type Attrs = TaikoDifficultyAttributes;
|
||||
|
||||
|
@ -193,6 +198,7 @@ impl<'a> PPCalc<'a> for TaikoPP<'a> {
|
|||
self.calculate().difficulty
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PPCalc<'a> for CatchPP<'a> {
|
||||
type Attrs = CatchDifficultyAttributes;
|
||||
|
||||
|
@ -227,6 +233,7 @@ impl<'a> PPCalc<'a> for CatchPP<'a> {
|
|||
self.calculate().difficulty
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PPCalc<'a> for ManiaPP<'a> {
|
||||
type Attrs = ManiaDifficultyAttributes;
|
||||
|
||||
|
@ -304,6 +311,7 @@ impl BeatmapContent {
|
|||
}
|
||||
|
||||
/// A central cache for the beatmaps.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BeatmapCache {
|
||||
client: ratelimit::Ratelimit<reqwest::Client>,
|
||||
pool: Pool,
|
||||
|
|
|
@ -15,15 +15,12 @@ use youmubot_prelude::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
discord::{
|
||||
display::ScoreListStyle,
|
||||
oppai_cache::{Accuracy, BeatmapCache},
|
||||
},
|
||||
discord::{display::ScoreListStyle, oppai_cache::Accuracy},
|
||||
models::{Mode, Mods},
|
||||
request::UserID,
|
||||
};
|
||||
|
||||
use super::{db::OsuSavedUsers, ModeArg, OsuClient};
|
||||
use super::{ModeArg, OsuEnv};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum RankQuery {
|
||||
|
@ -50,21 +47,23 @@ impl FromStr for RankQuery {
|
|||
#[max_args(1)]
|
||||
#[only_in(guilds)]
|
||||
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
|
||||
.single::<RankQuery>()
|
||||
.unwrap_or(RankQuery::Mode(Mode::Std));
|
||||
let guild = m.guild_id.expect("Guild-only command");
|
||||
let member_cache = data.get::<MemberCache>().unwrap();
|
||||
let osu_users = data
|
||||
.get::<OsuSavedUsers>()
|
||||
.unwrap()
|
||||
|
||||
let osu_users = env
|
||||
.saved_users
|
||||
.all()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|v| (v.user_id, v))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let users = member_cache
|
||||
|
||||
let users = env
|
||||
.prelude
|
||||
.members
|
||||
.query_members(&ctx, guild)
|
||||
.await?
|
||||
.iter()
|
||||
|
@ -102,7 +101,7 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let users = std::sync::Arc::new(users);
|
||||
let users = Arc::new(users);
|
||||
let last_update = last_update.unwrap();
|
||||
paginate_reply_fn(
|
||||
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;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
|
@ -215,52 +214,54 @@ impl std::str::FromStr for OrderBy {
|
|||
#[description = "See the server's ranks on the last seen beatmap"]
|
||||
#[max_args(2)]
|
||||
#[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 style = args.single::<ScoreListStyle>().unwrap_or_default();
|
||||
|
||||
let data = ctx.data.read().await;
|
||||
let member_cache = data.get::<MemberCache>().unwrap();
|
||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||
|
||||
let (bm, _) = match super::load_beatmap(ctx, m).await {
|
||||
let (bm, _) = match super::load_beatmap(&env, msg).await {
|
||||
Some((bm, mods_def)) => {
|
||||
let mods = args.find::<Mods>().ok().or(mods_def).unwrap_or(Mods::NOMOD);
|
||||
(bm, mods)
|
||||
}
|
||||
None => {
|
||||
m.reply(&ctx, "No beatmap queried on this channel.").await?;
|
||||
msg.reply(&ctx, "No beatmap queried on this channel.")
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let osu = data.get::<OsuClient>().unwrap().clone();
|
||||
let osu_client = env.client.clone();
|
||||
|
||||
// Get oppai map.
|
||||
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 guild = m.guild_id.expect("Guild-only command");
|
||||
let guild = msg.guild_id.expect("Guild-only command");
|
||||
let scores = {
|
||||
const NO_SCORES: &str = "No scores have been recorded for this beatmap.";
|
||||
// Signal that we are running.
|
||||
let running_reaction = m.react(&ctx, '⌛').await?;
|
||||
let running_reaction = msg.react(&ctx, '⌛').await?;
|
||||
|
||||
let osu_users = data
|
||||
.get::<OsuSavedUsers>()
|
||||
.unwrap()
|
||||
let osu_users = env
|
||||
.saved_users
|
||||
.all()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|v| (v.user_id, v))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let mut scores = member_cache
|
||||
let mut scores = env
|
||||
.prelude
|
||||
.members
|
||||
.query_members(&ctx, guild)
|
||||
.await?
|
||||
.iter()
|
||||
.filter_map(|m| osu_users.get(&m.user.id).map(|ou| (m.distinct(), ou.id)))
|
||||
.map(|(mem, osu_id)| {
|
||||
osu.scores(bm.0.beatmap_id, move |f| {
|
||||
osu_client
|
||||
.scores(bm.0.beatmap_id, move |f| {
|
||||
f.user(UserID::ID(osu_id)).mode(bm.1)
|
||||
})
|
||||
.map(|r| Some((mem, r.ok()?)))
|
||||
|
@ -300,7 +301,7 @@ pub async fn show_leaderboard(ctx: &Context, m: &Message, mut args: Args) -> Com
|
|||
running_reaction.delete(&ctx).await?;
|
||||
|
||||
if scores.is_empty() {
|
||||
m.reply(&ctx, NO_SCORES).await?;
|
||||
msg.reply(&ctx, NO_SCORES).await?;
|
||||
return Ok(());
|
||||
}
|
||||
match order {
|
||||
|
@ -315,7 +316,7 @@ pub async fn show_leaderboard(ctx: &Context, m: &Message, mut args: Args) -> Com
|
|||
};
|
||||
|
||||
if scores.is_empty() {
|
||||
m.reply(
|
||||
msg.reply(
|
||||
&ctx,
|
||||
"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(),
|
||||
mode,
|
||||
ctx,
|
||||
m,
|
||||
msg,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
|
@ -409,7 +410,7 @@ pub async fn show_leaderboard(ctx: &Context, m: &Message, mut args: Args) -> Com
|
|||
})
|
||||
},
|
||||
ctx,
|
||||
m,
|
||||
msg,
|
||||
std::time::Duration::from_secs(60),
|
||||
)
|
||||
.await?;
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
pub mod discord;
|
||||
pub mod models;
|
||||
pub mod request;
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
|
||||
use models::*;
|
||||
use request::builders::*;
|
||||
use request::*;
|
||||
use std::convert::TryInto;
|
||||
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.
|
||||
#[derive(Clone)]
|
||||
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> {
|
||||
|
@ -31,7 +34,9 @@ impl Client {
|
|||
.client_secret(client_secret)
|
||||
.build()
|
||||
.await?;
|
||||
Ok(Client { rosu })
|
||||
Ok(Client {
|
||||
rosu: Arc::new(rosu),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn beatmaps(
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::{AppData, MemberCache, Result};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures_util::{
|
||||
future::{join_all, ready, FutureExt},
|
||||
|
@ -18,9 +19,11 @@ use serenity::{
|
|||
prelude::*,
|
||||
utils::MessageBuilder,
|
||||
};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use youmubot_db::DB;
|
||||
|
||||
use crate::{AppData, MemberCache, Result};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CacheAndHttp(Arc<Cache>, Arc<Http>);
|
||||
|
||||
|
@ -28,15 +31,19 @@ impl CacheAndHttp {
|
|||
pub fn from_client(client: &Client) -> Self {
|
||||
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 {
|
||||
fn cache(&self) -> Option<&Arc<Cache>> {
|
||||
Some(&self.0)
|
||||
}
|
||||
fn http(&self) -> &Http {
|
||||
&self.1
|
||||
}
|
||||
fn cache(&self) -> Option<&Arc<Cache>> {
|
||||
Some(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub struct AnnouncerHandler {
|
||||
cache_http: CacheAndHttp,
|
||||
data: AppData,
|
||||
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.
|
||||
impl AnnouncerHandler {
|
||||
/// Create a new instance of the handler.
|
||||
pub fn new(client: &serenity::Client) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
cache_http: CacheAndHttp(client.cache.clone(), client.http.clone()),
|
||||
data: client.data.clone(),
|
||||
announcers: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
@ -136,10 +134,30 @@ impl AnnouncerHandler {
|
|||
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.
|
||||
impl AnnouncerHandler {
|
||||
impl AnnouncerRunner {
|
||||
/// 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)>> {
|
||||
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 data = &*ctx.data.read().await;
|
||||
let announcers = AnnouncerChannels::open(data);
|
||||
let channels = data.get::<AnnouncerHandler>().unwrap();
|
||||
let channels = data.get::<AnnouncerRunner>().unwrap();
|
||||
let channels = channels
|
||||
.iter()
|
||||
.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 {
|
||||
let data = ctx.data.read().await;
|
||||
let key = args.single::<String>()?;
|
||||
let keys = data.get::<AnnouncerHandler>().unwrap();
|
||||
let keys = data.get::<AnnouncerRunner>().unwrap();
|
||||
if !keys.contains(&&key[..]) {
|
||||
m.reply(
|
||||
&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 {
|
||||
let data = ctx.data.read().await;
|
||||
let key = args.single::<String>()?;
|
||||
let keys = data.get::<AnnouncerHandler>().unwrap();
|
||||
let keys = data.get::<AnnouncerRunner>().unwrap();
|
||||
if !keys.contains(&key.as_str()) {
|
||||
m.reply(
|
||||
&ctx,
|
||||
|
|
|
@ -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
|
||||
/// a Youmubot source file.
|
||||
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 args;
|
||||
|
@ -13,26 +30,6 @@ pub mod ratelimit;
|
|||
pub mod setup;
|
||||
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.
|
||||
pub type AppData = Arc<RwLock<TypeMap>>;
|
||||
|
||||
|
@ -50,8 +47,18 @@ impl TypeMapKey for SQLClient {
|
|||
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 {
|
||||
use crate::announcer::ANNOUNCERCOMMANDS_GROUP;
|
||||
use serenity::{
|
||||
framework::standard::{
|
||||
macros::{command, group},
|
||||
|
@ -61,6 +68,8 @@ pub mod prelude_commands {
|
|||
prelude::Context,
|
||||
};
|
||||
|
||||
use crate::announcer::ANNOUNCERCOMMANDS_GROUP;
|
||||
|
||||
#[group("Prelude")]
|
||||
#[description = "All the commands that makes the base of Youmu"]
|
||||
#[commands(ping)]
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
use std::ops::Deref;
|
||||
/// Provides a simple ratelimit lock (that only works in tokio)
|
||||
// use tokio::time::
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::Result;
|
||||
use flume::{bounded as channel, Receiver, Sender};
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::Result;
|
||||
|
||||
/// Holds the underlying `T` in a rate-limited way.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Ratelimit<T> {
|
||||
inner: T,
|
||||
recv: Receiver<()>,
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
use serenity::prelude::*;
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
use serenity::prelude::*;
|
||||
|
||||
use crate::Env;
|
||||
|
||||
/// Set up the prelude libraries.
|
||||
///
|
||||
/// 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>,
|
||||
sql_path: impl AsRef<Path>,
|
||||
data: &mut TypeMap,
|
||||
) {
|
||||
// Setup the announcer DB.
|
||||
) -> Env {
|
||||
// Set up the announcer DB.
|
||||
crate::announcer::AnnouncerChannels::insert_into(
|
||||
data,
|
||||
db_path.as_ref().join("announcers.yaml"),
|
||||
|
@ -22,17 +25,25 @@ pub async fn setup_prelude(
|
|||
.expect("SQL database set up");
|
||||
|
||||
// Set up the HTTP client.
|
||||
data.insert::<crate::HTTPClient>(
|
||||
reqwest::ClientBuilder::new()
|
||||
let http_client = reqwest::ClientBuilder::new()
|
||||
.connect_timeout(Duration::from_secs(5))
|
||||
.timeout(Duration::from_secs(60))
|
||||
.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.
|
||||
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.
|
||||
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
|
||||
}
|
||||
|
|
|
@ -9,37 +9,58 @@ use serenity::{
|
|||
permissions::Permissions,
|
||||
},
|
||||
};
|
||||
|
||||
use youmubot_prelude::announcer::AnnouncerHandler;
|
||||
use youmubot_prelude::*;
|
||||
|
||||
struct Handler {
|
||||
hooks: Vec<RwLock<Box<dyn Hook>>>,
|
||||
ready_hooks: Vec<fn(&Context) -> CommandResult>,
|
||||
}
|
||||
|
||||
impl Handler {
|
||||
fn new() -> Handler {
|
||||
Handler { hooks: vec![] }
|
||||
Handler {
|
||||
hooks: vec![],
|
||||
ready_hooks: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn push_hook<T: Hook + 'static>(&mut self, f: T) {
|
||||
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]
|
||||
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) {
|
||||
self.hooks
|
||||
.iter()
|
||||
|
@ -57,6 +78,23 @@ impl EventHandler for Handler {
|
|||
})
|
||||
.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.
|
||||
|
@ -79,16 +117,70 @@ async fn main() {
|
|||
}
|
||||
|
||||
let mut handler = Handler::new();
|
||||
#[cfg(feature = "core")]
|
||||
handler.push_ready_hook(youmubot_core::ready_hook);
|
||||
// Set up hooks
|
||||
#[cfg(feature = "osu")]
|
||||
{
|
||||
handler.push_hook(youmubot_osu::discord::hook);
|
||||
#[cfg(feature = "osu")]
|
||||
handler.push_hook(youmubot_osu::discord::dot_osu_hook);
|
||||
}
|
||||
#[cfg(feature = "codeforces")]
|
||||
handler.push_hook(youmubot_cf::InfoHook);
|
||||
|
||||
// Collect the token
|
||||
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
|
||||
let fw = setup_framework(&token[..]).await;
|
||||
|
||||
|
@ -105,60 +197,20 @@ async fn main() {
|
|||
| GatewayIntents::DIRECT_MESSAGES
|
||||
| GatewayIntents::DIRECT_MESSAGE_REACTIONS;
|
||||
Client::builder(token, intents)
|
||||
.type_map(data)
|
||||
.framework(fw)
|
||||
.event_handler(handler)
|
||||
.await
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
// Set up announcer handler
|
||||
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.");
|
||||
|
||||
let announcers = announcers.run(&client);
|
||||
tokio::spawn(announcers.scan(std::time::Duration::from_secs(300)));
|
||||
|
||||
println!("Starting...");
|
||||
if let Err(v) = client.start().await {
|
||||
panic!("{}", v)
|
||||
}
|
||||
|
||||
println!("Hello, world!");
|
||||
}
|
||||
|
||||
// Sets up a framework for a client
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue