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 last_updated = ranks.iter().map(|(_, cfu)| cfu.last_update).min().unwrap();
paginate_fn(
paginate_reply_fn(
move |page, ctx, msg| {
let ranks = ranks.clone();
Box::pin(async move {
@ -237,7 +237,7 @@ pub async fn ranks(ctx: &Context, m: &Message) -> CommandResult {
})
},
ctx,
m.channel_id,
m,
std::time::Duration::from_secs(60),
)
.await?;
@ -301,7 +301,7 @@ pub async fn contestranks(ctx: &Context, m: &Message, mut args: Args) -> Command
const ITEMS_PER_PAGE: usize = 10;
let total_pages = (ranks.len() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE;
paginate_fn(
paginate_reply_fn(
move |page, ctx, msg| {
let contest = contest.clone();
let problems = problems.clone();
@ -391,7 +391,7 @@ pub async fn contestranks(ctx: &Context, m: &Message, mut args: Args) -> Command
})
},
ctx,
m.channel_id,
m,
Duration::from_secs(60),
)
.await?;

View file

@ -114,6 +114,7 @@ pub async fn choose(ctx: &Context, m: &Message, mut args: Args) -> CommandResult
.push(". Congrats! 🎉 🎊 🥳")
.build(),
)
.reference_message(m)
})
.await?;

View file

