1#[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#[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#[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#[derive(Clone, Debug, Default)]
116pub struct SuggestedCommandName {
117 pub name: String,
118 pub levenshtein_distance: usize,
119}
120
121#[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#[derive(Clone, Debug, Default)]
139pub struct Suggestions(pub Vec<SuggestedCommandName>);
140
141#[cfg(all(feature = "cache", feature = "http"))]
142impl Suggestions {
143 #[inline]
145 #[must_use]
146 pub fn as_vec(&self) -> &Vec<SuggestedCommandName> {
147 &self.0
148 }
149
150 #[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#[derive(Clone, Debug)]
175#[non_exhaustive]
176pub enum CustomisedHelpData<'a> {
177 SuggestedCommands { help_description: String, suggestions: Suggestions },
179 GroupedCommands { help_description: String, groups: Vec<GroupCommandsPair> },
181 SingleCommand { command: Command<'a> },
183 NoCommandFound { help_error_message: &'a str },
185}
186
187#[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#[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#[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#[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 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 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 let Some(sub_command) = sub_command_found {
372 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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 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#[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#[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#[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#[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#[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#[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#[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}