mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 03:08:55 -05:00
[V3 Help] Convert help command to support embeds (#1106)
* Replace built in help with embedded help * Make embeds pagify * Fix thingy * Fix missing embed permissions
This commit is contained in:
parent
09ed5e67a6
commit
064e9b6bd0
@ -16,6 +16,7 @@ from . import (
|
||||
RedContext,
|
||||
rpc
|
||||
)
|
||||
from .help_formatter import Help, help as help_
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@ -95,7 +96,11 @@ class RedBase(BotBase, RpcMethodMixin):
|
||||
|
||||
self.register_rpc_methods()
|
||||
|
||||
super().__init__(**kwargs)
|
||||
super().__init__(formatter=Help(), **kwargs)
|
||||
|
||||
self.remove_command('help')
|
||||
|
||||
self.add_command(help_)
|
||||
|
||||
async def _dict_abuse(self, indict):
|
||||
"""
|
||||
|
||||
@ -33,10 +33,17 @@ class RedContext(commands.Context):
|
||||
|
||||
"""
|
||||
command = self.invoked_subcommand or self.command
|
||||
pages = await self.bot.formatter.format_help_for(self, command)
|
||||
embeds = await self.bot.formatter.format_help_for(self, command)
|
||||
destination = self
|
||||
ret = []
|
||||
for page in pages:
|
||||
ret.append(await self.send(page))
|
||||
for embed in embeds:
|
||||
try:
|
||||
m = await destination.send(embed=embed)
|
||||
except discord.HTTPException:
|
||||
destination = self.author
|
||||
m = await destination.send(embed=embed)
|
||||
ret.append(m)
|
||||
|
||||
return ret
|
||||
|
||||
async def tick(self) -> bool:
|
||||
|
||||
341
redbot/core/help_formatter.py
Normal file
341
redbot/core/help_formatter.py
Normal file
@ -0,0 +1,341 @@
|
||||
"""Overrides the built-in help formatter.
|
||||
|
||||
All help messages will be embed and pretty.
|
||||
|
||||
Most of the code stolen from
|
||||
discord.ext.commands.formatter.py and
|
||||
converted into embeds instead of codeblocks.
|
||||
|
||||
Docstr on cog class becomes category.
|
||||
Docstr on command definition becomes command
|
||||
summary and usage.
|
||||
Use [p] in command docstr for bot prefix.
|
||||
|
||||
See [p]help here for example.
|
||||
|
||||
await bot.formatter.format_help_for(ctx, command)
|
||||
to send help page for command. Optionally pass a
|
||||
string as third arg to add a more descriptive
|
||||
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"""
|
||||
from collections import namedtuple
|
||||
from typing import List
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from discord.ext.commands import formatter
|
||||
import inspect
|
||||
import itertools
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
|
||||
EMPTY_STRING = u'\u200b'
|
||||
|
||||
_mentions_transforms = {
|
||||
'@everyone': '@\u200beveryone',
|
||||
'@here': '@\u200bhere'
|
||||
}
|
||||
|
||||
_mention_pattern = re.compile('|'.join(_mentions_transforms.keys()))
|
||||
|
||||
EmbedField = namedtuple('EmbedField', 'name value inline')
|
||||
|
||||
|
||||
class Help(formatter.HelpFormatter):
|
||||
"""Formats help for commands."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.context = None
|
||||
self.command = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def pm_check(self, ctx):
|
||||
return isinstance(ctx.channel, discord.DMChannel)
|
||||
|
||||
@property
|
||||
def me(self):
|
||||
return self.context.me
|
||||
|
||||
@property
|
||||
def bot_all_commands(self):
|
||||
return self.context.bot.all_commands
|
||||
|
||||
@property
|
||||
def avatar(self):
|
||||
return self.context.bot.user.avatar_url_as(format='png')
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
if self.pm_check(self.context):
|
||||
return 0
|
||||
else:
|
||||
return self.me.color
|
||||
|
||||
@property
|
||||
def destination(self):
|
||||
if self.context.bot.pm_help:
|
||||
return self.context.author
|
||||
return self.context
|
||||
|
||||
# All the other shit
|
||||
|
||||
@property
|
||||
def author(self):
|
||||
# Get author dict with username if PM and display name in guild
|
||||
if self.pm_check(self.context):
|
||||
name = self.context.bot.user.name
|
||||
else:
|
||||
name = self.me.display_name if not '' else self.context.bot.user.name
|
||||
author = {
|
||||
'name': '{0} Help Manual'.format(name),
|
||||
'icon_url': self.avatar
|
||||
}
|
||||
return author
|
||||
|
||||
def _add_subcommands(self, cmds):
|
||||
entries = ''
|
||||
for name, command in cmds:
|
||||
if name in command.aliases:
|
||||
# skip aliases
|
||||
continue
|
||||
|
||||
if self.is_cog() or self.is_bot():
|
||||
name = '{0}{1}'.format(self.clean_prefix, name)
|
||||
|
||||
entries += '**{0}** {1}\n'.format(name, command.short_doc)
|
||||
return entries
|
||||
|
||||
def get_ending_note(self):
|
||||
# command_name = self.context.invoked_with
|
||||
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)
|
||||
|
||||
async def format(self) -> dict:
|
||||
"""Formats command for output.
|
||||
|
||||
Returns a dict used to build embed"""
|
||||
emb = {
|
||||
'embed': {
|
||||
'title': '',
|
||||
'description': '',
|
||||
},
|
||||
'footer': {
|
||||
'text': self.get_ending_note()
|
||||
},
|
||||
'fields': []
|
||||
}
|
||||
|
||||
description = self.command.description if not self.is_cog() else inspect.getdoc(self.command)
|
||||
if not description == '' and description is not None:
|
||||
description = '*{0}*'.format(description)
|
||||
|
||||
if description:
|
||||
# <description> portion
|
||||
emb['embed']['description'] = description
|
||||
|
||||
if isinstance(self.command, discord.ext.commands.core.Command):
|
||||
# <signature portion>
|
||||
emb['embed']['title'] = emb['embed']['description']
|
||||
emb['embed']['description'] = '`Syntax: {0}`'.format(self.get_command_signature())
|
||||
|
||||
# <long doc> section
|
||||
if self.command.help:
|
||||
name = '__{0}__'.format(self.command.help.split('\n\n')[0])
|
||||
name_length = len(name) - 4
|
||||
value = self.command.help[name_length:].replace('[p]', self.clean_prefix)
|
||||
if value == '':
|
||||
value = EMPTY_STRING
|
||||
field = EmbedField(name, value, False)
|
||||
emb['fields'].append(field)
|
||||
|
||||
# end it here if it's just a regular command
|
||||
if not self.has_subcommands():
|
||||
return emb
|
||||
|
||||
def category(tup):
|
||||
# Turn get cog (Category) name from cog/list tuples
|
||||
cog = tup[1].cog_name
|
||||
return '**__{0}:__**'.format(cog) if cog is not None else '**__\u200bNo Category:__**'
|
||||
|
||||
# Get subcommands for bot or category
|
||||
filtered = await self.filter_command_list()
|
||||
|
||||
if self.is_bot():
|
||||
# Get list of non-hidden commands for bot.
|
||||
data = sorted(filtered, key=category)
|
||||
for category, commands_ in itertools.groupby(data, key=category):
|
||||
commands_ = sorted(commands_)
|
||||
if len(commands_) > 0:
|
||||
field = EmbedField(category, self._add_subcommands(commands_), False)
|
||||
emb['fields'].append(field)
|
||||
|
||||
else:
|
||||
# Get list of commands for category
|
||||
filtered = sorted(filtered)
|
||||
if filtered:
|
||||
field = EmbedField(
|
||||
'**__Commands:__**' if not self.is_bot() and self.is_cog() else '**__Subcommands:__**',
|
||||
self._add_subcommands(filtered), # May need paginated
|
||||
False)
|
||||
|
||||
emb['fields'].append(field)
|
||||
|
||||
return emb
|
||||
|
||||
def group_fields(self, fields: List[EmbedField], max_chars=1000):
|
||||
curr_group = []
|
||||
ret = []
|
||||
for f in fields:
|
||||
curr_group.append(f)
|
||||
if sum(len(f.value) for f in curr_group) > max_chars:
|
||||
ret.append(curr_group)
|
||||
curr_group = []
|
||||
|
||||
if len(curr_group) > 0:
|
||||
ret.append(curr_group)
|
||||
|
||||
return ret
|
||||
|
||||
async def format_help_for(self, ctx, command_or_bot, reason: str=None):
|
||||
"""Formats the help page and handles the actual heavy lifting of how ### WTF HAPPENED?
|
||||
the help command looks like. To change the behaviour, override the
|
||||
:meth:`~.HelpFormatter.format` method.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
ctx: :class:`.Context`
|
||||
The context of the invoked help command.
|
||||
command_or_bot: :class:`.Command` or :class:`.Bot`
|
||||
The bot or command that we are getting the help of.
|
||||
reason : str
|
||||
|
||||
Returns
|
||||
--------
|
||||
list
|
||||
A paginated output of the help command.
|
||||
"""
|
||||
self.context = ctx
|
||||
self.command = command_or_bot
|
||||
emb = await self.format()
|
||||
|
||||
if reason:
|
||||
emb['embed']['title'] = "{0}".format(reason)
|
||||
|
||||
ret = []
|
||||
field_groups = self.group_fields(emb['fields'])
|
||||
|
||||
for i, group in enumerate(field_groups, 1):
|
||||
embed = discord.Embed(color=self.color, **emb['embed'])
|
||||
|
||||
if len(field_groups) > 1:
|
||||
description = "{} *- Page {} of {}*".format(embed.description, i, len(field_groups))
|
||||
embed.description = description
|
||||
|
||||
embed.set_author(**self.author)
|
||||
|
||||
for field in group:
|
||||
embed.add_field(**field._asdict())
|
||||
|
||||
embed.set_footer(**emb['footer'])
|
||||
|
||||
ret.append(embed)
|
||||
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def simple_embed(ctx, title=None, description=None, color=None, author=None):
|
||||
# Shortcut
|
||||
embed = discord.Embed(title=title, description=description, color=color)
|
||||
embed.set_footer(text=ctx.bot.formatter.get_ending_note())
|
||||
if author:
|
||||
embed.set_author(**author)
|
||||
return embed
|
||||
|
||||
@staticmethod
|
||||
def cmd_not_found(ctx, cmd, color=0):
|
||||
# Shortcut for a shortcut. Sue me
|
||||
embed = Help.simple_embed(
|
||||
ctx,
|
||||
title=ctx.bot.command_not_found.format(cmd),
|
||||
description='Commands are case sensitive. Please check your spelling and try again',
|
||||
color=color, author=ctx.author)
|
||||
return embed
|
||||
|
||||
|
||||
@commands.command()
|
||||
async def help(ctx, *cmds: str):
|
||||
"""Shows 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), '')
|
||||
|
||||
# help by itself just lists our own commands.
|
||||
if len(cmds) == 0:
|
||||
embeds = await ctx.bot.formatter.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:
|
||||
await destination.send(
|
||||
embed=Help.cmd_not_found(ctx, name, ctx.bot.formatter.color))
|
||||
return
|
||||
|
||||
embeds = await ctx.bot.formatter.format_help_for(ctx, command)
|
||||
else:
|
||||
name = _mention_pattern.sub(repl, cmds[0])
|
||||
command = ctx.bot.all_commands.get(name)
|
||||
if command is None:
|
||||
await destination.send(embed=Help.cmd_not_found(ctx, name, ctx.bot.formatter.color))
|
||||
return
|
||||
|
||||
for key in cmds[1:]:
|
||||
try:
|
||||
key = _mention_pattern.sub(repl, key)
|
||||
command = command.all_commands.get(key)
|
||||
if command is None:
|
||||
await destination.send(
|
||||
embed=Help.cmd_not_found(ctx, key, ctx.bot.formatter.color))
|
||||
return
|
||||
except AttributeError:
|
||||
await destination.send(
|
||||
embed=Help.simple_embed(
|
||||
ctx,
|
||||
title='Command "{0.name}" has no subcommands.'.format(command),
|
||||
color=ctx.bot.formatter.color,
|
||||
author=ctx.author.display_name))
|
||||
return
|
||||
|
||||
embeds = await ctx.bot.formatter.format_help_for(ctx, command)
|
||||
|
||||
if len(embeds) > 2:
|
||||
destination = ctx.author
|
||||
|
||||
for embed in embeds:
|
||||
try:
|
||||
await destination.send(embed=embed)
|
||||
except discord.HTTPException:
|
||||
destination = ctx.author
|
||||
await destination.send(embed=embed)
|
||||
|
||||
|
||||
@help.error
|
||||
async def help_error(self, error, ctx):
|
||||
await self.destination.send('{0.__name__}: {1}'.format(type(error), error))
|
||||
traceback.print_tb(error.original.__traceback__, file=sys.stderr)
|
||||
Loading…
x
Reference in New Issue
Block a user