Discord.py dep update 3.1 (#2587)

* Dependency update

discord.py==1.0.1
websockets<7

[style]
black==19.3b0

[Docs]
jinja==2.10.1
urllib3==1.24.2

Changes related to breaking changes from discord.py have also been made
to match

As of this commit, help formatter is back to discord.py's default
This commit is contained in:
Michael H
2019-04-23 21:40:38 -04:00
committed by GitHub
parent 0ff7259bc3
commit ad114295e7
83 changed files with 231 additions and 22307 deletions

View File

@@ -104,7 +104,7 @@ def main():
log.debug("Data Path: %s", data_manager._base_data_path())
log.debug("Storage Type: %s", data_manager.storage_type())
red = Red(cli_flags=cli_flags, description=description, pm_help=None)
red = Red(cli_flags=cli_flags, description=description, dm_help=None)
init_global_checks(red)
init_events(red, cli_flags)
red.add_cog(Core(red))

View File

@@ -66,7 +66,7 @@ class Admin(commands.Cog):
self.__current_announcer = None
def __unload(self):
def cog_unload(self):
try:
self.__current_announcer.cancel()
except AttributeError:

View File

@@ -20,7 +20,7 @@ class MemberDefaultAuthor(commands.Converter):
class SelfRole(commands.Converter):
async def convert(self, ctx: commands.Context, arg: str) -> discord.Role:
admin = ctx.command.instance
admin = ctx.command.cog
if admin is None:
raise commands.BadArgument(_("The Admin cog is not loaded."))

View File

@@ -4,7 +4,7 @@ from string import Formatter
from typing import Generator, Tuple, Iterable, Optional
import discord
from discord.ext.commands.view import StringView, quoted_word
from discord.ext.commands.view import StringView
from redbot.core import Config, commands, checks
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import box
@@ -182,7 +182,7 @@ class Alias(commands.Cog):
extra = []
while not view.eof:
prev = view.index
word = quoted_word(view)
word = view.get_quoted_word(view)
if len(word) < view.index - prev:
word = "".join((view.buffer[prev], word, view.buffer[view.index - 1]))
extra.append(word)
@@ -434,6 +434,7 @@ class Alias(commands.Cog):
else:
await ctx.send(box("\n".join(names), "diff"))
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
aliases = list(await self.unloaded_global_aliases())
if message.guild is not None:

View File

@@ -54,7 +54,7 @@ class AliasEntry:
if bot:
ret.has_real_data = True
ret.creator = bot.get_user(int(data["creator"]))
guild = bot.get_guild(int(data["guild"]))
guild = bot.fetch_guild(int(data["guild"]))
ret.guild = guild
else:
ret.guild = data["guild"]

View File

@@ -3493,6 +3493,7 @@ class Audio(commands.Cog):
)
return r
@commands.Cog.listener()
async def on_voice_state_update(self, member, before, after):
if after.channel != before.channel:
try:
@@ -3500,7 +3501,7 @@ class Audio(commands.Cog):
except (ValueError, KeyError, AttributeError):
pass
def __unload(self):
def cog_unload(self):
if not self._cleaned_up:
self.session.detach()
@@ -3515,8 +3516,9 @@ class Audio(commands.Cog):
shutdown_lavalink_server()
self._cleaned_up = True
__del__ = __unload
__del__ = cog_unload
@commands.Cog.listener()
async def on_guild_remove(self, guild: discord.Guild):
"""
This is to clean up players when

View File

@@ -90,7 +90,7 @@ class Cleanup(commands.Cog):
collected = []
async for message in channel.history(
limit=None, before=before, after=after, reverse=False
limit=None, before=before, after=after, oldest_first=False
):
if message.created_at < two_weeks_ago:
break
@@ -223,7 +223,7 @@ class Cleanup(commands.Cog):
author = ctx.author
try:
after = await channel.get_message(message_id)
after = await channel.fetch_message(message_id)
except discord.NotFound:
return await ctx.send(_("Message not found."))

View File

@@ -434,6 +434,7 @@ class CustomCommands(commands.Cog):
for p in pagify(text):
await ctx.send(box(p, lang="yaml"))
@commands.Cog.listener()
async def on_message(self, message):
is_private = isinstance(message.channel, discord.abc.PrivateChannel)

View File

@@ -564,12 +564,12 @@ class Downloader(commands.Cog):
return
# Check if in installed cogs
cog_name = self.cog_name_from_instance(command.instance)
cog_name = self.cog_name_from_instance(command.cog)
installed, cog_installable = await self.is_installed(cog_name)
if installed:
msg = self.format_findcog_info(command_name, cog_installable)
else:
# Assume it's in a base cog
msg = self.format_findcog_info(command_name, command.instance)
msg = self.format_findcog_info(command_name, command.cog)
await ctx.send(box(msg))

View File

@@ -33,7 +33,7 @@ class Filter(commands.Cog):
self.register_task = self.bot.loop.create_task(self.register_filterban())
self.pattern_cache = {}
def __unload(self):
def cog_unload(self):
self.register_task.cancel()
@staticmethod
@@ -409,6 +409,7 @@ class Filter(commands.Cog):
reason,
)
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
if isinstance(message.channel, discord.abc.PrivateChannel):
return
@@ -422,15 +423,18 @@ class Filter(commands.Cog):
await self.check_filter(message)
@commands.Cog.listener()
async def on_message_edit(self, _prior, message):
# message content has to change for non-bot's currently.
# if this changes, we should compare before passing it.
await self.on_message(message)
@commands.Cog.listener()
async def on_member_update(self, before: discord.Member, after: discord.Member):
if before.display_name != after.display_name:
await self.maybe_filter_name(after)
@commands.Cog.listener()
async def on_member_join(self, member: discord.Member):
await self.maybe_filter_name(member)

View File

@@ -24,7 +24,7 @@ class Image(commands.Cog):
self.session = aiohttp.ClientSession()
self.imgur_base_url = "https://api.imgur.com/3/"
def __unload(self):
def cog_unload(self):
self.session.detach()
async def initialize(self) -> None:

View File

@@ -8,7 +8,7 @@ from redbot.core.bot import Red
class MixinMeta(ABC):
"""
Metaclass for well behaved type hint detection with composite class.
Base class for well behaved type hint detection with composite class.
Basically, to keep developers sane when not all attributes are defined in each mixin.
"""

View File

@@ -2,7 +2,7 @@ from datetime import datetime
from collections import defaultdict, deque
import discord
from redbot.core import i18n, modlog
from redbot.core import i18n, modlog, commands
from redbot.core.utils.mod import is_mod_or_superior
from . import log
from .abc import MixinMeta
@@ -73,6 +73,7 @@ class Events(MixinMeta):
return True
return False
@commands.Cog.listener()
async def on_message(self, message):
author = message.author
if message.guild is None or self.bot.user == author:
@@ -92,6 +93,7 @@ class Events(MixinMeta):
if not deleted:
await self.check_mention_spam(message)
@commands.Cog.listener()
async def on_member_ban(self, guild: discord.Guild, member: discord.Member):
if (guild.id, member.id) in self.ban_queue:
self.ban_queue.remove((guild.id, member.id))
@@ -112,6 +114,7 @@ class Events(MixinMeta):
except RuntimeError as e:
print(e)
@commands.Cog.listener()
async def on_member_unban(self, guild: discord.Guild, user: discord.User):
if (guild.id, user.id) in self.unban_queue:
self.unban_queue.remove((guild.id, user.id))
@@ -130,8 +133,8 @@ class Events(MixinMeta):
except RuntimeError as e:
print(e)
@staticmethod
async def on_modlog_case_create(case: modlog.Case):
@commands.Cog.listener()
async def on_modlog_case_create(self, case: modlog.Case):
"""
An event for modlog case creation
"""
@@ -147,8 +150,8 @@ class Events(MixinMeta):
msg = await mod_channel.send(case_content)
await case.edit({"message": msg})
@staticmethod
async def on_modlog_case_edit(case: modlog.Case):
@commands.Cog.listener()
async def on_modlog_case_edit(self, case: modlog.Case):
"""
Event for modlog case edits
"""
@@ -161,6 +164,7 @@ class Events(MixinMeta):
else:
await case.message.edit(content=case_content)
@commands.Cog.listener()
async def on_member_update(self, before: discord.Member, after: discord.Member):
if before.name != after.name:
async with self.settings.user(before).past_names() as name_list:

View File

@@ -131,7 +131,7 @@ class KickBanMixin(MixinMeta):
)
now = datetime.utcnow()
if now > unban_time: # Time to unban the user
user = await self.bot.get_user_info(uid)
user = await self.bot.fetch_user(uid)
queue_entry = (guild.id, user.id)
self.unban_queue.append(queue_entry)
try:
@@ -335,7 +335,7 @@ class KickBanMixin(MixinMeta):
else:
banned.append(user_id)
user_info = await self.bot.get_user_info(user_id)
user_info = await self.bot.fetch_user(user_id)
try:
await modlog.create_case(
@@ -508,7 +508,7 @@ class KickBanMixin(MixinMeta):
click the user and select 'Copy ID'."""
guild = ctx.guild
author = ctx.author
user = await self.bot.get_user_info(user_id)
user = await self.bot.fetch_user(user_id)
if not user:
await ctx.send(_("Couldn't find a user with that ID!"))
return

