Skip to content

Commit

Permalink
change autocompletion to use proc macro attrs
Browse files Browse the repository at this point in the history
  • Loading branch information
Jacherr committed Sep 15, 2024
1 parent 3bbeae0 commit 5abb609
Show file tree
Hide file tree
Showing 10 changed files with 253 additions and 124 deletions.
4 changes: 2 additions & 2 deletions assyst-core/src/bad_translator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ impl BadTranslator {
}
}

pub async fn set_channel_language(&self, id: u64, language: impl Into<Box<str>>) {
pub async fn _set_channel_language(&self, id: u64, language: impl Into<Box<str>>) {
let mut lock = self.channels.write().await;
lock.entry(id).and_modify(|e| e.language = language.into());
}
Expand All @@ -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
}

Expand Down
85 changes: 7 additions & 78 deletions assyst-core/src/command/autocomplete.rs
Original file line number Diff line number Diff line change
@@ -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<InteractionMarker>,
interaction_token: String,
guild_id: Option<Id<GuildMarker>>,
user_id: Id<UserMarker>,
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<Id<GuildMarker>>,
pub user: User,
pub subcommand: Option<String>,
}

pub fn get_autocomplete_suggestions(text_to_autocomplete: &str, options: &[String]) -> Vec<CommandOptionChoice> {
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::<Vec<_>>()
}
pub const SUGG_LIMIT: usize = 25;
20 changes: 16 additions & 4 deletions assyst-core/src/command/fun/colour.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -36,8 +37,13 @@ const DEFAULT_COLOURS: &[(&str, u32)] = &[
("red", 0xe74c3c),
];

pub async fn colour_role_autocomplete(assyst: ThreadSafeAssyst, guild_id: u64) -> Vec<String> {
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<String> {
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:?}");
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -320,7 +329,10 @@ pub async fn reset(ctxt: CommandCtxt<'_>) -> anyhow::Result<()> {
usage = "",
examples = [""],
)]
pub async fn default(ctxt: CommandCtxt<'_>, colour: Option<WordAutocomplete>) -> anyhow::Result<()> {
pub async fn default(
ctxt: CommandCtxt<'_>,
#[autocomplete = "crate::command::fun::colour::colour_role_autocomplete"] colour: Option<WordAutocomplete>,
) -> 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)
Expand Down
38 changes: 38 additions & 0 deletions assyst-core/src/command/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<twilight_model::application::command::CommandOptionChoice>, 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())));
}
}
}
}
}
};
Expand Down
4 changes: 2 additions & 2 deletions assyst-core/src/command/misc/prefix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand All @@ -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<()> {
Expand Down
45 changes: 34 additions & 11 deletions assyst-core/src/command/misc/tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.")
Expand Down Expand Up @@ -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.")
Expand Down Expand Up @@ -541,7 +549,10 @@ pub async fn list(ctxt: CommandCtxt<'_>, user: Option<User>, 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.")
};
Expand Down Expand Up @@ -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.")
};
Expand Down Expand Up @@ -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.")
};
Expand Down Expand Up @@ -877,21 +894,27 @@ pub async fn paste(ctxt: CommandCtxt<'_>, name: Word) -> anyhow::Result<()> {
Ok(())
}

pub async fn tag_names_autocomplete(assyst: ThreadSafeAssyst, guild_id: u64) -> Vec<String> {
Tag::get_names_in_guild(&assyst.database_handler, guild_id as i64)
pub async fn tag_names_autocomplete(assyst: ThreadSafeAssyst, data: AutocompleteData) -> Vec<String> {
Tag::get_names_in_guild(&assyst.database_handler, data.guild_id.unwrap().get() as i64)
.await
.unwrap_or(vec![])
.iter()
.map(|x| x.1.clone())
.collect::<Vec<_>>()
}

pub async fn tag_names_autocomplete_for_user(assyst: ThreadSafeAssyst, guild_id: u64, user_id: u64) -> Vec<String> {
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<String> {
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::<Vec<_>>()
}

Expand All @@ -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<Vec<Word>>,
) -> anyhow::Result<()> {
let Some(guild_id) = ctxt.data.guild_id else {
Expand Down
12 changes: 11 additions & 1 deletion assyst-core/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Vec<CommandOptionChoice>, ExecutionError>;
}

/// A set of timings used to diagnose slow areas of parsing for commands.
Expand Down
10 changes: 8 additions & 2 deletions assyst-core/src/command/services/cooltext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
pub async fn cooltext_options_autocomplete(_a: ThreadSafeAssyst, _d: AutocompleteData) -> Vec<String> {
let options = STYLES.iter().map(|x| x.0.to_owned()).collect::<Vec<_>>();
options
}
Expand All @@ -22,7 +24,11 @@ pub fn cooltext_options_autocomplete() -> Vec<String> {
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
Expand Down
Loading

0 comments on commit 5abb609

Please sign in to comment.