From fba763263ecfa9d563eacad038b19b1040ad2bb9 Mon Sep 17 00:00:00 2001 From: Jacherr Date: Mon, 16 Sep 2024 16:27:30 +0100 Subject: [PATCH] define context commands --- assyst-core/src/command/arguments.rs | 36 +++++++++++++++++++ assyst-core/src/command/fun/mod.rs | 10 +++--- assyst-core/src/command/fun/translation.rs | 3 +- assyst-core/src/command/group.rs | 2 +- assyst-core/src/command/image/mod.rs | 12 +++++-- assyst-core/src/command/mod.rs | 4 ++- assyst-core/src/command/registry.rs | 16 +++++++-- assyst-core/src/command/services/mod.rs | 3 +- .../event_handlers/interaction_create.rs | 22 ++++++++++-- .../event_handlers/message_create.rs | 1 + .../event_handlers/message_update.rs | 1 + assyst-proc-macro/src/lib.rs | 2 +- 12 files changed, 96 insertions(+), 16 deletions(-) diff --git a/assyst-core/src/command/arguments.rs b/assyst-core/src/command/arguments.rs index 0fe782f..8667897 100644 --- a/assyst-core/src/command/arguments.rs +++ b/assyst-core/src/command/arguments.rs @@ -522,6 +522,16 @@ impl ParseArgument for Rest { ctxt: &mut InteractionCommandParseCtxt<'_>, label: Label, ) -> Result { + // look for resolved messages: if so, it's a context menu command, and use that message + if let Some(ref ms) = ctxt.cx.data.resolved_messages + && let Some(m) = ms.first() + { + if m.content.is_empty() { + return Err(TagParseError::ArgsExhausted(ArgsExhausted(label))); + } + return Ok(Rest(m.content.clone())); + } + // treat Rest as same as Word because there is no option type which is just one // whitespace-delimited word let word = &ctxt.option_by_name(&label.unwrap().0)?.value; @@ -568,6 +578,16 @@ impl ParseArgument for RestNoFlags { ctxt: &mut InteractionCommandParseCtxt<'_>, label: Label, ) -> Result { + // look for resolved messages: if so, it's a context menu command, and use that message + if let Some(ref ms) = ctxt.cx.data.resolved_messages + && let Some(m) = ms.first() + { + if m.content.is_empty() { + return Err(TagParseError::ArgsExhausted(ArgsExhausted(label))); + } + return Ok(RestNoFlags(m.content.clone())); + } + // treat Rest as same as Word because there is no option type which is just one // whitespace-delimited word let word = &ctxt.option_by_name(&label.unwrap().0)?.value; @@ -947,6 +967,22 @@ impl ParseArgument for ImageUrl { }; } + // if this is Some, this is a context menu command + // we must have our image defined here, instead of looking anywhere else + if let Some(ref r) = ctxt.cx.data.resolved_messages { + if let Some(m) = r.first() + && let Some(i) = m.attachments.first() + { + return ImageUrl::attachment(Some(i)); + } else if let Some(m) = r.first() + && let Some(e) = m.embeds.first() + { + return ImageUrl::embed(Some(e)); + } else { + return Err(TagParseError::ArgsExhausted(ArgsExhausted(label))); + } + } + let attachment_label = Some(( format!("{}-attachment", label.clone().unwrap().0), label.clone().unwrap().1, diff --git a/assyst-core/src/command/fun/mod.rs b/assyst-core/src/command/fun/mod.rs index 3ec8baf..6bb1ee5 100644 --- a/assyst-core/src/command/fun/mod.rs +++ b/assyst-core/src/command/fun/mod.rs @@ -19,10 +19,11 @@ pub mod translation; category = Category::Fun, usage = "[video]", examples = ["https://link.to.my/video.mp4"], - send_processing = true + send_processing = true, + context_menu_command = "Find Song" )] -pub async fn findsong(ctxt: CommandCtxt<'_>, input: ImageUrl) -> anyhow::Result<()> { - let result = identify_song_notsoidentify(&ctxt.assyst().reqwest_client, input.0) +pub async fn findsong(ctxt: CommandCtxt<'_>, audio: ImageUrl) -> anyhow::Result<()> { + let result = identify_song_notsoidentify(&ctxt.assyst().reqwest_client, audio.0) .await .context("Failed to identify song")?; @@ -56,7 +57,8 @@ pub async fn findsong(ctxt: CommandCtxt<'_>, input: ImageUrl) -> anyhow::Result< category = Category::Fun, usage = "[image]", examples = ["https://link.to.my/image.png"], - send_processing = true + send_processing = true, + context_menu_command = "Identify Image" )] pub async fn identify(ctxt: CommandCtxt<'_>, input: ImageUrl) -> anyhow::Result<()> { let result = identify_image(&ctxt.assyst().reqwest_client, &input.0) diff --git a/assyst-core/src/command/fun/translation.rs b/assyst-core/src/command/fun/translation.rs index a05b58b..653f871 100644 --- a/assyst-core/src/command/fun/translation.rs +++ b/assyst-core/src/command/fun/translation.rs @@ -98,7 +98,8 @@ impl ParseArgument for BadTranslateFlags { ("chain", "Show language chain"), ("count", "Set the amount of translations to perform") ], - send_processing = true + send_processing = true, + context_menu_command = "Bad Translate" )] pub async fn bad_translate(ctxt: CommandCtxt<'_>, text: Rest, flags: BadTranslateFlags) -> anyhow::Result<()> { if text.0 == "languages" { diff --git a/assyst-core/src/command/group.rs b/assyst-core/src/command/group.rs index 9f2b715..ab79db0 100644 --- a/assyst-core/src/command/group.rs +++ b/assyst-core/src/command/group.rs @@ -24,7 +24,7 @@ macro_rules! defaults { (send_processing) => { false }; (guild_only $x:expr) => { $x }; (guild_only) => { false }; - (context_menu_command) => { false }; + (context_menu_command) => { "" }; } #[allow(clippy::crate_in_macro_def)] diff --git a/assyst-core/src/command/image/mod.rs b/assyst-core/src/command/image/mod.rs index 1b5b099..496e828 100644 --- a/assyst-core/src/command/image/mod.rs +++ b/assyst-core/src/command/image/mod.rs @@ -329,7 +329,8 @@ pub async fn grayscale(ctxt: CommandCtxt<'_>, source: Image) -> anyhow::Result<( category = Category::Image, usage = "[image]", examples = ["https://link.to.my/image.png"], - send_processing = true + send_processing = true, + context_menu_command = "Image Information" )] pub async fn imageinfo(ctxt: CommandCtxt<'_>, source: Image) -> anyhow::Result<()> { let result = ctxt.flux_handler().image_info(source.0).await?; @@ -793,9 +794,14 @@ pub async fn scramble(ctxt: CommandCtxt<'_>, source: Image) -> anyhow::Result<() pub async fn setloop(ctxt: CommandCtxt<'_>, source: Image, loops: i64) -> anyhow::Result<()> { let result = ctxt .flux_handler() - .set_loop(source.0, ctxt.data.author.id.get(), ctxt.data.guild_id.map(|x| x.get()), loops) + .set_loop( + source.0, + ctxt.data.author.id.get(), + ctxt.data.guild_id.map(|x| x.get()), + loops, + ) .await?; - + ctxt.reply(result).await?; Ok(()) diff --git a/assyst-core/src/command/mod.rs b/assyst-core/src/command/mod.rs index 886df87..71627a1 100644 --- a/assyst-core/src/command/mod.rs +++ b/assyst-core/src/command/mod.rs @@ -106,7 +106,7 @@ pub struct CommandMetadata { pub send_processing: bool, pub age_restricted: bool, pub flag_descriptions: HashMap<&'static str, &'static str>, - pub context_menu_command: bool, + pub context_menu_command: &'static str, pub guild_only: bool, } @@ -279,6 +279,8 @@ pub struct CommandData<'a> { pub interaction_id: Option>, pub interaction_attachments: HashMap, Attachment>, pub command_from_install_context: bool, + /// None if not a context menu command. + pub resolved_messages: Option>, } pub type RawMessageArgsIter<'a> = SplitAsciiWhitespace<'a>; diff --git a/assyst-core/src/command/registry.rs b/assyst-core/src/command/registry.rs index 20a49b7..33e59f1 100644 --- a/assyst-core/src/command/registry.rs +++ b/assyst-core/src/command/registry.rs @@ -127,7 +127,16 @@ pub fn get_or_init_commands() -> &'static HashMap<&'static str, TCommand> { /// Finds a command by its name. pub fn find_command_by_name(name: &str) -> Option { - get_or_init_commands().get(name.to_lowercase().as_str()).copied() + get_or_init_commands() + .get(name.to_lowercase().as_str()) + .copied() + // support context menu command names + .or_else(|| { + get_or_init_commands() + .iter() + .find(|x| x.1.metadata().context_menu_command == name) + .map(|c| *c.1) + }) } pub async fn register_interaction_commands(assyst: ThreadSafeAssyst) -> anyhow::Result> { @@ -149,9 +158,12 @@ pub async fn register_interaction_commands(assyst: ThreadSafeAssyst) -> anyhow:: let mut deduplicated_commands: Vec = vec![]; for command in interaction_commands { if !deduplicated_commands.iter().any(|x| x.name == command.0.name) { - if command.1 { + if !command.1.is_empty() { let mut copy = command.0.clone(); + copy.name = command.1.to_owned(); copy.kind = CommandType::Message; + copy.options = vec![]; + copy.description = String::new(); deduplicated_commands.push(copy); } deduplicated_commands.push(command.0); diff --git a/assyst-core/src/command/services/mod.rs b/assyst-core/src/command/services/mod.rs index e925642..0372d6d 100644 --- a/assyst-core/src/command/services/mod.rs +++ b/assyst-core/src/command/services/mod.rs @@ -19,7 +19,8 @@ pub mod download; category = Category::Services, usage = "[text]", examples = ["yep im burning"], - send_processing = true + send_processing = true, + context_menu_command = "Burn Text" )] pub async fn burntext(ctxt: CommandCtxt<'_>, text: Rest) -> anyhow::Result<()> { let result = burn_text(&text.0).await?; diff --git a/assyst-core/src/gateway_handler/event_handlers/interaction_create.rs b/assyst-core/src/gateway_handler/event_handlers/interaction_create.rs index c217108..26d397b 100644 --- a/assyst-core/src/gateway_handler/event_handlers/interaction_create.rs +++ b/assyst-core/src/gateway_handler/event_handlers/interaction_create.rs @@ -5,10 +5,12 @@ use assyst_common::config::CONFIG; use assyst_common::err; use assyst_database::model::active_guild_premium_entitlement::ActiveGuildPremiumEntitlement; use tracing::{debug, warn}; +use twilight_model::application::command::CommandType; use twilight_model::application::interaction::application_command::{ CommandData as DiscordCommandData, CommandDataOption, CommandOptionValue, }; use twilight_model::application::interaction::{InteractionContextType, InteractionData, InteractionType}; +use twilight_model::channel::Message; use twilight_model::gateway::payload::incoming::InteractionCreate; use twilight_model::http::interaction::{InteractionResponse, InteractionResponseType}; use twilight_model::util::Timestamp; @@ -115,7 +117,8 @@ pub async fn handle(assyst: ThreadSafeAssyst, InteractionCreate(interaction): In let incoming_match = incoming_options.iter().find(|x| x.name == option.name); if let Some(op) = incoming_match { sorted_incoming_options.push(op.clone()); - } else { + // context menu commands have no options and are handled independently + } else if command_data.kind != CommandType::Message { // default required: false if option.required.unwrap_or(false) { err!( @@ -137,6 +140,20 @@ pub async fn handle(assyst: ThreadSafeAssyst, InteractionCreate(interaction): In None }; + let interaction_attachments = command_data + .resolved + .clone() + .map(|x| x.attachments) + .unwrap_or(HashMap::new()); + + // resolve message attachments for context menu commands + let mut resolved_messages: Option> = None; + if let Some(ref ms) = command_data.resolved.map(|x| x.messages) + && command_data.kind == CommandType::Message + { + resolved_messages = Some(ms.values().cloned().collect()); + } + let data = CommandData { source: Source::Interaction, assyst: &assyst, @@ -155,11 +172,12 @@ pub async fn handle(assyst: ThreadSafeAssyst, InteractionCreate(interaction): In author: interaction.member.and_then(|x| x.user).or(interaction.user).unwrap(), interaction_token: Some(interaction.token), interaction_id: Some(interaction.id), - interaction_attachments: command_data.resolved.map(|x| x.attachments).unwrap_or(HashMap::new()), + interaction_attachments, command_from_install_context: match interaction.context { Some(c) => c == InteractionContextType::PrivateChannel, None => false, }, + resolved_messages, }; let ctxt = InteractionCommandParseCtxt::new(CommandCtxt::new(&data), &sorted_incoming_options); diff --git a/assyst-core/src/gateway_handler/event_handlers/message_create.rs b/assyst-core/src/gateway_handler/event_handlers/message_create.rs index 077814a..5ae910c 100644 --- a/assyst-core/src/gateway_handler/event_handlers/message_create.rs +++ b/assyst-core/src/gateway_handler/event_handlers/message_create.rs @@ -46,6 +46,7 @@ pub async fn handle(assyst: ThreadSafeAssyst, MessageCreate(message): MessageCre interaction_id: None, interaction_attachments: HashMap::new(), command_from_install_context: false, + resolved_messages: None, }; let ctxt = RawMessageParseCtxt::new(CommandCtxt::new(&data), result.args); diff --git a/assyst-core/src/gateway_handler/event_handlers/message_update.rs b/assyst-core/src/gateway_handler/event_handlers/message_update.rs index 81d6c6a..1bd1132 100644 --- a/assyst-core/src/gateway_handler/event_handlers/message_update.rs +++ b/assyst-core/src/gateway_handler/event_handlers/message_update.rs @@ -46,6 +46,7 @@ pub async fn handle(assyst: ThreadSafeAssyst, event: MessageUpdate) { interaction_id: None, interaction_attachments: HashMap::new(), command_from_install_context: false, + resolved_messages: None, }; let ctxt = RawMessageParseCtxt::new(CommandCtxt::new(&data), result.args); diff --git a/assyst-proc-macro/src/lib.rs b/assyst-proc-macro/src/lib.rs index f48ba19..17dd5f2 100644 --- a/assyst-proc-macro/src/lib.rs +++ b/assyst-proc-macro/src/lib.rs @@ -188,7 +188,7 @@ pub fn command(attrs: TokenStream, func: TokenStream) -> TokenStream { }); let send_processing = fields.remove("send_processing").unwrap_or_else(false_expr); let age_restricted = fields.remove("age_restricted").unwrap_or_else(false_expr); - let context_menu_command = fields.remove("context_menu_command").unwrap_or_else(false_expr); + let context_menu_command = fields.remove("context_menu_command").unwrap_or_else(|| str_expr("")); let flag_descriptions = fields.remove("flag_descriptions").unwrap_or_else(empty_array_expr); let guild_only = fields.remove("guild_only").unwrap_or_else(false_expr);