mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-19 16:58: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 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?;
|
||||
|
|
|
@ -114,6 +114,7 @@ pub async fn choose(ctx: &Context, m: &Message, mut args: Args) -> CommandResult
|
|||
.push(". Congrats! 🎉 🎊 🥳")
|
||||
.build(),
|
||||
)
|
||||
.reference_message(m)
|
||||
})
|
||||
.await?;
|
||||
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
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::{
|
||||
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(())
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue