serenity/framework/standard/
help_commands.rs

1//! A collection of default help commands for the framework.
2//!
3//! # Example
4//!
5//! Using the [`with_embeds`] function to have the framework's help message use embeds:
6//!
7//! ```rust,no_run
8//! use std::collections::HashSet;
9//! use std::env;
10//!
11//! use serenity::client::{Client, Context, EventHandler};
12//! use serenity::framework::standard::macros::help;
13//! use serenity::framework::standard::{
14//!     help_commands,
15//!     Args,
16//!     CommandGroup,
17//!     CommandResult,
18//!     HelpOptions,
19//!     StandardFramework,
20//! };
21//! use serenity::model::prelude::{Message, UserId};
22//!
23//! struct Handler;
24//!
25//! impl EventHandler for Handler {}
26//!
27//! #[help]
28//! async fn my_help(
29//!     context: &Context,
30//!     msg: &Message,
31//!     args: Args,
32//!     help_options: &'static HelpOptions,
33//!     groups: &[&'static CommandGroup],
34//!     owners: HashSet<UserId>,
35//! ) -> CommandResult {
36//! #  #[cfg(all(feature = "cache", feature = "http"))]
37//! # {
38//!     let _ = help_commands::with_embeds(context, msg, args, help_options, groups, owners).await;
39//!     Ok(())
40//! # }
41//! #
42//! # #[cfg(not(all(feature = "cache", feature = "http")))]
43//! # Ok(())
44//! }
45//!
46//! let framework = StandardFramework::new().help(&MY_HELP);
47//! ```
48//!
49//! The same can be accomplished with no embeds by substituting `with_embeds` with the [`plain`]
50//! function.
51
52#[cfg(all(feature = "cache", feature = "http"))]
53use std::{collections::HashSet, fmt::Write};
54
55#[cfg(all(feature = "cache", feature = "http"))]
56use futures::future::{BoxFuture, FutureExt};
57#[cfg(all(feature = "cache", feature = "http"))]
58use levenshtein::levenshtein;
59#[cfg(all(feature = "cache", feature = "http"))]
60use tracing::warn;
61
62#[cfg(all(feature = "cache", feature = "http"))]
63use super::structures::Command as InternalCommand;
64#[cfg(all(feature = "cache", feature = "http"))]
65use super::{
66    has_correct_permissions,
67    has_correct_roles,
68    Args,
69    Check,
70    CommandGroup,
71    CommandOptions,
72    HelpBehaviour,
73    HelpOptions,
74    OnlyIn,
75};
76#[cfg(all(feature = "cache", feature = "http"))]
77use crate::{
78    builder::{CreateEmbed, CreateMessage},
79    cache::Cache,
80    client::Context,
81    framework::standard::CommonOptions,
82    http::CacheHttp,
83    model::channel::Message,
84    model::id::{ChannelId, UserId},
85    model::Colour,
86    Error,
87};
88
89/// Macro to format a command according to a [`HelpBehaviour`] or continue to the next command-name
90/// upon hiding.
91#[cfg(all(feature = "cache", feature = "http"))]
92macro_rules! format_command_name {
93    ($behaviour:expr, $command_name:expr) => {
94        match $behaviour {
95            HelpBehaviour::Strike => format!("~~`{}`~~", $command_name),
96            HelpBehaviour::Nothing => format!("`{}`", $command_name),
97            HelpBehaviour::Hide => continue,
98        }
99    };
100}
101
102/// A single group containing its name and all related commands that are eligible in relation of
103/// help-settings measured to the user.
104#[derive(Clone, Debug, Default)]
105pub struct GroupCommandsPair {
106    pub name: &'static str,
107    pub prefixes: Vec<&'static str>,
108    pub command_names: Vec<String>,
109    pub summary: Option<&'static str>,
110    pub sub_groups: Vec<GroupCommandsPair>,
111}
112
113/// A single suggested command containing its name and Levenshtein distance to the actual user's
114/// searched command name.
115#[derive(Clone, Debug, Default)]
116pub struct SuggestedCommandName {
117    pub name: String,
118    pub levenshtein_distance: usize,
119}
120
121/// A single command containing all related pieces of information.
122#[derive(Clone, Debug)]
123#[non_exhaustive]
124pub struct Command<'a> {
125    pub name: &'static str,
126    pub group_name: &'static str,
127    pub group_prefixes: &'a [&'static str],
128    pub sub_commands: Vec<String>,
129    pub aliases: Vec<&'static str>,
130    pub availability: &'a str,
131    pub description: Option<&'static str>,
132    pub usage: Option<&'static str>,
133    pub usage_sample: Vec<&'static str>,
134    pub checks: Vec<String>,
135}
136
137/// Contains possible suggestions in case a command could not be found but are similar enough.
138#[derive(Clone, Debug, Default)]
139pub struct Suggestions(pub Vec<SuggestedCommandName>);
140
141#[cfg(all(feature = "cache", feature = "http"))]
142impl Suggestions {
143    /// Immutably borrow inner [`Vec`].
144    #[inline]
145    #[must_use]
146    pub fn as_vec(&self) -> &Vec<SuggestedCommandName> {
147        &self.0
148    }
149
150    /// Concats names of suggestions with a given `separator`.
151    #[must_use]
152    pub fn join(&self, separator: &str) -> String {
153        match self.as_vec().as_slice() {
154            [] => String::new(),
155            [one] => one.name.clone(),
156            [first, rest @ ..] => {
157                let size = first.name.len() + rest.iter().map(|e| e.name.len()).sum::<usize>();
158                let sep_size = rest.len() * separator.len();
159
160                let mut joined = String::with_capacity(size + sep_size);
161                joined.push_str(&first.name);
162                for e in rest {
163                    joined.push_str(separator);
164                    joined.push_str(&e.name);
165                }
166                joined
167            },
168        }
169    }
170}
171
172/// Covers possible outcomes of a help-request and yields relevant data in customised textual
173/// representation.
174#[derive(Clone, Debug)]
175#[non_exhaustive]
176pub enum CustomisedHelpData<'a> {
177    /// To display suggested commands.
178    SuggestedCommands { help_description: String, suggestions: Suggestions },
179    /// To display groups and their commands by name.
180    GroupedCommands { help_description: String, groups: Vec<GroupCommandsPair> },
181    /// To display one specific command.
182    SingleCommand { command: Command<'a> },
183    /// To display failure in finding a fitting command.
184    NoCommandFound { help_error_message: &'a str },
185}
186
187/// Checks whether a user is member of required roles and given the required permissions.
188#[cfg(feature = "cache")]
189pub fn has_all_requirements(cache: impl AsRef<Cache>, cmd: &CommandOptions, msg: &Message) -> bool {
190    let cache = cache.as_ref();
191
192    if let Some(guild_id) = msg.guild_id {
193        if let Some(member) = cache.member(guild_id, msg.author.id) {
194            if let Ok(permissions) = member.permissions(cache) {
195                return if cmd.allowed_roles.is_empty() {
196                    permissions.administrator() || has_correct_permissions(cache, &cmd, msg)
197                } else if let Some(roles) = cache.guild_roles(guild_id) {
198                    permissions.administrator()
199                        || (has_correct_roles(&cmd, &roles, &member)
200                            && has_correct_permissions(cache, &cmd, msg))
201                } else {
202                    warn!("Failed to find the guild and its roles.");
203
204                    false
205                };
206            }
207        }
208    }
209
210    cmd.only_in != OnlyIn::Guild
211}
212
213/// Checks if `search_on` starts with `word` and is then cleanly followed by a `" "`.
214#[inline]
215#[cfg(all(feature = "cache", feature = "http"))]
216fn starts_with_whole_word(search_on: &str, word: &str) -> bool {
217    search_on.starts_with(word)
218        && search_on.get(word.len()..=word.len()).is_some_and(|slice| slice == " ")
219}
220
221// Decides how a listed help entry shall be displayed.
222#[cfg(all(feature = "cache", feature = "http"))]
223fn check_common_behaviour(
224    cache: impl AsRef<Cache>,
225    msg: &Message,
226    options: &impl CommonOptions,
227    owners: &HashSet<UserId, impl std::hash::BuildHasher + Send + Sync>,
228    help_options: &HelpOptions,
229) -> HelpBehaviour {
230    if !options.help_available() {
231        return HelpBehaviour::Hide;
232    }
233
234    if options.only_in() == OnlyIn::Dm && !msg.is_private()
235        || options.only_in() == OnlyIn::Guild && msg.is_private()
236    {
237        return help_options.wrong_channel;
238    }
239
240    if options.owners_only() && !owners.contains(&msg.author.id) {
241        return help_options.lacking_ownership;
242    }
243
244    if options.owner_privilege() && owners.contains(&msg.author.id) {
245        return HelpBehaviour::Nothing;
246    }
247
248    if !has_correct_permissions(&cache, options, msg) {
249        return help_options.lacking_permissions;
250    }
251
252    msg.guild(cache.as_ref())
253        .and_then(|guild| {
254            if let Some(member) = guild.members.get(&msg.author.id) {
255                if !has_correct_roles(options, &guild.roles, member) {
256                    return Some(help_options.lacking_role);
257                }
258            }
259
260            None
261        })
262        .unwrap_or(HelpBehaviour::Nothing)
263}
264
265#[cfg(all(feature = "cache", feature = "http"))]
266async fn check_command_behaviour(
267    ctx: &Context,
268    msg: &Message,
269    options: &CommandOptions,
270    group_checks: &[&Check],
271    owners: &HashSet<UserId, impl std::hash::BuildHasher + Send + Sync>,
272    help_options: &HelpOptions,
273) -> HelpBehaviour {
274    let behaviour = check_common_behaviour(ctx, msg, &options, owners, help_options);
275
276    if behaviour == HelpBehaviour::Nothing
277        && (!options.owner_privilege || !owners.contains(&msg.author.id))
278    {
279        for check in group_checks.iter().chain(options.checks) {
280            if !check.check_in_help {
281                continue;
282            }
283
284            let mut args = Args::new("", &[]);
285
286            if (check.function)(ctx, msg, &mut args, options).await.is_err() {
287                return help_options.lacking_conditions;
288            }
289        }
290    }
291
292    behaviour
293}
294
295// This function will recursively go through all commands and their sub-commands, trying to find
296// `name`. Similar commands will be collected into `similar_commands`.
297#[cfg(all(feature = "cache", feature = "http"))]
298#[allow(clippy::too_many_arguments)]
299fn nested_commands_search<'rec, 'a: 'rec>(
300    ctx: &'rec Context,
301    msg: &'rec Message,
302    group: &'rec CommandGroup,
303    commands: &'rec [&'static InternalCommand],
304    name: &'rec mut String,
305    help_options: &'a HelpOptions,
306    similar_commands: &'rec mut Vec<SuggestedCommandName>,
307    owners: &'rec HashSet<UserId, impl std::hash::BuildHasher + Send + Sync>,
308) -> BoxFuture<'rec, Option<&'a InternalCommand>> {
309    async move {
310        for command in commands {
311            let mut command = *command;
312
313            let search_command_name_matched = {
314                let mut command_found = None;
315
316                for command_name in command.options.names {
317                    if name == *command_name {
318                        command_found = Some(*command_name);
319
320                        break;
321                    }
322                }
323
324                if command_found.is_some() {
325                    command_found
326                } else {
327                    // Since the command could not be found in the group, we now will identify if
328                    // the command is actually using a sub-command. We iterate all command names
329                    // and check if one matches, if it does, we potentially have a sub-command.
330                    for command_name in command.options.names {
331                        if starts_with_whole_word(name, command_name) {
332                            name.drain(..=command_name.len());
333                            break;
334                        }
335
336                        if help_options.max_levenshtein_distance > 0 {
337                            let levenshtein_distance = levenshtein(command_name, name);
338
339                            if levenshtein_distance <= help_options.max_levenshtein_distance
340                                && HelpBehaviour::Nothing
341                                    == check_command_behaviour(
342                                        ctx,
343                                        msg,
344                                        command.options,
345                                        group.options.checks,
346                                        owners,
347                                        help_options,
348                                    )
349                                    .await
350                            {
351                                similar_commands.push(SuggestedCommandName {
352                                    name: (*command_name).to_string(),
353                                    levenshtein_distance,
354                                });
355                            }
356                        }
357                    }
358
359                    // We check all sub-command names in order to see if one variant has been
360                    // issued to the help-system.
361                    let name_str = name.as_str();
362                    let sub_command_found = command
363                        .options
364                        .sub_commands
365                        .iter()
366                        .find(|n| n.options.names.contains(&name_str))
367                        .copied();
368
369                    // If we found a sub-command, we replace the parent with it. This allows the
370                    // help-system to extract information from the sub-command.
371                    if let Some(sub_command) = sub_command_found {
372                        // Check parent command's behaviour and permission first before we consider
373                        // the sub-command overwrite it.
374                        if HelpBehaviour::Nothing
375                            == check_command_behaviour(
376                                ctx,
377                                msg,
378                                command.options,
379                                group.options.checks,
380                                owners,
381                                help_options,
382                            )
383                            .await
384                        {
385                            command = sub_command;
386                            Some(sub_command.options.names[0])
387                        } else {
388                            break;
389                        }
390                    } else {
391                        match nested_commands_search(
392                            ctx,
393                            msg,
394                            group,
395                            command.options.sub_commands,
396                            name,
397                            help_options,
398                            similar_commands,
399                            owners,
400                        )
401                        .await
402                        {
403                            Some(found) => return Some(found),
404                            None => None,
405                        }
406                    }
407                }
408            };
409
410            if search_command_name_matched.is_some() {
411                if HelpBehaviour::Nothing
412                    == check_command_behaviour(
413                        ctx,
414                        msg,
415                        command.options,
416                        group.options.checks,
417                        owners,
418                        help_options,
419                    )
420                    .await
421                {
422                    return Some(command);
423                }
424                break;
425            }
426        }
427
428        None
429    }
430    .boxed()
431}
432
433// This function will recursively go through all groups and their groups, trying to find `name`.
434// Similar commands will be collected into `similar_commands`.
435#[cfg(all(feature = "cache", feature = "http"))]
436fn nested_group_command_search<'rec, 'a: 'rec>(
437    ctx: &'rec Context,
438    msg: &'rec Message,
439    groups: &'rec [&'static CommandGroup],
440    name: &'rec mut String,
441    help_options: &'a HelpOptions,
442    similar_commands: &'rec mut Vec<SuggestedCommandName>,
443    owners: &'rec HashSet<UserId, impl std::hash::BuildHasher + Send + Sync>,
444) -> BoxFuture<'rec, Result<CustomisedHelpData<'a>, ()>> {
445    async move {
446        for group in groups {
447            let group = *group;
448            let group_behaviour =
449                check_common_behaviour(ctx, msg, &group.options, owners, help_options);
450
451            match &group_behaviour {
452                HelpBehaviour::Nothing => (),
453                _ => {
454                    continue;
455                },
456            }
457
458            if !group.options.prefixes.is_empty()
459                && !group.options.prefixes.iter().any(|prefix| trim_prefixless_group(prefix, name))
460            {
461                continue;
462            }
463
464            let found = nested_commands_search(
465                ctx,
466                msg,
467                group,
468                group.options.commands,
469                name,
470                help_options,
471                similar_commands,
472                owners,
473            )
474            .await;
475
476            if let Some(command) = found {
477                let options = &command.options;
478
479                if !options.help_available {
480                    return Ok(CustomisedHelpData::NoCommandFound {
481                        help_error_message: help_options.no_help_available_text,
482                    });
483                }
484
485                let is_only = |only| group.options.only_in == only || options.only_in == only;
486
487                let available_text = if is_only(OnlyIn::Dm) {
488                    &help_options.dm_only_text
489                } else if is_only(OnlyIn::Guild) {
490                    &help_options.guild_only_text
491                } else {
492                    &help_options.dm_and_guild_text
493                };
494
495                similar_commands
496                    .sort_unstable_by(|a, b| a.levenshtein_distance.cmp(&b.levenshtein_distance));
497
498                let check_names: Vec<String> = command
499                    .options
500                    .checks
501                    .iter()
502                    .chain(group.options.checks.iter())
503                    .filter(|check| check.display_in_help)
504                    .map(|check| check.name.to_string())
505                    .collect();
506
507                let sub_command_names: Vec<String> = options
508                    .sub_commands
509                    .iter()
510                    .filter(|cmd| cmd.options.help_available)
511                    .map(|cmd| cmd.options.names[0].to_string())
512                    .collect();
513
514                return Ok(CustomisedHelpData::SingleCommand {
515                    command: Command {
516                        name: options.names[0],
517                        description: options.desc,
518                        group_name: group.name,
519                        group_prefixes: group.options.prefixes,
520                        checks: check_names,
521                        aliases: options.names[1..].to_vec(),
522                        availability: available_text,
523                        usage: options.usage,
524                        usage_sample: options.examples.to_vec(),
525                        sub_commands: sub_command_names,
526                    },
527                });
528            }
529
530            if let Ok(found) = nested_group_command_search(
531                ctx,
532                msg,
533                group.options.sub_groups,
534                name,
535                help_options,
536                similar_commands,
537                owners,
538            )
539            .await
540            {
541                return Ok(found);
542            }
543        }
544
545        Err(())
546    }
547    .boxed()
548}
549
550/// Tries to extract a single command matching searched command name otherwise returns similar
551/// commands.
552#[cfg(feature = "cache")]
553async fn fetch_single_command<'a>(
554    ctx: &Context,
555    msg: &Message,
556    groups: &[&'static CommandGroup],
557    name: &'a str,
558    help_options: &'a HelpOptions,
559    owners: &HashSet<UserId, impl std::hash::BuildHasher + Send + Sync>,
560) -> Result<CustomisedHelpData<'a>, Vec<SuggestedCommandName>> {
561    let mut similar_commands: Vec<SuggestedCommandName> = Vec::new();
562    let mut name = name.to_string();
563
564    nested_group_command_search(
565        ctx,
566        msg,
567        groups,
568        &mut name,
569        help_options,
570        &mut similar_commands,
571        owners,
572    )
573    .await
574    .map_err(|()| similar_commands)
575}
576
577#[cfg(feature = "cache")]
578#[allow(clippy::too_many_arguments)]
579async fn fill_eligible_commands<'a>(
580    ctx: &Context,
581    msg: &Message,
582    commands: &[&'static InternalCommand],
583    owners: &HashSet<UserId, impl std::hash::BuildHasher + Send + Sync>,
584    help_options: &'a HelpOptions,
585    group: &'a CommandGroup,
586    to_fill: &mut GroupCommandsPair,
587    highest_formatter: &mut HelpBehaviour,
588) {
589    to_fill.name = group.name;
590    to_fill.prefixes = group.options.prefixes.to_vec();
591
592    let group_behaviour = {
593        if let HelpBehaviour::Hide = highest_formatter {
594            HelpBehaviour::Hide
595        } else {
596            std::cmp::max(
597                *highest_formatter,
598                check_common_behaviour(ctx, msg, &group.options, owners, help_options),
599            )
600        }
601    };
602
603    *highest_formatter = group_behaviour;
604
605    for command in commands {
606        let command = *command;
607        let options = &command.options;
608        let name = &options.names[0];
609
610        if group_behaviour != HelpBehaviour::Nothing {
611            let name = format_command_name!(&group_behaviour, &name);
612            to_fill.command_names.push(name);
613
614            continue;
615        }
616
617        let command_behaviour = check_command_behaviour(
618            ctx,
619            msg,
620            command.options,
621            group.options.checks,
622            owners,
623            help_options,
624        )
625        .await;
626
627        let name = format_command_name!(command_behaviour, &name);
628        to_fill.command_names.push(name);
629    }
630}
631
632/// Tries to fetch all commands visible to the user within a group and its sub-groups.
633#[cfg(feature = "cache")]
634#[allow(clippy::too_many_arguments)]
635fn fetch_all_eligible_commands_in_group<'rec, 'a: 'rec>(
636    ctx: &'rec Context,
637    msg: &'rec Message,
638    commands: &'rec [&'static InternalCommand],
639    owners: &'rec HashSet<UserId, impl std::hash::BuildHasher + Send + Sync>,
640    help_options: &'a HelpOptions,
641    group: &'a CommandGroup,
642    highest_formatter: HelpBehaviour,
643) -> BoxFuture<'rec, GroupCommandsPair> {
644    async move {
645        let mut group_with_cmds = GroupCommandsPair::default();
646        let mut highest_formatter = highest_formatter;
647
648        fill_eligible_commands(
649            ctx,
650            msg,
651            commands,
652            owners,
653            help_options,
654            group,
655            &mut group_with_cmds,
656            &mut highest_formatter,
657        )
658        .await;
659
660        for sub_group in group.options.sub_groups {
661            if HelpBehaviour::Hide == highest_formatter {
662                break;
663            } else if sub_group.options.commands.is_empty()
664                && sub_group.options.sub_groups.is_empty()
665            {
666                continue;
667            }
668
669            let grouped_cmd = fetch_all_eligible_commands_in_group(
670                ctx,
671                msg,
672                sub_group.options.commands,
673                owners,
674                help_options,
675                sub_group,
676                highest_formatter,
677            )
678            .await;
679
680            group_with_cmds.sub_groups.push(grouped_cmd);
681        }
682
683        group_with_cmds
684    }
685    .boxed()
686}
687
688/// Fetch groups with their commands.
689#[cfg(feature = "cache")]
690async fn create_command_group_commands_pair_from_groups(
691    ctx: &Context,
692    msg: &Message,
693    groups: &[&'static CommandGroup],
694    owners: &HashSet<UserId, impl std::hash::BuildHasher + Send + Sync>,
695    help_options: &HelpOptions,
696) -> Vec<GroupCommandsPair> {
697    let mut listed_groups: Vec<GroupCommandsPair> = Vec::default();
698
699    for group in groups {
700        let group = *group;
701
702        let group_with_cmds = create_single_group(ctx, msg, group, owners, help_options).await;
703
704        if !group_with_cmds.command_names.is_empty() || !group_with_cmds.sub_groups.is_empty() {
705            listed_groups.push(group_with_cmds);
706        }
707    }
708
709    listed_groups
710}
711
712/// Fetches a single group with its commands.
713#[cfg(feature = "cache")]
714async fn create_single_group(
715    ctx: &Context,
716    msg: &Message,
717    group: &CommandGroup,
718    owners: &HashSet<UserId, impl std::hash::BuildHasher + Send + Sync>,
719    help_options: &HelpOptions,
720) -> GroupCommandsPair {
721    let mut group_with_cmds = fetch_all_eligible_commands_in_group(
722        ctx,
723        msg,
724        group.options.commands,
725        owners,
726        help_options,
727        group,
728        HelpBehaviour::Nothing,
729    )
730    .await;
731
732    group_with_cmds.name = group.name;
733    group_with_cmds.summary = group.options.summary;
734
735    group_with_cmds
736}
737
738/// If `searched_group` is exactly matching `group_name`, this function returns `true` but does not
739/// trim. Otherwise, it is treated as an optionally passed group-name and ends up being removed
740/// from `searched_group`.
741///
742/// If a group has no prefixes, it is not required to be part of `searched_group` to reach a
743/// sub-group of `group_name`.
744#[cfg(feature = "cache")]
745fn trim_prefixless_group(group_name: &str, searched_group: &mut String) -> bool {
746    if group_name == searched_group.as_str() {
747        return true;
748    } else if starts_with_whole_word(searched_group, group_name) {
749        searched_group.drain(..=group_name.len());
750        return true;
751    }
752
753    false
754}
755
756#[cfg(feature = "cache")]
757pub fn searched_lowercase<'rec, 'a: 'rec>(
758    ctx: &'rec Context,
759    msg: &'rec Message,
760    group: &'rec CommandGroup,
761    owners: &'rec HashSet<UserId, impl std::hash::BuildHasher + Send + Sync>,
762    help_options: &'a HelpOptions,
763    searched_named_lowercase: &'rec mut String,
764) -> BoxFuture<'rec, Option<CustomisedHelpData<'a>>> {
765    async move {
766        let is_prefixless_group = {
767            group.options.prefixes.is_empty()
768                && trim_prefixless_group(&group.name.to_lowercase(), searched_named_lowercase)
769        };
770        let mut progressed = is_prefixless_group;
771        let is_word_prefix = group.options.prefixes.iter().any(|prefix| {
772            if starts_with_whole_word(searched_named_lowercase, prefix) {
773                searched_named_lowercase.drain(..=prefix.len());
774                progressed = true;
775            }
776
777            prefix == searched_named_lowercase
778        });
779
780        if is_prefixless_group || is_word_prefix {
781            let single_group = create_single_group(ctx, msg, group, owners, help_options).await;
782
783            if !single_group.command_names.is_empty() {
784                return Some(CustomisedHelpData::GroupedCommands {
785                    help_description: group
786                        .options
787                        .description
788                        .map(ToString::to_string)
789                        .unwrap_or_default(),
790                    groups: vec![single_group],
791                });
792            }
793        } else if progressed || group.options.prefixes.is_empty() {
794            for sub_group in group.options.sub_groups {
795                if let Some(found_set) = searched_lowercase(
796                    ctx,
797                    msg,
798                    sub_group,
799                    owners,
800                    help_options,
801                    searched_named_lowercase,
802                )
803                .await
804                {
805                    return Some(found_set);
806                }
807            }
808        }
809
810        None
811    }
812    .boxed()
813}
814
815/// Iterates over all commands and forges them into a [`CustomisedHelpData`], taking
816/// [`HelpOptions`] into consideration when deciding on whether a command shall be picked and in
817/// what textual format.
818#[cfg(feature = "cache")]
819pub async fn create_customised_help_data<'a>(
820    ctx: &Context,
821    msg: &Message,
822    args: &'a Args,
823    groups: &[&'static CommandGroup],
824    owners: &HashSet<UserId, impl std::hash::BuildHasher + Send + Sync>,
825    help_options: &'a HelpOptions,
826) -> CustomisedHelpData<'a> {
827    if !args.is_empty() {
828        let name = args.message();
829
830        return match fetch_single_command(ctx, msg, groups, name, help_options, owners).await {
831            Ok(single_command) => single_command,
832            Err(suggestions) => {
833                let mut searched_named_lowercase = name.to_lowercase();
834
835                for group in groups {
836                    if let Some(found_command) = searched_lowercase(
837                        ctx,
838                        msg,
839                        group,
840                        owners,
841                        help_options,
842                        &mut searched_named_lowercase,
843                    )
844                    .await
845                    {
846                        return found_command;
847                    }
848                }
849
850                if suggestions.is_empty() {
851                    CustomisedHelpData::NoCommandFound {
852                        help_error_message: help_options.no_help_available_text,
853                    }
854                } else {
855                    CustomisedHelpData::SuggestedCommands {
856                        help_description: help_options.suggestion_text.to_string(),
857                        suggestions: Suggestions(suggestions),
858                    }
859                }
860            },
861        };
862    }
863
864    let strikethrough_command_tip = if msg.is_private() {
865        help_options.strikethrough_commands_tip_in_dm
866    } else {
867        help_options.strikethrough_commands_tip_in_guild
868    };
869
870    let description = if let Some(strikethrough_command_text) = strikethrough_command_tip {
871        format!("{}\n{strikethrough_command_text}", help_options.individual_command_tip)
872    } else {
873        help_options.individual_command_tip.to_string()
874    };
875
876    let listed_groups =
877        create_command_group_commands_pair_from_groups(ctx, msg, groups, owners, help_options)
878            .await;
879
880    if listed_groups.is_empty() {
881        CustomisedHelpData::NoCommandFound {
882            help_error_message: help_options.no_help_available_text,
883        }
884    } else {
885        CustomisedHelpData::GroupedCommands {
886            help_description: description,
887            groups: listed_groups,
888        }
889    }
890}
891
892/// Flattens a group with all its nested sub-groups into the passed `group_text` buffer. If
893/// `nest_level` is `0`, this function will skip the group's name.
894#[cfg(all(feature = "cache", feature = "http"))]
895fn flatten_group_to_string(
896    group_text: &mut String,
897    group: &GroupCommandsPair,
898    nest_level: usize,
899    help_options: &HelpOptions,
900) -> Result<(), Error> {
901    let repeated_indent_str = help_options.indention_prefix.repeat(nest_level);
902
903    if nest_level > 0 {
904        writeln!(group_text, "{repeated_indent_str}__**{}**__", group.name,)?;
905    }
906
907    let mut summary_or_prefixes = false;
908
909    if let Some(group_summary) = group.summary {
910        writeln!(group_text, "{}*{group_summary}*", &repeated_indent_str)?;
911        summary_or_prefixes = true;
912    }
913
914    if !group.prefixes.is_empty() {
915        writeln!(
916            group_text,
917            "{}{}: `{}`",
918            &repeated_indent_str,
919            help_options.group_prefix,
920            group.prefixes.join("`, `"),
921        )?;
922        summary_or_prefixes = true;
923    }
924
925    if summary_or_prefixes {
926        writeln!(group_text)?;
927    }
928
929    let mut joined_commands = group.command_names.join(&format!("\n{repeated_indent_str}"));
930
931    if !group.command_names.is_empty() {
932        joined_commands.insert_str(0, &repeated_indent_str);
933    }
934
935    writeln!(group_text, "{joined_commands}")?;
936
937    for sub_group in &group.sub_groups {
938        if !(sub_group.command_names.is_empty() && sub_group.sub_groups.is_empty()) {
939            let mut sub_group_text = String::default();
940
941            flatten_group_to_string(&mut sub_group_text, sub_group, nest_level + 1, help_options)?;
942
943            write!(group_text, "{sub_group_text}")?;
944        }
945    }
946
947    Ok(())
948}
949
950/// Flattens a group with all its nested sub-groups into the passed `group_text` buffer respecting
951/// the plain help format. If `nest_level` is `0`, this function will skip the group's name.
952#[cfg(all(feature = "cache", feature = "http"))]
953fn flatten_group_to_plain_string(
954    group_text: &mut String,
955    group: &GroupCommandsPair,
956    nest_level: usize,
957    help_options: &HelpOptions,
958) {
959    let repeated_indent_str = help_options.indention_prefix.repeat(nest_level);
960
961    if nest_level > 0 {
962        write!(group_text, "\n{repeated_indent_str}**{}**", group.name).unwrap();
963    }
964
965    if group.prefixes.is_empty() {
966        group_text.push_str(": ");
967    } else {
968        write!(
969            group_text,
970            " ({}: `{}`): ",
971            help_options.group_prefix,
972            group.prefixes.join("`, `"),
973        ).unwrap();
974    }
975
976    let joined_commands = group.command_names.join(", ");
977
978    group_text.push_str(&joined_commands);
979
980    for sub_group in &group.sub_groups {
981        let mut sub_group_text = String::default();
982
983        flatten_group_to_plain_string(&mut sub_group_text, sub_group, nest_level + 1, help_options);
984
985        group_text.push_str(&sub_group_text);
986    }
987}
988
989/// Sends an embed listing all groups with their commands.
990#[cfg(all(feature = "cache", feature = "http"))]
991async fn send_grouped_commands_embed(
992    cache_http: impl CacheHttp,
993    help_options: &HelpOptions,
994    channel_id: ChannelId,
995    help_description: &str,
996    groups: &[GroupCommandsPair],
997    colour: Colour,
998) -> Result<Message, Error> {
999    // creating embed outside message builder since flatten_group_to_string may return an error.
1000
1001    let mut embed = CreateEmbed::new().colour(colour).description(help_description);
1002    for group in groups {
1003        let mut embed_text = String::default();
1004
1005        flatten_group_to_string(&mut embed_text, group, 0, help_options)?;
1006
1007        embed = embed.field(group.name, &embed_text, true);
1008    }
1009
1010    let builder = CreateMessage::new().embed(embed);
1011    channel_id.send_message(cache_http, builder).await
1012}
1013
1014/// Sends embed showcasing information about a single command.
1015#[cfg(all(feature = "cache", feature = "http"))]
1016async fn send_single_command_embed(
1017    cache_http: impl CacheHttp,
1018    help_options: &HelpOptions,
1019    channel_id: ChannelId,
1020    command: &Command<'_>,
1021    colour: Colour,
1022) -> Result<Message, Error> {
1023    let mut embed = CreateEmbed::new().title(command.name).colour(colour);
1024
1025    if let Some(desc) = command.description {
1026        embed = embed.description(desc);
1027    }
1028
1029    if let Some(usage) = command.usage {
1030        let full_usage_text = if let Some(first_prefix) = command.group_prefixes.first() {
1031            format!("`{first_prefix} {} {usage}`", command.name)
1032        } else {
1033            format!("`{} {usage}`", command.name)
1034        };
1035
1036        embed = embed.field(help_options.usage_label, full_usage_text, true);
1037    }
1038
1039    if !command.usage_sample.is_empty() {
1040        let full_example_text = if let Some(first_prefix) = command.group_prefixes.first() {
1041            let format_example = |example| format!("`{first_prefix} {} {example}`\n", command.name);
1042            command.usage_sample.iter().map(format_example).collect::<String>()
1043        } else {
1044            let format_example = |example| format!("`{} {example}`\n", command.name);
1045            command.usage_sample.iter().map(format_example).collect::<String>()
1046        };
1047        embed = embed.field(help_options.usage_sample_label, full_example_text, true);
1048    }
1049
1050    embed = embed.field(help_options.grouped_label, command.group_name, true);
1051
1052    if !command.aliases.is_empty() {
1053        embed = embed.field(
1054            help_options.aliases_label,
1055            format!("`{}`", command.aliases.join("`, `")),
1056            true,
1057        );
1058    }
1059
1060    if !help_options.available_text.is_empty() && !command.availability.is_empty() {
1061        embed = embed.field(help_options.available_text, command.availability, true);
1062    }
1063
1064    if !command.checks.is_empty() {
1065        embed = embed.field(
1066            help_options.checks_label,
1067            format!("`{}`", command.checks.join("`, `")),
1068            true,
1069        );
1070    }
1071
1072    if !command.sub_commands.is_empty() {
1073        embed = embed.field(
1074            help_options.sub_commands_label,
1075            format!("`{}`", command.sub_commands.join("`, `")),
1076            true,
1077        );
1078    }
1079
1080    let builder = CreateMessage::new().embed(embed);
1081    channel_id.send_message(cache_http, builder).await
1082}
1083
1084/// Sends embed listing commands that are similar to the sent one.
1085#[cfg(all(feature = "cache", feature = "http"))]
1086async fn send_suggestion_embed(
1087    cache_http: impl CacheHttp,
1088    channel_id: ChannelId,
1089    help_description: &str,
1090    suggestions: &Suggestions,
1091    colour: Colour,
1092) -> Result<Message, Error> {
1093    let text = help_description.replace("{}", &suggestions.join("`, `"));
1094
1095    let embed = CreateEmbed::new().colour(colour).description(text);
1096    let builder = CreateMessage::new().embed(embed);
1097    channel_id.send_message(cache_http, builder).await
1098}
1099
1100/// Sends an embed explaining fetching commands failed.
1101#[cfg(all(feature = "cache", feature = "http"))]
1102async fn send_error_embed(
1103    cache_http: impl CacheHttp,
1104    channel_id: ChannelId,
1105    input: &str,
1106    colour: Colour,
1107) -> Result<Message, Error> {
1108    let embed = CreateEmbed::new().colour(colour).description(input);
1109    let builder = CreateMessage::new().embed(embed);
1110    channel_id.send_message(cache_http, builder).await
1111}
1112
1113/// Posts an embed showing each individual command group and its commands.
1114///
1115/// # Examples
1116///
1117/// Use the command with [`StandardFramework::help`]:
1118///
1119/// ```rust,no_run
1120/// # use serenity::prelude::*;
1121/// use std::collections::HashSet;
1122/// use std::hash::BuildHasher;
1123///
1124/// use serenity::framework::standard::help_commands::*;
1125/// use serenity::framework::standard::macros::help;
1126/// use serenity::framework::standard::{
1127///     Args,
1128///     CommandGroup,
1129///     CommandResult,
1130///     HelpOptions,
1131///     StandardFramework,
1132/// };
1133/// use serenity::model::prelude::*;
1134///
1135/// #[help]
1136/// async fn my_help(
1137///     context: &Context,
1138///     msg: &Message,
1139///     args: Args,
1140///     help_options: &'static HelpOptions,
1141///     groups: &[&'static CommandGroup],
1142///     owners: HashSet<UserId>,
1143/// ) -> CommandResult {
1144///     let _ = with_embeds(context, msg, args, &help_options, groups, owners).await?;
1145///     Ok(())
1146/// }
1147///
1148/// let framework = StandardFramework::new().help(&MY_HELP);
1149/// ```
1150///
1151/// # Errors
1152///
1153/// Returns the same errors as [`ChannelId::send_message`].
1154///
1155/// [`StandardFramework::help`]: crate::framework::standard::StandardFramework::help
1156#[cfg(all(feature = "cache", feature = "http"))]
1157pub async fn with_embeds(
1158    ctx: &Context,
1159    msg: &Message,
1160    args: Args,
1161    help_options: &HelpOptions,
1162    groups: &[&'static CommandGroup],
1163    owners: HashSet<UserId, impl std::hash::BuildHasher + Send + Sync>,
1164) -> Result<Message, Error> {
1165    let formatted_help =
1166        create_customised_help_data(ctx, msg, &args, groups, &owners, help_options).await;
1167
1168    match formatted_help {
1169        CustomisedHelpData::SuggestedCommands {
1170            ref help_description,
1171            ref suggestions,
1172        } => {
1173            send_suggestion_embed(
1174                &ctx.http,
1175                msg.channel_id,
1176                help_description,
1177                suggestions,
1178                help_options.embed_error_colour,
1179            )
1180            .await
1181        },
1182        CustomisedHelpData::NoCommandFound {
1183            help_error_message,
1184        } => {
1185            send_error_embed(
1186                &ctx.http,
1187                msg.channel_id,
1188                help_error_message,
1189                help_options.embed_error_colour,
1190            )
1191            .await
1192        },
1193        CustomisedHelpData::GroupedCommands {
1194            ref help_description,
1195            ref groups,
1196        } => {
1197            send_grouped_commands_embed(
1198                &ctx.http,
1199                help_options,
1200                msg.channel_id,
1201                help_description,
1202                groups,
1203                help_options.embed_success_colour,
1204            )
1205            .await
1206        },
1207        CustomisedHelpData::SingleCommand {
1208            ref command,
1209        } => {
1210            send_single_command_embed(
1211                &ctx.http,
1212                help_options,
1213                msg.channel_id,
1214                command,
1215                help_options.embed_success_colour,
1216            )
1217            .await
1218        },
1219    }
1220}
1221
1222/// Turns grouped commands into a [`String`] taking plain help format into account.
1223#[cfg(all(feature = "cache", feature = "http"))]
1224fn grouped_commands_to_plain_string(
1225    help_options: &HelpOptions,
1226    help_description: &str,
1227    groups: &[GroupCommandsPair],
1228) -> String {
1229    let mut result = "__**Commands**__\n".to_string();
1230
1231    result.push_str(help_description);
1232    result.push('\n');
1233
1234    for group in groups {
1235        write!(result, "\n**{}**", &group.name).unwrap();
1236
1237        flatten_group_to_plain_string(&mut result, group, 0, help_options);
1238    }
1239
1240    result
1241}
1242
1243/// Turns a single command into a [`String`] taking plain help format into account.
1244#[cfg(all(feature = "cache", feature = "http"))]
1245fn single_command_to_plain_string(help_options: &HelpOptions, command: &Command<'_>) -> String {
1246    let mut result = String::new();
1247
1248    writeln!(result, "__**{}**__", command.name).unwrap();
1249
1250    if !command.aliases.is_empty() {
1251        write!(result, "**{}**: `{}`", help_options.aliases_label, command.aliases.join("`, `"))
1252            .unwrap();
1253    }
1254
1255    if let Some(description) = command.description {
1256        writeln!(result, "**{}**: {description}", help_options.description_label).unwrap();
1257    }
1258
1259    if let Some(usage) = command.usage {
1260        if let Some(first_prefix) = command.group_prefixes.first() {
1261            writeln!(
1262                result,
1263                "**{}**: `{first_prefix} {} {usage}`",
1264                help_options.usage_label, command.name
1265            )
1266            .unwrap();
1267        } else {
1268            writeln!(result, "**{}**: `{} {usage}`", help_options.usage_label, command.name)
1269                .unwrap();
1270        }
1271    }
1272
1273    if !command.usage_sample.is_empty() {
1274        if let Some(first_prefix) = command.group_prefixes.first() {
1275            let format_example = |example| {
1276                writeln!(
1277                    result,
1278                    "**{}**: `{first_prefix} {} {example}`",
1279                    help_options.usage_sample_label, command.name
1280                )
1281                .unwrap();
1282            };
1283            command.usage_sample.iter().for_each(format_example);
1284        } else {
1285            let format_example = |example| {
1286                writeln!(
1287                    result,
1288                    "**{}**: `{} {example}`",
1289                    help_options.usage_sample_label, command.name
1290                )
1291                .unwrap();
1292            };
1293            command.usage_sample.iter().for_each(format_example);
1294        }
1295    }
1296
1297    writeln!(result, "**{}**: {}", help_options.grouped_label, command.group_name).unwrap();
1298
1299    if !help_options.available_text.is_empty() && !command.availability.is_empty() {
1300        writeln!(result, "**{}**: {}", help_options.available_text, command.availability).unwrap();
1301    }
1302
1303    if !command.sub_commands.is_empty() {
1304        writeln!(
1305            result,
1306            "**{}**: `{}`",
1307            help_options.sub_commands_label,
1308            command.sub_commands.join("`, `"),
1309        )
1310        .unwrap();
1311    }
1312
1313    result
1314}
1315
1316/// Posts formatted text displaying each individual command group and its commands.
1317///
1318/// # Examples
1319///
1320/// Use the command with `exec_help`:
1321///
1322/// ```rust,no_run
1323/// # use serenity::prelude::*;
1324/// use std::collections::HashSet;
1325/// use std::hash::BuildHasher;
1326///
1327/// use serenity::framework::standard::help_commands::*;
1328/// use serenity::framework::standard::macros::help;
1329/// use serenity::framework::standard::{
1330///     Args,
1331///     CommandGroup,
1332///     CommandResult,
1333///     HelpOptions,
1334///     StandardFramework,
1335/// };
1336/// use serenity::model::prelude::*;
1337///
1338/// #[help]
1339/// async fn my_help(
1340///     context: &Context,
1341///     msg: &Message,
1342///     args: Args,
1343///     help_options: &'static HelpOptions,
1344///     groups: &[&'static CommandGroup],
1345///     owners: HashSet<UserId>,
1346/// ) -> CommandResult {
1347///     let _ = plain(context, msg, args, &help_options, groups, owners).await?;
1348///     Ok(())
1349/// }
1350///
1351/// let framework = StandardFramework::new().help(&MY_HELP);
1352/// ```
1353/// # Errors
1354///
1355/// Returns the same errors as [`ChannelId::send_message`].
1356#[cfg(all(feature = "cache", feature = "http"))]
1357pub async fn plain(
1358    ctx: &Context,
1359    msg: &Message,
1360    args: Args,
1361    help_options: &HelpOptions,
1362    groups: &[&'static CommandGroup],
1363    owners: HashSet<UserId, impl std::hash::BuildHasher + Send + Sync>,
1364) -> Result<Message, Error> {
1365    let formatted_help =
1366        create_customised_help_data(ctx, msg, &args, groups, &owners, help_options).await;
1367
1368    let result = match formatted_help {
1369        CustomisedHelpData::SuggestedCommands {
1370            ref help_description,
1371            ref suggestions,
1372        } => help_description.replace("{}", &suggestions.join("`, `")),
1373        CustomisedHelpData::NoCommandFound {
1374            help_error_message,
1375        } => help_error_message.to_string(),
1376        CustomisedHelpData::GroupedCommands {
1377            ref help_description,
1378            ref groups,
1379        } => grouped_commands_to_plain_string(help_options, help_description, groups),
1380        CustomisedHelpData::SingleCommand {
1381            ref command,
1382        } => single_command_to_plain_string(help_options, command),
1383    };
1384
1385    msg.channel_id.say(&ctx, result).await
1386}
1387
1388#[cfg(test)]
1389#[cfg(all(feature = "cache", feature = "http"))]
1390mod tests {
1391    use super::{SuggestedCommandName, Suggestions};
1392
1393    #[test]
1394    fn suggestions_join() {
1395        let names = vec![
1396            SuggestedCommandName {
1397                name: "aa".to_owned(),
1398                levenshtein_distance: 0,
1399            },
1400            SuggestedCommandName {
1401                name: "bbb".to_owned(),
1402                levenshtein_distance: 0,
1403            },
1404            SuggestedCommandName {
1405                name: "cccc".to_owned(),
1406                levenshtein_distance: 0,
1407            },
1408        ];
1409
1410        let actual = Suggestions(names).join(", ");
1411
1412        assert_eq!(actual, "aa, bbb, cccc");
1413        assert_eq!(actual.capacity(), 13);
1414    }
1415}