@ -33,7 +33,7 @@ async fn list(ctx: &Context, m: &Message, _: Args) -> CommandResult {
const ROLES_PER_PAGE: usize = 8;
let pages = (roles.len() + ROLES_PER_PAGE - 1) / ROLES_PER_PAGE;
paginate_fn(
paginate_reply_fn(
|page, ctx, msg| {
let roles = roles.clone();
Box::pin(async move {
@ -99,7 +99,7 @@ async fn list(ctx: &Context, m: &Message, _: Args) -> CommandResult {
})
},
ctx,
m.channel_id,
m,
std::time::Duration::from_secs(60 * 10),
)
.await?;

View file

@ -64,7 +64,7 @@ async fn message_command(
return Ok(());
}
let images = std::sync::Arc::new(images);
paginate_fn(
paginate_reply_fn(
move |page, ctx, msg: &mut Message| {
let images = images.clone();
Box::pin(async move {
@ -87,7 +87,7 @@ async fn message_command(
})
},
ctx,
msg.channel_id,
msg,
std::time::Duration::from_secs(120),
)
.await?;

View file

@ -144,7 +144,7 @@ impl Announcer {
.filter(|u| u.mode == mode && u.date > last_update)
.map(|ev| CollectedScore::from_event(&*client, &user, ev, user_id, &channels[..]))
.collect::<stream::FuturesUnordered<_>>()
.filter_map(|u| future::ready(u.ok_or_print()))
.filter_map(|u| future::ready(u.pls_ok()))
.collect::<Vec<_>>()
.await;
let top_scores = scores.into_iter().filter_map(|(rank, score)| {
@ -169,7 +169,7 @@ impl Announcer {
.collect::<stream::FuturesUnordered<_>>()
.try_collect::<Vec<_>>()
.await
.ok_or_print();
.pls_ok();
});
Ok(pp)
}
@ -304,7 +304,7 @@ impl<'a> CollectedScore<'a> {
})
})
.await?;
save_beatmap(&*ctx.data.read().await, channel, &bm).ok_or_print();
save_beatmap(&*ctx.data.read().await, channel, &bm).pls_ok();
Ok(m)
}
}
@ -313,22 +313,3 @@ enum ScoreType {
TopRecord(u8),
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 {
client: Arc<Client>,
cache: DashMap<(u64, Mode), Beatmap>,
beatmapsets: DashMap<u64, Vec<u64>>,
}
impl TypeMapKey for BeatmapMetaCache {
@ -23,6 +24,7 @@ impl BeatmapMetaCache {
BeatmapMetaCache {
client,
cache: DashMap::new(),
beatmapsets: DashMap::new(),
}
}
async fn insert_if_possible(&self, id: u64, mode: Option<Mode>) -> Result<Beatmap> {
@ -54,17 +56,47 @@ impl BeatmapMetaCache {
Ok(
match (&[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania])
.iter()
.filter_map(|&mode| {
.find_map(|&mode| {
self.cache
.get(&(id, mode))
.filter(|b| b.mode == mode)
.map(|b| b.clone())
})
.next()
{
}) {
Some(v) => v,
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::{
discord::beatmap_cache::BeatmapMetaCache,
discord::oppai_cache::{BeatmapCache, BeatmapInfo},
models::{Beatmap, Mode, Mods},
request::BeatmapRequestKind,
};
use lazy_static::lazy_static;
use regex::Regex;
use serenity::{builder::CreateMessage, model::channel::Message, utils::MessageBuilder};
use serenity::{model::channel::Message, utils::MessageBuilder};
use std::str::FromStr;
use youmubot_prelude::*;
use super::embeds::{beatmap_embed, beatmapset_embed};
use super::embeds::beatmap_embed;
lazy_static! {
static ref OLD_LINK_REGEX: Regex = Regex::new(
@ -38,39 +36,32 @@ pub fn hook<'a>(
handle_new_links(ctx, &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 {
let mut bm: Option<super::BeatmapWithMode> = None;
msg.channel_id
.send_message(&ctx, |m| match l.embed {
EmbedType::Beatmap(b, info, mods) => {
let t = handle_beatmap(&b, info, l.link, l.mode, mods, m);
let mode = l.mode.unwrap_or(b.mode);
bm = Some(super::BeatmapWithMode(b, mode));
t
}
EmbedType::Beatmapset(b) => handle_beatmapset(b, l.link, l.mode, m),
})
.await?;
let r: Result<_> = Ok(bm);
r
})
.filter_map(|v| async move {
match v {
Ok(v) => v,
Err(e) => {
eprintln!("{}", e);
None
match l.embed {
EmbedType::Beatmap(b, info, mods) => {
handle_beatmap(ctx, &b, info, l.link, l.mode, mods, msg)
.await
.pls_ok();
let mode = l.mode.unwrap_or(b.mode);
let bm = super::BeatmapWithMode(b, mode);
crate::discord::cache::save_beatmap(
&*ctx.data.read().await,
msg.channel_id,
&bm,
)
.pls_ok();
}
EmbedType::Beatmapset(b) => {
handle_beatmapset(ctx, b, l.link, l.mode, msg)
.await
.pls_ok();
}
}
})
.fold(None, |_, v| async move { Some(v) })
.collect::<()>()
.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(())
})
}
@ -94,14 +85,9 @@ fn handle_old_links<'a>(
.captures_iter(content)
.map(move |capture| async move {
let data = ctx.data.read().await;
let osu = data.get::<OsuClient>().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 = match req_type {
"b" => BeatmapRequestKind::Beatmap(capture["id"].parse()?),
"s" => BeatmapRequestKind::Beatmapset(capture["id"].parse()?),
_ => unreachable!(),
};
let mode = capture
.name("mode")
.map(|v| v.as_str().parse())
@ -115,12 +101,14 @@ fn handle_old_links<'a>(
_ => return None,
})
});
let beatmaps = osu
.beatmaps(req, |v| match mode {
Some(m) => v.mode(m, true),
None => v,
})
.await?;
let beatmaps = match req_type {
"b" => vec![match mode {
Some(mode) => osu.get_beatmap(capture["id"].parse()?, mode).await?,
None => osu.get_beatmap_default(capture["id"].parse()?).await?,
}],
"s" => osu.get_beatmapset(capture["id"].parse()?).await?,
_ => unreachable!(),
};
if beatmaps.is_empty() {
return Ok(None);
}
@ -130,7 +118,7 @@ fn handle_old_links<'a>(
// collect beatmap info
let mods = capture
.name("mods")
.map(|v| Mods::from_str(v.as_str()).ok())
.map(|v| Mods::from_str(v.as_str()).pls_ok())
.flatten()
.unwrap_or(Mods::NOMOD);
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)
.await
.and_then(|b| b.get_info_with(Some(mode), mods))
.ok(),
.pls_ok(),
None => None,
};
Some(ToPrint {
@ -176,24 +164,22 @@ fn handle_new_links<'a>(
.captures_iter(content)
.map(|capture| async move {
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 mode = capture
.name("mode")
.and_then(|v| Mode::parse_from_new_site(v.as_str()));
let link = capture.get(0).unwrap().as_str();
let req = match capture.name("beatmap_id") {
Some(ref v) => BeatmapRequestKind::Beatmap(v.as_str().parse()?),
None => BeatmapRequestKind::Beatmapset(
capture.name("set_id").unwrap().as_str().parse()?,
),
let beatmaps = match capture.name("beatmap_id") {
Some(ref v) => vec![match mode {
Some(mode) => osu.get_beatmap(v.as_str().parse()?, mode).await?,
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() {
return Ok(None);
}
@ -203,14 +189,14 @@ fn handle_new_links<'a>(
// collect beatmap info
let mods = capture
.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);
let info = match mode.unwrap_or(beatmap.mode).to_oppai_mode() {
Some(mode) => cache
.get_beatmap(beatmap.beatmap_id)
.await
.and_then(|b| b.get_info_with(Some(mode), mods))
.ok(),
.pls_ok(),
None => None,
};
Some(ToPrint {
@ -269,14 +255,14 @@ fn handle_short_links<'a>(
}?;
let mods = capture
.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);
let info = match mode.unwrap_or(beatmap.mode).to_oppai_mode() {
Some(mode) => cache
.get_beatmap(beatmap.beatmap_id)
.await
.and_then(|b| b.get_info_with(Some(mode), mods))
.ok(),
.pls_ok(),
None => None,
};
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,
info: Option<BeatmapInfo>,
link: &'_ str,
mode: Option<Mode>,
mods: Mods,
m: &'a mut CreateMessage<'b>,
) -> &'a mut CreateMessage<'b> {
m.content(
MessageBuilder::new()
.push("Beatmap information for ")
.push_mono_safe(link)
.build(),
)
.embed(|b| beatmap_embed(beatmap, mode.unwrap_or(beatmap.mode), mods, info, b))
reply_to: &Message,
) -> Result<()> {
reply_to
.channel_id
.send_message(ctx, |m| {
m.content(
MessageBuilder::new()
.push("Beatmap information for ")
.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>,
link: &'_ str,
mode: Option<Mode>,
m: &'a mut CreateMessage<'b>,
) -> &'a mut CreateMessage<'b> {
let mut beatmaps = beatmaps;
beatmaps.sort_by(|a, b| {
(mode.unwrap_or(a.mode) as u8, a.difficulty.stars)
.partial_cmp(&(mode.unwrap_or(b.mode) as u8, b.difficulty.stars))
.unwrap()
});
m.content(
MessageBuilder::new()
.push("Beatmapset information for ")
.push_mono_safe(link)
.build(),
reply_to: &Message,
) -> Result<()> {
crate::discord::display::display_beatmapset(
&ctx,
beatmaps,
mode,
None,
reply_to,
format!("Beatmapset information for `{}`", link),
)
.embed(|b| beatmapset_embed(&beatmaps, mode, b))
.await
.pls_ok();
Ok(())
}

View file

@ -8,7 +8,7 @@ use crate::{
use serenity::{
framework::standard::{
macros::{command, group},
Args, CommandError as Error, CommandResult,
Args, CommandResult,
},
model::channel::Message,
utils::MessageBuilder,
@ -20,6 +20,7 @@ mod announcer;
pub(crate) mod beatmap_cache;
mod cache;
mod db;
pub(crate) mod display;
pub(crate) mod embeds;
mod hook;
pub(crate) mod oppai_cache;
@ -292,7 +293,7 @@ fn to_user_id_query(
db.get(&id)
.cloned()
.map(|u| UserID::ID(u.id))
.ok_or(Error::from("No saved account found"))
.ok_or(Error::msg("No saved account found"))
}
enum Nth {
@ -306,7 +307,7 @@ impl FromStr for Nth {
if s == "--all" || s == "-a" || s == "##" {
Ok(Nth::All)
} else if !s.starts_with("#") {
Err(Error::from("Not an order"))
Err(Error::msg("Not an order"))
} else {
let v = s.split_at("#".len()).1.parse()?;
Ok(Nth::Nth(v))
@ -328,7 +329,7 @@ async fn list_plays<'a>(
const ITEMS_PER_PAGE: usize = 5;
let total_pages = (plays.len() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE;
paginate_fn(
paginate_reply_fn(
move |page, ctx, msg| {
let plays = plays.clone();
Box::pin(async move {
@ -464,7 +465,7 @@ async fn list_plays<'a>(
})
},
ctx,
m.channel_id,
m,
std::time::Duration::from_secs(60),
)
.await?;
@ -488,7 +489,7 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
let user = osu
.user(user, |f| f.mode(mode))
.await?
.ok_or(Error::from("User not found"))?;
.ok_or(Error::msg("User not found"))?;
match nth {
Nth::Nth(nth) => {
let recent_play = osu
@ -496,18 +497,18 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
.await?
.into_iter()
.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 content = oppai.get_beatmap(beatmap.beatmap_id).await?;
let beatmap_mode = BeatmapWithMode(beatmap, mode);
msg.channel_id
.send_message(&ctx, |m| {
m.content(format!(
"{}: here is the play that you requested",
msg.author
))
.embed(|m| score_embed(&recent_play, &beatmap_mode, &content, &user).build(m))
m.content(format!("Here is the play that you requested",))
.embed(|m| {
score_embed(&recent_play, &beatmap_mode, &content, &user).build(m)
})
.reference_message(msg)
})
.await?;
@ -524,17 +525,46 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
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]
#[description = "Show information from the last queried beatmap."]
#[usage = "[mods = no mod]"]
#[max_args(1)]
#[usage = "[--set/-s/--beatmapset] / [mods = no mod]"]
#[max_args(2)]
pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let data = ctx.data.read().await;
let b = cache::get_beatmap(&*data, msg.channel_id)?;
let beatmapset = args.find::<OptBeatmapset>().is_ok();
match b {
Some(BeatmapWithMode(b, m)) => {
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
.get::<BeatmapCache>()
.unwrap()
@ -544,11 +574,9 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
.ok();
msg.channel_id
.send_message(&ctx, |f| {
f.content(format!(
"{}: here is the beatmap you requested!",
msg.author
))
.embed(|c| beatmap_embed(&b, m, mods, info, c))
f.content("Here is the beatmap you requested!")
.embed(|c| beatmap_embed(&b, m, mods, info, c))
.reference_message(msg)
})
.await?;
}
@ -594,7 +622,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
let user = osu
.user(user, |f| f)
.await?
.ok_or(Error::from("User not found"))?;
.ok_or(Error::msg("User not found"))?;
let scores = osu
.scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(m))
.await?;
@ -646,7 +674,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
let user = osu
.user(user, |f| f.mode(mode))
.await?
.ok_or(Error::from("User not found"))?;
.ok_or(Error::msg("User not found"))?;
match 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
.into_iter()
.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 content = oppai.get_beatmap(beatmap.beatmap_id).await?;
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 last_update = last_update.unwrap();
paginate_fn(
paginate_reply_fn(
move |page: u8, ctx: &Context, m: &mut Message| {
const ITEMS_PER_PAGE: usize = 10;
let users = users.clone();
@ -98,7 +98,7 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR
})
},
ctx,
m.channel_id,
m,
std::time::Duration::from_secs(60),
)
.await?;
@ -380,7 +380,7 @@ async fn show_leaderboard(
.await?;
return Ok(());
}
paginate_fn(
paginate_reply_fn(
move |page: u8, ctx: &Context, m: &mut Message| {
const ITEMS_PER_PAGE: usize = 5;
let start = (page as usize) * ITEMS_PER_PAGE;
@ -516,7 +516,7 @@ async fn show_leaderboard(
})
},
ctx,
m.channel_id,
m,
std::time::Duration::from_secs(60),
)
.await?;

View file

@ -74,6 +74,10 @@ impl std::str::FromStr for Mods {
type Err = String;
fn from_str(mut s: &str) -> Result<Self, Self::Err> {
let mut res = Self::default();
// Strip leading +
if s.starts_with("+") {
s = &s[1..];
}
while s.len() >= 2 {
let (m, nw) = s.split_at(2);
s = nw;
@ -87,7 +91,7 @@ impl std::str::FromStr for Mods {
"DT" => res |= Mods::DT,
"RX" => res |= Mods::RX,
"HT" => res |= Mods::HT,
"NC" => res |= Mods::NC,
"NC" => res |= Mods::NC | Mods::DT,
"FL" => res |= Mods::FL,
"AT" => res |= Mods::AT,
"SO" => res |= Mods::SO,
@ -121,9 +125,13 @@ impl fmt::Display for Mods {
}
write!(f, "+")?;
for p in MODS_WITH_NAMES.iter() {
if self.contains(p.0) {
write!(f, "{}", p.1)?;
if !self.contains(p.0) {
continue;
}
if p.0 == Mods::DT && self.contains(Mods::NC) {
continue;
}
write!(f, "{}", p.1)?;
}
Ok(())
}

View file

@ -15,13 +15,14 @@ pub use announcer::{Announcer, AnnouncerHandler};
pub use args::{Duration, UsernameArg};
pub use hook::Hook;
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.
pub use async_trait::async_trait;
/// Re-export the anyhow errors
pub use anyhow::{Error, Result};
pub use debugging_ok::OkPrint;
/// Re-export useful future and stream utils
pub use futures_util::{future, stream, FutureExt, StreamExt, TryFutureExt, TryStreamExt};
@ -63,3 +64,24 @@ pub mod prelude_commands {
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_LEFT: &'static str = "⬅️";
/// A trait that provides the implementation of a paginator.
#[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>;
/// 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]
@ -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.
/// If awaited, will block until everything is done.
pub async fn paginate(
mut pager: impl Paginate,
pager: impl Paginate,
ctx: &Context,
channel: ChannelId,
timeout: std::time::Duration,
) -> Result<()> {
let mut message = channel
let message = channel
.send_message(&ctx, |e| e.content("Youmu is loading the first page..."))
.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
message
.react(&ctx, ReactionType::try_from(ARROW_LEFT)?)
@ -51,6 +97,7 @@ pub async fn paginate(
message
.react(&ctx, ReactionType::try_from(ARROW_RIGHT)?)
.await?;
pager.prerender(&ctx, &mut message).await?;
pager.render(0, ctx, &mut message).await?;
// Build a reaction collector
let mut reaction_collector = message.await_reactions(&ctx).removed(true).await;
@ -62,8 +109,12 @@ pub async fn paginate(
Err(_) => break Ok(()),
Ok(None) => break Ok(()),
Ok(Some(reaction)) => {
page = match handle_reaction(page, &mut pager, ctx, &mut message, &reaction).await {
Ok(v) => v,
page = match pager
.handle_reaction(page, ctx, &mut message, &reaction)
.await
{
Ok(Some(v)) => v,
Ok(None) => break Ok(()),
Err(e) => break Err(e),
};
}
@ -90,8 +141,23 @@ pub async fn paginate_fn(
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.
async fn handle_reaction(
pub async fn handle_pagination_reaction(
page: u8,
pager: &mut impl Paginate,
ctx: &Context,