aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/__main__.py1
-rw-r--r--bot/cogs/clean.py223
-rw-r--r--bot/cogs/modlog.py29
-rw-r--r--bot/constants.py22
-rw-r--r--config-default.yml13
5 files changed, 270 insertions, 18 deletions
diff --git a/bot/__main__.py b/bot/__main__.py
index 4429c2a0d..cd6f409dd 100644
--- a/bot/__main__.py
+++ b/bot/__main__.py
@@ -45,6 +45,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..8df6d6e83
--- /dev/null
+++ b/bot/cogs/clean.py
@@ -0,0 +1,223 @@
+import logging
+import random
+
+from discord import Colour, Embed, Message, User
+from discord.ext.commands import Bot, Context, group
+
+from bot.cogs.modlog import ModLog
+from bot.constants import (
+ Channels, CleanMessages, Colours, Icons,
+ Keys, NEGATIVE_REPLIES, Roles, URLs
+)
+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
+
+ @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.
+
+ 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: 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.
+ """
+
+ # 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 > CleanMessages.message_limit:
+ embed = Embed(
+ color=Colour(Colours.soft_red),
+ title=random.choice(NEGATIVE_REPLIES),
+ description=f"You cannot clean more than {CleanMessages.message_limit} messages."
+ )
+ await ctx.send(embed=embed)
+ return
+
+ # Are we already performing a clean?
+ if self.cleaning:
+ embed = Embed(
+ color=Colour(Colours.soft_red),
+ title=random.choice(NEGATIVE_REPLIES),
+ description="Multiple simultaneous cleaning processes is not allowed."
+ )
+ await ctx.send(embed=embed)
+ return
+
+ # Look through the history and retrieve message data
+ message_log = []
+ message_ids = []
+
+ self.cleaning = True
+
+ 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 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")
+ })
+
+ 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.
+ predicate = None
+
+ if bots_only:
+ predicate = predicate_bots_only
+ elif user:
+ predicate = predicate_specific_user
+
+ await ctx.channel.purge(
+ limit=amount,
+ check=predicate
+ )
+
+ # Reverse the list to restore chronological order
+ if message_log:
+ message_log = list(reversed(message_log))
+ upload_log = await self._upload_log(message_log)
+ else:
+ # Can't build an embed, nothing to clean!
+ embed = Embed(
+ color=Colour(Colours.soft_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(
+ color=Colour(Colours.soft_red),
+ description=message
+ )
+
+ embed.set_author(
+ name=f"Bulk message delete",
+ icon_url=Icons.message_bulk_delete
+ )
+
+ 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):
+ """
+ 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.
+ """
+
+ await self._clean_messages(amount, ctx, user=user)
+
+ @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 poster,
+ and stop cleaning after traversing `amount` messages.
+ """
+
+ await self._clean_messages(amount, ctx)
+
+ @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.
+ """
+
+ 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)
+
+
+def setup(bot):
+ bot.add_cog(Clean(bot))
+ log.info("Cog loaded: Clean")
diff --git a/bot/cogs/modlog.py b/bot/cogs/modlog.py
index 87cea2b5a..e8ff19bb5 100644
--- a/bot/cogs/modlog.py
+++ b/bot/cogs/modlog.py
@@ -13,16 +13,13 @@ 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
from bot.utils.time import humanize
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",)
@@ -92,7 +89,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:
@@ -111,7 +108,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
)
@@ -157,7 +154,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}"
@@ -174,7 +171,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}`"
)
@@ -183,7 +180,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}`)"
)
@@ -229,7 +226,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}"
@@ -277,7 +274,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}"
@@ -292,7 +289,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")
)
@@ -312,7 +309,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")
)
@@ -322,7 +319,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")
)
@@ -410,7 +407,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}"
@@ -528,7 +525,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 adfd5d014..3b4dd8323 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -198,8 +198,16 @@ 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"
defcon_disabled: str # noqa: E704
@@ -210,12 +218,13 @@ class Emojis(metaclass=YAMLGetter):
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
@@ -245,6 +254,13 @@ class Icons(metaclass=YAMLGetter):
user_update: str
+class CleanMessages(metaclass=YAMLGetter):
+ section = "bot"
+ subsection = "clean"
+
+ message_limit: int
+
+
class Channels(metaclass=YAMLGetter):
section = "guild"
subsection = "channels"
@@ -331,6 +347,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 84fa86a75..abef0fcc3 100644
--- a/config-default.yml
+++ b/config-default.yml
@@ -6,6 +6,16 @@ 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:
defcon_disabled: "<:defcondisabled:470326273952972810>"
defcon_enabled: "<:defconenabled:470326274213150730>"
@@ -16,6 +26,7 @@ bot:
white_chevron: "<:whitechevron:418110396973711363>"
lemoneye2: "<:lemoneye2:435193765582340098>"
+ bullet: "\u2022"
pencil: "\u270F"
new: "\U0001F195"
@@ -116,6 +127,8 @@ urls:
site_schema: &SCHEMA "https://"
site_bigbrother_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/bigbrother"]
+ site_clean_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/clean"]
+ site_clean_logs: !JOIN [*SCHEMA, *DOMAIN, "/bot/clean_logs"]
site_docs_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/docs"]
site_facts_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/snake_facts"]
site_hiphopify_api: !JOIN [*SCHEMA, *DOMAIN, "/bot/hiphopify"]