From 6022c0f7d71b18209fbcf3b1306ce1d2cfee7aea Mon Sep 17 00:00:00 2001 From: zephyrkul Date: Thu, 11 Oct 2018 22:47:39 -0600 Subject: [PATCH 01/11] [Mod] Mute/unmute bugfixes (#2230) - Helper methods mute_user and unmute_user now take GuildChannel instead of solely TextChannel. - The bot will not attempt to mute a member with the Administrator permission, as that permission bypasses channel overwrites anyway. The bot will still unmute members with the Administrator permission (see #2076). - Audit reasons are now specified for mass mutes / unmutes. - A few typos and missing keyword specifiers were corrected. - Streamlined some logic and used some already-existing functions. --- redbot/cogs/mod/mod.py | 221 ++++++++++++++++++----------------------- 1 file changed, 96 insertions(+), 125 deletions(-) diff --git a/redbot/cogs/mod/mod.py b/redbot/cogs/mod/mod.py index 31f105af3..081b1dcec 100644 --- a/redbot/cogs/mod/mod.py +++ b/redbot/cogs/mod/mod.py @@ -893,34 +893,33 @@ class Mod(commands.Cog): author = ctx.author if user_voice_state: channel = user_voice_state.channel - if channel and channel.permissions_for(user).speak: - overwrites = channel.overwrites_for(user) - overwrites.speak = False - audit_reason = get_audit_reason(ctx.author, reason) - await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason) - await ctx.send( - _("Muted {user} in channel {channel.name}").format(user, channel=channel) - ) - try: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at, - "boicemute", - user, - author, - reason, - until=None, - channel=channel, + if channel: + audit_reason = get_audit_reason(author, reason) + + success, issue = await self.mute_user(guild, channel, author, user, audit_reason) + + if success: + await ctx.send( + _("Muted {user} in channel {channel.name}").format( + user=user, channel=channel + ) ) - except RuntimeError as e: - await ctx.send(e) - return - elif channel.permissions_for(user).speak is False: - await ctx.send( - _("That user is already muted in {channel}!").format(channel=channel.name) - ) - return + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "vmute", + user, + author, + reason, + until=None, + channel=channel, + ) + except RuntimeError as e: + await ctx.send(e) + else: + await channel.send(issue) else: await ctx.send(_("That user is not in a voice channel right now!")) else: @@ -938,13 +937,7 @@ class Mod(commands.Cog): author = ctx.message.author channel = ctx.message.channel guild = ctx.guild - - if reason is None: - audit_reason = "Channel mute requested by {a} (ID {a.id})".format(a=author) - else: - audit_reason = "Channel mute requested by {a} (ID {a.id}). Reason: {r}".format( - a=author, r=reason - ) + audit_reason = get_audit_reason(author, reason) success, issue = await self.mute_user(guild, channel, author, user, audit_reason) @@ -975,26 +968,12 @@ class Mod(commands.Cog): """Mutes user in the server""" author = ctx.message.author guild = ctx.guild - if reason is None: - audit_reason = "server mute requested by {author} (ID {author.id})".format( - author=author - ) - else: - audit_reason = ( - "server mute requested by {author} (ID {author.id}). Reason: {reason}" - ).format(author=author, reason=reason) + audit_reason = get_audit_reason(author, reason) mute_success = [] for channel in guild.channels: - if not isinstance(channel, discord.TextChannel): - if channel.permissions_for(user).speak: - overwrites = channel.overwrites_for(user) - overwrites.speak = False - audit_reason = get_audit_reason(ctx.author, reason) - await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason) - else: - success, issue = await self.mute_user(guild, channel, author, user, audit_reason) - mute_success.append((success, issue)) + success, issue = await self.mute_user(guild, channel, author, user, audit_reason) + mute_success.append((success, issue)) await asyncio.sleep(0.1) await ctx.send(_("User has been muted in this server.")) try: @@ -1015,7 +994,7 @@ class Mod(commands.Cog): async def mute_user( self, guild: discord.Guild, - channel: discord.TextChannel, + channel: discord.abc.GuildChannel, author: discord.Member, user: discord.Member, reason: str, @@ -1023,25 +1002,32 @@ class Mod(commands.Cog): """Mutes the specified user in the specified channel""" overwrites = channel.overwrites_for(user) permissions = channel.permissions_for(user) - perms_cache = await self.settings.member(user).perms_cache() - if overwrites.send_messages is False or permissions.send_messages is False: + if permissions.administrator: + return False, T_(mute_unmute_issues["is_admin"]) + + new_overs = {} + if not isinstance(channel, discord.TextChannel): + new_overs.update(speak=False) + if not isinstance(channel, discord.VoiceChannel): + new_overs.update(send_messages=False, add_reactions=False) + + if all(getattr(permissions, p) is False for p in new_overs.keys()): return False, T_(mute_unmute_issues["already_muted"]) elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user): return False, T_(mute_unmute_issues["hierarchy_problem"]) - perms_cache[str(channel.id)] = { - "send_messages": overwrites.send_messages, - "add_reactions": overwrites.add_reactions, - } - overwrites.update(send_messages=False, add_reactions=False) + old_overs = {k: getattr(overwrites, k) for k in new_overs} + overwrites.update(**new_overs) try: await channel.set_permissions(user, overwrite=overwrites, reason=reason) except discord.Forbidden: return False, T_(mute_unmute_issues["permissions_issue"]) else: - await self.settings.member(user).perms_cache.set(perms_cache) + await self.settings.member(user).set_raw( + "perms_cache", str(channel.id), value=old_overs + ) return True, None @commands.group() @@ -1061,37 +1047,39 @@ class Mod(commands.Cog): ): """Unmute a user in their current voice channel.""" user_voice_state = user.voice + guild = ctx.guild + author = ctx.author if user_voice_state: channel = user_voice_state.channel - if channel and channel.permissions_for(user).speak is False: - overwrites = channel.overwrites_for(user) - overwrites.speak = None - audit_reason = get_audit_reason(ctx.author, reason) - await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason) - author = ctx.author - guild = ctx.guild - await ctx.send( - _("Unmuted {}#{} in channel {}").format( - user.name, user.discriminator, channel.name - ) + if channel: + audit_reason = get_audit_reason(author, reason) + + success, message = await self.unmute_user( + guild, channel, author, user, audit_reason ) - try: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at, - "voiceunmute", - user, - author, - reason, - until=None, - channel=channel, + + if success: + await ctx.send( + _("Unmuted {user} in channel {channel.name}").format( + user=user, channel=channel + ) ) - except RuntimeError as e: - await ctx.send(e) - elif channel.permissions_for(user).speak: - await ctx.send(_("That user is already unmuted in {}!").format(channel.name)) - return + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "vunmute", + user, + author, + reason, + until=None, + channel=channel, + ) + except RuntimeError as e: + await ctx.send(e) + else: + await ctx.send(_("Unmute failed. Reason: {}").format(message)) else: await ctx.send(_("That user is not in a voice channel right now!")) else: @@ -1109,8 +1097,9 @@ class Mod(commands.Cog): channel = ctx.channel author = ctx.author guild = ctx.guild + audit_reason = get_audit_reason(author, reason) - success, message = await self.unmute_user(guild, channel, author, user) + success, message = await self.unmute_user(guild, channel, author, user, audit_reason) if success: await ctx.send(_("User unmuted in this channel.")) @@ -1141,16 +1130,11 @@ class Mod(commands.Cog): """Unmute a user in this server.""" guild = ctx.guild author = ctx.author + audit_reason = get_audit_reason(author, reason) unmute_success = [] for channel in guild.channels: - if not isinstance(channel, discord.TextChannel): - if channel.permissions_for(user).speak is False: - overwrites = channel.overwrites_for(user) - overwrites.speak = None - audit_reason = get_audit_reason(author, reason) - await channel.set_permissions(user, overwrite=overwrites, reason=audit_reason) - success, message = await self.unmute_user(guild, channel, author, user) + success, message = await self.unmute_user(guild, channel, author, user, audit_reason) unmute_success.append((success, message)) await asyncio.sleep(0.1) await ctx.send(_("User has been unmuted in this server.")) @@ -1171,45 +1155,37 @@ class Mod(commands.Cog): async def unmute_user( self, guild: discord.Guild, - channel: discord.TextChannel, + channel: discord.abc.GuildChannel, author: discord.Member, user: discord.Member, + reason: str, ) -> (bool, str): overwrites = channel.overwrites_for(user) - permissions = channel.permissions_for(user) perms_cache = await self.settings.member(user).perms_cache() - if overwrites.send_messages or permissions.send_messages: + if channel.id in perms_cache: + old_values = perms_cache[channel.id] + else: + old_values = {"send_messages": None, "add_reactions": None, "speak": None} + + if all(getattr(overwrites, k) == v for k, v in old_values.items()): return False, T_(mute_unmute_issues["already_unmuted"]) elif not await is_allowed_by_hierarchy(self.bot, self.settings, guild, author, user): return False, T_(mute_unmute_issues["hierarchy_problem"]) - if channel.id in perms_cache: - old_values = perms_cache[channel.id] - else: - old_values = {"send_messages": None, "add_reactions": None} - overwrites.update( - send_messages=old_values["send_messages"], add_reactions=old_values["add_reactions"] - ) - is_empty = self.are_overwrites_empty(overwrites) - + overwrites.update(**old_values) try: - if not is_empty: - await channel.set_permissions(user, overwrite=overwrites) - else: + if overwrites.is_empty(): await channel.set_permissions( - user, overwrite=cast(discord.PermissionOverwrite, None) + user, overwrite=cast(discord.PermissionOverwrite, None), reason=reason ) + else: + await channel.set_permissions(user, overwrite=overwrites, reason=reason) except discord.Forbidden: return False, T_(mute_unmute_issues["permissions_issue"]) else: - try: - del perms_cache[channel.id] - except KeyError: - pass - else: - await self.settings.member(user).perms_cache.set(perms_cache) + await self.settings.member(user).clear_raw("perms_cache", str(channel.id)) return True, None @commands.group() @@ -1695,20 +1671,15 @@ class Mod(commands.Cog): while len(nick_list) > 20: nick_list.pop(0) - @staticmethod - def are_overwrites_empty(overwrites): - """There is currently no cleaner way to check if a - PermissionOverwrite object is empty""" - return [p for p in iter(overwrites)] == [p for p in iter(discord.PermissionOverwrite())] - _ = lambda s: s mute_unmute_issues = { "already_muted": _("That user can't send messages in this channel."), - "already_unmuted": _("That user isn't muted in this channel!"), + "already_unmuted": _("That user isn't muted in this channel."), "hierarchy_problem": _( - "I cannot let you do that. You are not higher than " "the user in the role hierarchy." + "I cannot let you do that. You are not higher than the user in the role hierarchy." ), + "is_admin": _("That user cannot be muted, as they have the Administrator permission."), "permissions_issue": _( "Failed to mute user. I need the manage roles " "permission and the user I'm muting must be " From 7cd765d54838a2215c3b24c041a6b54bdbd6b32d Mon Sep 17 00:00:00 2001 From: Toby Harradine Date: Sun, 14 Oct 2018 11:52:39 +1100 Subject: [PATCH 02/11] Fix permissions hook removal (#2234) Some in-progress work slipped through #2149, and I figure it should be fixed before RC2. I've also just decided to allow discovery of permissions hooks from superclasses as well. We should try to be more aware of the possibility of cog superclasses moving forward. Signed-off-by: Toby Harradine --- redbot/core/bot.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/redbot/core/bot.py b/redbot/core/bot.py index 607a61b27..d09a87a5a 100644 --- a/redbot/core/bot.py +++ b/redbot/core/bot.py @@ -1,4 +1,5 @@ import asyncio +import inspect import os import logging from collections import Counter @@ -236,20 +237,13 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): if cog is None: return - for when in ("before", "after"): + for cls in inspect.getmro(cog.__class__): try: - hook = getattr(cog, f"_{cog.__class__.__name__}__red_permissions_{when}") + hook = getattr(cog, f"_{cls.__name__}__permissions_hook") except AttributeError: pass else: - self.remove_permissions_hook(hook, when) - - try: - hook = getattr(cog, f"_{cog.__class__.__name__}__red_permissions_before") - except AttributeError: - pass - else: - self.remove_permissions_hook(hook) + self.remove_permissions_hook(hook) super().remove_cog(cogname) @@ -390,10 +384,17 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): ) if not hasattr(cog, "requires"): commands.Cog.__init__(cog) + + for cls in inspect.getmro(cog.__class__): + try: + hook = getattr(cog, f"_{cls.__name__}__permissions_hook") + except AttributeError: + pass + else: + self.add_permissions_hook(hook) + for attr in dir(cog): _attr = getattr(cog, attr) - if attr == f"_{cog.__class__.__name__}__permissions_hook": - self.add_permissions_hook(_attr) if isinstance(_attr, discord.ext.commands.Command) and not isinstance( _attr, commands.Command ): From 9588a5740c4d2780da51878a5113c2d9ec1b4fae Mon Sep 17 00:00:00 2001 From: Christopher Rice Date: Sun, 14 Oct 2018 02:09:54 -0400 Subject: [PATCH 03/11] [Downloader] Define Translator in converters module (#2239) Fixes #2236 --- redbot/cogs/downloader/converters.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/redbot/cogs/downloader/converters.py b/redbot/cogs/downloader/converters.py index f2a64ddc5..54f7522cd 100644 --- a/redbot/cogs/downloader/converters.py +++ b/redbot/cogs/downloader/converters.py @@ -1,7 +1,10 @@ import discord from redbot.core import commands +from redbot.core.i18n import Translator from .installable import Installable +_ = Translator("Koala", __file__) + class InstalledCog(Installable): @classmethod From 1ba922eba22f0ce4e2bf4145e7660ab7f40e6e6c Mon Sep 17 00:00:00 2001 From: Christopher Rice Date: Sun, 14 Oct 2018 02:11:16 -0400 Subject: [PATCH 04/11] [Downloader] Add missing `prefix` format kwarg (#2238) Fixes #2237. --- redbot/cogs/downloader/downloader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redbot/cogs/downloader/downloader.py b/redbot/cogs/downloader/downloader.py index f9df14c99..4019a01f8 100644 --- a/redbot/cogs/downloader/downloader.py +++ b/redbot/cogs/downloader/downloader.py @@ -344,7 +344,7 @@ class Downloader(commands.Cog): " files manually if it is still usable." " Also make sure you've unloaded the cog" " with `{prefix}unload {cog_name}`." - ).format(cog_name=real_name) + ).format(prefix=ctx.prefix, cog_name=real_name) ) @cog.command(name="update") From ad51fa830b45812fc7f5c20c0100c2c42e796d49 Mon Sep 17 00:00:00 2001 From: Toby Harradine Date: Mon, 15 Oct 2018 22:29:07 +1100 Subject: [PATCH 05/11] [Cleanup] [p]cleanup bot includes aliases and CCs (#2213) Resolves #1920. Signed-off-by: Toby Harradine --- redbot/cogs/cleanup/cleanup.py | 26 +++++++++++++++++++++++--- redbot/cogs/customcom/customcom.py | 13 ++++++++++++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/redbot/cogs/cleanup/cleanup.py b/redbot/cogs/cleanup/cleanup.py index 8c61dc172..5b941f3c5 100644 --- a/redbot/cogs/cleanup/cleanup.py +++ b/redbot/cogs/cleanup/cleanup.py @@ -1,6 +1,6 @@ import re from datetime import datetime, timedelta -from typing import Union, List, Callable +from typing import Union, List, Callable, Set import discord @@ -323,15 +323,35 @@ class Cleanup(commands.Cog): if "" in prefixes: prefixes.remove("") + cc_cog = self.bot.get_cog("CustomCommands") + if cc_cog is not None: + command_names: Set[str] = await cc_cog.get_command_names(ctx.guild) + is_cc = lambda name: name in command_names + else: + is_cc = lambda name: False + alias_cog = self.bot.get_cog("Alias") + if alias_cog is not None: + alias_names: Set[str] = ( + set((a.name for a in await alias_cog.unloaded_global_aliases())) + | set(a.name for a in await alias_cog.unloaded_aliases(ctx.guild)) + ) + is_alias = lambda name: name in alias_names + else: + is_alias = lambda name: False + + bot_id = self.bot.user.id + def check(m): - if m.author.id == self.bot.user.id: + if m.author.id == bot_id: return True elif m == ctx.message: return True p = discord.utils.find(m.content.startswith, prefixes) if p and len(p) > 0: cmd_name = m.content[len(p) :].split(" ")[0] - return bool(self.bot.get_command(cmd_name)) + return ( + bool(self.bot.get_command(cmd_name)) or is_alias(cmd_name) or is_cc(cmd_name) + ) return False to_delete = await self.get_messages_for_deletion( diff --git a/redbot/cogs/customcom/customcom.py b/redbot/cogs/customcom/customcom.py index 095755de2..acaa9164e 100644 --- a/redbot/cogs/customcom/customcom.py +++ b/redbot/cogs/customcom/customcom.py @@ -3,7 +3,7 @@ import random from datetime import datetime, timedelta from inspect import Parameter from collections import OrderedDict -from typing import Mapping, Tuple, Dict +from typing import Mapping, Tuple, Dict, Set import discord @@ -553,3 +553,14 @@ class CustomCommands(commands.Cog): else: return raw_result return str(getattr(first, second, raw_result)) + + async def get_command_names(self, guild: discord.Guild) -> Set[str]: + """Get all custom command names in a guild. + + Returns + -------- + Set[str] + A set of all custom command names. + + """ + return set(await CommandObj.get_commands(self.config.guild(guild))) From 5ba95090d9fe0ce3ea1189473ffc956af303788c Mon Sep 17 00:00:00 2001 From: Toby Harradine Date: Mon, 15 Oct 2018 22:31:14 +1100 Subject: [PATCH 06/11] [Streams] Suppress HTTPExceptions on load (#2228) Resolves #2227. Signed-off-by: Toby Harradine --- redbot/cogs/streams/streams.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/redbot/cogs/streams/streams.py b/redbot/cogs/streams/streams.py index 901ca6803..9665b4755 100644 --- a/redbot/cogs/streams/streams.py +++ b/redbot/cogs/streams/streams.py @@ -626,8 +626,13 @@ class Streams(commands.Cog): raw_stream["_messages_cache"] = [] for raw_msg in raw_msg_cache: chn = self.bot.get_channel(raw_msg["channel"]) - msg = await chn.get_message(raw_msg["message"]) - raw_stream["_messages_cache"].append(msg) + if chn is not None: + try: + msg = await chn.get_message(raw_msg["message"]) + except discord.HTTPException: + pass + else: + raw_stream["_messages_cache"].append(msg) token = await self.db.tokens.get_raw(_class.__name__, default=None) if token is not None: raw_stream["token"] = token @@ -646,8 +651,13 @@ class Streams(commands.Cog): raw_community["_messages_cache"] = [] for raw_msg in raw_msg_cache: chn = self.bot.get_channel(raw_msg["channel"]) - msg = await chn.get_message(raw_msg["message"]) - raw_community["_messages_cache"].append(msg) + if chn is not None: + try: + msg = await chn.get_message(raw_msg["message"]) + except discord.HTTPException: + pass + else: + raw_community["_messages_cache"].append(msg) token = await self.db.tokens.get_raw(_class.__name__, default=None) communities.append(_class(token=token, **raw_community)) From c510ebe5e57562d4ad9bb54934cdf7654129b2f8 Mon Sep 17 00:00:00 2001 From: Michael H Date: Mon, 15 Oct 2018 08:29:56 -0400 Subject: [PATCH 07/11] [Downloader] Only prompt reload of loaded cogs (#2233) --- redbot/cogs/downloader/downloader.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/redbot/cogs/downloader/downloader.py b/redbot/cogs/downloader/downloader.py index 4019a01f8..416daad78 100644 --- a/redbot/cogs/downloader/downloader.py +++ b/redbot/cogs/downloader/downloader.py @@ -372,13 +372,18 @@ class Downloader(commands.Cog): await self._reinstall_libraries(installed_and_updated) message = _("Cog update completed successfully.") - cognames = [c.name for c in installed_and_updated] + cognames = {c.name for c in installed_and_updated} message += _("\nUpdated: ") + humanize_list(tuple(map(inline, cognames))) else: await ctx.send(_("All installed cogs are already up to date.")) return await ctx.send(message) + cognames &= set(ctx.bot.extensions.keys()) # only reload loaded cogs + if not cognames: + return await ctx.send( + _("None of the updated cogs were previously loaded. Update complete.") + ) message = _("Would you like to reload the updated cogs?") can_react = ctx.channel.permissions_for(ctx.me).add_reactions if not can_react: @@ -402,7 +407,6 @@ class Downloader(commands.Cog): if can_react: with contextlib.suppress(discord.Forbidden): await query.clear_reactions() - await ctx.invoke(ctx.bot.get_cog("Core").reload, *cognames) else: if can_react: From b5fd28ef7c9289a0b59e213790cc1d091425503e Mon Sep 17 00:00:00 2001 From: Toby Harradine Date: Tue, 16 Oct 2018 08:39:44 +1100 Subject: [PATCH 08/11] [CustomCom] Better display for [p]cc list (#2215) Uses a menu, optionally embedded with respect to the embed settings, for scrolling through the custom command list, each cc with a ~50 character preview. Format is purposefully similar to the help menu. Resolves #2104. Signed-off-by: Toby Harradine --- redbot/cogs/customcom/customcom.py | 52 +++++++++++++++++++++--------- redbot/cogs/mod/mod.py | 2 +- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/redbot/cogs/customcom/customcom.py b/redbot/cogs/customcom/customcom.py index acaa9164e..59990d342 100644 --- a/redbot/cogs/customcom/customcom.py +++ b/redbot/cogs/customcom/customcom.py @@ -8,8 +8,9 @@ from typing import Mapping, Tuple, Dict, Set import discord from redbot.core import Config, checks, commands -from redbot.core.utils.chat_formatting import box, pagify from redbot.core.i18n import Translator, cog_i18n +from redbot.core.utils import menus +from redbot.core.utils.chat_formatting import box, pagify, escape from redbot.core.utils.predicates import MessagePredicate _ = Translator("CustomCommands", __file__) @@ -114,7 +115,7 @@ class CommandObj: *, response=None, cooldowns: Mapping[str, int] = None, - ask_for: bool = True + ask_for: bool = True, ): """Edit an already existing custom command""" ccinfo = await self.db(ctx.guild).commands.get_raw(command, default=None) @@ -323,12 +324,16 @@ class CustomCommands(commands.Cog): await ctx.send(e.args[0]) @customcom.command(name="list") - async def cc_list(self, ctx): - """List all available custom commands.""" + @checks.bot_has_permissions(add_reactions=True) + async def cc_list(self, ctx: commands.Context): + """List all available custom commands. - response = await CommandObj.get_commands(self.config.guild(ctx.guild)) + The list displays a preview of each command's response, with + markdown escaped and newlines replaced with spaces. + """ + cc_dict = await CommandObj.get_commands(self.config.guild(ctx.guild)) - if not response: + if not cc_dict: await ctx.send( _( "There are no custom commands in this server." @@ -338,8 +343,7 @@ class CustomCommands(commands.Cog): return results = [] - - for command, body in response.items(): + for command, body in sorted(cc_dict.items(), key=lambda t: t[0]): responses = body["response"] if isinstance(responses, list): result = ", ".join(responses) @@ -347,15 +351,33 @@ class CustomCommands(commands.Cog): result = responses else: continue - results.append("{command:<15} : {result}".format(command=command, result=result)) + # Replace newlines with spaces + # Cut preview to 52 characters max + if len(result) > 52: + result = result[:49] + "..." + # Replace newlines with spaces + result = result.replace("\n", " ") + # Escape markdown and mass mentions + result = escape(result, formatting=True, mass_mentions=True) + results.append((f"{ctx.clean_prefix}{command}", result)) - _commands = "\n".join(results) - - if len(_commands) < 1500: - await ctx.send(box(_commands)) + if await ctx.embed_requested(): + content = "\n".join(map("**{0[0]}** {0[1]}".format, results)) + pages = list(pagify(content, page_length=1024)) + embed_pages = [] + for idx, page in enumerate(pages, start=1): + embed = discord.Embed( + title=_("Custom Command List"), + description=page, + colour=await ctx.embed_colour(), + ) + embed.set_footer(text=_("Page {num}/{total}").format(num=idx, total=len(pages))) + embed_pages.append(embed) + await menus.menu(ctx, embed_pages, menus.DEFAULT_CONTROLS) else: - for page in pagify(_commands, delims=[" ", "\n"]): - await ctx.author.send(box(page)) + content = "\n".join(map("{0[0]:<12} : {0[1]}".format, results)) + pages = list(map(box, pagify(content, page_length=2000, shorten_by=10))) + await menus.menu(ctx, pages, menus.DEFAULT_CONTROLS) async def on_message(self, message): is_private = isinstance(message.channel, discord.abc.PrivateChannel) diff --git a/redbot/cogs/mod/mod.py b/redbot/cogs/mod/mod.py index 081b1dcec..c2b0211a6 100644 --- a/redbot/cogs/mod/mod.py +++ b/redbot/cogs/mod/mod.py @@ -825,7 +825,7 @@ class Mod(commands.Cog): @admin_or_voice_permissions(mute_members=True, deafen_members=True) @bot_has_voice_permissions(mute_members=True, deafen_members=True) async def voiceunban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): - """Unban a the user from speaking and listening in the server's voice channels.""" + """Unban a user from speaking and listening in the server's voice channels.""" user_voice_state = user.voice if user_voice_state is None: await ctx.send(_("No voice state for that user!")) From aff62a8006eed24db2874482e50f22e2b521912d Mon Sep 17 00:00:00 2001 From: Toby Harradine Date: Tue, 16 Oct 2018 09:19:32 +1100 Subject: [PATCH 09/11] [Downloader] Unload extensions on uninstall (#2243) Resolves #2216. Signed-off-by: Toby Harradine --- redbot/cogs/downloader/downloader.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/redbot/cogs/downloader/downloader.py b/redbot/cogs/downloader/downloader.py index 416daad78..ce3df16ae 100644 --- a/redbot/cogs/downloader/downloader.py +++ b/redbot/cogs/downloader/downloader.py @@ -325,13 +325,12 @@ class Downloader(commands.Cog): You may only uninstall cogs which were previously installed by Downloader. """ - # noinspection PyUnresolvedReferences,PyProtectedMember real_name = cog.name poss_installed_path = (await self.cog_install_path()) / real_name if poss_installed_path.exists(): + ctx.bot.unload_extension(real_name) await self._delete_cog(poss_installed_path) - # noinspection PyTypeChecker await self._remove_from_installed(cog) await ctx.send( _("Cog `{cog_name}` was successfully uninstalled.").format(cog_name=real_name) From d2d26835c3a5a38a8ebccfdbe1ce194629191b1d Mon Sep 17 00:00:00 2001 From: Toby Harradine Date: Tue, 16 Oct 2018 09:30:53 +1100 Subject: [PATCH 10/11] [Economy] Detect max balance and prevent OverflowError (#2211) Resolves #2091. This doesn't fix every OverflowError with MongoDB; but at least the seemingly easiest one to achieve with core cogs. Signed-off-by: Toby Harradine --- redbot/cogs/economy/economy.py | 92 ++++++++++++++++++++++++---------- redbot/core/bank.py | 17 ++++++- redbot/core/errors.py | 28 +++++++++++ 3 files changed, 109 insertions(+), 28 deletions(-) diff --git a/redbot/cogs/economy/economy.py b/redbot/cogs/economy/economy.py index 13edd54cf..9e32351d1 100644 --- a/redbot/cogs/economy/economy.py +++ b/redbot/cogs/economy/economy.py @@ -8,7 +8,7 @@ from typing import cast, Iterable import discord from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin -from redbot.core import Config, bank, commands +from redbot.core import Config, bank, commands, errors from redbot.core.i18n import Translator, cog_i18n from redbot.core.utils.chat_formatting import box from redbot.core.utils.menus import menu, DEFAULT_CONTROLS @@ -171,7 +171,7 @@ class Economy(commands.Cog): try: await bank.transfer_credits(from_, to, amount) - except ValueError as e: + except (ValueError, errors.BalanceTooHigh) as e: return await ctx.send(str(e)) await ctx.send( @@ -195,36 +195,35 @@ class Economy(commands.Cog): author = ctx.author currency = await bank.get_currency_name(ctx.guild) - if creds.operation == "deposit": - await bank.deposit_credits(to, creds.sum) - await ctx.send( - _("{author} added {num} {currency} to {user}'s account.").format( + try: + if creds.operation == "deposit": + await bank.deposit_credits(to, creds.sum) + msg = _("{author} added {num} {currency} to {user}'s account.").format( author=author.display_name, num=creds.sum, currency=currency, user=to.display_name, ) - ) - elif creds.operation == "withdraw": - await bank.withdraw_credits(to, creds.sum) - await ctx.send( - _("{author} removed {num} {currency} from {user}'s account.").format( + elif creds.operation == "withdraw": + await bank.withdraw_credits(to, creds.sum) + msg = _("{author} removed {num} {currency} from {user}'s account.").format( author=author.display_name, num=creds.sum, currency=currency, user=to.display_name, ) - ) + else: + await bank.set_balance(to, creds.sum) + msg = _("{author} set {user}'s account balance to {num} {currency}.").format( + author=author.display_name, + num=creds.sum, + currency=currency, + user=to.display_name, + ) + except (ValueError, errors.BalanceTooHigh) as e: + await ctx.send(str(e)) else: - await bank.set_balance(to, creds.sum) - await ctx.send( - _("{author} set {user}'s account balance to {num} {currency}.").format( - author=author.display_name, - num=creds.sum, - currency=currency, - user=to.display_name, - ) - ) + await ctx.send(msg) @_bank.command() @check_global_setting_guildowner() @@ -260,7 +259,18 @@ class Economy(commands.Cog): if await bank.is_global(): # Role payouts will not be used next_payday = await self.config.user(author).next_payday() if cur_time >= next_payday: - await bank.deposit_credits(author, await self.config.PAYDAY_CREDITS()) + try: + await bank.deposit_credits(author, await self.config.PAYDAY_CREDITS()) + except errors.BalanceTooHigh as exc: + await bank.set_balance(author, exc.max_balance) + await ctx.send( + _( + "You've reached the maximum amount of {currency}! (**{balance:,}**) " + "Please spend some more \N{GRIMACING FACE}\n\n" + "You currently have {new_balance} {currency}." + ).format(currency=credits_name, new_balance=exc.max_balance) + ) + return next_payday = cur_time + await self.config.PAYDAY_TIME() await self.config.user(author).next_payday.set(next_payday) @@ -297,14 +307,25 @@ class Economy(commands.Cog): ).PAYDAY_CREDITS() # Nice variable name if role_credits > credit_amount: credit_amount = role_credits - await bank.deposit_credits(author, credit_amount) + try: + await bank.deposit_credits(author, credit_amount) + except errors.BalanceTooHigh as exc: + await bank.set_balance(author, exc.max_balance) + await ctx.send( + _( + "You've reached the maximum amount of {currency}! " + "Please spend some more \N{GRIMACING FACE}\n\n" + "You currently have {new_balance} {currency}." + ).format(currency=credits_name, new_balance=exc.max_balance) + ) + return next_payday = cur_time + await self.config.guild(guild).PAYDAY_TIME() await self.config.member(author).next_payday.set(next_payday) pos = await bank.get_leaderboard_position(author) await ctx.send( _( "{author.mention} Here, take some {currency}. " - "Enjoy! (+{amount} {new_balance}!)\n\n" + "Enjoy! (+{amount} {currency}!)\n\n" "You currently have {new_balance} {currency}.\n\n" "You are currently #{pos} on the global leaderboard!" ).format( @@ -444,7 +465,21 @@ class Economy(commands.Cog): then = await bank.get_balance(author) pay = payout["payout"](bid) now = then - bid + pay - await bank.set_balance(author, now) + try: + await bank.set_balance(author, now) + except errors.BalanceTooHigh as exc: + await bank.set_balance(author, exc.max_balance) + await channel.send( + _( + "You've reached the maximum amount of {currency}! " + "Please spend some more \N{GRIMACING FACE}\n{old_balance} -> {new_balance}!" + ).format( + currency=await bank.get_currency_name(getattr(channel, "guild", None)), + old_balance=then, + new_balance=exc.max_balance, + ) + ) + return phrase = T_(payout["phrase"]) else: then = await bank.get_balance(author) @@ -561,10 +596,10 @@ class Economy(commands.Cog): async def paydayamount(self, ctx: commands.Context, creds: int): """Set the amount earned each payday.""" guild = ctx.guild - credits_name = await bank.get_currency_name(guild) - if creds <= 0: + if creds <= 0 or creds > bank.MAX_BALANCE: await ctx.send(_("Har har so funny.")) return + credits_name = await bank.get_currency_name(guild) if await bank.is_global(): await self.config.PAYDAY_CREDITS.set(creds) else: @@ -579,6 +614,9 @@ class Economy(commands.Cog): async def rolepaydayamount(self, ctx: commands.Context, role: discord.Role, creds: int): """Set the amount earned each payday for a role.""" guild = ctx.guild + if creds <= 0 or creds > bank.MAX_BALANCE: + await ctx.send(_("Har har so funny.")) + return credits_name = await bank.get_currency_name(guild) if await bank.is_global(): await ctx.send(_("The bank must be per-server for per-role paydays to work.")) diff --git a/redbot/core/bank.py b/redbot/core/bank.py index 840721147..27b2866e2 100644 --- a/redbot/core/bank.py +++ b/redbot/core/bank.py @@ -4,9 +4,10 @@ from typing import Union, List, Optional import discord -from redbot.core import Config +from . import Config, errors __all__ = [ + "MAX_BALANCE", "Account", "get_balance", "set_balance", @@ -26,6 +27,8 @@ __all__ = [ "set_default_balance", ] +MAX_BALANCE = 2 ** 63 - 1 + _DEFAULT_GLOBAL = { "is_global": False, "bank_name": "Twentysix bank", @@ -170,10 +173,22 @@ async def set_balance(member: discord.Member, amount: int) -> int: ------ ValueError If attempting to set the balance to a negative number. + BalanceTooHigh + If attempting to set the balance to a value greater than + ``bank.MAX_BALANCE`` """ if amount < 0: raise ValueError("Not allowed to have negative balance.") + if amount > MAX_BALANCE: + currency = ( + await get_currency_name() + if await is_global() + else await get_currency_name(member.guild) + ) + raise errors.BalanceTooHigh( + user=member.display_name, max_balance=MAX_BALANCE, currency_name=currency + ) if await is_global(): group = _conf.user(member) else: diff --git a/redbot/core/errors.py b/redbot/core/errors.py index 466f75715..e3446ca1c 100644 --- a/redbot/core/errors.py +++ b/redbot/core/errors.py @@ -1,4 +1,11 @@ import importlib.machinery +from typing import Optional + +import discord + +from .i18n import Translator + +_ = Translator(__name__, __file__) class RedError(Exception): @@ -14,3 +21,24 @@ class PackageAlreadyLoaded(RedError): def __str__(self) -> str: return f"There is already a package named {self.spec.name.split('.')[-1]} loaded" + + +class BankError(RedError): + """Base error class for bank-related errors.""" + + +class BalanceTooHigh(BankError, OverflowError): + """Raised when trying to set a user's balance to higher than the maximum.""" + + def __init__( + self, user: discord.abc.User, max_balance: int, currency_name: str, *args, **kwargs + ): + super().__init__(*args, **kwargs) + self.user = user + self.max_balance = max_balance + self.currency_name = currency_name + + def __str__(self) -> str: + return _("{user}'s balance cannot rise above {max:,} {currency}.").format( + user=self.user, max=self.max_balance, currency=self.currency_name + ) From 8bba860f85d0c1c9ff65caab4efd27152099116c Mon Sep 17 00:00:00 2001 From: Toby Harradine Date: Tue, 16 Oct 2018 09:37:23 +1100 Subject: [PATCH 11/11] Bump version to 3.0.0rc2 Signed-off-by: Toby Harradine --- redbot/core/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redbot/core/__init__.py b/redbot/core/__init__.py index b4dd05d5b..d3b2c08b3 100644 --- a/redbot/core/__init__.py +++ b/redbot/core/__init__.py @@ -148,5 +148,5 @@ class VersionInfo: ) -__version__ = "3.0.0rc1.post1" +__version__ = "3.0.0rc2" version_info = VersionInfo.from_str(__version__)