From 982156e44374876d650bdcac2785137d45558848 Mon Sep 17 00:00:00 2001 From: Kappeh Date: Thu, 30 Jan 2025 02:34:03 +0000 Subject: [PATCH] Add help command --- Cargo.lock | 13 ++ cipher_discord_bot/Cargo.toml | 1 + cipher_discord_bot/src/app/framework.rs | 1 + cipher_discord_bot/src/app/mod.rs | 7 + cipher_discord_bot/src/commands/help.rs | 173 ++++++++++++++++++++++++ cipher_discord_bot/src/commands/mod.rs | 29 ++++ 6 files changed, 224 insertions(+) create mode 100644 cipher_discord_bot/src/commands/help.rs diff --git a/Cargo.lock b/Cargo.lock index 67d97ea..3bf3f12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -343,6 +343,7 @@ dependencies = [ "clap 4.5.27", "dotenvy", "env_logger", + "futures", "humantime", "log", "poise", @@ -857,6 +858,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -879,6 +881,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.31" diff --git a/cipher_discord_bot/Cargo.toml b/cipher_discord_bot/Cargo.toml index bf28553..f9065a0 100644 --- a/cipher_discord_bot/Cargo.toml +++ b/cipher_discord_bot/Cargo.toml @@ -16,6 +16,7 @@ secrecy = "0.10.3" serenity = "0.12.4" thiserror = "2.0.11" tokio = { version = "1.43.0", features = ["full"] } +futures = "0.3.31" [features] default = ["mysql", "postgres", "sqlite"] diff --git a/cipher_discord_bot/src/app/framework.rs b/cipher_discord_bot/src/app/framework.rs index 7d8ef1b..19efc09 100644 --- a/cipher_discord_bot/src/app/framework.rs +++ b/cipher_discord_bot/src/app/framework.rs @@ -19,6 +19,7 @@ where let app_data = AppData { repository_provider, + qualified_command_names: commands::qualified_command_names(&commands), }; let options = FrameworkOptions::, AppError> { diff --git a/cipher_discord_bot/src/app/mod.rs b/cipher_discord_bot/src/app/mod.rs index 93624b4..bae00d0 100644 --- a/cipher_discord_bot/src/app/mod.rs +++ b/cipher_discord_bot/src/app/mod.rs @@ -18,6 +18,7 @@ pub enum AppStartError { pub struct AppData { repository_provider: R, + qualified_command_names: Vec, } #[derive(Debug, thiserror::Error)] @@ -61,3 +62,9 @@ where self.repository_provider.get().await } } + +impl AppData { + pub fn qualified_command_names(&self) -> &[String] { + &self.qualified_command_names + } +} diff --git a/cipher_discord_bot/src/commands/help.rs b/cipher_discord_bot/src/commands/help.rs new file mode 100644 index 0000000..c295b62 --- /dev/null +++ b/cipher_discord_bot/src/commands/help.rs @@ -0,0 +1,173 @@ +use futures::Stream; +use cipher_core::repository::RepositoryProvider; +use poise::CreateReply; +use serenity::all::Color; +use serenity::all::CreateEmbed; +use serenity::futures::StreamExt; + +use crate::app::AppCommand; +use crate::app::AppContext; +use crate::app::AppError; + +async fn autocomplete_command<'a, R> ( + ctx: AppContext<'a, R, R::BackendError>, + partial: &'a str, +) -> impl Stream + 'a +where + R: RepositoryProvider, +{ + futures::stream::iter(ctx.data().qualified_command_names()) + .filter(move |name| futures::future::ready(name.contains(partial))) + .map(|name| name.to_string()) +} + +/// Show help message. +#[poise::command(slash_command)] +pub async fn help( + ctx: AppContext<'_, R, R::BackendError>, + #[rename = "command"] + #[description = "Command to get help for."] + #[autocomplete = "autocomplete_command"] + option_query: Option, + #[description = "Show hidden commands. Defaults to False."] all: Option, + #[description = "Hide reply from other users. Defaults to True."] ephemeral: Option, +) -> Result<(), AppError> { + let embed = if let Some(query) = &option_query { + let option_command = poise::find_command( + &ctx.framework().options.commands, + query, + true, + &mut Vec::new(), + ); + + if let Some((command, _, _)) = option_command { + command_help_embed(command, all.unwrap_or(false)) + } else { + CreateEmbed::new() + .title("Help") + .description(format!("Could not find command `/{}`", query)) + .color(Color::BLURPLE) + } + } else { + root_help_embed(&ctx, all.unwrap_or(false)) + }; + + let reply = CreateReply::default() + .embed(embed) + .ephemeral(ephemeral.unwrap_or(true)); + + ctx.send(reply).await?; + + Ok(()) +} + +fn root_help_embed(ctx: &AppContext<'_, R, R::BackendError>, all: bool) -> CreateEmbed +where + R: RepositoryProvider, +{ + let mut commands_field_value = String::new(); + for command in &ctx.framework().options.commands { + if !all && command.hide_in_help { + continue; + } + + commands_field_value.push('`'); + commands_field_value.push('/'); + commands_field_value.push_str(&command.name); + commands_field_value.push('`'); + + if let Some(command_description) = &command.description { + commands_field_value.push_str(" - "); + commands_field_value.push_str(command_description); + } + + commands_field_value.push('\n'); + } + commands_field_value.pop(); + + let mut embed = CreateEmbed::new() + .title("Help") + .color(Color::BLURPLE); + + if !commands_field_value.is_empty() { + embed = embed.field("Commands", commands_field_value, false); + } + + embed = embed.field("More", "For information on specific commands `/help `.", false); + + embed +} + +fn command_help_embed(command: &AppCommand, all: bool) -> CreateEmbed +where + R: RepositoryProvider, +{ + let mut required_parameters_field_value = String::new(); + let mut optional_parameters_field_value = String::new(); + for parameter in &command.parameters { + let parameters_field_value = match parameter.required { + true => &mut required_parameters_field_value, + false => &mut optional_parameters_field_value, + }; + + parameters_field_value.push('`'); + parameters_field_value.push_str(¶meter.name); + parameters_field_value.push('`'); + + if let Some(parameter_description) = ¶meter.description { + parameters_field_value.push_str(" - "); + parameters_field_value.push_str(parameter_description); + } + + parameters_field_value.push('\n'); + } + required_parameters_field_value.pop(); // Removes final newline + optional_parameters_field_value.pop(); // Removes final newline + + let mut subcommands_field_value = String::new(); + for subcommand in &command.subcommands { + if !all && subcommand.hide_in_help { + continue; + } + + subcommands_field_value.push('`'); + subcommands_field_value.push_str(&subcommand.name); + subcommands_field_value.push('`'); + + if let Some(command_description) = &subcommand.description { + subcommands_field_value.push_str(" - "); + subcommands_field_value.push_str(command_description); + } + + subcommands_field_value.push('\n'); + } + subcommands_field_value.pop(); // Removes final newline + + let mut embed = CreateEmbed::new() + .title(format!("Help `/{}`", command.qualified_name)) + .color(Color::BLURPLE); + + if let Some(category) = &command.category { + embed = embed.field("Category", category, false); + } + + if !required_parameters_field_value.is_empty() { + embed = embed.field("Required Parameters", required_parameters_field_value, false); + } + + if !optional_parameters_field_value.is_empty() { + embed = embed.field("Optional Parameters", optional_parameters_field_value, false); + } + + if !subcommands_field_value.is_empty() { + embed = embed.field("Subcommands", subcommands_field_value, false); + } + + if let Some(description) = &command.description { + embed = embed.description(description); + } + + embed = embed.field("More", "For information on specific commands `/help `.", false); + + embed +} diff --git a/cipher_discord_bot/src/commands/mod.rs b/cipher_discord_bot/src/commands/mod.rs index f1682be..b5e5ec5 100644 --- a/cipher_discord_bot/src/commands/mod.rs +++ b/cipher_discord_bot/src/commands/mod.rs @@ -2,6 +2,7 @@ use cipher_core::repository::RepositoryProvider; use crate::app::AppCommand; +mod help; mod ping; mod profile; @@ -10,7 +11,35 @@ where R: RepositoryProvider + Send + Sync + 'static, { vec![ + help::help(), ping::ping(), profile::profile(), ] } + +pub fn qualified_command_names(commands: &[AppCommand]) -> Vec +where + R: RepositoryProvider, +{ + let mut prefix = String::new(); + let mut names = Vec::new(); + qualified_command_names_inner(&mut prefix, commands, &mut names); + names +} + +fn qualified_command_names_inner(prefix: &mut String, commands: &[AppCommand], names: &mut Vec) +where + R: RepositoryProvider, +{ + for command in commands { + if command.subcommands.is_empty() { + names.push(format!("{}{}", prefix, command.qualified_name.clone())); + } else { + let old_len = prefix.len(); + prefix.push_str(&command.qualified_name); + prefix.push(' '); + qualified_command_names_inner(prefix, &command.subcommands, names); + prefix.truncate(old_len); + } + } +}