mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-19 16:58:55 +00:00
Move Pagination to some generic replyable trait
This commit is contained in:
parent
d5fb2cce69
commit
bcd59c673c
20 changed files with 509 additions and 267 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -3213,6 +3213,7 @@ dependencies = [
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
|
"poise",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -3230,6 +3231,7 @@ dependencies = [
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"flume 0.10.14",
|
"flume 0.10.14",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"poise",
|
||||||
"rand",
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"serenity",
|
"serenity",
|
||||||
|
@ -3280,6 +3282,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serenity",
|
"serenity",
|
||||||
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"youmubot-db",
|
"youmubot-db",
|
||||||
"youmubot-db-sql",
|
"youmubot-db-sql",
|
||||||
|
|
|
@ -10,6 +10,7 @@ serde = { version = "1.0.137", features = ["derive"] }
|
||||||
tokio = { version = "1.19.2", features = ["time"] }
|
tokio = { version = "1.19.2", features = ["time"] }
|
||||||
reqwest = "0.11.10"
|
reqwest = "0.11.10"
|
||||||
serenity = "0.12"
|
serenity = "0.12"
|
||||||
|
poise = "0.6"
|
||||||
Inflector = "0.11.4"
|
Inflector = "0.11.4"
|
||||||
codeforces = "0.3.1"
|
codeforces = "0.3.1"
|
||||||
regex = "1.5.6"
|
regex = "1.5.6"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use codeforces::Contest;
|
use codeforces::Contest;
|
||||||
|
use poise::CreateReply;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
builder::{CreateMessage, EditMessage},
|
builder::{CreateMessage, EditMessage},
|
||||||
framework::standard::{
|
framework::standard::{
|
||||||
|
@ -173,14 +174,14 @@ pub async fn ranks(ctx: &Context, m: &Message) -> CommandResult {
|
||||||
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_reply_fn(
|
paginate_reply_fn(
|
||||||
move |page, ctx, msg| {
|
move |page, _| {
|
||||||
let ranks = ranks.clone();
|
let ranks = ranks.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let page = page as usize;
|
let page = page as usize;
|
||||||
let start = ITEMS_PER_PAGE * page;
|
let start = ITEMS_PER_PAGE * page;
|
||||||
let end = ranks.len().min(start + ITEMS_PER_PAGE);
|
let end = ranks.len().min(start + ITEMS_PER_PAGE);
|
||||||
if start >= end {
|
if start >= end {
|
||||||
return Ok(false);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let ranks = &ranks[start..end];
|
let ranks = &ranks[start..end];
|
||||||
|
|
||||||
|
@ -233,12 +234,11 @@ pub async fn ranks(ctx: &Context, m: &Message) -> CommandResult {
|
||||||
last_updated.to_rfc2822()
|
last_updated.to_rfc2822()
|
||||||
));
|
));
|
||||||
|
|
||||||
msg.edit(ctx, EditMessage::new().content(m.build())).await?;
|
Ok(Some(CreateReply::default().content(m.build())))
|
||||||
Ok(true)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
ctx,
|
ctx,
|
||||||
m,
|
m.clone(),
|
||||||
std::time::Duration::from_secs(60),
|
std::time::Duration::from_secs(60),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -328,7 +328,7 @@ pub(crate) async fn contest_rank_table(
|
||||||
let ranks = Arc::new(ranks);
|
let ranks = Arc::new(ranks);
|
||||||
|
|
||||||
paginate_reply_fn(
|
paginate_reply_fn(
|
||||||
move |page, ctx, msg| {
|
move |page, ctx| {
|
||||||
let contest = contest.clone();
|
let contest = contest.clone();
|
||||||
let problems = problems.clone();
|
let problems = problems.clone();
|
||||||
let ranks = ranks.clone();
|
let ranks = ranks.clone();
|
||||||
|
@ -337,7 +337,7 @@ pub(crate) async fn contest_rank_table(
|
||||||
let start = page * ITEMS_PER_PAGE;
|
let start = page * ITEMS_PER_PAGE;
|
||||||
let end = ranks.len().min(start + ITEMS_PER_PAGE);
|
let end = ranks.len().min(start + ITEMS_PER_PAGE);
|
||||||
if start >= end {
|
if start >= end {
|
||||||
return Ok(false);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let ranks = &ranks[start..end];
|
let ranks = &ranks[start..end];
|
||||||
let hw = ranks
|
let hw = ranks
|
||||||
|
@ -412,12 +412,11 @@ pub(crate) async fn contest_rank_table(
|
||||||
.push_line(contest.url())
|
.push_line(contest.url())
|
||||||
.push_codeblock(table.build(), None)
|
.push_codeblock(table.build(), None)
|
||||||
.push_line(format!("Page **{}/{}**", page + 1, total_pages));
|
.push_line(format!("Page **{}/{}**", page + 1, total_pages));
|
||||||
msg.edit(ctx, EditMessage::new().content(m.build())).await?;
|
Ok(Some(CreateReply::default().content(m.build())))
|
||||||
Ok(true)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
ctx,
|
ctx,
|
||||||
reply_to,
|
reply_to.clone(),
|
||||||
Duration::from_secs(60),
|
Duration::from_secs(60),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -8,6 +8,7 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serenity = { version = "0.12", features = ["collector"] }
|
serenity = { version = "0.12", features = ["collector"] }
|
||||||
|
poise = "0.6"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
serde = { version = "1.0.137", features = ["derive"] }
|
serde = { version = "1.0.137", features = ["derive"] }
|
||||||
chrono = "0.4.19"
|
chrono = "0.4.19"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::db::Roles as DB;
|
use crate::db::Roles as DB;
|
||||||
|
use poise::CreateReply;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
builder::EditMessage,
|
|
||||||
framework::standard::{macros::command, Args, CommandResult},
|
framework::standard::{macros::command, Args, CommandResult},
|
||||||
model::{
|
model::{
|
||||||
channel::{Message, ReactionType},
|
channel::{Message, ReactionType},
|
||||||
|
@ -41,14 +41,14 @@ async fn list(ctx: &Context, m: &Message, _: Args) -> CommandResult {
|
||||||
let pages = (roles.len() + ROLES_PER_PAGE - 1) / ROLES_PER_PAGE;
|
let pages = (roles.len() + ROLES_PER_PAGE - 1) / ROLES_PER_PAGE;
|
||||||
|
|
||||||
paginate_reply_fn(
|
paginate_reply_fn(
|
||||||
|page, ctx, msg| {
|
|page, _| {
|
||||||
let roles = roles.clone();
|
let roles = roles.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let page = page as usize;
|
let page = page as usize;
|
||||||
let start = page * ROLES_PER_PAGE;
|
let start = page * ROLES_PER_PAGE;
|
||||||
let end = roles.len().min(start + ROLES_PER_PAGE);
|
let end = roles.len().min(start + ROLES_PER_PAGE);
|
||||||
if end <= start {
|
if end <= start {
|
||||||
return Ok(false);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let roles = &roles[start..end];
|
let roles = &roles[start..end];
|
||||||
let nw = roles // name width
|
let nw = roles // name width
|
||||||
|
@ -101,13 +101,11 @@ async fn list(ctx: &Context, m: &Message, _: Args) -> CommandResult {
|
||||||
m.push_line("```");
|
m.push_line("```");
|
||||||
m.push(format!("Page **{}/{}**", page + 1, pages));
|
m.push(format!("Page **{}/{}**", page + 1, pages));
|
||||||
|
|
||||||
msg.edit(ctx, EditMessage::new().content(m.to_string()))
|
Ok(Some(CreateReply::default().content(m.to_string())))
|
||||||
.await?;
|
|
||||||
Ok(true)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
ctx,
|
ctx,
|
||||||
m,
|
m.clone(),
|
||||||
std::time::Duration::from_secs(60 * 10),
|
std::time::Duration::from_secs(60 * 10),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use poise::CreateReply;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serenity::builder::EditMessage;
|
use serenity::builder::EditMessage;
|
||||||
use serenity::framework::standard::CommandError as Error;
|
use serenity::framework::standard::CommandError as Error;
|
||||||
|
@ -66,30 +67,24 @@ async fn message_command(
|
||||||
}
|
}
|
||||||
let images = std::sync::Arc::new(images);
|
let images = std::sync::Arc::new(images);
|
||||||
paginate_reply_fn(
|
paginate_reply_fn(
|
||||||
move |page, ctx, msg: &mut Message| {
|
move |page, _| {
|
||||||
let images = images.clone();
|
let images = images.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let page = page as usize;
|
let page = page as usize;
|
||||||
if page >= images.len() {
|
if page >= images.len() {
|
||||||
Ok(false)
|
Ok(None)
|
||||||
} else {
|
} else {
|
||||||
msg.edit(
|
Ok(Some(CreateReply::default().content(format!(
|
||||||
ctx,
|
"[🖼️ **{}/{}**] Here's the image you requested!\n\n{}",
|
||||||
EditMessage::new().content(format!(
|
page + 1,
|
||||||
"[🖼️ **{}/{}**] Here's the image you requested!\n\n{}",
|
images.len(),
|
||||||
page + 1,
|
images[page]
|
||||||
images.len(),
|
))))
|
||||||
images[page]
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| true)
|
|
||||||
.map_err(|e| e.into())
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
ctx,
|
ctx,
|
||||||
msg,
|
msg.clone(),
|
||||||
std::time::Duration::from_secs(120),
|
std::time::Duration::from_secs(120),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -23,6 +23,7 @@ serenity = "0.12"
|
||||||
poise = "0.6"
|
poise = "0.6"
|
||||||
zip = "0.6.2"
|
zip = "0.6.2"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
|
thiserror = "1"
|
||||||
|
|
||||||
youmubot-db = { path = "../youmubot-db" }
|
youmubot-db = { path = "../youmubot-db" }
|
||||||
youmubot-db-sql = { path = "../youmubot-db-sql" }
|
youmubot-db-sql = { path = "../youmubot-db-sql" }
|
||||||
|
|
|
@ -320,9 +320,13 @@ impl<'a> CollectedScore<'a> {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
save_beatmap(&*ctx.data.read().await, channel, bm)
|
save_beatmap(
|
||||||
.await
|
ctx.data.read().await.get::<crate::discord::Env>().unwrap(),
|
||||||
.pls_ok();
|
channel,
|
||||||
|
bm,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.pls_ok();
|
||||||
Ok(m)
|
Ok(m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,21 @@
|
||||||
|
use serenity::all::Member;
|
||||||
use youmubot_prelude::*;
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
|
use crate::{discord::args::ScoreDisplay, models::Mods};
|
||||||
|
|
||||||
|
#[poise::command(slash_command, subcommands("check"))]
|
||||||
|
pub async fn osu<T: AsRef<crate::Env> + Sync>(_ctx: CmdContext<'_, T>) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[poise::command(slash_command)]
|
#[poise::command(slash_command)]
|
||||||
pub async fn example<T: AsRef<crate::Env> + Sync>(
|
/// Check your/someone's score on the last beatmap in the channel
|
||||||
context: poise::Context<'_, T, Error>,
|
async fn check<T: AsRef<crate::Env> + Sync>(
|
||||||
arg: String,
|
ctx: CmdContext<'_, T>,
|
||||||
) -> Result<(), Error> {
|
#[description = "Pass an osu! username to check for scores"] osu_id: Option<String>,
|
||||||
|
#[description = "Pass a member of the guild to check for scores"] member: Option<Member>,
|
||||||
|
#[description = "Filter mods that should appear in the scores returned"] mods: Option<Mods>,
|
||||||
|
#[description = "Score display style"] style: Option<ScoreDisplay>,
|
||||||
|
) -> Result<()> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
70
youmubot-osu/src/discord/args.rs
Normal file
70
youmubot-osu/src/discord/args.rs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
use serenity::all::Message;
|
||||||
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
|
// One of the interaction sources.
|
||||||
|
pub enum InteractionSrc<'a, 'c: 'a, T, E> {
|
||||||
|
Serenity(&'a Message),
|
||||||
|
Poise(&'a poise::Context<'c, T, E>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'c, T, E> InteractionSrc<'a, 'c, T, E> {
|
||||||
|
pub async fn reply(&self, ctx: &Context, msg: impl Into<String>) -> Result<Message> {
|
||||||
|
Ok(match self {
|
||||||
|
InteractionSrc::Serenity(m) => m.reply(ctx, msg).await?,
|
||||||
|
InteractionSrc::Poise(ctx) => ctx.reply(msg).await?.message().await?.into_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'c, T, E> From<&'a Message> for InteractionSrc<'a, 'c, T, E> {
|
||||||
|
fn from(value: &'a Message) -> Self {
|
||||||
|
Self::Serenity(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'c, T, E> From<&'a poise::Context<'c, T, E>> for InteractionSrc<'a, 'c, T, E> {
|
||||||
|
fn from(value: &'a poise::Context<'c, T, E>) -> Self {
|
||||||
|
Self::Poise(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, poise::ChoiceParameter, Default)]
|
||||||
|
pub enum ScoreDisplay {
|
||||||
|
#[name = "table"]
|
||||||
|
#[default]
|
||||||
|
Table,
|
||||||
|
#[name = "grid"]
|
||||||
|
Grid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for ScoreDisplay {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"--table" => Ok(Self::Table),
|
||||||
|
"--grid" => Ok(Self::Grid),
|
||||||
|
_ => Err(Error::unknown(s)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("unknown value: {0}")]
|
||||||
|
UnknownValue(String),
|
||||||
|
#[error("parse error: {0}")]
|
||||||
|
Custom(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
fn unknown(s: impl AsRef<str>) -> Self {
|
||||||
|
Self::UnknownValue(s.as_ref().to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for Error {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Error::Custom(value)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,29 +1,24 @@
|
||||||
use super::db::OsuLastBeatmap;
|
|
||||||
use super::BeatmapWithMode;
|
use super::BeatmapWithMode;
|
||||||
use serenity::model::id::ChannelId;
|
use serenity::model::id::ChannelId;
|
||||||
use youmubot_prelude::*;
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
/// Save the beatmap into the server data storage.
|
/// Save the beatmap into the server data storage.
|
||||||
pub(crate) async fn save_beatmap(
|
pub(crate) async fn save_beatmap(
|
||||||
data: &TypeMap,
|
env: &crate::discord::Env,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
bm: &BeatmapWithMode,
|
bm: &BeatmapWithMode,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
data.get::<OsuLastBeatmap>()
|
env.last_beatmaps.save(channel_id, &bm.0, bm.1).await?;
|
||||||
.unwrap()
|
|
||||||
.save(channel_id, &bm.0, bm.1)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the last beatmap requested from this channel.
|
/// Get the last beatmap requested from this channel.
|
||||||
pub(crate) async fn get_beatmap(
|
pub(crate) async fn get_beatmap(
|
||||||
data: &TypeMap,
|
env: &crate::discord::Env,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
) -> Result<Option<BeatmapWithMode>> {
|
) -> Result<Option<BeatmapWithMode>> {
|
||||||
data.get::<OsuLastBeatmap>()
|
env.last_beatmaps
|
||||||
.unwrap()
|
|
||||||
.by_channel(channel_id)
|
.by_channel(channel_id)
|
||||||
.await
|
.await
|
||||||
.map(|v| v.map(|(bm, mode)| BeatmapWithMode(bm, mode)))
|
.map(|v| v.map(|(bm, mode)| BeatmapWithMode(bm, mode)))
|
||||||
|
|
|
@ -1,47 +1,26 @@
|
||||||
pub use beatmapset::display_beatmapset;
|
pub use beatmapset::display_beatmapset;
|
||||||
pub use scores::ScoreListStyle;
|
pub use scores::ScoreListStyle;
|
||||||
|
|
||||||
mod scores {
|
pub(in crate::discord) mod scores {
|
||||||
use crate::models::{Mode, Score};
|
use crate::models::{Mode, Score};
|
||||||
use serenity::{framework::standard::CommandResult, model::channel::Message};
|
use serenity::{all::ChannelId, framework::standard::CommandResult};
|
||||||
use youmubot_prelude::*;
|
use youmubot_prelude::{replyable::Replyable, *};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
/// The style for the scores list to be displayed.
|
/// The style for the scores list to be displayed.
|
||||||
pub enum ScoreListStyle {
|
pub type ScoreListStyle = crate::discord::args::ScoreDisplay;
|
||||||
Table,
|
|
||||||
Grid,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ScoreListStyle {
|
pub async fn display_scores<'a>(
|
||||||
fn default() -> Self {
|
style: ScoreListStyle,
|
||||||
Self::Table
|
scores: Vec<Score>,
|
||||||
}
|
mode: Mode,
|
||||||
}
|
ctx: &'a Context,
|
||||||
|
m: impl Replyable,
|
||||||
impl std::str::FromStr for ScoreListStyle {
|
channel_id: ChannelId,
|
||||||
type Err = Error;
|
) -> CommandResult {
|
||||||
|
match style {
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
ScoreListStyle::Table => table::display_scores_table(scores, mode, ctx, m).await,
|
||||||
match s {
|
ScoreListStyle::Grid => {
|
||||||
"--table" => Ok(Self::Table),
|
grid::display_scores_grid(scores, mode, ctx, m, channel_id).await
|
||||||
"--grid" => Ok(Self::Grid),
|
|
||||||
_ => Err(Error::msg("unknown value")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScoreListStyle {
|
|
||||||
pub async fn display_scores<'a>(
|
|
||||||
self,
|
|
||||||
scores: Vec<Score>,
|
|
||||||
mode: Mode,
|
|
||||||
ctx: &'a Context,
|
|
||||||
m: &'a Message,
|
|
||||||
) -> CommandResult {
|
|
||||||
match self {
|
|
||||||
ScoreListStyle::Table => table::display_scores_table(scores, mode, ctx, m).await,
|
|
||||||
ScoreListStyle::Grid => grid::display_scores_grid(scores, mode, ctx, m).await,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,15 +30,18 @@ mod scores {
|
||||||
cache::save_beatmap, BeatmapCache, BeatmapMetaCache, BeatmapWithMode,
|
cache::save_beatmap, BeatmapCache, BeatmapMetaCache, BeatmapWithMode,
|
||||||
};
|
};
|
||||||
use crate::models::{Mode, Score};
|
use crate::models::{Mode, Score};
|
||||||
use serenity::builder::EditMessage;
|
use poise::CreateReply;
|
||||||
use serenity::{framework::standard::CommandResult, model::channel::Message};
|
use serenity::all::ChannelId;
|
||||||
|
use serenity::framework::standard::CommandResult;
|
||||||
|
use youmubot_prelude::replyable::Replyable;
|
||||||
use youmubot_prelude::*;
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
pub async fn display_scores_grid<'a>(
|
pub async fn display_scores_grid<'a>(
|
||||||
scores: Vec<Score>,
|
scores: Vec<Score>,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
ctx: &'a Context,
|
ctx: &'a Context,
|
||||||
m: &'a Message,
|
m: impl Replyable,
|
||||||
|
channel_id: ChannelId,
|
||||||
) -> CommandResult {
|
) -> CommandResult {
|
||||||
if scores.is_empty() {
|
if scores.is_empty() {
|
||||||
m.reply(&ctx, "No plays found").await?;
|
m.reply(&ctx, "No plays found").await?;
|
||||||
|
@ -67,7 +49,11 @@ mod scores {
|
||||||
}
|
}
|
||||||
|
|
||||||
paginate_reply(
|
paginate_reply(
|
||||||
Paginate { scores, mode },
|
Paginate {
|
||||||
|
channel_id,
|
||||||
|
scores,
|
||||||
|
mode,
|
||||||
|
},
|
||||||
ctx,
|
ctx,
|
||||||
m,
|
m,
|
||||||
std::time::Duration::from_secs(60),
|
std::time::Duration::from_secs(60),
|
||||||
|
@ -77,13 +63,14 @@ mod scores {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Paginate {
|
pub struct Paginate {
|
||||||
|
channel_id: ChannelId,
|
||||||
scores: Vec<Score>,
|
scores: Vec<Score>,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl pagination::Paginate for Paginate {
|
impl pagination::Paginate for Paginate {
|
||||||
async fn render(&mut self, page: u8, ctx: &Context, msg: &mut Message) -> Result<bool> {
|
async fn render(&mut self, page: u8, ctx: &Context) -> Result<Option<CreateReply>> {
|
||||||
let data = ctx.data.read().await;
|
let data = ctx.data.read().await;
|
||||||
let client = data.get::<crate::discord::OsuClient>().unwrap();
|
let client = data.get::<crate::discord::OsuClient>().unwrap();
|
||||||
let osu = data.get::<BeatmapMetaCache>().unwrap();
|
let osu = data.get::<BeatmapMetaCache>().unwrap();
|
||||||
|
@ -91,7 +78,6 @@ mod scores {
|
||||||
let page = page as usize;
|
let page = page as usize;
|
||||||
let score = &self.scores[page];
|
let score = &self.scores[page];
|
||||||
|
|
||||||
let hourglass = msg.react(ctx, '⌛').await?;
|
|
||||||
let mode = self.mode;
|
let mode = self.mode;
|
||||||
let beatmap = osu.get_beatmap(score.beatmap_id, mode).await?;
|
let beatmap = osu.get_beatmap(score.beatmap_id, mode).await?;
|
||||||
let content = beatmap_cache.get_beatmap(beatmap.beatmap_id).await?;
|
let content = beatmap_cache.get_beatmap(beatmap.beatmap_id).await?;
|
||||||
|
@ -101,20 +87,18 @@ mod scores {
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| Error::msg("user not found"))?;
|
.ok_or_else(|| Error::msg("user not found"))?;
|
||||||
|
|
||||||
msg.edit(
|
let edit = CreateReply::default().embed({
|
||||||
ctx,
|
crate::discord::embeds::score_embed(score, &bm, &content, &user)
|
||||||
EditMessage::new().embed({
|
.footer(format!("Page {}/{}", page + 1, self.scores.len()))
|
||||||
crate::discord::embeds::score_embed(score, &bm, &content, &user)
|
.build()
|
||||||
.footer(format!("Page {}/{}", page + 1, self.scores.len()))
|
});
|
||||||
.build()
|
save_beatmap(
|
||||||
}),
|
ctx.data.read().await.get::<crate::discord::Env>().unwrap(),
|
||||||
|
self.channel_id,
|
||||||
|
&bm,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
save_beatmap(&*ctx.data.read().await, msg.channel_id, &bm).await?;
|
Ok(Some(edit))
|
||||||
|
|
||||||
// End
|
|
||||||
hourglass.delete(ctx).await?;
|
|
||||||
Ok(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn len(&self) -> Option<usize> {
|
fn len(&self) -> Option<usize> {
|
||||||
|
@ -129,15 +113,16 @@ mod scores {
|
||||||
use crate::discord::oppai_cache::Accuracy;
|
use crate::discord::oppai_cache::Accuracy;
|
||||||
use crate::discord::{Beatmap, BeatmapCache, BeatmapInfo, BeatmapMetaCache};
|
use crate::discord::{Beatmap, BeatmapCache, BeatmapInfo, BeatmapMetaCache};
|
||||||
use crate::models::{Mode, Score};
|
use crate::models::{Mode, Score};
|
||||||
use serenity::builder::EditMessage;
|
use poise::CreateReply;
|
||||||
use serenity::{framework::standard::CommandResult, model::channel::Message};
|
use serenity::framework::standard::CommandResult;
|
||||||
|
use youmubot_prelude::replyable::Replyable;
|
||||||
use youmubot_prelude::*;
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
pub async fn display_scores_table<'a>(
|
pub async fn display_scores_table<'a>(
|
||||||
scores: Vec<Score>,
|
scores: Vec<Score>,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
ctx: &'a Context,
|
ctx: &'a Context,
|
||||||
m: &'a Message,
|
m: impl Replyable,
|
||||||
) -> CommandResult {
|
) -> CommandResult {
|
||||||
if scores.is_empty() {
|
if scores.is_empty() {
|
||||||
m.reply(&ctx, "No plays found").await?;
|
m.reply(&ctx, "No plays found").await?;
|
||||||
|
@ -169,7 +154,7 @@ mod scores {
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl pagination::Paginate for Paginate {
|
impl pagination::Paginate for Paginate {
|
||||||
async fn render(&mut self, page: u8, ctx: &Context, msg: &mut Message) -> Result<bool> {
|
async fn render(&mut self, page: u8, ctx: &Context) -> Result<Option<CreateReply>> {
|
||||||
let data = ctx.data.read().await;
|
let data = ctx.data.read().await;
|
||||||
let osu = data.get::<BeatmapMetaCache>().unwrap();
|
let osu = data.get::<BeatmapMetaCache>().unwrap();
|
||||||
let beatmap_cache = data.get::<BeatmapCache>().unwrap();
|
let beatmap_cache = data.get::<BeatmapCache>().unwrap();
|
||||||
|
@ -177,10 +162,9 @@ mod scores {
|
||||||
let start = page * ITEMS_PER_PAGE;
|
let start = page * ITEMS_PER_PAGE;
|
||||||
let end = self.scores.len().min(start + ITEMS_PER_PAGE);
|
let end = self.scores.len().min(start + ITEMS_PER_PAGE);
|
||||||
if start >= end {
|
if start >= end {
|
||||||
return Ok(false);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let hourglass = msg.react(ctx, '⌛').await?;
|
|
||||||
let plays = &self.scores[start..end];
|
let plays = &self.scores[start..end];
|
||||||
let mode = self.mode;
|
let mode = self.mode;
|
||||||
let beatmaps = plays
|
let beatmaps = plays
|
||||||
|
@ -330,10 +314,7 @@ mod scores {
|
||||||
self.total_pages()
|
self.total_pages()
|
||||||
));
|
));
|
||||||
m.push_line("[?] means pp was predicted by oppai-rs.");
|
m.push_line("[?] means pp was predicted by oppai-rs.");
|
||||||
msg.edit(ctx, EditMessage::new().content(m.to_string()))
|
Ok(Some(CreateReply::default().content(m.to_string())))
|
||||||
.await?;
|
|
||||||
hourglass.delete(ctx).await?;
|
|
||||||
Ok(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn len(&self) -> Option<usize> {
|
fn len(&self) -> Option<usize> {
|
||||||
|
@ -350,13 +331,14 @@ mod beatmapset {
|
||||||
},
|
},
|
||||||
models::{Beatmap, Mode, Mods},
|
models::{Beatmap, Mode, Mods},
|
||||||
};
|
};
|
||||||
|
use poise::CreateReply;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
all::Reaction,
|
all::{ChannelId, Reaction},
|
||||||
builder::{CreateEmbedFooter, EditMessage},
|
builder::CreateEmbedFooter,
|
||||||
model::channel::Message,
|
|
||||||
model::channel::ReactionType,
|
model::channel::ReactionType,
|
||||||
};
|
};
|
||||||
use youmubot_prelude::*;
|
use youmubot_prelude::*;
|
||||||
|
use youmubot_prelude::{pagination::PageUpdate, replyable::Replyable};
|
||||||
|
|
||||||
const SHOW_ALL_EMOTE: &str = "🗒️";
|
const SHOW_ALL_EMOTE: &str = "🗒️";
|
||||||
|
|
||||||
|
@ -365,16 +347,18 @@ mod beatmapset {
|
||||||
beatmapset: Vec<Beatmap>,
|
beatmapset: Vec<Beatmap>,
|
||||||
mode: Option<Mode>,
|
mode: Option<Mode>,
|
||||||
mods: Option<Mods>,
|
mods: Option<Mods>,
|
||||||
reply_to: &Message,
|
reply_to: impl Replyable + Send + 'static,
|
||||||
|
channel_id: ChannelId,
|
||||||
message: impl AsRef<str>,
|
message: impl AsRef<str>,
|
||||||
) -> Result<bool> {
|
) -> Result<()> {
|
||||||
let mods = mods.unwrap_or(Mods::NOMOD);
|
let mods = mods.unwrap_or(Mods::NOMOD);
|
||||||
|
|
||||||
if beatmapset.is_empty() {
|
if beatmapset.is_empty() {
|
||||||
return Ok(false);
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let p = Paginate {
|
let p = Paginate {
|
||||||
|
channel_id,
|
||||||
infos: vec![None; beatmapset.len()],
|
infos: vec![None; beatmapset.len()],
|
||||||
maps: beatmapset,
|
maps: beatmapset,
|
||||||
mode,
|
mode,
|
||||||
|
@ -383,16 +367,17 @@ mod beatmapset {
|
||||||
};
|
};
|
||||||
|
|
||||||
let ctx = ctx.clone();
|
let ctx = ctx.clone();
|
||||||
let reply_to = reply_to.clone();
|
// let reply_to = reply_to.clone();
|
||||||
spawn_future(async move {
|
spawn_future(async move {
|
||||||
pagination::paginate_reply(p, &ctx, &reply_to, std::time::Duration::from_secs(60))
|
pagination::paginate_reply(p, &ctx, reply_to, std::time::Duration::from_secs(60))
|
||||||
.await
|
.await
|
||||||
.pls_ok();
|
.pls_ok();
|
||||||
});
|
});
|
||||||
Ok(true)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Paginate {
|
struct Paginate {
|
||||||
|
channel_id: ChannelId,
|
||||||
maps: Vec<Beatmap>,
|
maps: Vec<Beatmap>,
|
||||||
infos: Vec<Option<BeatmapInfoWithPP>>,
|
infos: Vec<Option<BeatmapInfoWithPP>>,
|
||||||
mode: Option<Mode>,
|
mode: Option<Mode>,
|
||||||
|
@ -417,26 +402,15 @@ mod beatmapset {
|
||||||
Some(self.maps.len())
|
Some(self.maps.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn render(
|
async fn render(&mut self, page: u8, ctx: &Context) -> Result<Option<CreateReply>> {
|
||||||
&mut self,
|
|
||||||
page: u8,
|
|
||||||
ctx: &Context,
|
|
||||||
m: &mut serenity::model::channel::Message,
|
|
||||||
) -> Result<bool> {
|
|
||||||
let page = page as usize;
|
let page = page as usize;
|
||||||
if page == self.maps.len() {
|
if page == self.maps.len() {
|
||||||
m.edit(
|
return Ok(Some(CreateReply::default().embed(
|
||||||
ctx,
|
crate::discord::embeds::beatmapset_embed(&self.maps[..], self.mode),
|
||||||
EditMessage::new().embed(crate::discord::embeds::beatmapset_embed(
|
)));
|
||||||
&self.maps[..],
|
|
||||||
self.mode,
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
return Ok(true);
|
|
||||||
}
|
}
|
||||||
if page > self.maps.len() {
|
if page > self.maps.len() {
|
||||||
return Ok(false);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let map = &self.maps[page];
|
let map = &self.maps[page];
|
||||||
|
@ -448,8 +422,7 @@ mod beatmapset {
|
||||||
info
|
info
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
m.edit(ctx,
|
let edit = CreateReply::default().content(self.message.as_str()).embed(
|
||||||
EditMessage::new().content(self.message.as_str()).embed(
|
|
||||||
crate::discord::embeds::beatmap_embed(
|
crate::discord::embeds::beatmap_embed(
|
||||||
map,
|
map,
|
||||||
self.mode.unwrap_or(map.mode),
|
self.mode.unwrap_or(map.mode),
|
||||||
|
@ -464,47 +437,46 @@ mod beatmapset {
|
||||||
SHOW_ALL_EMOTE,
|
SHOW_ALL_EMOTE,
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
save_beatmap(
|
save_beatmap(
|
||||||
&*ctx.data.read().await,
|
ctx.data.read().await.get::<crate::discord::Env>().unwrap(),
|
||||||
m.channel_id,
|
self.channel_id,
|
||||||
&BeatmapWithMode(map.clone(), self.mode.unwrap_or(map.mode)),
|
&BeatmapWithMode(map.clone(), self.mode.unwrap_or(map.mode)),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.pls_ok();
|
.pls_ok();
|
||||||
|
|
||||||
Ok(true)
|
Ok(Some(edit))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn prerender(
|
async fn prerender(&mut self, ctx: &Context) -> Result<PageUpdate> {
|
||||||
&mut self,
|
// m.react(&ctx, SHOW_ALL_EMOTE.parse::<ReactionType>().unwrap())
|
||||||
ctx: &Context,
|
// .await?;
|
||||||
m: &mut serenity::model::channel::Message,
|
Ok(PageUpdate {
|
||||||
) -> Result<()> {
|
react: vec![SHOW_ALL_EMOTE.parse::<ReactionType>().unwrap()],
|
||||||
m.react(&ctx, SHOW_ALL_EMOTE.parse::<ReactionType>().unwrap())
|
..Default::default()
|
||||||
.await?;
|
})
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_reaction(
|
async fn handle_reaction(
|
||||||
&mut self,
|
&mut self,
|
||||||
page: u8,
|
page: u8,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
message: &mut serenity::model::channel::Message,
|
|
||||||
reaction: &Reaction,
|
reaction: &Reaction,
|
||||||
) -> Result<Option<u8>> {
|
) -> Result<PageUpdate> {
|
||||||
// Render the old style.
|
// Render the old style.
|
||||||
if let ReactionType::Unicode(s) = &reaction.emoji {
|
if let ReactionType::Unicode(s) = &reaction.emoji {
|
||||||
if s == SHOW_ALL_EMOTE {
|
if s == SHOW_ALL_EMOTE {
|
||||||
self.render(self.maps.len() as u8, ctx, message).await?;
|
let message = self.render(self.maps.len() as u8, ctx).await?;
|
||||||
return Ok(Some(self.maps.len() as u8));
|
let update = PageUpdate {
|
||||||
|
message,
|
||||||
|
page: Some(self.maps.len() as u8),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
return Ok(update);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pagination::handle_pagination_reaction(page, self, ctx, message, reaction)
|
pagination::handle_pagination_reaction(page, self, ctx, reaction).await
|
||||||
.await
|
|
||||||
.map(Some)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -134,7 +134,7 @@ pub fn hook<'a>(
|
||||||
let mode = l.mode.unwrap_or(b.mode);
|
let mode = l.mode.unwrap_or(b.mode);
|
||||||
let bm = super::BeatmapWithMode(*b, mode);
|
let bm = super::BeatmapWithMode(*b, mode);
|
||||||
crate::discord::cache::save_beatmap(
|
crate::discord::cache::save_beatmap(
|
||||||
&*ctx.data.read().await,
|
ctx.data.read().await.get::<crate::discord::Env>().unwrap(),
|
||||||
msg.channel_id,
|
msg.channel_id,
|
||||||
&bm,
|
&bm,
|
||||||
)
|
)
|
||||||
|
@ -413,7 +413,8 @@ async fn handle_beatmapset<'a, 'b>(
|
||||||
beatmaps,
|
beatmaps,
|
||||||
mode,
|
mode,
|
||||||
None,
|
None,
|
||||||
reply_to,
|
reply_to.clone(),
|
||||||
|
reply_to.channel_id,
|
||||||
format!("Beatmapset information for `{}`", link),
|
format!("Beatmapset information for `{}`", link),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -8,6 +8,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
use rand::seq::IteratorRandom;
|
use rand::seq::IteratorRandom;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
|
all::{ChannelId, Member},
|
||||||
builder::{CreateMessage, EditMessage},
|
builder::{CreateMessage, EditMessage},
|
||||||
collector,
|
collector,
|
||||||
framework::standard::{
|
framework::standard::{
|
||||||
|
@ -18,10 +19,11 @@ use serenity::{
|
||||||
utils::MessageBuilder,
|
utils::MessageBuilder,
|
||||||
};
|
};
|
||||||
use std::{str::FromStr, sync::Arc};
|
use std::{str::FromStr, sync::Arc};
|
||||||
use youmubot_prelude::*;
|
use youmubot_prelude::{replyable::Replyable, *};
|
||||||
|
|
||||||
mod announcer;
|
mod announcer;
|
||||||
pub mod app_commands;
|
pub mod app_commands;
|
||||||
|
mod args;
|
||||||
pub(crate) mod beatmap_cache;
|
pub(crate) mod beatmap_cache;
|
||||||
mod cache;
|
mod cache;
|
||||||
mod db;
|
mod db;
|
||||||
|
@ -386,17 +388,16 @@ impl FromStr for ModeArg {
|
||||||
|
|
||||||
async fn to_user_id_query(
|
async fn to_user_id_query(
|
||||||
s: Option<UsernameArg>,
|
s: Option<UsernameArg>,
|
||||||
data: &TypeMap,
|
env: &Env,
|
||||||
msg: &Message,
|
sender: &serenity::all::User,
|
||||||
) -> Result<UserID, Error> {
|
) -> Result<UserID, Error> {
|
||||||
let id = match s {
|
let id = match s {
|
||||||
Some(UsernameArg::Raw(s)) => return Ok(UserID::from_string(s)),
|
Some(UsernameArg::Raw(s)) => return Ok(UserID::from_string(s)),
|
||||||
Some(UsernameArg::Tagged(r)) => r,
|
Some(UsernameArg::Tagged(r)) => r,
|
||||||
None => msg.author.id,
|
None => sender.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
data.get::<OsuSavedUsers>()
|
env.saved_users
|
||||||
.unwrap()
|
|
||||||
.by_user_id(id)
|
.by_user_id(id)
|
||||||
.await?
|
.await?
|
||||||
.map(|u| UserID::ID(u.id))
|
.map(|u| UserID::ID(u.id))
|
||||||
|
@ -431,13 +432,14 @@ impl FromStr for Nth {
|
||||||
#[max_args(4)]
|
#[max_args(4)]
|
||||||
pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
let data = ctx.data.read().await;
|
let data = ctx.data.read().await;
|
||||||
|
let env = data.get::<Env>().unwrap();
|
||||||
let nth = args.single::<Nth>().unwrap_or(Nth::All);
|
let nth = args.single::<Nth>().unwrap_or(Nth::All);
|
||||||
let style = args.single::<ScoreListStyle>().unwrap_or_default();
|
let style = args.single::<ScoreListStyle>().unwrap_or_default();
|
||||||
let mode = args.single::<ModeArg>().unwrap_or(ModeArg(Mode::Std)).0;
|
let mode = args.single::<ModeArg>().unwrap_or(ModeArg(Mode::Std)).0;
|
||||||
let user = to_user_id_query(
|
let user = to_user_id_query(
|
||||||
args.quoted().trimmed().single::<UsernameArg>().ok(),
|
args.quoted().trimmed().single::<UsernameArg>().ok(),
|
||||||
&data,
|
&env,
|
||||||
msg,
|
&msg.author,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -471,13 +473,14 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Save the beatmap...
|
// Save the beatmap...
|
||||||
cache::save_beatmap(&data, msg.channel_id, &beatmap_mode).await?;
|
cache::save_beatmap(&env, msg.channel_id, &beatmap_mode).await?;
|
||||||
}
|
}
|
||||||
Nth::All => {
|
Nth::All => {
|
||||||
let plays = osu
|
let plays = osu
|
||||||
.user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50))
|
.user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50))
|
||||||
.await?;
|
.await?;
|
||||||
style.display_scores(plays, mode, ctx, msg).await?;
|
display::scores::display_scores(style, plays, mode, ctx, msg.clone(), msg.channel_id)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -499,12 +502,11 @@ impl FromStr for OptBeatmapset {
|
||||||
|
|
||||||
/// Load the mentioned beatmap from the given message.
|
/// Load the mentioned beatmap from the given message.
|
||||||
pub(crate) async fn load_beatmap(
|
pub(crate) async fn load_beatmap(
|
||||||
ctx: &Context,
|
env: &Env,
|
||||||
msg: &Message,
|
msg: Option<&Message>,
|
||||||
|
channel_id: ChannelId,
|
||||||
) -> Option<(BeatmapWithMode, Option<Mods>)> {
|
) -> Option<(BeatmapWithMode, Option<Mods>)> {
|
||||||
let data = ctx.data.read().await;
|
if let Some(replied) = msg.and_then(|m| m.referenced_message.as_ref()) {
|
||||||
|
|
||||||
if let Some(replied) = &msg.referenced_message {
|
|
||||||
// Try to look for a mention of the replied message.
|
// Try to look for a mention of the replied message.
|
||||||
let beatmap_id = SHORT_LINK_REGEX.captures(&replied.content).or_else(|| {
|
let beatmap_id = SHORT_LINK_REGEX.captures(&replied.content).or_else(|| {
|
||||||
replied.embeds.iter().find_map(|e| {
|
replied.embeds.iter().find_map(|e| {
|
||||||
|
@ -526,8 +528,8 @@ pub(crate) async fn load_beatmap(
|
||||||
let mods = caps
|
let mods = caps
|
||||||
.name("mods")
|
.name("mods")
|
||||||
.and_then(|m| m.as_str().parse::<Mods>().ok());
|
.and_then(|m| m.as_str().parse::<Mods>().ok());
|
||||||
let osu = data.get::<OsuClient>().unwrap();
|
let bms = env
|
||||||
let bms = osu
|
.client
|
||||||
.beatmaps(BeatmapRequestKind::Beatmap(id), |f| f.maybe_mode(mode))
|
.beatmaps(BeatmapRequestKind::Beatmap(id), |f| f.maybe_mode(mode))
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
|
@ -536,19 +538,14 @@ pub(crate) async fn load_beatmap(
|
||||||
let bm_mode = beatmap.mode;
|
let bm_mode = beatmap.mode;
|
||||||
let bm = BeatmapWithMode(beatmap, mode.unwrap_or(bm_mode));
|
let bm = BeatmapWithMode(beatmap, mode.unwrap_or(bm_mode));
|
||||||
// Store the beatmap in history
|
// Store the beatmap in history
|
||||||
cache::save_beatmap(&data, msg.channel_id, &bm)
|
cache::save_beatmap(env, channel_id, &bm).await.pls_ok();
|
||||||
.await
|
|
||||||
.pls_ok();
|
|
||||||
|
|
||||||
return Some((bm, mods));
|
return Some((bm, mods));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let b = cache::get_beatmap(&data, msg.channel_id)
|
let b = cache::get_beatmap(env, channel_id).await.ok().flatten();
|
||||||
.await
|
|
||||||
.ok()
|
|
||||||
.flatten();
|
|
||||||
b.map(|b| (b, None))
|
b.map(|b| (b, None))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -560,7 +557,8 @@ pub(crate) async fn load_beatmap(
|
||||||
#[max_args(2)]
|
#[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 = load_beatmap(ctx, msg).await;
|
let env = data.get::<Env>().unwrap();
|
||||||
|
let b = load_beatmap(&env, Some(msg), msg.channel_id).await;
|
||||||
let beatmapset = args.find::<OptBeatmapset>().is_ok();
|
let beatmapset = args.find::<OptBeatmapset>().is_ok();
|
||||||
|
|
||||||
match b {
|
match b {
|
||||||
|
@ -574,7 +572,8 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
beatmapset,
|
beatmapset,
|
||||||
None,
|
None,
|
||||||
Some(mods),
|
Some(mods),
|
||||||
msg,
|
msg.clone(),
|
||||||
|
msg.channel_id,
|
||||||
"Here is the beatmapset you requested!",
|
"Here is the beatmapset you requested!",
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -612,7 +611,8 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
#[max_args(3)]
|
#[max_args(3)]
|
||||||
pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
let data = ctx.data.read().await;
|
let data = ctx.data.read().await;
|
||||||
let bm = load_beatmap(ctx, msg).await;
|
let env = data.get::<Env>().unwrap();
|
||||||
|
let bm = load_beatmap(&env, Some(msg), msg.channel_id).await;
|
||||||
|
|
||||||
match bm {
|
match bm {
|
||||||
None => {
|
None => {
|
||||||
|
@ -632,7 +632,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
|
||||||
None => Some(msg.author.id),
|
None => Some(msg.author.id),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let user = to_user_id_query(username_arg, &data, msg).await?;
|
let user = to_user_id_query(username_arg, &env, &msg.author).await?;
|
||||||
|
|
||||||
let osu = data.get::<OsuClient>().unwrap();
|
let osu = data.get::<OsuClient>().unwrap();
|
||||||
|
|
||||||
|
@ -662,7 +662,63 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
|
||||||
.pls_ok();
|
.pls_ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
style.display_scores(scores, m, ctx, msg).await?;
|
display::scores::display_scores(style, scores, m, ctx, msg.clone(), msg.channel_id)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check_impl(
|
||||||
|
env: &Env,
|
||||||
|
ctx: &Context,
|
||||||
|
reply: impl Replyable,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
sender: &serenity::all::User,
|
||||||
|
msg: Option<&Message>,
|
||||||
|
osu_id: Option<String>,
|
||||||
|
member: Option<Member>,
|
||||||
|
mods: Option<Mods>,
|
||||||
|
style: Option<ScoreListStyle>,
|
||||||
|
) -> CommandResult {
|
||||||
|
let bm = load_beatmap(&env, msg, channel_id).await;
|
||||||
|
|
||||||
|
match bm {
|
||||||
|
None => {
|
||||||
|
reply
|
||||||
|
.reply(&ctx, "No beatmap queried on this channel.")
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Some((bm, mods_def)) => {
|
||||||
|
let mods = mods.unwrap_or_default();
|
||||||
|
let b = &bm.0;
|
||||||
|
let m = bm.1;
|
||||||
|
let style = style.unwrap_or_default();
|
||||||
|
let username_arg = member
|
||||||
|
.map(|m| UsernameArg::Tagged(m.user.id))
|
||||||
|
.or(osu_id.map(|id| UsernameArg::Raw(id)));
|
||||||
|
let user = to_user_id_query(username_arg, env, sender).await?;
|
||||||
|
|
||||||
|
let user = env
|
||||||
|
.client
|
||||||
|
.user(user, |f| f)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| Error::msg("User not found"))?;
|
||||||
|
let mut scores = env
|
||||||
|
.client
|
||||||
|
.scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(m))
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|s| s.mods.contains(mods))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
scores.sort_by(|a, b| b.pp.unwrap().partial_cmp(&a.pp.unwrap()).unwrap());
|
||||||
|
|
||||||
|
if scores.is_empty() {
|
||||||
|
reply.reply(&ctx, "No scores found").await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
display::scores::display_scores(style, scores, m, ctx, reply, channel_id).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -677,6 +733,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
|
||||||
#[max_args(4)]
|
#[max_args(4)]
|
||||||
pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||||
let data = ctx.data.read().await;
|
let data = ctx.data.read().await;
|
||||||
|
let env = data.get::<Env>().unwrap();
|
||||||
let nth = args.single::<Nth>().unwrap_or(Nth::All);
|
let nth = args.single::<Nth>().unwrap_or(Nth::All);
|
||||||
let style = args.single::<ScoreListStyle>().unwrap_or_default();
|
let style = args.single::<ScoreListStyle>().unwrap_or_default();
|
||||||
let mode = args
|
let mode = args
|
||||||
|
@ -684,7 +741,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
.map(|ModeArg(t)| t)
|
.map(|ModeArg(t)| t)
|
||||||
.unwrap_or(Mode::Std);
|
.unwrap_or(Mode::Std);
|
||||||
|
|
||||||
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &data, msg).await?;
|
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &env, &msg.author).await?;
|
||||||
let meta_cache = data.get::<BeatmapMetaCache>().unwrap();
|
let meta_cache = data.get::<BeatmapMetaCache>().unwrap();
|
||||||
let osu = data.get::<OsuClient>().unwrap();
|
let osu = data.get::<OsuClient>().unwrap();
|
||||||
|
|
||||||
|
@ -726,13 +783,14 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Save the beatmap...
|
// Save the beatmap...
|
||||||
cache::save_beatmap(&data, msg.channel_id, &beatmap).await?;
|
cache::save_beatmap(data.get::<Env>().unwrap(), msg.channel_id, &beatmap).await?;
|
||||||
}
|
}
|
||||||
Nth::All => {
|
Nth::All => {
|
||||||
let plays = osu
|
let plays = osu
|
||||||
.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100))
|
.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100))
|
||||||
.await?;
|
.await?;
|
||||||
style.display_scores(plays, mode, ctx, msg).await?;
|
display::scores::display_scores(style, plays, mode, ctx, msg.clone(), msg.channel_id)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -757,7 +815,8 @@ pub async fn clean_cache(ctx: &Context, msg: &Message, args: Args) -> CommandRes
|
||||||
|
|
||||||
async fn get_user(ctx: &Context, msg: &Message, mut args: Args, mode: Mode) -> CommandResult {
|
async fn get_user(ctx: &Context, msg: &Message, mut args: Args, mode: Mode) -> CommandResult {
|
||||||
let data = ctx.data.read().await;
|
let data = ctx.data.read().await;
|
||||||
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &data, msg).await?;
|
let env = data.get::<Env>().unwrap();
|
||||||
|
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &env, &msg.author).await?;
|
||||||
let osu = data.get::<OsuClient>().unwrap();
|
let osu = data.get::<OsuClient>().unwrap();
|
||||||
let cache = data.get::<BeatmapMetaCache>().unwrap();
|
let cache = data.get::<BeatmapMetaCache>().unwrap();
|
||||||
let user = osu.user(user, |f| f.mode(mode)).await?;
|
let user = osu.user(user, |f| f.mode(mode)).await?;
|
||||||
|
|
|
@ -10,6 +10,7 @@ use crate::{
|
||||||
request::UserID,
|
request::UserID,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use poise::CreateReply;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
builder::EditMessage,
|
builder::EditMessage,
|
||||||
framework::standard::{macros::command, Args, CommandResult},
|
framework::standard::{macros::command, Args, CommandResult},
|
||||||
|
@ -88,14 +89,14 @@ 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_reply_fn(
|
paginate_reply_fn(
|
||||||
move |page: u8, ctx: &Context, m: &mut Message| {
|
move |page: u8, ctx: &Context| {
|
||||||
const ITEMS_PER_PAGE: usize = 10;
|
const ITEMS_PER_PAGE: usize = 10;
|
||||||
let users = users.clone();
|
let users = users.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let start = (page as usize) * ITEMS_PER_PAGE;
|
let start = (page as usize) * ITEMS_PER_PAGE;
|
||||||
let end = (start + ITEMS_PER_PAGE).min(users.len());
|
let end = (start + ITEMS_PER_PAGE).min(users.len());
|
||||||
if start >= end {
|
if start >= end {
|
||||||
return Ok(false);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let total_len = users.len();
|
let total_len = users.len();
|
||||||
let users = &users[start..end];
|
let users = &users[start..end];
|
||||||
|
@ -142,13 +143,11 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR
|
||||||
(total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE,
|
(total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE,
|
||||||
last_update.format("<t:%s:R>"),
|
last_update.format("<t:%s:R>"),
|
||||||
));
|
));
|
||||||
m.edit(ctx, EditMessage::new().content(content.to_string()))
|
Ok(Some(CreateReply::default().content(content.to_string())))
|
||||||
.await?;
|
|
||||||
Ok(true)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
ctx,
|
ctx,
|
||||||
m,
|
m.clone(),
|
||||||
std::time::Duration::from_secs(60),
|
std::time::Duration::from_secs(60),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -191,9 +190,10 @@ pub async fn show_leaderboard(ctx: &Context, m: &Message, mut args: Args) -> Com
|
||||||
let style = args.single::<ScoreListStyle>().unwrap_or_default();
|
let style = args.single::<ScoreListStyle>().unwrap_or_default();
|
||||||
|
|
||||||
let data = ctx.data.read().await;
|
let data = ctx.data.read().await;
|
||||||
|
let env = data.get::<crate::discord::Env>().unwrap();
|
||||||
let member_cache = data.get::<MemberCache>().unwrap();
|
let member_cache = data.get::<MemberCache>().unwrap();
|
||||||
|
|
||||||
let (bm, _) = match super::load_beatmap(ctx, m).await {
|
let (bm, _) = match super::load_beatmap(env, Some(m), m.channel_id).await {
|
||||||
Some((bm, mods_def)) => {
|
Some((bm, mods_def)) => {
|
||||||
let mods = args.find::<Mods>().ok().or(mods_def).unwrap_or(Mods::NOMOD);
|
let mods = args.find::<Mods>().ok().or(mods_def).unwrap_or(Mods::NOMOD);
|
||||||
(bm, mods)
|
(bm, mods)
|
||||||
|
@ -295,25 +295,26 @@ pub async fn show_leaderboard(ctx: &Context, m: &Message, mut args: Args) -> Com
|
||||||
}
|
}
|
||||||
|
|
||||||
if let ScoreListStyle::Grid = style {
|
if let ScoreListStyle::Grid = style {
|
||||||
style
|
crate::discord::display::scores::display_scores(
|
||||||
.display_scores(
|
style,
|
||||||
scores.into_iter().map(|(_, _, a)| a).collect(),
|
scores.into_iter().map(|(_, _, a)| a).collect(),
|
||||||
mode,
|
mode,
|
||||||
ctx,
|
ctx,
|
||||||
m,
|
m.clone(),
|
||||||
)
|
m.channel_id,
|
||||||
.await?;
|
)
|
||||||
|
.await?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let has_lazer_score = scores.iter().any(|(_, _, v)| v.score.is_none());
|
let has_lazer_score = scores.iter().any(|(_, _, v)| v.score.is_none());
|
||||||
|
|
||||||
paginate_reply_fn(
|
paginate_reply_fn(
|
||||||
move |page: u8, ctx: &Context, m: &mut Message| {
|
move |page: u8, ctx: &Context| {
|
||||||
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;
|
||||||
let end = (start + ITEMS_PER_PAGE).min(scores.len());
|
let end = (start + ITEMS_PER_PAGE).min(scores.len());
|
||||||
if start >= end {
|
if start >= end {
|
||||||
return Box::pin(future::ready(Ok(false)));
|
return Box::pin(future::ready(Ok(None)));
|
||||||
}
|
}
|
||||||
let total_len = scores.len();
|
let total_len = scores.len();
|
||||||
let scores = scores[start..end].to_vec();
|
let scores = scores[start..end].to_vec();
|
||||||
|
@ -436,12 +437,11 @@ pub async fn show_leaderboard(ctx: &Context, m: &Message, mut args: Args) -> Com
|
||||||
content.push_line("PP was calculated by `oppai-rs`, **not** official values.");
|
content.push_line("PP was calculated by `oppai-rs`, **not** official values.");
|
||||||
}
|
}
|
||||||
|
|
||||||
m.edit(&ctx, EditMessage::new().content(content.build())).await?;
|
Ok(Some(CreateReply::default().content(content.build())))
|
||||||
Ok(true)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
ctx,
|
ctx,
|
||||||
m,
|
m.clone(),
|
||||||
std::time::Duration::from_secs(60),
|
std::time::Duration::from_secs(60),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -97,8 +97,16 @@ impl Mods {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ModParseError {
|
||||||
|
#[error("String of odd length is not a mod string")]
|
||||||
|
OddLength,
|
||||||
|
#[error("{0} is not a valid mod")]
|
||||||
|
InvalidMod(String),
|
||||||
|
}
|
||||||
|
|
||||||
impl std::str::FromStr for Mods {
|
impl std::str::FromStr for Mods {
|
||||||
type Err = String;
|
type Err = ModParseError;
|
||||||
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 +
|
// Strip leading +
|
||||||
|
@ -134,11 +142,11 @@ impl std::str::FromStr for Mods {
|
||||||
"8K" => res |= Mods::KEY8,
|
"8K" => res |= Mods::KEY8,
|
||||||
"9K" => res |= Mods::KEY9,
|
"9K" => res |= Mods::KEY9,
|
||||||
"??" => res |= Mods::UNKNOWN,
|
"??" => res |= Mods::UNKNOWN,
|
||||||
v => return Err(format!("{} is not a valid mod", v)),
|
v => return Err(ModParseError::InvalidMod(v.to_owned())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !s.is_empty() {
|
if !s.is_empty() {
|
||||||
Err("String of odd length is not a mod string".to_owned())
|
Err(ModParseError::OddLength)
|
||||||
} else {
|
} else {
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ use serenity::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
utils::MessageBuilder,
|
utils::MessageBuilder,
|
||||||
};
|
};
|
||||||
use std::{arch::x86_64::_bittestandcomplement, collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
use youmubot_db::DB;
|
use youmubot_db::DB;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use announcer::AnnouncerChannels;
|
|
||||||
/// Module `prelude` provides a sane set of default imports that can be used inside
|
/// Module `prelude` provides a sane set of default imports that can be used inside
|
||||||
/// a Youmubot source file.
|
/// a Youmubot source file.
|
||||||
pub use serenity::prelude::*;
|
pub use serenity::prelude::*;
|
||||||
|
@ -11,6 +10,7 @@ pub mod hook;
|
||||||
pub mod member_cache;
|
pub mod member_cache;
|
||||||
pub mod pagination;
|
pub mod pagination;
|
||||||
pub mod ratelimit;
|
pub mod ratelimit;
|
||||||
|
pub mod replyable;
|
||||||
pub mod setup;
|
pub mod setup;
|
||||||
|
|
||||||
pub use announcer::{Announcer, AnnouncerHandler};
|
pub use announcer::{Announcer, AnnouncerHandler};
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
use crate::{Context, OkPrint, Result};
|
use crate::{
|
||||||
|
replyable::{Replyable, Updateable},
|
||||||
|
Context, OkPrint, Result,
|
||||||
|
};
|
||||||
use futures_util::{future::Future, StreamExt as _};
|
use futures_util::{future::Future, StreamExt as _};
|
||||||
|
use poise::CreateReply;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
builder::CreateMessage,
|
builder::CreateMessage,
|
||||||
collector,
|
collector,
|
||||||
model::{
|
model::{
|
||||||
channel::{Message, Reaction, ReactionType},
|
channel::{Reaction, ReactionType},
|
||||||
id::ChannelId,
|
id::ChannelId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -16,15 +20,41 @@ const ARROW_LEFT: &str = "⬅️";
|
||||||
const REWIND: &str = "⏪";
|
const REWIND: &str = "⏪";
|
||||||
const FAST_FORWARD: &str = "⏩";
|
const FAST_FORWARD: &str = "⏩";
|
||||||
|
|
||||||
|
/// Represents a page update.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct PageUpdate {
|
||||||
|
pub message: Option<CreateReply>,
|
||||||
|
pub page: Option<u8>,
|
||||||
|
pub react: Vec<ReactionType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u8> for PageUpdate {
|
||||||
|
fn from(value: u8) -> Self {
|
||||||
|
PageUpdate {
|
||||||
|
page: Some(value),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CreateReply> for PageUpdate {
|
||||||
|
fn from(value: CreateReply) -> Self {
|
||||||
|
PageUpdate {
|
||||||
|
message: Some(value),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A trait that provides the implementation of a paginator.
|
/// A trait that provides the implementation of a paginator.
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait Paginate: Send + Sized {
|
pub trait Paginate: Send + Sized {
|
||||||
/// Render the given page.
|
/// 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) -> Result<Option<CreateReply>>;
|
||||||
|
|
||||||
/// Any setting-up before the rendering stage.
|
/// Any setting-up before the rendering stage.
|
||||||
async fn prerender(&mut self, _ctx: &Context, _m: &mut Message) -> Result<()> {
|
async fn prerender(&mut self, _ctx: &Context) -> Result<PageUpdate> {
|
||||||
Ok(())
|
Ok(PageUpdate::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle the incoming reaction. Defaults to calling `handle_pagination_reaction`, but you can do some additional handling
|
/// Handle the incoming reaction. Defaults to calling `handle_pagination_reaction`, but you can do some additional handling
|
||||||
|
@ -35,12 +65,9 @@ pub trait Paginate: Send + Sized {
|
||||||
&mut self,
|
&mut self,
|
||||||
page: u8,
|
page: u8,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
message: &mut Message,
|
|
||||||
reaction: &Reaction,
|
reaction: &Reaction,
|
||||||
) -> Result<Option<u8>> {
|
) -> Result<PageUpdate> {
|
||||||
handle_pagination_reaction(page, self, ctx, message, reaction)
|
handle_pagination_reaction(page, self, ctx, reaction).await
|
||||||
.await
|
|
||||||
.map(Some)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the number of pages, if it is known in advance.
|
/// Return the number of pages, if it is known in advance.
|
||||||
|
@ -60,12 +87,12 @@ where
|
||||||
T: for<'m> FnMut(
|
T: for<'m> FnMut(
|
||||||
u8,
|
u8,
|
||||||
&'m Context,
|
&'m Context,
|
||||||
&'m mut Message,
|
) -> std::pin::Pin<
|
||||||
) -> std::pin::Pin<Box<dyn Future<Output = Result<bool>> + Send + 'm>>
|
Box<dyn Future<Output = Result<Option<CreateReply>>> + Send + 'm>,
|
||||||
+ Send,
|
> + Send,
|
||||||
{
|
{
|
||||||
async fn render(&mut self, page: u8, ctx: &Context, m: &mut Message) -> Result<bool> {
|
async fn render(&mut self, page: u8, ctx: &Context) -> Result<Option<CreateReply>> {
|
||||||
self(page, ctx, m).await
|
self(page, ctx).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,13 +101,13 @@ where
|
||||||
pub async fn paginate_reply(
|
pub async fn paginate_reply(
|
||||||
pager: impl Paginate,
|
pager: impl Paginate,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
reply_to: &Message,
|
reply_to: impl Replyable,
|
||||||
timeout: std::time::Duration,
|
timeout: std::time::Duration,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let message = reply_to
|
let update = reply_to
|
||||||
.reply(&ctx, "Youmu is loading the first page...")
|
.reply(&ctx, "Youmu is loading the first page...")
|
||||||
.await?;
|
.await?;
|
||||||
paginate_with_first_message(pager, ctx, message, timeout).await
|
paginate_with_first_message(pager, ctx, update, timeout).await
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paginate! with a pager function.
|
// Paginate! with a pager function.
|
||||||
|
@ -103,11 +130,17 @@ pub async fn paginate(
|
||||||
async fn paginate_with_first_message(
|
async fn paginate_with_first_message(
|
||||||
mut pager: impl Paginate,
|
mut pager: impl Paginate,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
mut message: Message,
|
mut update: impl Updateable,
|
||||||
timeout: std::time::Duration,
|
timeout: std::time::Duration,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
pager.prerender(ctx, &mut message).await?;
|
let message = update.message().await?;
|
||||||
pager.render(0, ctx, &mut message).await?;
|
let prerender = pager.prerender(ctx).await?;
|
||||||
|
if let Some(cr) = prerender.message {
|
||||||
|
update.edit(ctx, cr).await?;
|
||||||
|
}
|
||||||
|
if let Some(cr) = pager.render(0, ctx).await? {
|
||||||
|
update.edit(ctx, cr).await?;
|
||||||
|
}
|
||||||
// Just quit if there is only one page
|
// Just quit if there is only one page
|
||||||
if pager.len().filter(|&v| v == 1).is_some() {
|
if pager.len().filter(|&v| v == 1).is_some() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
@ -115,7 +148,7 @@ async fn paginate_with_first_message(
|
||||||
// React to the message
|
// React to the message
|
||||||
let large_count = pager.len().filter(|&p| p > 10).is_some();
|
let large_count = pager.len().filter(|&p| p > 10).is_some();
|
||||||
let reactions = {
|
let reactions = {
|
||||||
let mut rs = Vec::<Reaction>::with_capacity(4);
|
let mut rs = Vec::<Reaction>::with_capacity(4 + prerender.react.len());
|
||||||
if large_count {
|
if large_count {
|
||||||
// add >> and << buttons
|
// add >> and << buttons
|
||||||
rs.push(message.react(&ctx, ReactionType::try_from(REWIND)?).await?);
|
rs.push(message.react(&ctx, ReactionType::try_from(REWIND)?).await?);
|
||||||
|
@ -138,6 +171,9 @@ async fn paginate_with_first_message(
|
||||||
.await?,
|
.await?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
for r in prerender.react.into_iter() {
|
||||||
|
rs.push(message.react(&ctx, r).await?);
|
||||||
|
}
|
||||||
rs
|
rs
|
||||||
};
|
};
|
||||||
// Build a reaction collector
|
// Build a reaction collector
|
||||||
|
@ -161,12 +197,16 @@ async fn paginate_with_first_message(
|
||||||
Err(_) => break Ok(()),
|
Err(_) => break Ok(()),
|
||||||
Ok(None) => break Ok(()),
|
Ok(None) => break Ok(()),
|
||||||
Ok(Some(reaction)) => {
|
Ok(Some(reaction)) => {
|
||||||
page = match pager
|
page = match pager.handle_reaction(page, ctx, &reaction).await {
|
||||||
.handle_reaction(page, ctx, &mut message, &reaction)
|
Ok(pu) => {
|
||||||
.await
|
if let Some(cr) = pu.message {
|
||||||
{
|
update.edit(ctx, cr).await?;
|
||||||
Ok(Some(v)) => v,
|
}
|
||||||
Ok(None) => break Ok(()),
|
match pu.page {
|
||||||
|
Some(v) => v,
|
||||||
|
None => break Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(e) => break Err(e),
|
Err(e) => break Err(e),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -188,9 +228,9 @@ pub async fn paginate_fn(
|
||||||
pager: impl for<'m> FnMut(
|
pager: impl for<'m> FnMut(
|
||||||
u8,
|
u8,
|
||||||
&'m Context,
|
&'m Context,
|
||||||
&'m mut Message,
|
) -> std::pin::Pin<
|
||||||
) -> std::pin::Pin<Box<dyn Future<Output = Result<bool>> + Send + 'm>>
|
Box<dyn Future<Output = Result<Option<CreateReply>>> + Send + 'm>,
|
||||||
+ Send,
|
> + Send,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
channel: ChannelId,
|
channel: ChannelId,
|
||||||
timeout: std::time::Duration,
|
timeout: std::time::Duration,
|
||||||
|
@ -203,11 +243,11 @@ pub async fn paginate_reply_fn(
|
||||||
pager: impl for<'m> FnMut(
|
pager: impl for<'m> FnMut(
|
||||||
u8,
|
u8,
|
||||||
&'m Context,
|
&'m Context,
|
||||||
&'m mut Message,
|
) -> std::pin::Pin<
|
||||||
) -> std::pin::Pin<Box<dyn Future<Output = Result<bool>> + Send + 'm>>
|
Box<dyn Future<Output = Result<Option<CreateReply>>> + Send + 'm>,
|
||||||
+ Send,
|
> + Send,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
reply_to: &Message,
|
reply_to: impl Replyable,
|
||||||
timeout: std::time::Duration,
|
timeout: std::time::Duration,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
paginate_reply(pager, ctx, reply_to, timeout).await
|
paginate_reply(pager, ctx, reply_to, timeout).await
|
||||||
|
@ -218,15 +258,14 @@ pub async fn handle_pagination_reaction(
|
||||||
page: u8,
|
page: u8,
|
||||||
pager: &mut impl Paginate,
|
pager: &mut impl Paginate,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
message: &mut Message,
|
|
||||||
reaction: &Reaction,
|
reaction: &Reaction,
|
||||||
) -> Result<u8> {
|
) -> Result<PageUpdate> {
|
||||||
let pages = pager.len();
|
let pages = pager.len();
|
||||||
let fast = pages.map(|v| v / 10).unwrap_or(5).max(5) as u8;
|
let fast = pages.map(|v| v / 10).unwrap_or(5).max(5) as u8;
|
||||||
match &reaction.emoji {
|
match &reaction.emoji {
|
||||||
ReactionType::Unicode(ref s) => {
|
ReactionType::Unicode(ref s) => {
|
||||||
let new_page = match s.as_str() {
|
let new_page = match s.as_str() {
|
||||||
ARROW_LEFT | REWIND if page == 0 => return Ok(page),
|
ARROW_LEFT | REWIND if page == 0 => return Ok(page.into()),
|
||||||
ARROW_LEFT => page - 1,
|
ARROW_LEFT => page - 1,
|
||||||
REWIND => {
|
REWIND => {
|
||||||
if page < fast {
|
if page < fast {
|
||||||
|
@ -236,18 +275,26 @@ pub async fn handle_pagination_reaction(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ARROW_RIGHT if pages.filter(|&pages| page as usize + 1 >= pages).is_some() => {
|
ARROW_RIGHT if pages.filter(|&pages| page as usize + 1 >= pages).is_some() => {
|
||||||
return Ok(page)
|
return Ok(page.into())
|
||||||
}
|
}
|
||||||
ARROW_RIGHT => page + 1,
|
ARROW_RIGHT => page + 1,
|
||||||
FAST_FORWARD => (pages.unwrap() as u8 - 1).min(page + fast),
|
FAST_FORWARD => (pages.unwrap() as u8 - 1).min(page + fast),
|
||||||
_ => return Ok(page),
|
_ => return Ok(page.into()),
|
||||||
};
|
};
|
||||||
Ok(if pager.render(new_page, ctx, message).await? {
|
let reply = pager.render(new_page, ctx).await?;
|
||||||
new_page
|
Ok(reply
|
||||||
} else {
|
.map(|cr| PageUpdate {
|
||||||
page
|
message: Some(cr),
|
||||||
})
|
page: Some(page),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| page.into()))
|
||||||
|
// Ok(if pager.render(new_page, ctx, message).await? {
|
||||||
|
// new_page
|
||||||
|
// } else {
|
||||||
|
// page
|
||||||
|
// })
|
||||||
}
|
}
|
||||||
_ => Ok(page),
|
_ => Ok(page.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
76
youmubot-prelude/src/replyable.rs
Normal file
76
youmubot-prelude/src/replyable.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
use poise::{CreateReply, ReplyHandle};
|
||||||
|
use serenity::{all::Message, builder::EditMessage};
|
||||||
|
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Represents a target where replying is possible and returns a message.
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Replyable {
|
||||||
|
type Resp: Updateable + Send;
|
||||||
|
/// Reply to the context.
|
||||||
|
async fn reply(
|
||||||
|
&self,
|
||||||
|
ctx: impl CacheHttp + Send,
|
||||||
|
content: impl Into<String> + Send,
|
||||||
|
) -> Result<Self::Resp>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Replyable for Message {
|
||||||
|
type Resp = Message;
|
||||||
|
async fn reply(
|
||||||
|
&self,
|
||||||
|
ctx: impl CacheHttp + Send,
|
||||||
|
content: impl Into<String> + Send,
|
||||||
|
) -> Result<Self::Resp> {
|
||||||
|
Ok(Message::reply(self, ctx, content).await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<'c, T: Sync, E> Replyable for poise::Context<'c, T, E> {
|
||||||
|
type Resp = (ReplyHandle<'c>, Self);
|
||||||
|
async fn reply(
|
||||||
|
&self,
|
||||||
|
_ctx: impl CacheHttp + Send,
|
||||||
|
content: impl Into<String> + Send,
|
||||||
|
) -> Result<Self::Resp> {
|
||||||
|
let handle = poise::Context::reply(*self, content).await?;
|
||||||
|
Ok((handle, *self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a message representation that allows deletion and editing.
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Updateable {
|
||||||
|
async fn message(&self) -> Result<Message>;
|
||||||
|
async fn edit(&mut self, ctx: impl CacheHttp + Send, content: CreateReply) -> Result<()>;
|
||||||
|
async fn delete(&self, ctx: impl CacheHttp + Send) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Updateable for Message {
|
||||||
|
async fn message(&self) -> Result<Message> {
|
||||||
|
Ok(self.clone())
|
||||||
|
}
|
||||||
|
async fn edit(&mut self, ctx: impl CacheHttp + Send, content: CreateReply) -> Result<()> {
|
||||||
|
let content = content.to_prefix_edit(EditMessage::new());
|
||||||
|
Ok(Message::edit(self, ctx, content).await?)
|
||||||
|
}
|
||||||
|
async fn delete(&self, ctx: impl CacheHttp + Send) -> Result<()> {
|
||||||
|
Ok(Message::delete(self, ctx).await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<'a, T: Sync, E> Updateable for (poise::ReplyHandle<'a>, poise::Context<'a, T, E>) {
|
||||||
|
async fn message(&self) -> Result<Message> {
|
||||||
|
Ok(poise::ReplyHandle::message(&self.0).await?.into_owned())
|
||||||
|
}
|
||||||
|
async fn edit(&mut self, _ctx: impl CacheHttp, content: CreateReply) -> Result<()> {
|
||||||
|
Ok(poise::ReplyHandle::edit(&self.0, self.1, content).await?)
|
||||||
|
}
|
||||||
|
async fn delete(&self, _ctx: impl CacheHttp) -> Result<()> {
|
||||||
|
Ok(poise::ReplyHandle::delete(&self.0, self.1).await?)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue