From 9bec011dee4ef3f0968aa44d2459a9367d25313c Mon Sep 17 00:00:00 2001 From: sco1 Date: Sat, 29 Dec 2018 13:10:38 -0500 Subject: Add optional return to modlog post coro Enables generation of a context for AntiSpam to reference for posting infractions --- bot/cogs/modlog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py index 1d1546d5b..ef4544f81 100644 --- a/bot/cogs/modlog.py +++ b/bot/cogs/modlog.py @@ -123,7 +123,8 @@ class ModLog: if ping_everyone: content = "@everyone" - await self.bot.get_channel(channel_id).send(content=content, embed=embed, files=files) + log_message = await self.bot.get_channel(channel_id).send(content=content, embed=embed, files=files) + return self.bot.get_context(log_message) # Optionally return for use with antispam async def on_guild_channel_create(self, channel: GUILD_CHANNEL): if channel.guild.id != GuildConstant.id: -- cgit v1.2.3 From 9698cf42a22e0637c1afc21dbf3c8282829ccff0 Mon Sep 17 00:00:00 2001 From: sco1 Date: Sat, 29 Dec 2018 13:58:03 -0500 Subject: Add infraction posting for AntiSpam mutes --- bot/cogs/antispam.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index d5b72718c..052fd48b2 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -15,6 +15,7 @@ from bot.constants import ( Colours, DEBUG_MODE, Event, Guild as GuildConfig, Icons, Roles, ) +from bot.utils.moderation import post_infraction from bot.utils.time import humanize_delta @@ -133,7 +134,8 @@ class AntiSpam: mod_alert_message += f"{content}" - await self.mod_log.send_log_message( + # Return the mod log message Context that we can use to post the infraction + mod_log_message = await self.mod_log.send_log_message( icon_url=Icons.filtering, colour=Colour(Colours.soft_red), title=f"Spam detected!", @@ -143,28 +145,30 @@ class AntiSpam: ping_everyone=AntiSpamConfig.ping_everyone ) - await member.add_roles(self.muted_role, reason=reason) + # Post AntiSpam mute as a regular infraction so it can be reversed + ctx = await self.bot.get_context(mod_log_message) + response_object = await post_infraction(ctx, member, type="mute", reason=reason, duration=remove_role_after) + if response_object is None: + return # Appropriate error(s) are already raised by post_infraction + + self.mod_log.ignore(Event.member_update, member.id) + await member.add_roles(self._muted_role, reason=reason) + + loop = asyncio.get_event_loop() + infraction_object = response_object["infraction"] + self.schedule_task(loop, infraction_object["id"], infraction_object) + description = textwrap.dedent(f""" **Channel**: {msg.channel.mention} **User**: {msg.author.mention} (`{msg.author.id}`) **Reason**: {reason} Role will be removed after {human_duration}. """) - await self.mod_log.send_log_message( icon_url=Icons.user_mute, colour=Colour(Colours.soft_red), title="User muted", text=description ) - await asyncio.sleep(remove_role_after) - await member.remove_roles(self.muted_role, reason="AntiSpam mute expired") - - await self.mod_log.send_log_message( - icon_url=Icons.user_mute, colour=Colour(Colours.soft_green), - title="User unmuted", - text=f"Was muted by `AntiSpam` cog for {human_duration}." - ) - async def maybe_delete_messages(self, channel: TextChannel, messages: List[Message]): # Is deletion of offending messages actually enabled? if AntiSpamConfig.clean_offending: -- cgit v1.2.3 From 9f8e4774ae6eeac8cbcece8c65c8de390c3300fd Mon Sep 17 00:00:00 2001 From: sco1 Date: Sun, 6 Jan 2019 00:27:49 -0500 Subject: Antispam Infraction Fixes Add muted role object to cog attributes Fix unawaited coroutine in modlog Adjust modlog message ctx variable to be more explicit Fix duration being sent to API as integer instead of string Fix temporary infraction being placed into a nonexistent schedule, now placed into the moderation cog's task schedule --- bot/cogs/antispam.py | 15 +++++++++------ bot/cogs/modlog.py | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index 052fd48b2..cf52d30fa 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -45,7 +45,7 @@ WHITELISTED_ROLES = (Roles.owner, Roles.admin, Roles.moderator, Roles.helpers) class AntiSpam: def __init__(self, bot: Bot): self.bot = bot - self.muted_role = None + self._muted_role = Object(Roles.muted) @property def mod_log(self) -> ModLog: @@ -135,7 +135,7 @@ class AntiSpam: mod_alert_message += f"{content}" # Return the mod log message Context that we can use to post the infraction - mod_log_message = await self.mod_log.send_log_message( + mod_log_ctx = await self.mod_log.send_log_message( icon_url=Icons.filtering, colour=Colour(Colours.soft_red), title=f"Spam detected!", @@ -146,17 +146,20 @@ class AntiSpam: ) # Post AntiSpam mute as a regular infraction so it can be reversed - ctx = await self.bot.get_context(mod_log_message) - response_object = await post_infraction(ctx, member, type="mute", reason=reason, duration=remove_role_after) + response_object = await post_infraction( + mod_log_ctx, member, type="mute", reason=reason, duration=f"{remove_role_after}S" + ) if response_object is None: return # Appropriate error(s) are already raised by post_infraction self.mod_log.ignore(Event.member_update, member.id) await member.add_roles(self._muted_role, reason=reason) - loop = asyncio.get_event_loop() + # Insert ourselves into the moderation infraction loop infraction_object = response_object["infraction"] - self.schedule_task(loop, infraction_object["id"], infraction_object) + loop = asyncio.get_event_loop() + moderation_cog = self.bot.get_cog('Moderation') + moderation_cog.schedule_task(loop, infraction_object["id"], infraction_object) description = textwrap.dedent(f""" **Channel**: {msg.channel.mention} diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py index f36c431e6..9d26fa925 100644 --- a/bot/cogs/modlog.py +++ b/bot/cogs/modlog.py @@ -126,7 +126,7 @@ class ModLog: content = "@everyone" log_message = await self.bot.get_channel(channel_id).send(content=content, embed=embed, files=files) - return self.bot.get_context(log_message) # Optionally return for use with antispam + return await self.bot.get_context(log_message) # Optionally return for use with antispam async def on_guild_channel_create(self, channel: GUILD_CHANNEL): if channel.guild.id != GuildConstant.id: -- cgit v1.2.3 From 76951b4f1b0ada06b45f38271e6eb8c93ce8e51e Mon Sep 17 00:00:00 2001 From: sco1 Date: Sun, 6 Jan 2019 00:54:27 -0500 Subject: Invoke Moderation tempmute directly --- bot/cogs/antispam.py | 36 +++--------------------------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index cf52d30fa..800700a50 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -1,22 +1,18 @@ -import asyncio import logging -import textwrap from datetime import datetime, timedelta from typing import List -from dateutil.relativedelta import relativedelta from discord import Colour, Member, Message, Object, TextChannel from discord.ext.commands import Bot from bot import rules +from bot.cogs.moderation import Moderation from bot.cogs.modlog import ModLog from bot.constants import ( AntiSpam as AntiSpamConfig, Channels, Colours, DEBUG_MODE, Event, Guild as GuildConfig, Icons, Roles, ) -from bot.utils.moderation import post_infraction -from bot.utils.time import humanize_delta log = logging.getLogger(__name__) @@ -111,8 +107,6 @@ class AntiSpam: # Sanity check to ensure we're not lagging behind if self.muted_role not in member.roles: remove_role_after = AntiSpamConfig.punishment['remove_after'] - duration_delta = relativedelta(seconds=remove_role_after) - human_duration = humanize_delta(duration_delta) mod_alert_message = ( f"**Triggered by:** {member.display_name}#{member.discriminator} (`{member.id}`)\n" @@ -145,32 +139,8 @@ class AntiSpam: ping_everyone=AntiSpamConfig.ping_everyone ) - # Post AntiSpam mute as a regular infraction so it can be reversed - response_object = await post_infraction( - mod_log_ctx, member, type="mute", reason=reason, duration=f"{remove_role_after}S" - ) - if response_object is None: - return # Appropriate error(s) are already raised by post_infraction - - self.mod_log.ignore(Event.member_update, member.id) - await member.add_roles(self._muted_role, reason=reason) - - # Insert ourselves into the moderation infraction loop - infraction_object = response_object["infraction"] - loop = asyncio.get_event_loop() - moderation_cog = self.bot.get_cog('Moderation') - moderation_cog.schedule_task(loop, infraction_object["id"], infraction_object) - - description = textwrap.dedent(f""" - **Channel**: {msg.channel.mention} - **User**: {msg.author.mention} (`{msg.author.id}`) - **Reason**: {reason} - Role will be removed after {human_duration}. - """) - await self.mod_log.send_log_message( - icon_url=Icons.user_mute, colour=Colour(Colours.soft_red), - title="User muted", text=description - ) + # Run a tempmute + await mod_log_ctx.invoke(Moderation.tempmute, member, f"{remove_role_after}S", reason=reason) async def maybe_delete_messages(self, channel: TextChannel, messages: List[Message]): # Is deletion of offending messages actually enabled? -- cgit v1.2.3 From 71eac59a69b36e21efb0bcf165ad8cd41ccaa1fb Mon Sep 17 00:00:00 2001 From: sco1 Date: Mon, 7 Jan 2019 17:51:00 -0500 Subject: Add mute infraction check to role restoration --- bot/cogs/events.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/bot/cogs/events.py b/bot/cogs/events.py index edfc6e579..c604169c0 100644 --- a/bot/cogs/events.py +++ b/bot/cogs/events.py @@ -25,6 +25,7 @@ class Events: def __init__(self, bot: Bot): self.bot = bot + self.headers = {"X-API-KEY": Keys.site_api} @property def mod_log(self) -> ModLog: @@ -103,6 +104,29 @@ class Events: resp = await response.json() return resp["data"] + async def has_active_mute(self, user_id: str) -> bool: + """ + Check whether a user has any active mute infractions + """ + response = await self.bot.http_session.get( + URLs.site_infractions_user.format( + user_id=user_id + ), + params={"hidden": "True"}, + headers=self.headers + ) + infraction_list = await response.json() + + # Check for active mute infractions + if len(infraction_list) == 0: + # Short circuit + return False + + muted_check = any( + [infraction["active"] for infraction in infraction_list if infraction["type"].lower() == "mute"] + ) + return muted_check + async def on_command_error(self, ctx: Context, e: CommandError): command = ctx.command parent = None @@ -236,6 +260,10 @@ class Events: for role in RESTORE_ROLES: if role in old_roles: + # Check for mute roles that were not able to be removed and skip if present + if role == str(Roles.muted) and not await self.has_active_mute(str(member.id)): + continue + new_roles.append(Object(int(role))) for role in new_roles: -- cgit v1.2.3 From d5cb479be6925a10cf0c46e3088518f56a5a91b3 Mon Sep 17 00:00:00 2001 From: sco1 Date: Mon, 7 Jan 2019 19:16:03 -0500 Subject: Add line after docstring --- bot/cogs/events.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/cogs/events.py b/bot/cogs/events.py index c604169c0..77a15733d 100644 --- a/bot/cogs/events.py +++ b/bot/cogs/events.py @@ -108,6 +108,7 @@ class Events: """ Check whether a user has any active mute infractions """ + response = await self.bot.http_session.get( URLs.site_infractions_user.format( user_id=user_id -- cgit v1.2.3 From 0b8c5f074c725d739fffabb18076519d33a033a7 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Tue, 8 Jan 2019 16:18:43 -0500 Subject: Remove list comprehension since any() works on generators Co-Authored-By: sco1 --- bot/cogs/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/events.py b/bot/cogs/events.py index 77a15733d..b44e5871e 100644 --- a/bot/cogs/events.py +++ b/bot/cogs/events.py @@ -124,7 +124,7 @@ class Events: return False muted_check = any( - [infraction["active"] for infraction in infraction_list if infraction["type"].lower() == "mute"] + infraction["active"] for infraction in infraction_list if infraction["type"].lower() == "mute" ) return muted_check -- cgit v1.2.3 From e8a5db64dc70270488a6b9a43e21470c11936878 Mon Sep 17 00:00:00 2001 From: sco1 Date: Tue, 8 Jan 2019 16:36:54 -0500 Subject: Switch short-circuit logic, add logging --- bot/cogs/events.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/cogs/events.py b/bot/cogs/events.py index b44e5871e..78878dcb9 100644 --- a/bot/cogs/events.py +++ b/bot/cogs/events.py @@ -119,7 +119,7 @@ class Events: infraction_list = await response.json() # Check for active mute infractions - if len(infraction_list) == 0: + if not infraction_list: # Short circuit return False @@ -263,6 +263,10 @@ class Events: if role in old_roles: # Check for mute roles that were not able to be removed and skip if present if role == str(Roles.muted) and not await self.has_active_mute(str(member.id)): + log.debug( + f"User {member.id} has no active mute infraction, " + "their leftover muted role will not be persisted" + ) continue new_roles.append(Object(int(role))) -- cgit v1.2.3 From 25d9b1ea36b03ed431d7262d373124e3f788d408 Mon Sep 17 00:00:00 2001 From: sco1 Date: Tue, 8 Jan 2019 17:34:51 -0500 Subject: From review --- bot/cogs/events.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/cogs/events.py b/bot/cogs/events.py index 78878dcb9..f0baecd4b 100644 --- a/bot/cogs/events.py +++ b/bot/cogs/events.py @@ -123,10 +123,9 @@ class Events: # Short circuit return False - muted_check = any( + return any( infraction["active"] for infraction in infraction_list if infraction["type"].lower() == "mute" ) - return muted_check async def on_command_error(self, ctx: Context, e: CommandError): command = ctx.command -- cgit v1.2.3 From b2929ef2e72d42950169db4a46c064eedc228c4e Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 10 Jan 2019 16:55:55 +0100 Subject: Adding watch reason to the big-brother header embeds --- bot/cogs/bigbrother.py | 65 +++++++++++++++++++++++++++++++++++++++++++------- config-default.yml | 1 + 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/bot/cogs/bigbrother.py b/bot/cogs/bigbrother.py index 29b13f038..26d35a97c 100644 --- a/bot/cogs/bigbrother.py +++ b/bot/cogs/bigbrother.py @@ -2,8 +2,10 @@ import asyncio import logging import re from collections import defaultdict, deque +from time import strptime from typing import List, Union +from aiohttp import ClientError from discord import Color, Embed, Guild, Member, Message, TextChannel, User from discord.ext.commands import Bot, Context, group @@ -26,13 +28,15 @@ class BigBrother: def __init__(self, bot: Bot): self.bot = bot self.watched_users = {} # { user_id: log_channel_id } + self.watch_reasons = {} # { user_id: watch_reason } self.channel_queues = defaultdict(lambda: defaultdict(deque)) # { user_id: { channel_id: queue(messages) } self.last_log = [None, None, 0] # [user_id, channel_id, message_count] self.consuming = False + self.infraction_watch_prefix = "bb watch: " # Please do not change or we won't be able to find old reasons self.bot.loop.create_task(self.get_watched_users()) - def update_cache(self, api_response: List[dict]): + async def update_cache(self, api_response: List[dict]): """ Updates the internal cache of watched users from the given `api_response`. This function will only add (or update) existing keys, it will not delete @@ -54,13 +58,52 @@ class BigBrother: "but the given channel could not be found. Ignoring." ) + watch_reason = await self.get_watch_reason(user_id) + self.watch_reasons[user_id] = watch_reason + async def get_watched_users(self): """Retrieves watched users from the API.""" await self.bot.wait_until_ready() async with self.bot.http_session.get(URLs.site_bigbrother_api, headers=self.HEADERS) as response: data = await response.json() - self.update_cache(data) + await self.update_cache(data) + + async def get_watch_reason(self, user_id: int) -> str: + """ Fetches and returns the latest watch reason for a user using the infraction API """ + + re_bb_watch = rf"^{self.infraction_watch_prefix}" + user_id = str(user_id) + + try: + response = await self.bot.http_session.get( + URLs.site_infractions_user_type.format( + user_id=user_id, + infraction_type="note", + ), + params={"search": re_bb_watch, "hidden": "True", "active": "False"}, + headers=self.HEADERS + ) + infraction_list = await response.json() + except ClientError: + log.exception(f"Failed to retrieve bb watch reason for {user_id}.") + return "(error retrieving bb reason)" + + if infraction_list: + latest_reason_infraction = max(infraction_list, key=self._parse_time) + latest_reason = latest_reason_infraction['reason'][10:] + log.trace(f"The latest bb watch reason for {user_id}: {latest_reason}") + return latest_reason + + log.trace(f"No bb watch reason found for {user_id}; returning default string") + return "(no reason specified)" + + @staticmethod + def _parse_time(infraction): + """Takes RFC1123 date_time string and returns time object for sorting purposes""" + + date_string = infraction["inserted_at"] + return strptime(date_string, "%a, %d %b %Y %H:%M:%S %Z") async def on_member_ban(self, guild: Guild, user: Union[User, Member]): if guild.id == GuildConfig.id and user.id in self.watched_users: @@ -70,6 +113,7 @@ class BigBrother: async with self.bot.http_session.delete(url, headers=self.HEADERS) as response: del self.watched_users[user.id] del self.channel_queues[user.id] + del self.watch_reasons[user.id] if response.status == 204: await channel.send( f"{Emojis.bb_message}:hammer: {user} got banned, so " @@ -143,6 +187,7 @@ class BigBrother: embed = Embed(description=f"{message.author.mention} in [#{message.channel.name}]({message.jump_url})") embed.set_author(name=message.author.nick or message.author.name, icon_url=message.author.avatar_url) + embed.set_footer(text=f"Watch reason: {self.watch_reasons[message.author.id]}") await destination.send(embed=embed) @staticmethod @@ -201,7 +246,7 @@ class BigBrother: async with self.bot.http_session.get(URLs.site_bigbrother_api, headers=self.HEADERS) as response: if response.status == 200: data = await response.json() - self.update_cache(data) + await self.update_cache(data) lines = tuple(f"• <@{entry['user_id']}> in <#{entry['channel_id']}>" for entry in data) await LinePaginator.paginate( @@ -246,15 +291,15 @@ class BigBrother: ) else: self.watched_users[user.id] = channel + self.watch_reasons[user.id] = reason + # Add a note (shadow warning) with the reason for watching + reason = f"{self.infraction_watch_prefix}{reason}" + await post_infraction(ctx, user, type="warning", reason=reason, hidden=True) else: data = await response.json() - reason = data.get('error_message', "no message provided") - await ctx.send(f":x: the API returned an error: {reason}") - - # Add a note (shadow warning) with the reason for watching - reason = "bb watch: " + reason # Prepend for situational awareness - await post_infraction(ctx, user, type="warning", reason=reason, hidden=True) + error_reason = data.get('error_message', "no message provided") + await ctx.send(f":x: the API returned an error: {error_reason}") @bigbrother_group.command(name='unwatch', aliases=('uw',)) @with_role(Roles.owner, Roles.admin, Roles.moderator) @@ -270,6 +315,8 @@ class BigBrother: del self.watched_users[user.id] if user.id in self.channel_queues: del self.channel_queues[user.id] + if user.id in self.watch_reasons: + del self.watch_reasons[user.id] else: log.warning(f"user {user.id} was unwatched but was not found in the cache") diff --git a/config-default.yml b/config-default.yml index 21d7f20b9..1a5a63c6a 100644 --- a/config-default.yml +++ b/config-default.yml @@ -240,6 +240,7 @@ urls: site_infractions_type: !JOIN [*SCHEMA, *API, "/bot/infractions/type/{infraction_type}"] site_infractions_by_id: !JOIN [*SCHEMA, *API, "/bot/infractions/id/{infraction_id}"] site_infractions_user_type_current: !JOIN [*SCHEMA, *API, "/bot/infractions/user/{user_id}/{infraction_type}/current"] + site_infractions_user_type: !JOIN [*SCHEMA, *API, "/bot/infractions/user/{user_id}/{infraction_type}"] site_logs_api: !JOIN [*SCHEMA, *API, "/bot/logs"] site_logs_view: !JOIN [*SCHEMA, *DOMAIN, "/bot/logs"] site_names_api: !JOIN [*SCHEMA, *API, "/bot/snake_names"] -- cgit v1.2.3 From f113c2bed932f52439b1185b96a03c6e3ae03c8e Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 10 Jan 2019 17:03:45 +0100 Subject: Adding type annotations --- bot/cogs/bigbrother.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/bigbrother.py b/bot/cogs/bigbrother.py index 26d35a97c..b325fa5fe 100644 --- a/bot/cogs/bigbrother.py +++ b/bot/cogs/bigbrother.py @@ -2,7 +2,7 @@ import asyncio import logging import re from collections import defaultdict, deque -from time import strptime +from time import strptime, struct_time from typing import List, Union from aiohttp import ClientError @@ -99,7 +99,7 @@ class BigBrother: return "(no reason specified)" @staticmethod - def _parse_time(infraction): + def _parse_time(infraction: str) -> struct_time: """Takes RFC1123 date_time string and returns time object for sorting purposes""" date_string = infraction["inserted_at"] -- cgit v1.2.3 From 66e476f9ca024c5e6e2917679aaa4da03ef69acd Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Thu, 10 Jan 2019 17:37:20 +0100 Subject: Removing default argument for the alias watch to require a reason via the alias as well --- bot/cogs/alias.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/alias.py b/bot/cogs/alias.py index 2ce4a51e3..0b848c773 100644 --- a/bot/cogs/alias.py +++ b/bot/cogs/alias.py @@ -71,7 +71,7 @@ class Alias: @command(name="watch", hidden=True) async def bigbrother_watch_alias( - self, ctx, user: User, *, reason: str = None + self, ctx, user: User, *, reason: str ): """ Alias for invoking bigbrother watch user [text_channel]. -- cgit v1.2.3 From a2f8b21adf7880a116ae9f80e95cd7e696e7e135 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Fri, 11 Jan 2019 09:24:57 +0100 Subject: Only retrieve/cache watch reason when user becomes active; restore update_cache to synchronous as it was before --- bot/cogs/bigbrother.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/bot/cogs/bigbrother.py b/bot/cogs/bigbrother.py index b325fa5fe..70916cd7b 100644 --- a/bot/cogs/bigbrother.py +++ b/bot/cogs/bigbrother.py @@ -36,7 +36,7 @@ class BigBrother: self.bot.loop.create_task(self.get_watched_users()) - async def update_cache(self, api_response: List[dict]): + def update_cache(self, api_response: List[dict]): """ Updates the internal cache of watched users from the given `api_response`. This function will only add (or update) existing keys, it will not delete @@ -58,16 +58,13 @@ class BigBrother: "but the given channel could not be found. Ignoring." ) - watch_reason = await self.get_watch_reason(user_id) - self.watch_reasons[user_id] = watch_reason - async def get_watched_users(self): """Retrieves watched users from the API.""" await self.bot.wait_until_ready() async with self.bot.http_session.get(URLs.site_bigbrother_api, headers=self.HEADERS) as response: data = await response.json() - await self.update_cache(data) + self.update_cache(data) async def get_watch_reason(self, user_id: int) -> str: """ Fetches and returns the latest watch reason for a user using the infraction API """ @@ -90,8 +87,8 @@ class BigBrother: return "(error retrieving bb reason)" if infraction_list: - latest_reason_infraction = max(infraction_list, key=self._parse_time) - latest_reason = latest_reason_infraction['reason'][10:] + latest_reason_infraction = max(infraction_list, key=self._parse_infraction_time) + latest_reason = latest_reason_infraction['reason'][len(self.infraction_watch_prefix):] log.trace(f"The latest bb watch reason for {user_id}: {latest_reason}") return latest_reason @@ -99,7 +96,7 @@ class BigBrother: return "(no reason specified)" @staticmethod - def _parse_time(infraction: str) -> struct_time: + def _parse_infraction_time(infraction: str) -> struct_time: """Takes RFC1123 date_time string and returns time object for sorting purposes""" date_string = infraction["inserted_at"] @@ -183,6 +180,12 @@ class BigBrother: # Send header if user/channel are different or if message limit exceeded. if message.author.id != last_user or message.channel.id != last_channel or msg_count > limit: + # Retrieve watch reason from API if it's not already in the cache + if message.author.id not in self.watch_reasons: + log.trace(f"No watch reason for {message.author.id} found in cache; retrieving from API") + user_watch_reason = await self.get_watch_reason(message.author.id) + self.watch_reasons[message.author.id] = user_watch_reason + self.last_log = [message.author.id, message.channel.id, 0] embed = Embed(description=f"{message.author.mention} in [#{message.channel.name}]({message.jump_url})") @@ -246,7 +249,7 @@ class BigBrother: async with self.bot.http_session.get(URLs.site_bigbrother_api, headers=self.HEADERS) as response: if response.status == 200: data = await response.json() - await self.update_cache(data) + self.update_cache(data) lines = tuple(f"• <@{entry['user_id']}> in <#{entry['channel_id']}>" for entry in data) await LinePaginator.paginate( -- cgit v1.2.3 From 5dd611793c598508c5be8868725ca5400ad0c304 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Fri, 11 Jan 2019 11:01:53 +0100 Subject: Using stronger language in the message and emphazising the 'strongly recommend' with bold --- bot/cogs/token_remover.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bot/cogs/token_remover.py b/bot/cogs/token_remover.py index 8277513a7..c1a0e18ba 100644 --- a/bot/cogs/token_remover.py +++ b/bot/cogs/token_remover.py @@ -16,8 +16,9 @@ log = logging.getLogger(__name__) DELETION_MESSAGE_TEMPLATE = ( "Hey {mention}! I noticed you posted a seemingly valid Discord API " - "token in your message and have removed your message to prevent abuse. " - "We recommend regenerating your token regardless, which you can do here: " + "token in your message and have removed your message. " + "We **strongly recommend** regenerating your token as it's probably " + "been compromised. You can do that here: " "\n" "Feel free to re-post it with the token removed. " "If you believe this was a mistake, please let us know!" -- cgit v1.2.3 From 8f30b52fd378bb3546c84d0fdb945a4b492236b8 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Fri, 11 Jan 2019 15:06:12 +0100 Subject: Catching the superclass CheckFailure instead of the subclass --- bot/cogs/help.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/help.py b/bot/cogs/help.py index c82a25417..ded068123 100644 --- a/bot/cogs/help.py +++ b/bot/cogs/help.py @@ -6,10 +6,10 @@ from contextlib import suppress from discord import Colour, Embed, HTTPException from discord.ext import commands +from discord.ext.commands import CheckFailure from fuzzywuzzy import fuzz, process from bot import constants -from bot.decorators import InChannelCheckFailure from bot.pagination import ( DELETE_EMOJI, FIRST_EMOJI, LAST_EMOJI, LEFT_EMOJI, LinePaginator, RIGHT_EMOJI, @@ -435,7 +435,7 @@ class HelpSession: # the mean time. try: can_run = await command.can_run(self._ctx) - except InChannelCheckFailure: + except CheckFailure: can_run = False if not can_run: -- cgit v1.2.3 From 0651779383423ad08b860cde79d29c3e00dca677 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Fri, 11 Jan 2019 15:14:43 +0100 Subject: Revert "Catching the superclass CheckFailure instead of the subclass" This reverts commit 8f30b52fd378bb3546c84d0fdb945a4b492236b8. Accidentally pushed to master when I thought I was at a branch --- bot/cogs/help.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/help.py b/bot/cogs/help.py index ded068123..c82a25417 100644 --- a/bot/cogs/help.py +++ b/bot/cogs/help.py @@ -6,10 +6,10 @@ from contextlib import suppress from discord import Colour, Embed, HTTPException from discord.ext import commands -from discord.ext.commands import CheckFailure from fuzzywuzzy import fuzz, process from bot import constants +from bot.decorators import InChannelCheckFailure from bot.pagination import ( DELETE_EMOJI, FIRST_EMOJI, LAST_EMOJI, LEFT_EMOJI, LinePaginator, RIGHT_EMOJI, @@ -435,7 +435,7 @@ class HelpSession: # the mean time. try: can_run = await command.can_run(self._ctx) - except CheckFailure: + except InChannelCheckFailure: can_run = False if not can_run: -- cgit v1.2.3 From 3905e8ff52ae652c94a1ff0372aa044709060774 Mon Sep 17 00:00:00 2001 From: sco1 Date: Fri, 11 Jan 2019 09:48:50 -0500 Subject: Disable rich embed filter Discord adding the embeds is causing it to trigger. --- config-default.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-default.yml b/config-default.yml index 21d7f20b9..866a5b5ab 100644 --- a/config-default.yml +++ b/config-default.yml @@ -137,7 +137,7 @@ filter: filter_zalgo: false filter_invites: true filter_domains: true - filter_rich_embeds: true + filter_rich_embeds: false watch_words: true watch_tokens: true -- cgit v1.2.3 From 4bc92be7c4831fa3b714d7091700d587f8f62373 Mon Sep 17 00:00:00 2001 From: sco1 Date: Fri, 11 Jan 2019 22:39:15 -0500 Subject: Add edit delta to modlog for multi-edit messages To help with self-bot detection, if a message has been previously edited, generate a human-readable delta between the last edit and the new one Use message timestamp for modlog embeds generated during on_message event Visually separate send_log_message kwargs to make them easier to read --- bot/cogs/modlog.py | 47 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py index 06f81cb36..bded3baa0 100644 --- a/bot/cogs/modlog.py +++ b/bot/cogs/modlog.py @@ -104,9 +104,19 @@ class ModLog: self._ignored[event].append(item) async def send_log_message( - self, icon_url: Optional[str], colour: Colour, title: Optional[str], text: str, - thumbnail: str = None, channel_id: int = Channels.modlog, ping_everyone: bool = False, - files: List[File] = None, content: str = None, additional_embeds: List[Embed] = None, + self, + icon_url: Optional[str], + colour: Colour, + title: Optional[str], + text: str, + thumbnail: str = None, + channel_id: int = Channels.modlog, + ping_everyone: bool = False, + files: List[File] = None, + content: str = None, + additional_embeds: List[Embed] = None, + timestamp_override: datetime.datetime = None, + footer_override: str = None, ): embed = Embed(description=text) @@ -114,7 +124,14 @@ class ModLog: embed.set_author(name=title, icon_url=icon_url) embed.colour = colour - embed.timestamp = datetime.datetime.utcnow() + + if timestamp_override: + embed.timestamp = timestamp_override + else: + embed.timestamp = datetime.datetime.utcnow() + + if footer_override: + embed.set_footer(text=footer_override) if thumbnail is not None: embed.set_thumbnail(url=thumbnail) @@ -676,14 +693,28 @@ class ModLog: f"{after.clean_content}" ) + if before.edited_at: + # Message was previously edited, to assist with self-bot detection, use the edited_at + # datetime as the baseline and create a human-readable delta between this edit event + # and the last time the message was edited + timestamp = before.edited_at + delta = humanize_delta(relativedelta(after.edited_at, before.edited_at)) + footer = f"Last edited {delta} ago" + else: + # Message was not previously edited, use the created_at datetime as the baseline, no + # delta calculation needed + timestamp = before.created_at + footer = None + + print(timestamp, footer) await self.send_log_message( - Icons.message_edit, Colour.blurple(), "Message edited (Before)", - before_response, channel_id=Channels.message_log + Icons.message_edit, Colour.blurple(), "Message edited (Before)", before_response, + channel_id=Channels.message_log, timestamp_override=timestamp, footer_override=footer ) await self.send_log_message( - Icons.message_edit, Colour.blurple(), "Message edited (After)", - after_response, channel_id=Channels.message_log + Icons.message_edit, Colour.blurple(), "Message edited (After)", after_response, + channel_id=Channels.message_log, timestamp_override=after.edited_at ) async def on_raw_message_edit(self, event: RawMessageUpdateEvent): -- cgit v1.2.3 From ddfb1f4689c54a8f7adccd90bd9038e69ff67168 Mon Sep 17 00:00:00 2001 From: sco1 Date: Fri, 11 Jan 2019 23:12:07 -0500 Subject: Remove debug print statement --- bot/cogs/modlog.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py index bded3baa0..76a5eff58 100644 --- a/bot/cogs/modlog.py +++ b/bot/cogs/modlog.py @@ -706,7 +706,6 @@ class ModLog: timestamp = before.created_at footer = None - print(timestamp, footer) await self.send_log_message( Icons.message_edit, Colour.blurple(), "Message edited (Before)", before_response, channel_id=Channels.message_log, timestamp_override=timestamp, footer_override=footer -- cgit v1.2.3 From 4dc7ba5b54ab8de7f86f5239b956d94d80146c85 Mon Sep 17 00:00:00 2001 From: SebastiaanZ <33516116+SebastiaanZ@users.noreply.github.com> Date: Sat, 12 Jan 2019 15:04:17 +0100 Subject: Changing check-specific exception to superclass exception so help doesn't break when new check exceptions are added --- bot/cogs/help.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/help.py b/bot/cogs/help.py index c82a25417..ded068123 100644 --- a/bot/cogs/help.py +++ b/bot/cogs/help.py @@ -6,10 +6,10 @@ from contextlib import suppress from discord import Colour, Embed, HTTPException from discord.ext import commands +from discord.ext.commands import CheckFailure from fuzzywuzzy import fuzz, process from bot import constants -from bot.decorators import InChannelCheckFailure from bot.pagination import ( DELETE_EMOJI, FIRST_EMOJI, LAST_EMOJI, LEFT_EMOJI, LinePaginator, RIGHT_EMOJI, @@ -435,7 +435,7 @@ class HelpSession: # the mean time. try: can_run = await command.can_run(self._ctx) - except InChannelCheckFailure: + except CheckFailure: can_run = False if not can_run: -- cgit v1.2.3 From e4a707157fe9cadef5debef67779a5443b48d11e Mon Sep 17 00:00:00 2001 From: sco1 Date: Sat, 12 Jan 2019 13:16:58 -0500 Subject: Add Optional type hints where appropriate --- bot/cogs/modlog.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py index 76a5eff58..589052b1e 100644 --- a/bot/cogs/modlog.py +++ b/bot/cogs/modlog.py @@ -109,14 +109,14 @@ class ModLog: colour: Colour, title: Optional[str], text: str, - thumbnail: str = None, + thumbnail: Optional[str] = None, channel_id: int = Channels.modlog, ping_everyone: bool = False, - files: List[File] = None, - content: str = None, - additional_embeds: List[Embed] = None, - timestamp_override: datetime.datetime = None, - footer_override: str = None, + files: Optional[List[File]] = None, + content: Optional[str] = None, + additional_embeds: Optional[List[Embed]] = None, + timestamp_override: Optional[datetime.datetime] = None, + footer_override: Optional[str] = None, ): embed = Embed(description=text) -- cgit v1.2.3 From 3fc94a97dc88504c29985fff735b346e2122c4a2 Mon Sep 17 00:00:00 2001 From: sco1 Date: Sat, 12 Jan 2019 13:22:11 -0500 Subject: Condense logic --- bot/cogs/modlog.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py index 589052b1e..55611c5e4 100644 --- a/bot/cogs/modlog.py +++ b/bot/cogs/modlog.py @@ -125,15 +125,12 @@ class ModLog: embed.colour = colour - if timestamp_override: - embed.timestamp = timestamp_override - else: - embed.timestamp = datetime.datetime.utcnow() + embed.timestamp = timestamp_override or datetime.datetime.utcnow() if footer_override: embed.set_footer(text=footer_override) - if thumbnail is not None: + if thumbnail: embed.set_thumbnail(url=thumbnail) if ping_everyone: -- cgit v1.2.3