mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-20 01:08:55 +00:00
Merge remote-tracking branch 'origin/master' into async-youmu
This commit is contained in:
commit
bc888e816b
3 changed files with 229 additions and 11 deletions
|
@ -1,6 +1,6 @@
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
use crate::models::{Beatmap, Mode};
|
use crate::models::{Beatmap, Mode, Score};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serenity::model::id::{ChannelId, UserId};
|
use serenity::model::id::{ChannelId, UserId};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
@ -12,6 +12,10 @@ pub type OsuSavedUsers = DB<HashMap<UserId, OsuUser>>;
|
||||||
/// Save each channel's last requested beatmap.
|
/// Save each channel's last requested beatmap.
|
||||||
pub type OsuLastBeatmap = DB<HashMap<ChannelId, (Beatmap, Mode)>>;
|
pub type OsuLastBeatmap = DB<HashMap<ChannelId, (Beatmap, Mode)>>;
|
||||||
|
|
||||||
|
/// Save each beatmap's plays by user.
|
||||||
|
pub type OsuUserBests =
|
||||||
|
DB<HashMap<(u64, Mode) /* Beatmap ID and Mode */, HashMap<UserId, Vec<Score>>>>;
|
||||||
|
|
||||||
/// An osu! saved user.
|
/// An osu! saved user.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct OsuUser {
|
pub struct OsuUser {
|
||||||
|
|
|
@ -26,10 +26,10 @@ pub(crate) mod oppai_cache;
|
||||||
mod server_rank;
|
mod server_rank;
|
||||||
|
|
||||||
use db::OsuUser;
|
use db::OsuUser;
|
||||||
use db::{OsuLastBeatmap, OsuSavedUsers};
|
use db::{OsuLastBeatmap, OsuSavedUsers, OsuUserBests};
|
||||||
use embeds::{beatmap_embed, score_embed, user_embed};
|
use embeds::{beatmap_embed, score_embed, user_embed};
|
||||||
pub use hook::hook;
|
pub use hook::hook;
|
||||||
use server_rank::SERVER_RANK_COMMAND;
|
use server_rank::{LEADERBOARD_COMMAND, SERVER_RANK_COMMAND};
|
||||||
|
|
||||||
/// The osu! client.
|
/// The osu! client.
|
||||||
pub(crate) struct OsuClient;
|
pub(crate) struct OsuClient;
|
||||||
|
@ -57,6 +57,7 @@ pub fn setup(
|
||||||
// Databases
|
// Databases
|
||||||
OsuSavedUsers::insert_into(&mut *data, &path.join("osu_saved_users.yaml"))?;
|
OsuSavedUsers::insert_into(&mut *data, &path.join("osu_saved_users.yaml"))?;
|
||||||
OsuLastBeatmap::insert_into(&mut *data, &path.join("last_beatmaps.yaml"))?;
|
OsuLastBeatmap::insert_into(&mut *data, &path.join("last_beatmaps.yaml"))?;
|
||||||
|
OsuUserBests::insert_into(&mut *data, &path.join("osu_user_bests.yaml"))?;
|
||||||
|
|
||||||
// API client
|
// API client
|
||||||
let http_client = data.get::<HTTPClient>().unwrap().clone();
|
let http_client = data.get::<HTTPClient>().unwrap().clone();
|
||||||
|
@ -77,7 +78,19 @@ pub fn setup(
|
||||||
#[group]
|
#[group]
|
||||||
#[prefix = "osu"]
|
#[prefix = "osu"]
|
||||||
#[description = "osu! related commands."]
|
#[description = "osu! related commands."]
|
||||||
#[commands(std, taiko, catch, mania, save, recent, last, check, top, server_rank)]
|
#[commands(
|
||||||
|
std,
|
||||||
|
taiko,
|
||||||
|
catch,
|
||||||
|
mania,
|
||||||
|
save,
|
||||||
|
recent,
|
||||||
|
last,
|
||||||
|
check,
|
||||||
|
top,
|
||||||
|
server_rank,
|
||||||
|
leaderboard
|
||||||
|
)]
|
||||||
#[default_command(std)]
|
#[default_command(std)]
|
||||||
struct Osu;
|
struct Osu;
|
||||||
|
|
||||||
|
@ -469,7 +482,8 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
#[aliases("c", "chk")]
|
#[aliases("c", "chk")]
|
||||||
#[description = "Check your own or someone else's best record on the last beatmap."]
|
#[usage = "[username or tag = yourself]"]
|
||||||
|
#[description = "Check your own or someone else's best record on the last beatmap. Also stores the result if possible."]
|
||||||
#[max_args(1)]
|
#[max_args(1)]
|
||||||
pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
let data = ctx.data.read().await;
|
let data = ctx.data.read().await;
|
||||||
|
@ -483,7 +497,13 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
|
||||||
Some(bm) => {
|
Some(bm) => {
|
||||||
let b = &bm.0;
|
let b = &bm.0;
|
||||||
let m = bm.1;
|
let m = bm.1;
|
||||||
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*data, msg)?;
|
let username_arg = args.single::<UsernameArg>().ok();
|
||||||
|
let user_id = match username_arg.as_ref() {
|
||||||
|
Some(UsernameArg::Tagged(v)) => Some(v.clone()),
|
||||||
|
None => Some(msg.author.id),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let user = to_user_id_query(username_arg, &*data, msg)?;
|
||||||
|
|
||||||
let osu = data.get::<OsuClient>().unwrap();
|
let osu = data.get::<OsuClient>().unwrap();
|
||||||
let oppai = data.get::<BeatmapCache>().unwrap();
|
let oppai = data.get::<BeatmapCache>().unwrap();
|
||||||
|
@ -502,13 +522,22 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
|
||||||
msg.reply(&ctx, "No scores found").await?;
|
msg.reply(&ctx, "No scores found").await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for score in scores.into_iter() {
|
for score in scores.iter() {
|
||||||
msg.channel_id
|
msg.channel_id
|
||||||
.send_message(&ctx, |c| {
|
.send_message(&ctx, |c| {
|
||||||
c.embed(|m| score_embed(&score, &bm, &content, &user, None, m))
|
c.embed(|m| score_embed(&score, &bm, &content, &user, None, m))
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(user_id) = user_id {
|
||||||
|
// Save to database
|
||||||
|
OsuUserBests::open(&*data)
|
||||||
|
.borrow_mut()?
|
||||||
|
.entry((bm.0.beatmap_id, bm.1))
|
||||||
|
.or_default()
|
||||||
|
.insert(user_id, scores);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
use super::{db::OsuSavedUsers, ModeArg};
|
use super::{
|
||||||
use crate::models::Mode;
|
cache::get_beatmap,
|
||||||
|
db::{OsuSavedUsers, OsuUserBests},
|
||||||
|
ModeArg,
|
||||||
|
};
|
||||||
|
use crate::models::{Mode, Score};
|
||||||
use serenity::{
|
use serenity::{
|
||||||
framework::standard::{macros::command, Args, CommandResult},
|
framework::standard::{macros::command, Args, CommandResult},
|
||||||
model::channel::Message,
|
model::channel::Message,
|
||||||
|
@ -7,8 +11,6 @@ use serenity::{
|
||||||
};
|
};
|
||||||
use youmubot_prelude::*;
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
const ITEMS_PER_PAGE: usize = 10;
|
|
||||||
|
|
||||||
#[command("ranks")]
|
#[command("ranks")]
|
||||||
#[description = "See the server's ranks"]
|
#[description = "See the server's ranks"]
|
||||||
#[usage = "[mode (Std, Taiko, Catch, Mania) = Std]"]
|
#[usage = "[mode (Std, Taiko, Catch, Mania) = Std]"]
|
||||||
|
@ -52,6 +54,7 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR
|
||||||
let last_update = last_update.unwrap();
|
let last_update = last_update.unwrap();
|
||||||
paginate(
|
paginate(
|
||||||
move |page: u8, ctx: &Context, m: &mut Message| {
|
move |page: u8, ctx: &Context, m: &mut Message| {
|
||||||
|
const ITEMS_PER_PAGE: usize = 10;
|
||||||
let users = users.clone();
|
let users = users.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let start = (page as usize) * ITEMS_PER_PAGE;
|
let start = (page as usize) * ITEMS_PER_PAGE;
|
||||||
|
@ -94,3 +97,185 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[command("leaderboard")]
|
||||||
|
#[aliases("lb", "bmranks", "br", "cc")]
|
||||||
|
#[description = "See the server's ranks on the last seen beatmap"]
|
||||||
|
#[max_args(0)]
|
||||||
|
#[only_in(guilds)]
|
||||||
|
pub async fn leaderboard(ctx: &Context, m: &Message, mut _args: Args) -> CommandResult {
|
||||||
|
let data = ctx.data.read().await;
|
||||||
|
let bm = match get_beatmap(&*data, m.channel_id)? {
|
||||||
|
Some(bm) => bm,
|
||||||
|
None => {
|
||||||
|
m.reply(&ctx, "No beatmap queried on this channel.").await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let guild = m.guild_id.expect("Guild-only command");
|
||||||
|
let scores = {
|
||||||
|
const NO_SCORES: &'static str =
|
||||||
|
"No scores have been recorded for this beatmap. Run `osu check` to scan for yours!";
|
||||||
|
|
||||||
|
let users = OsuUserBests::open(&*data);
|
||||||
|
let users = users.borrow()?.get(&(bm.0.beatmap_id, bm.1)).cloned();
|
||||||
|
let users = match users {
|
||||||
|
None => {
|
||||||
|
m.reply(&ctx, NO_SCORES).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Some(v) if v.is_empty() => {
|
||||||
|
m.reply(&ctx, NO_SCORES).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Some(v) => v,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut scores: Vec<(f64, String, Score)> = users
|
||||||
|
.into_iter()
|
||||||
|
.map(|(user_id, scores)| async move {
|
||||||
|
guild
|
||||||
|
.member(&ctx, user_id)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|m| Some((m.distinct(), scores)))
|
||||||
|
})
|
||||||
|
.collect::<stream::FuturesUnordered<_>>()
|
||||||
|
.filter_map(|v| future::ready(v))
|
||||||
|
.flat_map(|(user, scores)| {
|
||||||
|
scores
|
||||||
|
.into_iter()
|
||||||
|
.map(move |v| future::ready((user.clone(), v.clone())))
|
||||||
|
.collect::<stream::FuturesUnordered<_>>()
|
||||||
|
})
|
||||||
|
.filter_map(|(user, score)| future::ready(score.pp.map(|v| (v, user, score))))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.await;
|
||||||
|
scores
|
||||||
|
.sort_by(|(a, _, _), (b, _, _)| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
|
scores
|
||||||
|
};
|
||||||
|
|
||||||
|
if scores.is_empty() {
|
||||||
|
m.reply(
|
||||||
|
&ctx,
|
||||||
|
"No scores have been recorded for this beatmap. Run `osu check` to scan for yours!",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
paginate(
|
||||||
|
move |page: u8, ctx: &Context, m: &mut Message| {
|
||||||
|
const ITEMS_PER_PAGE: usize = 5;
|
||||||
|
let start = (page as usize) * ITEMS_PER_PAGE;
|
||||||
|
let end = (start + ITEMS_PER_PAGE).min(scores.len());
|
||||||
|
if start >= end {
|
||||||
|
return Box::pin(future::ready(Ok(false)));
|
||||||
|
}
|
||||||
|
let total_len = scores.len();
|
||||||
|
let scores = (&scores[start..end]).iter().cloned().collect::<Vec<_>>();
|
||||||
|
let bm = (bm.0.clone(), bm.1.clone());
|
||||||
|
Box::pin(async move {
|
||||||
|
// username width
|
||||||
|
let uw = scores
|
||||||
|
.iter()
|
||||||
|
.map(|(_, u, _)| u.len())
|
||||||
|
.max()
|
||||||
|
.unwrap_or(8)
|
||||||
|
.max(8);
|
||||||
|
let accuracies = scores
|
||||||
|
.iter()
|
||||||
|
.map(|(_, _, v)| format!("{:.2}%", v.accuracy(bm.1)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let aw = accuracies.iter().map(|v| v.len()).max().unwrap().max(3);
|
||||||
|
let misses = scores
|
||||||
|
.iter()
|
||||||
|
.map(|(_, _, v)| format!("{}", v.count_miss))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mw = misses.iter().map(|v| v.len()).max().unwrap().max(4);
|
||||||
|
let ranks = scores
|
||||||
|
.iter()
|
||||||
|
.map(|(_, _, v)| v.rank.to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let rw = ranks.iter().map(|v| v.len()).max().unwrap().max(4);
|
||||||
|
let pp = scores
|
||||||
|
.iter()
|
||||||
|
.map(|(pp, _, _)| format!("{:.2}", pp))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let pw = pp.iter().map(|v| v.len()).max().unwrap_or(2);
|
||||||
|
/*mods width*/
|
||||||
|
let mdw = scores
|
||||||
|
.iter()
|
||||||
|
.map(|(_, _, v)| v.mods.to_string().len())
|
||||||
|
.max()
|
||||||
|
.unwrap()
|
||||||
|
.max(4);
|
||||||
|
let mut content = MessageBuilder::new();
|
||||||
|
content
|
||||||
|
.push_line("```")
|
||||||
|
.push_line(format!(
|
||||||
|
"rank | {:>pw$} | {:mdw$} | {:rw$} | {:>aw$} | {:mw$} | {:uw$}",
|
||||||
|
"pp",
|
||||||
|
"mods",
|
||||||
|
"rank",
|
||||||
|
"acc",
|
||||||
|
"miss",
|
||||||
|
"user",
|
||||||
|
pw = pw,
|
||||||
|
mdw = mdw,
|
||||||
|
rw = rw,
|
||||||
|
aw = aw,
|
||||||
|
mw = mw,
|
||||||
|
uw = uw,
|
||||||
|
))
|
||||||
|
.push_line(format!(
|
||||||
|
"-------{:-<pw$}---{:-<mdw$}---{:-<rw$}---{:-<aw$}---{:-<mw$}---{:-<uw$}",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
pw = pw,
|
||||||
|
mdw = mdw,
|
||||||
|
rw = rw,
|
||||||
|
aw = aw,
|
||||||
|
mw = mw,
|
||||||
|
uw = uw,
|
||||||
|
));
|
||||||
|
for (id, (_, member, p)) in scores.iter().enumerate() {
|
||||||
|
content.push_line_safe(format!(
|
||||||
|
"{:>4} | {:>pw$} | {:>mdw$} | {:>rw$} | {:>aw$} | {:>mw$} | {:uw$}",
|
||||||
|
format!("#{}", 1 + id + start),
|
||||||
|
pp[id],
|
||||||
|
p.mods.to_string(),
|
||||||
|
ranks[id],
|
||||||
|
accuracies[id],
|
||||||
|
misses[id],
|
||||||
|
member,
|
||||||
|
pw = pw,
|
||||||
|
mdw = mdw,
|
||||||
|
rw = rw,
|
||||||
|
aw = aw,
|
||||||
|
mw = mw,
|
||||||
|
uw = uw,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
content.push_line("```").push_line(format!(
|
||||||
|
"Page **{}**/**{}**. Not seeing your scores? Run `osu check` to update.",
|
||||||
|
page + 1,
|
||||||
|
(total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE,
|
||||||
|
));
|
||||||
|
m.edit(&ctx, |f| f.content(content.build())).await?;
|
||||||
|
Ok(true)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
|
m.channel_id,
|
||||||
|
std::time::Duration::from_secs(60),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue