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:
Natsu Kagami 2021-02-02 02:13:40 +09:00 committed by GitHub
parent 61a71b819c
commit bd845d9662
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 450 additions and 156 deletions

View file

@ -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?;

View file

@ -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?;

View file

@ -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?;

View file

@ -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?;

View file

@ -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
}
}
}
}

View file

@ -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)
}
}
}
} }

View 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)
}
}
}

View file

@ -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 EmbedType::Beatmap(b, info, mods) => {
.send_message(&ctx, |m| match l.embed { handle_beatmap(ctx, &b, info, l.link, l.mode, mods, msg)
EmbedType::Beatmap(b, info, mods) => { .await
let t = handle_beatmap(&b, info, l.link, l.mode, mods, m); .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,
EmbedType::Beatmapset(b) => handle_beatmapset(b, l.link, l.mode, m), msg.channel_id,
}) &bm,
.await?; )
let r: Result<_> = Ok(bm); .pls_ok();
r }
}) EmbedType::Beatmapset(b) => {
.filter_map(|v| async move { handle_beatmapset(ctx, b, l.link, l.mode, msg)
match v { .await
Ok(v) => v, .pls_ok();
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,40 +284,47 @@ 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<()> {
m.content( reply_to
MessageBuilder::new() .channel_id
.push("Beatmap information for ") .send_message(ctx, |m| {
.push_mono_safe(link) m.content(
.build(), MessageBuilder::new()
) .push("Beatmap information for ")
.embed(|b| beatmap_embed(beatmap, mode.unwrap_or(beatmap.mode), mods, info, b)) .push_mono_safe(link)
.build(),
)
.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(())
} }

View file

@ -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!", .embed(|c| beatmap_embed(&b, m, mods, info, c))
msg.author .reference_message(msg)
))
.embed(|c| beatmap_embed(&b, m, mods, info, c))
}) })
.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);

View file

@ -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?;

View file

@ -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(())
} }

View file

@ -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
}
}
}
}
}

View file

@ -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,