From 7b3fad4dc207a27db65349204c20a899b4fac3c2 Mon Sep 17 00:00:00 2001 From: Gnome! Date: Wed, 13 Nov 2024 21:11:25 +0000 Subject: [PATCH] Remove help commands from built-ins (#323) * Remove help commands from built-ins * Fix up docs and remove help_text_fn --- examples/README.md | 3 +- examples/basic_structure/commands.rs | 20 -- examples/basic_structure/main.rs | 2 +- examples/feature_showcase/builtins.rs | 20 -- examples/feature_showcase/main.rs | 2 - examples/help_generation/main.rs | 358 --------------------- macros/src/command/mod.rs | 10 +- macros/src/lib.rs | 2 - src/builtins/help.rs | 429 -------------------------- src/builtins/mod.rs | 10 +- src/builtins/pretty_help.rs | 277 ----------------- src/lib.rs | 7 - 12 files changed, 7 insertions(+), 1133 deletions(-) delete mode 100644 examples/help_generation/main.rs delete mode 100644 src/builtins/help.rs delete mode 100644 src/builtins/pretty_help.rs diff --git a/examples/README.md b/examples/README.md index 74d4f2264135..5eb1890eacb4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -9,8 +9,7 @@ by poise. # basic_structure -Showcases the basics of poise: `FrameworkOptions`, creating and accessing the data struct, a help -command, defining commands and sending responses. +Showcases the basics of poise: `FrameworkOptions`, creating and accessing the data struct, defining commands and sending responses. # feature_showcase diff --git a/examples/basic_structure/commands.rs b/examples/basic_structure/commands.rs index 376da639e3c2..5262615aaaf6 100644 --- a/examples/basic_structure/commands.rs +++ b/examples/basic_structure/commands.rs @@ -1,25 +1,5 @@ use crate::{Context, Error}; -/// Show this help menu -#[poise::command(prefix_command, track_edits, slash_command)] -pub async fn help( - ctx: Context<'_>, - #[description = "Specific command to show help about"] - #[autocomplete = "poise::builtins::autocomplete_command"] - command: Option, -) -> Result<(), Error> { - poise::builtins::help( - ctx, - command.as_deref(), - poise::builtins::HelpConfiguration { - extra_text_at_bottom: "This is an example bot made to showcase features of my custom Discord bot framework", - ..Default::default() - }, - ) - .await?; - Ok(()) -} - /// Vote for something /// /// Enter `~vote pumpkin` to vote for pumpkins diff --git a/examples/basic_structure/main.rs b/examples/basic_structure/main.rs index 70f4df5c2291..d8d6f70b35ae 100644 --- a/examples/basic_structure/main.rs +++ b/examples/basic_structure/main.rs @@ -43,7 +43,7 @@ async fn main() { // FrameworkOptions contains all of poise's configuration option in one struct // Every option can be omitted to use its default value let options = poise::FrameworkOptions { - commands: vec![commands::help(), commands::vote(), commands::getvotes()], + commands: vec![commands::vote(), commands::getvotes()], prefix_options: poise::PrefixFrameworkOptions { prefix: Some("~".into()), edit_tracker: Some(Arc::new(poise::EditTracker::for_timespan( diff --git a/examples/feature_showcase/builtins.rs b/examples/feature_showcase/builtins.rs index a7cab6857e95..bb9f01a2ac07 100644 --- a/examples/feature_showcase/builtins.rs +++ b/examples/feature_showcase/builtins.rs @@ -6,23 +6,3 @@ pub async fn servers(ctx: Context<'_>) -> Result<(), Error> { poise::builtins::servers(ctx).await?; Ok(()) } - -#[poise::command(slash_command, prefix_command)] -pub async fn help(ctx: Context<'_>, command: Option) -> Result<(), Error> { - let configuration = poise::builtins::HelpConfiguration { - // [configure aspects about the help message here] - ..Default::default() - }; - poise::builtins::help(ctx, command.as_deref(), configuration).await?; - Ok(()) -} - -#[poise::command(slash_command, prefix_command)] -pub async fn pretty_help(ctx: Context<'_>, command: Option) -> Result<(), Error> { - let configuration = poise::builtins::PrettyHelpConfiguration { - // [configure aspects about the help message here] - ..Default::default() - }; - poise::builtins::pretty_help(ctx, command.as_deref(), configuration).await?; - Ok(()) -} diff --git a/examples/feature_showcase/main.rs b/examples/feature_showcase/main.rs index 5a2e1a1ce1dc..e74f7ddca8e0 100644 --- a/examples/feature_showcase/main.rs +++ b/examples/feature_showcase/main.rs @@ -40,8 +40,6 @@ async fn main() { bool_parameter::oracle(), #[cfg(feature = "cache")] builtins::servers(), - builtins::help(), - builtins::pretty_help(), checks::shutdown(), checks::modonly(), checks::delete(), diff --git a/examples/help_generation/main.rs b/examples/help_generation/main.rs deleted file mode 100644 index 756372918930..000000000000 --- a/examples/help_generation/main.rs +++ /dev/null @@ -1,358 +0,0 @@ -use poise::{samples::HelpConfiguration, serenity_prelude as serenity}; -use rand::Rng; - -struct Data {} // User data, which is stored and accessible in all command invocations -type Error = Box; -type Context<'a> = poise::Context<'a, Data, Error>; - -const FRUIT: &[&str] = &["🍎", "🍌", "🍊", "🍉", "🍇", "🍓"]; -const VEGETABLES: &[&str] = &["🥕", "🥦", "🥬", "🥒", "🌽", "🥔"]; -const MEAT: &[&str] = &["🥩", "🍗", "🍖", "🥓", "🍔", "🍕"]; -const DAIRY: &[&str] = &["🥛", "🧀", "🍦", "🍨", "🍩", "🍪"]; -const FOOD: &[&str] = &[ - "🍎", "🍌", "🍊", "🍉", "🍇", "🍓", "🥕", "🥦", "🥬", "🥒", "🌽", "🥔", "🥩", "🍗", "🍖", "🥓", - "🍔", "🍕", "🥛", "🧀", "🍦", "🍨", "🍩", "🍪", -]; - -fn ninetynine_bottles() -> String { - let mut bottles = String::new(); - for i in (95..100).rev() { - bottles.push_str(&format!( - "{0} bottles of beer on the wall, {0} bottles of beer!\n", - i - )); - bottles.push_str(&format!( - "Take one down, pass it around, {0} bottles of beer on the wall!\n", - i - 1 - )); - } - bottles += "That's quite enough to demonstrate this function!"; - bottles -} - -#[poise::command( - slash_command, - prefix_command, - category = "Vegan", - help_text_fn = "ninetynine_bottles" -)] -async fn beer(ctx: Context<'_>) -> Result<(), Error> { - ctx.say("🍺").await?; - Ok(()) -} - -/// Respond with a random fruit -/// -/// Subcommands can be used to get a specific fruit -#[poise::command( - slash_command, - prefix_command, - subcommands( - "apple", - "banana", - "orange", - "watermelon", - "grape", - "strawberry", - "help" - ), - category = "Vegan" -)] -async fn fruit(ctx: Context<'_>) -> Result<(), Error> { - let response = FRUIT[rand::thread_rng().gen_range(0..FRUIT.len())]; - ctx.say(response).await?; - Ok(()) -} - -/// Respond with an apple -#[poise::command(slash_command, prefix_command, subcommands("red", "green"))] -async fn apple(ctx: Context<'_>) -> Result<(), Error> { - ctx.say("🍎").await?; - Ok(()) -} - -/// Respond with a red apple -#[poise::command(slash_command, prefix_command)] -async fn red(ctx: Context<'_>) -> Result<(), Error> { - ctx.say("🍎").await?; - Ok(()) -} - -/// Respond with a green apple -#[poise::command(slash_command, prefix_command)] -async fn green(ctx: Context<'_>) -> Result<(), Error> { - ctx.say("🍏").await?; - Ok(()) -} - -/// Respond with a banana -#[poise::command(slash_command, prefix_command)] -async fn banana(ctx: Context<'_>) -> Result<(), Error> { - ctx.say("🍌").await?; - Ok(()) -} - -/// Respond with an orange -#[poise::command(slash_command, prefix_command)] -async fn orange(ctx: Context<'_>) -> Result<(), Error> { - ctx.say("🍊").await?; - Ok(()) -} - -/// Respond with a watermelon -#[poise::command(slash_command, prefix_command)] -async fn watermelon(ctx: Context<'_>) -> Result<(), Error> { - ctx.say("🍉").await?; - Ok(()) -} - -/// Respond with a grape -#[poise::command(slash_command, prefix_command)] -async fn grape(ctx: Context<'_>) -> Result<(), Error> { - ctx.say("🍇").await?; - Ok(()) -} - -/// Respond with a strawberry -#[poise::command(slash_command, prefix_command)] -async fn strawberry(ctx: Context<'_>) -> Result<(), Error> { - ctx.say("🍓").await?; - Ok(()) -} - -/// Respond with a random vegetable -#[poise::command(slash_command, prefix_command, category = "Vegan")] -async fn vegetable(ctx: Context<'_>) -> Result<(), Error> { - let response = VEGETABLES[rand::thread_rng().gen_range(0..VEGETABLES.len())]; - ctx.say(response).await?; - Ok(()) -} - -/// Respond with a random meat -#[poise::command(slash_command, prefix_command, category = "Other")] -async fn meat(ctx: Context<'_>) -> Result<(), Error> { - let response = MEAT[rand::thread_rng().gen_range(0..MEAT.len())]; - ctx.say(response).await?; - Ok(()) -} - -/// Respond with a random dairy product -#[poise::command(slash_command, prefix_command, category = "Other")] -async fn dairy(ctx: Context<'_>) -> Result<(), Error> { - let response = DAIRY[rand::thread_rng().gen_range(0..DAIRY.len())]; - ctx.say(response).await?; - Ok(()) -} - -/// Give a user some random food -#[poise::command(context_menu_command = "Give food")] -async fn context_food( - ctx: Context<'_>, - #[description = "User to give food to"] user: serenity::User, -) -> Result<(), Error> { - let response = format!( - "<@{}>: {}", - user.id, - FOOD[rand::thread_rng().gen_range(0..FOOD.len())] - ); - - ctx.say(response).await?; - Ok(()) -} - -/// Give a user some random fruit -#[poise::command( - slash_command, - context_menu_command = "Give fruit", - category = "Context menu but also slash/prefix" -)] -async fn context_fruit( - ctx: Context<'_>, - #[description = "User to give fruit to"] user: serenity::User, -) -> Result<(), Error> { - let response = format!( - "<@{}>: {}", - user.id, - FRUIT[rand::thread_rng().gen_range(0..FRUIT.len())] - ); - - ctx.say(response).await?; - Ok(()) -} - -/// Give a user some random vegetable -#[poise::command( - prefix_command, - context_menu_command = "Give vegetable", - category = "Context menu but also slash/prefix" -)] -async fn context_vegetable( - ctx: Context<'_>, - #[description = "User to give vegetable to"] user: serenity::User, -) -> Result<(), Error> { - let response = format!( - "<@{}>: {}", - user.id, - VEGETABLES[rand::thread_rng().gen_range(0..VEGETABLES.len())] - ); - - ctx.say(response).await?; - Ok(()) -} - -/// Give a user some random meat -#[poise::command( - prefix_command, - slash_command, - context_menu_command = "Give meat", - category = "Context menu but also slash/prefix" -)] -async fn context_meat( - ctx: Context<'_>, - #[description = "User to give meat to"] user: serenity::User, -) -> Result<(), Error> { - let response = format!( - "<@{}>: {}", - user.id, - MEAT[rand::thread_rng().gen_range(0..MEAT.len())] - ); - - ctx.say(response).await?; - Ok(()) -} - -/// React to a message with random food -// This command intentionally doesn't have a slash/prefix command, and its own -// category, so that we can test whether the category shows up in the help -// message. It shouldn't. -#[poise::command( - context_menu_command = "React with food", - ephemeral, - category = "No slash/prefix", - subcommands("fruit_react", "vegetable_react") -)] -async fn food_react( - ctx: Context<'_>, - #[description = "Message to react to (enter a link or ID)"] msg: serenity::Message, -) -> Result<(), Error> { - let reaction = FOOD[rand::thread_rng().gen_range(0..FOOD.len())].to_string(); - msg.react(ctx, serenity::ReactionType::Unicode(reaction)) - .await?; - ctx.say("Reacted!").await?; - Ok(()) -} - -// These next two commands are subcommands of `food_react`, so they're not -// visible in the overview help command. But they should still show up in -// `?help react with food` - -/// React to a message with a random fruit -#[poise::command( - slash_command, - context_menu_command = "React with fruit", - ephemeral, - category = "No slash/prefix" -)] -async fn fruit_react( - ctx: Context<'_>, - #[description = "Message to react to (enter a link or ID)"] msg: serenity::Message, -) -> Result<(), Error> { - let reaction = FRUIT[rand::thread_rng().gen_range(0..FRUIT.len())].to_string(); - msg.react(ctx, serenity::ReactionType::Unicode(reaction)) - .await?; - ctx.say("Reacted!").await?; - Ok(()) -} - -/// React to a message with a random vegetable -#[poise::command( - slash_command, - context_menu_command = "React with vegetable", - ephemeral, - category = "No slash/prefix" -)] -async fn vegetable_react( - ctx: Context<'_>, - #[description = "Message to react to (enter a link or ID)"] msg: serenity::Message, -) -> Result<(), Error> { - let reaction = VEGETABLES[rand::thread_rng().gen_range(0..VEGETABLES.len())].to_string(); - msg.react(ctx, serenity::ReactionType::Unicode(reaction)) - .await?; - ctx.say("Reacted!").await?; - Ok(()) -} - -/// Show help message -#[poise::command(prefix_command, track_edits, category = "Utility")] -async fn help( - ctx: Context<'_>, - #[description = "Command to get help for"] - #[rest] - mut command: Option, -) -> Result<(), Error> { - // This makes it possible to just make `help` a subcommand of any command - // `/fruit help` turns into `/help fruit` - // `/fruit help apple` turns into `/help fruit apple` - if ctx.invoked_command_name() != "help" { - command = match command { - Some(c) => Some(format!("{} {}", ctx.invoked_command_name(), c)), - None => Some(ctx.invoked_command_name().to_string()), - }; - } - let extra_text_at_bottom = "\ -Type `?help command` for more info on a command. -You can edit your `?help` message to the bot and the bot will edit its response."; - - let config = HelpConfiguration { - show_subcommands: true, - show_context_menu_commands: true, - ephemeral: true, - extra_text_at_bottom, - - ..Default::default() - }; - poise::builtins::help(ctx, command.as_deref(), config).await?; - Ok(()) -} - -#[tokio::main] -async fn main() { - let token = std::env::var("DISCORD_TOKEN").expect("missing DISCORD_TOKEN"); - let intents = - serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT; - - let framework = poise::Framework::builder() - .options(poise::FrameworkOptions { - commands: vec![ - fruit(), - vegetable(), - beer(), - meat(), - dairy(), - help(), - context_food(), - context_fruit(), - context_vegetable(), - context_meat(), - food_react(), - ], - prefix_options: poise::PrefixFrameworkOptions { - prefix: Some("?".into()), - ..Default::default() - }, - ..Default::default() - }) - .setup(|ctx, _ready, framework| { - Box::pin(async move { - poise::builtins::register_globally(ctx, &framework.options().commands).await?; - Ok(Data {}) - }) - }) - .build(); - - let client = serenity::ClientBuilder::new(token, intents) - .framework(framework) - .await; - - client.unwrap().start().await.unwrap(); -} diff --git a/macros/src/command/mod.rs b/macros/src/command/mod.rs index 26ebdd90d87d..9369ff39a253 100644 --- a/macros/src/command/mod.rs +++ b/macros/src/command/mod.rs @@ -26,7 +26,6 @@ pub struct CommandArgs { track_deletion: bool, track_edits: bool, broadcast_typing: bool, - help_text_fn: Option, #[darling(multiple)] check: Vec, on_error: Option, @@ -318,12 +317,9 @@ fn generate_command(mut inv: Invocation) -> Result quote::quote! { Some(#help_text_fn()) }, - None => match &inv.help_text { - Some(extracted_explanation) => quote::quote! { Some(#extracted_explanation.into()) }, - None => quote::quote! { None }, - }, + let help_text = match &inv.help_text { + Some(extracted_explanation) => quote::quote! { Some(#extracted_explanation.into()) }, + None => quote::quote! { None }, }; let checks = &inv.args.check; diff --git a/macros/src/lib.rs b/macros/src/lib.rs index e3585769e195..76158deb785a 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -58,8 +58,6 @@ for example for command-specific help (i.e. `~help command_name`). Escape newlin ## Help-related arguments - `hide_in_help`: Hide this command in help menus -- `help_text_fn`: Path to a string-returning function which is used for command help text instead of documentation comments - - Useful if you have many commands with very similar help messages: you can abstract the common parts into a function ## Edit tracking (prefix only) diff --git a/src/builtins/help.rs b/src/builtins/help.rs deleted file mode 100644 index b247533e2dac..000000000000 --- a/src/builtins/help.rs +++ /dev/null @@ -1,429 +0,0 @@ -//! Contains the built-in help command and surrounding infrastructure - -use crate::{serenity_prelude as serenity, CreateReply}; -use std::{borrow::Cow, fmt::Write as _}; - -/// Optional configuration for how the help message from [`help()`] looks -pub struct HelpConfiguration<'a> { - /// Extra text displayed at the bottom of your message. Can be used for help and tips specific - /// to your bot - pub extra_text_at_bottom: &'a str, - /// Whether to make the response ephemeral if possible. Can be nice to reduce clutter - pub ephemeral: bool, - /// Whether to list context menu commands as well - pub show_context_menu_commands: bool, - /// Whether to list context menu commands as well - pub show_subcommands: bool, - /// Whether to include [`crate::Command::description`] (above [`crate::Command::help_text`]). - pub include_description: bool, - #[doc(hidden)] - pub __non_exhaustive: (), -} - -impl Default for HelpConfiguration<'_> { - fn default() -> Self { - Self { - extra_text_at_bottom: "", - ephemeral: true, - show_context_menu_commands: false, - show_subcommands: false, - include_description: true, - __non_exhaustive: (), - } - } -} - -/// Convenience function to align descriptions behind commands -struct TwoColumnList(Vec<(String, Option)>); - -impl TwoColumnList { - /// Creates a new [`TwoColumnList`] - fn new() -> Self { - Self(Vec::new()) - } - - /// Add a line that needs the padding between the columns - fn push_two_colums(&mut self, command: String, description: String) { - self.0.push((command, Some(description))); - } - - /// Add a line that doesn't influence the first columns's width - fn push_heading(&mut self, category: &str) { - if !self.0.is_empty() { - self.0.push(("".to_string(), None)); - } - let mut category = category.to_string(); - category += ":"; - self.0.push((category, None)); - } - - /// Convert the list into a string with aligned descriptions - fn into_string(self) -> String { - let longest_command = self - .0 - .iter() - .filter_map(|(command, description)| { - if description.is_some() { - Some(command.len()) - } else { - None - } - }) - .max() - .unwrap_or(0); - let mut text = String::new(); - for (command, description) in self.0 { - if let Some(description) = description { - let padding = " ".repeat(longest_command - command.len() + 3); - writeln!(text, "{}{}{}", command, padding, description).unwrap(); - } else { - writeln!(text, "{}", command).unwrap(); - } - } - text - } -} - -/// Get the prefix from options -pub(super) async fn get_prefix_from_options( - ctx: crate::Context<'_, U, E>, -) -> Option> { - let options = &ctx.framework().options().prefix_options; - match &options.prefix { - Some(fixed_prefix) => Some(fixed_prefix.clone()), - None => match options.dynamic_prefix { - Some(dynamic_prefix_callback) => { - match dynamic_prefix_callback(crate::PartialContext::from(ctx)).await { - Ok(Some(dynamic_prefix)) => Some(dynamic_prefix), - _ => None, - } - } - None => None, - }, - } -} - -/// Format context menu command name -fn format_context_menu_name(command: &crate::Command) -> Option { - let kind = match command.context_menu_action { - Some(crate::ContextMenuCommandAction::User(_)) => "user", - Some(crate::ContextMenuCommandAction::Message(_)) => "message", - Some(crate::ContextMenuCommandAction::__NonExhaustive) => unreachable!(), - None => return None, - }; - Some(format!( - "{} (on {})", - command - .context_menu_name - .as_deref() - .unwrap_or(&command.name), - kind - )) -} - -/// Code for printing help of a specific command (e.g. `~help my_command`) -async fn help_single_command( - ctx: crate::Context<'_, U, E>, - command_name: &str, - config: HelpConfiguration<'_>, -) -> Result<(), serenity::Error> { - let commands = &ctx.framework().options().commands; - // Try interpret the command name as a context menu command first - let mut command = commands.iter().find(|command| { - if let Some(context_menu_name) = &command.context_menu_name { - if context_menu_name.eq_ignore_ascii_case(command_name) { - return true; - } - } - false - }); - // Then interpret command name as a normal command (possibly nested subcommand) - if command.is_none() { - if let Some((c, _, _)) = crate::find_command(commands, command_name, true, &mut vec![]) { - command = Some(c); - } - } - - let reply = if let Some(command) = command { - let mut invocations = Vec::new(); - let mut subprefix = None; - if command.slash_action.is_some() { - invocations.push(format!("`/{}`", command.name)); - subprefix = Some(format!(" /{}", command.name)); - } - if command.prefix_action.is_some() { - let prefix = match get_prefix_from_options(ctx).await { - Some(prefix) => prefix, - // None can happen if the prefix is dynamic, and the callback - // fails due to help being invoked with slash or context menu - // commands. Not sure there's a better way to handle this. - None => Cow::Borrowed(""), - }; - invocations.push(format!("`{}{}`", prefix, command.name)); - if subprefix.is_none() { - subprefix = Some(format!(" {}{}", prefix, command.name)); - } - } - if command.context_menu_name.is_some() && command.context_menu_action.is_some() { - // Since command.context_menu_action is Some, this unwrap is safe - invocations.push(format_context_menu_name(command).unwrap()); - if subprefix.is_none() { - subprefix = Some(String::from(" ")); - } - } - // At least one of the three if blocks should have triggered - assert!(subprefix.is_some()); - assert!(!invocations.is_empty()); - let invocations = invocations.join("\n"); - - let mut text = match (&command.description, &command.help_text) { - (Some(description), Some(help_text)) => { - if config.include_description { - format!("{}\n\n{}", description, help_text) - } else { - help_text.clone() - } - } - (Some(description), None) => description.to_owned(), - (None, Some(help_text)) => help_text.clone(), - (None, None) => "No help available".to_string(), - }; - if !command.parameters.is_empty() { - text += "\n\n```\nParameters:\n"; - let mut parameterlist = TwoColumnList::new(); - for parameter in &command.parameters { - let name = parameter.name.clone(); - let description = parameter.description.as_deref().unwrap_or(""); - let description = format!( - "({}) {}", - if parameter.required { - "required" - } else { - "optional" - }, - description, - ); - parameterlist.push_two_colums(name, description); - } - text += ¶meterlist.into_string(); - text += "```"; - } - if !command.subcommands.is_empty() { - text += "\n\n```\nSubcommands:\n"; - let mut commandlist = TwoColumnList::new(); - // Subcommands can exist on context menu commands, but there's no - // hierarchy in the menu, so just display them as a list without - // subprefix. - preformat_subcommands( - &mut commandlist, - command, - &subprefix.unwrap_or_else(|| String::from(" ")), - ); - text += &commandlist.into_string(); - text += "```"; - } - format!("**{}**\n\n{}", invocations, text) - } else { - format!("No such command `{}`", command_name) - }; - - let reply = CreateReply::default() - .content(reply) - .ephemeral(config.ephemeral); - - ctx.send(reply).await?; - Ok(()) -} - -/// Recursively formats all subcommands -fn preformat_subcommands( - commands: &mut TwoColumnList, - command: &crate::Command, - prefix: &str, -) { - let as_context_command = command.slash_action.is_none() && command.prefix_action.is_none(); - for subcommand in &command.subcommands { - let command = if as_context_command { - let name = format_context_menu_name(subcommand); - if name.is_none() { - continue; - }; - name.unwrap() - } else { - format!("{} {}", prefix, subcommand.name) - }; - let description = subcommand.description.as_deref().unwrap_or("").to_string(); - commands.push_two_colums(command, description); - // We could recurse here, but things can get cluttered quickly. - // Instead, we show (using this function) subsubcommands when - // the user asks for help on the subcommand. - } -} - -/// Preformat lines (except for padding,) like `(" /ping", "Emits a ping message")` -fn preformat_command( - commands: &mut TwoColumnList, - config: &HelpConfiguration<'_>, - command: &crate::Command, - indent: &str, - options_prefix: Option<&str>, -) { - let prefix = if command.slash_action.is_some() { - String::from("/") - } else if command.prefix_action.is_some() { - options_prefix.map(String::from).unwrap_or_default() - } else { - // This is not a prefix or slash command, i.e. probably a context menu only command - // This should have been filtered out in `generate_all_commands` - unreachable!(); - }; - - let prefix = format!("{}{}{}", indent, prefix, command.name); - commands.push_two_colums( - prefix.clone(), - command.description.as_deref().unwrap_or("").to_string(), - ); - if config.show_subcommands { - preformat_subcommands(commands, command, &prefix) - } -} - -/// Create help text for `help_all_commands` -/// -/// This is a separate function so we can have tests for it -async fn generate_all_commands( - ctx: crate::Context<'_, U, E>, - config: &HelpConfiguration<'_>, -) -> Result { - let mut categories = indexmap::IndexMap::, Vec<&crate::Command>>::new(); - for cmd in &ctx.framework().options().commands { - categories - .entry(cmd.category.as_deref()) - .or_default() - .push(cmd); - } - - let options_prefix = get_prefix_from_options(ctx).await; - - let mut menu = String::from("```\n"); - - let mut commandlist = TwoColumnList::new(); - for (category_name, commands) in categories { - let commands = commands - .into_iter() - .filter(|cmd| { - !cmd.hide_in_help && (cmd.prefix_action.is_some() || cmd.slash_action.is_some()) - }) - .collect::>(); - if commands.is_empty() { - continue; - } - commandlist.push_heading(category_name.unwrap_or("Commands")); - for command in commands { - preformat_command( - &mut commandlist, - config, - command, - " ", - options_prefix.as_deref(), - ); - } - } - menu += &commandlist.into_string(); - - if config.show_context_menu_commands { - menu += "\nContext menu commands:\n"; - - for command in &ctx.framework().options().commands { - let name = format_context_menu_name(command); - if name.is_none() { - continue; - }; - let _ = writeln!(menu, " {}", name.unwrap()); - } - } - - menu += "\n"; - menu += config.extra_text_at_bottom; - menu += "\n```"; - - Ok(menu) -} - -/// Code for printing an overview of all commands (e.g. `~help`) -async fn help_all_commands( - ctx: crate::Context<'_, U, E>, - config: HelpConfiguration<'_>, -) -> Result<(), serenity::Error> { - let menu = generate_all_commands(ctx, &config).await?; - let reply = CreateReply::default() - .content(menu) - .ephemeral(config.ephemeral); - - ctx.send(reply).await?; - Ok(()) -} - -/// A help command that outputs text in a code block, groups commands by categories, and annotates -/// commands with a slash if they exist as slash commands. -/// -/// Example usage from Ferris, the Discord bot running in the Rust community server: -/// ```rust -/// # type Error = Box; -/// # type Context<'a> = poise::Context<'a, (), Error>; -/// /// Show this menu -/// #[poise::command(prefix_command, track_edits, slash_command)] -/// pub async fn help( -/// ctx: Context<'_>, -/// #[description = "Specific command to show help about"] command: Option, -/// ) -> Result<(), Error> { -/// let config = poise::builtins::HelpConfiguration { -/// extra_text_at_bottom: "\ -/// Type ?help command for more info on a command. -/// You can edit your message to the bot and the bot will edit its response.", -/// ..Default::default() -/// }; -/// poise::builtins::help(ctx, command.as_deref(), config).await?; -/// Ok(()) -/// } -/// ``` -/// Output: -/// ```text -/// Playground: -/// ?play Compile and run Rust code in a playground -/// ?eval Evaluate a single Rust expression -/// ?miri Run code and detect undefined behavior using Miri -/// ?expand Expand macros to their raw desugared form -/// ?clippy Catch common mistakes using the Clippy linter -/// ?fmt Format code using rustfmt -/// ?microbench Benchmark small snippets of code -/// ?procmacro Compile and use a procedural macro -/// ?godbolt View assembly using Godbolt -/// ?mca Run performance analysis using llvm-mca -/// ?llvmir View LLVM IR using Godbolt -/// Crates: -/// /crate Lookup crates on crates.io -/// /doc Lookup documentation -/// Moderation: -/// /cleanup Deletes the bot's messages for cleanup -/// /ban Bans another person -/// ?move Move a discussion to another channel -/// /rustify Adds the Rustacean role to members -/// Miscellaneous: -/// ?go Evaluates Go code -/// /source Links to the bot GitHub repo -/// /help Show this menu -/// -/// Type ?help command for more info on a command. -/// You can edit your message to the bot and the bot will edit its response. -/// ``` -pub async fn help( - ctx: crate::Context<'_, U, E>, - command: Option<&str>, - config: HelpConfiguration<'_>, -) -> Result<(), serenity::Error> { - match command { - Some(command) => help_single_command(ctx, command, config).await, - None => help_all_commands(ctx, config).await, - } -} diff --git a/src/builtins/mod.rs b/src/builtins/mod.rs index 4538c0c95c88..88b66dc44524 100644 --- a/src/builtins/mod.rs +++ b/src/builtins/mod.rs @@ -1,14 +1,8 @@ -//! Building blocks for common commands like help commands or application command registration +//! Building blocks for common commands //! -//! This file provides sample commands and utility functions like help menus or error handlers to +//! This file provides sample commands and utility functions like pagination or error handlers to //! use as a starting point for the framework. -mod help; -pub use help::*; - -mod pretty_help; -pub use pretty_help::*; - mod register; pub use register::*; diff --git a/src/builtins/pretty_help.rs b/src/builtins/pretty_help.rs deleted file mode 100644 index 2125f9c76ee0..000000000000 --- a/src/builtins/pretty_help.rs +++ /dev/null @@ -1,277 +0,0 @@ -//! Contains a built-in help command and surrounding infrastructure that uses embeds. - -use crate::{serenity_prelude as serenity, CreateReply}; -use std::{borrow::Cow, fmt::Write as _}; - -/// Optional configuration for how the help message from [`pretty_help()`] looks -pub struct PrettyHelpConfiguration<'a> { - /// Extra text displayed at the bottom of your message. Can be used for help and tips specific - /// to your bot - pub extra_text_at_bottom: &'a str, - /// Whether to make the response ephemeral if possible. Can be nice to reduce clutter - pub ephemeral: bool, - /// Whether to list context menu commands as well - pub show_context_menu_commands: bool, - /// Whether to list context menu commands as well - pub show_subcommands: bool, - /// Whether to include [`crate::Command::description`] (above [`crate::Command::help_text`]). - pub include_description: bool, - /// Color of the Embed - pub color: (u8, u8, u8), - #[doc(hidden)] - pub __non_exhaustive: (), -} - -impl Default for PrettyHelpConfiguration<'_> { - fn default() -> Self { - Self { - extra_text_at_bottom: "", - ephemeral: true, - show_context_menu_commands: false, - show_subcommands: false, - include_description: true, - color: (0, 110, 51), - __non_exhaustive: (), - } - } -} - -/// A help command that works similarly to `builtin::help` but outputs text in an embed. -/// -pub async fn pretty_help( - ctx: crate::Context<'_, U, E>, - command: Option<&str>, - config: PrettyHelpConfiguration<'_>, -) -> Result<(), serenity::Error> { - match command { - Some(command) => pretty_help_single_command(ctx, command, config).await, - None => pretty_help_all_commands(ctx, config).await, - } -} - -/// Printing an overview of all commands (e.g. `~help`) -async fn pretty_help_all_commands( - ctx: crate::Context<'_, U, E>, - config: PrettyHelpConfiguration<'_>, -) -> Result<(), serenity::Error> { - let commands = ctx.framework().options().commands.iter().filter(|cmd| { - !cmd.hide_in_help - && (cmd.prefix_action.is_some() - || cmd.slash_action.is_some() - || (cmd.context_menu_action.is_some() && config.show_context_menu_commands)) - }); - - let mut categories = indexmap::IndexMap::, Vec<&crate::Command>>::new(); - for cmd in commands { - categories - .entry(cmd.category.as_deref()) - .or_default() - .push(cmd); - } - - let options_prefix = super::help::get_prefix_from_options(ctx).await; - - let fields = categories - .into_iter() - .filter(|(_, cmds)| !cmds.is_empty()) - .map(|(category, mut cmds)| { - // get context menu items at the bottom - cmds.sort_by_key(|cmd| cmd.slash_action.is_none() && cmd.prefix_action.is_none()); - - let mut buffer = String::new(); - - for cmd in cmds { - let name = cmd.context_menu_name.as_deref().unwrap_or(&cmd.name); - let prefix = format_cmd_prefix(cmd, options_prefix.as_deref()); - - if let Some(description) = cmd.description.as_deref() { - writeln!(buffer, "{}{}`: *{}*", prefix, name, description).ok(); - } else { - writeln!(buffer, "{}{}`.", prefix, name).ok(); - } - - if config.show_subcommands { - for sbcmd in &cmd.subcommands { - let name = sbcmd.context_menu_name.as_deref().unwrap_or(&sbcmd.name); - let prefix = format_cmd_prefix(sbcmd, options_prefix.as_deref()); - - if let Some(description) = sbcmd.description.as_deref() { - writeln!(buffer, "> {}{}`: *{}*", prefix, name, description).ok(); - } else { - writeln!(buffer, "> {}{}`.", prefix, name).ok(); - } - } - } - } - if let Some((i, _)) = buffer.char_indices().nth(1024) { - buffer.truncate(i); - } - (category.unwrap_or_default(), buffer, false) - }) - .collect::>(); - - let embed = serenity::CreateEmbed::new() - .title("Help") - .fields(fields) - .color(config.color) - .footer(serenity::CreateEmbedFooter::new( - config.extra_text_at_bottom, - )); - - let reply = crate::CreateReply::default() - .embed(embed) - .ephemeral(config.ephemeral); - - ctx.send(reply).await?; - - Ok(()) -} - -/// Figures out which prefix a command should have -fn format_cmd_prefix(cmd: &crate::Command, options_prefix: Option<&str>) -> String { - if cmd.slash_action.is_some() { - "`/".into() - } else if cmd.prefix_action.is_some() { - format!("`{}", options_prefix.unwrap_or_default()) - } else if cmd.context_menu_action.is_some() { - match cmd.context_menu_action { - Some(crate::ContextMenuCommandAction::Message(_)) => "Message menu: `".into(), - Some(crate::ContextMenuCommandAction::User(_)) => "User menu: `".into(), - Some(crate::ContextMenuCommandAction::__NonExhaustive) | None => { - unreachable!() - } - } - } else { - "`".into() - } -} - -/// Code for printing help of a specific command (e.g. `~help my_command`) -async fn pretty_help_single_command( - ctx: crate::Context<'_, U, E>, - command_name: &str, - config: PrettyHelpConfiguration<'_>, -) -> Result<(), serenity::Error> { - let commands = &ctx.framework().options().commands; - - // Try interpret the command name as a context menu command first - let command = commands - .iter() - .find(|cmd| { - cmd.context_menu_name - .as_ref() - .is_some_and(|n| n.eq_ignore_ascii_case(command_name)) - }) - // Then interpret command name as a normal command (possibly nested subcommand) - .or(crate::find_command(commands, command_name, true, &mut vec![]).map(|(c, _, _)| c)); - - let Some(command) = command else { - ctx.send( - CreateReply::default() - .content(format!("No such command `{}`", command_name)) - .ephemeral(config.ephemeral), - ) - .await?; - - return Ok(()); - }; - - let mut invocations = Vec::new(); - let mut subprefix = None; - - if command.slash_action.is_some() { - invocations.push(format!("`/{}`", command.name)); - subprefix = Some(format!("> `/{}`", command.name)); - } - if command.prefix_action.is_some() { - let prefix = super::help::get_prefix_from_options(ctx) - .await - // This can happen if the prefix is dynamic, and the callback fails - // due to help being invoked with slash or context menu commands. - .unwrap_or(Cow::Borrowed("")); - invocations.push(format!("`{}{}`", prefix, command.name)); - subprefix = subprefix.or(Some(format!("> `{}{}`", prefix, command.name))); - } - if command.context_menu_name.is_some() && command.context_menu_action.is_some() { - let kind = match command.context_menu_action { - Some(crate::ContextMenuCommandAction::User(_)) => "user", - Some(crate::ContextMenuCommandAction::Message(_)) => "message", - Some(crate::ContextMenuCommandAction::__NonExhaustive) | None => unreachable!(), - }; - invocations.push(format!( - "`{}` (on {})", - command - .context_menu_name - .as_deref() - .unwrap_or(&command.name), - kind - )); - subprefix = subprefix.or(Some(String::from("> "))); - } - // At least one of the three if blocks should have triggered - assert!(!invocations.is_empty()); - assert!(subprefix.is_some()); - - let invocations = invocations - .into_iter() - .reduce(|x, y| format!("{x}\n{y}")) - .map(|s| ("", s, false)); - - let description = match (&command.description, &command.help_text) { - (Some(description), Some(help_text)) if config.include_description => { - format!("{}\n\n{}", description, help_text) - } - (_, Some(help_text)) => help_text.clone(), - (Some(description), None) => description.clone(), - (None, None) => "No help available".to_string(), - }; - - let parameters = command - .parameters - .iter() - .map(|parameter| { - let req = if parameter.required { - "required" - } else { - "optional" - }; - if let Some(description) = parameter.description.as_deref() { - format!("`{}` ({}) *{} *.", parameter.name, req, description) - } else { - format!("`{}` ({}).", parameter.name, req) - } - }) - .reduce(|x, y| format!("{x}\n{y}")) - .map(|s| ("Parameters", s, false)); - - let sbcmds = command - .subcommands - .iter() - .map(|sbcmd| { - let prefix = format_cmd_prefix(sbcmd, subprefix.as_deref()); // i have no idea about this really - let name = sbcmd.context_menu_name.as_deref().unwrap_or(&sbcmd.name); - if let Some(description) = sbcmd.description.as_deref() { - format!("> {}{}`: *{} *", prefix, name, description) - } else { - format!("> {}{}`", prefix, name,) - } - }) - .reduce(|x, y| format!("{x}\n{y}")) - .map(|s| ("Subcommands", s, false)); - - let fields = invocations - .into_iter() - .chain(parameters.into_iter()) - .chain(sbcmds.into_iter()); - - let embed = serenity::CreateEmbed::default() - .description(description) - .fields(fields); - - let reply = CreateReply::default() - .embed(embed) - .ephemeral(config.ephemeral); - - ctx.send(reply).await?; - Ok(()) -} diff --git a/src/lib.rs b/src/lib.rs index ef3c164d6fa9..032a9ef40ca2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -185,7 +185,6 @@ type Context<'a> = poise::Context<'a, Data, Error>; hide_in_help, required_permissions = "SEND_MESSAGES", aliases("bigounce", "abomination"), - help_text_fn = "my_huge_ass_command_help", check = "check", on_error = "error_handler", )] @@ -201,12 +200,6 @@ async fn my_huge_ass_command( Ok(()) } -fn my_huge_ass_command_help() -> String { - String::from("\ -Example usage: -~my_huge_ass_command 127.0.0.1 @kangalio `i = i + 1` my_flag rest of the message") -} - async fn check(ctx: Context<'_>) -> Result { // We discriminate against users starting with an X Ok(!ctx.author().name.starts_with('X'))