View File

@@ -1,5 +1,6 @@
from collections import defaultdict
from typing import List, Tuple
from abc import ABC
import discord
from redbot.core import Config, modlog, commands
@@ -18,8 +19,26 @@ _ = T_ = Translator("Mod", __file__)
__version__ = "1.0.0"
class CompositeMetaClass(type(commands.Cog), type(ABC)):
"""
This allows the metaclass used for proper type detection to
coexist with discord.py's metaclass
"""
pass
@cog_i18n(_)
class Mod(ModSettings, Events, KickBanMixin, MoveToCore, MuteMixin, ModInfo, commands.Cog):
class Mod(
ModSettings,
Events,
KickBanMixin,
MoveToCore,
MuteMixin,
ModInfo,
commands.Cog,
metaclass=CompositeMetaClass,
):
"""Moderation tools."""
default_global_settings = {"version": ""}
@@ -60,7 +79,7 @@ class Mod(ModSettings, Events, KickBanMixin, MoveToCore, MuteMixin, ModInfo, com
async def initialize(self):
await self._maybe_update_config()
def __unload(self):
def cog_unload(self):
self.registration_task.cancel()
self.tban_expiry_task.cancel()
@@ -87,7 +106,7 @@ class Mod(ModSettings, Events, KickBanMixin, MoveToCore, MuteMixin, ModInfo, com
# TODO: Move this to core.
# This would be in .movetocore , but the double-under name here makes that more trouble
async def __global_check(self, ctx):
async def bot_check(self, ctx):
"""Global check to see if a channel or server is ignored.
Any users who have permission to use the `ignore` or `unignore` commands

View File

@@ -16,10 +16,12 @@ class MoveToCore(MixinMeta):
Mixin for things which should really not be in mod, but have not been moved out yet.
"""
@commands.Cog.listener()
async def on_command_completion(self, ctx: commands.Context):
await self._delete_delay(ctx)
# noinspection PyUnusedLocal
@commands.Cog.listener()
async def on_command_error(self, ctx: commands.Context, error):
await self._delete_delay(ctx)

View File

@@ -8,6 +8,6 @@ async def setup(bot):
# the permissions commands themselves have rules added.
# Automatic listeners being added in add_cog happen in arbitrary
# order, so we want to circumvent that.
bot.add_listener(cog.cog_added, "on_cog_add")
bot.add_listener(cog.command_added, "on_command_add")
bot.add_listener(cog.red_cog_added, "on_cog_add")
bot.add_listener(cog.red_command_added, "on_command_add")
bot.add_cog(cog)

View File

@@ -433,20 +433,26 @@ class Permissions(commands.Cog):
await self._clear_rules(guild_id=ctx.guild.id)
await ctx.tick()
async def cog_added(self, cog: commands.Cog) -> None:
async def red_cog_added(self, cog: commands.Cog) -> None:
"""Event listener for `cog_add`.
This loads rules whenever a new cog is added.
Do not convert to using Cog.listener decorator !!
This *must* be added manually prior to cog load, and removed at unload
"""
self._load_rules_for(
cog_or_command=cog,
rule_dict=await self.config.custom(COG, cog.__class__.__name__).all(),
)
async def command_added(self, command: commands.Command) -> None:
async def red_command_added(self, command: commands.Command) -> None:
"""Event listener for `command_add`.
This loads rules whenever a new command is added.
Do not convert to using Cog.listener decorator !!
This *must* be added manually prior to cog load, and removed at unload
"""
self._load_rules_for(
cog_or_command=command,
@@ -701,9 +707,9 @@ class Permissions(commands.Cog):
elif rule is False:
cog_or_command.deny_to(model_id, guild_id=guild_id)
def __unload(self) -> None:
self.bot.remove_listener(self.cog_added, "on_cog_add")
self.bot.remove_listener(self.command_added, "on_command_add")
def cog_unload(self) -> None:
self.bot.remove_listener(self.red_cog_added, "on_cog_add")
self.bot.remove_listener(self.red_command_added, "on_command_add")
self.bot.loop.create_task(self._unload_all_rules())
async def _unload_all_rules(self) -> None:

View File

@@ -289,6 +289,7 @@ class Reports(commands.Cog):
except discord.NotFound:
pass
@commands.Cog.listener()
async def on_raw_reaction_add(self, payload):
"""
oh dear....
@@ -308,6 +309,7 @@ class Reports(commands.Cog):
)
self.tunnel_store.pop(t[0], None)
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
for k, v in self.tunnel_store.items():
topic = _("Re: ticket# {1} in {0.name}").format(*k)

View File

@@ -614,7 +614,7 @@ class Streams(commands.Cog):
chn = self.bot.get_channel(raw_msg["channel"])
if chn is not None:
try:
msg = await chn.get_message(raw_msg["message"])
msg = await chn.fetch_message(raw_msg["message"])
except discord.HTTPException:
pass
else:
@@ -633,8 +633,8 @@ class Streams(commands.Cog):
await self.db.streams.set(raw_streams)
def __unload(self):
def cog_unload(self):
if self.task:
self.task.cancel()
__del__ = __unload
__del__ = cog_unload

View File

@@ -444,6 +444,7 @@ class Trivia(commands.Cog):
break
return "\n".join(lines)
@commands.Cog.listener()
async def on_trivia_end(self, session: TriviaSession):
"""Event for a trivia session ending.
@@ -520,7 +521,7 @@ class Trivia(commands.Cog):
return personal_lists + get_core_lists()
def __unload(self):
def cog_unload(self):
for session in self.trivia_sessions:
session.force_stop()

View File

@@ -380,7 +380,7 @@ class Warnings(commands.Cog):
self.bot.get_all_members(), id=user_warnings[key]["mod"]
)
if mod is None:
mod = await self.bot.get_user_info(user_warnings[key]["mod"])
mod = await self.bot.fetch_user(user_warnings[key]["mod"])
msg += _(
"{num_points} point warning {reason_name} issued by {user} for "
"{description}\n"

View File

@@ -14,7 +14,7 @@ from discord.ext.commands import when_mentioned_or
from . import Config, i18n, commands, errors
from .cog_manager import CogManager
from .help_formatter import Help, help as help_
from .rpc import RPCMixin
from .utils import common_filters
@@ -29,10 +29,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
"""Mixin for the main bot class.
This exists because `Red` inherits from `discord.AutoShardedClient`, which
is something other bot classes (namely selfbots) may not want to have as
a parent class.
Selfbots should inherit from this mixin along with `discord.Client`.
is something other bot classes may not want to have as a parent class.
"""
def __init__(self, *args, cli_flags=None, bot_dir: Path = Path.cwd(), **kwargs):
@@ -118,11 +115,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
self.cog_mgr = CogManager()
super().__init__(*args, formatter=Help(), **kwargs)
self.remove_command("help")
self.add_command(help_)
super().__init__(*args, help_command=commands.DefaultHelpCommand(), **kwargs)
self._permissions_hooks: List[commands.CheckPredicate] = []
@@ -216,6 +209,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
curr_pkgs.remove(pkg_name)
async def load_extension(self, spec: ModuleSpec):
# NB: this completely bypasses `discord.ext.commands.Bot._load_from_module_spec`
name = spec.name.split(".")[-1]
if name in self.extensions:
raise errors.PackageAlreadyLoaded(spec)
@@ -225,12 +219,17 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
del lib
raise discord.ClientException(f"extension {name} does not have a setup function")
if asyncio.iscoroutinefunction(lib.setup):
await lib.setup(self)
try:
if asyncio.iscoroutinefunction(lib.setup):
await lib.setup(self)
else:
lib.setup(self)
except Exception as e:
self._remove_module_references(lib.__name__)
self._call_module_finalizers(lib, key)
raise errors.ExtensionFailed(key, e) from e
else:
lib.setup(self)
self.extensions[name] = lib
self._BotBase__extensions[name] = lib
def remove_cog(self, cogname: str):
cog = self.get_cog(cogname)
@@ -250,62 +249,6 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
for meth in self.rpc_handlers.pop(cogname.upper(), ()):
self.unregister_rpc_handler(meth)
def unload_extension(self, name):
lib = self.extensions.get(name)
if lib is None:
return
lib_name = lib.__name__ # Thank you
# find all references to the module
# remove the cogs registered from the module
for cogname, cog in self.cogs.copy().items():
if cog.__module__ and _is_submodule(lib_name, cog.__module__):
self.remove_cog(cogname)
# first remove all the commands from the module
for cmd in self.all_commands.copy().values():
if cmd.module and _is_submodule(lib_name, cmd.module):
if isinstance(cmd, discord.ext.commands.GroupMixin):
cmd.recursively_remove_all_commands()
self.remove_command(cmd.name)
# then remove all the listeners from the module
for event_list in self.extra_events.copy().values():
remove = []
for index, event in enumerate(event_list):
if event.__module__ and _is_submodule(lib_name, event.__module__):
remove.append(index)
for index in reversed(remove):
del event_list[index]
try:
func = getattr(lib, "teardown")
except AttributeError:
pass
else:
try:
func(self)
except:
pass
finally:
# finally remove the import..
pkg_name = lib.__package__
del lib
del self.extensions[name]
for module in list(sys.modules):
if _is_submodule(lib_name, module):
del sys.modules[module]
if pkg_name.startswith("redbot.cogs."):
del sys.modules["redbot.cogs"].__dict__[name]
async def is_automod_immune(
self, to_check: Union[discord.Message, commands.Context, discord.abc.User, discord.Role]
) -> bool:
@@ -399,11 +342,9 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
else:
self.add_permissions_hook(hook)
for attr in dir(cog):
_attr = getattr(cog, attr)
if isinstance(_attr, discord.ext.commands.Command) and not isinstance(
_attr, commands.Command
):
for command in cog.__cog_commands__:
if not isinstance(command, commands.Command):
raise RuntimeError(
f"The {cog.__class__.__name__} cog in the {cog.__module__} package,"
" is not using Red's command module, and cannot be added. "
@@ -414,13 +355,8 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
)
super().add_cog(cog)
self.dispatch("cog_add", cog)
def add_command(self, command: commands.Command):
if not isinstance(command, commands.Command):
raise TypeError("Command objects must derive from redbot.core.commands.Command")
super().add_command(command)
self.dispatch("command_add", command)
for command in cog.__cog_commands__:
self.dispatch("command_add", command)
def clear_permission_rules(self, guild_id: Optional[int]) -> None:
"""Clear all permission overrides in a scope.

View File

@@ -4,3 +4,4 @@ from .context import *
from .converter import *
from .errors import *
from .requires import *
from .help import *

View File

@@ -20,6 +20,7 @@ if TYPE_CHECKING:
__all__ = [
"Cog",
"CogMixin",
"CogCommandMixin",
"CogGroupMixin",
"Command",
@@ -241,9 +242,9 @@ class Command(CogCommandMixin, commands.Command):
if result is False:
return False
if self.parent is None and self.instance is not None:
if self.parent is None and self.cog is not None:
# For top-level commands, we need to check the cog's requires too
ret = await self.instance.requires.verify(ctx)
ret = await self.cog.requires.verify(ctx)
if ret is False:
return False
@@ -374,8 +375,8 @@ class Command(CogCommandMixin, commands.Command):
def allow_for(self, model_id: Union[int, str], guild_id: int) -> None:
super().allow_for(model_id, guild_id=guild_id)
parents = self.parents
if self.instance is not None:
parents.append(self.instance)
if self.cog is not None:
parents.append(self.cog)
for parent in parents:
cur_rule = parent.requires.get_rule(model_id, guild_id=guild_id)
if cur_rule is PermState.NORMAL:
@@ -389,8 +390,8 @@ class Command(CogCommandMixin, commands.Command):
old_rule, new_rule = super().clear_rule_for(model_id, guild_id=guild_id)
if old_rule is PermState.ACTIVE_ALLOW:
parents = self.parents
if self.instance is not None:
parents.append(self.instance)
if self.cog is not None:
parents.append(self.cog)
for parent in parents:
should_continue = parent.reevaluate_rules_for(model_id, guild_id=guild_id)[1]
if not should_continue:
@@ -445,10 +446,11 @@ class GroupMixin(discord.ext.commands.GroupMixin):
def command(self, *args, **kwargs):
"""A shortcut decorator that invokes :func:`.command` and adds it to
the internal command list.
the internal command list via :meth:`~.GroupMixin.add_command`.
"""
def decorator(func):
kwargs.setdefault("parent", self)
result = command(*args, **kwargs)(func)
self.add_command(result)
return result
@@ -457,10 +459,11 @@ class GroupMixin(discord.ext.commands.GroupMixin):
def group(self, *args, **kwargs):
"""A shortcut decorator that invokes :func:`.group` and adds it to
the internal command list.
the internal command list via :meth:`~.GroupMixin.add_command`.
"""
def decorator(func):
kwargs.setdefault("parent", self)
result = group(*args, **kwargs)(func)
self.add_command(result)
return result
@@ -551,12 +554,24 @@ class Group(GroupMixin, Command, CogGroupMixin, commands.Group):
await super().invoke(ctx)
class Cog(CogCommandMixin, CogGroupMixin):
"""Base class for a cog."""
class CogMixin(CogGroupMixin, CogCommandMixin):
"""Mixin class for a cog, intended for use with discord.py's cog class"""
@property
def all_commands(self) -> Dict[str, Command]:
return {cmd.name: cmd for cmd in self.__dict__.values() if isinstance(cmd, Command)}
return {cmd.name: cmd for cmd in self.__cog_commands__}
class Cog(CogMixin, commands.Cog):
"""
Red's Cog base class
This includes a metaclass from discord.py
"""
# NB: Do not move the inheritcance of this. Keeping the mix of that metaclass
# seperate gives us more freedoms in several places.
pass
def command(name=None, cls=Command, **attrs):

View File

@@ -63,44 +63,9 @@ class Context(commands.Context):
return await super().send(content=content, **kwargs)
async def send_help(self) -> List[discord.Message]:
"""Send the command help message.
Returns
-------
`list` of `discord.Message`
A list of help messages which were sent to the user.
"""
""" Send the command help message. """
command = self.invoked_subcommand or self.command
embed_wanted = await self.bot.embed_requested(
self.channel, self.author, command=self.bot.get_command("help")
)
if self.guild and not self.channel.permissions_for(self.guild.me).embed_links:
embed_wanted = False
ret = []
destination = self
if embed_wanted:
embeds = await self.bot.formatter.format_help_for(self, command)
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)
else:
f = commands.HelpFormatter()
msgs = await f.format_help_for(self, command)
for msg in msgs:
try:
m = await destination.send(msg)
except discord.HTTPException:
destination = self.author
m = await destination.send(msg)
ret.append(m)
return ret
await super().send_help(command)
async def tick(self) -> bool:
"""Add a tick reaction to the command message.

View File

@@ -0,0 +1,23 @@
from discord.ext import commands
from .commands import Command
__all__ = ["HelpCommand", "DefaultHelpCommand", "MinimalHelpCommand"]
class _HelpCommandImpl(Command, commands.help._HelpCommandImpl):
pass
class HelpCommand(commands.help.HelpCommand):
def _add_to_bot(self, bot):
command = _HelpCommandImpl(self, self.command_callback, **self.command_attrs)
bot.add_command(command)
self._command_impl = command
class DefaultHelpCommand(HelpCommand, commands.help.DefaultHelpCommand):
pass
class MinimalHelpCommand(HelpCommand, commands.help.MinimalHelpCommand):
pass

View File

@@ -119,7 +119,7 @@ def init_events(bot, cli_flags):
"Outdated version! {} is available "
"but you're using {}".format(data["info"]["version"], red_version)
)
owner = await bot.get_user_info(bot.owner_id)
owner = await bot.fetch_user(bot.owner_id)
await owner.send(
"Your Red instance is out of date! {} is the current "
"version, however you are using {}!".format(
@@ -168,8 +168,9 @@ def init_events(bot, cli_flags):
if hasattr(ctx.command, "on_error"):
return
if ctx.cog and hasattr(ctx.cog, f"_{ctx.cog.__class__.__name__}__error"):
return
if ctx.cog:
if commands.Cog._get_overridden_method(ctx.cog.cog_command_error) is not None:
return
if isinstance(error, commands.MissingRequiredArgument):
await ctx.send_help()

View File

@@ -1,403 +0,0 @@
"""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
This help formatter contains work by Rapptz (Danny) and SirThane#1780.
"""
import contextlib
from collections import namedtuple
from typing import List, Optional, Union
import discord
from discord.ext.commands import formatter as dpy_formatter
import inspect
import itertools
import re
from . import commands
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"
_mentions_transforms = {"@everyone": "@\u200beveryone", "@here": "@\u200bhere"}
_mention_pattern = re.compile("|".join(_mentions_transforms.keys()))
EmbedField = namedtuple("EmbedField", "name value inline")
class Help(dpy_formatter.HelpFormatter):
"""Formats help for commands."""
def __init__(self, *args, **kwargs):
self.context = None
self.command = None
super().__init__(*args, **kwargs)
@staticmethod
def pm_check(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")
async def color(self):
if self.pm_check(self.context):
return self.context.bot.color
else:
return await self.context.embed_colour()
colour = 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.context.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. "
"You can also type {0}help <category> for more info on a category.".format(
self.context.clean_prefix
)
)
async def format(self) -> dict:
"""Formats command for output.
Returns a dict used to build embed"""
emb = {"embed": {"title": "", "description": ""}, "footer": {"text": ""}, "fields": []}
if self.is_cog():
translator = getattr(self.command, "__translator__", lambda s: s)
description = (
inspect.cleandoc(translator(self.command.__doc__))
if self.command.__doc__
else EMPTY_STRING
)
else:
description = self.command.description
if not description == "" and description is not None:
description = "*{0}*".format(description)
if description:
# <description> portion
emb["embed"]["description"] = description[:2046]
tagline = await self.context.bot.db.help.tagline()
if tagline:
footer = tagline
else:
footer = self.get_ending_note()
emb["footer"]["text"] = footer
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:
splitted = self.command.help.split("\n\n")
name = "__{0}__".format(splitted[0])
value = "\n\n".join(splitted[1:]).replace("[p]", self.context.clean_prefix)
if value == "":
value = EMPTY_STRING
field = EmbedField(name[:252], value[:1024], 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:
for i, page in enumerate(
pagify(self._add_subcommands(commands_), page_length=1000)
):
title = category if i < 1 else f"{category} (continued)"
field = EmbedField(title, page, False)
emb["fields"].append(field)
else:
# Get list of commands for category
filtered = sorted(filtered)
if filtered:
for i, page in enumerate(
pagify(self._add_subcommands(filtered), page_length=1000)
):
title = (
"**__Commands:__**"
if not self.is_bot() and self.is_cog()
else "**__Subcommands:__**"
)
if i > 0:
title += " (continued)"
field = EmbedField(title, page, False)
emb["fields"].append(field)
return emb
@staticmethod
def group_fields(fields: List[EmbedField], max_chars=1000):
curr_group = []
ret = []
for f in fields:
if sum(len(f2.value) for f2 in curr_group) + len(f.value) > max_chars and curr_group:
ret.append(curr_group)
curr_group = []
curr_group.append(f)
if len(curr_group) > 0:
ret.append(curr_group)
return ret
async def format_help_for(self, ctx, command_or_bot, reason: str = ""):
"""Formats the help page and handles the actual heavy lifting of how
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
# We want the permission state to be set as if the author had run the command he is
# requesting help for. This is so the subcommands shown in the help menu correctly reflect
# any permission rules set.
if isinstance(self.command, commands.Command):
with contextlib.suppress(commands.CommandError):
await self.command.can_run(
self.context, check_all_parents=True, change_permission_state=True
)
elif isinstance(self.command, commands.Cog):
with contextlib.suppress(commands.CommandError):
# Cog's don't have a `can_run` method, so we use the `Requires` object directly.
await self.command.requires.verify(self.context)
emb = await self.format()
if reason:
emb["embed"]["title"] = reason
ret = []
page_char_limit = await ctx.bot.db.help.page_char_limit()
field_groups = self.group_fields(emb["fields"], page_char_limit)
for i, group in enumerate(field_groups, 1):
embed = discord.Embed(color=await 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
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
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()},
)
@commands.command(hidden=True)
async def help(ctx: commands.Context, *, command_name: str = ""):
"""Show help documentation.
- `[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()
if use_embeds:
formatter = bot.formatter
else:
formatter = dpy_formatter.HelpFormatter()
if not command_name:
# help by itself just lists our own commands.
pages = await formatter.format_help_for(ctx, bot)
else:
# First check if it's a cog
command = bot.get_cog(command_name)
if command is None:
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:
pages = await formatter.format_help_for(ctx, command)
max_pages_in_guild = await ctx.bot.db.help.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 page in pages:
if isinstance(page, discord.Embed):
await destination.send(embed=page)
else:
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."
)
)
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

View File

@@ -249,10 +249,10 @@ class Case:
guild = mod_channel.guild
if data["message"]:
try:
message = await mod_channel.get_message(data["message"])
message = await mod_channel.fetch_message(data["message"])
except discord.NotFound:
message = None
user = await bot.get_user_info(data["user"])
user = await bot.fetch_user(data["user"])
moderator = guild.get_member(data["moderator"])
channel = guild.get_channel(data["channel"])
amended_by = guild.get_member(data["amended_by"])
@@ -489,7 +489,7 @@ async def get_cases_for_member(
if not member:
member = guild.get_member(member_id)
if not member:
member = await bot.get_user_info(member_id)
member = await bot.fetch_user(member_id)
try:
mod_channel = await get_modlog_channel(guild)
@@ -501,7 +501,7 @@ async def get_cases_for_member(
message = None
if data["message"] and mod_channel:
try:
message = await mod_channel.get_message(data["message"])
message = await mod_channel.fetch_message(data["message"])
except discord.NotFound:
pass

View File

@@ -176,11 +176,9 @@ def red(config_fr):
Config.get_core_conf = lambda *args, **kwargs: config_fr
red = Red(cli_flags=cli_flags, description=description, pm_help=None)
red = Red(cli_flags=cli_flags, description=description, dm_help=None)
yield red
red.http._session.detach()
# endregion