mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-19 16:58:55 +00:00
Merge branch 'osu-serverranks'
This commit is contained in:
commit
ec63a035e0
8 changed files with 253 additions and 24 deletions
|
@ -28,25 +28,20 @@ impl Announcer for OsuAnnouncer {
|
||||||
let osu = d.get_cloned::<OsuClient>();
|
let osu = d.get_cloned::<OsuClient>();
|
||||||
// For each user...
|
// For each user...
|
||||||
let mut data = OsuSavedUsers::open(&*d.read()).borrow()?.clone();
|
let mut data = OsuSavedUsers::open(&*d.read()).borrow()?.clone();
|
||||||
for (user_id, osu_user) in data.iter_mut() {
|
'user_loop: for (user_id, osu_user) in data.iter_mut() {
|
||||||
let mut user = None;
|
let mut pp_values = vec![]; // Store the pp values here...
|
||||||
for mode in &[Mode::Std, Mode::Taiko, Mode::Mania, Mode::Catch] {
|
for mode in &[Mode::Std, Mode::Taiko, Mode::Mania, Mode::Catch] {
|
||||||
let scores = OsuAnnouncer::scan_user(&osu, osu_user, *mode)?;
|
let scores = OsuAnnouncer::scan_user(&osu, osu_user, *mode)?;
|
||||||
if scores.is_empty() {
|
if scores.is_empty() && !osu_user.pp.is_empty() {
|
||||||
|
// Nothing to update: no new scores and pp is there.
|
||||||
|
pp_values.push(osu_user.pp[*mode as usize]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let user = {
|
let user = match osu.user(UserID::ID(osu_user.id), |f| f.mode(*mode)) {
|
||||||
let user = &mut user;
|
Ok(Some(u)) => u,
|
||||||
if let None = user {
|
_ => continue 'user_loop,
|
||||||
match osu.user(UserID::ID(osu_user.id), |f| f.mode(*mode)) {
|
|
||||||
Ok(u) => {
|
|
||||||
*user = u;
|
|
||||||
}
|
|
||||||
Err(_) => continue,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
user.as_ref().unwrap()
|
|
||||||
};
|
};
|
||||||
|
pp_values.push(user.pp);
|
||||||
scores
|
scores
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.filter_map(|(rank, score)| {
|
.filter_map(|(rank, score)| {
|
||||||
|
@ -74,6 +69,7 @@ impl Announcer for OsuAnnouncer {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
osu_user.last_update = chrono::Utc::now();
|
osu_user.last_update = chrono::Utc::now();
|
||||||
|
osu_user.pp = pp_values;
|
||||||
}
|
}
|
||||||
// Update users
|
// Update users
|
||||||
*OsuSavedUsers::open(&*d.read()).borrow_mut()? = data;
|
*OsuSavedUsers::open(&*d.read()).borrow_mut()? = data;
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
use chrono::{DateTime, Utc};
|
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};
|
use crate::models::{Beatmap, Mode};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serenity::model::id::{ChannelId, UserId};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use youmubot_db::DB;
|
||||||
|
|
||||||
/// Save the user IDs.
|
/// Save the user IDs.
|
||||||
pub type OsuSavedUsers = DB<HashMap<UserId, OsuUser>>;
|
pub type OsuSavedUsers = DB<HashMap<UserId, OsuUser>>;
|
||||||
|
@ -19,4 +17,6 @@ pub type OsuLastBeatmap = DB<HashMap<ChannelId, (Beatmap, Mode)>>;
|
||||||
pub struct OsuUser {
|
pub struct OsuUser {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub last_update: DateTime<Utc>,
|
pub last_update: DateTime<Utc>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub pp: Vec<Option<f64>>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,12 +19,14 @@ mod cache;
|
||||||
mod db;
|
mod db;
|
||||||
pub(crate) mod embeds;
|
pub(crate) mod embeds;
|
||||||
mod hook;
|
mod hook;
|
||||||
|
mod server_rank;
|
||||||
|
|
||||||
pub use announcer::OsuAnnouncer;
|
pub use announcer::OsuAnnouncer;
|
||||||
use db::OsuUser;
|
use db::OsuUser;
|
||||||
use db::{OsuLastBeatmap, OsuSavedUsers};
|
use db::{OsuLastBeatmap, OsuSavedUsers};
|
||||||
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;
|
||||||
|
|
||||||
/// The osu! client.
|
/// The osu! client.
|
||||||
pub(crate) struct OsuClient;
|
pub(crate) struct OsuClient;
|
||||||
|
@ -68,7 +70,7 @@ 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)]
|
#[commands(std, taiko, catch, mania, save, recent, last, check, top, server_rank)]
|
||||||
struct Osu;
|
struct Osu;
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
|
@ -145,6 +147,7 @@ pub fn save(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
OsuUser {
|
OsuUser {
|
||||||
id: u.id,
|
id: u.id,
|
||||||
last_update: chrono::Utc::now(),
|
last_update: chrono::Utc::now(),
|
||||||
|
pp: vec![],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
msg.reply(
|
msg.reply(
|
||||||
|
|
84
youmubot-osu/src/discord/server_rank.rs
Normal file
84
youmubot-osu/src/discord/server_rank.rs
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
use super::{db::OsuSavedUsers, ModeArg};
|
||||||
|
use crate::models::Mode;
|
||||||
|
use serenity::{
|
||||||
|
builder::EditMessage,
|
||||||
|
framework::standard::{macros::command, Args, CommandError as Error, CommandResult},
|
||||||
|
model::channel::Message,
|
||||||
|
utils::MessageBuilder,
|
||||||
|
};
|
||||||
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
|
const ITEMS_PER_PAGE: usize = 10;
|
||||||
|
|
||||||
|
#[command("ranks")]
|
||||||
|
#[description = "See the server's ranks"]
|
||||||
|
#[usage = "[mode (Std, Taiko, Catch, Mania) = Std]"]
|
||||||
|
#[max_args(1)]
|
||||||
|
#[only_in(guilds)]
|
||||||
|
pub fn server_rank(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult {
|
||||||
|
let mode = args.single::<ModeArg>().map(|v| v.0).unwrap_or(Mode::Std);
|
||||||
|
let guild = m.guild_id.expect("Guild-only command");
|
||||||
|
let users = OsuSavedUsers::open(&*ctx.data.read())
|
||||||
|
.borrow()
|
||||||
|
.expect("DB initialized")
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(user_id, osu_user)| {
|
||||||
|
guild.member(&ctx, user_id).ok().and_then(|member| {
|
||||||
|
osu_user
|
||||||
|
.pp
|
||||||
|
.get(mode as usize)
|
||||||
|
.cloned()
|
||||||
|
.and_then(|pp| pp)
|
||||||
|
.map(|pp| (pp, member.distinct(), osu_user.last_update.clone()))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let last_update = users.iter().map(|(_, _, a)| a).min().cloned();
|
||||||
|
let mut users = users
|
||||||
|
.into_iter()
|
||||||
|
.map(|(a, b, _)| (a, b))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
users.sort_by(|(a, _), (b, _)| (*b).partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
|
|
||||||
|
if users.is_empty() {
|
||||||
|
m.reply(&ctx, "No saved users in the current server...")?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let last_update = last_update.unwrap();
|
||||||
|
ctx.data.get_cloned::<ReactionWatcher>().paginate_fn(
|
||||||
|
ctx.clone(),
|
||||||
|
m.channel_id,
|
||||||
|
move |page: u8, e: &mut EditMessage| {
|
||||||
|
let start = (page as usize) * ITEMS_PER_PAGE;
|
||||||
|
if start >= users.len() {
|
||||||
|
return (e, Err(Error("No more items".to_owned())));
|
||||||
|
}
|
||||||
|
let total_len = users.len();
|
||||||
|
let users = users.iter().skip(start).take(ITEMS_PER_PAGE);
|
||||||
|
let mut content = MessageBuilder::new();
|
||||||
|
content
|
||||||
|
.push_line("```")
|
||||||
|
.push_line("Rank | pp | Username")
|
||||||
|
.push_line("-------------------------");
|
||||||
|
for (id, (pp, member)) in users.enumerate() {
|
||||||
|
content
|
||||||
|
.push(format!(
|
||||||
|
"{:>4} | {:>7.2} | ",
|
||||||
|
format!("#{}", 1 + id + start),
|
||||||
|
pp
|
||||||
|
))
|
||||||
|
.push_line_safe(member);
|
||||||
|
}
|
||||||
|
content.push_line("```").push_line(format!(
|
||||||
|
"Page **{}**/**{}**. Last updated: `{}`",
|
||||||
|
page + 1,
|
||||||
|
(total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE,
|
||||||
|
last_update.to_rfc2822()
|
||||||
|
));
|
||||||
|
(e.content(content.build()), Ok(()))
|
||||||
|
},
|
||||||
|
std::time::Duration::from_secs(60),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -3,11 +3,13 @@ use std::sync::Arc;
|
||||||
|
|
||||||
pub mod announcer;
|
pub mod announcer;
|
||||||
pub mod args;
|
pub mod args;
|
||||||
|
pub mod pagination;
|
||||||
pub mod reaction_watch;
|
pub mod reaction_watch;
|
||||||
pub mod setup;
|
pub mod setup;
|
||||||
|
|
||||||
pub use announcer::Announcer;
|
pub use announcer::Announcer;
|
||||||
pub use args::Duration;
|
pub use args::Duration;
|
||||||
|
pub use pagination::Pagination;
|
||||||
pub use reaction_watch::{ReactionHandler, ReactionWatcher};
|
pub use reaction_watch::{ReactionHandler, ReactionWatcher};
|
||||||
|
|
||||||
/// The global app data.
|
/// The global app data.
|
||||||
|
|
145
youmubot-prelude/src/pagination.rs
Normal file
145
youmubot-prelude/src/pagination.rs
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
use crate::{Context, ReactionHandler, ReactionWatcher};
|
||||||
|
use serenity::{
|
||||||
|
builder::EditMessage,
|
||||||
|
framework::standard::{CommandError, CommandResult},
|
||||||
|
model::{
|
||||||
|
channel::{Message, Reaction, ReactionType},
|
||||||
|
id::ChannelId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ARROW_RIGHT: &'static str = "➡️";
|
||||||
|
const ARROW_LEFT: &'static str = "⬅️";
|
||||||
|
|
||||||
|
impl ReactionWatcher {
|
||||||
|
/// Start a pagination.
|
||||||
|
///
|
||||||
|
/// Takes a copy of Context (which you can `clone`), a pager (see "Pagination") and a target channel id.
|
||||||
|
/// Pagination will handle all events on adding/removing an "arrow" emoji (⬅️ and ➡️).
|
||||||
|
/// This is a blocking call - it will block the thread until duration is over.
|
||||||
|
pub fn paginate<T: Pagination>(
|
||||||
|
&self,
|
||||||
|
ctx: Context,
|
||||||
|
channel: ChannelId,
|
||||||
|
pager: T,
|
||||||
|
duration: std::time::Duration,
|
||||||
|
) -> CommandResult {
|
||||||
|
let handler = PaginationHandler::new(pager, ctx, channel)?;
|
||||||
|
self.handle_reactions(handler, duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A version of `paginate` that compiles for closures.
|
||||||
|
///
|
||||||
|
/// A workaround until https://github.com/rust-lang/rust/issues/36582 is solved.
|
||||||
|
pub fn paginate_fn<T>(
|
||||||
|
&self,
|
||||||
|
ctx: Context,
|
||||||
|
channel: ChannelId,
|
||||||
|
pager: T,
|
||||||
|
duration: std::time::Duration,
|
||||||
|
) -> CommandResult
|
||||||
|
where
|
||||||
|
T: for<'a> Fn(u8, &'a mut EditMessage) -> (&'a mut EditMessage, CommandResult),
|
||||||
|
{
|
||||||
|
self.paginate(ctx, channel, pager, duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pagination allows the bot to display content in multiple pages.
|
||||||
|
///
|
||||||
|
/// You need to implement the "render_page" function, which takes a dummy content and
|
||||||
|
/// embed assigning function.
|
||||||
|
/// Pagination is automatically implemented for functions with the same signature as `render_page`.
|
||||||
|
///
|
||||||
|
/// Pages start at 0.
|
||||||
|
pub trait Pagination {
|
||||||
|
/// Render a page.
|
||||||
|
///
|
||||||
|
/// This would either create or edit a message, but you should not be worry about it.
|
||||||
|
fn render_page<'a>(
|
||||||
|
&self,
|
||||||
|
page: u8,
|
||||||
|
target: &'a mut EditMessage,
|
||||||
|
) -> (&'a mut EditMessage, CommandResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Pagination for T
|
||||||
|
where
|
||||||
|
T: for<'a> Fn(u8, &'a mut EditMessage) -> (&'a mut EditMessage, CommandResult),
|
||||||
|
{
|
||||||
|
fn render_page<'a>(
|
||||||
|
&self,
|
||||||
|
page: u8,
|
||||||
|
target: &'a mut EditMessage,
|
||||||
|
) -> (&'a mut EditMessage, CommandResult) {
|
||||||
|
self(page, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PaginationHandler<T: Pagination> {
|
||||||
|
pager: T,
|
||||||
|
message: Message,
|
||||||
|
page: u8,
|
||||||
|
ctx: Context,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Pagination> PaginationHandler<T> {
|
||||||
|
pub fn new(pager: T, mut ctx: Context, channel: ChannelId) -> Result<Self, CommandError> {
|
||||||
|
let message = channel.send_message(&mut ctx, |e| {
|
||||||
|
e.content("Youmu is loading the first page...")
|
||||||
|
})?;
|
||||||
|
// React to the message
|
||||||
|
message.react(&mut ctx, ARROW_LEFT)?;
|
||||||
|
message.react(&mut ctx, ARROW_RIGHT)?;
|
||||||
|
let mut p = Self {
|
||||||
|
pager,
|
||||||
|
message: message.clone(),
|
||||||
|
page: 0,
|
||||||
|
ctx,
|
||||||
|
};
|
||||||
|
p.call_pager()?;
|
||||||
|
Ok(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Pagination> PaginationHandler<T> {
|
||||||
|
/// Call the pager, log the error (if any).
|
||||||
|
fn call_pager(&mut self) -> CommandResult {
|
||||||
|
let mut res: CommandResult = Ok(());
|
||||||
|
let mut msg = self.message.clone();
|
||||||
|
msg.edit(&self.ctx, |e| {
|
||||||
|
let (e, r) = self.pager.render_page(self.page, e);
|
||||||
|
res = r;
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
self.message = msg;
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Pagination> ReactionHandler for PaginationHandler<T> {
|
||||||
|
fn handle_reaction(&mut self, reaction: &Reaction, _is_add: bool) -> CommandResult {
|
||||||
|
match &reaction.emoji {
|
||||||
|
ReactionType::Unicode(ref s) => match s.as_str() {
|
||||||
|
ARROW_LEFT if self.page == 0 => return Ok(()),
|
||||||
|
ARROW_LEFT => {
|
||||||
|
self.page -= 1;
|
||||||
|
if let Err(e) = self.call_pager() {
|
||||||
|
self.page += 1;
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ARROW_RIGHT => {
|
||||||
|
self.page += 1;
|
||||||
|
if let Err(e) = self.call_pager() {
|
||||||
|
self.page -= 1;
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -67,7 +67,7 @@ impl ReactionWatcher {
|
||||||
recv(timeout) -> _ => break,
|
recv(timeout) -> _ => break,
|
||||||
};
|
};
|
||||||
if let Err(v) = r {
|
if let Err(v) = r {
|
||||||
return Err(v);
|
dbg!(v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -25,7 +25,6 @@ impl EventHandler for Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn message(&self, mut ctx: Context, message: Message) {
|
fn message(&self, mut ctx: Context, message: Message) {
|
||||||
println!("{:?}", message);
|
|
||||||
self.hooks.iter().for_each(|f| f(&mut ctx, &message));
|
self.hooks.iter().for_each(|f| f(&mut ctx, &message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue