mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-19 16:58:55 +00:00
osu: make save
command use interaction
This commit is contained in:
parent
247001f8c7
commit
5afe20cba8
6 changed files with 94 additions and 59 deletions
|
@ -36,9 +36,27 @@ pub mod errors {
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("sqlx error: {:?}", .0)]
|
#[error("sqlx error: {:?}", .0)]
|
||||||
SQLx(#[from] sqlx::Error),
|
SQLx(sqlx::Error),
|
||||||
#[error("sqlx migration error: {:?}", .0)]
|
#[error("sqlx migration error: {:?}", .0)]
|
||||||
Migration(#[from] sqlx::migrate::MigrateError),
|
Migration(#[from] sqlx::migrate::MigrateError),
|
||||||
|
#[error("values already existed for: {}", .0)]
|
||||||
|
Duplicate(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<sqlx::Error> for Error {
|
||||||
|
fn from(value: sqlx::Error) -> Self {
|
||||||
|
match value {
|
||||||
|
// if we can match a constraint error, give it a special case.
|
||||||
|
sqlx::Error::Database(database_error) => {
|
||||||
|
let msg = database_error.message();
|
||||||
|
match msg.strip_prefix("UNIQUE constraint failed: ") {
|
||||||
|
Some(con) => Error::Duplicate(con.to_owned()),
|
||||||
|
None => Error::SQLx(sqlx::Error::Database(database_error)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => Error::SQLx(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -186,21 +186,31 @@ pub async fn save<U: HasOsuEnv>(
|
||||||
CreateReply::default()
|
CreateReply::default()
|
||||||
.content(save_request_message(&u.username, score.beatmap_id, mode))
|
.content(save_request_message(&u.username, score.beatmap_id, mode))
|
||||||
.embed(beatmap_embed(&beatmap, mode, Mods::NOMOD, &info))
|
.embed(beatmap_embed(&beatmap, mode, Mods::NOMOD, &info))
|
||||||
.components(vec![beatmap_components(mode, ctx.guild_id())]),
|
.components(vec![
|
||||||
|
beatmap_components(mode, ctx.guild_id()),
|
||||||
|
save_button(),
|
||||||
|
]),
|
||||||
)
|
)
|
||||||
.await?
|
|
||||||
.into_message()
|
|
||||||
.await?;
|
.await?;
|
||||||
handle_save_respond(
|
let mut p = (reply, ctx.clone());
|
||||||
|
match handle_save_respond(
|
||||||
ctx.serenity_context(),
|
ctx.serenity_context(),
|
||||||
&env,
|
&env,
|
||||||
ctx.author().id,
|
ctx.author().id,
|
||||||
reply,
|
&mut p,
|
||||||
&beatmap,
|
&beatmap,
|
||||||
u,
|
u,
|
||||||
mode,
|
mode,
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => {
|
||||||
|
p.0.delete(ctx).await?;
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,14 @@ impl OsuSavedUsers {
|
||||||
let mut t = self.pool.begin().await?;
|
let mut t = self.pool.begin().await?;
|
||||||
model::OsuUser::delete(u.user_id.get() as i64, &mut *t).await?;
|
model::OsuUser::delete(u.user_id.get() as i64, &mut *t).await?;
|
||||||
assert!(
|
assert!(
|
||||||
model::OsuUser::from(u).store(&mut t).await?,
|
match model::OsuUser::from(u).store(&mut t).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(youmubot_db_sql::Error::Duplicate(_)) =>
|
||||||
|
return Err(Error::msg(
|
||||||
|
"another Discord user has already saved your account with the same id!"
|
||||||
|
)),
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
},
|
||||||
"Should be updated"
|
"Should be updated"
|
||||||
);
|
);
|
||||||
t.commit().await?;
|
t.commit().await?;
|
||||||
|
|
|
@ -8,8 +8,8 @@ use link_parser::EmbedType;
|
||||||
use oppai_cache::BeatmapInfoWithPP;
|
use oppai_cache::BeatmapInfoWithPP;
|
||||||
use rand::seq::IteratorRandom;
|
use rand::seq::IteratorRandom;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
builder::{CreateMessage, EditMessage},
|
all::{CreateActionRow, CreateButton},
|
||||||
collector,
|
builder::CreateMessage,
|
||||||
framework::standard::{
|
framework::standard::{
|
||||||
macros::{command, group},
|
macros::{command, group},
|
||||||
Args, CommandResult,
|
Args, CommandResult,
|
||||||
|
@ -257,10 +257,17 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
CreateMessage::new()
|
CreateMessage::new()
|
||||||
.content(save_request_message(&u.username, score.beatmap_id, mode))
|
.content(save_request_message(&u.username, score.beatmap_id, mode))
|
||||||
.embed(beatmap_embed(&beatmap, mode, Mods::NOMOD, &info))
|
.embed(beatmap_embed(&beatmap, mode, Mods::NOMOD, &info))
|
||||||
.components(vec![beatmap_components(mode, msg.guild_id)]),
|
.components(vec![beatmap_components(mode, msg.guild_id), save_button()]),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
handle_save_respond(ctx, &env, msg.author.id, reply, &beatmap, u, mode).await?;
|
let mut p = (reply, ctx);
|
||||||
|
match handle_save_respond(ctx, &env, msg.author.id, &mut p, &beatmap, u, mode).await {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => {
|
||||||
|
p.0.delete(&ctx).await?;
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -325,11 +332,18 @@ pub(crate) async fn find_save_requirements(
|
||||||
Ok((u, mode, score, beatmap, info))
|
Ok((u, mode, score, beatmap, info))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SAVE_BUTTON: &str = "youmubot::osu::save";
|
||||||
|
pub(crate) fn save_button() -> CreateActionRow {
|
||||||
|
CreateActionRow::Buttons(vec![CreateButton::new(SAVE_BUTTON)
|
||||||
|
.label("I'm done!")
|
||||||
|
.emoji('👌')
|
||||||
|
.style(serenity::all::ButtonStyle::Primary)])
|
||||||
|
}
|
||||||
pub(crate) async fn handle_save_respond(
|
pub(crate) async fn handle_save_respond(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
env: &OsuEnv,
|
env: &OsuEnv,
|
||||||
sender: serenity::all::UserId,
|
sender: serenity::all::UserId,
|
||||||
mut reply: Message,
|
reply: &mut impl CanEdit,
|
||||||
beatmap: &Beatmap,
|
beatmap: &Beatmap,
|
||||||
user: crate::models::User,
|
user: crate::models::User,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
|
@ -343,50 +357,36 @@ pub(crate) async fn handle_save_respond(
|
||||||
.take(1)
|
.take(1)
|
||||||
.any(|s| s.beatmap_id == map_id))
|
.any(|s| s.beatmap_id == map_id))
|
||||||
}
|
}
|
||||||
let reaction = reply.react(&ctx, '👌').await?;
|
let msg_id = reply.get_message().await?.id;
|
||||||
|
let recv = InteractionCollector::create(&ctx, msg_id).await?;
|
||||||
|
let timeout = std::time::Duration::from_secs(300) + beatmap.difficulty.total_length;
|
||||||
let completed = loop {
|
let completed = loop {
|
||||||
let emoji = reaction.emoji.clone();
|
let Some(reaction) = recv.next(timeout).await else {
|
||||||
let user_reaction = collector::ReactionCollector::new(ctx)
|
|
||||||
.message_id(reply.id)
|
|
||||||
.author_id(sender)
|
|
||||||
.filter(move |r| r.emoji == emoji)
|
|
||||||
.timeout(std::time::Duration::from_secs(300) + beatmap.difficulty.total_length)
|
|
||||||
.next()
|
|
||||||
.await;
|
|
||||||
if let Some(ur) = user_reaction {
|
|
||||||
if check(osu_client, &user, mode, beatmap.beatmap_id).await? {
|
|
||||||
break true;
|
|
||||||
}
|
|
||||||
ur.delete(&ctx).await?;
|
|
||||||
} else {
|
|
||||||
break false;
|
break false;
|
||||||
|
};
|
||||||
|
if reaction == SAVE_BUTTON && check(osu_client, &user, mode, beatmap.beatmap_id).await? {
|
||||||
|
break true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if !completed {
|
if !completed {
|
||||||
reply
|
reply
|
||||||
.edit(
|
.apply_edit(
|
||||||
&ctx,
|
CreateReply::default()
|
||||||
EditMessage::new()
|
|
||||||
.content(format!(
|
.content(format!(
|
||||||
"Setting username to **{}** failed due to timeout. Please try again!",
|
"Setting username to **{}** failed due to timeout. Please try again!",
|
||||||
user.username
|
user.username
|
||||||
))
|
))
|
||||||
.embeds(vec![])
|
|
||||||
.components(vec![]),
|
.components(vec![]),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
reaction.delete(&ctx).await?;
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
add_user(sender, &user, &env).await?;
|
add_user(sender, &user, &env).await?;
|
||||||
let ex = UserExtras::from_user(env, &user, mode).await?;
|
let ex = UserExtras::from_user(env, &user, mode).await?;
|
||||||
reply
|
reply
|
||||||
.channel_id
|
.apply_edit(
|
||||||
.send_message(
|
CreateReply::default()
|
||||||
&ctx,
|
|
||||||
CreateMessage::new()
|
|
||||||
.reference_message(&reply)
|
|
||||||
.content(
|
.content(
|
||||||
MessageBuilder::new()
|
MessageBuilder::new()
|
||||||
.push("Youmu is now tracking user ")
|
.push("Youmu is now tracking user ")
|
||||||
|
@ -395,7 +395,8 @@ pub(crate) async fn handle_save_respond(
|
||||||
.push(user.mention().to_string())
|
.push(user.mention().to_string())
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
.add_embed(user_embed(user.clone(), ex)),
|
.embed(user_embed(user.clone(), ex))
|
||||||
|
.components(vec![]),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -3,7 +3,7 @@ use serenity::{
|
||||||
all::{CreateInteractionResponse, Interaction, MessageId},
|
all::{CreateInteractionResponse, Interaction, MessageId},
|
||||||
prelude::TypeMapKey,
|
prelude::TypeMapKey,
|
||||||
};
|
};
|
||||||
use std::{ops::Deref, sync::Arc};
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
/// Handles distributing interaction to the handlers.
|
/// Handles distributing interaction to the handlers.
|
||||||
|
@ -13,16 +13,25 @@ pub struct InteractionCollector {
|
||||||
|
|
||||||
/// Wraps the interfaction receiver channel, automatically cleaning up upon drop.
|
/// Wraps the interfaction receiver channel, automatically cleaning up upon drop.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct InteractionCollectorGuard {
|
pub struct InteractionCollectorGuard {
|
||||||
msg_id: MessageId,
|
msg_id: MessageId,
|
||||||
ch: flume::Receiver<String>,
|
ch: flume::Receiver<String>,
|
||||||
collector: InteractionCollector,
|
collector: InteractionCollector,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for InteractionCollectorGuard {
|
impl InteractionCollectorGuard {
|
||||||
type Target = flume::Receiver<String>;
|
/// Returns the next fetched interaction, with the given timeout.
|
||||||
|
pub async fn next(&self, timeout: std::time::Duration) -> Option<String> {
|
||||||
|
match tokio::time::timeout(timeout, self.ch.clone().into_recv_async()).await {
|
||||||
|
Err(_) => None,
|
||||||
|
Ok(Err(_)) => None,
|
||||||
|
Ok(Ok(interaction)) => Some(interaction),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
impl AsRef<flume::Receiver<String>> for InteractionCollectorGuard {
|
||||||
|
fn as_ref(&self) -> &flume::Receiver<String> {
|
||||||
&self.ch
|
&self.ch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +49,7 @@ impl InteractionCollector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Create a new collector, returning a receiver.
|
/// Create a new collector, returning a receiver.
|
||||||
pub fn create_collector(&self, msg: MessageId) -> impl Deref<Target = flume::Receiver<String>> {
|
pub fn create_collector(&self, msg: MessageId) -> InteractionCollectorGuard {
|
||||||
let (send, recv) = flume::unbounded();
|
let (send, recv) = flume::unbounded();
|
||||||
self.channels.insert(msg.clone(), send);
|
self.channels.insert(msg.clone(), send);
|
||||||
InteractionCollectorGuard {
|
InteractionCollectorGuard {
|
||||||
|
@ -51,10 +60,7 @@ impl InteractionCollector {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new collector, returning a receiver.
|
/// Create a new collector, returning a receiver.
|
||||||
pub(crate) async fn create(
|
pub async fn create(ctx: &Context, msg: MessageId) -> Result<InteractionCollectorGuard> {
|
||||||
ctx: &Context,
|
|
||||||
msg: MessageId,
|
|
||||||
) -> Result<impl Deref<Target = flume::Receiver<String>>> {
|
|
||||||
Ok(ctx
|
Ok(ctx
|
||||||
.data
|
.data
|
||||||
.read()
|
.read()
|
||||||
|
|
|
@ -8,12 +8,6 @@ use serenity::{
|
||||||
builder::CreateMessage,
|
builder::CreateMessage,
|
||||||
model::{channel::Message, id::ChannelId},
|
model::{channel::Message, id::ChannelId},
|
||||||
};
|
};
|
||||||
use tokio::time as tokio_time;
|
|
||||||
|
|
||||||
// const ARROW_RIGHT: &str = "➡️";
|
|
||||||
// const ARROW_LEFT: &str = "⬅️";
|
|
||||||
// const REWIND: &str = "⏪";
|
|
||||||
// const FAST_FORWARD: &str = "⏩";
|
|
||||||
|
|
||||||
const NEXT: &str = "youmubot_pagination_next";
|
const NEXT: &str = "youmubot_pagination_next";
|
||||||
const PREV: &str = "youmubot_pagination_prev";
|
const PREV: &str = "youmubot_pagination_prev";
|
||||||
|
@ -269,10 +263,9 @@ pub async fn paginate_with_first_message(
|
||||||
|
|
||||||
// Loop the handler function.
|
// Loop the handler function.
|
||||||
let res: Result<()> = loop {
|
let res: Result<()> = loop {
|
||||||
match tokio_time::timeout(timeout, recv.clone().into_recv_async()).await {
|
match recv.next(timeout).await {
|
||||||
Err(_) => break Ok(()),
|
None => break Ok(()),
|
||||||
Ok(Err(_)) => break Ok(()),
|
Some(reaction) => {
|
||||||
Ok(Ok(reaction)) => {
|
|
||||||
page = match pager
|
page = match pager
|
||||||
.handle_reaction(page, ctx, &mut message, &reaction)
|
.handle_reaction(page, ctx, &mut message, &reaction)
|
||||||
.await
|
.await
|
||||||
|
|
Loading…
Add table
Reference in a new issue