From f7161f316b9844321584c126c046196ec5dacd5d Mon Sep 17 00:00:00 2001 From: WrongEnd Date: Wed, 18 Jul 2018 12:33:32 -0500 Subject: Adds framework for commands and warning command. --- bot/cogs/moderation.py | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++ bot/constants.py | 4 +++ 2 files changed, 84 insertions(+) create mode 100644 bot/cogs/moderation.py diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py new file mode 100644 index 000000000..80712b259 --- /dev/null +++ b/bot/cogs/moderation.py @@ -0,0 +1,80 @@ +import logging +from datetime import datetime, timedelta + +from discord import Colour, Embed, User, utils +from discord.ext.commands import Bot, Context, command + +from bot.constants import Channels, Keys, Roles, URLs +from bot.decorators import with_role + +log = logging.getLogger(__name__) + + +class Defcon: + """ + Rowboat replacement moderation tools. + """ + + def __init__(self, bot: Bot): + self.bot = bot + self.headers = {"X-API-KEY": Keys.site_api} + + + @with_role(Roles.admin, Roles.owner, Roles.moderator) + @command(name="moderation.warn") + async def warn(self, ctx, user: User, reason: str): + """ + Create a warning infraction in the database for a user. + :param user: accepts user mention, ID, etc. + :param reason: Wrap in quotes to make a warning larger than one word. + """ + + try: + response = await self.bot.http_session.put( + URLs.site_infractions, + headers=self.headers, + json={ + "type": "warning", + "reason": reason, + "user_id": str(user.id), + "actor_id": str(ctx.message.author.id) + } + ) + except Exception: + # Same as defcon. Probably not the best but may work for now. + log.Exception("There was an error adding an infraction.") + await ctx.send("There was an error updating the site.") + return + + await ctx.send("Warning added.") + + @with_role(Roles.admin, Roles.owner, Roles.moderator) + @command(name="moderation.ban") + async def ban(self, ctx, user: User, reason: str, duration: str=None): + """ + Create a banning infraction in the database for a user. + :param user: Accepts user mention, ID, etc. + :param reason: Wrap in quotes to make reason larger than one word. + :param duration: Accepts #d, #h, #m, and #s. + """ + + @with_role(Roles.admin, Roles.owner, Roles.moderator) + @command(name="moderation.mute") + async def mute(self, ctx, user: User, reason: str, duration: str=None): + """ + Create a muting infraction in the database for a user. + :param user: Accepts user mention, ID, etc. + :param reason: Wrap in quotes to make reason larger than one word. + :param duration: Accepts #d, #h, #m, and #s. + """ + + + + + +def setup(bot): + bot.add_cog(Moderation(bot)) + # Here we'll need to call a command I haven't made yet + # It'll check the expiry queue and automatically set up tasks for + # temporary bans, mutes, etc. + log.info("Cog loaded: Moderation") diff --git a/bot/constants.py b/bot/constants.py index 17ec1779e..4c7a7f09d 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -262,6 +262,10 @@ class URLs(metaclass=YAMLGetter): site_tags_api: str site_user_api: str site_user_complete_api: str + site_infractions: str + site_infractions_user: str + site_infractions_types: str + site_infractions_by_id: str status: str paste_service: str -- cgit v1.2.3 From 480da64050d7c1702e700586421e34bc284dd392 Mon Sep 17 00:00:00 2001 From: momothereal Date: Wed, 18 Jul 2018 16:50:53 -0400 Subject: Add default infraction URLs to config, cleanup moderation cog --- bot/cogs/moderation.py | 25 ++++++++++--------------- bot/constants.py | 2 +- config-default.yml | 4 ++++ 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 80712b259..23188ea57 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -1,16 +1,15 @@ import logging -from datetime import datetime, timedelta -from discord import Colour, Embed, User, utils -from discord.ext.commands import Bot, Context, command +from discord import User +from discord.ext.commands import Bot, command -from bot.constants import Channels, Keys, Roles, URLs +from bot.constants import Keys, Roles, URLs from bot.decorators import with_role log = logging.getLogger(__name__) -class Defcon: +class Moderation: """ Rowboat replacement moderation tools. """ @@ -19,18 +18,17 @@ class Defcon: self.bot = bot self.headers = {"X-API-KEY": Keys.site_api} - @with_role(Roles.admin, Roles.owner, Roles.moderator) @command(name="moderation.warn") - async def warn(self, ctx, user: User, reason: str): + async def warn(self, ctx, user: User, *, reason: str): """ Create a warning infraction in the database for a user. :param user: accepts user mention, ID, etc. - :param reason: Wrap in quotes to make a warning larger than one word. + :param reason: the reason for the warning. """ try: - response = await self.bot.http_session.put( + response = await self.bot.http_session.post( URLs.site_infractions, headers=self.headers, json={ @@ -42,7 +40,7 @@ class Defcon: ) except Exception: # Same as defcon. Probably not the best but may work for now. - log.Exception("There was an error adding an infraction.") + log.exception("There was an error adding an infraction.") await ctx.send("There was an error updating the site.") return @@ -50,7 +48,7 @@ class Defcon: @with_role(Roles.admin, Roles.owner, Roles.moderator) @command(name="moderation.ban") - async def ban(self, ctx, user: User, reason: str, duration: str=None): + async def ban(self, ctx, user: User, reason: str, duration: str = None): """ Create a banning infraction in the database for a user. :param user: Accepts user mention, ID, etc. @@ -60,7 +58,7 @@ class Defcon: @with_role(Roles.admin, Roles.owner, Roles.moderator) @command(name="moderation.mute") - async def mute(self, ctx, user: User, reason: str, duration: str=None): + async def mute(self, ctx, user: User, reason: str, duration: str = None): """ Create a muting infraction in the database for a user. :param user: Accepts user mention, ID, etc. @@ -69,9 +67,6 @@ class Defcon: """ - - - def setup(bot): bot.add_cog(Moderation(bot)) # Here we'll need to call a command I haven't made yet diff --git a/bot/constants.py b/bot/constants.py index 4c7a7f09d..c435fbad9 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -264,7 +264,7 @@ class URLs(metaclass=YAMLGetter): site_user_complete_api: str site_infractions: str site_infractions_user: str - site_infractions_types: str + site_infractions_type: str site_infractions_by_id: str status: str paste_service: str diff --git a/config-default.yml b/config-default.yml index 5b87f8d78..8f0c32af9 100644 --- a/config-default.yml +++ b/config-default.yml @@ -85,5 +85,9 @@ urls: site_tags_api: 'https://api.pythondiscord.com/bot/tags' site_user_api: 'https://api.pythondiscord.com/bot/users' site_user_complete_api: 'https://api.pythondiscord.com/bot/users/complete' + site_infractions: 'https://api.pythondiscord.com/bot/infractions' + site_infractions_user: 'https://api.pythondiscord.com/bot/infractions/user/{user_id}' + site_infractions_type: 'https://api.pythondiscord.com/bot/infractions/type/{infraction_type}' + site_infractions_by_id: 'https://api.pythondiscord.com/bot/infractions/id/{infraction_id}' status: !ENV 'STATUS_URL' paste_service: 'https://paste.pydis.com/{key}' -- cgit v1.2.3 From 81e391a48139c1220f9aee66fd539c99e3ab752f Mon Sep 17 00:00:00 2001 From: momothereal Date: Wed, 18 Jul 2018 17:06:13 -0400 Subject: Revert change to warning command format --- bot/cogs/moderation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 23188ea57..d3f8511e0 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -20,11 +20,11 @@ class Moderation: @with_role(Roles.admin, Roles.owner, Roles.moderator) @command(name="moderation.warn") - async def warn(self, ctx, user: User, *, reason: str): + async def warn(self, ctx, user: User, reason: str): """ Create a warning infraction in the database for a user. :param user: accepts user mention, ID, etc. - :param reason: the reason for the warning. + :param reason: the reason for the warning. Wrap in string quotes for multiple words. """ try: -- cgit v1.2.3 From 061e3ec079be918870bc2041ee44f4fcb3b93aa6 Mon Sep 17 00:00:00 2001 From: momothereal Date: Wed, 18 Jul 2018 20:33:48 -0400 Subject: Make reason optional, register moderation cog --- bot/__main__.py | 1 + bot/cogs/moderation.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/bot/__main__.py b/bot/__main__.py index f470a42d6..e4fe78c40 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -67,6 +67,7 @@ bot.load_extension("bot.cogs.doc") bot.load_extension("bot.cogs.eval") bot.load_extension("bot.cogs.fun") bot.load_extension("bot.cogs.hiphopify") +bot.load_extension("bot.cogs.moderation") bot.load_extension("bot.cogs.off_topic_names") bot.load_extension("bot.cogs.snakes") bot.load_extension("bot.cogs.snekbox") diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index d3f8511e0..5c44aa751 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -20,7 +20,7 @@ class Moderation: @with_role(Roles.admin, Roles.owner, Roles.moderator) @command(name="moderation.warn") - async def warn(self, ctx, user: User, reason: str): + async def warn(self, ctx, user: User, reason: str = None): """ Create a warning infraction in the database for a user. :param user: accepts user mention, ID, etc. @@ -44,7 +44,14 @@ class Moderation: await ctx.send("There was an error updating the site.") return - await ctx.send("Warning added.") + response_data = await response.json() + reason = response_data["infraction"]["reason"] + if reason is None: + result_message = f":ok_hand: warned {user.mention}." + else: + result_message = f":ok_hand: warned {user.mention} ({reason})." + + await ctx.send(result_message) @with_role(Roles.admin, Roles.owner, Roles.moderator) @command(name="moderation.ban") -- cgit v1.2.3 From 7c43f3820c347d1f2cf31d1134bb899216e4f0e2 Mon Sep 17 00:00:00 2001 From: momothereal Date: Fri, 20 Jul 2018 12:12:59 -0400 Subject: Permanent ban and mute commands --- bot/cogs/moderation.py | 71 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 5c44aa751..1349d8607 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -1,7 +1,7 @@ import logging -from discord import User -from discord.ext.commands import Bot, command +from discord import Guild, Member, User +from discord.ext.commands import Bot, Context, command from bot.constants import Keys, Roles, URLs from bot.decorators import with_role @@ -20,7 +20,7 @@ class Moderation: @with_role(Roles.admin, Roles.owner, Roles.moderator) @command(name="moderation.warn") - async def warn(self, ctx, user: User, reason: str = None): + async def warn(self, ctx: Context, user: User, reason: str = None): """ Create a warning infraction in the database for a user. :param user: accepts user mention, ID, etc. @@ -28,7 +28,7 @@ class Moderation: """ try: - response = await self.bot.http_session.post( + await self.bot.http_session.post( URLs.site_infractions, headers=self.headers, json={ @@ -39,13 +39,10 @@ class Moderation: } ) except Exception: - # Same as defcon. Probably not the best but may work for now. log.exception("There was an error adding an infraction.") - await ctx.send("There was an error updating the site.") + await ctx.send("There was an error adding the infraction.") return - response_data = await response.json() - reason = response_data["infraction"]["reason"] if reason is None: result_message = f":ok_hand: warned {user.mention}." else: @@ -55,23 +52,71 @@ class Moderation: @with_role(Roles.admin, Roles.owner, Roles.moderator) @command(name="moderation.ban") - async def ban(self, ctx, user: User, reason: str, duration: str = None): + async def ban(self, ctx: Context, user: User, reason: str = None): """ - Create a banning infraction in the database for a user. + Create a permanent ban infraction in the database for a user. :param user: Accepts user mention, ID, etc. :param reason: Wrap in quotes to make reason larger than one word. - :param duration: Accepts #d, #h, #m, and #s. """ + try: + await self.bot.http_session.post( + URLs.site_infractions, + headers=self.headers, + json={ + "type": "ban", + "reason": reason, + "user_id": str(user.id), + "actor_id": str(ctx.message.author.id) + } + ) + except Exception: + log.exception("There was an error adding an infraction.") + await ctx.send("There was an error adding the infraction.") + return + + guild: Guild = ctx.guild + await guild.ban(user, reason=reason, delete_message_days=0) + + if reason is None: + result_message = f":ok_hand: permanently banned {user.mention}." + else: + result_message = f":ok_hand: permanently banned {user.mention} ({reason})." + + await ctx.send(result_message) @with_role(Roles.admin, Roles.owner, Roles.moderator) @command(name="moderation.mute") - async def mute(self, ctx, user: User, reason: str, duration: str = None): + async def mute(self, ctx: Context, user: Member, reason: str): """ - Create a muting infraction in the database for a user. + Create a permanent mute infraction in the database for a user. :param user: Accepts user mention, ID, etc. :param reason: Wrap in quotes to make reason larger than one word. :param duration: Accepts #d, #h, #m, and #s. """ + try: + await self.bot.http_session.post( + URLs.site_infractions, + headers=self.headers, + json={ + "type": "mute", + "reason": reason, + "user_id": str(user.id), + "actor_id": str(ctx.message.author.id) + } + ) + except Exception: + log.exception("There was an error adding an infraction.") + await ctx.send("There was an error adding the infraction.") + return + + await user.edit(reason=reason, mute=True) + + if reason is None: + result_message = f":ok_hand: permanently muted {user.mention}." + else: + result_message = f":ok_hand: permanently muted {user.mention} ({reason})." + + await ctx.send(result_message) def setup(bot): -- cgit v1.2.3 From 50548104768b500b12ef5773d3654565b3eb84d1 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 21 Jul 2018 02:46:12 +0200 Subject: initial commit for the cleaning cog. Not completely done, need to merge in master so I can work with new changes from modlog merge. --- bot/__main__.py | 1 + bot/cogs/clean.py | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++++ bot/constants.py | 9 ++++ config-default.yml | 2 + 4 files changed, 164 insertions(+) create mode 100644 bot/cogs/clean.py diff --git a/bot/__main__.py b/bot/__main__.py index 9014bce14..70336cb59 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -53,6 +53,7 @@ bot.load_extension("bot.cogs.events") # Commands, etc bot.load_extension("bot.cogs.bigbrother") bot.load_extension("bot.cogs.bot") +bot.load_extension("bot.cogs.clean") bot.load_extension("bot.cogs.cogs") # Local setups usually don't have the clickup key set, diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py new file mode 100644 index 000000000..8848befab --- /dev/null +++ b/bot/cogs/clean.py @@ -0,0 +1,152 @@ +import json +import logging +import random + +from discord import Embed, Message, User, Colour +from discord.ext.commands import Bot, Context, group + +from bot.constants import Roles, Clean, URLs, Keys, NEGATIVE_REPLIES +from bot.decorators import with_role + +log = logging.getLogger(__name__) + + +class Clean: + + def __init__(self, bot: Bot): + self.bot = bot + self.headers = {"X-API-KEY": Keys.site_api} + self.cleaning = False + + async def _upload_log(self, log_data): + """ + Uploads the log data to the database via + an API endpoint for uploading logs. + + Returns a URL that can be used to view the log. + """ + + response = await self.bot.http_session.post( + URLs.site_clean_api, + headers=self.headers, + json={"log_data": log_data} + ) + + data = await response.json() + log_id = data["log_id"] + + return f"{URLs.site_clean_logs}/{log_id}" + + async def _clean_messages(self, amount, channel, bots_only: bool=False, user: User=None): + """ + A helper function that does the actual message cleaning. + + :param bots_only: Set this to True if you only want to delete bot messages. + :param user: Specify a user and it will only delete messages by this user. + :return: Returns an embed + """ + + # Is this an acceptable amount of messages to clean? + if amount > Clean.message_limit: + embed = Embed( + color=Colour.red(), + title=random.choice(NEGATIVE_REPLIES), + description=f"You cannot clean more than {Clean.message_limit} messages." + ) + return embed + + # Are we already performing a clean? + if self.cleaning: + embed = Embed( + color=Colour.red(), + title=random.choice(NEGATIVE_REPLIES), + description="Multiple simultaneous cleaning processes is not allowed." + ) + return embed + + # Skip the first message, as that will be the invocation + history = channel.history(limit=amount) + await history.next() + + message_log = [] + + async for message in history: + + delete_condition = ( + bots_only and message.author.bot # Delete bot messages + or user and message.author == user # Delete user messages + or not bots_only and not user # Delete all messages + ) + + if delete_condition: + await message.delete() + content = message.content or message.embeds[0].description + author = f"{message.author.name}#{message.author.discriminator}" + message_log.append({ + "content": content, + "author": author, + "timestamp": message.created_at.strftime("%D %H:%M") + }) + + if message_log: + # Reverse the list to restore chronological order + message_log = list(reversed(message_log)) + upload_log = await self._upload_log(message_log) + else: + upload_log = "Naw, nothing there!" + + embed = Embed( + description=upload_log + ) + + return embed + + @group(invoke_without_command=True, name="clean", hidden=True) + @with_role(Roles.moderator, Roles.admin, Roles.owner) + async def clean_group(self, ctx: Context): + """ + Commands for cleaning messages in channels + """ + + await ctx.invoke(self.bot.get_command("help"), "clean") + + @clean_group.command(aliases=["user"]) + @with_role(Roles.moderator, Roles.admin, Roles.owner) + async def clean_user(self, ctx: Context, user: User, amount: int = 10): + """ + Delete messages posted by the provided user, + and stop cleaning after traversing `amount` messages. + """ + + embed = await self._clean_messages(amount, ctx.channel, user=user) + + await ctx.send(embed=embed) + + @clean_group.command(aliases=["all"]) + @with_role(Roles.moderator, Roles.admin, Roles.owner) + async def clean_all(self, ctx: Context, amount: int = 10): + """ + Delete all messages, regardless of posted, + and stop cleaning after traversing `amount` messages. + """ + + embed = await self._clean_messages(amount, ctx.channel) + + await ctx.send(embed=embed) + + @clean_group.command(aliases=["bots"]) + @with_role(Roles.moderator, Roles.admin, Roles.owner) + async def clean_bots(self, ctx: Context, amount: int = 10): + """ + Delete all messages posted by a bot, + and stop cleaning after traversing `amount` messages. + """ + + embed = await self._clean_messages(amount, ctx.channel, bots_only=True) + + await ctx.send(embed=embed) + + +def setup(bot): + bot.add_cog(Clean(bot)) + log.info("Cog loaded: Clean") diff --git a/bot/constants.py b/bot/constants.py index 7248cd36a..6a8eb6fe2 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -176,6 +176,13 @@ class Emojis(metaclass=YAMLGetter): white_chevron: str +class Clean(metaclass=YAMLGetter): + section = "bot" + subsection = "clean" + + message_limit: int + + class Channels(metaclass=YAMLGetter): section = "guild" subsection = "channels" @@ -259,6 +266,8 @@ class URLs(metaclass=YAMLGetter): omdb: str site: str site_facts_api: str + site_clean_api: str + site_clean_logs: str site_hiphopify_api: str site_idioms_api: str site_names_api: str diff --git a/config-default.yml b/config-default.yml index bd77e695e..ca15040e8 100644 --- a/config-default.yml +++ b/config-default.yml @@ -80,6 +80,8 @@ urls: omdb: 'http://omdbapi.com' site: 'pythondiscord.com' site_bigbrother_api: 'https://api.pythondiscord.com/bot/bigbrother' + site_clean_api: 'https://api.pythondiscord.com/bot/clean' + site_clean_logs: 'https://pythondiscord.com/bot/clean_logs' site_docs_api: 'https://api.pythondiscord.com/bot/docs' site_facts_api: 'https://api.pythondiscord.com/bot/snake_facts' site_hiphopify_api: 'https://api.pythondiscord.com/bot/hiphopify' -- cgit v1.2.3 From 3fd68ad50801f4c5fc006fa043fc4fac4f50c9fe Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 21 Jul 2018 04:46:08 +0200 Subject: Completed clean cog --- bot/cogs/clean.py | 132 +++++++++++++++++++++++++++++++++++++++++------------ bot/constants.py | 2 +- config-default.yml | 4 ++ 3 files changed, 108 insertions(+), 30 deletions(-) diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py index 8848befab..7675206a7 100644 --- a/bot/cogs/clean.py +++ b/bot/cogs/clean.py @@ -1,15 +1,20 @@ -import json import logging import random -from discord import Embed, Message, User, Colour +from discord import Colour, Embed, Message, User from discord.ext.commands import Bot, Context, group -from bot.constants import Roles, Clean, URLs, Keys, NEGATIVE_REPLIES +from bot.cogs.modlog import ModLog +from bot.constants import ( + Channels, CleanMessages, Icons, + Keys, NEGATIVE_REPLIES, Roles, URLs +) from bot.decorators import with_role log = logging.getLogger(__name__) +COLOUR_RED = Colour(0xcd6d6d) + class Clean: @@ -18,7 +23,11 @@ class Clean: self.headers = {"X-API-KEY": Keys.site_api} self.cleaning = False - async def _upload_log(self, log_data): + @property + def mod_log(self) -> ModLog: + return self.bot.get_cog("ModLog") + + async def _upload_log(self, log_data: list) -> str: """ Uploads the log data to the database via an API endpoint for uploading logs. @@ -37,23 +46,33 @@ class Clean: return f"{URLs.site_clean_logs}/{log_id}" - async def _clean_messages(self, amount, channel, bots_only: bool=False, user: User=None): + async def _clean_messages( + self, amount: int, ctx: Context, + bots_only: bool=False, user: User=None + ): """ A helper function that does the actual message cleaning. :param bots_only: Set this to True if you only want to delete bot messages. :param user: Specify a user and it will only delete messages by this user. - :return: Returns an embed """ + # Bulk delete checks + def predicate_bots_only(message: Message): + return message.author.bot + + def predicate_specific_user(message: Message): + return message.author == user + # Is this an acceptable amount of messages to clean? - if amount > Clean.message_limit: + if amount > CleanMessages.message_limit: embed = Embed( color=Colour.red(), title=random.choice(NEGATIVE_REPLIES), - description=f"You cannot clean more than {Clean.message_limit} messages." + description=f"You cannot clean more than {CleanMessages.message_limit} messages." ) - return embed + await ctx.send(embed=embed) + return # Are we already performing a clean? if self.cleaning: @@ -62,44 +81,89 @@ class Clean: title=random.choice(NEGATIVE_REPLIES), description="Multiple simultaneous cleaning processes is not allowed." ) - return embed - - # Skip the first message, as that will be the invocation - history = channel.history(limit=amount) - await history.next() + await ctx.send(embed=embed) + return + # Look through the history and retrieve message data message_log = [] + message_ids = [] - async for message in history: + self.cleaning = True - delete_condition = ( + async for message in ctx.channel.history(limit=amount): + + if not self.cleaning: + return + + delete = ( bots_only and message.author.bot # Delete bot messages or user and message.author == user # Delete user messages or not bots_only and not user # Delete all messages ) - if delete_condition: - await message.delete() + if delete and message.content or message.embeds: content = message.content or message.embeds[0].description author = f"{message.author.name}#{message.author.discriminator}" + + # Store the message data + message_ids.append(message.id) message_log.append({ "content": content, "author": author, "timestamp": message.created_at.strftime("%D %H:%M") }) + self.cleaning = False + + # We should ignore the ID's we stored, so we don't get mod-log spam. + self.mod_log.ignore_message_deletion(*message_ids) + + # Use bulk delete to actually do the cleaning. It's far faster. + if bots_only: + await ctx.channel.purge( + limit=amount, + check=predicate_bots_only, + ) + elif user: + await ctx.channel.purge( + limit=amount, + check=predicate_specific_user, + ) + else: + await ctx.channel.purge( + limit=amount + ) + + # Reverse the list to restore chronological order if message_log: - # Reverse the list to restore chronological order message_log = list(reversed(message_log)) upload_log = await self._upload_log(message_log) else: - upload_log = "Naw, nothing there!" + # Can't build an embed, nothing to clean! + embed = Embed( + color=Colour.red(), + description="No matching messages could be found." + ) + await ctx.send(embed=embed) + return + + # Build the embed and send it + message = ( + f"**{len(message_ids)}** messages deleted in <#{ctx.channel.id}> by **{ctx.author.name}**\n\n" + f"A log of the deleted messages can be found [here]({upload_log})." + ) embed = Embed( - description=upload_log + color=COLOUR_RED, + description=message + ) + + embed.set_author( + name=f"Bulk message delete", + icon_url=Icons.message_bulk_delete ) - return embed + await self.bot.get_channel(Channels.modlog).send(embed=embed) @group(invoke_without_command=True, name="clean", hidden=True) @with_role(Roles.moderator, Roles.admin, Roles.owner) @@ -118,9 +182,7 @@ class Clean: and stop cleaning after traversing `amount` messages. """ - embed = await self._clean_messages(amount, ctx.channel, user=user) - - await ctx.send(embed=embed) + await self._clean_messages(amount, ctx, user=user) @clean_group.command(aliases=["all"]) @with_role(Roles.moderator, Roles.admin, Roles.owner) @@ -130,9 +192,7 @@ class Clean: and stop cleaning after traversing `amount` messages. """ - embed = await self._clean_messages(amount, ctx.channel) - - await ctx.send(embed=embed) + await self._clean_messages(amount, ctx) @clean_group.command(aliases=["bots"]) @with_role(Roles.moderator, Roles.admin, Roles.owner) @@ -142,8 +202,22 @@ class Clean: and stop cleaning after traversing `amount` messages. """ - embed = await self._clean_messages(amount, ctx.channel, bots_only=True) + await self._clean_messages(amount, ctx, bots_only=True) + @clean_group.command(aliases=["stop", "cancel", "abort"]) + @with_role(Roles.moderator, Roles.admin, Roles.owner) + async def clean_cancel(self, ctx: Context): + """ + If there is an ongoing cleaning process, + attempt to immediately cancel it. + """ + + self.cleaning = False + + embed = Embed( + color=Colour.blurple(), + description="Clean interrupted." + ) await ctx.send(embed=embed) diff --git a/bot/constants.py b/bot/constants.py index 182510e99..b49cb48f6 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -236,7 +236,7 @@ class Icons(metaclass=YAMLGetter): user_update: str -class Clean(metaclass=YAMLGetter): +class CleanMessages(metaclass=YAMLGetter): section = "bot" subsection = "clean" diff --git a/config-default.yml b/config-default.yml index 6dc3cce63..118ba165d 100644 --- a/config-default.yml +++ b/config-default.yml @@ -37,6 +37,10 @@ bot: user_unban: "https://cdn.discordapp.com/emojis/469952898692808704.png" user_update: "https://cdn.discordapp.com/emojis/469952898684551168.png" + clean: + # Maximum number of messages to traverse for clean commands + message_limit: 10000 + guild: id: 267624335836053506 -- cgit v1.2.3 From ff8c0084d437d56d55f0a22a7f8614dd4031d080 Mon Sep 17 00:00:00 2001 From: momothereal Date: Sat, 21 Jul 2018 15:24:36 -0400 Subject: Add expiration scheduler + startup, tempmute command --- bot/cogs/moderation.py | 116 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 110 insertions(+), 6 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 1349d8607..48857faf3 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -1,8 +1,11 @@ +import asyncio +import datetime import logging from discord import Guild, Member, User from discord.ext.commands import Bot, Context, command +from bot import constants from bot.constants import Keys, Roles, URLs from bot.decorators import with_role @@ -18,6 +21,19 @@ class Moderation: self.bot = bot self.headers = {"X-API-KEY": Keys.site_api} + async def on_ready(self): + # Schedule expiration for previous infractions + response = await self.bot.http_session.get( + URLs.site_infractions, + params={"active": "true"}, + headers=self.headers + ) + infraction_list = await response.json() + loop = asyncio.get_event_loop() + for infraction_object in infraction_list: + if infraction_object["expires_at"] is not None: + loop.create_task(self._scheduled_expiration(infraction_object)) + @with_role(Roles.admin, Roles.owner, Roles.moderator) @command(name="moderation.warn") async def warn(self, ctx: Context, user: User, reason: str = None): @@ -40,7 +56,7 @@ class Moderation: ) except Exception: log.exception("There was an error adding an infraction.") - await ctx.send("There was an error adding the infraction.") + await ctx.send(":x: There was an error adding the infraction.") return if reason is None: @@ -71,11 +87,11 @@ class Moderation: ) except Exception: log.exception("There was an error adding an infraction.") - await ctx.send("There was an error adding the infraction.") + await ctx.send(":x: There was an error adding the infraction.") return guild: Guild = ctx.guild - await guild.ban(user, reason=reason, delete_message_days=0) + await guild.ban(user, reason=reason) if reason is None: result_message = f":ok_hand: permanently banned {user.mention}." @@ -86,12 +102,11 @@ class Moderation: @with_role(Roles.admin, Roles.owner, Roles.moderator) @command(name="moderation.mute") - async def mute(self, ctx: Context, user: Member, reason: str): + async def mute(self, ctx: Context, user: Member, reason: str = None): """ Create a permanent mute infraction in the database for a user. :param user: Accepts user mention, ID, etc. :param reason: Wrap in quotes to make reason larger than one word. - :param duration: Accepts #d, #h, #m, and #s. """ try: await self.bot.http_session.post( @@ -106,7 +121,7 @@ class Moderation: ) except Exception: log.exception("There was an error adding an infraction.") - await ctx.send("There was an error adding the infraction.") + await ctx.send(":x: There was an error adding the infraction.") return await user.edit(reason=reason, mute=True) @@ -118,6 +133,95 @@ class Moderation: await ctx.send(result_message) + @with_role(Roles.admin, Roles.owner, Roles.moderator) + @command(name="moderation.tempmute") + async def tempmute(self, ctx: Context, user: Member, duration: str, reason: str = None): + """ + Create a temporary mute infraction in the database for a user. + :param user: Accepts user mention, ID, etc. + :param duration: The duration for the temporary mute infraction + :param reason: Wrap in quotes to make reason larger than one word. + """ + try: + response = await self.bot.http_session.post( + URLs.site_infractions, + headers=self.headers, + json={ + "type": "mute", + "reason": reason, + "duration": duration, + "user_id": str(user.id), + "actor_id": str(ctx.message.author.id) + } + ) + except Exception: + log.exception("There was an error adding an infraction.") + await ctx.send(":x: There was an error adding the infraction.") + return + + response_object = await response.json() + if "error_code" in response_object: + # something went wrong + await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}") + return + + await user.edit(reason=reason, mute=True) + + infraction_object = response_object["infraction"] + infraction_expiration = infraction_object["expires_at"] + + loop = asyncio.get_event_loop() + loop.create_task(self._scheduled_expiration(infraction_object)) + + if reason is None: + result_message = f":ok_hand: muted {user.mention} until {infraction_expiration}." + else: + result_message = f":ok_hand: muted {user.mention} until {infraction_expiration} ({reason})." + + await ctx.send(result_message) + + async def _scheduled_expiration(self, infraction_object): + guild: Guild = self.bot.get_guild(constants.Guild.id) + infraction_id = infraction_object["id"] + infraction_type = infraction_object["type"] + + # transform expiration to delay in seconds + expiration_datetime = parse_rfc1123(infraction_object["expires_at"]) + delay = expiration_datetime - datetime.datetime.now(tz=datetime.timezone.utc) + delay_seconds = delay.total_seconds() + + if delay_seconds > 1.0: + log.debug(f"Scheduling expiration for infraction {infraction_id} in {delay_seconds} seconds") + await asyncio.sleep(delay_seconds) + + log.debug(f"Marking infraction {infraction_id} as inactive (expired).") + log.debug(infraction_object) + user_id = infraction_object["user"]["user_id"] + + if infraction_type == "mute": + member: Member = guild.get_member(user_id) + if member: + await member.edit(mute=False) + elif infraction_type == "ban": + user: User = self.bot.get_user(user_id) + await guild.unban(user) + + await self.bot.http_session.patch( + URLs.site_infractions, + headers=self.headers, + json={ + "id": infraction_id, + "active": False + } + ) + + +RFC1123_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" + + +def parse_rfc1123(time_str): + return datetime.datetime.strptime(time_str, RFC1123_FORMAT).replace(tzinfo=datetime.timezone.utc) + def setup(bot): bot.add_cog(Moderation(bot)) -- cgit v1.2.3 From ed9dd532055d48ec36b4df731d073e207b866ed4 Mon Sep 17 00:00:00 2001 From: momothereal Date: Sat, 21 Jul 2018 18:13:43 -0400 Subject: Improve dangling infraction support --- bot/cogs/moderation.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 48857faf3..c85adf33d 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -1,6 +1,7 @@ import asyncio import datetime import logging +from typing import Dict from discord import Guild, Member, User from discord.ext.commands import Bot, Context, command @@ -20,19 +21,20 @@ class Moderation: def __init__(self, bot: Bot): self.bot = bot self.headers = {"X-API-KEY": Keys.site_api} + self.expiration_tasks: Dict[str, asyncio.Task] = {} async def on_ready(self): # Schedule expiration for previous infractions response = await self.bot.http_session.get( URLs.site_infractions, - params={"active": "true"}, + params={"dangling": "true"}, headers=self.headers ) infraction_list = await response.json() loop = asyncio.get_event_loop() for infraction_object in infraction_list: if infraction_object["expires_at"] is not None: - loop.create_task(self._scheduled_expiration(infraction_object)) + self.schedule_expiration(loop, infraction_object) @with_role(Roles.admin, Roles.owner, Roles.moderator) @command(name="moderation.warn") @@ -171,7 +173,7 @@ class Moderation: infraction_expiration = infraction_object["expires_at"] loop = asyncio.get_event_loop() - loop.create_task(self._scheduled_expiration(infraction_object)) + self.schedule_expiration(loop, infraction_object) if reason is None: result_message = f":ok_hand: muted {user.mention} until {infraction_expiration}." @@ -180,6 +182,14 @@ class Moderation: await ctx.send(result_message) + def schedule_expiration(self, loop: asyncio.AbstractEventLoop, infraction_object: dict): + infraction_id = infraction_object["id"] + if infraction_id in self.expiration_tasks: + return + + task: asyncio.Task = loop.create_task(self._scheduled_expiration(infraction_object)) + self.expiration_tasks[infraction_id] = task + async def _scheduled_expiration(self, infraction_object): guild: Guild = self.bot.get_guild(constants.Guild.id) infraction_id = infraction_object["id"] @@ -195,7 +205,6 @@ class Moderation: await asyncio.sleep(delay_seconds) log.debug(f"Marking infraction {infraction_id} as inactive (expired).") - log.debug(infraction_object) user_id = infraction_object["user"]["user_id"] if infraction_type == "mute": -- cgit v1.2.3 From d321962d0d966bd9b77c05a89fcc2862a6e98a01 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 22 Jul 2018 12:15:09 +0200 Subject: Moving constants into the yaml, fixing feedback from gdudes review. --- bot/cogs/clean.py | 35 +++++++++++++++-------------------- bot/cogs/modlog.py | 29 +++++++++++++---------------- bot/constants.py | 13 +++++++++++-- config-default.yml | 15 +++++++++++---- 4 files changed, 50 insertions(+), 42 deletions(-) diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py index 7675206a7..fe9b14003 100644 --- a/bot/cogs/clean.py +++ b/bot/cogs/clean.py @@ -6,15 +6,13 @@ from discord.ext.commands import Bot, Context, group from bot.cogs.modlog import ModLog from bot.constants import ( - Channels, CleanMessages, Icons, + Channels, CleanMessages, Colours, Icons, Keys, NEGATIVE_REPLIES, Roles, URLs ) from bot.decorators import with_role log = logging.getLogger(__name__) -COLOUR_RED = Colour(0xcd6d6d) - class Clean: @@ -67,7 +65,7 @@ class Clean: # Is this an acceptable amount of messages to clean? if amount > CleanMessages.message_limit: embed = Embed( - color=Colour.red(), + color=Colour(Colours.soft_red), title=random.choice(NEGATIVE_REPLIES), description=f"You cannot clean more than {CleanMessages.message_limit} messages." ) @@ -77,7 +75,7 @@ class Clean: # Are we already performing a clean? if self.cleaning: embed = Embed( - color=Colour.red(), + color=Colour(Colours.soft_red), title=random.choice(NEGATIVE_REPLIES), description="Multiple simultaneous cleaning processes is not allowed." ) @@ -119,20 +117,17 @@ class Clean: self.mod_log.ignore_message_deletion(*message_ids) # Use bulk delete to actually do the cleaning. It's far faster. + predicate = None + if bots_only: - await ctx.channel.purge( - limit=amount, - check=predicate_bots_only, - ) + predicate = predicate_bots_only elif user: - await ctx.channel.purge( - limit=amount, - check=predicate_specific_user, - ) - else: - await ctx.channel.purge( - limit=amount - ) + predicate = predicate_specific_user + + await ctx.channel.purge( + limit=amount, + check=predicate + ) # Reverse the list to restore chronological order if message_log: @@ -141,7 +136,7 @@ class Clean: else: # Can't build an embed, nothing to clean! embed = Embed( - color=Colour.red(), + color=Colour(Colours.soft_red), description="No matching messages could be found." ) await ctx.send(embed=embed) @@ -154,7 +149,7 @@ class Clean: ) embed = Embed( - color=COLOUR_RED, + color=Colour(Colours.soft_red), description=message ) @@ -188,7 +183,7 @@ class Clean: @with_role(Roles.moderator, Roles.admin, Roles.owner) async def clean_all(self, ctx: Context, amount: int = 10): """ - Delete all messages, regardless of posted, + Delete all messages, regardless of poster, and stop cleaning after traversing `amount` messages. """ diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py index 2f13fe32b..dfeca473d 100644 --- a/bot/cogs/modlog.py +++ b/bot/cogs/modlog.py @@ -13,15 +13,12 @@ from discord import ( from discord.abc import GuildChannel from discord.ext.commands import Bot -from bot.constants import Channels, Emojis, Icons +from bot.constants import Channels, Colours, Emojis, Icons from bot.constants import Guild as GuildConstant log = logging.getLogger(__name__) -BULLET_POINT = "\u2022" -COLOUR_RED = Colour(0xcd6d6d) -COLOUR_GREEN = Colour(0x68c290) GUILD_CHANNEL = Union[CategoryChannel, TextChannel, VoiceChannel] CHANNEL_CHANGES_UNSUPPORTED = ("permissions",) @@ -85,7 +82,7 @@ class ModLog: else: message = f"{channel.name} (`{channel.id}`)" - await self.send_log_message(Icons.hash_green, COLOUR_GREEN, title, message) + await self.send_log_message(Icons.hash_green, Colour(Colours.soft_green), title, message) async def on_guild_channel_delete(self, channel: GUILD_CHANNEL): if channel.guild.id != GuildConstant.id: @@ -104,7 +101,7 @@ class ModLog: message = f"{channel.name} (`{channel.id}`)" await self.send_log_message( - Icons.hash_red, COLOUR_RED, + Icons.hash_red, Colour(Colours.soft_red), title, message ) @@ -150,7 +147,7 @@ class ModLog: message = "" for item in sorted(changes): - message += f"{BULLET_POINT} {item}\n" + message += f"{Emojis.bullet} {item}\n" if after.category: message = f"**{after.category}/#{after.name} (`{after.id}`)**\n{message}" @@ -167,7 +164,7 @@ class ModLog: return await self.send_log_message( - Icons.crown_green, COLOUR_GREEN, + Icons.crown_green, Colour(Colours.soft_green), "Role created", f"`{role.id}`" ) @@ -176,7 +173,7 @@ class ModLog: return await self.send_log_message( - Icons.crown_red, COLOUR_RED, + Icons.crown_red, Colour(Colours.soft_red), "Role removed", f"{role.name} (`{role.id}`)" ) @@ -222,7 +219,7 @@ class ModLog: message = "" for item in sorted(changes): - message += f"{BULLET_POINT} {item}\n" + message += f"{Emojis.bullet} {item}\n" message = f"**{after.name} (`{after.id}`)**\n{message}" @@ -270,7 +267,7 @@ class ModLog: message = "" for item in sorted(changes): - message += f"{BULLET_POINT} {item}\n" + message += f"{Emojis.bullet} {item}\n" message = f"**{after.name} (`{after.id}`)**\n{message}" @@ -285,7 +282,7 @@ class ModLog: return await self.send_log_message( - Icons.user_ban, COLOUR_RED, + Icons.user_ban, Colour(Colours.soft_red), "User banned", f"{member.name}#{member.discriminator} (`{member.id}`)", thumbnail=member.avatar_url_as(static_format="png") ) @@ -325,7 +322,7 @@ class ModLog: message = f"{Emojis.new} {message}" await self.send_log_message( - Icons.sign_in, COLOUR_GREEN, + Icons.sign_in, Colour(Colours.soft_green), "User joined", message, thumbnail=member.avatar_url_as(static_format="png") ) @@ -335,7 +332,7 @@ class ModLog: return await self.send_log_message( - Icons.sign_out, COLOUR_RED, + Icons.sign_out, Colour(Colours.soft_red), "User left", f"{member.name}#{member.discriminator} (`{member.id}`)", thumbnail=member.avatar_url_as(static_format="png") ) @@ -404,7 +401,7 @@ class ModLog: message = "" for item in sorted(changes): - message += f"{BULLET_POINT} {item}\n" + message += f"{Emojis.bullet} {item}\n" message = f"**{after.name}#{after.discriminator} (`{after.id}`)**\n{message}" @@ -482,7 +479,7 @@ class ModLog: ) await self.send_log_message( - Icons.message_delete, COLOUR_RED, + Icons.message_delete, Colour(Colours.soft_red), "Message deleted", response, channel_id=Channels.message_log diff --git a/bot/constants.py b/bot/constants.py index b49cb48f6..532a2af1d 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -198,20 +198,29 @@ class Cooldowns(metaclass=YAMLGetter): tags: int +class Colours(metaclass=YAMLGetter): + section = "style" + subsection = "colours" + + soft_red: int + soft_green: int + + class Emojis(metaclass=YAMLGetter): - section = "bot" + section = "style" subsection = "emojis" green_chevron: str red_chevron: str white_chevron: str + bullet: str new: str pencil: str class Icons(metaclass=YAMLGetter): - section = "bot" + section = "style" subsection = "icons" crown_blurple: str diff --git a/config-default.yml b/config-default.yml index 118ba165d..d55a50d57 100644 --- a/config-default.yml +++ b/config-default.yml @@ -6,12 +6,23 @@ bot: # Per channel, per tag. tags: 60 + clean: + # Maximum number of messages to traverse for clean commands + message_limit: 10000 + + +style: + colours: + soft_red: 0xcd6d6d + soft_green: 0x68c290 + emojis: green_chevron: "<:greenchevron:418104310329769993>" red_chevron: "<:redchevron:418112778184818698>" white_chevron: "<:whitechevron:418110396973711363>" lemoneye2: "<:lemoneye2:435193765582340098>" + bullet: "\u2022" pencil: "\u270F" new: "\U0001F195" @@ -37,10 +48,6 @@ bot: user_unban: "https://cdn.discordapp.com/emojis/469952898692808704.png" user_update: "https://cdn.discordapp.com/emojis/469952898684551168.png" - clean: - # Maximum number of messages to traverse for clean commands - message_limit: 10000 - guild: id: 267624335836053506 -- cgit v1.2.3 From fa39f51e8abaead97f86ee3b3624da23d6f01f20 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 22 Jul 2018 21:12:48 +0200 Subject: Sending the users top_role --- bot/cogs/clean.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py index fe9b14003..8df6d6e83 100644 --- a/bot/cogs/clean.py +++ b/bot/cogs/clean.py @@ -102,12 +102,14 @@ class Clean: if delete and message.content or message.embeds: content = message.content or message.embeds[0].description author = f"{message.author.name}#{message.author.discriminator}" + role = message.author.top_role.name # Store the message data message_ids.append(message.id) message_log.append({ "content": content, "author": author, + "role": role.lower(), "timestamp": message.created_at.strftime("%D %H:%M") }) -- cgit v1.2.3 From e8594ba79d54f27e810233619ed056e8720825ed Mon Sep 17 00:00:00 2001 From: momothereal Date: Mon, 23 Jul 2018 20:31:00 -0400 Subject: Add tempban, add function to cancel active expiration tasks --- bot/cogs/moderation.py | 97 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 7 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index c85adf33d..f2ab64850 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -46,7 +46,7 @@ class Moderation: """ try: - await self.bot.http_session.post( + response = await self.bot.http_session.post( URLs.site_infractions, headers=self.headers, json={ @@ -61,6 +61,12 @@ class Moderation: await ctx.send(":x: There was an error adding the infraction.") return + response_object = await response.json() + if "error_code" in response_object: + # something went wrong + await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}") + return + if reason is None: result_message = f":ok_hand: warned {user.mention}." else: @@ -77,7 +83,7 @@ class Moderation: :param reason: Wrap in quotes to make reason larger than one word. """ try: - await self.bot.http_session.post( + response = await self.bot.http_session.post( URLs.site_infractions, headers=self.headers, json={ @@ -92,6 +98,12 @@ class Moderation: await ctx.send(":x: There was an error adding the infraction.") return + response_object = await response.json() + if "error_code" in response_object: + # something went wrong + await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}") + return + guild: Guild = ctx.guild await guild.ban(user, reason=reason) @@ -111,7 +123,7 @@ class Moderation: :param reason: Wrap in quotes to make reason larger than one word. """ try: - await self.bot.http_session.post( + response = await self.bot.http_session.post( URLs.site_infractions, headers=self.headers, json={ @@ -126,6 +138,12 @@ class Moderation: await ctx.send(":x: There was an error adding the infraction.") return + response_object = await response.json() + if "error_code" in response_object: + # something went wrong + await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}") + return + await user.edit(reason=reason, mute=True) if reason is None: @@ -182,18 +200,76 @@ class Moderation: await ctx.send(result_message) + @with_role(Roles.admin, Roles.owner, Roles.moderator) + @command(name="moderation.tempban") + async def tempban(self, ctx, user: User, duration: str, reason: str = None): + """ + Create a temporary ban infraction in the database for a user. + :param user: Accepts user mention, ID, etc. + :param duration: The duration for the temporary ban infraction + :param reason: Wrap in quotes to make reason larger than one word. + """ + try: + response = await self.bot.http_session.post( + URLs.site_infractions, + headers=self.headers, + json={ + "type": "ban", + "reason": reason, + "duration": duration, + "user_id": str(user.id), + "actor_id": str(ctx.message.author.id) + } + ) + except Exception: + log.exception("There was an error adding an infraction.") + await ctx.send(":x: There was an error adding the infraction.") + return + + response_object = await response.json() + if "error_code" in response_object: + # something went wrong + await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}") + return + + guild: Guild = ctx.guild + await guild.ban(user, reason=reason) + + infraction_object = response_object["infraction"] + infraction_expiration = infraction_object["expires_at"] + + loop = asyncio.get_event_loop() + self.schedule_expiration(loop, infraction_object) + + if reason is None: + result_message = f":ok_hand: banned {user.mention} until {infraction_expiration}." + else: + result_message = f":ok_hand: banned {user.mention} until {infraction_expiration} ({reason})." + + await ctx.send(result_message) + def schedule_expiration(self, loop: asyncio.AbstractEventLoop, infraction_object: dict): infraction_id = infraction_object["id"] if infraction_id in self.expiration_tasks: return - task: asyncio.Task = loop.create_task(self._scheduled_expiration(infraction_object)) + task: asyncio.Task = asyncio.ensure_future(self._scheduled_expiration(infraction_object), loop=loop) + # Silently ignore exceptions in a callback (handles the CancelledError nonsense) + task.add_done_callback(lambda future: future.exception()) + self.expiration_tasks[infraction_id] = task + def cancel_expiration(self, infraction_id: str): + task = self.expiration_tasks.get(infraction_id) + if task is None: + log.warning(f"Failed to unschedule {infraction_id}: no task found.") + return + task.cancel() + log.debug(f"Unscheduled {infraction_id}.") + del self.expiration_tasks[infraction_id] + async def _scheduled_expiration(self, infraction_object): - guild: Guild = self.bot.get_guild(constants.Guild.id) infraction_id = infraction_object["id"] - infraction_type = infraction_object["type"] # transform expiration to delay in seconds expiration_datetime = parse_rfc1123(infraction_object["expires_at"]) @@ -205,7 +281,14 @@ class Moderation: await asyncio.sleep(delay_seconds) log.debug(f"Marking infraction {infraction_id} as inactive (expired).") + await self._deactivate_infraction(infraction_object) + + self.cancel_expiration(infraction_object["id"]) + + async def _deactivate_infraction(self, infraction_object): + guild: Guild = self.bot.get_guild(constants.Guild.id) user_id = infraction_object["user"]["user_id"] + infraction_type = infraction_object["type"] if infraction_type == "mute": member: Member = guild.get_member(user_id) @@ -219,7 +302,7 @@ class Moderation: URLs.site_infractions, headers=self.headers, json={ - "id": infraction_id, + "id": infraction_object["id"], "active": False } ) -- cgit v1.2.3 From ef1fc0e923ed0d9e730bcda20c4f5adfd3b46241 Mon Sep 17 00:00:00 2001 From: momothereal Date: Mon, 23 Jul 2018 22:20:40 -0400 Subject: Unmute and unban commands --- bot/cogs/moderation.py | 91 ++++++++++++++++++++++++++++++++++++++++++++++++-- bot/constants.py | 3 +- config-default.yml | 1 + 3 files changed, 91 insertions(+), 4 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index f2ab64850..8829c595d 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -248,14 +248,91 @@ class Moderation: await ctx.send(result_message) + @with_role(Roles.admin, Roles.owner, Roles.moderator) + @command(name="moderation.unmute") + async def unmute(self, ctx, user: Member): + """ + Deactivates the active mute infraction for a user. + :param user: Accepts user mention, ID, etc. + """ + try: + # check the current active infraction + response = await self.bot.http_session.get( + URLs.site_infractions_user_type_current.format( + user_id=user.id, + infraction_type="mute" + ), + headers=self.headers + ) + response_object = await response.json() + if "error_code" in response_object: + # something went wrong + await ctx.send(f":x: There was an error removing the infraction: {response_object['error_message']}") + return + + infraction_object = response_object["infraction"] + if infraction_object is None: + # no active infraction + await ctx.send(f":x: There is no active mute infraction for user {user.mention}.") + return + + await self._deactivate_infraction(infraction_object) + if infraction_object["expires_at"] is not None: + self.cancel_expiration(infraction_object["id"]) + + await ctx.send(f":ok_hand: Un-muted {user.mention}.") + except Exception: + log.exception("There was an error removing an infraction.") + await ctx.send(":x: There was an error removing the infraction.") + return + + @with_role(Roles.admin, Roles.owner, Roles.moderator) + @command(name="moderation.unban") + async def unban(self, ctx, user: User): + """ + Deactivates the active ban infraction for a user. + :param user: Accepts user mention, ID, etc. + """ + try: + # check the current active infraction + response = await self.bot.http_session.get( + URLs.site_infractions_user_type_current.format( + user_id=user.id, + infraction_type="ban" + ), + headers=self.headers + ) + response_object = await response.json() + if "error_code" in response_object: + # something went wrong + await ctx.send(f":x: There was an error removing the infraction: {response_object['error_message']}") + return + + infraction_object = response_object["infraction"] + if infraction_object is None: + # no active infraction + await ctx.send(f":x: There is no active ban infraction for user {user.mention}.") + return + + await self._deactivate_infraction(infraction_object, user=user) + if infraction_object["expires_at"] is not None: + self.cancel_expiration(infraction_object["id"]) + + await ctx.send(f":ok_hand: Un-banned {user.mention}.") + except Exception: + log.exception("There was an error removing an infraction.") + await ctx.send(":x: There was an error removing the infraction.") + return + def schedule_expiration(self, loop: asyncio.AbstractEventLoop, infraction_object: dict): infraction_id = infraction_object["id"] if infraction_id in self.expiration_tasks: return task: asyncio.Task = asyncio.ensure_future(self._scheduled_expiration(infraction_object), loop=loop) + # Silently ignore exceptions in a callback (handles the CancelledError nonsense) - task.add_done_callback(lambda future: future.exception()) + task.add_done_callback(_silent_exception) self.expiration_tasks[infraction_id] = task @@ -285,7 +362,7 @@ class Moderation: self.cancel_expiration(infraction_object["id"]) - async def _deactivate_infraction(self, infraction_object): + async def _deactivate_infraction(self, infraction_object, user: User = None): guild: Guild = self.bot.get_guild(constants.Guild.id) user_id = infraction_object["user"]["user_id"] infraction_type = infraction_object["type"] @@ -295,7 +372,8 @@ class Moderation: if member: await member.edit(mute=False) elif infraction_type == "ban": - user: User = self.bot.get_user(user_id) + if user is None: + user: User = self.bot.get_user(user_id) await guild.unban(user) await self.bot.http_session.patch( @@ -315,6 +393,13 @@ def parse_rfc1123(time_str): return datetime.datetime.strptime(time_str, RFC1123_FORMAT).replace(tzinfo=datetime.timezone.utc) +def _silent_exception(future): + try: + future.exception() + except Exception: + pass + + def setup(bot): bot.add_cog(Moderation(bot)) # Here we'll need to call a command I haven't made yet diff --git a/bot/constants.py b/bot/constants.py index c435fbad9..915373add 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -265,7 +265,8 @@ class URLs(metaclass=YAMLGetter): site_infractions: str site_infractions_user: str site_infractions_type: str - site_infractions_by_id: str + site_infractions_user_type_current: str + site_infractions_user_type: str status: str paste_service: str diff --git a/config-default.yml b/config-default.yml index 8f0c32af9..15cdd948c 100644 --- a/config-default.yml +++ b/config-default.yml @@ -89,5 +89,6 @@ urls: site_infractions_user: 'https://api.pythondiscord.com/bot/infractions/user/{user_id}' site_infractions_type: 'https://api.pythondiscord.com/bot/infractions/type/{infraction_type}' site_infractions_by_id: 'https://api.pythondiscord.com/bot/infractions/id/{infraction_id}' + site_infractions_user_type_current: 'https://api.pythondiscord.com/bot/infractions/user/{user_id}/{infraction_type}/current' status: !ENV 'STATUS_URL' paste_service: 'https://paste.pydis.com/{key}' -- cgit v1.2.3 From d5bdc261acaba5d736ccecadd51d660fcc2e034c Mon Sep 17 00:00:00 2001 From: momothereal Date: Mon, 23 Jul 2018 23:48:54 -0400 Subject: Add infraction search for user, converter for user-string-fallback --- bot/cogs/moderation.py | 69 +++++++++++++++++++++++++++++++++++++++++++++++++- bot/converters.py | 17 ++++++++++++- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 8829c595d..6eebd4f8d 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -3,12 +3,14 @@ import datetime import logging from typing import Dict -from discord import Guild, Member, User +from discord import Colour, Embed, Guild, Member, User from discord.ext.commands import Bot, Context, command from bot import constants from bot.constants import Keys, Roles, URLs +from bot.converters import InfractionSearchQuery from bot.decorators import with_role +from bot.pagination import LinePaginator log = logging.getLogger(__name__) @@ -36,6 +38,8 @@ class Moderation: if infraction_object["expires_at"] is not None: self.schedule_expiration(loop, infraction_object) + # Permanent infractions + @with_role(Roles.admin, Roles.owner, Roles.moderator) @command(name="moderation.warn") async def warn(self, ctx: Context, user: User, reason: str = None): @@ -153,6 +157,8 @@ class Moderation: await ctx.send(result_message) + # Temporary infractions + @with_role(Roles.admin, Roles.owner, Roles.moderator) @command(name="moderation.tempmute") async def tempmute(self, ctx: Context, user: Member, duration: str, reason: str = None): @@ -248,6 +254,8 @@ class Moderation: await ctx.send(result_message) + # Remove infractions (un- commands) + @with_role(Roles.admin, Roles.owner, Roles.moderator) @command(name="moderation.unmute") async def unmute(self, ctx, user: Member): @@ -324,6 +332,54 @@ class Moderation: await ctx.send(":x: There was an error removing the infraction.") return + @with_role(Roles.admin, Roles.owner, Roles.moderator) + @command(name="infraction.search") + async def search(self, ctx, arg: InfractionSearchQuery): + """ + Searches for infractions in the database. + :param arg: Either a user or a reason string. + """ + if isinstance(arg, User): + user: User = arg + # get infractions for this user + try: + response = await self.bot.http_session.get( + URLs.site_infractions_user.format( + user_id=user.id + ), + headers=self.headers + ) + infraction_list = await response.json() + except Exception: + log.exception("There was an error fetching infractions.") + await ctx.send(":x: There was an error fetching infraction.") + return + + if not infraction_list: + await ctx.send(f":warning: No infractions found for {user}.") + return + + embed = Embed( + title=f"Infractions for {user} ({len(infraction_list)} total)", + colour=Colour.orange() + ) + + await LinePaginator.paginate( + lines=(infraction_to_string(infraction_object) for infraction_object in infraction_list), + ctx=ctx, + embed=embed, + empty=True, + max_lines=5 + ) + + elif isinstance(arg, str): + # search by reason + return + else: + await ctx.send(":x: Invalid infraction search query.") + + # Utility functions + def schedule_expiration(self, loop: asyncio.AbstractEventLoop, infraction_object: dict): infraction_id = infraction_object["id"] if infraction_id in self.expiration_tasks: @@ -400,6 +456,17 @@ def _silent_exception(future): pass +def infraction_to_string(infraction_object): + return "\n".join(( + "===============", + "Type: **{0}**".format(infraction_object["type"]), + "Reason: {0}".format(infraction_object["reason"] or "*None*"), + "Created: {0}".format(infraction_object["inserted_at"]), + "Expires: {0}".format(infraction_object["expires_at"] or "*Permanent*"), + "===============", + )) + + def setup(bot): bot.add_cog(Moderation(bot)) # Here we'll need to call a command I haven't made yet diff --git a/bot/converters.py b/bot/converters.py index 5637ab8b2..f98e02015 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -4,7 +4,7 @@ from ssl import CertificateError import discord from aiohttp import AsyncResolver, ClientConnectorError, ClientSession, TCPConnector -from discord.ext.commands import BadArgument, Converter +from discord.ext.commands import BadArgument, Converter, UserConverter from fuzzywuzzy import fuzz from bot.constants import DEBUG_MODE, Keys, URLs @@ -157,3 +157,18 @@ class ValidURL(Converter): except ClientConnectorError: raise BadArgument(f"Cannot connect to host with URL `{url}`.") return url + + +class InfractionSearchQuery(Converter): + """ + A converter that checks if the argument is a Discord user, and if not, fall-backs to a string. + """ + + @staticmethod + async def convert(ctx, arg): + try: + user_converter = UserConverter() + user = await user_converter.convert(ctx, arg) + except Exception: + return arg + return user or arg -- cgit v1.2.3 From 79879317fd639eb795160c8e190186886f775e68 Mon Sep 17 00:00:00 2001 From: momothereal Date: Tue, 24 Jul 2018 01:03:36 -0400 Subject: Use 'Muted' role, show actor in infraction list --- bot/cogs/moderation.py | 50 +++++++++++++++++++++++++++++--------------------- bot/constants.py | 1 + config-default.yml | 1 + 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 6eebd4f8d..c4ae52401 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -3,7 +3,7 @@ import datetime import logging from typing import Dict -from discord import Colour, Embed, Guild, Member, User +from discord import Colour, Embed, Guild, Member, Object, User from discord.ext.commands import Bot, Context, command from bot import constants @@ -24,6 +24,7 @@ class Moderation: self.bot = bot self.headers = {"X-API-KEY": Keys.site_api} self.expiration_tasks: Dict[str, asyncio.Task] = {} + self._muted_role = Object(constants.Roles.muted) async def on_ready(self): # Schedule expiration for previous infractions @@ -148,7 +149,8 @@ class Moderation: await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}") return - await user.edit(reason=reason, mute=True) + # add the mute role + await user.add_roles(self._muted_role, reason=reason) if reason is None: result_message = f":ok_hand: permanently muted {user.mention}." @@ -191,7 +193,7 @@ class Moderation: await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}") return - await user.edit(reason=reason, mute=True) + await user.add_roles(self._muted_role, reason=reason) infraction_object = response_object["infraction"] infraction_expiration = infraction_object["expires_at"] @@ -322,7 +324,7 @@ class Moderation: await ctx.send(f":x: There is no active ban infraction for user {user.mention}.") return - await self._deactivate_infraction(infraction_object, user=user) + await self._deactivate_infraction(infraction_object) if infraction_object["expires_at"] is not None: self.cancel_expiration(infraction_object["id"]) @@ -365,7 +367,7 @@ class Moderation: ) await LinePaginator.paginate( - lines=(infraction_to_string(infraction_object) for infraction_object in infraction_list), + lines=(self.infraction_to_string(infraction_object) for infraction_object in infraction_list), ctx=ctx, embed=embed, empty=True, @@ -418,18 +420,20 @@ class Moderation: self.cancel_expiration(infraction_object["id"]) - async def _deactivate_infraction(self, infraction_object, user: User = None): + async def _deactivate_infraction(self, infraction_object): guild: Guild = self.bot.get_guild(constants.Guild.id) - user_id = infraction_object["user"]["user_id"] + user_id = int(infraction_object["user"]["user_id"]) infraction_type = infraction_object["type"] if infraction_type == "mute": member: Member = guild.get_member(user_id) if member: - await member.edit(mute=False) + # remove the mute role + await member.remove_roles(self._muted_role) + else: + log.warning(f"Failed to un-mute user: {user_id} (not found)") elif infraction_type == "ban": - if user is None: - user: User = self.bot.get_user(user_id) + user: User = self.bot.get_user(user_id) await guild.unban(user) await self.bot.http_session.patch( @@ -441,6 +445,21 @@ class Moderation: } ) + def infraction_to_string(self, infraction_object): + actor_id = int(infraction_object["actor"]["user_id"]) + guild: Guild = self.bot.get_guild(constants.Guild.id) + actor = guild.get_member(actor_id) + + return "\n".join(( + "===============", + "Type: **{0}**".format(infraction_object["type"]), + "Reason: {0}".format(infraction_object["reason"] or "*None*"), + "Created: {0}".format(infraction_object["inserted_at"]), + "Expires: {0}".format(infraction_object["expires_at"] or "*Permanent*"), + "Actor: {0}".format(actor.mention if actor else actor_id), + "===============", + )) + RFC1123_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" @@ -456,17 +475,6 @@ def _silent_exception(future): pass -def infraction_to_string(infraction_object): - return "\n".join(( - "===============", - "Type: **{0}**".format(infraction_object["type"]), - "Reason: {0}".format(infraction_object["reason"] or "*None*"), - "Created: {0}".format(infraction_object["inserted_at"]), - "Expires: {0}".format(infraction_object["expires_at"] or "*Permanent*"), - "===============", - )) - - def setup(bot): bot.add_cog(Moderation(bot)) # Here we'll need to call a command I haven't made yet diff --git a/bot/constants.py b/bot/constants.py index 915373add..163066fc0 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -209,6 +209,7 @@ class Roles(metaclass=YAMLGetter): moderator: int owner: int verified: int + muted: int class Guild(metaclass=YAMLGetter): diff --git a/config-default.yml b/config-default.yml index 15cdd948c..b7efb0010 100644 --- a/config-default.yml +++ b/config-default.yml @@ -44,6 +44,7 @@ guild: moderator: 267629731250176001 owner: 267627879762755584 verified: 352427296948486144 + muted: 0 keys: -- cgit v1.2.3 From 3a9b3b77fb350cdfd02970e6e00b1df383954b79 Mon Sep 17 00:00:00 2001 From: momothereal Date: Tue, 24 Jul 2018 02:08:07 -0400 Subject: Show active status in infraction list, fix Discord pagination rendering issue --- bot/cogs/moderation.py | 9 ++++++--- bot/pagination.py | 8 ++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index c4ae52401..69cdfb10c 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -371,7 +371,8 @@ class Moderation: ctx=ctx, embed=embed, empty=True, - max_lines=5 + max_lines=3, + max_size=1000 ) elif isinstance(arg, str): @@ -449,15 +450,17 @@ class Moderation: actor_id = int(infraction_object["actor"]["user_id"]) guild: Guild = self.bot.get_guild(constants.Guild.id) actor = guild.get_member(actor_id) + active = infraction_object["active"] is True return "\n".join(( - "===============", + "**===============**" if active else "===============", + "Status: {0}".format("__**Active**__" if active else "Inactive"), "Type: **{0}**".format(infraction_object["type"]), "Reason: {0}".format(infraction_object["reason"] or "*None*"), "Created: {0}".format(infraction_object["inserted_at"]), "Expires: {0}".format(infraction_object["expires_at"] or "*Permanent*"), "Actor: {0}".format(actor.mention if actor else actor_id), - "===============", + "**===============**" if active else "===============" )) diff --git a/bot/pagination.py b/bot/pagination.py index 49fae1e2e..9319a5b60 100644 --- a/bot/pagination.py +++ b/bot/pagination.py @@ -207,6 +207,8 @@ class LinePaginator(Paginator): log.debug(f"Got first page reaction - changing to page 1/{len(paginator.pages)}") + embed.description = "" + await message.edit(embed=embed) embed.description = paginator.pages[current_page] if footer_text: embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})") @@ -220,6 +222,8 @@ class LinePaginator(Paginator): log.debug(f"Got last page reaction - changing to page {current_page + 1}/{len(paginator.pages)}") + embed.description = "" + await message.edit(embed=embed) embed.description = paginator.pages[current_page] if footer_text: embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})") @@ -237,6 +241,8 @@ class LinePaginator(Paginator): current_page -= 1 log.debug(f"Got previous page reaction - changing to page {current_page + 1}/{len(paginator.pages)}") + embed.description = "" + await message.edit(embed=embed) embed.description = paginator.pages[current_page] if footer_text: @@ -256,6 +262,8 @@ class LinePaginator(Paginator): current_page += 1 log.debug(f"Got next page reaction - changing to page {current_page + 1}/{len(paginator.pages)}") + embed.description = "" + await message.edit(embed=embed) embed.description = paginator.pages[current_page] if footer_text: -- cgit v1.2.3 From 869e7e3e7bbedf2d6aa3ae6e6ab49adaf1c264a3 Mon Sep 17 00:00:00 2001 From: momothereal Date: Tue, 24 Jul 2018 02:46:18 -0400 Subject: Add support for searching infractions by reason --- bot/cogs/moderation.py | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 69cdfb10c..59357e4cb 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -339,7 +339,7 @@ class Moderation: async def search(self, ctx, arg: InfractionSearchQuery): """ Searches for infractions in the database. - :param arg: Either a user or a reason string. + :param arg: Either a user or a reason string. If a string, you can use the Re2 matching syntax. """ if isinstance(arg, User): user: User = arg @@ -366,20 +366,41 @@ class Moderation: colour=Colour.orange() ) - await LinePaginator.paginate( - lines=(self.infraction_to_string(infraction_object) for infraction_object in infraction_list), - ctx=ctx, - embed=embed, - empty=True, - max_lines=3, - max_size=1000 - ) - elif isinstance(arg, str): # search by reason - return + try: + response = await self.bot.http_session.get( + URLs.site_infractions, + headers=self.headers, + params={"search": arg} + ) + infraction_list = await response.json() + except Exception: + log.exception("There was an error fetching infractions.") + await ctx.send(":x: There was an error fetching infraction.") + return + + if not infraction_list: + await ctx.send(f":warning: No infractions matching \"{arg}\".") + return + + embed = Embed( + title=f"Infractions matching \"{arg}\" ({len(infraction_list)} total)", + colour=Colour.orange() + ) + else: await ctx.send(":x: Invalid infraction search query.") + return + + await LinePaginator.paginate( + lines=(self.infraction_to_string(infraction_object) for infraction_object in infraction_list), + ctx=ctx, + embed=embed, + empty=True, + max_lines=3, + max_size=1000 + ) # Utility functions -- cgit v1.2.3 From 2baa550f4bf436a4fcaedb750de4a0b7c2c8a5c1 Mon Sep 17 00:00:00 2001 From: momothereal Date: Tue, 24 Jul 2018 02:53:28 -0400 Subject: Show user when querying by reason --- bot/cogs/moderation.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 59357e4cb..067bba131 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -394,7 +394,10 @@ class Moderation: return await LinePaginator.paginate( - lines=(self.infraction_to_string(infraction_object) for infraction_object in infraction_list), + lines=( + self.infraction_to_string(infraction_object, show_user=isinstance(arg, str)) + for infraction_object in infraction_list + ), ctx=ctx, embed=embed, empty=True, @@ -467,13 +470,13 @@ class Moderation: } ) - def infraction_to_string(self, infraction_object): + def infraction_to_string(self, infraction_object, show_user=False): actor_id = int(infraction_object["actor"]["user_id"]) guild: Guild = self.bot.get_guild(constants.Guild.id) actor = guild.get_member(actor_id) active = infraction_object["active"] is True - return "\n".join(( + lines = [ "**===============**" if active else "===============", "Status: {0}".format("__**Active**__" if active else "Inactive"), "Type: **{0}**".format(infraction_object["type"]), @@ -482,7 +485,14 @@ class Moderation: "Expires: {0}".format(infraction_object["expires_at"] or "*Permanent*"), "Actor: {0}".format(actor.mention if actor else actor_id), "**===============**" if active else "===============" - )) + ] + + if show_user: + user_id = int(infraction_object["user"]["user_id"]) + user = self.bot.get_user(user_id) + lines.insert(1, "User: {0}".format(user.mention if user else user_id)) + + return "\n".join(lines) RFC1123_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" -- cgit v1.2.3 From 3a14300ec9d7d9a8564d5f867d60735e421b0af4 Mon Sep 17 00:00:00 2001 From: momothereal Date: Tue, 24 Jul 2018 04:09:26 -0400 Subject: Add command to edit infraction duration, show ID in infraction list --- bot/cogs/moderation.py | 64 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 067bba131..16803bb98 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -14,6 +14,8 @@ from bot.pagination import LinePaginator log = logging.getLogger(__name__) +MODERATION_ROLES = Roles.owner, Roles.admin, Roles.moderator + class Moderation: """ @@ -41,7 +43,7 @@ class Moderation: # Permanent infractions - @with_role(Roles.admin, Roles.owner, Roles.moderator) + @with_role(*MODERATION_ROLES) @command(name="moderation.warn") async def warn(self, ctx: Context, user: User, reason: str = None): """ @@ -79,7 +81,7 @@ class Moderation: await ctx.send(result_message) - @with_role(Roles.admin, Roles.owner, Roles.moderator) + @with_role(*MODERATION_ROLES) @command(name="moderation.ban") async def ban(self, ctx: Context, user: User, reason: str = None): """ @@ -119,7 +121,7 @@ class Moderation: await ctx.send(result_message) - @with_role(Roles.admin, Roles.owner, Roles.moderator) + @with_role(*MODERATION_ROLES) @command(name="moderation.mute") async def mute(self, ctx: Context, user: Member, reason: str = None): """ @@ -161,7 +163,7 @@ class Moderation: # Temporary infractions - @with_role(Roles.admin, Roles.owner, Roles.moderator) + @with_role(*MODERATION_ROLES) @command(name="moderation.tempmute") async def tempmute(self, ctx: Context, user: Member, duration: str, reason: str = None): """ @@ -208,7 +210,7 @@ class Moderation: await ctx.send(result_message) - @with_role(Roles.admin, Roles.owner, Roles.moderator) + @with_role(*MODERATION_ROLES) @command(name="moderation.tempban") async def tempban(self, ctx, user: User, duration: str, reason: str = None): """ @@ -258,7 +260,7 @@ class Moderation: # Remove infractions (un- commands) - @with_role(Roles.admin, Roles.owner, Roles.moderator) + @with_role(*MODERATION_ROLES) @command(name="moderation.unmute") async def unmute(self, ctx, user: Member): """ @@ -296,7 +298,7 @@ class Moderation: await ctx.send(":x: There was an error removing the infraction.") return - @with_role(Roles.admin, Roles.owner, Roles.moderator) + @with_role(*MODERATION_ROLES) @command(name="moderation.unban") async def unban(self, ctx, user: User): """ @@ -334,7 +336,52 @@ class Moderation: await ctx.send(":x: There was an error removing the infraction.") return - @with_role(Roles.admin, Roles.owner, Roles.moderator) + @with_role(*MODERATION_ROLES) + @command(name="infraction.edit.duration") + async def edit_duration(self, ctx, infraction_id: str, duration: str): + """ + Sets the duration of the given infraction, relative to the time of updating. + :param infraction_id: the id (UUID) of the infraction + :param duration: the new duration of the infraction, relative to the time of updating. Use "permanent" to mark + the infraction as permanent. + """ + try: + if duration == "permanent": + duration = None + # check the current active infraction + response = await self.bot.http_session.patch( + URLs.site_infractions, + json={ + "id": infraction_id, + "duration": duration + }, + headers=self.headers + ) + response_object = await response.json() + if "error_code" in response_object or response_object.get("success") is False: + # something went wrong + await ctx.send(f":x: There was an error updating the infraction: {response_object['error_message']}") + return + + infraction_object = response_object["infraction"] + # Re-schedule + self.cancel_expiration(infraction_id) + loop = asyncio.get_event_loop() + self.schedule_expiration(loop, infraction_object) + + if duration is None: + await ctx.send(f":ok_hand: Updated infraction: marked as permanent.") + else: + await ctx.send(f":ok_hand: Updated infraction: set to expire on {infraction_object['expires_at']}.") + + except Exception: + log.exception("There was an error updating an infraction.") + await ctx.send(":x: There was an error updating the infraction.") + return + + # Search infractions + + @with_role(*MODERATION_ROLES) @command(name="infraction.search") async def search(self, ctx, arg: InfractionSearchQuery): """ @@ -484,6 +531,7 @@ class Moderation: "Created: {0}".format(infraction_object["inserted_at"]), "Expires: {0}".format(infraction_object["expires_at"] or "*Permanent*"), "Actor: {0}".format(actor.mention if actor else actor_id), + "ID: `{0}`".format(infraction_object["id"]), "**===============**" if active else "===============" ] -- cgit v1.2.3 From 08fb2084a19c1b2eae1fec70f59954ff1d7b8372 Mon Sep 17 00:00:00 2001 From: momothereal Date: Tue, 24 Jul 2018 07:09:11 -0400 Subject: Remove old comments, some message cleanup --- bot/cogs/moderation.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 16803bb98..4acce6962 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -428,11 +428,11 @@ class Moderation: return if not infraction_list: - await ctx.send(f":warning: No infractions matching \"{arg}\".") + await ctx.send(f":warning: No infractions matching `{arg}`.") return embed = Embed( - title=f"Infractions matching \"{arg}\" ({len(infraction_list)} total)", + title=f"Infractions matching `{arg}` ({len(infraction_list)} total)", colour=Colour.orange() ) @@ -559,7 +559,4 @@ def _silent_exception(future): def setup(bot): bot.add_cog(Moderation(bot)) - # Here we'll need to call a command I haven't made yet - # It'll check the expiry queue and automatically set up tasks for - # temporary bans, mutes, etc. log.info("Cog loaded: Moderation") -- cgit v1.2.3 From df1fafc918c2b7771a2d2f5111f00aba47de772c Mon Sep 17 00:00:00 2001 From: momothereal Date: Wed, 25 Jul 2018 00:25:47 -0400 Subject: Add kick command --- bot/cogs/moderation.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 4acce6962..ae4217d78 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -81,6 +81,45 @@ class Moderation: await ctx.send(result_message) + @with_role(*MODERATION_ROLES) + @command(name="moderation.kick") + async def kick(self, ctx, user: Member, reason: str = None): + """ + Kicks a user. + :param user: accepts user mention, ID, etc. + :param reason: the reason for the kick. Wrap in string quotes for multiple words. + """ + try: + response = await self.bot.http_session.post( + URLs.site_infractions, + headers=self.headers, + json={ + "type": "kick", + "reason": reason, + "user_id": str(user.id), + "actor_id": str(ctx.message.author.id) + } + ) + except Exception: + log.exception("There was an error adding an infraction.") + await ctx.send(":x: There was an error adding the infraction.") + return + + response_object = await response.json() + if "error_code" in response_object: + # something went wrong + await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}") + return + + await user.kick(reason=reason) + + if reason is None: + result_message = f":ok_hand: kicked {user.mention}." + else: + result_message = f":ok_hand: kicked {user.mention} ({reason})." + + await ctx.send(result_message) + @with_role(*MODERATION_ROLES) @command(name="moderation.ban") async def ban(self, ctx: Context, user: User, reason: str = None): -- cgit v1.2.3 From e1b32b984f076306fe88bf218e070cb3deb7757d Mon Sep 17 00:00:00 2001 From: momothereal Date: Wed, 25 Jul 2018 00:31:22 -0400 Subject: Add 'edit reason' command --- bot/cogs/moderation.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index ae4217d78..7ffd64b37 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -375,6 +375,8 @@ class Moderation: await ctx.send(":x: There was an error removing the infraction.") return + # Edit infraction commands + @with_role(*MODERATION_ROLES) @command(name="infraction.edit.duration") async def edit_duration(self, ctx, infraction_id: str, duration: str): @@ -418,6 +420,35 @@ class Moderation: await ctx.send(":x: There was an error updating the infraction.") return + @with_role(*MODERATION_ROLES) + @command(name="infraction.edit.reason") + async def edit_reason(self, ctx, infraction_id: str, reason: str): + """ + Sets the reason of the given infraction. + :param infraction_id: the id (UUID) of the infraction + :param reason: the new reason of the infraction + """ + try: + response = await self.bot.http_session.patch( + URLs.site_infractions, + json={ + "id": infraction_id, + "reason": reason + }, + headers=self.headers + ) + response_object = await response.json() + if "error_code" in response_object or response_object.get("success") is False: + # something went wrong + await ctx.send(f":x: There was an error updating the infraction: {response_object['error_message']}") + return + + await ctx.send(f":ok_hand: Updated infraction: set reason to \"{reason}\".") + except Exception: + log.exception("There was an error updating an infraction.") + await ctx.send(":x: There was an error updating the infraction.") + return + # Search infractions @with_role(*MODERATION_ROLES) -- cgit v1.2.3 From eff828980f3ba0a5afbb069207558d50db76205a Mon Sep 17 00:00:00 2001 From: momothereal Date: Wed, 25 Jul 2018 14:31:31 -0400 Subject: Add code folding regions --- bot/cogs/moderation.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 7ffd64b37..72c1bd67d 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -41,7 +41,7 @@ class Moderation: if infraction_object["expires_at"] is not None: self.schedule_expiration(loop, infraction_object) - # Permanent infractions + # region: Permanent infractions @with_role(*MODERATION_ROLES) @command(name="moderation.warn") @@ -200,7 +200,8 @@ class Moderation: await ctx.send(result_message) - # Temporary infractions + # endregion + # region: Temporary infractions @with_role(*MODERATION_ROLES) @command(name="moderation.tempmute") @@ -297,7 +298,8 @@ class Moderation: await ctx.send(result_message) - # Remove infractions (un- commands) + # endregion + # region: Remove infractions (un- commands) @with_role(*MODERATION_ROLES) @command(name="moderation.unmute") @@ -375,7 +377,8 @@ class Moderation: await ctx.send(":x: There was an error removing the infraction.") return - # Edit infraction commands + # endregion + # region: Edit infraction commands @with_role(*MODERATION_ROLES) @command(name="infraction.edit.duration") @@ -449,7 +452,8 @@ class Moderation: await ctx.send(":x: There was an error updating the infraction.") return - # Search infractions + # endregion + # region: Search infractions @with_role(*MODERATION_ROLES) @command(name="infraction.search") @@ -522,7 +526,8 @@ class Moderation: max_size=1000 ) - # Utility functions + # endregion + # region: Utility functions def schedule_expiration(self, loop: asyncio.AbstractEventLoop, infraction_object: dict): infraction_id = infraction_object["id"] @@ -612,6 +617,8 @@ class Moderation: return "\n".join(lines) + # endregion + RFC1123_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" -- cgit v1.2.3 From 62f7f7d9303bf9a13345c771089a41c3674e6f03 Mon Sep 17 00:00:00 2001 From: momothereal Date: Wed, 25 Jul 2018 14:32:36 -0400 Subject: Remove useless comment --- bot/cogs/moderation.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 72c1bd67d..a724467ad 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -70,7 +70,6 @@ class Moderation: response_object = await response.json() if "error_code" in response_object: - # something went wrong await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}") return @@ -107,7 +106,6 @@ class Moderation: response_object = await response.json() if "error_code" in response_object: - # something went wrong await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}") return @@ -146,7 +144,6 @@ class Moderation: response_object = await response.json() if "error_code" in response_object: - # something went wrong await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}") return @@ -186,7 +183,6 @@ class Moderation: response_object = await response.json() if "error_code" in response_object: - # something went wrong await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}") return @@ -231,7 +227,6 @@ class Moderation: response_object = await response.json() if "error_code" in response_object: - # something went wrong await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}") return @@ -278,7 +273,6 @@ class Moderation: response_object = await response.json() if "error_code" in response_object: - # something went wrong await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}") return @@ -319,7 +313,6 @@ class Moderation: ) response_object = await response.json() if "error_code" in response_object: - # something went wrong await ctx.send(f":x: There was an error removing the infraction: {response_object['error_message']}") return @@ -357,7 +350,6 @@ class Moderation: ) response_object = await response.json() if "error_code" in response_object: - # something went wrong await ctx.send(f":x: There was an error removing the infraction: {response_object['error_message']}") return @@ -403,7 +395,6 @@ class Moderation: ) response_object = await response.json() if "error_code" in response_object or response_object.get("success") is False: - # something went wrong await ctx.send(f":x: There was an error updating the infraction: {response_object['error_message']}") return @@ -442,7 +433,6 @@ class Moderation: ) response_object = await response.json() if "error_code" in response_object or response_object.get("success") is False: - # something went wrong await ctx.send(f":x: There was an error updating the infraction: {response_object['error_message']}") return -- cgit v1.2.3 From a5440914ef7e29e3be653315da70b415ab04a22e Mon Sep 17 00:00:00 2001 From: momothereal Date: Wed, 25 Jul 2018 14:40:23 -0400 Subject: Fix documentation issues --- bot/cogs/moderation.py | 37 +++++++++++++++++++++++++++++++++++-- bot/converters.py | 2 +- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index a724467ad..c8d2f59cf 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -88,6 +88,7 @@ class Moderation: :param user: accepts user mention, ID, etc. :param reason: the reason for the kick. Wrap in string quotes for multiple words. """ + try: response = await self.bot.http_session.post( URLs.site_infractions, @@ -126,6 +127,7 @@ class Moderation: :param user: Accepts user mention, ID, etc. :param reason: Wrap in quotes to make reason larger than one word. """ + try: response = await self.bot.http_session.post( URLs.site_infractions, @@ -165,6 +167,7 @@ class Moderation: :param user: Accepts user mention, ID, etc. :param reason: Wrap in quotes to make reason larger than one word. """ + try: response = await self.bot.http_session.post( URLs.site_infractions, @@ -208,6 +211,7 @@ class Moderation: :param duration: The duration for the temporary mute infraction :param reason: Wrap in quotes to make reason larger than one word. """ + try: response = await self.bot.http_session.post( URLs.site_infractions, @@ -254,6 +258,7 @@ class Moderation: :param duration: The duration for the temporary ban infraction :param reason: Wrap in quotes to make reason larger than one word. """ + try: response = await self.bot.http_session.post( URLs.site_infractions, @@ -302,6 +307,7 @@ class Moderation: Deactivates the active mute infraction for a user. :param user: Accepts user mention, ID, etc. """ + try: # check the current active infraction response = await self.bot.http_session.get( @@ -339,6 +345,7 @@ class Moderation: Deactivates the active ban infraction for a user. :param user: Accepts user mention, ID, etc. """ + try: # check the current active infraction response = await self.bot.http_session.get( @@ -381,6 +388,7 @@ class Moderation: :param duration: the new duration of the infraction, relative to the time of updating. Use "permanent" to mark the infraction as permanent. """ + try: if duration == "permanent": duration = None @@ -422,6 +430,7 @@ class Moderation: :param infraction_id: the id (UUID) of the infraction :param reason: the new reason of the infraction """ + try: response = await self.bot.http_session.patch( URLs.site_infractions, @@ -452,6 +461,7 @@ class Moderation: Searches for infractions in the database. :param arg: Either a user or a reason string. If a string, you can use the Re2 matching syntax. """ + if isinstance(arg, User): user: User = arg # get infractions for this user @@ -506,7 +516,7 @@ class Moderation: await LinePaginator.paginate( lines=( - self.infraction_to_string(infraction_object, show_user=isinstance(arg, str)) + self._infraction_to_string(infraction_object, show_user=isinstance(arg, str)) for infraction_object in infraction_list ), ctx=ctx, @@ -520,6 +530,12 @@ class Moderation: # region: Utility functions def schedule_expiration(self, loop: asyncio.AbstractEventLoop, infraction_object: dict): + """ + Schedules a task to expire a temporary infraction. + :param loop: the asyncio event loop + :param infraction_object: the infraction object to expire at the end of the task + """ + infraction_id = infraction_object["id"] if infraction_id in self.expiration_tasks: return @@ -532,6 +548,11 @@ class Moderation: self.expiration_tasks[infraction_id] = task def cancel_expiration(self, infraction_id: str): + """ + Un-schedules a task set to expire a temporary infraction. + :param infraction_id: the ID of the infraction in question + """ + task = self.expiration_tasks.get(infraction_id) if task is None: log.warning(f"Failed to unschedule {infraction_id}: no task found.") @@ -541,6 +562,13 @@ class Moderation: del self.expiration_tasks[infraction_id] async def _scheduled_expiration(self, infraction_object): + """ + A co-routine which marks an infraction as expired after the delay from the time of scheduling + to the time of expiration. At the time of expiration, the infraction is marked as inactive on the website, + and the expiration task is cancelled. + :param infraction_object: the infraction in question + """ + infraction_id = infraction_object["id"] # transform expiration to delay in seconds @@ -558,6 +586,11 @@ class Moderation: self.cancel_expiration(infraction_object["id"]) async def _deactivate_infraction(self, infraction_object): + """ + A co-routine which marks an infraction as inactive on the website. This co-routine does not cancel or + un-schedule an expiration task. + :param infraction_object: the infraction in question + """ guild: Guild = self.bot.get_guild(constants.Guild.id) user_id = int(infraction_object["user"]["user_id"]) infraction_type = infraction_object["type"] @@ -582,7 +615,7 @@ class Moderation: } ) - def infraction_to_string(self, infraction_object, show_user=False): + def _infraction_to_string(self, infraction_object, show_user=False): actor_id = int(infraction_object["actor"]["user_id"]) guild: Guild = self.bot.get_guild(constants.Guild.id) actor = guild.get_member(actor_id) diff --git a/bot/converters.py b/bot/converters.py index f98e02015..f18b2f6c7 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -161,7 +161,7 @@ class ValidURL(Converter): class InfractionSearchQuery(Converter): """ - A converter that checks if the argument is a Discord user, and if not, fall-backs to a string. + A converter that checks if the argument is a Discord user, and if not, falls back to a string. """ @staticmethod -- cgit v1.2.3 From f1c42a99324fad332ed9e74253b8be8d3c8dc2e4 Mon Sep 17 00:00:00 2001 From: momothereal Date: Wed, 25 Jul 2018 14:42:32 -0400 Subject: Use less generic Exceptions when possible --- bot/cogs/moderation.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index c8d2f59cf..0483ab716 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -3,6 +3,7 @@ import datetime import logging from typing import Dict +from aiohttp import ClientError from discord import Colour, Embed, Guild, Member, Object, User from discord.ext.commands import Bot, Context, command @@ -63,7 +64,7 @@ class Moderation: "actor_id": str(ctx.message.author.id) } ) - except Exception: + except ClientError: log.exception("There was an error adding an infraction.") await ctx.send(":x: There was an error adding the infraction.") return @@ -100,7 +101,7 @@ class Moderation: "actor_id": str(ctx.message.author.id) } ) - except Exception: + except ClientError: log.exception("There was an error adding an infraction.") await ctx.send(":x: There was an error adding the infraction.") return @@ -139,7 +140,7 @@ class Moderation: "actor_id": str(ctx.message.author.id) } ) - except Exception: + except ClientError: log.exception("There was an error adding an infraction.") await ctx.send(":x: There was an error adding the infraction.") return @@ -179,7 +180,7 @@ class Moderation: "actor_id": str(ctx.message.author.id) } ) - except Exception: + except ClientError: log.exception("There was an error adding an infraction.") await ctx.send(":x: There was an error adding the infraction.") return @@ -224,7 +225,7 @@ class Moderation: "actor_id": str(ctx.message.author.id) } ) - except Exception: + except ClientError: log.exception("There was an error adding an infraction.") await ctx.send(":x: There was an error adding the infraction.") return @@ -271,7 +272,7 @@ class Moderation: "actor_id": str(ctx.message.author.id) } ) - except Exception: + except ClientError: log.exception("There was an error adding an infraction.") await ctx.send(":x: There was an error adding the infraction.") return @@ -473,7 +474,7 @@ class Moderation: headers=self.headers ) infraction_list = await response.json() - except Exception: + except ClientError: log.exception("There was an error fetching infractions.") await ctx.send(":x: There was an error fetching infraction.") return @@ -496,7 +497,7 @@ class Moderation: params={"search": arg} ) infraction_list = await response.json() - except Exception: + except ClientError: log.exception("There was an error fetching infractions.") await ctx.send(":x: There was an error fetching infraction.") return -- cgit v1.2.3 From 3b909bf8b244ad11d56450632dc20146b8127076 Mon Sep 17 00:00:00 2001 From: momothereal Date: Wed, 25 Jul 2018 14:45:26 -0400 Subject: Add aliases for moderation commands --- bot/cogs/moderation.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 0483ab716..f5002a046 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -45,7 +45,7 @@ class Moderation: # region: Permanent infractions @with_role(*MODERATION_ROLES) - @command(name="moderation.warn") + @command(name="moderation.warn", aliases=["warn"]) async def warn(self, ctx: Context, user: User, reason: str = None): """ Create a warning infraction in the database for a user. @@ -82,7 +82,7 @@ class Moderation: await ctx.send(result_message) @with_role(*MODERATION_ROLES) - @command(name="moderation.kick") + @command(name="moderation.kick", aliases=["kick"]) async def kick(self, ctx, user: Member, reason: str = None): """ Kicks a user. @@ -121,7 +121,7 @@ class Moderation: await ctx.send(result_message) @with_role(*MODERATION_ROLES) - @command(name="moderation.ban") + @command(name="moderation.ban", aliases=["ban"]) async def ban(self, ctx: Context, user: User, reason: str = None): """ Create a permanent ban infraction in the database for a user. @@ -161,7 +161,7 @@ class Moderation: await ctx.send(result_message) @with_role(*MODERATION_ROLES) - @command(name="moderation.mute") + @command(name="moderation.mute", aliases=["mute"]) async def mute(self, ctx: Context, user: Member, reason: str = None): """ Create a permanent mute infraction in the database for a user. @@ -204,7 +204,7 @@ class Moderation: # region: Temporary infractions @with_role(*MODERATION_ROLES) - @command(name="moderation.tempmute") + @command(name="moderation.tempmute", aliases=["tempmute"]) async def tempmute(self, ctx: Context, user: Member, duration: str, reason: str = None): """ Create a temporary mute infraction in the database for a user. @@ -251,7 +251,7 @@ class Moderation: await ctx.send(result_message) @with_role(*MODERATION_ROLES) - @command(name="moderation.tempban") + @command(name="moderation.tempban", aliases=["tempban"]) async def tempban(self, ctx, user: User, duration: str, reason: str = None): """ Create a temporary ban infraction in the database for a user. @@ -302,7 +302,7 @@ class Moderation: # region: Remove infractions (un- commands) @with_role(*MODERATION_ROLES) - @command(name="moderation.unmute") + @command(name="moderation.unmute", aliases=["unmute"]) async def unmute(self, ctx, user: Member): """ Deactivates the active mute infraction for a user. @@ -340,7 +340,7 @@ class Moderation: return @with_role(*MODERATION_ROLES) - @command(name="moderation.unban") + @command(name="moderation.unban", aliases=["unban"]) async def unban(self, ctx, user: User): """ Deactivates the active ban infraction for a user. -- cgit v1.2.3 From 0d1bd1f8ad2e8d92006203009819113e4ea28091 Mon Sep 17 00:00:00 2001 From: momothereal Date: Wed, 25 Jul 2018 14:46:37 -0400 Subject: Simplify ban line --- bot/cogs/moderation.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index f5002a046..7f516a298 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -150,8 +150,7 @@ class Moderation: await ctx.send(f":x: There was an error adding the infraction: {response_object['error_message']}") return - guild: Guild = ctx.guild - await guild.ban(user, reason=reason) + await ctx.guild.ban(user, reason=reason) if reason is None: result_message = f":ok_hand: permanently banned {user.mention}." -- cgit v1.2.3 From 60509512bf176d0bf4b0400c20972cc38fa6ca0b Mon Sep 17 00:00:00 2001 From: momothereal Date: Fri, 27 Jul 2018 19:09:43 -0400 Subject: Refactor moderation cog with namespace changes --- bot/cogs/moderation.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 7f516a298..fdb3b67f8 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -5,7 +5,7 @@ from typing import Dict from aiohttp import ClientError from discord import Colour, Embed, Guild, Member, Object, User -from discord.ext.commands import Bot, Context, command +from discord.ext.commands import Bot, Context, command, group from bot import constants from bot.constants import Keys, Roles, URLs @@ -45,7 +45,7 @@ class Moderation: # region: Permanent infractions @with_role(*MODERATION_ROLES) - @command(name="moderation.warn", aliases=["warn"]) + @command(name="warn") async def warn(self, ctx: Context, user: User, reason: str = None): """ Create a warning infraction in the database for a user. @@ -82,7 +82,7 @@ class Moderation: await ctx.send(result_message) @with_role(*MODERATION_ROLES) - @command(name="moderation.kick", aliases=["kick"]) + @command(name="kick") async def kick(self, ctx, user: Member, reason: str = None): """ Kicks a user. @@ -121,7 +121,7 @@ class Moderation: await ctx.send(result_message) @with_role(*MODERATION_ROLES) - @command(name="moderation.ban", aliases=["ban"]) + @command(name="ban") async def ban(self, ctx: Context, user: User, reason: str = None): """ Create a permanent ban infraction in the database for a user. @@ -160,7 +160,7 @@ class Moderation: await ctx.send(result_message) @with_role(*MODERATION_ROLES) - @command(name="moderation.mute", aliases=["mute"]) + @command(name="mute") async def mute(self, ctx: Context, user: Member, reason: str = None): """ Create a permanent mute infraction in the database for a user. @@ -203,7 +203,7 @@ class Moderation: # region: Temporary infractions @with_role(*MODERATION_ROLES) - @command(name="moderation.tempmute", aliases=["tempmute"]) + @command(name="tempmute") async def tempmute(self, ctx: Context, user: Member, duration: str, reason: str = None): """ Create a temporary mute infraction in the database for a user. @@ -250,7 +250,7 @@ class Moderation: await ctx.send(result_message) @with_role(*MODERATION_ROLES) - @command(name="moderation.tempban", aliases=["tempban"]) + @command(name="tempban") async def tempban(self, ctx, user: User, duration: str, reason: str = None): """ Create a temporary ban infraction in the database for a user. @@ -301,7 +301,7 @@ class Moderation: # region: Remove infractions (un- commands) @with_role(*MODERATION_ROLES) - @command(name="moderation.unmute", aliases=["unmute"]) + @command(name="unmute") async def unmute(self, ctx, user: Member): """ Deactivates the active mute infraction for a user. @@ -339,7 +339,7 @@ class Moderation: return @with_role(*MODERATION_ROLES) - @command(name="moderation.unban", aliases=["unban"]) + @command(name="unban") async def unban(self, ctx, user: User): """ Deactivates the active ban infraction for a user. @@ -380,7 +380,17 @@ class Moderation: # region: Edit infraction commands @with_role(*MODERATION_ROLES) - @command(name="infraction.edit.duration") + @group(name='infraction', aliases=('infr',)) + async def infraction_group(self, ctx: Context): + """Infraction manipulation commands.""" + + @with_role(*MODERATION_ROLES) + @infraction_group.group(name='edit') + async def infraction_edit_group(self, ctx: Context): + """Infraction editing commands.""" + + @with_role(*MODERATION_ROLES) + @infraction_edit_group.command(name="duration") async def edit_duration(self, ctx, infraction_id: str, duration: str): """ Sets the duration of the given infraction, relative to the time of updating. @@ -423,7 +433,7 @@ class Moderation: return @with_role(*MODERATION_ROLES) - @command(name="infraction.edit.reason") + @infraction_edit_group.command(name="reason") async def edit_reason(self, ctx, infraction_id: str, reason: str): """ Sets the reason of the given infraction. @@ -455,7 +465,7 @@ class Moderation: # region: Search infractions @with_role(*MODERATION_ROLES) - @command(name="infraction.search") + @infraction_group.command(name="search") async def search(self, ctx, arg: InfractionSearchQuery): """ Searches for infractions in the database. -- cgit v1.2.3 From e78b619cdcf5fb2921ffde0a607f465dbe3dbeb3 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Sat, 28 Jul 2018 01:46:32 +0200 Subject: Add missing `Muted` role. --- config-default.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-default.yml b/config-default.yml index 50505d4da..ee3e6a74e 100644 --- a/config-default.yml +++ b/config-default.yml @@ -87,7 +87,7 @@ guild: owner: 267627879762755584 verified: 352427296948486144 helpers: 267630620367257601 - muted: 0 + muted: 277914926603829249 keys: -- cgit v1.2.3 From c944364fb721ebe0ce33785435e33a362a5da142 Mon Sep 17 00:00:00 2001 From: Joseph Date: Sat, 28 Jul 2018 01:13:42 +0100 Subject: Use splats for infraction reasons --- bot/cogs/moderation.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index fdb3b67f8..245f17fda 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -46,11 +46,11 @@ class Moderation: @with_role(*MODERATION_ROLES) @command(name="warn") - async def warn(self, ctx: Context, user: User, reason: str = None): + async def warn(self, ctx: Context, user: User, *, reason: str = None): """ Create a warning infraction in the database for a user. :param user: accepts user mention, ID, etc. - :param reason: the reason for the warning. Wrap in string quotes for multiple words. + :param reason: The reason for the warning. """ try: @@ -83,11 +83,11 @@ class Moderation: @with_role(*MODERATION_ROLES) @command(name="kick") - async def kick(self, ctx, user: Member, reason: str = None): + async def kick(self, ctx, user: Member, *, reason: str = None): """ Kicks a user. :param user: accepts user mention, ID, etc. - :param reason: the reason for the kick. Wrap in string quotes for multiple words. + :param reason: The reason for the kick. """ try: @@ -122,11 +122,11 @@ class Moderation: @with_role(*MODERATION_ROLES) @command(name="ban") - async def ban(self, ctx: Context, user: User, reason: str = None): + async def ban(self, ctx: Context, user: User, *, reason: str = None): """ Create a permanent ban infraction in the database for a user. :param user: Accepts user mention, ID, etc. - :param reason: Wrap in quotes to make reason larger than one word. + :param reason: The reason for the ban. """ try: @@ -161,11 +161,11 @@ class Moderation: @with_role(*MODERATION_ROLES) @command(name="mute") - async def mute(self, ctx: Context, user: Member, reason: str = None): + async def mute(self, ctx: Context, user: Member, *, reason: str = None): """ Create a permanent mute infraction in the database for a user. :param user: Accepts user mention, ID, etc. - :param reason: Wrap in quotes to make reason larger than one word. + :param reason: The reason for the mute. """ try: @@ -204,12 +204,12 @@ class Moderation: @with_role(*MODERATION_ROLES) @command(name="tempmute") - async def tempmute(self, ctx: Context, user: Member, duration: str, reason: str = None): + async def tempmute(self, ctx: Context, user: Member, duration: str, *, reason: str = None): """ Create a temporary mute infraction in the database for a user. :param user: Accepts user mention, ID, etc. :param duration: The duration for the temporary mute infraction - :param reason: Wrap in quotes to make reason larger than one word. + :param reason: The reason for the temporary mute. """ try: @@ -251,12 +251,12 @@ class Moderation: @with_role(*MODERATION_ROLES) @command(name="tempban") - async def tempban(self, ctx, user: User, duration: str, reason: str = None): + async def tempban(self, ctx, user: User, duration: str, *, reason: str = None): """ Create a temporary ban infraction in the database for a user. :param user: Accepts user mention, ID, etc. :param duration: The duration for the temporary ban infraction - :param reason: Wrap in quotes to make reason larger than one word. + :param reason: The reason for the temporary ban. """ try: @@ -434,11 +434,11 @@ class Moderation: @with_role(*MODERATION_ROLES) @infraction_edit_group.command(name="reason") - async def edit_reason(self, ctx, infraction_id: str, reason: str): + async def edit_reason(self, ctx, infraction_id: str, *, reason: str): """ Sets the reason of the given infraction. :param infraction_id: the id (UUID) of the infraction - :param reason: the new reason of the infraction + :param reason: The new reason of the infraction """ try: -- cgit v1.2.3 From ea33140c3eab95ae8f42cd36afda7c83972b41f7 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 29 Jul 2018 01:17:29 +0200 Subject: Added regex cleanups, and now sending over complete embed details and a number of other message details. --- bot/cogs/clean.py | 126 +++++++++++++++++++++++++++++++++++++++++------------ bot/cogs/modlog.py | 2 +- 2 files changed, 99 insertions(+), 29 deletions(-) diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py index 8df6d6e83..efedc2dce 100644 --- a/bot/cogs/clean.py +++ b/bot/cogs/clean.py @@ -1,6 +1,9 @@ import logging import random +import re +from typing import Optional +from aiohttp.client_exceptions import ClientResponseError from discord import Colour, Embed, Message, User from discord.ext.commands import Bot, Context, group @@ -39,29 +42,76 @@ class Clean: json={"log_data": log_data} ) - data = await response.json() - log_id = data["log_id"] + try: + data = await response.json() + log_id = data["log_id"] + except (KeyError, ClientResponseError): + log.debug( + "API returned an unexpected result:\n" + f"{response.text}" + ) + return return f"{URLs.site_clean_logs}/{log_id}" async def _clean_messages( self, amount: int, ctx: Context, - bots_only: bool=False, user: User=None + bots_only: bool = False, user: User = None, + regex: Optional[str] = None ): """ A helper function that does the actual message cleaning. :param bots_only: Set this to True if you only want to delete bot messages. :param user: Specify a user and it will only delete messages by this user. + :param regular_expression: Specify a regular expression and it will only + delete messages that match this. """ - # Bulk delete checks - def predicate_bots_only(message: Message): + def predicate_bots_only(message: Message) -> bool: + """ + Returns true if the message was sent by a bot + """ + return message.author.bot - def predicate_specific_user(message: Message): + def predicate_specific_user(message: Message) -> bool: + """ + Return True if the message was sent by the + user provided in the _clean_messages call. + """ + return message.author == user + def predicate_regex(message: Message): + """ + Returns True if the regex provided in the + _clean_messages matches the message content + or any embed attributes the message may have. + """ + + content = [message.content] + + # Add the content for all embed attributes + for embed in message.embeds: + content.append(embed.title) + content.append(embed.description) + content.append(embed.footer.text) + content.append(embed.author.name) + for field in embed.fields: + content.append(field.name) + content.append(field.value) + + # Get rid of empty attributes and turn it into a string + content = [attr for attr in content if attr] + content = "\n".join(content) + + # Now let's see if there's a regex match + if not content: + return False + else: + return bool(re.search(regex.lower(), content.lower())) + # Is this an acceptable amount of messages to clean? if amount > CleanMessages.message_limit: embed = Embed( @@ -82,35 +132,52 @@ class Clean: await ctx.send(embed=embed) return + # Set up the correct predicate + if bots_only: + predicate = predicate_bots_only # Delete messages from bots + elif user: + predicate = predicate_specific_user # Delete messages from specific user + elif regex: + predicate = predicate_regex # Delete messages that match regex + else: + predicate = None # Delete all messages + # Look through the history and retrieve message data message_log = [] message_ids = [] - self.cleaning = True + invocation_deleted = False async for message in ctx.channel.history(limit=amount): + # If at any point the cancel command is invoked, we should stop. if not self.cleaning: return - delete = ( - bots_only and message.author.bot # Delete bot messages - or user and message.author == user # Delete user messages - or not bots_only and not user # Delete all messages - ) + # Always start by deleting the invocation + if not invocation_deleted: + await message.delete() + invocation_deleted = True + continue - if delete and message.content or message.embeds: - content = message.content or message.embeds[0].description + # If the message passes predicate, let's save it. + if predicate is None or predicate(message): author = f"{message.author.name}#{message.author.discriminator}" role = message.author.top_role.name - # Store the message data + content = message.content + embeds = [embed.to_dict() for embed in message.embeds] + attachments = ["" for _ in message.attachments] + message_ids.append(message.id) message_log.append({ "content": content, "author": author, + "user_id": str(message.author.id), "role": role.lower(), - "timestamp": message.created_at.strftime("%D %H:%M") + "timestamp": message.created_at.strftime("%D %H:%M"), + "attachments": attachments, + "embeds": embeds, }) self.cleaning = False @@ -119,13 +186,6 @@ class Clean: self.mod_log.ignore_message_deletion(*message_ids) # Use bulk delete to actually do the cleaning. It's far faster. - predicate = None - - if bots_only: - predicate = predicate_bots_only - elif user: - predicate = predicate_specific_user - await ctx.channel.purge( limit=amount, check=predicate @@ -141,7 +201,7 @@ class Clean: color=Colour(Colours.soft_red), description="No matching messages could be found." ) - await ctx.send(embed=embed) + await ctx.send(embed=embed, delete_after=10.0) return # Build the embed and send it @@ -171,7 +231,7 @@ class Clean: await ctx.invoke(self.bot.get_command("help"), "clean") - @clean_group.command(aliases=["user"]) + @clean_group.command(name="user", aliases=["users"]) @with_role(Roles.moderator, Roles.admin, Roles.owner) async def clean_user(self, ctx: Context, user: User, amount: int = 10): """ @@ -181,7 +241,7 @@ class Clean: await self._clean_messages(amount, ctx, user=user) - @clean_group.command(aliases=["all"]) + @clean_group.command(name="all", aliases=["everything"]) @with_role(Roles.moderator, Roles.admin, Roles.owner) async def clean_all(self, ctx: Context, amount: int = 10): """ @@ -191,7 +251,7 @@ class Clean: await self._clean_messages(amount, ctx) - @clean_group.command(aliases=["bots"]) + @clean_group.command(name="bots", aliases=["bot"]) @with_role(Roles.moderator, Roles.admin, Roles.owner) async def clean_bots(self, ctx: Context, amount: int = 10): """ @@ -201,7 +261,17 @@ class Clean: await self._clean_messages(amount, ctx, bots_only=True) - @clean_group.command(aliases=["stop", "cancel", "abort"]) + @clean_group.command(name="regex", aliases=["word", "expression"]) + @with_role(Roles.moderator, Roles.admin, Roles.owner) + async def clean_regex(self, ctx: Context, regex, amount: int = 10): + """ + Delete all messages that match a certain regex, + and stop cleaning after traversing `amount` messages. + """ + + await self._clean_messages(amount, ctx, regex=regex) + + @clean_group.command(name="stop", aliases=["cancel", "abort"]) @with_role(Roles.moderator, Roles.admin, Roles.owner) async def clean_cancel(self, ctx: Context): """ diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py index e8ff19bb5..b5a73d6e0 100644 --- a/bot/cogs/modlog.py +++ b/bot/cogs/modlog.py @@ -486,7 +486,7 @@ class ModLog: response = f"**Attachments:** {len(message.attachments)}\n" + response await self.send_log_message( - Icons.message_delete, COLOUR_RED, + Icons.message_delete, Colours.soft_red, "Message deleted", response, channel_id=Channels.message_log -- cgit v1.2.3 From d4d6be7104e6eab72c1c0d73ec7e8770bc7cdeb1 Mon Sep 17 00:00:00 2001 From: Christopher Baklid Date: Sun, 29 Jul 2018 09:40:50 +0000 Subject: Pip cache --- .gitlab-ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 88ab5d927..3edfb2bf8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,13 @@ image: pythondiscord/bot-ci:latest +variables: + PIPENV_CACHE_DIR: "$CI_PROJECT_DIR/pipenv-cache" + +cache: + paths: + - "$CI_PROJECT_DIR/pipenv-cache" + - "$CI_PROJECT_DIR/.venv" + stages: - test - build -- cgit v1.2.3 From 89db231108d309c9ad2c2ccf476e662d807c7c0e Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 29 Jul 2018 11:41:10 +0200 Subject: Addressing all gdude comments --- bot/cogs/clean.py | 36 +++++++++++++++++++++++------------- bot/cogs/defcon.py | 16 ++++++++-------- bot/cogs/token_remover.py | 6 +++--- bot/cogs/verification.py | 4 ++-- 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py index efedc2dce..5644913b5 100644 --- a/bot/cogs/clean.py +++ b/bot/cogs/clean.py @@ -18,6 +18,19 @@ log = logging.getLogger(__name__) class Clean: + """ + A cog that allows messages to be deleted in + bulk, while applying various filters. + + You can delete messages sent by a specific user, + messages sent by bots, all messages, or messages + that match a specific regular expression. + + The deleted messages are saved and uploaded + to the database via an API endpoint, and a URL is + returned which can be used to view the messages + in the Discord dark theme style. + """ def __init__(self, bot: Bot): self.bot = bot @@ -127,7 +140,7 @@ class Clean: embed = Embed( color=Colour(Colours.soft_red), title=random.choice(NEGATIVE_REPLIES), - description="Multiple simultaneous cleaning processes is not allowed." + description="Please wait for the currently ongoing clean operation to complete." ) await ctx.send(embed=embed) return @@ -163,7 +176,7 @@ class Clean: # If the message passes predicate, let's save it. if predicate is None or predicate(message): author = f"{message.author.name}#{message.author.discriminator}" - role = message.author.top_role.name + role_id = message.author.top_role.id content = message.content embeds = [embed.to_dict() for embed in message.embeds] @@ -174,7 +187,7 @@ class Clean: "content": content, "author": author, "user_id": str(message.author.id), - "role": role.lower(), + "role_id": str(role_id), "timestamp": message.created_at.strftime("%D %H:%M"), "attachments": attachments, "embeds": embeds, @@ -205,23 +218,20 @@ class Clean: return # Build the embed and send it + print(upload_log) message = ( f"**{len(message_ids)}** messages deleted in <#{ctx.channel.id}> by **{ctx.author.name}**\n\n" f"A log of the deleted messages can be found [here]({upload_log})." ) - embed = Embed( - color=Colour(Colours.soft_red), - description=message - ) - - embed.set_author( - name=f"Bulk message delete", - icon_url=Icons.message_bulk_delete + await self.mod_log.send_log_message( + icon_url=Icons.message_bulk_delete, + colour=Colour(Colours.soft_red), + title="Bulk message delete", + text=message, + channel_id=Channels.modlog, ) - await self.bot.get_channel(Channels.modlog).send(embed=embed) - @group(invoke_without_command=True, name="clean", hidden=True) @with_role(Roles.moderator, Roles.admin, Roles.owner) async def clean_group(self, ctx: Context): diff --git a/bot/cogs/defcon.py b/bot/cogs/defcon.py index 8ca59b058..beb05ba46 100644 --- a/bot/cogs/defcon.py +++ b/bot/cogs/defcon.py @@ -36,7 +36,7 @@ class Defcon: self.headers = {"X-API-KEY": Keys.site_api} @property - def modlog(self) -> ModLog: + def mod_log(self) -> ModLog: return self.bot.get_cog("ModLog") async def on_ready(self): @@ -92,7 +92,7 @@ class Defcon: if not message_sent: message = f"{message}\n\nUnable to send rejection message via DM; they probably have DMs disabled." - await self.modlog.send_log_message( + await self.mod_log.send_log_message( Icons.defcon_denied, COLOUR_RED, "Entry denied", message, member.avatar_url_as(static_format="png") ) @@ -133,7 +133,7 @@ class Defcon: f"```py\n{e}\n```" ) - await self.modlog.send_log_message( + await self.mod_log.send_log_message( Icons.defcon_enabled, COLOUR_GREEN, "DEFCON enabled", f"**Staffer:** {ctx.author.name}#{ctx.author.discriminator} (`{ctx.author.id}`)\n" f"**Days:** {self.days.days}\n\n" @@ -144,7 +144,7 @@ class Defcon: else: await ctx.send(f"{Emojis.defcon_enabled} DEFCON enabled.") - await self.modlog.send_log_message( + await self.mod_log.send_log_message( Icons.defcon_enabled, COLOUR_GREEN, "DEFCON enabled", f"**Staffer:** {ctx.author.name}#{ctx.author.discriminator} (`{ctx.author.id}`)\n" f"**Days:** {self.days.days}\n\n" @@ -176,7 +176,7 @@ class Defcon: f"```py\n{e}\n```" ) - await self.modlog.send_log_message( + await self.mod_log.send_log_message( Icons.defcon_disabled, COLOUR_RED, "DEFCON disabled", f"**Staffer:** {ctx.author.name}#{ctx.author.discriminator} (`{ctx.author.id}`)\n" "**There was a problem updating the site** - This setting may be reverted when the bot is " @@ -186,7 +186,7 @@ class Defcon: else: await ctx.send(f"{Emojis.defcon_disabled} DEFCON disabled.") - await self.modlog.send_log_message( + await self.mod_log.send_log_message( Icons.defcon_disabled, COLOUR_RED, "DEFCON disabled", f"**Staffer:** {ctx.author.name}#{ctx.author.discriminator} (`{ctx.author.id}`)" ) @@ -233,7 +233,7 @@ class Defcon: f"```py\n{e}\n```" ) - await self.modlog.send_log_message( + await self.mod_log.send_log_message( Icons.defcon_updated, Colour.blurple(), "DEFCON updated", f"**Staffer:** {ctx.author.name}#{ctx.author.discriminator} (`{ctx.author.id}`)\n" f"**Days:** {self.days.days}\n\n" @@ -246,7 +246,7 @@ class Defcon: f"{Emojis.defcon_updated} DEFCON days updated; accounts must be {days} days old to join to the server" ) - await self.modlog.send_log_message( + await self.mod_log.send_log_message( Icons.defcon_updated, Colour.blurple(), "DEFCON updated", f"**Staffer:** {ctx.author.name}#{ctx.author.discriminator} (`{ctx.author.id}`)\n" f"**Days:** {self.days.days}" diff --git a/bot/cogs/token_remover.py b/bot/cogs/token_remover.py index c8621118b..74bc0d9b2 100644 --- a/bot/cogs/token_remover.py +++ b/bot/cogs/token_remover.py @@ -40,10 +40,10 @@ class TokenRemover: def __init__(self, bot: Bot): self.bot = bot - self.modlog = None + self.mod_log = None async def on_ready(self): - self.modlog = self.bot.get_channel(Channels.modlog) + self.mod_log = self.bot.get_channel(Channels.modlog) async def on_message(self, msg: Message): if msg.author.bot: @@ -61,7 +61,7 @@ class TokenRemover: if self.is_valid_user_id(user_id) and self.is_valid_timestamp(creation_timestamp): await msg.delete() await msg.channel.send(DELETION_MESSAGE_TEMPLATE.format(mention=msg.author.mention)) - await self.modlog.send( + await self.mod_log.send( ":key2::mute: censored a seemingly valid token sent by " f"{msg.author} (`{msg.author.id}`) in {msg.channel.mention}, token was " f"`{user_id}.{creation_timestamp}.{'x' * len(hmac)}`" diff --git a/bot/cogs/verification.py b/bot/cogs/verification.py index b0667fdd0..7f1c9e68a 100644 --- a/bot/cogs/verification.py +++ b/bot/cogs/verification.py @@ -37,7 +37,7 @@ class Verification: self.bot = bot @property - def modlog(self) -> ModLog: + def mod_log(self) -> ModLog: return self.bot.get_cog("ModLog") async def on_message(self, message: Message): @@ -90,7 +90,7 @@ class Verification: log.trace(f"Deleting the message posted by {ctx.author}.") try: - self.modlog.ignore_message_deletion(ctx.message.id) + self.mod_log.ignore_message_deletion(ctx.message.id) await ctx.message.delete() except NotFound: log.trace("No message found, it must have been deleted by another bot.") -- cgit v1.2.3 From 3bcf72df3f69d23d9a41b0ffbc0ad929474b6d77 Mon Sep 17 00:00:00 2001 From: Gareth Coles Date: Sun, 29 Jul 2018 10:58:10 +0100 Subject: [Verification] Fix double-post in subscribe command --- bot/cogs/verification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/verification.py b/bot/cogs/verification.py index b0667fdd0..d36513c4a 100644 --- a/bot/cogs/verification.py +++ b/bot/cogs/verification.py @@ -110,7 +110,7 @@ class Verification: break if has_role: - await ctx.send( + return await ctx.send( f"{ctx.author.mention} You're already subscribed!", ) -- cgit v1.2.3 From 69dbeb3d3ea4e075ebedc499fb42fd0f242623d1 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 29 Jul 2018 12:07:17 +0200 Subject: Deleting all clean command embed responses after 10 seconds. --- bot/cogs/clean.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py index 5644913b5..d0861babc 100644 --- a/bot/cogs/clean.py +++ b/bot/cogs/clean.py @@ -214,7 +214,7 @@ class Clean: color=Colour(Colours.soft_red), description="No matching messages could be found." ) - await ctx.send(embed=embed, delete_after=10.0) + await ctx.send(embed=embed, delete_after=10) return # Build the embed and send it @@ -295,7 +295,7 @@ class Clean: color=Colour.blurple(), description="Clean interrupted." ) - await ctx.send(embed=embed) + await ctx.send(embed=embed, delete_after=10) def setup(bot): -- cgit v1.2.3 From 963ad9abb631fc40384623de6b4d582a5e2760e7 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 29 Jul 2018 12:23:39 +0200 Subject: Default to developer role if message.author returns a User instead of a Member. --- bot/cogs/clean.py | 7 ++++++- bot/constants.py | 1 + config-default.yml | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/bot/cogs/clean.py b/bot/cogs/clean.py index d0861babc..85c9ec781 100644 --- a/bot/cogs/clean.py +++ b/bot/cogs/clean.py @@ -176,7 +176,12 @@ class Clean: # If the message passes predicate, let's save it. if predicate is None or predicate(message): author = f"{message.author.name}#{message.author.discriminator}" - role_id = message.author.top_role.id + + # message.author may return either a User or a Member. Users don't have roles. + if type(message.author) is User: + role_id = Roles.developer + else: + role_id = message.author.top_role.id content = message.content embeds = [embed.to_dict() for embed in message.embeds] diff --git a/bot/constants.py b/bot/constants.py index e8176b377..58bf62b15 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -297,6 +297,7 @@ class Roles(metaclass=YAMLGetter): announcements: int champion: int contributor: int + developer: int devops: int jammer: int moderator: int diff --git a/config-default.yml b/config-default.yml index 05ff54cae..8ef74f6c3 100644 --- a/config-default.yml +++ b/config-default.yml @@ -92,6 +92,7 @@ guild: announcements: 463658397560995840 champion: 430492892331769857 contributor: 295488872404484098 + developer: 352427296948486144 devops: 409416496733880320 jammer: 423054537079783434 moderator: 267629731250176001 -- cgit v1.2.3