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 2f17f6ef1..c9307e817 100644 --- a/redbot/cogs/customcom/customcom.py +++ b/redbot/cogs/customcom/customcom.py @@ -3,13 +3,14 @@ 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 from redbot.core import Config, checks, commands -from redbot.core.utils.chat_formatting import box, pagify, escape 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__) @@ -121,7 +122,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) @@ -330,12 +331,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." @@ -345,8 +350,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) @@ -354,15 +358,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) @customcom.command(name="show") async def cc_show(self, ctx, command_name: str): @@ -606,3 +628,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))) 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 diff --git a/redbot/cogs/downloader/downloader.py b/redbot/cogs/downloader/downloader.py index f9df14c99..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) @@ -344,7 +343,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") @@ -372,13 +371,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 +406,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: 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/cogs/mod/mod.py b/redbot/cogs/mod/mod.py index 31f105af3..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!")) @@ -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 " 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)) 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__) 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/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 ): diff --git a/redbot/core/errors.py b/redbot/core/errors.py index ca9f4f9b4..a67097dc0 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): @@ -21,3 +28,24 @@ class CogLoadError(RedError): The message will be send to the user.""" pass + + +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 + )