mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-06 17:32:31 -05:00
Refactor fuzzy help and clean up help command (#2122)
What's changed: - Fixed issues mentioned on #2031 - Fuzzy help displays more like help manual - Fuzzy help is easier and more flexible to use - Fuzzy help string-matching ratio lowered to 80 - Help formatter is more extendable - Help command has been optimized, cleaned up and better incorporates fuzzy help - Added async_filter and async_enumerate utility functions because I was using them for this PR, then no longer needed them, but decided they might be useful anyway. - Added `Context.me` property which is a shortcut to `Context.guild.me` or `Context.bot.user`, depending on the channel type.
This commit is contained in:
@@ -20,25 +20,24 @@ message to help page.
|
||||
e.g. format_help_for(ctx, ctx.command, "Missing required arguments")
|
||||
|
||||
discord.py 1.0.0a
|
||||
Experimental: compatibility with 0.16.8
|
||||
|
||||
Copyrights to logic of code belong to Rapptz (Danny)
|
||||
Everything else credit to SirThane#1780"""
|
||||
This help formatter contains work by Rapptz (Danny) and SirThane#1780.
|
||||
"""
|
||||
from collections import namedtuple
|
||||
from typing import List
|
||||
from typing import List, Optional, Union
|
||||
|
||||
import discord
|
||||
from discord.ext.commands import formatter
|
||||
from discord.ext.commands import formatter as dpy_formatter
|
||||
import inspect
|
||||
import itertools
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from . import commands
|
||||
from redbot.core.utils.chat_formatting import pagify, box
|
||||
from redbot.core.utils import fuzzy_command_search
|
||||
from .i18n import Translator
|
||||
from .utils.chat_formatting import pagify
|
||||
from .utils import fuzzy_command_search, format_fuzzy_results
|
||||
|
||||
_ = Translator("Help", __file__)
|
||||
|
||||
EMPTY_STRING = "\u200b"
|
||||
|
||||
@@ -49,7 +48,7 @@ _mention_pattern = re.compile("|".join(_mentions_transforms.keys()))
|
||||
EmbedField = namedtuple("EmbedField", "name value inline")
|
||||
|
||||
|
||||
class Help(formatter.HelpFormatter):
|
||||
class Help(dpy_formatter.HelpFormatter):
|
||||
"""Formats help for commands."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -57,15 +56,10 @@ class Help(formatter.HelpFormatter):
|
||||
self.command = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def pm_check(self, ctx):
|
||||
@staticmethod
|
||||
def pm_check(ctx):
|
||||
return isinstance(ctx.channel, discord.DMChannel)
|
||||
|
||||
@property
|
||||
def clean_prefix(self):
|
||||
maybe_member = self.context.guild.me if self.context.guild else self.context.bot.user
|
||||
pretty = f"@{maybe_member.display_name}"
|
||||
return self.context.prefix.replace(maybe_member.mention, pretty)
|
||||
|
||||
@property
|
||||
def me(self):
|
||||
return self.context.me
|
||||
@@ -84,6 +78,8 @@ class Help(formatter.HelpFormatter):
|
||||
else:
|
||||
return await self.context.embed_colour()
|
||||
|
||||
colour = color
|
||||
|
||||
@property
|
||||
def destination(self):
|
||||
if self.context.bot.pm_help:
|
||||
@@ -110,7 +106,7 @@ class Help(formatter.HelpFormatter):
|
||||
continue
|
||||
|
||||
if self.is_cog() or self.is_bot():
|
||||
name = "{0}{1}".format(self.clean_prefix, name)
|
||||
name = "{0}{1}".format(self.context.clean_prefix, name)
|
||||
|
||||
entries += "**{0}** {1}\n".format(name, command.short_doc)
|
||||
return entries
|
||||
@@ -120,7 +116,7 @@ class Help(formatter.HelpFormatter):
|
||||
return (
|
||||
"Type {0}help <command> for more info on a command.\n"
|
||||
"You can also type {0}help <category> for more info on a category.".format(
|
||||
self.clean_prefix
|
||||
self.context.clean_prefix
|
||||
)
|
||||
)
|
||||
|
||||
@@ -163,7 +159,7 @@ class Help(formatter.HelpFormatter):
|
||||
if self.command.help:
|
||||
splitted = self.command.help.split("\n\n")
|
||||
name = "__{0}__".format(splitted[0])
|
||||
value = "\n\n".join(splitted[1:]).replace("[p]", self.clean_prefix)
|
||||
value = "\n\n".join(splitted[1:]).replace("[p]", self.context.clean_prefix)
|
||||
if value == "":
|
||||
value = EMPTY_STRING
|
||||
field = EmbedField(name[:252], value[:1024], False)
|
||||
@@ -213,7 +209,8 @@ class Help(formatter.HelpFormatter):
|
||||
|
||||
return emb
|
||||
|
||||
def group_fields(self, fields: List[EmbedField], max_chars=1000):
|
||||
@staticmethod
|
||||
def group_fields(fields: List[EmbedField], max_chars=1000):
|
||||
curr_group = []
|
||||
ret = []
|
||||
for f in fields:
|
||||
@@ -277,158 +274,112 @@ class Help(formatter.HelpFormatter):
|
||||
|
||||
return ret
|
||||
|
||||
async def simple_embed(self, ctx, title=None, description=None, color=None):
|
||||
# Shortcut
|
||||
async def format_command_not_found(
|
||||
self, ctx: commands.Context, command_name: str
|
||||
) -> Optional[Union[str, discord.Message]]:
|
||||
"""Get the response for a user calling help on a missing command."""
|
||||
self.context = ctx
|
||||
if color is None:
|
||||
color = await self.color()
|
||||
embed = discord.Embed(title=title, description=description, color=color)
|
||||
embed.set_footer(text=ctx.bot.formatter.get_ending_note())
|
||||
embed.set_author(**self.author)
|
||||
return embed
|
||||
|
||||
async def cmd_not_found(self, ctx, cmd, description=None, color=None):
|
||||
# Shortcut for a shortcut. Sue me
|
||||
embed = await self.simple_embed(
|
||||
ctx, title="Command {} not found.".format(cmd), description=description, color=color
|
||||
return await default_command_not_found(
|
||||
ctx,
|
||||
command_name,
|
||||
use_embeds=True,
|
||||
colour=await self.colour(),
|
||||
author=self.author,
|
||||
footer={"text": self.get_ending_note()},
|
||||
)
|
||||
return embed
|
||||
|
||||
async def cmd_has_no_subcommands(self, ctx, cmd, color=None):
|
||||
embed = await self.simple_embed(
|
||||
ctx, title=ctx.bot.command_has_no_subcommands.format(cmd), color=color
|
||||
)
|
||||
return embed
|
||||
|
||||
|
||||
@commands.command(hidden=True)
|
||||
async def help(ctx, *cmds: str):
|
||||
"""Shows help documentation.
|
||||
async def help(ctx: commands.Context, *, command_name: str = ""):
|
||||
"""Show help documentation.
|
||||
|
||||
[p]**help**: Shows the help manual.
|
||||
[p]**help** command: Show help for a command
|
||||
[p]**help** Category: Show commands and description for a category"""
|
||||
destination = ctx.author if ctx.bot.pm_help else ctx
|
||||
|
||||
def repl(obj):
|
||||
return _mentions_transforms.get(obj.group(0), "")
|
||||
- `[p]help`: Show the help manual.
|
||||
- `[p]help command`: Show help for a command.
|
||||
- `[p]help Category`: Show commands and description for a category,
|
||||
"""
|
||||
bot = ctx.bot
|
||||
if bot.pm_help:
|
||||
destination = ctx.author
|
||||
else:
|
||||
destination = ctx.channel
|
||||
|
||||
use_embeds = await ctx.embed_requested()
|
||||
f = formatter.HelpFormatter()
|
||||
# help by itself just lists our own commands.
|
||||
if len(cmds) == 0:
|
||||
if use_embeds:
|
||||
embeds = await ctx.bot.formatter.format_help_for(ctx, ctx.bot)
|
||||
else:
|
||||
embeds = await f.format_help_for(ctx, ctx.bot)
|
||||
elif len(cmds) == 1:
|
||||
# try to see if it is a cog name
|
||||
name = _mention_pattern.sub(repl, cmds[0])
|
||||
command = None
|
||||
if name in ctx.bot.cogs:
|
||||
command = ctx.bot.cogs[name]
|
||||
else:
|
||||
command = ctx.bot.all_commands.get(name)
|
||||
if command is None:
|
||||
if use_embeds:
|
||||
fuzzy_result = await fuzzy_command_search(ctx, name)
|
||||
if fuzzy_result is not None:
|
||||
await destination.send(
|
||||
embed=await ctx.bot.formatter.cmd_not_found(
|
||||
ctx, name, description=fuzzy_result
|
||||
)
|
||||
)
|
||||
else:
|
||||
fuzzy_result = await fuzzy_command_search(ctx, name)
|
||||
if fuzzy_result is not None:
|
||||
await destination.send(
|
||||
ctx.bot.command_not_found.format(name, fuzzy_result)
|
||||
)
|
||||
return
|
||||
if use_embeds:
|
||||
embeds = await ctx.bot.formatter.format_help_for(ctx, command)
|
||||
else:
|
||||
embeds = await f.format_help_for(ctx, command)
|
||||
if use_embeds:
|
||||
formatter = bot.formatter
|
||||
else:
|
||||
name = _mention_pattern.sub(repl, cmds[0])
|
||||
command = ctx.bot.all_commands.get(name)
|
||||
if command is None:
|
||||
if use_embeds:
|
||||
fuzzy_result = await fuzzy_command_search(ctx, name)
|
||||
if fuzzy_result is not None:
|
||||
await destination.send(
|
||||
embed=await ctx.bot.formatter.cmd_not_found(
|
||||
ctx, name, description=fuzzy_result
|
||||
)
|
||||
)
|
||||
else:
|
||||
fuzzy_result = await fuzzy_command_search(ctx, name)
|
||||
if fuzzy_result is not None:
|
||||
await destination.send(ctx.bot.command_not_found.format(name, fuzzy_result))
|
||||
return
|
||||
formatter = dpy_formatter.HelpFormatter()
|
||||
|
||||
for key in cmds[1:]:
|
||||
try:
|
||||
key = _mention_pattern.sub(repl, key)
|
||||
command = command.all_commands.get(key)
|
||||
if command is None:
|
||||
if use_embeds:
|
||||
fuzzy_result = await fuzzy_command_search(ctx, name)
|
||||
if fuzzy_result is not None:
|
||||
await destination.send(
|
||||
embed=await ctx.bot.formatter.cmd_not_found(
|
||||
ctx, name, description=fuzzy_result
|
||||
)
|
||||
)
|
||||
else:
|
||||
fuzzy_result = await fuzzy_command_search(ctx, name)
|
||||
if fuzzy_result is not None:
|
||||
await destination.send(
|
||||
ctx.bot.command_not_found.format(name, fuzzy_result)
|
||||
)
|
||||
return
|
||||
except AttributeError:
|
||||
if use_embeds:
|
||||
await destination.send(
|
||||
embed=await ctx.bot.formatter.simple_embed(
|
||||
ctx,
|
||||
title='Command "{0.name}" has no subcommands.'.format(command),
|
||||
color=await ctx.bot.formatter.color(),
|
||||
)
|
||||
)
|
||||
else:
|
||||
await destination.send(ctx.bot.command_has_no_subcommands.format(command))
|
||||
return
|
||||
if use_embeds:
|
||||
embeds = await ctx.bot.formatter.format_help_for(ctx, command)
|
||||
if not command_name:
|
||||
# help by itself just lists our own commands.
|
||||
pages = await formatter.format_help_for(ctx, bot)
|
||||
else:
|
||||
command: commands.Command = bot.get_command(command_name)
|
||||
if command is None:
|
||||
if hasattr(formatter, "format_command_not_found"):
|
||||
msg = await formatter.format_command_not_found(ctx, command_name)
|
||||
else:
|
||||
msg = await default_command_not_found(ctx, command_name, use_embeds=use_embeds)
|
||||
pages = [msg]
|
||||
else:
|
||||
embeds = await f.format_help_for(ctx, command)
|
||||
pages = await formatter.format_help_for(ctx, command)
|
||||
|
||||
max_pages_in_guild = await ctx.bot.db.help.max_pages_in_guild()
|
||||
if len(embeds) > max_pages_in_guild:
|
||||
if len(pages) > max_pages_in_guild:
|
||||
destination = ctx.author
|
||||
if ctx.guild and not ctx.guild.me.permissions_in(ctx.channel).send_messages:
|
||||
destination = ctx.author
|
||||
try:
|
||||
for embed in embeds:
|
||||
if use_embeds:
|
||||
try:
|
||||
await destination.send(embed=embed)
|
||||
except discord.HTTPException:
|
||||
destination = ctx.author
|
||||
await destination.send(embed=embed)
|
||||
for page in pages:
|
||||
if isinstance(page, discord.Embed):
|
||||
await destination.send(embed=page)
|
||||
else:
|
||||
try:
|
||||
await destination.send(embed)
|
||||
except discord.HTTPException:
|
||||
destination = ctx.author
|
||||
await destination.send(embed)
|
||||
await destination.send(page)
|
||||
except discord.Forbidden:
|
||||
await ctx.channel.send(
|
||||
"I couldn't send the help message to you in DM. Either you blocked me or you disabled DMs in this server."
|
||||
_(
|
||||
"I couldn't send the help message to you in DM. Either you blocked me or you "
|
||||
"disabled DMs in this server."
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@help.error
|
||||
async def help_error(ctx, error):
|
||||
destination = ctx.author if ctx.bot.pm_help else ctx
|
||||
await destination.send("{0.__name__}: {1}".format(type(error), error))
|
||||
traceback.print_tb(error.original.__traceback__, file=sys.stderr)
|
||||
async def default_command_not_found(
|
||||
ctx: commands.Context, command_name: str, *, use_embeds: bool, **embed_options
|
||||
) -> Optional[Union[str, discord.Embed]]:
|
||||
"""Default function for formatting the response to a missing command."""
|
||||
ret = None
|
||||
cmds = command_name.split()
|
||||
prev_command = None
|
||||
for invoked in itertools.accumulate(cmds, lambda *args: " ".join(args)):
|
||||
command = ctx.bot.get_command(invoked)
|
||||
if command is None:
|
||||
if prev_command is not None and not isinstance(prev_command, commands.Group):
|
||||
ret = _("Command *{command_name}* has no subcommands.").format(
|
||||
command_name=prev_command.qualified_name
|
||||
)
|
||||
break
|
||||
elif not await command.can_see(ctx):
|
||||
return
|
||||
prev_command = command
|
||||
|
||||
if ret is None:
|
||||
fuzzy_commands = await fuzzy_command_search(ctx, command_name, min_score=75)
|
||||
if fuzzy_commands:
|
||||
ret = await format_fuzzy_results(ctx, fuzzy_commands, embed=use_embeds)
|
||||
else:
|
||||
ret = _("Command *{command_name}* not found.").format(command_name=command_name)
|
||||
|
||||
if use_embeds:
|
||||
if isinstance(ret, str):
|
||||
ret = discord.Embed(title=ret)
|
||||
if "colour" in embed_options:
|
||||
ret.colour = embed_options.pop("colour")
|
||||
elif "color" in embed_options:
|
||||
ret.colour = embed_options.pop("color")
|
||||
|
||||
if "author" in embed_options:
|
||||
ret.set_author(**embed_options.pop("author"))
|
||||
if "footer" in embed_options:
|
||||
ret.set_footer(**embed_options.pop("footer"))
|
||||
|
||||
return ret
|
||||
|
||||
Reference in New Issue
Block a user