diff --git a/assyst-core/src/bad_translator.rs b/assyst-core/src/bad_translator.rs index 91cfbbb..baad153 100644 --- a/assyst-core/src/bad_translator.rs +++ b/assyst-core/src/bad_translator.rs @@ -79,7 +79,7 @@ impl BadTranslator { } } - pub async fn set_channel_language(&self, id: u64, language: impl Into>) { + pub async fn _set_channel_language(&self, id: u64, language: impl Into>) { let mut lock = self.channels.write().await; lock.entry(id).and_modify(|e| e.language = language.into()); } @@ -100,7 +100,7 @@ impl BadTranslator { *self.channels.write().await = channels; } - pub async fn should_fetch(&self) -> bool { + pub async fn _should_fetch(&self) -> bool { !self.is_disabled().await && self.channels.read().await.len() == 0 } diff --git a/assyst-core/src/command/autocomplete.rs b/assyst-core/src/command/autocomplete.rs index 191d080..be206b5 100644 --- a/assyst-core/src/command/autocomplete.rs +++ b/assyst-core/src/command/autocomplete.rs @@ -1,82 +1,11 @@ -use assyst_common::err; -use twilight_model::application::command::{CommandOptionChoice, CommandOptionChoiceValue}; -use twilight_model::http::interaction::{InteractionResponse, InteractionResponseType}; -use twilight_model::id::marker::{GuildMarker, InteractionMarker, UserMarker}; +use twilight_model::id::marker::GuildMarker; use twilight_model::id::Id; -use twilight_util::builder::InteractionResponseDataBuilder; +use twilight_model::user::User; -use super::fun::colour::colour_role_autocomplete; -use super::misc::tag::{tag_names_autocomplete, tag_names_autocomplete_for_user}; -use super::services::cooltext::cooltext_options_autocomplete; -use crate::assyst::ThreadSafeAssyst; - -const SUGG_LIMIT: usize = 25; - -// FIXME: pass a struct with data instead of having so many arguments -#[allow(clippy::too_many_arguments)] -pub async fn handle_autocomplete( - assyst: ThreadSafeAssyst, - interaction_id: Id, - interaction_token: String, - guild_id: Option>, - user_id: Id, - command_full_name: &str, - option: &str, - text_to_autocomplete: &str, -) { - // FIXME: minimise hardcoding strings etc as much as possible - // future improvement is to use callbacks, but quite a lot of work - // considering this is only used in a small handful of places - // FIXME: guild id unwrap needs handling properly when tags come to dms etc - let opts = match (command_full_name, option) { - ("cooltext create", "style") => cooltext_options_autocomplete(), - ("tag run", "name") | ("tag raw", "name") | ("tag copy", "name") | ("tag info", "name") => { - tag_names_autocomplete(assyst.clone(), guild_id.unwrap().get()).await - }, - ("tag edit", "name") | ("tag delete", "name") => { - tag_names_autocomplete_for_user(assyst.clone(), guild_id.unwrap().get(), user_id.get()).await - }, - ("colour assign", "colour") | ("colour remove", "name") => { - colour_role_autocomplete(assyst.clone(), guild_id.unwrap().get()).await - }, - _ => { - err!("Trying to autocomplete for invalid command: {command_full_name} (arg {option})"); - return; - }, - }; - - let suggestions = get_autocomplete_suggestions(text_to_autocomplete, &opts); - - let b = InteractionResponseDataBuilder::new(); - let b = b.choices(suggestions); - let r = b.build(); - let r = InteractionResponse { - kind: InteractionResponseType::ApplicationCommandAutocompleteResult, - data: Some(r), - }; - - if let Err(e) = assyst - .interaction_client() - .create_response(interaction_id, &interaction_token, &r) - .await - { - err!("Failed to send autocomplete options: {e:?}"); - }; +pub struct AutocompleteData { + pub guild_id: Option>, + pub user: User, + pub subcommand: Option, } -pub fn get_autocomplete_suggestions(text_to_autocomplete: &str, options: &[String]) -> Vec { - options - .iter() - .filter(|x| { - x.to_ascii_lowercase() - .starts_with(&text_to_autocomplete.to_ascii_lowercase()) - }) - .take(SUGG_LIMIT) - .map(|x| CommandOptionChoice { - name: x.clone(), - name_localizations: None, - // FIXME: hardcoded string type - value: CommandOptionChoiceValue::String(x.clone()), - }) - .collect::>() -} +pub const SUGG_LIMIT: usize = 25; diff --git a/assyst-core/src/command/fun/colour.rs b/assyst-core/src/command/fun/colour.rs index 88dfd99..19a002a 100644 --- a/assyst-core/src/command/fun/colour.rs +++ b/assyst-core/src/command/fun/colour.rs @@ -11,6 +11,7 @@ use twilight_model::id::Id; use crate::assyst::ThreadSafeAssyst; use crate::command::arguments::{Word, WordAutocomplete}; +use crate::command::autocomplete::AutocompleteData; use crate::command::flags::{flags_from_str, FlagDecode, FlagType}; use crate::command::{Availability, Category, CommandCtxt}; use crate::{define_commandgroup, flag_parse_argument}; @@ -36,8 +37,13 @@ const DEFAULT_COLOURS: &[(&str, u32)] = &[ ("red", 0xe74c3c), ]; -pub async fn colour_role_autocomplete(assyst: ThreadSafeAssyst, guild_id: u64) -> Vec { - let roles = match ColourRole::list_in_guild(&assyst.database_handler, guild_id as i64).await { +pub async fn colour_role_autocomplete(assyst: ThreadSafeAssyst, autocomplete_data: AutocompleteData) -> Vec { + let roles = match ColourRole::list_in_guild( + &assyst.database_handler, + autocomplete_data.guild_id.unwrap().get() as i64, + ) + .await + { Ok(l) => l, Err(e) => { err!("Error fetching colour roles for autocompletion: {e:?}"); @@ -182,7 +188,10 @@ pub async fn add_default(ctxt: CommandCtxt<'_>) -> anyhow::Result<()> { usage = "[name]", examples = ["red"], )] -pub async fn remove(ctxt: CommandCtxt<'_>, name: WordAutocomplete) -> anyhow::Result<()> { +pub async fn remove( + ctxt: CommandCtxt<'_>, + #[autocomplete = "crate::command::fun::colour::colour_role_autocomplete"] name: WordAutocomplete, +) -> anyhow::Result<()> { if let Some(id) = ctxt.data.guild_id.map(|x| x.get()) { let colour = name.0.to_ascii_lowercase(); @@ -320,7 +329,10 @@ pub async fn reset(ctxt: CommandCtxt<'_>) -> anyhow::Result<()> { usage = "", examples = [""], )] -pub async fn default(ctxt: CommandCtxt<'_>, colour: Option) -> anyhow::Result<()> { +pub async fn default( + ctxt: CommandCtxt<'_>, + #[autocomplete = "crate::command::fun::colour::colour_role_autocomplete"] colour: Option, +) -> anyhow::Result<()> { if let Some(id) = ctxt.data.guild_id.map(|x| x.get()) { if let Some(colour) = colour.map(|x| x.0.to_ascii_lowercase()) { let roles = ColourRole::list_in_guild(&ctxt.assyst().database_handler, id as i64) diff --git a/assyst-core/src/command/group.rs b/assyst-core/src/command/group.rs index 280c8ed..9f2b715 100644 --- a/assyst-core/src/command/group.rs +++ b/assyst-core/src/command/group.rs @@ -178,6 +178,44 @@ macro_rules! define_commandgroup { Err(err) => Err(err) } } + + async fn arg_autocomplete( + &self, + assyst: crate::assyst::ThreadSafeAssyst, + arg_name: String, + user_input: String, + data: crate::command::autocomplete::AutocompleteData + ) -> Result, crate::command::ExecutionError> { + #[allow(unused_mut, unused_assignments)] + let mut default = ""; + $( + default = $default_interaction_subcommand; + )? + + // if subcommand is the default command, try and call default + #[allow(unreachable_code)] + if let Some(s) = data.subcommand.clone() && s == default { + $( + return [<$default _command>].arg_autocomplete(assyst, arg_name, user_input, data).await; + )? + return Err(crate::command::ExecutionError::Parse(crate::command::errors::TagParseError::InvalidSubcommand("unknown".to_owned()))); + }; + + let sub = data.subcommand.clone().map(|x| crate::command::group::find_subcommand_interaction_command(&x, Self::SUBCOMMANDS)).flatten(); + + #[allow(unreachable_code)] + match sub { + Some(s) => { + return s.arg_autocomplete(assyst, arg_name, user_input, data).await; + }, + None => { + $( + return [<$default _command>].arg_autocomplete(assyst, arg_name, user_input, data).await; + )? + return Err(crate::command::ExecutionError::Parse(crate::command::errors::TagParseError::InvalidSubcommand("unknown".to_owned()))); + } + } + } } } }; diff --git a/assyst-core/src/command/misc/prefix.rs b/assyst-core/src/command/misc/prefix.rs index e7a3158..6b1ede0 100644 --- a/assyst-core/src/command/misc/prefix.rs +++ b/assyst-core/src/command/misc/prefix.rs @@ -13,7 +13,7 @@ use crate::define_commandgroup; description = "get server prefix", access = Availability::Public, cooldown = Duration::from_secs(2), - category = Category::Services, + category = Category::Misc, examples = [""], )] pub async fn default(ctxt: CommandCtxt<'_>) -> anyhow::Result<()> { @@ -36,7 +36,7 @@ pub async fn default(ctxt: CommandCtxt<'_>) -> anyhow::Result<()> { description = "set server prefix", access = Availability::ServerManagers, cooldown = Duration::from_secs(2), - category = Category::Services, + category = Category::Misc, examples = ["-", "%"], )] pub async fn set(ctxt: CommandCtxt<'_>, new: Word) -> anyhow::Result<()> { diff --git a/assyst-core/src/command/misc/tag.rs b/assyst-core/src/command/misc/tag.rs index e205833..d28eea2 100644 --- a/assyst-core/src/command/misc/tag.rs +++ b/assyst-core/src/command/misc/tag.rs @@ -23,6 +23,7 @@ use zip::ZipWriter; use super::CommandCtxt; use crate::assyst::ThreadSafeAssyst; use crate::command::arguments::{Image, ImageUrl, RestNoFlags, User, Word, WordAutocomplete}; +use crate::command::autocomplete::AutocompleteData; use crate::command::componentctxt::{ button_emoji_new, button_new, respond_modal, respond_update_text, ComponentCtxt, ComponentInteractionData, ComponentMetadata, @@ -93,7 +94,11 @@ pub async fn create(ctxt: CommandCtxt<'_>, name: Word, contents: RestNoFlags) -> examples = ["test hello there", "script 2+2 is: {js:2+2}"], guild_only = true )] -pub async fn edit(ctxt: CommandCtxt<'_>, name: WordAutocomplete, contents: RestNoFlags) -> anyhow::Result<()> { +pub async fn edit( + ctxt: CommandCtxt<'_>, + #[autocomplete = "crate::command::misc::tag::tag_names_autocomplete_for_user"] name: WordAutocomplete, + contents: RestNoFlags, +) -> anyhow::Result<()> { let author = ctxt.data.author.id.get(); let Some(guild_id) = ctxt.data.guild_id else { bail!("Tags can only be edited in guilds.") @@ -130,7 +135,10 @@ pub async fn edit(ctxt: CommandCtxt<'_>, name: WordAutocomplete, contents: RestN examples = ["test", "script"], guild_only = true )] -pub async fn delete(ctxt: CommandCtxt<'_>, name: WordAutocomplete) -> anyhow::Result<()> { +pub async fn delete( + ctxt: CommandCtxt<'_>, + #[autocomplete = "crate::command::misc::tag::tag_names_autocomplete"] name: WordAutocomplete, +) -> anyhow::Result<()> { let author = ctxt.data.author.id.get(); let Some(guild_id) = ctxt.data.guild_id else { bail!("Tags can only be deleted in guilds.") @@ -541,7 +549,10 @@ pub async fn list(ctxt: CommandCtxt<'_>, user: Option, flags: TagListFlags examples = ["test", "script"], guild_only = true )] -pub async fn info(ctxt: CommandCtxt<'_>, name: WordAutocomplete) -> anyhow::Result<()> { +pub async fn info( + ctxt: CommandCtxt<'_>, + #[autocomplete = "crate::command::misc::tag::tag_names_autocomplete"] name: WordAutocomplete, +) -> anyhow::Result<()> { let Some(guild_id) = ctxt.data.guild_id else { bail!("Tag information can only be fetched in guilds.") }; @@ -576,7 +587,10 @@ pub async fn info(ctxt: CommandCtxt<'_>, name: WordAutocomplete) -> anyhow::Resu examples = ["test", "script"], guild_only = true )] -pub async fn raw(ctxt: CommandCtxt<'_>, name: WordAutocomplete) -> anyhow::Result<()> { +pub async fn raw( + ctxt: CommandCtxt<'_>, + #[autocomplete = "crate::command::misc::tag::tag_names_autocomplete"] name: WordAutocomplete, +) -> anyhow::Result<()> { let Some(guild_id) = ctxt.data.guild_id else { bail!("Tag raw content can only be fetched in guilds.") }; @@ -806,7 +820,10 @@ pub async fn backup(ctxt: CommandCtxt<'_>) -> anyhow::Result<()> { examples = ["test", "script"], guild_only = true )] -pub async fn copy(ctxt: CommandCtxt<'_>, name: WordAutocomplete) -> anyhow::Result<()> { +pub async fn copy( + ctxt: CommandCtxt<'_>, + #[autocomplete = "crate::command::misc::tag::tag_names_autocomplete"] name: WordAutocomplete, +) -> anyhow::Result<()> { let Some(guild_id) = ctxt.data.guild_id else { bail!("Tags can only be copied from guilds.") }; @@ -877,8 +894,8 @@ pub async fn paste(ctxt: CommandCtxt<'_>, name: Word) -> anyhow::Result<()> { Ok(()) } -pub async fn tag_names_autocomplete(assyst: ThreadSafeAssyst, guild_id: u64) -> Vec { - Tag::get_names_in_guild(&assyst.database_handler, guild_id as i64) +pub async fn tag_names_autocomplete(assyst: ThreadSafeAssyst, data: AutocompleteData) -> Vec { + Tag::get_names_in_guild(&assyst.database_handler, data.guild_id.unwrap().get() as i64) .await .unwrap_or(vec![]) .iter() @@ -886,12 +903,18 @@ pub async fn tag_names_autocomplete(assyst: ThreadSafeAssyst, guild_id: u64) -> .collect::>() } -pub async fn tag_names_autocomplete_for_user(assyst: ThreadSafeAssyst, guild_id: u64, user_id: u64) -> Vec { - Tag::get_names_in_guild(&assyst.database_handler, guild_id as i64) +pub async fn tag_names_autocomplete_for_user(assyst: ThreadSafeAssyst, data: AutocompleteData) -> Vec { + Tag::get_names_in_guild(&assyst.database_handler, data.guild_id.unwrap().get() as i64) .await .unwrap_or(vec![]) .iter() - .filter_map(|x| if x.0 == user_id { Some(x.1.clone()) } else { None }) + .filter_map(|x| { + if x.0 == data.user.id.get() { + Some(x.1.clone()) + } else { + None + } + }) .collect::>() } @@ -907,7 +930,7 @@ pub async fn tag_names_autocomplete_for_user(assyst: ThreadSafeAssyst, guild_id: )] pub async fn default( ctxt: CommandCtxt<'_>, - tag_name: WordAutocomplete, + #[autocomplete = "crate::command::misc::tag::tag_names_autocomplete"] tag_name: WordAutocomplete, arguments: Option>, ) -> anyhow::Result<()> { let Some(guild_id) = ctxt.data.guild_id else { diff --git a/assyst-core/src/command/mod.rs b/assyst-core/src/command/mod.rs index 5e97cef..886df87 100644 --- a/assyst-core/src/command/mod.rs +++ b/assyst-core/src/command/mod.rs @@ -35,8 +35,9 @@ use assyst_common::config::CONFIG; use assyst_database::model::guild_disabled_command::GuildDisabledCommand; use assyst_flux_iface::FluxHandler; use async_trait::async_trait; +use autocomplete::AutocompleteData; use errors::TagParseError; -use twilight_model::application::command::CommandOption; +use twilight_model::application::command::{CommandOption, CommandOptionChoice}; use twilight_model::application::interaction::application_command::{CommandDataOption, CommandOptionValue}; use twilight_model::channel::{Attachment, Message}; use twilight_model::http::interaction::InteractionResponse; @@ -230,6 +231,15 @@ pub trait Command { /// Parses arguments and executes the command, when the source is an interaction command. async fn execute_interaction_command(&self, ctxt: InteractionCommandParseCtxt<'_>) -> Result<(), ExecutionError>; + + /// Provides autocompletion data for autocomplete arguments + async fn arg_autocomplete( + &self, + assyst: ThreadSafeAssyst, + arg_name: String, + user_input: String, + data: AutocompleteData, + ) -> Result, ExecutionError>; } /// A set of timings used to diagnose slow areas of parsing for commands. diff --git a/assyst-core/src/command/services/cooltext.rs b/assyst-core/src/command/services/cooltext.rs index 2f44b74..a048d64 100644 --- a/assyst-core/src/command/services/cooltext.rs +++ b/assyst-core/src/command/services/cooltext.rs @@ -4,12 +4,14 @@ use assyst_proc_macro::command; use assyst_string_fmt::Markdown; use rand::{thread_rng, Rng}; +use crate::assyst::ThreadSafeAssyst; use crate::command::arguments::{Rest, WordAutocomplete}; +use crate::command::autocomplete::AutocompleteData; use crate::command::{Availability, Category, CommandCtxt}; use crate::define_commandgroup; use crate::rest::cooltext::STYLES; -pub fn cooltext_options_autocomplete() -> Vec { +pub async fn cooltext_options_autocomplete(_a: ThreadSafeAssyst, _d: AutocompleteData) -> Vec { let options = STYLES.iter().map(|x| x.0.to_owned()).collect::>(); options } @@ -22,7 +24,11 @@ pub fn cooltext_options_autocomplete() -> Vec { examples = ["burning hello", "saint fancy", "random im random"], send_processing = true )] -pub async fn default(ctxt: CommandCtxt<'_>, style: WordAutocomplete, text: Rest) -> anyhow::Result<()> { +pub async fn default( + ctxt: CommandCtxt<'_>, + #[autocomplete = "crate::command::services::cooltext::cooltext_options_autocomplete"] style: WordAutocomplete, + text: Rest, +) -> anyhow::Result<()> { let style = if &style.0 == "random" { let rand = thread_rng().gen_range(0..STYLES.len()); STYLES[rand].0 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 5b63c50..c217108 100644 --- a/assyst-core/src/gateway_handler/event_handlers/interaction_create.rs +++ b/assyst-core/src/gateway_handler/event_handlers/interaction_create.rs @@ -12,10 +12,11 @@ use twilight_model::application::interaction::{InteractionContextType, Interacti use twilight_model::gateway::payload::incoming::InteractionCreate; use twilight_model::http::interaction::{InteractionResponse, InteractionResponseType}; use twilight_model::util::Timestamp; +use twilight_util::builder::InteractionResponseDataBuilder; use super::after_command_execution_success; use crate::assyst::ThreadSafeAssyst; -use crate::command::autocomplete::handle_autocomplete; +use crate::command::autocomplete::AutocompleteData; use crate::command::componentctxt::ComponentInteractionData; use crate::command::registry::find_command_by_name; use crate::command::source::Source; @@ -303,27 +304,38 @@ pub async fn handle(assyst: ThreadSafeAssyst, InteractionCreate(interaction): In unreachable!() }; - // full_name will use the interaction command replacement for any "default" subcommand (e.g., "tag - // run") - let full_name = if let Some(i) = interaction_subcommand { - format!("{} {}", command.metadata().name, i.0) - } else { - command.metadata().name.to_owned() + let data = AutocompleteData { + guild_id: interaction.guild_id, + user: interaction.author().unwrap().clone(), + subcommand: interaction_subcommand.map(|x| x.0), + }; + + let options = match command + .arg_autocomplete(assyst.clone(), focused_option.name.clone(), inner_option.0, data) + .await + { + Ok(o) => o, + Err(e) => { + err!("Failed to generate options for option {}: {e:?}", focused_option.name); + return; + }, }; - let author_id = interaction.author().unwrap().id; + let b = InteractionResponseDataBuilder::new(); + let b = b.choices(options); + let r = b.build(); + let r = InteractionResponse { + kind: InteractionResponseType::ApplicationCommandAutocompleteResult, + data: Some(r), + }; - handle_autocomplete( - assyst.clone(), - interaction.id, - interaction.token, - interaction.guild_id, - author_id, - &full_name, - &focused_option.name, - &inner_option.0, - ) - .await; + if let Err(e) = assyst + .interaction_client() + .create_response(interaction.id, &interaction.token, &r) + .await + { + err!("Failed to send autocomplete options: {e:?}"); + }; } } } diff --git a/assyst-proc-macro/src/lib.rs b/assyst-proc-macro/src/lib.rs index 8536776..c180998 100644 --- a/assyst-proc-macro/src/lib.rs +++ b/assyst-proc-macro/src/lib.rs @@ -6,7 +6,7 @@ use proc_macro::TokenStream; use proc_macro2::Span; use quote::{quote, ToTokens}; use syn::punctuated::Punctuated; -use syn::token::Bracket; +use syn::token::{Bracket, Comma}; use syn::{ parse_macro_input, parse_quote, Expr, ExprArray, ExprLit, FnArg, Ident, Item, Lit, LitBool, LitStr, Meta, Pat, PatType, Token, Type, @@ -49,7 +49,7 @@ impl syn::parse::Parse for CommandAttributes { pub fn command(attrs: TokenStream, func: TokenStream) -> TokenStream { let CommandAttributes(attrs) = syn::parse_macro_input!(attrs as CommandAttributes); - let Item::Fn(item) = parse_macro_input!(func as syn::Item) else { + let Item::Fn(mut item) = parse_macro_input!(func as syn::Item) else { panic!("#[command] applied to non-function") }; @@ -82,7 +82,7 @@ pub fn command(attrs: TokenStream, func: TokenStream) -> TokenStream { // but this gives us a more useful error verify_input_is_ctxt(&item.sig.inputs); - for (index, input) in item.sig.inputs.iter().skip(1).enumerate() { + for (index, input) in item.sig.inputs.iter_mut().skip(1).enumerate() { match input { FnArg::Receiver(_) => panic!("#[command] cannot have `self` arguments"), FnArg::Typed(PatType { ty, pat, attrs, .. }) => { @@ -92,9 +92,11 @@ pub fn command(attrs: TokenStream, func: TokenStream) -> TokenStream { command_option_exprs.push(quote! {{ <#ty>::as_command_option(#ident_string) }}); + + parse_attrs.push((ident_string, attrs.clone(), ty.clone())); } - parse_attrs.push((stringify!(#pat).to_string(), attrs.clone())); + attrs.clear(); parse_idents.push(Ident::new(&format!("p{index}"), Span::call_site())); parse_exprs.push(quote!(<#ty>::parse_raw_message(&mut ctxt, Some((stringify!(#pat).to_string(), stringify!(#ty).to_string()))).await)); parse_usage.push(quote!(<#ty as crate::command::arguments::ParseArgument>::usage(stringify!(#pat)))); @@ -103,6 +105,71 @@ pub fn command(attrs: TokenStream, func: TokenStream) -> TokenStream { } } + struct AutocompleteVisitable(bool); + impl<'ast> syn::visit::Visit<'ast> for AutocompleteVisitable { + fn visit_type(&mut self, i: &'ast syn::Type) { + if let Type::Path(p) = i + && let Some(seg) = p.path.segments.last() + && seg.ident == "WordAutocomplete" + { + self.0 = true; + } else { + syn::visit::visit_type(self, i); + } + } + } + + // collect stuff from argument attributes + // add more here as required + // todo: support parameter descriptions later + let mut autocomplete_fns: Punctuated = Punctuated::new(); + + for param in parse_attrs { + use syn::visit::Visit; + + if param.1.is_empty() { + let mut visitor = AutocompleteVisitable(false); + visitor.visit_type(param.2.as_ref()); + + if visitor.0 { + panic!("autocomplete attr must be defined on WordAutocomplete arg type"); + } + } + + for attr in param.1 { + if let Meta::NameValue(n) = attr.meta.clone() + && let Some(s) = n.path.segments.first() + { + if s.ident == "autocomplete" { + if let Expr::Lit(ref l) = n.value + && let Lit::Str(ref s) = l.lit + { + let mut visitor = AutocompleteVisitable(false); + visitor.visit_type(param.2.as_ref()); + + if visitor.0 { + let path = s.parse::().expect("autocomplete: invalid path"); + let arg = param.0.clone(); + autocomplete_fns.push(quote::quote!(#arg => #path(assyst, data).await)); + } else { + panic!("autocomplete attr is only valid on WordAutocomplete arg type"); + } + } else { + panic!("autocomplete: invalid value ({:?})", n.value); + } + } else { + panic!("fn arg attr: invalid name ({:?})", s.ident.to_string()); + } + } else if let Meta::Path(p) = attr.meta { + // add any value-less attrs here + panic!("fn arg attr: invalid attr ({:?})", p.get_ident().map(|x| x.to_string())); + } + } + } + + autocomplete_fns + .push(quote::quote!(_ => panic!("unhandled autocomplete arg name {arg_name:?} for command {}", meta.name))); + let name = fields.remove("name").unwrap_or_else(|| str_expr(&fn_name.to_string())); let aliases = fields.remove("aliases").unwrap_or_else(empty_array_expr); let description = fields.remove("description").expect("missing description"); @@ -251,13 +318,45 @@ pub fn command(attrs: TokenStream, func: TokenStream) -> TokenStream { #fn_name(ctxt.cx, #(#parse_idents),*).await.map_err(crate::command::ExecutionError::Command) } + + #[allow(unreachable_code)] + async fn arg_autocomplete( + &self, + assyst: crate::assyst::ThreadSafeAssyst, + arg_name: String, + user_input: String, + data: crate::command::autocomplete::AutocompleteData + ) -> Result, crate::command::ExecutionError> { + let meta = self.metadata(); + + let options: Vec = match arg_name.as_str() { + #autocomplete_fns + }; + + let choices: Vec = options + .iter() + .filter(|x| { + x.to_ascii_lowercase() + .starts_with(&user_input.to_ascii_lowercase()) + }) + .take(crate::command::autocomplete::SUGG_LIMIT) + .map(|x| twilight_model::application::command::CommandOptionChoice { + name: x.clone(), + name_localizations: None, + // FIXME: hardcoded string type + value: twilight_model::application::command::CommandOptionChoiceValue::String(x.clone()), + }) + .collect::>(); + + Ok(choices) + } } }; let mut output = item.into_token_stream(); output.extend(following); - //panic!("{}", output); + //panic!("{output}"); output.into() }