mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-20 01:08:55 +00:00
More osu improvements (#11)
* Don't show DT when NC is on * Allow leading + in mods parsing * Extend Pagination * Implement beatmapset display * Move OkPrint to prelude * Beatmapset display seems to work * Put reaction handler into static * Make clippy happy * Add beatmapset caching and last --set * Delay loading of beatmap info * Simplify hook link handling * Replies everywhere! * Replies everywhereee!
This commit is contained in:
parent
61a71b819c
commit
bd845d9662
13 changed files with 450 additions and 156 deletions
|
@ -171,7 +171,7 @@ pub async fn ranks(ctx: &Context, m: &Message) -> CommandResult {
|
||||||
let total_pages = (ranks.len() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE;
|
let total_pages = (ranks.len() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE;
|
||||||
let last_updated = ranks.iter().map(|(_, cfu)| cfu.last_update).min().unwrap();
|
let last_updated = ranks.iter().map(|(_, cfu)| cfu.last_update).min().unwrap();
|
||||||
|
|
||||||
paginate_fn(
|
paginate_reply_fn(
|
||||||
move |page, ctx, msg| {
|
move |page, ctx, msg| {
|
||||||
let ranks = ranks.clone();
|
let ranks = ranks.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
@ -237,7 +237,7 @@ pub async fn ranks(ctx: &Context, m: &Message) -> CommandResult {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
ctx,
|
ctx,
|
||||||
m.channel_id,
|
m,
|
||||||
std::time::Duration::from_secs(60),
|
std::time::Duration::from_secs(60),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -301,7 +301,7 @@ pub async fn contestranks(ctx: &Context, m: &Message, mut args: Args) -> Command
|
||||||
const ITEMS_PER_PAGE: usize = 10;
|
const ITEMS_PER_PAGE: usize = 10;
|
||||||
let total_pages = (ranks.len() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE;
|
let total_pages = (ranks.len() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE;
|
||||||
|
|
||||||
paginate_fn(
|
paginate_reply_fn(
|
||||||
move |page, ctx, msg| {
|
move |page, ctx, msg| {
|
||||||
let contest = contest.clone();
|
let contest = contest.clone();
|
||||||
let problems = problems.clone();
|
let problems = problems.clone();
|
||||||
|
@ -391,7 +391,7 @@ pub async fn contestranks(ctx: &Context, m: &Message, mut args: Args) -> Command
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
ctx,
|
ctx,
|
||||||
m.channel_id,
|
m,
|
||||||
Duration::from_secs(60),
|
Duration::from_secs(60),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -114,6 +114,7 @@ pub async fn choose(ctx: &Context, m: &Message, mut args: Args) -> CommandResult
|
||||||
.push(". Congrats! 🎉 🎊 🥳")
|
.push(". Congrats! 🎉 🎊 🥳")
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
|
.reference_message(m)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ async fn list(ctx: &Context, m: &Message, _: Args) -> CommandResult {
|
||||||
const ROLES_PER_PAGE: usize = 8;
|
const ROLES_PER_PAGE: usize = 8;
|
||||||
let pages = (roles.len() + ROLES_PER_PAGE - 1) / ROLES_PER_PAGE;
|
let pages = (roles.len() + ROLES_PER_PAGE - 1) / ROLES_PER_PAGE;
|
||||||
|
|
||||||
paginate_fn(
|
paginate_reply_fn(
|
||||||
|page, ctx, msg| {
|
|page, ctx, msg| {
|
||||||
let roles = roles.clone();
|
let roles = roles.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
@ -99,7 +99,7 @@ async fn list(ctx: &Context, m: &Message, _: Args) -> CommandResult {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
ctx,
|
ctx,
|
||||||
m.channel_id,
|
m,
|
||||||
std::time::Duration::from_secs(60 * 10),
|
std::time::Duration::from_secs(60 * 10),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -64,7 +64,7 @@ async fn message_command(
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let images = std::sync::Arc::new(images);
|
let images = std::sync::Arc::new(images);
|
||||||
paginate_fn(
|
paginate_reply_fn(
|
||||||
move |page, ctx, msg: &mut Message| {
|
move |page, ctx, msg: &mut Message| {
|
||||||
let images = images.clone();
|
let images = images.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
@ -87,7 +87,7 @@ async fn message_command(
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
ctx,
|
ctx,
|
||||||
msg.channel_id,
|
msg,
|
||||||
std::time::Duration::from_secs(120),
|
std::time::Duration::from_secs(120),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -144,7 +144,7 @@ impl Announcer {
|
||||||
.filter(|u| u.mode == mode && u.date > last_update)
|
.filter(|u| u.mode == mode && u.date > last_update)
|
||||||
.map(|ev| CollectedScore::from_event(&*client, &user, ev, user_id, &channels[..]))
|
.map(|ev| CollectedScore::from_event(&*client, &user, ev, user_id, &channels[..]))
|
||||||
.collect::<stream::FuturesUnordered<_>>()
|
.collect::<stream::FuturesUnordered<_>>()
|
||||||
.filter_map(|u| future::ready(u.ok_or_print()))
|
.filter_map(|u| future::ready(u.pls_ok()))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.await;
|
.await;
|
||||||
let top_scores = scores.into_iter().filter_map(|(rank, score)| {
|
let top_scores = scores.into_iter().filter_map(|(rank, score)| {
|
||||||
|
@ -169,7 +169,7 @@ impl Announcer {
|
||||||
.collect::<stream::FuturesUnordered<_>>()
|
.collect::<stream::FuturesUnordered<_>>()
|
||||||
.try_collect::<Vec<_>>()
|
.try_collect::<Vec<_>>()
|
||||||
.await
|
.await
|
||||||
.ok_or_print();
|
.pls_ok();
|
||||||
});
|
});
|
||||||
Ok(pp)
|
Ok(pp)
|
||||||
}
|
}
|
||||||
|
@ -304,7 +304,7 @@ impl<'a> CollectedScore<'a> {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
save_beatmap(&*ctx.data.read().await, channel, &bm).ok_or_print();
|
save_beatmap(&*ctx.data.read().await, channel, &bm).pls_ok();
|
||||||
Ok(m)
|
Ok(m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -313,22 +313,3 @@ enum ScoreType {
|
||||||
TopRecord(u8),
|
TopRecord(u8),
|
||||||
WorldRecord(u16),
|
WorldRecord(u16),
|
||||||
}
|
}
|
||||||
|
|
||||||
trait OkPrint {
|
|
||||||
type Output;
|
|
||||||
fn ok_or_print(self) -> Option<Self::Output>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, E: std::fmt::Debug> OkPrint for Result<T, E> {
|
|
||||||
type Output = T;
|
|
||||||
|
|
||||||
fn ok_or_print(self) -> Option<Self::Output> {
|
|
||||||
match self {
|
|
||||||
Ok(v) => Some(v),
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Error: {:?}", e);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ use youmubot_prelude::*;
|
||||||
pub struct BeatmapMetaCache {
|
pub struct BeatmapMetaCache {
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
cache: DashMap<(u64, Mode), Beatmap>,
|
cache: DashMap<(u64, Mode), Beatmap>,
|
||||||
|
beatmapsets: DashMap<u64, Vec<u64>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TypeMapKey for BeatmapMetaCache {
|
impl TypeMapKey for BeatmapMetaCache {
|
||||||
|
@ -23,6 +24,7 @@ impl BeatmapMetaCache {
|
||||||
BeatmapMetaCache {
|
BeatmapMetaCache {
|
||||||
client,
|
client,
|
||||||
cache: DashMap::new(),
|
cache: DashMap::new(),
|
||||||
|
beatmapsets: DashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async fn insert_if_possible(&self, id: u64, mode: Option<Mode>) -> Result<Beatmap> {
|
async fn insert_if_possible(&self, id: u64, mode: Option<Mode>) -> Result<Beatmap> {
|
||||||
|
@ -54,17 +56,47 @@ impl BeatmapMetaCache {
|
||||||
Ok(
|
Ok(
|
||||||
match (&[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania])
|
match (&[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania])
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|&mode| {
|
.find_map(|&mode| {
|
||||||
self.cache
|
self.cache
|
||||||
.get(&(id, mode))
|
.get(&(id, mode))
|
||||||
.filter(|b| b.mode == mode)
|
.filter(|b| b.mode == mode)
|
||||||
.map(|b| b.clone())
|
.map(|b| b.clone())
|
||||||
})
|
}) {
|
||||||
.next()
|
|
||||||
{
|
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => self.insert_if_possible(id, None).await?,
|
None => self.insert_if_possible(id, None).await?,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a beatmapset from its ID.
|
||||||
|
pub async fn get_beatmapset(&self, id: u64) -> Result<Vec<Beatmap>> {
|
||||||
|
match self.beatmapsets.get(&id).map(|v| v.clone()) {
|
||||||
|
Some(v) => {
|
||||||
|
v.into_iter()
|
||||||
|
.map(|id| self.get_beatmap_default(id))
|
||||||
|
.collect::<stream::FuturesOrdered<_>>()
|
||||||
|
.try_collect()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let beatmaps = self
|
||||||
|
.client
|
||||||
|
.beatmaps(crate::BeatmapRequestKind::Beatmapset(id), |f| f)
|
||||||
|
.await?;
|
||||||
|
if beatmaps.is_empty() {
|
||||||
|
return Err(Error::msg("beatmapset not found"));
|
||||||
|
}
|
||||||
|
if let ApprovalStatus::Ranked(_) = &beatmaps[0].approval {
|
||||||
|
// Save each beatmap.
|
||||||
|
beatmaps.iter().for_each(|b| {
|
||||||
|
self.cache.insert((b.beatmap_id, b.mode), b.clone());
|
||||||
|
});
|
||||||
|
// Save the beatmapset mapping.
|
||||||
|
self.beatmapsets
|
||||||
|
.insert(id, beatmaps.iter().map(|v| v.beatmap_id).collect());
|
||||||
|
}
|
||||||
|
Ok(beatmaps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
163
youmubot-osu/src/discord/display.rs
Normal file
163
youmubot-osu/src/discord/display.rs
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
pub use beatmapset::display_beatmapset;
|
||||||
|
|
||||||
|
mod beatmapset {
|
||||||
|
use crate::{
|
||||||
|
discord::{cache::save_beatmap, oppai_cache::BeatmapInfo, BeatmapCache, BeatmapWithMode},
|
||||||
|
models::{Beatmap, Mode, Mods},
|
||||||
|
};
|
||||||
|
use serenity::{
|
||||||
|
collector::ReactionAction, model::channel::Message, model::channel::ReactionType,
|
||||||
|
};
|
||||||
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
|
const SHOW_ALL_EMOTE: &str = "🗒️";
|
||||||
|
|
||||||
|
pub async fn display_beatmapset(
|
||||||
|
ctx: &Context,
|
||||||
|
beatmapset: Vec<Beatmap>,
|
||||||
|
mode: Option<Mode>,
|
||||||
|
mods: Option<Mods>,
|
||||||
|
reply_to: &Message,
|
||||||
|
message: impl AsRef<str>,
|
||||||
|
) -> Result<bool> {
|
||||||
|
let mods = mods.unwrap_or(Mods::NOMOD);
|
||||||
|
|
||||||
|
if beatmapset.is_empty() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = Paginate {
|
||||||
|
infos: vec![None; beatmapset.len()],
|
||||||
|
maps: beatmapset,
|
||||||
|
mode,
|
||||||
|
mods,
|
||||||
|
message: message.as_ref().to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
let reply_to = reply_to.clone();
|
||||||
|
spawn_future(async move {
|
||||||
|
pagination::paginate_reply(p, &ctx, &reply_to, std::time::Duration::from_secs(60))
|
||||||
|
.await
|
||||||
|
.pls_ok();
|
||||||
|
});
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Paginate {
|
||||||
|
maps: Vec<Beatmap>,
|
||||||
|
infos: Vec<Option<Option<BeatmapInfo>>>,
|
||||||
|
mode: Option<Mode>,
|
||||||
|
mods: Mods,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Paginate {
|
||||||
|
async fn get_beatmap_info(&self, ctx: &Context, b: &Beatmap) -> Option<BeatmapInfo> {
|
||||||
|
let data = ctx.data.read().await;
|
||||||
|
let cache = data.get::<BeatmapCache>().unwrap();
|
||||||
|
let mode = self.mode.unwrap_or(b.mode).to_oppai_mode();
|
||||||
|
cache
|
||||||
|
.get_beatmap(b.beatmap_id)
|
||||||
|
.map(move |v| {
|
||||||
|
v.ok()
|
||||||
|
.and_then(move |v| v.get_info_with(Some(mode?), self.mods).ok())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl pagination::Paginate for Paginate {
|
||||||
|
async fn render(
|
||||||
|
&mut self,
|
||||||
|
page: u8,
|
||||||
|
ctx: &Context,
|
||||||
|
m: &mut serenity::model::channel::Message,
|
||||||
|
) -> Result<bool> {
|
||||||
|
let page = page as usize;
|
||||||
|
if page == self.maps.len() {
|
||||||
|
m.edit(ctx, |f| {
|
||||||
|
f.embed(|em| {
|
||||||
|
crate::discord::embeds::beatmapset_embed(&self.maps[..], self.mode, em)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
if page > self.maps.len() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let map = &self.maps[page];
|
||||||
|
let info = match &self.infos[page] {
|
||||||
|
Some(info) => info.clone(),
|
||||||
|
None => {
|
||||||
|
let info = self.get_beatmap_info(ctx, map).await;
|
||||||
|
self.infos[page] = Some(info.clone());
|
||||||
|
info
|
||||||
|
}
|
||||||
|
};
|
||||||
|
m.edit(ctx, |e| {
|
||||||
|
e.content(self.message.as_str()).embed(|em| {
|
||||||
|
crate::discord::embeds::beatmap_embed(
|
||||||
|
map,
|
||||||
|
self.mode.unwrap_or(map.mode),
|
||||||
|
self.mods,
|
||||||
|
info,
|
||||||
|
em,
|
||||||
|
)
|
||||||
|
.footer(|f| {
|
||||||
|
f.text(format!(
|
||||||
|
"Difficulty {}/{}. To show all difficulties in a single embed (old style), react {}",
|
||||||
|
page + 1,
|
||||||
|
self.maps.len(),
|
||||||
|
SHOW_ALL_EMOTE,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
save_beatmap(
|
||||||
|
&*ctx.data.read().await,
|
||||||
|
m.channel_id,
|
||||||
|
&BeatmapWithMode(map.clone(), self.mode.unwrap_or(map.mode)),
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn prerender(
|
||||||
|
&mut self,
|
||||||
|
ctx: &Context,
|
||||||
|
m: &mut serenity::model::channel::Message,
|
||||||
|
) -> Result<()> {
|
||||||
|
m.react(&ctx, SHOW_ALL_EMOTE.parse::<ReactionType>().unwrap())
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_reaction(
|
||||||
|
&mut self,
|
||||||
|
page: u8,
|
||||||
|
ctx: &Context,
|
||||||
|
message: &mut serenity::model::channel::Message,
|
||||||
|
reaction: &ReactionAction,
|
||||||
|
) -> Result<Option<u8>> {
|
||||||
|
// Render the old style.
|
||||||
|
let v = match reaction {
|
||||||
|
ReactionAction::Added(v) | ReactionAction::Removed(v) => v,
|
||||||
|
};
|
||||||
|
if let ReactionType::Unicode(s) = &v.emoji {
|
||||||
|
if s == SHOW_ALL_EMOTE {
|
||||||
|
self.render(self.maps.len() as u8, ctx, message).await?;
|
||||||
|
return Ok(Some(self.maps.len() as u8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pagination::handle_pagination_reaction(page, self, ctx, message, reaction)
|
||||||
|
.await
|
||||||
|
.map(Some)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +1,15 @@
|
||||||
use super::OsuClient;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
discord::beatmap_cache::BeatmapMetaCache,
|
discord::beatmap_cache::BeatmapMetaCache,
|
||||||
discord::oppai_cache::{BeatmapCache, BeatmapInfo},
|
discord::oppai_cache::{BeatmapCache, BeatmapInfo},
|
||||||
models::{Beatmap, Mode, Mods},
|
models::{Beatmap, Mode, Mods},
|
||||||
request::BeatmapRequestKind,
|
|
||||||
};
|
};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serenity::{builder::CreateMessage, model::channel::Message, utils::MessageBuilder};
|
use serenity::{model::channel::Message, utils::MessageBuilder};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use youmubot_prelude::*;
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
use super::embeds::{beatmap_embed, beatmapset_embed};
|
use super::embeds::beatmap_embed;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref OLD_LINK_REGEX: Regex = Regex::new(
|
static ref OLD_LINK_REGEX: Regex = Regex::new(
|
||||||
|
@ -38,39 +36,32 @@ pub fn hook<'a>(
|
||||||
handle_new_links(ctx, &msg.content),
|
handle_new_links(ctx, &msg.content),
|
||||||
handle_short_links(ctx, &msg, &msg.content),
|
handle_short_links(ctx, &msg, &msg.content),
|
||||||
);
|
);
|
||||||
let last_beatmap = stream::select(old_links, stream::select(new_links, short_links))
|
stream::select(old_links, stream::select(new_links, short_links))
|
||||||
.then(|l| async move {
|
.then(|l| async move {
|
||||||
let mut bm: Option<super::BeatmapWithMode> = None;
|
match l.embed {
|
||||||
msg.channel_id
|
|
||||||
.send_message(&ctx, |m| match l.embed {
|
|
||||||
EmbedType::Beatmap(b, info, mods) => {
|
EmbedType::Beatmap(b, info, mods) => {
|
||||||
let t = handle_beatmap(&b, info, l.link, l.mode, mods, m);
|
handle_beatmap(ctx, &b, info, l.link, l.mode, mods, msg)
|
||||||
|
.await
|
||||||
|
.pls_ok();
|
||||||
let mode = l.mode.unwrap_or(b.mode);
|
let mode = l.mode.unwrap_or(b.mode);
|
||||||
bm = Some(super::BeatmapWithMode(b, mode));
|
let bm = super::BeatmapWithMode(b, mode);
|
||||||
t
|
crate::discord::cache::save_beatmap(
|
||||||
|
&*ctx.data.read().await,
|
||||||
|
msg.channel_id,
|
||||||
|
&bm,
|
||||||
|
)
|
||||||
|
.pls_ok();
|
||||||
}
|
}
|
||||||
EmbedType::Beatmapset(b) => handle_beatmapset(b, l.link, l.mode, m),
|
EmbedType::Beatmapset(b) => {
|
||||||
})
|
handle_beatmapset(ctx, b, l.link, l.mode, msg)
|
||||||
.await?;
|
.await
|
||||||
let r: Result<_> = Ok(bm);
|
.pls_ok();
|
||||||
r
|
|
||||||
})
|
|
||||||
.filter_map(|v| async move {
|
|
||||||
match v {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("{}", e);
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.fold(None, |_, v| async move { Some(v) })
|
.collect::<()>()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Save the beatmap for query later.
|
|
||||||
if let Some(t) = last_beatmap {
|
|
||||||
super::cache::save_beatmap(&*ctx.data.read().await, msg.channel_id, &t)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -94,14 +85,9 @@ fn handle_old_links<'a>(
|
||||||
.captures_iter(content)
|
.captures_iter(content)
|
||||||
.map(move |capture| async move {
|
.map(move |capture| async move {
|
||||||
let data = ctx.data.read().await;
|
let data = ctx.data.read().await;
|
||||||
let osu = data.get::<OsuClient>().unwrap();
|
|
||||||
let cache = data.get::<BeatmapCache>().unwrap();
|
let cache = data.get::<BeatmapCache>().unwrap();
|
||||||
|
let osu = data.get::<BeatmapMetaCache>().unwrap();
|
||||||
let req_type = capture.name("link_type").unwrap().as_str();
|
let req_type = capture.name("link_type").unwrap().as_str();
|
||||||
let req = match req_type {
|
|
||||||
"b" => BeatmapRequestKind::Beatmap(capture["id"].parse()?),
|
|
||||||
"s" => BeatmapRequestKind::Beatmapset(capture["id"].parse()?),
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
let mode = capture
|
let mode = capture
|
||||||
.name("mode")
|
.name("mode")
|
||||||
.map(|v| v.as_str().parse())
|
.map(|v| v.as_str().parse())
|
||||||
|
@ -115,12 +101,14 @@ fn handle_old_links<'a>(
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
let beatmaps = osu
|
let beatmaps = match req_type {
|
||||||
.beatmaps(req, |v| match mode {
|
"b" => vec![match mode {
|
||||||
Some(m) => v.mode(m, true),
|
Some(mode) => osu.get_beatmap(capture["id"].parse()?, mode).await?,
|
||||||
None => v,
|
None => osu.get_beatmap_default(capture["id"].parse()?).await?,
|
||||||
})
|
}],
|
||||||
.await?;
|
"s" => osu.get_beatmapset(capture["id"].parse()?).await?,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
if beatmaps.is_empty() {
|
if beatmaps.is_empty() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
@ -130,7 +118,7 @@ fn handle_old_links<'a>(
|
||||||
// collect beatmap info
|
// collect beatmap info
|
||||||
let mods = capture
|
let mods = capture
|
||||||
.name("mods")
|
.name("mods")
|
||||||
.map(|v| Mods::from_str(v.as_str()).ok())
|
.map(|v| Mods::from_str(v.as_str()).pls_ok())
|
||||||
.flatten()
|
.flatten()
|
||||||
.unwrap_or(Mods::NOMOD);
|
.unwrap_or(Mods::NOMOD);
|
||||||
let info = match mode.unwrap_or(b.mode).to_oppai_mode() {
|
let info = match mode.unwrap_or(b.mode).to_oppai_mode() {
|
||||||
|
@ -138,7 +126,7 @@ fn handle_old_links<'a>(
|
||||||
.get_beatmap(b.beatmap_id)
|
.get_beatmap(b.beatmap_id)
|
||||||
.await
|
.await
|
||||||
.and_then(|b| b.get_info_with(Some(mode), mods))
|
.and_then(|b| b.get_info_with(Some(mode), mods))
|
||||||
.ok(),
|
.pls_ok(),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
Some(ToPrint {
|
Some(ToPrint {
|
||||||
|
@ -176,24 +164,22 @@ fn handle_new_links<'a>(
|
||||||
.captures_iter(content)
|
.captures_iter(content)
|
||||||
.map(|capture| async move {
|
.map(|capture| async move {
|
||||||
let data = ctx.data.read().await;
|
let data = ctx.data.read().await;
|
||||||
let osu = data.get::<OsuClient>().unwrap();
|
let osu = data.get::<BeatmapMetaCache>().unwrap();
|
||||||
let cache = data.get::<BeatmapCache>().unwrap();
|
let cache = data.get::<BeatmapCache>().unwrap();
|
||||||
let mode = capture
|
let mode = capture
|
||||||
.name("mode")
|
.name("mode")
|
||||||
.and_then(|v| Mode::parse_from_new_site(v.as_str()));
|
.and_then(|v| Mode::parse_from_new_site(v.as_str()));
|
||||||
let link = capture.get(0).unwrap().as_str();
|
let link = capture.get(0).unwrap().as_str();
|
||||||
let req = match capture.name("beatmap_id") {
|
let beatmaps = match capture.name("beatmap_id") {
|
||||||
Some(ref v) => BeatmapRequestKind::Beatmap(v.as_str().parse()?),
|
Some(ref v) => vec![match mode {
|
||||||
None => BeatmapRequestKind::Beatmapset(
|
Some(mode) => osu.get_beatmap(v.as_str().parse()?, mode).await?,
|
||||||
capture.name("set_id").unwrap().as_str().parse()?,
|
None => osu.get_beatmap_default(v.as_str().parse()?).await?,
|
||||||
),
|
}],
|
||||||
|
None => {
|
||||||
|
osu.get_beatmapset(capture.name("set_id").unwrap().as_str().parse()?)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let beatmaps = osu
|
|
||||||
.beatmaps(req, |v| match mode {
|
|
||||||
Some(m) => v.mode(m, true),
|
|
||||||
None => v,
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
if beatmaps.is_empty() {
|
if beatmaps.is_empty() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
@ -203,14 +189,14 @@ fn handle_new_links<'a>(
|
||||||
// collect beatmap info
|
// collect beatmap info
|
||||||
let mods = capture
|
let mods = capture
|
||||||
.name("mods")
|
.name("mods")
|
||||||
.and_then(|v| Mods::from_str(v.as_str()).ok())
|
.and_then(|v| Mods::from_str(v.as_str()).pls_ok())
|
||||||
.unwrap_or(Mods::NOMOD);
|
.unwrap_or(Mods::NOMOD);
|
||||||
let info = match mode.unwrap_or(beatmap.mode).to_oppai_mode() {
|
let info = match mode.unwrap_or(beatmap.mode).to_oppai_mode() {
|
||||||
Some(mode) => cache
|
Some(mode) => cache
|
||||||
.get_beatmap(beatmap.beatmap_id)
|
.get_beatmap(beatmap.beatmap_id)
|
||||||
.await
|
.await
|
||||||
.and_then(|b| b.get_info_with(Some(mode), mods))
|
.and_then(|b| b.get_info_with(Some(mode), mods))
|
||||||
.ok(),
|
.pls_ok(),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
Some(ToPrint {
|
Some(ToPrint {
|
||||||
|
@ -269,14 +255,14 @@ fn handle_short_links<'a>(
|
||||||
}?;
|
}?;
|
||||||
let mods = capture
|
let mods = capture
|
||||||
.name("mods")
|
.name("mods")
|
||||||
.and_then(|v| Mods::from_str(v.as_str()).ok())
|
.and_then(|v| Mods::from_str(v.as_str()).pls_ok())
|
||||||
.unwrap_or(Mods::NOMOD);
|
.unwrap_or(Mods::NOMOD);
|
||||||
let info = match mode.unwrap_or(beatmap.mode).to_oppai_mode() {
|
let info = match mode.unwrap_or(beatmap.mode).to_oppai_mode() {
|
||||||
Some(mode) => cache
|
Some(mode) => cache
|
||||||
.get_beatmap(beatmap.beatmap_id)
|
.get_beatmap(beatmap.beatmap_id)
|
||||||
.await
|
.await
|
||||||
.and_then(|b| b.get_info_with(Some(mode), mods))
|
.and_then(|b| b.get_info_with(Some(mode), mods))
|
||||||
.ok(),
|
.pls_ok(),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
let r: Result<_> = Ok(ToPrint {
|
let r: Result<_> = Ok(ToPrint {
|
||||||
|
@ -298,14 +284,18 @@ fn handle_short_links<'a>(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_beatmap<'a, 'b>(
|
async fn handle_beatmap<'a, 'b>(
|
||||||
|
ctx: &Context,
|
||||||
beatmap: &Beatmap,
|
beatmap: &Beatmap,
|
||||||
info: Option<BeatmapInfo>,
|
info: Option<BeatmapInfo>,
|
||||||
link: &'_ str,
|
link: &'_ str,
|
||||||
mode: Option<Mode>,
|
mode: Option<Mode>,
|
||||||
mods: Mods,
|
mods: Mods,
|
||||||
m: &'a mut CreateMessage<'b>,
|
reply_to: &Message,
|
||||||
) -> &'a mut CreateMessage<'b> {
|
) -> Result<()> {
|
||||||
|
reply_to
|
||||||
|
.channel_id
|
||||||
|
.send_message(ctx, |m| {
|
||||||
m.content(
|
m.content(
|
||||||
MessageBuilder::new()
|
MessageBuilder::new()
|
||||||
.push("Beatmap information for ")
|
.push("Beatmap information for ")
|
||||||
|
@ -313,25 +303,28 @@ fn handle_beatmap<'a, 'b>(
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
.embed(|b| beatmap_embed(beatmap, mode.unwrap_or(beatmap.mode), mods, info, b))
|
.embed(|b| beatmap_embed(beatmap, mode.unwrap_or(beatmap.mode), mods, info, b))
|
||||||
|
.reference_message(reply_to)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_beatmapset<'a, 'b>(
|
async fn handle_beatmapset<'a, 'b>(
|
||||||
|
ctx: &Context,
|
||||||
beatmaps: Vec<Beatmap>,
|
beatmaps: Vec<Beatmap>,
|
||||||
link: &'_ str,
|
link: &'_ str,
|
||||||
mode: Option<Mode>,
|
mode: Option<Mode>,
|
||||||
m: &'a mut CreateMessage<'b>,
|
reply_to: &Message,
|
||||||
) -> &'a mut CreateMessage<'b> {
|
) -> Result<()> {
|
||||||
let mut beatmaps = beatmaps;
|
crate::discord::display::display_beatmapset(
|
||||||
beatmaps.sort_by(|a, b| {
|
&ctx,
|
||||||
(mode.unwrap_or(a.mode) as u8, a.difficulty.stars)
|
beatmaps,
|
||||||
.partial_cmp(&(mode.unwrap_or(b.mode) as u8, b.difficulty.stars))
|
mode,
|
||||||
.unwrap()
|
None,
|
||||||
});
|
reply_to,
|
||||||
m.content(
|
format!("Beatmapset information for `{}`", link),
|
||||||
MessageBuilder::new()
|
|
||||||
.push("Beatmapset information for ")
|
|
||||||
.push_mono_safe(link)
|
|
||||||
.build(),
|
|
||||||
)
|
)
|
||||||
.embed(|b| beatmapset_embed(&beatmaps, mode, b))
|
.await
|
||||||
|
.pls_ok();
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ use crate::{
|
||||||
use serenity::{
|
use serenity::{
|
||||||
framework::standard::{
|
framework::standard::{
|
||||||
macros::{command, group},
|
macros::{command, group},
|
||||||
Args, CommandError as Error, CommandResult,
|
Args, CommandResult,
|
||||||
},
|
},
|
||||||
model::channel::Message,
|
model::channel::Message,
|
||||||
utils::MessageBuilder,
|
utils::MessageBuilder,
|
||||||
|
@ -20,6 +20,7 @@ mod announcer;
|
||||||
pub(crate) mod beatmap_cache;
|
pub(crate) mod beatmap_cache;
|
||||||
mod cache;
|
mod cache;
|
||||||
mod db;
|
mod db;
|
||||||
|
pub(crate) mod display;
|
||||||
pub(crate) mod embeds;
|
pub(crate) mod embeds;
|
||||||
mod hook;
|
mod hook;
|
||||||
pub(crate) mod oppai_cache;
|
pub(crate) mod oppai_cache;
|
||||||
|
@ -292,7 +293,7 @@ fn to_user_id_query(
|
||||||
db.get(&id)
|
db.get(&id)
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(|u| UserID::ID(u.id))
|
.map(|u| UserID::ID(u.id))
|
||||||
.ok_or(Error::from("No saved account found"))
|
.ok_or(Error::msg("No saved account found"))
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Nth {
|
enum Nth {
|
||||||
|
@ -306,7 +307,7 @@ impl FromStr for Nth {
|
||||||
if s == "--all" || s == "-a" || s == "##" {
|
if s == "--all" || s == "-a" || s == "##" {
|
||||||
Ok(Nth::All)
|
Ok(Nth::All)
|
||||||
} else if !s.starts_with("#") {
|
} else if !s.starts_with("#") {
|
||||||
Err(Error::from("Not an order"))
|
Err(Error::msg("Not an order"))
|
||||||
} else {
|
} else {
|
||||||
let v = s.split_at("#".len()).1.parse()?;
|
let v = s.split_at("#".len()).1.parse()?;
|
||||||
Ok(Nth::Nth(v))
|
Ok(Nth::Nth(v))
|
||||||
|
@ -328,7 +329,7 @@ async fn list_plays<'a>(
|
||||||
|
|
||||||
const ITEMS_PER_PAGE: usize = 5;
|
const ITEMS_PER_PAGE: usize = 5;
|
||||||
let total_pages = (plays.len() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE;
|
let total_pages = (plays.len() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE;
|
||||||
paginate_fn(
|
paginate_reply_fn(
|
||||||
move |page, ctx, msg| {
|
move |page, ctx, msg| {
|
||||||
let plays = plays.clone();
|
let plays = plays.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
@ -464,7 +465,7 @@ async fn list_plays<'a>(
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
ctx,
|
ctx,
|
||||||
m.channel_id,
|
m,
|
||||||
std::time::Duration::from_secs(60),
|
std::time::Duration::from_secs(60),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -488,7 +489,7 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
|
||||||
let user = osu
|
let user = osu
|
||||||
.user(user, |f| f.mode(mode))
|
.user(user, |f| f.mode(mode))
|
||||||
.await?
|
.await?
|
||||||
.ok_or(Error::from("User not found"))?;
|
.ok_or(Error::msg("User not found"))?;
|
||||||
match nth {
|
match nth {
|
||||||
Nth::Nth(nth) => {
|
Nth::Nth(nth) => {
|
||||||
let recent_play = osu
|
let recent_play = osu
|
||||||
|
@ -496,18 +497,18 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.last()
|
.last()
|
||||||
.ok_or(Error::from("No such play"))?;
|
.ok_or(Error::msg("No such play"))?;
|
||||||
let beatmap = meta_cache.get_beatmap(recent_play.beatmap_id, mode).await?;
|
let beatmap = meta_cache.get_beatmap(recent_play.beatmap_id, mode).await?;
|
||||||
let content = oppai.get_beatmap(beatmap.beatmap_id).await?;
|
let content = oppai.get_beatmap(beatmap.beatmap_id).await?;
|
||||||
let beatmap_mode = BeatmapWithMode(beatmap, mode);
|
let beatmap_mode = BeatmapWithMode(beatmap, mode);
|
||||||
|
|
||||||
msg.channel_id
|
msg.channel_id
|
||||||
.send_message(&ctx, |m| {
|
.send_message(&ctx, |m| {
|
||||||
m.content(format!(
|
m.content(format!("Here is the play that you requested",))
|
||||||
"{}: here is the play that you requested",
|
.embed(|m| {
|
||||||
msg.author
|
score_embed(&recent_play, &beatmap_mode, &content, &user).build(m)
|
||||||
))
|
})
|
||||||
.embed(|m| score_embed(&recent_play, &beatmap_mode, &content, &user).build(m))
|
.reference_message(msg)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -524,17 +525,46 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
|
||||||
Ok(())
|
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")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
#[description = "Show information from the last queried beatmap."]
|
#[description = "Show information from the last queried beatmap."]
|
||||||
#[usage = "[mods = no mod]"]
|
#[usage = "[--set/-s/--beatmapset] / [mods = no mod]"]
|
||||||
#[max_args(1)]
|
#[max_args(2)]
|
||||||
pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
let data = ctx.data.read().await;
|
let data = ctx.data.read().await;
|
||||||
let b = cache::get_beatmap(&*data, msg.channel_id)?;
|
let b = cache::get_beatmap(&*data, msg.channel_id)?;
|
||||||
|
let beatmapset = args.find::<OptBeatmapset>().is_ok();
|
||||||
|
|
||||||
match b {
|
match b {
|
||||||
Some(BeatmapWithMode(b, m)) => {
|
Some(BeatmapWithMode(b, m)) => {
|
||||||
let mods = args.find::<Mods>().unwrap_or(Mods::NOMOD);
|
let mods = args.find::<Mods>().unwrap_or(Mods::NOMOD);
|
||||||
|
if beatmapset {
|
||||||
|
let beatmap_cache = data.get::<BeatmapMetaCache>().unwrap();
|
||||||
|
let beatmapset = beatmap_cache.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 = data
|
let info = data
|
||||||
.get::<BeatmapCache>()
|
.get::<BeatmapCache>()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -544,11 +574,9 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
.ok();
|
.ok();
|
||||||
msg.channel_id
|
msg.channel_id
|
||||||
.send_message(&ctx, |f| {
|
.send_message(&ctx, |f| {
|
||||||
f.content(format!(
|
f.content("Here is the beatmap you requested!")
|
||||||
"{}: here is the beatmap you requested!",
|
|
||||||
msg.author
|
|
||||||
))
|
|
||||||
.embed(|c| beatmap_embed(&b, m, mods, info, c))
|
.embed(|c| beatmap_embed(&b, m, mods, info, c))
|
||||||
|
.reference_message(msg)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
@ -594,7 +622,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
|
||||||
let user = osu
|
let user = osu
|
||||||
.user(user, |f| f)
|
.user(user, |f| f)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(Error::from("User not found"))?;
|
.ok_or(Error::msg("User not found"))?;
|
||||||
let scores = osu
|
let scores = osu
|
||||||
.scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(m))
|
.scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(m))
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -646,7 +674,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
let user = osu
|
let user = osu
|
||||||
.user(user, |f| f.mode(mode))
|
.user(user, |f| f.mode(mode))
|
||||||
.await?
|
.await?
|
||||||
.ok_or(Error::from("User not found"))?;
|
.ok_or(Error::msg("User not found"))?;
|
||||||
|
|
||||||
match nth {
|
match nth {
|
||||||
Nth::Nth(nth) => {
|
Nth::Nth(nth) => {
|
||||||
|
@ -659,7 +687,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
let top_play = top_play
|
let top_play = top_play
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.last()
|
.last()
|
||||||
.ok_or(Error::from("No such play"))?;
|
.ok_or(Error::msg("No such play"))?;
|
||||||
let beatmap = meta_cache.get_beatmap(top_play.beatmap_id, mode).await?;
|
let beatmap = meta_cache.get_beatmap(top_play.beatmap_id, mode).await?;
|
||||||
let content = oppai.get_beatmap(beatmap.beatmap_id).await?;
|
let content = oppai.get_beatmap(beatmap.beatmap_id).await?;
|
||||||
let beatmap = BeatmapWithMode(beatmap, mode);
|
let beatmap = BeatmapWithMode(beatmap, mode);
|
||||||
|
|
|
@ -60,7 +60,7 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR
|
||||||
|
|
||||||
let users = std::sync::Arc::new(users);
|
let users = std::sync::Arc::new(users);
|
||||||
let last_update = last_update.unwrap();
|
let last_update = last_update.unwrap();
|
||||||
paginate_fn(
|
paginate_reply_fn(
|
||||||
move |page: u8, ctx: &Context, m: &mut Message| {
|
move |page: u8, ctx: &Context, m: &mut Message| {
|
||||||
const ITEMS_PER_PAGE: usize = 10;
|
const ITEMS_PER_PAGE: usize = 10;
|
||||||
let users = users.clone();
|
let users = users.clone();
|
||||||
|
@ -98,7 +98,7 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
ctx,
|
ctx,
|
||||||
m.channel_id,
|
m,
|
||||||
std::time::Duration::from_secs(60),
|
std::time::Duration::from_secs(60),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -380,7 +380,7 @@ async fn show_leaderboard(
|
||||||
.await?;
|
.await?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
paginate_fn(
|
paginate_reply_fn(
|
||||||
move |page: u8, ctx: &Context, m: &mut Message| {
|
move |page: u8, ctx: &Context, m: &mut Message| {
|
||||||
const ITEMS_PER_PAGE: usize = 5;
|
const ITEMS_PER_PAGE: usize = 5;
|
||||||
let start = (page as usize) * ITEMS_PER_PAGE;
|
let start = (page as usize) * ITEMS_PER_PAGE;
|
||||||
|
@ -516,7 +516,7 @@ async fn show_leaderboard(
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
ctx,
|
ctx,
|
||||||
m.channel_id,
|
m,
|
||||||
std::time::Duration::from_secs(60),
|
std::time::Duration::from_secs(60),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -74,6 +74,10 @@ impl std::str::FromStr for Mods {
|
||||||
type Err = String;
|
type Err = String;
|
||||||
fn from_str(mut s: &str) -> Result<Self, Self::Err> {
|
fn from_str(mut s: &str) -> Result<Self, Self::Err> {
|
||||||
let mut res = Self::default();
|
let mut res = Self::default();
|
||||||
|
// Strip leading +
|
||||||
|
if s.starts_with("+") {
|
||||||
|
s = &s[1..];
|
||||||
|
}
|
||||||
while s.len() >= 2 {
|
while s.len() >= 2 {
|
||||||
let (m, nw) = s.split_at(2);
|
let (m, nw) = s.split_at(2);
|
||||||
s = nw;
|
s = nw;
|
||||||
|
@ -87,7 +91,7 @@ impl std::str::FromStr for Mods {
|
||||||
"DT" => res |= Mods::DT,
|
"DT" => res |= Mods::DT,
|
||||||
"RX" => res |= Mods::RX,
|
"RX" => res |= Mods::RX,
|
||||||
"HT" => res |= Mods::HT,
|
"HT" => res |= Mods::HT,
|
||||||
"NC" => res |= Mods::NC,
|
"NC" => res |= Mods::NC | Mods::DT,
|
||||||
"FL" => res |= Mods::FL,
|
"FL" => res |= Mods::FL,
|
||||||
"AT" => res |= Mods::AT,
|
"AT" => res |= Mods::AT,
|
||||||
"SO" => res |= Mods::SO,
|
"SO" => res |= Mods::SO,
|
||||||
|
@ -121,9 +125,13 @@ impl fmt::Display for Mods {
|
||||||
}
|
}
|
||||||
write!(f, "+")?;
|
write!(f, "+")?;
|
||||||
for p in MODS_WITH_NAMES.iter() {
|
for p in MODS_WITH_NAMES.iter() {
|
||||||
if self.contains(p.0) {
|
if !self.contains(p.0) {
|
||||||
write!(f, "{}", p.1)?;
|
continue;
|
||||||
}
|
}
|
||||||
|
if p.0 == Mods::DT && self.contains(Mods::NC) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
write!(f, "{}", p.1)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,13 +15,14 @@ pub use announcer::{Announcer, AnnouncerHandler};
|
||||||
pub use args::{Duration, UsernameArg};
|
pub use args::{Duration, UsernameArg};
|
||||||
pub use hook::Hook;
|
pub use hook::Hook;
|
||||||
pub use member_cache::MemberCache;
|
pub use member_cache::MemberCache;
|
||||||
pub use pagination::{paginate, paginate_fn};
|
pub use pagination::{paginate, paginate_fn, paginate_reply, paginate_reply_fn, Paginate};
|
||||||
|
|
||||||
/// Re-exporting async_trait helps with implementing Announcer.
|
/// Re-exporting async_trait helps with implementing Announcer.
|
||||||
pub use async_trait::async_trait;
|
pub use async_trait::async_trait;
|
||||||
|
|
||||||
/// Re-export the anyhow errors
|
/// Re-export the anyhow errors
|
||||||
pub use anyhow::{Error, Result};
|
pub use anyhow::{Error, Result};
|
||||||
|
pub use debugging_ok::OkPrint;
|
||||||
|
|
||||||
/// Re-export useful future and stream utils
|
/// Re-export useful future and stream utils
|
||||||
pub use futures_util::{future, stream, FutureExt, StreamExt, TryFutureExt, TryStreamExt};
|
pub use futures_util::{future, stream, FutureExt, StreamExt, TryFutureExt, TryStreamExt};
|
||||||
|
@ -63,3 +64,24 @@ pub mod prelude_commands {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod debugging_ok {
|
||||||
|
pub trait OkPrint {
|
||||||
|
type Output;
|
||||||
|
fn pls_ok(self) -> Option<Self::Output>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E: std::fmt::Debug> OkPrint for Result<T, E> {
|
||||||
|
type Output = T;
|
||||||
|
|
||||||
|
fn pls_ok(self) -> Option<Self::Output> {
|
||||||
|
match self {
|
||||||
|
Ok(v) => Some(v),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error: {:?}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -13,9 +13,32 @@ use tokio::time as tokio_time;
|
||||||
const ARROW_RIGHT: &'static str = "➡️";
|
const ARROW_RIGHT: &'static str = "➡️";
|
||||||
const ARROW_LEFT: &'static str = "⬅️";
|
const ARROW_LEFT: &'static str = "⬅️";
|
||||||
|
|
||||||
|
/// A trait that provides the implementation of a paginator.
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait Paginate: Send {
|
pub trait Paginate: Send + Sized {
|
||||||
|
/// Render the given page.
|
||||||
async fn render(&mut self, page: u8, ctx: &Context, m: &mut Message) -> Result<bool>;
|
async fn render(&mut self, page: u8, ctx: &Context, m: &mut Message) -> Result<bool>;
|
||||||
|
|
||||||
|
/// Any setting-up before the rendering stage.
|
||||||
|
async fn prerender(&mut self, _ctx: &Context, _m: &mut Message) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle the incoming reaction. Defaults to calling `handle_pagination_reaction`, but you can do some additional handling
|
||||||
|
/// before handing the functionality over.
|
||||||
|
///
|
||||||
|
/// Return the resulting current page, or `None` if the pagination should stop.
|
||||||
|
async fn handle_reaction(
|
||||||
|
&mut self,
|
||||||
|
page: u8,
|
||||||
|
ctx: &Context,
|
||||||
|
message: &mut Message,
|
||||||
|
reaction: &ReactionAction,
|
||||||
|
) -> Result<Option<u8>> {
|
||||||
|
handle_pagination_reaction(page, self, ctx, message, reaction)
|
||||||
|
.await
|
||||||
|
.map(Some)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
|
@ -33,17 +56,40 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Paginate! with a pager function, and replying to a message.
|
||||||
|
/// If awaited, will block until everything is done.
|
||||||
|
pub async fn paginate_reply(
|
||||||
|
pager: impl Paginate,
|
||||||
|
ctx: &Context,
|
||||||
|
reply_to: &Message,
|
||||||
|
timeout: std::time::Duration,
|
||||||
|
) -> Result<()> {
|
||||||
|
let message = reply_to
|
||||||
|
.reply(&ctx, "Youmu is loading the first page...")
|
||||||
|
.await?;
|
||||||
|
paginate_with_first_message(pager, ctx, message, timeout).await
|
||||||
|
}
|
||||||
|
|
||||||
// Paginate! with a pager function.
|
// Paginate! with a pager function.
|
||||||
/// If awaited, will block until everything is done.
|
/// If awaited, will block until everything is done.
|
||||||
pub async fn paginate(
|
pub async fn paginate(
|
||||||
mut pager: impl Paginate,
|
pager: impl Paginate,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
channel: ChannelId,
|
channel: ChannelId,
|
||||||
timeout: std::time::Duration,
|
timeout: std::time::Duration,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut message = channel
|
let message = channel
|
||||||
.send_message(&ctx, |e| e.content("Youmu is loading the first page..."))
|
.send_message(&ctx, |e| e.content("Youmu is loading the first page..."))
|
||||||
.await?;
|
.await?;
|
||||||
|
paginate_with_first_message(pager, ctx, message, timeout).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn paginate_with_first_message(
|
||||||
|
mut pager: impl Paginate,
|
||||||
|
ctx: &Context,
|
||||||
|
mut message: Message,
|
||||||
|
timeout: std::time::Duration,
|
||||||
|
) -> Result<()> {
|
||||||
// React to the message
|
// React to the message
|
||||||
message
|
message
|
||||||
.react(&ctx, ReactionType::try_from(ARROW_LEFT)?)
|
.react(&ctx, ReactionType::try_from(ARROW_LEFT)?)
|
||||||
|
@ -51,6 +97,7 @@ pub async fn paginate(
|
||||||
message
|
message
|
||||||
.react(&ctx, ReactionType::try_from(ARROW_RIGHT)?)
|
.react(&ctx, ReactionType::try_from(ARROW_RIGHT)?)
|
||||||
.await?;
|
.await?;
|
||||||
|
pager.prerender(&ctx, &mut message).await?;
|
||||||
pager.render(0, ctx, &mut message).await?;
|
pager.render(0, ctx, &mut message).await?;
|
||||||
// Build a reaction collector
|
// Build a reaction collector
|
||||||
let mut reaction_collector = message.await_reactions(&ctx).removed(true).await;
|
let mut reaction_collector = message.await_reactions(&ctx).removed(true).await;
|
||||||
|
@ -62,8 +109,12 @@ pub async fn paginate(
|
||||||
Err(_) => break Ok(()),
|
Err(_) => break Ok(()),
|
||||||
Ok(None) => break Ok(()),
|
Ok(None) => break Ok(()),
|
||||||
Ok(Some(reaction)) => {
|
Ok(Some(reaction)) => {
|
||||||
page = match handle_reaction(page, &mut pager, ctx, &mut message, &reaction).await {
|
page = match pager
|
||||||
Ok(v) => v,
|
.handle_reaction(page, ctx, &mut message, &reaction)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Some(v)) => v,
|
||||||
|
Ok(None) => break Ok(()),
|
||||||
Err(e) => break Err(e),
|
Err(e) => break Err(e),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -90,8 +141,23 @@ pub async fn paginate_fn(
|
||||||
paginate(pager, ctx, channel, timeout).await
|
paginate(pager, ctx, channel, timeout).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Same as `paginate_reply`, but for function inputs, especially anonymous functions.
|
||||||
|
pub async fn paginate_reply_fn(
|
||||||
|
pager: impl for<'m> FnMut(
|
||||||
|
u8,
|
||||||
|
&'m Context,
|
||||||
|
&'m mut Message,
|
||||||
|
) -> std::pin::Pin<Box<dyn Future<Output = Result<bool>> + Send + 'm>>
|
||||||
|
+ Send,
|
||||||
|
ctx: &Context,
|
||||||
|
reply_to: &Message,
|
||||||
|
timeout: std::time::Duration,
|
||||||
|
) -> Result<()> {
|
||||||
|
paginate_reply(pager, ctx, reply_to, timeout).await
|
||||||
|
}
|
||||||
|
|
||||||
// Handle the reaction and return a new page number.
|
// Handle the reaction and return a new page number.
|
||||||
async fn handle_reaction(
|
pub async fn handle_pagination_reaction(
|
||||||
page: u8,
|
page: u8,
|
||||||
pager: &mut impl Paginate,
|
pager: &mut impl Paginate,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
|
|
Loading…
Add table
Reference in a new issue