From 7322f0c676f038ac7d31ad55eb2defe6a20b65b3 Mon Sep 17 00:00:00 2001 From: Tobotimus Date: Mon, 20 Nov 2017 09:23:47 +1100 Subject: [PATCH] [V3 Mod/ModLog] Fix duplicate cases and allow case creation without audit log perms (#1102) * Use ban/unban queue * Refactor unban's cmd help * Better support for no audit log perms --- redbot/cogs/mod/mod.py | 246 +++++++++++++++++++---------------------- redbot/core/modlog.py | 22 ++-- 2 files changed, 129 insertions(+), 139 deletions(-) diff --git a/redbot/cogs/mod/mod.py b/redbot/cogs/mod/mod.py index 09c452d68..1056aadbb 100644 --- a/redbot/cogs/mod/mod.py +++ b/redbot/cogs/mod/mod.py @@ -1,4 +1,5 @@ import asyncio +from datetime import datetime from collections import deque, defaultdict import discord @@ -51,8 +52,8 @@ class Mod: self.settings.register_channel(**self.default_channel_settings) self.settings.register_member(**self.default_member_settings) self.settings.register_user(**self.default_user_settings) - self.current_softban = {} - self.ban_type = None + self.ban_queue = [] + self.unban_queue = [] self.cache = defaultdict(lambda: deque(maxlen=3)) self.bot.loop.create_task(self._casetype_registration()) @@ -355,14 +356,17 @@ class Mod: if days < 0 or days > 7: await ctx.send(_("Invalid days. Must be between 0 and 7.")) return - + queue_entry = (guild.id, user.id) + self.ban_queue.append(queue_entry) try: await guild.ban(user, reason=audit_reason, delete_message_days=days) log.info("{}({}) banned {}({}), deleting {} days worth of messages".format( author.name, author.id, user.name, user.id, str(days))) except discord.Forbidden: + self.ban_queue.remove(queue_entry) await ctx.send(_("I'm not allowed to do that.")) except Exception as e: + self.ban_queue.remove(queue_entry) print(e) else: await ctx.send(_("Done. It was about time.")) @@ -402,15 +406,18 @@ class Mod: user = discord.Object(id=user_id) # User not in the guild, but audit_reason = get_audit_reason(author, reason) - + queue_entry = (guild.id, user_id) + self.ban_queue.append(queue_entry) try: await guild.ban(user, reason=audit_reason) log.info("{}({}) hackbanned {}" "".format(author.name, author.id, user_id)) except discord.NotFound: + self.ban_queue.remove(queue_entry) await ctx.send(_("User not found. Have you provided the " "correct user ID?")) except discord.Forbidden: + self.ban_queue.remove(queue_entry) await ctx.send(_("I lack the permissions to do this.")) else: await ctx.send(_("Done. The user will not be able to join this " @@ -452,7 +459,7 @@ class Mod: invite = "" if can_ban: - self.current_softban[str(guild.id)] = user + queue_entry = (guild.id, user.id) try: # We don't want blocked DMs preventing us from banning msg = await user.send( _("You have been banned and " @@ -460,17 +467,26 @@ class Mod: "You can now join the guild again. {}").format(invite)) except discord.HTTPException: msg = None + self.ban_queue.append(queue_entry) try: await guild.ban( user, reason=audit_reason, delete_message_days=1) - await guild.unban(user) except discord.errors.Forbidden: + self.ban_queue.remove(queue_entry) await ctx.send( _("My role is not high enough to softban that user.")) if msg is not None: await msg.delete() return except discord.HTTPException as e: + self.ban_queue.remove(queue_entry) + print(e) + return + self.unban_queue.append(queue_entry) + try: + await guild.unban(user) + except discord.HTTPException as e: + self.unban_queue.remove(queue_entry) print(e) return else: @@ -490,9 +506,6 @@ class Mod: channel=None) except RuntimeError as e: await ctx.send(e) - finally: - await asyncio.sleep(5) - self.current_softban = None else: await ctx.send(_("I'm not allowed to do that.")) @@ -501,11 +514,12 @@ class Mod: @checks.admin_or_permissions(ban_members=True) @commands.bot_has_permissions(ban_members=True) async def unban(self, ctx: RedContext, user_id: int, *, reason: str = None): - """Unbans the target user. Requires specifying the target user's ID - (which can be found in the mod log channel (if logging was enabled for - the casetype associated with the command used to ban the user) or (if - developer mode is enabled) by looking in Bans in guild settings, - finding the user, right-clicking, and selecting 'Copy ID'""" + """Unbans the target user. + + Requires specifying the target user's ID. To find this, you may either: + 1. Copy it from the mod log case (if one was created), or + 2. enable developer mode, go to Bans in this server's settings, right- + click the user and select 'Copy ID'.""" guild = ctx.guild author = ctx.author user = self.bot.get_user_info(user_id) @@ -518,10 +532,12 @@ class Mod: if user not in bans: await ctx.send(_("It seems that user isn't banned!")) return - + queue_entry = (guild.id, user.id) + self.unban_queue.append(queue_entry) try: await guild.unban(user, reason=reason) except discord.HTTPException: + self.unban_queue.remove(queue_entry) await ctx.send(_("Something went wrong while attempting to unban that user")) return else: @@ -1149,130 +1165,100 @@ class Mod: deleted = await self.check_mention_spam(message) async def on_member_ban(self, guild: discord.Guild, member: discord.Member): - if str(guild.id) in self.current_softban and \ - self.current_softban[str(guild.id)] == member: - return # softban in progress, so a case will be created + if (guild.id, member.id) in self.ban_queue: + self.ban_queue.remove((guild.id, member.id)) + return try: - mod_ch = await modlog.get_modlog_channel(guild) + await modlog.get_modlog_channel(guild) except RuntimeError: return # No modlog channel so no point in continuing - audit_case = None - permissions = guild.me.guild_permissions - modlog_cases = await modlog.get_all_cases(guild, self.bot) - if permissions.view_audit_log: - async for entry in guild.audit_logs(action=discord.AuditLogAction.ban): - if entry.target == member: - audit_case = entry - break - - if audit_case: - mod = audit_case.user - reason = audit_case.reason - for case in sorted(modlog_cases, key=lambda x: x.case_number, reverse=True): - if mod == guild.me and case.user == member\ - and case.action_type in ["ban", "hackban"]: - log.info("Case already exists for ban of {}".format(member.name)) - break - else: # no ban, softban, or hackban case with the mod and user combo - try: - await modlog.create_case(guild, audit_case.created_at, "ban", - member, mod, reason if reason else None) - except RuntimeError as e: - print(e) - else: - return - else: # No permissions to view audit logs, so message the guild owner - owner = guild.owner - try: - await owner.send( - _("Hi, I noticed that someone in your server " - "(the server named {}) banned {}#{} (user ID {}). " - "However, I don't have permissions to view audit logs, " - "so I could not determine if a mod log case was created " - "for that ban, meaning I could not create a case in " - "the mod log. If you want me to be able to add cases " - "to the mod log for bans done manually, I need the " - "`View Audit Logs` permission.").format( - guild.name, - member.name, - member.discriminator, - member.id - ) - ) - except discord.Forbidden: - log.warning( - "I attempted to inform a guild owner of a lack of the " - "'View Audit Log' permission but I am unable to send " - "the guild owner the message!" - ) - except discord.HTTPException: - log.warning( - "Something else went wrong while attempting to " - "message a guild owner." - ) + mod, reason, date = await self.get_audit_entry_info( + guild, discord.AuditLogAction.ban, member) + if date is None: + date = datetime.now() + try: + await modlog.create_case(guild, date, + "ban", member, mod, + reason if reason else None) + except RuntimeError as e: + print(e) async def on_member_unban(self, guild: discord.Guild, user: discord.User): - if str(guild.id) in self.current_softban and \ - self.current_softban[str(guild.id)] == user: - return # softban in progress, so a case will be created + if (guild.id, user.id) in self.unban_queue: + self.unban_queue.remove((guild.id, user.id)) + return try: - mod_ch = await modlog.get_modlog_channel(guild) + await modlog.get_modlog_channel(guild) except RuntimeError: return # No modlog channel so no point in continuing - audit_case = None - permissions = guild.me.guild_permissions - if permissions.view_audit_log: - async for entry in guild.audit_logs(action=discord.AuditLogAction.unban): - if entry.target == user: - audit_case = entry - break - else: - return - if audit_case: - mod = audit_case.user - reason = audit_case.reason + mod, reason, date = await self.get_audit_entry_info( + guild, discord.AuditLogAction.unban, user) + if date is None: + date = datetime.now() + try: + await modlog.create_case(guild, date, "unban", + user, mod, reason) + except RuntimeError as e: + print(e) - cases = await modlog.get_all_cases(guild, self.bot) - for case in sorted(cases, key=lambda x: x.case_number, reverse=True): - if mod == guild.me and case.user == user\ - and case.action_type == "unban": - log.info("Case already exists for unban of {}".format(user.name)) - break - else: - try: - await modlog.create_case(guild, audit_case.created_at, "unban", - user, mod, reason if reason else None) - except RuntimeError as e: - print(e) - else: # No permissions to view audit logs, so message the guild owner - owner = guild.owner - try: - await owner.send( - _("Hi, I noticed that someone in your server " - "(the server named {}) unbanned {}#{} (user ID {}). " - "However, I don't have permissions to view audit logs, " - "so I could not determine if a mod log case was created " - "for that unban, meaning I could not create a case in " - "the mod log. If you want me to be able to add cases " - "to the mod log for unbans done manually, I need the " - "`View Audit Logs` permission.").format( - guild.name, - user.name, - user.discriminator, - user.id - ) - ) - except discord.Forbidden: - log.warning( - "I attempted to inform a guild owner of a lack of the " - "'View Audit Log' permission but I am unable to send " - "the guild owner the message!" - ) - except discord.HTTPException: - log.warning( - "Something else went wrong while attempting to " - "message a guild owner." - ) + async def get_audit_entry_info(self, + guild: discord.Guild, + action: int, + target): + """Get info about an audit log entry. + + Parameters + ---------- + guild : discord.Guild + Same as ``guild`` in `get_audit_log_entry`. + action : int + Same as ``action`` in `get_audit_log_entry`. + target : `discord.User` or `discord.Member` + Same as ``target`` in `get_audit_log_entry`. + + Returns + ------- + tuple + A tuple in the form``(mod: discord.Member, reason: str, + date_created: datetime.datetime)``. Returns ``(None, None, None)`` + if the audit log entry could not be found. + """ + try: + entry = await self.get_audit_log_entry( + guild, action=action, target=target) + except discord.HTTPException: + entry = None + if entry is None: + return None, None, None + return entry.user, entry.reason, entry.created_at + + async def get_audit_log_entry(self, + guild: discord.Guild, + action: int, + target): + """Get an audit log entry. + + Any exceptions encountered when looking through the audit log will be + propogated out of this function. + + Parameters + ---------- + guild : discord.Guild + The guild for the audit log. + action : int + The audit log action (see `discord.AuditLogAction`). + target : `discord.Member` or `discord.User` + The target of the audit log action. + + Returns + ------- + discord.AuditLogEntry + The audit log entry. Returns ``None`` if not found. + + """ + async for entry in guild.audit_logs(action=action): + if entry.target == target: + return entry async def on_member_update(self, before, after): if before.name != after.name: diff --git a/redbot/core/modlog.py b/redbot/core/modlog.py index fc0600fe7..52e20bc94 100644 --- a/redbot/core/modlog.py +++ b/redbot/core/modlog.py @@ -107,13 +107,13 @@ class Case: user = "{}#{} ({})\n".format( self.user.name, self.user.discriminator, self.user.id) emb.set_author(name=user, icon_url=self.user.avatar_url) - - moderator = "{}#{} ({})\n".format( - self.moderator.name, - self.moderator.discriminator, - self.moderator.id - ) - emb.add_field(name="Moderator", value=moderator, inline=False) + if self.moderator is not None: + moderator = "{}#{} ({})\n".format( + self.moderator.name, + self.moderator.discriminator, + self.moderator.id + ) + emb.add_field(name="Moderator", value=moderator, inline=False) if self.until: start = datetime.fromtimestamp(self.created_at) end = datetime.fromtimestamp(self.until) @@ -153,13 +153,17 @@ class Case: The case in the form of a dict """ + if self.moderator is not None: + mod = self.moderator.id + else: + mod = None data = { "case_number": self.case_number, "action_type": self.action_type, "guild": self.guild.id, "created_at": self.created_at, "user": self.user.id, - "moderator": self.moderator.id, + "moderator": mod, "reason": self.reason, "until": self.until, "channel": self.channel.id if hasattr(self.channel, "id") else None, @@ -373,7 +377,7 @@ async def get_all_cases(guild: discord.Guild, bot: Red) -> List[Case]: async def create_case(guild: discord.Guild, created_at: datetime, action_type: str, user: Union[discord.User, discord.Member], - moderator: discord.Member, reason: str=None, + moderator: discord.Member=None, reason: str=None, until: datetime=None, channel: discord.TextChannel=None ) -> Union[Case, None]: """