mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-19 00:38:54 +00:00
888 lines
27 KiB
Rust
888 lines
27 KiB
Rust
use std::{str::FromStr, sync::Arc};
|
|
|
|
use futures_util::join;
|
|
use interaction::{beatmap_components, score_components};
|
|
use rand::seq::IteratorRandom;
|
|
use serenity::{
|
|
builder::{CreateMessage, EditMessage},
|
|
collector,
|
|
framework::standard::{
|
|
macros::{command, group},
|
|
Args, CommandResult,
|
|
},
|
|
model::channel::Message,
|
|
utils::MessageBuilder,
|
|
};
|
|
|
|
use db::{OsuLastBeatmap, OsuSavedUsers, OsuUser, OsuUserBests};
|
|
use embeds::{beatmap_embed, score_embed, user_embed};
|
|
pub use hook::{dot_osu_hook, hook, score_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;
|
|
mod db;
|
|
pub(crate) mod display;
|
|
pub(crate) mod embeds;
|
|
mod hook;
|
|
pub mod interaction;
|
|
mod link_parser;
|
|
pub(crate) mod oppai_cache;
|
|
mod server_rank;
|
|
|
|
/// The osu! client.
|
|
pub(crate) struct OsuClient;
|
|
|
|
impl TypeMapKey for OsuClient {
|
|
type Value = Arc<crate::Client>;
|
|
}
|
|
|
|
/// 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<crate::Client>,
|
|
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:
|
|
/// - Related databases
|
|
/// - An announcer system (that will eventually be revamped)
|
|
/// - The osu! API client.
|
|
///
|
|
/// This does NOT automatically enable:
|
|
/// - Commands on the "osu" prefix
|
|
/// - Hooks. Hooks are completely opt-in.
|
|
pub async fn setup(
|
|
data: &mut TypeMap,
|
|
prelude: youmubot_prelude::Env,
|
|
announcers: &mut AnnouncerHandler,
|
|
) -> Result<OsuEnv> {
|
|
// Databases
|
|
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 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.")
|
|
.parse()
|
|
.expect("client_id should be u64"),
|
|
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"),
|
|
);
|
|
let oppai_cache = BeatmapCache::new(prelude.http.clone(), prelude.sql.clone());
|
|
let beatmap_cache = BeatmapMetaCache::new(osu_client.clone(), prelude.sql.clone());
|
|
|
|
// Announcer
|
|
announcers.add(
|
|
announcer::ANNOUNCER_KEY,
|
|
announcer::Announcer::new(osu_client.clone()),
|
|
);
|
|
|
|
// 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]
|
|
#[prefix = "osu"]
|
|
#[description = "osu! related commands."]
|
|
#[commands(
|
|
std,
|
|
taiko,
|
|
catch,
|
|
mania,
|
|
save,
|
|
forcesave,
|
|
recent,
|
|
last,
|
|
check,
|
|
top,
|
|
server_rank,
|
|
show_leaderboard,
|
|
clean_cache
|
|
)]
|
|
#[default_command(std)]
|
|
struct Osu;
|
|
|
|
#[command]
|
|
#[aliases("osu", "osu!")]
|
|
#[description = "Receive information about an user in osu!std mode."]
|
|
#[usage = "[username or user_id = your saved username]"]
|
|
#[max_args(1)]
|
|
pub async fn std(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
|
get_user(ctx, &env, msg, args, Mode::Std).await
|
|
}
|
|
|
|
#[command]
|
|
#[aliases("osu!taiko")]
|
|
#[description = "Receive information about an user in osu!taiko mode."]
|
|
#[usage = "[username or user_id = your saved username]"]
|
|
#[max_args(1)]
|
|
pub async fn taiko(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
|
get_user(ctx, &env, msg, args, Mode::Taiko).await
|
|
}
|
|
|
|
#[command]
|
|
#[aliases("fruits", "osu!catch", "ctb")]
|
|
#[description = "Receive information about an user in osu!catch mode."]
|
|
#[usage = "[username or user_id = your saved username]"]
|
|
#[max_args(1)]
|
|
pub async fn catch(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
|
get_user(ctx, &env, msg, args, Mode::Catch).await
|
|
}
|
|
|
|
#[command]
|
|
#[aliases("osu!mania")]
|
|
#[description = "Receive information about an user in osu!mania mode."]
|
|
#[usage = "[username or user_id = your saved username]"]
|
|
#[max_args(1)]
|
|
pub async fn mania(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
|
get_user(ctx, &env, msg, args, Mode::Mania).await
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) struct BeatmapWithMode(pub Beatmap, pub Mode);
|
|
|
|
impl BeatmapWithMode {
|
|
pub fn short_link(&self, mods: Mods) -> String {
|
|
self.0.short_link(Some(self.1), Some(mods))
|
|
}
|
|
|
|
fn mode(&self) -> Mode {
|
|
self.1
|
|
}
|
|
}
|
|
|
|
impl AsRef<Beatmap> for BeatmapWithMode {
|
|
fn as_ref(&self) -> &Beatmap {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
#[command]
|
|
#[description = "Save the given username as your username."]
|
|
#[usage = "[username or user_id]"]
|
|
#[num_args(1)]
|
|
pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
|
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
|
let osu_client = &env.client;
|
|
|
|
let user = args.single::<String>()?;
|
|
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<(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))
|
|
.await?;
|
|
if let Some(v) = scores.into_iter().choose(&mut rand::thread_rng()) {
|
|
return Ok(Some((v, *mode)));
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|
|
let (score, mode) = match find_score(osu_client, &u).await? {
|
|
Some(v) => v,
|
|
None => {
|
|
msg.reply(
|
|
&ctx,
|
|
"No plays found in this account! Play something first...!",
|
|
)
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
async fn check(client: &OsuHttpClient, u: &User, map_id: u64) -> Result<bool> {
|
|
Ok(client
|
|
.user_recent(UserID::ID(u.id), |f| f.mode(Mode::Std).limit(1))
|
|
.await?
|
|
.into_iter()
|
|
.take(1)
|
|
.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_client
|
|
.beatmaps(BeatmapRequestKind::Beatmap(score.beatmap_id), |f| {
|
|
f.mode(mode, true)
|
|
})
|
|
.await?
|
|
.into_iter()
|
|
.next()
|
|
.unwrap();
|
|
let info = env
|
|
.oppai
|
|
.get_beatmap(beatmap.beatmap_id)
|
|
.await?
|
|
.get_possible_pp_with(mode, Mods::NOMOD)?;
|
|
let mut reply = reply.await?;
|
|
reply
|
|
.edit(
|
|
&ctx,
|
|
EditMessage::new()
|
|
.embed(beatmap_embed(&beatmap, mode, Mods::NOMOD, info))
|
|
.components(vec![beatmap_components()]),
|
|
)
|
|
.await?;
|
|
let reaction = reply.react(&ctx, '👌').await?;
|
|
let completed = loop {
|
|
let emoji = reaction.emoji.clone();
|
|
let user_reaction = collector::ReactionCollector::new(ctx)
|
|
.message_id(reply.id)
|
|
.author_id(msg.author.id)
|
|
.filter(move |r| r.emoji == emoji)
|
|
.timeout(std::time::Duration::from_secs(300))
|
|
.next()
|
|
.await;
|
|
if let Some(ur) = user_reaction {
|
|
if check(osu_client, &u, score.beatmap_id).await? {
|
|
break true;
|
|
}
|
|
ur.delete(&ctx).await?;
|
|
} else {
|
|
break false;
|
|
}
|
|
};
|
|
if !completed {
|
|
reaction.delete(&ctx).await?;
|
|
return Ok(());
|
|
}
|
|
|
|
let username = u.username.clone();
|
|
add_user(msg.author.id, u, &env).await?;
|
|
msg.reply(
|
|
&ctx,
|
|
MessageBuilder::new()
|
|
.push("user has been set to ")
|
|
.push_mono_safe(username)
|
|
.build(),
|
|
)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[description = "Save the given username as someone's username."]
|
|
#[owners_only]
|
|
#[usage = "[ping user]/[username or user_id]"]
|
|
#[delimiters(" ")]
|
|
#[num_args(2)]
|
|
pub async fn forcesave(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
|
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_client
|
|
.user(&UserID::from_string(username.clone()), |f| f)
|
|
.await?;
|
|
match user {
|
|
Some(u) => {
|
|
add_user(target, u, &env).await?;
|
|
msg.reply(
|
|
&ctx,
|
|
MessageBuilder::new()
|
|
.push("user has been set to ")
|
|
.push_mono_safe(username)
|
|
.build(),
|
|
)
|
|
.await?;
|
|
}
|
|
None => {
|
|
msg.reply(&ctx, "user not found...").await?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn add_user(target: serenity::model::id::UserId, user: User, env: &OsuEnv) -> Result<()> {
|
|
let pp_fut = async {
|
|
[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania]
|
|
.into_iter()
|
|
.map(|mode| async move {
|
|
env.client
|
|
.user(&UserID::ID(user.id), |f| f.mode(mode))
|
|
.await
|
|
.unwrap_or_else(|err| {
|
|
eprintln!("{}", err);
|
|
None
|
|
})
|
|
.and_then(|u| u.pp)
|
|
})
|
|
.collect::<stream::FuturesOrdered<_>>()
|
|
.collect::<Vec<_>>()
|
|
.await
|
|
};
|
|
|
|
let std_weight_map_length_fut = async {
|
|
let scores = env
|
|
.client
|
|
.user_best(UserID::ID(user.id), |f| f.mode(Mode::Std).limit(100))
|
|
.await
|
|
.unwrap_or_else(|err| {
|
|
eprintln!("{}", err);
|
|
vec![]
|
|
});
|
|
|
|
calculate_weighted_map_length(&scores, &env.beatmaps, Mode::Std)
|
|
.await
|
|
.pls_ok()
|
|
};
|
|
|
|
let (pp, std_weight_map_length) = join!(pp_fut, std_weight_map_length_fut);
|
|
|
|
let u = OsuUser {
|
|
user_id: target,
|
|
username: user.username.into(),
|
|
id: user.id,
|
|
failures: 0,
|
|
last_update: chrono::Utc::now(),
|
|
pp: pp.try_into().unwrap(),
|
|
std_weighted_map_length: std_weight_map_length,
|
|
};
|
|
env.saved_users.new_user(u).await?;
|
|
Ok(())
|
|
}
|
|
|
|
struct ModeArg(Mode);
|
|
|
|
impl FromStr for ModeArg {
|
|
type Err = String;
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
Ok(ModeArg(match &s.to_lowercase()[..] {
|
|
"osu" | "std" => Mode::Std,
|
|
"taiko" | "osu!taiko" => Mode::Taiko,
|
|
"ctb" | "fruits" | "catch" | "osu!ctb" | "osu!catch" => Mode::Catch,
|
|
"osu!mania" | "mania" => Mode::Mania,
|
|
_ => return Err(format!("Unknown mode {}", s)),
|
|
}))
|
|
}
|
|
}
|
|
|
|
async fn to_user_id_query(
|
|
s: Option<UsernameArg>,
|
|
env: &OsuEnv,
|
|
author: serenity::all::UserId,
|
|
) -> Result<UserID, Error> {
|
|
let id = match s {
|
|
Some(UsernameArg::Raw(s)) => return Ok(UserID::from_string(s)),
|
|
Some(UsernameArg::Tagged(r)) => r,
|
|
None => author,
|
|
};
|
|
|
|
env.saved_users
|
|
.by_user_id(id)
|
|
.await?
|
|
.map(|u| UserID::ID(u.id))
|
|
.ok_or_else(|| Error::msg("No saved account found"))
|
|
}
|
|
|
|
enum Nth {
|
|
All,
|
|
Nth(u8),
|
|
}
|
|
|
|
impl FromStr for Nth {
|
|
type Err = Error;
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
if s == "--all" || s == "-a" || s == "##" {
|
|
Ok(Nth::All)
|
|
} else if !s.starts_with('#') {
|
|
Err(Error::msg("Not an order"))
|
|
} else {
|
|
let v = s.split_at("#".len()).1.parse()?;
|
|
Ok(Nth::Nth(v))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[command]
|
|
#[aliases("rs", "rc", "r")]
|
|
#[description = "Gets an user's recent play"]
|
|
#[usage = "#[the nth recent play = --all] / [style (table or grid) = --table] / [mode (std, taiko, mania, catch) = std] / [username / user id = your saved id]"]
|
|
#[example = "#1 / taiko / natsukagami"]
|
|
#[delimiters("/", " ")]
|
|
#[max_args(4)]
|
|
pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
|
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(),
|
|
&env,
|
|
msg.author.id,
|
|
)
|
|
.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 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 = 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
|
|
.send_message(
|
|
&ctx,
|
|
CreateMessage::new()
|
|
.content("Here is the play that you requested".to_string())
|
|
.embed(score_embed(&recent_play, &beatmap_mode, &content, &user).build())
|
|
.components(vec![score_components()])
|
|
.reference_message(msg),
|
|
)
|
|
.await?;
|
|
|
|
// Save the beatmap...
|
|
cache::save_beatmap(&env, msg.channel_id, &beatmap_mode).await?;
|
|
}
|
|
Nth::All => {
|
|
let plays = osu_client
|
|
.user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50))
|
|
.await?;
|
|
let reply = msg
|
|
.reply(
|
|
ctx,
|
|
format!("Here are the recent plays by `{}`!", user.username),
|
|
)
|
|
.await?;
|
|
style.display_scores(plays, mode, ctx, reply).await?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Get beatmapset.
|
|
struct OptBeatmapSet;
|
|
|
|
impl FromStr for OptBeatmapSet {
|
|
type Err = Error;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s {
|
|
"--set" | "-s" | "--beatmapset" => Ok(Self),
|
|
_ => Err(Error::msg("not opt beatmapset")),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Load the mentioned beatmap from the given message.
|
|
pub(crate) async fn load_beatmap(
|
|
env: &OsuEnv,
|
|
msg: &Message,
|
|
) -> Option<(BeatmapWithMode, Option<Mods>)> {
|
|
use link_parser::{parse_short_links, EmbedType};
|
|
if let Some(replied) = &msg.referenced_message {
|
|
async fn try_content(
|
|
env: &OsuEnv,
|
|
content: &str,
|
|
) -> Option<(BeatmapWithMode, Option<Mods>)> {
|
|
let tp = parse_short_links(env, content).next().await?;
|
|
match tp.embed {
|
|
EmbedType::Beatmap(b, _, mods) => {
|
|
let mode = tp.mode.unwrap_or(b.mode);
|
|
Some((BeatmapWithMode(*b, mode), Some(mods)))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
if let Some(v) = try_content(env, &replied.content).await {
|
|
return Some(v);
|
|
}
|
|
for embed in &replied.embeds {
|
|
if let Some(desc) = &embed.description {
|
|
if let Some(v) = try_content(env, desc).await {
|
|
return Some(v);
|
|
}
|
|
}
|
|
for field in &embed.fields {
|
|
if let Some(v) = try_content(env, &field.value).await {
|
|
return Some(v);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let b = cache::get_beatmap(&env, msg.channel_id)
|
|
.await
|
|
.ok()
|
|
.flatten();
|
|
b.map(|b| (b, None))
|
|
}
|
|
|
|
#[command]
|
|
#[aliases("map")]
|
|
#[description = "Show information from the last queried beatmap."]
|
|
#[usage = "[--set/-s/--beatmapset] / [mods = no mod]"]
|
|
#[delimiters(" ")]
|
|
#[max_args(2)]
|
|
pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
|
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 beatmapset = env.beatmaps.get_beatmapset(b.beatmapset_id).await?;
|
|
display::display_beatmapset(
|
|
ctx,
|
|
beatmapset,
|
|
None,
|
|
Some(mods),
|
|
msg,
|
|
"Here is the beatmapset you requested!",
|
|
)
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
let info = env
|
|
.oppai
|
|
.get_beatmap(b.beatmap_id)
|
|
.await?
|
|
.get_possible_pp_with(m, mods)?;
|
|
msg.channel_id
|
|
.send_message(
|
|
&ctx,
|
|
CreateMessage::new()
|
|
.content("Here is the beatmap you requested!")
|
|
.embed(beatmap_embed(&b, m, mods, info))
|
|
.components(vec![beatmap_components()])
|
|
.reference_message(msg),
|
|
)
|
|
.await?;
|
|
}
|
|
None => {
|
|
msg.reply(&ctx, "No beatmap was queried on this channel.")
|
|
.await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[aliases("c", "chk")]
|
|
#[usage = "[style (table or grid) = --table] / [username or tag = yourself] / [mods to filter]"]
|
|
#[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 env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
|
let bm = load_beatmap(&env, msg).await;
|
|
|
|
let bm = match bm {
|
|
Some((bm, _)) => bm,
|
|
None => {
|
|
msg.reply(&ctx, "No beatmap queried on this channel.")
|
|
.await?;
|
|
return Ok(());
|
|
}
|
|
};
|
|
let mode = bm.1;
|
|
let mods = args.find::<Mods>().ok().unwrap_or_default();
|
|
let style = args
|
|
.single::<ScoreListStyle>()
|
|
.unwrap_or(ScoreListStyle::Grid);
|
|
let username_arg = args.single::<UsernameArg>().ok();
|
|
let user = to_user_id_query(username_arg, &env, msg.author.id).await?;
|
|
|
|
let scores = do_check(&env, &bm, mods, &user).await?;
|
|
|
|
if scores.is_empty() {
|
|
msg.reply(&ctx, "No scores found").await?;
|
|
return Ok(());
|
|
}
|
|
let reply = msg
|
|
.reply(
|
|
&ctx,
|
|
format!(
|
|
"Here are the scores by `{}` on `{}`!",
|
|
&user,
|
|
bm.short_link(mods)
|
|
),
|
|
)
|
|
.await?;
|
|
style.display_scores(scores, mode, ctx, reply).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) async fn do_check(
|
|
env: &OsuEnv,
|
|
bm: &BeatmapWithMode,
|
|
mods: Mods,
|
|
user: &UserID,
|
|
) -> Result<Vec<Score>> {
|
|
let BeatmapWithMode(b, m) = bm;
|
|
|
|
let osu_client = &env.client;
|
|
|
|
let user = osu_client
|
|
.user(user, |f| f)
|
|
.await?
|
|
.ok_or_else(|| Error::msg("User not found"))?;
|
|
let mut scores = osu_client
|
|
.scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(*m))
|
|
.await?
|
|
.into_iter()
|
|
.filter(|s| s.mods.contains(mods))
|
|
.collect::<Vec<_>>();
|
|
scores.sort_by(|a, b| {
|
|
b.pp.unwrap_or(-1.0)
|
|
.partial_cmp(&a.pp.unwrap_or(-1.0))
|
|
.unwrap()
|
|
});
|
|
Ok(scores)
|
|
}
|
|
|
|
#[command]
|
|
#[aliases("t")]
|
|
#[description = "Get the n-th top record of an user."]
|
|
#[usage = "#[n-th = --all] / [style (table or grid) = --table] / [mode (std, taiko, catch, mania)] = std / [username or user_id = your saved user id]"]
|
|
#[example = "#2 / taiko / natsukagami"]
|
|
#[max_args(4)]
|
|
pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
|
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>()
|
|
.map(|ModeArg(t)| t)
|
|
.unwrap_or(Mode::Std);
|
|
|
|
let user_id = to_user_id_query(args.single::<UsernameArg>().ok(), &env, msg.author.id).await?;
|
|
let osu_client = &env.client;
|
|
let user = osu_client
|
|
.user(&user_id, |f| f.mode(mode))
|
|
.await?
|
|
.ok_or_else(|| Error::msg("User not found"))?;
|
|
|
|
match nth {
|
|
Nth::Nth(nth) => {
|
|
let top_play = osu_client
|
|
.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(nth))
|
|
.await?;
|
|
|
|
let rank = top_play.len() as u8;
|
|
|
|
let top_play = top_play
|
|
.into_iter()
|
|
.last()
|
|
.ok_or_else(|| Error::msg("No such play"))?;
|
|
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
|
|
.send_message(&ctx, {
|
|
CreateMessage::new()
|
|
.content(format!(
|
|
"{}: here is the play that you requested",
|
|
msg.author
|
|
))
|
|
.embed(
|
|
score_embed(&top_play, &beatmap, &content, &user)
|
|
.top_record(rank)
|
|
.build(),
|
|
)
|
|
.components(vec![score_components()])
|
|
})
|
|
.await?;
|
|
|
|
// Save the beatmap...
|
|
cache::save_beatmap(&env, msg.channel_id, &beatmap).await?;
|
|
}
|
|
Nth::All => {
|
|
let plays = osu_client
|
|
.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100))
|
|
.await?;
|
|
let reply = msg
|
|
.reply(&ctx, format!("Here are the top plays by `{}`!", user_id))
|
|
.await?;
|
|
style.display_scores(plays, mode, ctx, reply).await?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[command("cleancache")]
|
|
#[owners_only]
|
|
#[description = "Clean the beatmap cache."]
|
|
#[usage = "[--oppai to clear oppai cache as well]"]
|
|
#[max_args(1)]
|
|
pub async fn clean_cache(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
|
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
|
env.beatmaps.clear().await?;
|
|
|
|
if args.remains() == Some("--oppai") {
|
|
env.oppai.clear().await?;
|
|
}
|
|
msg.reply_ping(ctx, "Beatmap cache cleared!").await?;
|
|
Ok(())
|
|
}
|
|
|
|
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.author.id).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_client
|
|
.user_best(UserID::ID(u.id), |f| f.limit(100).mode(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 = 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)?;
|
|
Some((m, BeatmapWithMode(beatmap, mode), info))
|
|
}
|
|
None => None,
|
|
};
|
|
msg.channel_id
|
|
.send_message(
|
|
&ctx,
|
|
CreateMessage::new()
|
|
.content(format!(
|
|
"{}: here is the user that you requested",
|
|
msg.author
|
|
))
|
|
.embed(user_embed(u, map_length, best)),
|
|
)
|
|
.await?;
|
|
}
|
|
None => {
|
|
msg.reply(&ctx, "🔍 user not found!").await?;
|
|
}
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
pub(in crate::discord) async fn calculate_weighted_map_length(
|
|
from_scores: impl IntoIterator<Item = &Score>,
|
|
cache: &BeatmapMetaCache,
|
|
mode: Mode,
|
|
) -> Result<f64> {
|
|
from_scores
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(i, s)| async move {
|
|
let beatmap = cache.get_beatmap(s.beatmap_id, mode).await?;
|
|
const SCALING_FACTOR: f64 = 0.975;
|
|
Ok(beatmap
|
|
.difficulty
|
|
.apply_mods(s.mods, 0.0 /* dont care */)
|
|
.drain_length
|
|
.as_secs_f64()
|
|
* (SCALING_FACTOR.powi(i as i32)))
|
|
})
|
|
.collect::<FuturesUnordered<_>>()
|
|
.try_fold(0.0, |a, b| future::ready(Ok(a + b)))
|
|
.await
|
|
}
|