From f40c2df9e8735744a04b1419bc2ff7523a4f4d10 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 13 Jan 2024 08:15:30 -0500 Subject: [PATCH 01/99] Move new protect commands here --- techsupport_bot/commands/moderator.py | 605 ++++++++++++++++++++++++++ techsupport_bot/commands/protect.py | 176 -------- techsupport_bot/core/__init__.py | 1 + techsupport_bot/core/databases.py | 1 + techsupport_bot/core/moderation.py | 196 +++++++++ 5 files changed, 803 insertions(+), 176 deletions(-) create mode 100644 techsupport_bot/commands/moderator.py create mode 100644 techsupport_bot/core/moderation.py diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py new file mode 100644 index 000000000..763c7f61b --- /dev/null +++ b/techsupport_bot/commands/moderator.py @@ -0,0 +1,605 @@ +"""Manual moderation commands and helper functions""" + +from __future__ import annotations + +from datetime import datetime, timedelta +from typing import TYPE_CHECKING, Union + +import dateparser +import discord +import ui +from botlogging import LogContext, LogLevel +from core import auxiliary, cogs, moderation +from discord import app_commands + +if TYPE_CHECKING: + import bot + + +async def setup(bot: bot.TechSupportBot) -> None: + """Adds the cog to the bot. Setups config + + Args: + bot (bot.TechSupportBot): The bot object to register the cog with + """ + await bot.add_cog(ProtectCommands(bot=bot)) + + +class ProtectCommands(cogs.BaseCog): + """The cog for all manual moderation activities + These are all slash commands""" + + # Commands + + @app_commands.checks.has_permissions(ban_members=True) + @app_commands.checks.bot_has_permissions(ban_members=True) + @app_commands.command( + name="ban", + description="Bans a user from the guild", + extras={"module": "moderator"}, + ) + async def handle_ban_user( + self, + interaction: discord.Interaction, + target: discord.User, + reason: str, + delete_days: int = None, + ) -> None: + """The ban slash command. This checks that the permissions are correct + and that the user is not already banned + + Args: + interaction (discord.Interaction): The interaction that called this command + target (discord.User): The target to ban + reason (str): The reason the person is getting banned + delete_days (int, optional): How many days of messages to delete. Defaults to None. + """ + # Ensure we can ban the person + permission_check = await self.permission_check( + invoker=interaction.user, target=target, action_name="ban" + ) + if permission_check: + embed = auxiliary.prepare_deny_embed(message=permission_check) + await interaction.response.send_message(embed=embed) + return + + async for ban in interaction.guild.bans(limit=None): + if target == ban.user: + embed = auxiliary.prepare_deny_embed(message="User is already banned.") + await interaction.response.send_message(embed=embed) + return + + if not delete_days: + config = self.bot.guild_configs[str(interaction.guild.id)] + delete_days = config.extensions.protect.ban_delete_duration.value + + # Ban the user using the core moderation cog + result = await moderation.ban_user( + guild=interaction.guild, + user=target, + delete_days=delete_days, + reason=f"{reason} - banned by {interaction.user}", + ) + if not result: + embed = auxiliary.prepare_deny_embed( + message=f"Something went wrong when banning {target}" + ) + await interaction.response.send_message(embed=embed) + return + + await moderation.send_command_usage_alert( + bot=self.bot, + interaction=interaction, + command=( + f"/ban target: {target.display_name}, reason: {reason}, delete_days:" + f" {delete_days}" + ), + guild=interaction.guild, + target=target, + ) + embed = self.generate_response_embed(user=target, action="ban", reason=reason) + await interaction.response.send_message(content=target.mention, embed=embed) + + @app_commands.checks.has_permissions(ban_members=True) + @app_commands.checks.bot_has_permissions(ban_members=True) + @app_commands.command( + name="unban", + description="Unbans a user from the guild", + extras={"module": "moderator"}, + ) + async def handle_unban_user( + self, interaction: discord.Interaction, target: discord.User, reason: str + ): + permission_check = await self.permission_check( + invoker=interaction.user, target=target, action_name="unban" + ) + if permission_check: + embed = auxiliary.prepare_deny_embed(message=permission_check) + await interaction.response.send_message(embed=embed) + return + + is_banned = False + + async for ban in interaction.guild.bans(limit=None): + if target == ban.user: + is_banned = True + + if not is_banned: + embed = auxiliary.prepare_deny_embed(message=f"{target} is not banned") + await interaction.response.send_message(embed=embed) + return + + result = await moderation.unban_user( + guild=interaction.guild, + user=target, + reason=f"{reason} - unbanned by {interaction.user}", + ) + if not result: + embed = auxiliary.prepare_deny_embed( + message=f"Something went wrong when unbanning {target}" + ) + await interaction.response.send_message(embed=embed) + return + + await moderation.send_command_usage_alert( + bot=self.bot, + interaction=interaction, + command=f"/unban target: {target.display_name}, reason: {reason}", + guild=interaction.guild, + target=target, + ) + embed = self.generate_response_embed(user=target, action="unban", reason=reason) + await interaction.response.send_message(content=target.mention, embed=embed) + + @app_commands.checks.has_permissions(kick_members=True) + @app_commands.checks.bot_has_permissions(kick_members=True) + @app_commands.command( + name="kick", + description="Kicks a user from the guild", + extras={"module": "moderator"}, + ) + async def handle_kick_user( + self, interaction: discord.Interaction, target: discord.Member, reason: str + ): + permission_check = await self.permission_check( + invoker=interaction.user, target=target, action_name="kick" + ) + if permission_check: + embed = auxiliary.prepare_deny_embed(message=permission_check) + await interaction.response.send_message(embed=embed) + return + + result = await moderation.kick_user( + guild=interaction.guild, + user=target, + reason=f"{reason} - kicked by {interaction.user}", + ) + if not result: + embed = auxiliary.prepare_deny_embed( + message=f"Something went wrong when kicking {target}" + ) + await interaction.response.send_message(embed=embed) + return + + await moderation.send_command_usage_alert( + bot=self.bot, + interaction=interaction, + command=f"/kick target: {target.display_name}, reason: {reason}", + guild=interaction.guild, + target=target, + ) + embed = self.generate_response_embed(user=target, action="kick", reason=reason) + await interaction.response.send_message(content=target.mention, embed=embed) + + @app_commands.checks.has_permissions(moderate_members=True) + @app_commands.checks.bot_has_permissions(moderate_members=True) + @app_commands.command( + name="mute", description="Times out a user", extras={"module": "moderator"} + ) + async def handle_mute_user( + self, + interaction: discord.Interaction, + target: discord.Member, + reason: str, + duration: str = None, + ): + permission_check = await self.permission_check( + invoker=interaction.user, target=target, action_name="mute" + ) + if permission_check: + embed = auxiliary.prepare_deny_embed(message=permission_check) + await interaction.response.send_message(embed=embed) + return + + # The API prevents administrators from being timed out. Check it here + if target.guild_permissions.administrator: + embed = auxiliary.prepare_deny_embed( + message=( + "Someone with the `administrator` permissions cannot be timed out" + ) + ) + await interaction.response.send_message(embed=embed) + return + + delta_duration = None + + if duration: + # The date parser defaults to time in the past, so it is second + # This could be fixed by appending "in" to your query, but this is simpler + try: + delta_duration = datetime.now() - dateparser.parse(duration) + delta_duration = timedelta( + seconds=round(delta_duration.total_seconds()) + ) + except TypeError as exc: + raise ValueError("Invalid duration") from exc + if not delta_duration: + raise ValueError("Invalid duration") + else: + delta_duration = timedelta(hours=1) + + # Checks to ensure time is valid and within the scope of the API + if delta_duration > timedelta(days=28): + raise ValueError("Timeout duration cannot be more than 28 days") + if delta_duration < timedelta(seconds=1): + raise ValueError("Timeout duration cannot be less than 1 second") + + result = await moderation.mute_user( + user=target, + reason=f"{reason} - muted by {interaction.user}", + duration=delta_duration, + ) + if not result: + embed = auxiliary.prepare_deny_embed( + message=f"Something went wrong when muting {target}" + ) + await interaction.response.send_message(embed=embed) + return + + await moderation.send_command_usage_alert( + bot=self.bot, + interaction=interaction, + command=( + f"/mute target: {target.display_name}, reason: {reason}, duration:" + f" {duration}" + ), + guild=interaction.guild, + target=target, + ) + embed = self.generate_response_embed(user=target, action="mute", reason=reason) + await interaction.response.send_message(content=target.mention, embed=embed) + + @app_commands.checks.has_permissions(moderate_members=True) + @app_commands.checks.bot_has_permissions(moderate_members=True) + @app_commands.command( + name="unmute", + description="Removes timeout from a user", + extras={"module": "moderator"}, + ) + async def handle_unmute_user( + self, interaction: discord.Interaction, target: discord.Member, reason: str + ): + permission_check = await self.permission_check( + invoker=interaction.user, target=target, action_name="unmute" + ) + if permission_check: + embed = auxiliary.prepare_deny_embed(message=permission_check) + await interaction.response.send_message(embed=embed) + return + + if not target.timed_out_until: + embed = auxiliary.prepare_deny_embed( + message=(f"{target} is not currently muted") + ) + await interaction.response.send_message(embed=embed) + return + + result = await moderation.unmute_user( + user=target, + reason=f"{reason} - unmuted by {interaction.user}", + ) + if not result: + embed = auxiliary.prepare_deny_embed( + message=f"Something went wrong when unmuting {target}" + ) + await interaction.response.send_message(embed=embed) + return + + await moderation.send_command_usage_alert( + bot=self.bot, + interaction=interaction, + command=f"/unmute target: {target.display_name}, reason: {reason}", + guild=interaction.guild, + target=target, + ) + embed = self.generate_response_embed( + user=target, action="unmute", reason=reason + ) + await interaction.response.send_message(content=target.mention, embed=embed) + + @app_commands.checks.has_permissions(kick_members=True) + @app_commands.checks.bot_has_permissions(kick_members=True) + @app_commands.command( + name="warn", description="Warns a user", extras={"module": "moderator"} + ) + async def handle_warn_user( + self, interaction: discord.Interaction, target: discord.Member, reason: str + ): + permission_check = await self.permission_check( + invoker=interaction.user, target=target, action_name="warn" + ) + if permission_check: + embed = auxiliary.prepare_deny_embed(message=permission_check) + await interaction.response.send_message(embed=embed) + return + + if target not in interaction.channel.members: + embed = auxiliary.prepare_deny_embed( + message=f"{target} cannot see this warning. No warning was added." + ) + await interaction.response.send_message(embed=embed) + return + + config = self.bot.guild_configs[str(interaction.guild.id)] + + new_count_of_warnings = ( + len(await self.get_all_warnings(target, interaction.guild)) + 1 + ) + + should_ban = False + if new_count_of_warnings >= config.extensions.protect.max_warnings.value: + await interaction.response.defer(ephemeral=False) + view = ui.Confirm() + await view.send( + message="This user has exceeded the max warnings of " + + f"{config.extensions.protect.max_warnings.value}. Would " + + "you like to ban them instead?", + channel=interaction.channel, + author=interaction.user, + interaction=interaction, + ) + await view.wait() + if view.value is ui.ConfirmResponse.CONFIRMED: + should_ban = True + + warn_result = await moderation.warn_user( + bot=self.bot, user=target, invoker=interaction.user, reason=reason + ) + + if should_ban: + ban_result = await moderation.ban_user( + guild=interaction.guild, + user=target, + delete_days=config.extensions.protect.ban_delete_duration.value, + reason=( + f"Over max warning count {new_count_of_warnings} out of" + f" {config.extensions.protect.max_warnings.value} (final warning:" + f" {reason}) - banned by {interaction.user}" + ), + ) + if not ban_result: + embed = auxiliary.prepare_deny_embed( + message=f"Something went wrong when banning {target}" + ) + if interaction.response.is_done(): + await interaction.followup.send(embed=embed) + else: + await interaction.response.send_message(embed=embed) + return + + if not warn_result: + embed = auxiliary.prepare_deny_embed( + message=f"Something went wrong when warning {target}" + ) + if interaction.response.is_done(): + await interaction.followup.send(embed=embed) + else: + await interaction.response.send_message(embed=embed) + return + + await moderation.send_command_usage_alert( + bot=self.bot, + interaction=interaction, + command=f"/warn target: {target.display_name}, reason: {reason}", + guild=interaction.guild, + target=target, + ) + + embed = self.generate_response_embed( + user=target, + action="warn", + reason=f"{reason} ({new_count_of_warnings} total warnings)", + ) + if interaction.response.is_done(): + await interaction.followup.send(content=target.mention, embed=embed) + else: + await interaction.response.send_message(content=target.mention, embed=embed) + + try: + await target.send(embed=embed) + except (discord.HTTPException, discord.Forbidden): + channel = config.get("logging_channel") + await self.bot.logger.send_log( + message=f"Failed to DM warning to {target}", + level=LogLevel.WARNING, + channel=channel, + context=LogContext( + guild=interaction.guild, channel=interaction.channel + ), + ) + + @app_commands.checks.has_permissions(kick_members=True) + @app_commands.checks.bot_has_permissions(kick_members=True) + @app_commands.command( + name="unwarn", description="Unwarns a user", extras={"module": "moderator"} + ) + async def handle_unwarn_user( + self, + interaction: discord.Interaction, + target: discord.Member, + reason: str, + warning: str, + ): + permission_check = await self.permission_check( + invoker=interaction.user, target=target, action_name="unwarn" + ) + if permission_check: + embed = auxiliary.prepare_deny_embed(message=permission_check) + await interaction.response.send_message(embed=embed) + return + + database_warning = await self.get_warning(user=target, warning=warning) + + if not database_warning: + embed = auxiliary.prepare_deny_embed( + message=f"{warning} was not found on {target}" + ) + await interaction.response.send_message(embed=embed) + return + + result = await moderation.unwarn_user( + bot=self.bot, user=target, warning=warning + ) + if not result: + embed = auxiliary.prepare_deny_embed( + message=f"Something went wrong when unwarning {target}" + ) + await interaction.response.send_message(embed=embed) + return + + await moderation.send_command_usage_alert( + bot=self.bot, + interaction=interaction, + command=f"/unwarn target: {target.display_name}, reason: {reason}, warning: {warning}", + guild=interaction.guild, + target=target, + ) + embed = self.generate_response_embed( + user=target, action="unwarn", reason=reason + ) + await interaction.response.send_message(content=target.mention, embed=embed) + + # Helper functions + + async def permission_check( + self, + invoker: discord.Member, + target: Union[discord.User, discord.Member], + action_name: str, + ) -> str: + """Checks permissions to ensure the command should be executed. This checks: + If the target is the invoker + If the target is the bot + If the user is in the server + If the target has an immune role + If the target can be banned by the bot + If the invoker has a higher role than the target + + Args: + invoker (discord.Member): The invoker of the action. + Either will be the user who ran the command, or the bot itself + target (Union[discord.User, discord.Member]): The target of the command. + Can be a user or member. + action_name (str): The action name to be displayed in messages + + Returns: + str: The rejection string, if one exists. Otherwise, None is returned + """ + config = self.bot.guild_configs[str(invoker.guild.id)] + # Check to see if executed on author + if invoker == target: + return f"You cannot {action_name} yourself" + + # Check to see if executed on bot + if target == self.bot.user: + return f"It would be silly to {action_name} myself" + + # Check to see if User or Member + if isinstance(target, discord.User): + return None + + # Check to see if target has any immune roles + for name in config.extensions.protect.immune_roles.value: + role_check = discord.utils.get(target.guild.roles, name=name) + if role_check and role_check in getattr(target, "roles", []): + return ( + f"You cannot {action_name} {target} because they have" + f" `{role_check}` role" + ) + + # Check to see if the Bot can execute on the target + if invoker.guild.get_member(int(self.bot.user.id)).top_role <= target.top_role: + return f"Bot does not have enough permissions to {action_name} `{target}`" + + # Check to see if author top role is higher than targets + if invoker.top_role <= target.top_role: + return f"You do not have enough permissions to {action_name} `{target}`" + + return None + + def generate_response_embed( + self, user: discord.Member, action: str, reason: str + ) -> discord.Embed: + """This generates a simple embed to be displayed in the chat where the command was called. + + Args: + user (discord.Member): The user who was actioned against + action (str): The string representation of the action type + reason (str): The reason the action was taken + + Returns: + discord.Embed: The formatted embed ready to be sent + """ + embed = discord.Embed( + title="Chat Protection", description=f"{action.upper()} `{user}`" + ) + embed.add_field(name="Reason", value=reason) + embed.set_thumbnail(url=user.display_avatar.url) + embed.color = discord.Color.gold() + + return embed + + # Database functions + + async def get_all_warnings( + self, user: discord.User, guild: discord.Guild + ) -> list[bot.models.Warning]: + """Gets a list of all warnings for a specific user in a specific guild + + Args: + user (discord.User): The user that we want warns from + guild (discord.Guild): The guild that we want warns from + + Returns: + list[bot.models.Warning]: The list of all warnings for the user/guild, if any exist + """ + warnings = ( + await self.bot.models.Warning.query.where( + self.bot.models.Warning.user_id == str(user.id) + ) + .where(self.bot.models.Warning.guild_id == str(guild.id)) + .gino.all() + ) + return warnings + + async def get_warning( + self, user: discord.Member, warning: str + ) -> bot.models.Warning: + """Gets a specific warning by string for a user + + Args: + user (discord.Member): The user to get the warning for + warning (str): The warning to look for + + Returns: + bot.models.Warning: If it exists, the warning object + """ + query = ( + self.bot.models.Warning.query.where( + self.bot.models.Warning.guild_id == str(user.guild.id) + ) + .where(self.bot.models.Warning.reason == warning) + .where(self.bot.models.Warning.user_id == str(user.id)) + ) + entry = await query.gino.first() + return entry diff --git a/techsupport_bot/commands/protect.py b/techsupport_bot/commands/protect.py index a374d206d..0f7267f36 100644 --- a/techsupport_bot/commands/protect.py +++ b/techsupport_bot/commands/protect.py @@ -671,87 +671,6 @@ async def create_linx_embed(self, config, ctx, content): return embed - @commands.has_permissions(ban_members=True) - @commands.bot_has_permissions(ban_members=True) - @commands.command( - name="ban", - brief="Bans a user", - description="Bans a user with a given reason", - usage="@user [reason]", - ) - async def ban_user(self, ctx, user: discord.User, *, reason: str = None): - """Method to ban a user from discord.""" - - # Uses the discord.Member class to get the top role attribute if the - # user is a part of the target guild - if ctx.guild.get_member(user.id) is not None: - await self.handle_ban(ctx, ctx.guild.get_member(user.id), reason) - else: - await self.handle_ban(ctx, user, reason) - - config = self.bot.guild_configs[str(ctx.guild.id)] - await self.send_alert(config, ctx, "Ban command") - - @commands.has_permissions(ban_members=True) - @commands.bot_has_permissions(ban_members=True) - @commands.command( - name="unban", - brief="Unbans a user", - description="Unbans a user with a given reason", - usage="@user [reason]", - ) - async def unban_user(self, ctx, user: discord.User, *, reason: str = None): - """Method to unban a user from discord.""" - - # Uses the discord.Member class to get the top role attribute if the - # user is a part of the target guild - if ctx.guild.get_member(user.id) is not None: - await self.handle_unban(ctx, ctx.guild.get_member(user.id), reason) - else: - await self.handle_unban(ctx, user, reason) - - @commands.has_permissions(kick_members=True) - @commands.bot_has_permissions(kick_members=True) - @commands.command( - name="kick", - brief="Kicks a user", - description="Kicks a user with a given reason", - usage="@user [reason]", - ) - async def kick_user(self, ctx, user: discord.Member, *, reason: str = None): - """Method to kick a user from discord.""" - await self.handle_kick(ctx, user, reason) - - config = self.bot.guild_configs[str(ctx.guild.id)] - await self.send_alert(config, ctx, "Kick command") - - @commands.has_permissions(kick_members=True) - @commands.bot_has_permissions(kick_members=True) - @commands.command( - name="warn", - brief="Warns a user", - description="Warn a user with a given reason", - usage="@user [reason]", - ) - async def warn_user(self, ctx, user: discord.Member, *, reason: str = None): - """Method to warn a user of wrongdoing in discord.""" - await self.handle_warn(ctx, user, reason) - - config = self.bot.guild_configs[str(ctx.guild.id)] - await self.send_alert(config, ctx, "Warn command") - - @commands.has_permissions(kick_members=True) - @commands.bot_has_permissions(kick_members=True) - @commands.command( - name="unwarn", - brief="Unwarns a user", - description="Unwarns a user with a given reason", - usage="@user [reason]", - ) - async def unwarn_user(self, ctx, user: discord.Member, *, reason: str = None): - """Method to unwarn a user on discord.""" - await self.handle_unwarn(ctx, user, reason) - @commands.has_permissions(kick_members=True) @commands.bot_has_permissions(kick_members=True) @commands.command( @@ -779,101 +698,6 @@ async def get_warnings_command(self, ctx, user: discord.User): await ctx.send(embed=embed) - @commands.has_permissions(moderate_members=True) - @commands.bot_has_permissions(moderate_members=True) - @commands.command( - name="mute", - brief="Mutes a user", - description="Times out a user for the specified duration", - usage="@user [time] [reason]", - aliases=["timeout"], - ) - async def mute(self, ctx, user: discord.Member, *, duration: str = None): - """ - Method to mute a user in discord using the native timeout. - This should be run via discord - - Parameters: - user: The discord.Member to be timed out. Required - duration: A string (# [s|m|h|d]) that declares how long. - Max time is 28 days by discord API. Defaults to 1 hour - """ - can_execute = await self.can_execute(ctx, user) - if not can_execute: - return - - # The API prevents administrators from being timed out. Check it here - if user.guild_permissions.administrator: - await auxiliary.send_deny_embed( - message=( - "Someone with the `administrator` permissions cannot be timed out" - ), - channel=ctx.channel, - ) - return - - delta_duration = None - - if duration: - # The date parser defaults to time in the past, so it is second - # This could be fixed by appending "in" to your query, but this is simpler - try: - delta_duration = datetime.datetime.now() - dateparser.parse(duration) - delta_duration = timedelta( - seconds=round(delta_duration.total_seconds()) - ) - except TypeError as exc: - raise ValueError("Invalid duration") from exc - if not delta_duration: - raise ValueError("Invalid duration") - else: - delta_duration = timedelta(hours=1) - - # Checks to ensure time is valid and within the scope of the API - if delta_duration > timedelta(days=28): - raise ValueError("Timeout duration cannot be more than 28 days") - if delta_duration < timedelta(seconds=1): - raise ValueError("Timeout duration cannot be less than 1 second") - - # Timeout the user and send messages to both the invocation channel, and the protect log - await user.timeout(delta_duration) - - embed = await self.generate_user_modified_embed( - user, f"muted for {delta_duration}", reason=None - ) - - await ctx.send(embed=embed) - - config = self.bot.guild_configs[str(ctx.guild.id)] - await self.send_alert(config, ctx, "Mute command") - - @commands.has_permissions(moderate_members=True) - @commands.bot_has_permissions(moderate_members=True) - @commands.command( - name="unmute", - brief="Unutes a user", - description="Removes a timeout from the user", - usage="@user", - aliases=["untimeout"], - ) - async def unmute(self, ctx, user: discord.Member, reason: str = None): - """Method to unmute a user in discord.""" - can_execute = await self.can_execute(ctx, user) - if not can_execute: - return - - if user.timed_out_until is None: - await auxiliary.send_deny_embed( - message="That user is not timed out", channel=ctx.channel - ) - return - - await user.timeout(None) - - embed = await self.generate_user_modified_embed(user, "unmuted", reason) - - await ctx.send(embed=embed) - @commands.has_permissions(manage_messages=True) @commands.bot_has_permissions(manage_messages=True) @commands.group( diff --git a/techsupport_bot/core/__init__.py b/techsupport_bot/core/__init__.py index c34c5c7c6..220730286 100644 --- a/techsupport_bot/core/__init__.py +++ b/techsupport_bot/core/__init__.py @@ -5,3 +5,4 @@ from .custom_errors import * from .databases import * from .http import * +from .moderation import * diff --git a/techsupport_bot/core/databases.py b/techsupport_bot/core/databases.py index 0b4202208..5c0120ef5 100644 --- a/techsupport_bot/core/databases.py +++ b/techsupport_bot/core/databases.py @@ -135,6 +135,7 @@ class Warning(bot.db.Model): guild_id = bot.db.Column(bot.db.String) reason = bot.db.Column(bot.db.String) time = bot.db.Column(bot.db.DateTime, default=datetime.datetime.utcnow) + invoker_id = bot.db.Column(bot.db.String) class Config(bot.db.Model): """The postgres table for guild config diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py new file mode 100644 index 000000000..dd5ac9ef7 --- /dev/null +++ b/techsupport_bot/core/moderation.py @@ -0,0 +1,196 @@ +"""This file will hold the core moderation functions. These functions will: +Do the proper moderative action and return true if successful, false if not.""" + + +from datetime import timedelta + +import discord + + +async def ban_user( + guild: discord.Guild, user: discord.User, delete_days: int, reason: str +) -> bool: + """A very simple function that bans a given user from the passed guild + + Args: + guild (discord.Guild): The guild to ban from + user (discord.User): The user who needs to be banned + delete_days (int): The numbers of days of past messages to delete + reason (str): The reason for banning + + Returns: + bool: True if ban was successful + """ + # Ban the user + await guild.ban( + user, + reason=reason, + delete_message_days=delete_days, + ) + return True + + +async def unban_user(guild: discord.Guild, user: discord.User, reason: str) -> bool: + """A very simple functon that unbans a given user from the passed guild + + Args: + guild (discord.Guild): The guild to unban from + user (discord.User): The user to unban + reason (str): The reason they are being unbanned + + Returns: + bool: True if unban was successful + """ + # Attempt to unban. If the user isn't found, return false + try: + await guild.unban(user, reason=reason) + return True + except discord.NotFound: + return False + + +async def kick_user(guild: discord.Guild, user: discord.Member, reason: str) -> bool: + """A very simple function that kicks a given user from the guild + + Args: + guild (discord.Guild): The guild to kick from + user (discord.Member): The member to kick from the guild + reason (str): The reason they are being kicked + + Returns: + bool: True if kick was successful + """ + await guild.kick(user, reason=reason) + return True + + +async def mute_user(user: discord.Member, reason: str, duration: timedelta) -> bool: + """Times out a given user + + Args: + user (discord.Member): The user to timeout + reason (str): The reason they are being timed out + duration (timedelta): How long to timeout the user for + + Returns: + bool: True if the timeout was successful + """ + await user.timeout(duration, reason=reason) + return True + + +async def unmute_user(user: discord.Member, reason: str) -> bool: + """Untimes out a given user. + + Args: + user (discord.Member): The user to untimeout + reason (str): The reason they are being untimeout + + Returns: + bool: True if untimeout was successful + """ + if not user.timed_out_until: + return False + await user.timeout(None, reason=reason) + return True + + +async def warn_user( + bot, user: discord.Member, invoker: discord.Member, reason: str +) -> bool: + """Warns a user. Does NOT check config or how many warnings a user has + + Args: + user (discord.Member): The user to warn + invoker (discord.Member): The person who warned the user + reason (str): The reason for the warning + + Returns: + bool: True if warning was successful + """ + await bot.models.Warning( + user_id=str(user.id), + guild_id=str(invoker.guild.id), + reason=reason, + invoker_id=str(invoker.id), + ).create() + return True + + +async def unwarn_user(bot, user: discord.Member, warning: str) -> bool: + """Removes a specific warning from a user by string + + Args: + user (discord.Member): The member to remove a warning from + warning (str): The warning to remove + + Returns: + bool: True if unwarning was successful + """ + query = ( + bot.models.Warning.query.where( + bot.models.Warning.guild_id == str(user.guild.id) + ) + .where(bot.models.Warning.reason == warning) + .where(bot.models.Warning.user_id == str(user.id)) + ) + entry = await query.gino.first() + if not entry: + return False + await entry.delete() + return True + + +async def send_command_usage_alert( + bot, + interaction: discord.Interaction, + command: str, + guild: discord.Guild, + target: discord.Member, +) -> None: + """Sends a usage alert to the protect events channel, if configured + + Args: + interaction (discord.Interaction): The interaction that trigger the command + command (str): The string representation of the command that was run + guild (discord.Guild): The guild the command was run in + target (discord.Member): The target of the command + """ + + ALERT_ICON_URL = ( + "https://cdn.icon-icons.com/icons2/2063/PNG/512/" + + "alert_danger_warning_notification_icon_124692.png" + ) + + config = bot.guild_configs[str(guild.id)] + + try: + alert_channel = guild.get_channel( + int(config.extensions.protect.alert_channel.value) + ) + except TypeError: + alert_channel = None + + if not alert_channel: + return + + embed = discord.Embed(title="Protect Alert") + + embed.add_field(name="Command", value=f"`{command}`", inline=False) + embed.add_field( + name="Channel", + value=f"{interaction.channel.name} ({interaction.channel.mention})", + ) + embed.add_field( + name="Invoking User", + value=f"{interaction.user.display_name} ({interaction.user.mention})", + ) + embed.add_field( + name="Target", + value=f"{target.display_name} ({target.mention})", + ) + + embed.set_thumbnail(url=ALERT_ICON_URL) + embed.color = discord.Color.red() + + await alert_channel.send(embed=embed) From 27490cd53ef6791ebffe4eb22b2346e02a56c5a8 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:41:46 -0500 Subject: [PATCH 02/99] Move purge/paste --- techsupport_bot/commands/protect.py | 211 +++------------------------- techsupport_bot/commands/purge.py | 111 +++++++++++++++ techsupport_bot/functions/paste.py | 166 ++++++++++++++++++++++ 3 files changed, 295 insertions(+), 193 deletions(-) create mode 100644 techsupport_bot/commands/purge.py create mode 100644 techsupport_bot/functions/paste.py diff --git a/techsupport_bot/commands/protect.py b/techsupport_bot/commands/protect.py index 0f7267f36..08bde868b 100644 --- a/techsupport_bot/commands/protect.py +++ b/techsupport_bot/commands/protect.py @@ -1,11 +1,24 @@ -"""Module for the protect extension of the discord bot.""" +""" +Todo: + Purge to slash commands + Unwarn has autofill + Get all warnings command + + Make all of automod + Simplify paste + Make paste not work if message would be DELETED by automod + Create a ban logging system like carl - Needs a database for ban history + +Ban logs need to be more centralized: + Auto bans, command bans, and manual bans all need to be logged with a single message + A modlog highscores command, in a modlog.py command + +Move all config over to specific new files +""" + -import datetime -import io import re -from datetime import timedelta -import dateparser import discord import expiringdict import munch @@ -153,7 +166,6 @@ class Protector(cogs.MatchCog): CLIPBOARD_ICON_URL = ( "https://icon-icons.com/icons2/203/PNG/128/diagram-30_24487.png" ) - CHARS_PER_NEWLINE = 80 async def preconfig(self): """Method to preconfig the protect.""" @@ -273,54 +285,6 @@ async def response(self, config, ctx, content, _): # the message is deleted, no need to pastebin it return - # check length of content - if len(content) > config.extensions.protect.length_limit.value or content.count( - "\n" - ) > self.max_newlines(config.extensions.protect.length_limit.value): - await self.handle_length_alert(config, ctx, content) - - def max_newlines(self, max_length): - """Method to set up the number of max lines.""" - return int(max_length / self.CHARS_PER_NEWLINE) + 1 - - async def handle_length_alert(self, config, ctx, content) -> None: - """Method to handle alert for the protect extension.""" - attachments: list[discord.File] = [] - if ctx.message.attachments: - total_attachment_size = 0 - for attch in ctx.message.attachments: - if ( - total_attachment_size := total_attachment_size + attch.size - ) <= ctx.filesize_limit: - attachments.append(await attch.to_file()) - if (lf := len(ctx.message.attachments) - len(attachments)) != 0: - log_channel = config.get("logging_channel") - await self.bot.logger.send_log( - message=( - f"Protect did not reupload {lf} file(s) due to file size limit." - ), - level=LogLevel.INFO, - channel=log_channel, - context=LogContext(guild=ctx.guild, channel=ctx.channel), - ) - await ctx.message.delete() - - reason = "message too long (too many newlines or characters)" - - if not self.bot.file_config.api.api_url.linx: - await self.send_default_delete_response(config, ctx, content, reason) - return - - linx_embed = await self.create_linx_embed(config, ctx, content) - if not linx_embed: - await self.send_default_delete_response(config, ctx, content, reason) - await self.send_alert(config, ctx, "Could not convert text to Linx paste") - return - - await ctx.send( - ctx.message.author.mention, embed=linx_embed, files=attachments[:10] - ) - async def handle_mass_mention_alert(self, config, ctx, content): """Method for handling mass mentions in an alert.""" await ctx.message.delete() @@ -447,26 +411,6 @@ async def handle_warn(self, ctx, user: discord.Member, reason: str, bypass=False user_id=str(user.id), guild_id=str(ctx.guild.id), reason=reason ).create() - async def handle_unwarn(self, ctx, user, reason, bypass=False): - """Method to handle an unwarn of a user.""" - # Always allow admins to unwarn other admins - if not bypass and not ctx.message.author.guild_permissions.administrator: - can_execute = await self.can_execute(ctx, user) - if not can_execute: - return - - warnings = await self.get_warnings(user, ctx.guild) - if not warnings: - await auxiliary.send_deny_embed( - message="There are no warnings for that user", channel=ctx.channel - ) - return - - await self.clear_warnings(user, ctx.guild) - - embed = await self.generate_user_modified_embed(user, "unwarn", reason) - await ctx.send(embed=embed) - async def handle_ban(self, ctx, user, reason, bypass=False): """Method to handle the ban of a user.""" if not bypass: @@ -492,39 +436,6 @@ async def handle_ban(self, ctx, user, reason, bypass=False): await ctx.send(embed=embed) - async def handle_unban(self, ctx, user, reason, bypass=False): - """Method to handle an unban of a user.""" - if not bypass: - can_execute = await self.can_execute(ctx, user) - if not can_execute: - return - - try: - await ctx.guild.unban(user, reason=reason) - except discord.NotFound: - await auxiliary.send_deny_embed( - message="This user is not banned, or does not exist", - channel=ctx.channel, - ) - return - - embed = await self.generate_user_modified_embed(user, "unban", reason) - - await ctx.send(embed=embed) - - async def handle_kick(self, ctx, user, reason, bypass=False): - """Method to handle the kicking from the discord of a user.""" - if not bypass: - can_execute = await self.can_execute(ctx, user) - if not can_execute: - return - - await ctx.guild.kick(user, reason=reason) - - embed = await self.generate_user_modified_embed(user, "kick", reason) - - await ctx.send(embed=embed) - async def clear_warnings(self, user, guild): """Method to clear warnings of a user in discord.""" await self.bot.models.Warning.delete.where( @@ -640,37 +551,6 @@ async def get_warnings(self, user, guild): ) return warnings - async def create_linx_embed(self, config, ctx, content): - """Method to create a link for long messages.""" - if not content: - return None - - headers = { - "Linx-Expiry": "1800", - "Linx-Randomize": "yes", - "Accept": "application/json", - } - file = {"file": io.StringIO(content)} - response = await self.bot.http_functions.http_call( - "post", self.bot.file_config.api.api_url.linx, headers=headers, data=file - ) - - url = response.get("url") - if not url: - return None - - embed = discord.Embed(description=url) - - embed.add_field(name="Paste Link", value=url) - embed.description = content[0:100].replace("\n", " ") - embed.set_author( - name=f"Paste by {ctx.author}", icon_url=ctx.author.display_avatar.url - ) - embed.set_footer(text=config.extensions.protect.paste_footer_message.value) - embed.color = discord.Color.blue() - - return embed - @commands.has_permissions(kick_members=True) @commands.bot_has_permissions(kick_members=True) @commands.command( @@ -697,58 +577,3 @@ async def get_warnings_command(self, ctx, user: discord.User): embed.color = discord.Color.red() await ctx.send(embed=embed) - - @commands.has_permissions(manage_messages=True) - @commands.bot_has_permissions(manage_messages=True) - @commands.group( - brief="Executes a purge command", - description="Executes a purge command", - ) - async def purge(self, ctx): - """Method to purge messages in discord.""" - await auxiliary.extension_help(self, ctx, self.__module__[9:]) - - @purge.command( - name="amount", - aliases=["x"], - brief="Purges messages by amount", - description="Purges the current channel's messages based on amount", - usage="[amount]", - ) - async def purge_amount(self, ctx, amount: int = 1): - """Method to get the amount to purge messages in discord.""" - config = self.bot.guild_configs[str(ctx.guild.id)] - - if amount <= 0 or amount > config.extensions.protect.max_purge_amount.value: - amount = config.extensions.protect.max_purge_amount.value - - await ctx.channel.purge(limit=amount) - - await self.send_alert(config, ctx, "Purge command") - - @purge.command( - name="duration", - aliases=["d"], - brief="Purges messages by duration", - description="Purges the current channel's messages up to a time", - usage="[duration (minutes)]", - ) - async def purge_duration(self, ctx, duration_minutes: int): - """Method to purge a channel's message up to a time.""" - if duration_minutes < 0: - await auxiliary.send_deny_embed( - message="I can't use that input", channel=ctx.channel - ) - return - - timestamp = datetime.datetime.utcnow() - datetime.timedelta( - minutes=duration_minutes - ) - - config = self.bot.guild_configs[str(ctx.guild.id)] - - await ctx.channel.purge( - after=timestamp, limit=config.extensions.protect.max_purge_amount.value - ) - - await self.send_alert(config, ctx, "Purge command") diff --git a/techsupport_bot/commands/purge.py b/techsupport_bot/commands/purge.py new file mode 100644 index 000000000..02113f097 --- /dev/null +++ b/techsupport_bot/commands/purge.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import datetime +from typing import TYPE_CHECKING + +import discord +from core import auxiliary, cogs +from discord.ext import commands + +if TYPE_CHECKING: + import bot + + +async def setup(bot: bot.TechSupportBot) -> None: + """Adds the cog to the bot. Setups config + + Args: + bot (bot.TechSupportBot): The bot object to register the cog with + """ + await bot.add_cog(Purger(bot=bot)) + + +class Purger(cogs.BaseCog): + ALERT_ICON_URL = ( + "https://cdn.icon-icons.com/icons2/2063/PNG/512/" + + "alert_danger_warning_notification_icon_124692.png" + ) + + @commands.has_permissions(manage_messages=True) + @commands.bot_has_permissions(manage_messages=True) + @commands.group( + brief="Executes a purge command", + description="Executes a purge command", + ) + async def purge(self, ctx): + """Method to purge messages in discord.""" + await auxiliary.extension_help(self, ctx, self.__module__[9:]) + + @purge.command( + name="amount", + aliases=["x"], + brief="Purges messages by amount", + description="Purges the current channel's messages based on amount", + usage="[amount]", + ) + async def purge_amount(self, ctx: commands.context, amount: int = 1): + """Method to get the amount to purge messages in discord.""" + config = self.bot.guild_configs[str(ctx.guild.id)] + + if amount <= 0 or amount > config.extensions.protect.max_purge_amount.value: + amount = config.extensions.protect.max_purge_amount.value + + await ctx.channel.purge(limit=amount) + + await self.send_alert(config, ctx, "Purge command") + + @purge.command( + name="duration", + aliases=["d"], + brief="Purges messages by duration", + description="Purges the current channel's messages up to a time", + usage="[duration (minutes)]", + ) + async def purge_duration(self, ctx, duration_minutes: int): + """Method to purge a channel's message up to a time.""" + if duration_minutes < 0: + await auxiliary.send_deny_embed( + message="I can't use that input", channel=ctx.channel + ) + return + + timestamp = datetime.datetime.utcnow() - datetime.timedelta( + minutes=duration_minutes + ) + + config = self.bot.guild_configs[str(ctx.guild.id)] + + await ctx.channel.purge( + after=timestamp, limit=config.extensions.protect.max_purge_amount.value + ) + + await self.send_alert(config, ctx, "Purge command") + + async def send_alert(self, config, ctx: commands.Context, message: str): + """Method to send an alert to the channel about a protect command.""" + try: + alert_channel = ctx.guild.get_channel( + int(config.extensions.protect.alert_channel.value) + ) + except TypeError: + alert_channel = None + + if not alert_channel: + return + + embed = discord.Embed(title="Protect Alert", description=message) + + if len(ctx.message.content) >= 256: + message_content = ctx.message.content[0:256] + else: + message_content = ctx.message.content + + embed.add_field(name="Channel", value=f"#{ctx.channel.name}") + embed.add_field(name="User", value=ctx.author.mention) + embed.add_field(name="Message", value=message_content, inline=False) + embed.add_field(name="URL", value=ctx.message.jump_url, inline=False) + + embed.set_thumbnail(url=self.ALERT_ICON_URL) + embed.color = discord.Color.red() + + await alert_channel.send(embed=embed) diff --git a/techsupport_bot/functions/paste.py b/techsupport_bot/functions/paste.py new file mode 100644 index 000000000..5fbb28a5b --- /dev/null +++ b/techsupport_bot/functions/paste.py @@ -0,0 +1,166 @@ +from __future__ import annotations + +import io +from typing import TYPE_CHECKING + +import discord +from botlogging import LogContext, LogLevel +from core import cogs +from discord.ext import commands + +if TYPE_CHECKING: + import bot + + +async def setup(bot: bot.TechSupportBot) -> None: + """Adds the cog to the bot. Setups config + + Args: + bot (bot.TechSupportBot): The bot object to register the cog with + """ + await bot.add_cog(Paster(bot=bot)) + + +class Paster(cogs.MatchCog): + async def match(self, config, ctx, content): + """Method to match roles for the protect command.""" + # exit the match based on exclusion parameters + if not str(ctx.channel.id) in config.extensions.protect.channels.value: + await self.bot.logger.send_log( + message="Channel not in protected channels - ignoring protect check", + level=LogLevel.DEBUG, + context=LogContext(guild=ctx.guild, channel=ctx.channel), + ) + return False + + role_names = [role.name.lower() for role in getattr(ctx.author, "roles", [])] + + if any( + role_name.lower() in role_names + for role_name in config.extensions.protect.bypass_roles.value + ): + return False + + if ctx.author.id in config.extensions.protect.bypass_ids.value: + return False + + return True + + async def response(self, config, ctx, content, _): + # check length of content + if len(content) > config.extensions.protect.length_limit.value or content.count( + "\n" + ) > self.max_newlines(config.extensions.protect.length_limit.value): + await self.handle_length_alert(config, ctx, content) + + def max_newlines(self, max_length): + """Method to set up the number of max lines.""" + return int(max_length / 80) + 1 + + @commands.Cog.listener() + async def on_raw_message_edit(self, payload: discord.RawMessageUpdateEvent): + """Method to edit the raw message.""" + guild = self.bot.get_guild(payload.guild_id) + if not guild: + return + + config = self.bot.guild_configs[str(guild.id)] + if not self.extension_enabled(config): + return + + channel = self.bot.get_channel(payload.channel_id) + if not channel: + return + + message = await channel.fetch_message(payload.message_id) + if not message: + return + + # Don't trigger if content hasn't changed + if payload.cached_message and payload.cached_message.content == message.content: + return + + ctx = await self.bot.get_context(message) + matched = await self.match(config, ctx, message.content) + if not matched: + return + + await self.response(config, ctx, message.content, None) + + async def handle_length_alert(self, config, ctx, content) -> None: + """Method to handle alert for the protect extension.""" + attachments: list[discord.File] = [] + if ctx.message.attachments: + total_attachment_size = 0 + for attch in ctx.message.attachments: + if ( + total_attachment_size := total_attachment_size + attch.size + ) <= ctx.filesize_limit: + attachments.append(await attch.to_file()) + if (lf := len(ctx.message.attachments) - len(attachments)) != 0: + log_channel = config.get("logging_channel") + await self.bot.logger.send_log( + message=( + f"Protect did not reupload {lf} file(s) due to file size limit." + ), + level=LogLevel.INFO, + channel=log_channel, + context=LogContext(guild=ctx.guild, channel=ctx.channel), + ) + await ctx.message.delete() + + reason = "message too long (too many newlines or characters)" + + if not self.bot.file_config.api.api_url.linx: + await self.send_default_delete_response(config, ctx, content, reason) + return + + linx_embed = await self.create_linx_embed(config, ctx, content) + if not linx_embed: + await self.send_default_delete_response(config, ctx, content, reason) + # await self.send_alert(config, ctx, "Could not convert text to Linx paste") + return + + await ctx.send( + ctx.message.author.mention, embed=linx_embed, files=attachments[:10] + ) + + async def send_default_delete_response(self, config, ctx, content, reason): + """Method for the default delete of a message.""" + embed = discord.Embed( + title="Chat Protection", description=f"Message deleted. Reason: *{reason}*" + ) + embed.color = discord.Color.gold() + await ctx.send(ctx.message.author.mention, embed=embed) + await ctx.author.send(f"Deleted message: ```{content[:1994]}```") + + async def create_linx_embed(self, config, ctx, content): + """Method to create a link for long messages.""" + if not content: + return None + + headers = { + "Linx-Expiry": "1800", + "Linx-Randomize": "yes", + "Accept": "application/json", + } + file = {"file": io.StringIO(content)} + response = await self.bot.http_functions.http_call( + "post", self.bot.file_config.api.api_url.linx, headers=headers, data=file + ) + + url = response.get("url") + if not url: + return None + + embed = discord.Embed(description=url) + + embed.add_field(name="Paste Link", value=url) + embed.description = content[0:100].replace("\n", " ") + embed.set_author( + name=f"Paste by {ctx.author}", icon_url=ctx.author.display_avatar.url + ) + embed.set_footer(text=config.extensions.protect.paste_footer_message.value) + embed.color = discord.Color.blue() + + return embed From 9f9e99c3cd2efa7f2806fe470236831ad838b368 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 13 Jan 2024 16:10:34 -0500 Subject: [PATCH 03/99] Add base of automod --- techsupport_bot/functions/automod.py | 51 ++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 techsupport_bot/functions/automod.py diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py new file mode 100644 index 000000000..5eefdcebe --- /dev/null +++ b/techsupport_bot/functions/automod.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import io +from typing import TYPE_CHECKING + +import discord +from botlogging import LogContext, LogLevel +from core import cogs +from discord.ext import commands + +if TYPE_CHECKING: + import bot + + +async def setup(bot: bot.TechSupportBot) -> None: + """Adds the cog to the bot. Setups config + + Args: + bot (bot.TechSupportBot): The bot object to register the cog with + """ + await bot.add_cog(AutoMod(bot=bot)) + + +class AutoMod(cogs.MatchCog): + async def match(self, config, ctx, content): + ... + + async def response(self, config, ctx, content, _): + ... + + + async def run_all_checks(self, message: discord.Message) -> bool: + ... + + async def run_only_string_checks(self, content: str, member: discord.Member) -> bool: + ... + + async def handle_file_extensions(self, attachments: list[discord.Attachment]) -> bool: + ... + + async def handle_mentions(self, message: discord.Message) -> bool: + ... + + async def handle_exact_string(self, content: str) -> bool: + ... + + async def handle_regex_string(self, content: str) -> bool: + ... + + async def should_ban_or_warn(self, member: discord.Member): + ... \ No newline at end of file From 5007b22f6b914c2957dbfd1ed944efb76e3c0a2a Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 13 Jan 2024 16:11:04 -0500 Subject: [PATCH 04/99] Formatting --- techsupport_bot/functions/automod.py | 48 ++++++++++++++++------------ 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 5eefdcebe..f92b55403 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -24,28 +24,34 @@ async def setup(bot: bot.TechSupportBot) -> None: class AutoMod(cogs.MatchCog): async def match(self, config, ctx, content): ... - + async def response(self, config, ctx, content, _): ... - - async def run_all_checks(self, message: discord.Message) -> bool: - ... - - async def run_only_string_checks(self, content: str, member: discord.Member) -> bool: - ... - - async def handle_file_extensions(self, attachments: list[discord.Attachment]) -> bool: - ... - - async def handle_mentions(self, message: discord.Message) -> bool: - ... - - async def handle_exact_string(self, content: str) -> bool: - ... - - async def handle_regex_string(self, content: str) -> bool: - ... - async def should_ban_or_warn(self, member: discord.Member): - ... \ No newline at end of file +async def run_all_checks(message: discord.Message) -> bool: + ... + + +async def run_only_string_checks(content: str, member: discord.Member) -> bool: + ... + + +async def handle_file_extensions(attachments: list[discord.Attachment]) -> bool: + ... + + +async def handle_mentions(message: discord.Message) -> bool: + ... + + +async def handle_exact_string(content: str) -> bool: + ... + + +async def handle_regex_string(content: str) -> bool: + ... + + +async def should_ban_or_warn(member: discord.Member): + ... From af7b2ecd5d6b96eeaf562bb37b8743c59b9c8fc2 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 13 Jan 2024 16:20:11 -0500 Subject: [PATCH 05/99] Improve automod framework --- techsupport_bot/functions/automod.py | 52 +++++++++++++++++++++------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index f92b55403..a35942429 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -1,12 +1,10 @@ from __future__ import annotations -import io +from dataclasses import dataclass from typing import TYPE_CHECKING import discord -from botlogging import LogContext, LogLevel from core import cogs -from discord.ext import commands if TYPE_CHECKING: import bot @@ -21,6 +19,22 @@ async def setup(bot: bot.TechSupportBot) -> None: await bot.add_cog(AutoMod(bot=bot)) +@dataclass +class AutoModPunishment: + """This is a base class holding the violation and recommended actions + Since automod is a framework, the actions can translate to different things + + violation_str - The string of the policy broken. Should be displayed to user + recommend_delete - If the policy recommends deletion of the message + recommend_warn - If the policy recommends warning the user + + """ + + violation_str: str + recommend_delete: bool + recommend_warn: bool + + class AutoMod(cogs.MatchCog): async def match(self, config, ctx, content): ... @@ -28,30 +42,44 @@ async def match(self, config, ctx, content): async def response(self, config, ctx, content, _): ... - -async def run_all_checks(message: discord.Message) -> bool: - ... + async def should_ban_or_warn(member: discord.Member): + ... -async def run_only_string_checks(content: str, member: discord.Member) -> bool: +async def run_all_checks( + guild: discord.Guild, config, message: discord.Message +) -> list[AutoModPunishment]: + # Automod will only ever be a framework to say something needs to be done + # Outside of running from the response function, NO ACTION will be taken + # All checks will return a list of AutoModPunishment, which may be nothing ... -async def handle_file_extensions(attachments: list[discord.Attachment]) -> bool: +async def run_only_string_checks( + guild: discord.Guild, config, content: str +) -> list[AutoModPunishment]: ... -async def handle_mentions(message: discord.Message) -> bool: +async def handle_file_extensions( + guild: discord.Guild, config, attachments: list[discord.Attachment] +) -> list[AutoModPunishment]: ... -async def handle_exact_string(content: str) -> bool: +async def handle_mentions( + guild: discord.Guild, config, message: discord.Message +) -> list[AutoModPunishment]: ... -async def handle_regex_string(content: str) -> bool: +async def handle_exact_string( + guild: discord.Guild, config, content: str +) -> list[AutoModPunishment]: ... -async def should_ban_or_warn(member: discord.Member): +async def handle_regex_string( + guild: discord.Guild, config, content: str +) -> list[AutoModPunishment]: ... From 642fbab69b2187958fbf764cfc67bf68cfd4dc96 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 14 Jan 2024 08:01:16 -0500 Subject: [PATCH 06/99] Update automod framework, add banlogger framework --- techsupport_bot/commands/ban_log.py | 14 ++++++++++++++ techsupport_bot/functions/automod.py | 17 ++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 techsupport_bot/commands/ban_log.py diff --git a/techsupport_bot/commands/ban_log.py b/techsupport_bot/commands/ban_log.py new file mode 100644 index 000000000..a4f90b0d1 --- /dev/null +++ b/techsupport_bot/commands/ban_log.py @@ -0,0 +1,14 @@ + + + + +class BanLogger: + async def high_score_command(self, interaction): + ... + + async def on_ban(self, guild, ban): + ... + + +async def log_ban(banned_member, banning_moderator, guild): + ... \ No newline at end of file diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index a35942429..f50f0e026 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -4,7 +4,9 @@ from typing import TYPE_CHECKING import discord +import munch from core import cogs +from discord.ext import commands if TYPE_CHECKING: import bot @@ -36,13 +38,22 @@ class AutoModPunishment: class AutoMod(cogs.MatchCog): - async def match(self, config, ctx, content): + async def match( + self, config: munch.Munch, ctx: commands.Context, content: str + ) -> bool: ... - async def response(self, config, ctx, content, _): + async def response( + self, config: munch.Munch, ctx: commands.Context, content: str, result: bool + ) -> None: ... - async def should_ban_or_warn(member: discord.Member): + async def should_ban_instead_of_warn(self, member: discord.Member) -> bool: + ... + + async def send_automod_alert( + self, message: discord.Message, violation: AutoModPunishment + ) -> None: ... From 4f85d78d65c221601be5f4dbb918604fbfea8a46 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 14 Jan 2024 08:01:56 -0500 Subject: [PATCH 07/99] Rename file --- techsupport_bot/commands/{ban_log.py => modlog.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename techsupport_bot/commands/{ban_log.py => modlog.py} (100%) diff --git a/techsupport_bot/commands/ban_log.py b/techsupport_bot/commands/modlog.py similarity index 100% rename from techsupport_bot/commands/ban_log.py rename to techsupport_bot/commands/modlog.py From 746735e0615e93caf6c2b1e7a0af4cd4afb52ed0 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 14 Jan 2024 08:17:39 -0500 Subject: [PATCH 08/99] Write some of automod --- techsupport_bot/commands/modlog.py | 14 ++++---- techsupport_bot/commands/protect.py | 1 + techsupport_bot/functions/automod.py | 53 +++++++++++++++++----------- 3 files changed, 41 insertions(+), 27 deletions(-) diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index a4f90b0d1..6586c3946 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -1,14 +1,14 @@ - - - - class BanLogger: async def high_score_command(self, interaction): ... - + async def on_ban(self, guild, ban): ... - + async def log_ban(banned_member, banning_moderator, guild): - ... \ No newline at end of file + ... + + +async def log_unban(unbanned_member, unbanning_moderator, guild): + ... diff --git a/techsupport_bot/commands/protect.py b/techsupport_bot/commands/protect.py index 08bde868b..c76a79976 100644 --- a/techsupport_bot/commands/protect.py +++ b/techsupport_bot/commands/protect.py @@ -8,6 +8,7 @@ Simplify paste Make paste not work if message would be DELETED by automod Create a ban logging system like carl - Needs a database for ban history + Central unban logging but NO database needed Ban logs need to be more centralized: Auto bans, command bans, and manual bans all need to be logged with a single message diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index f50f0e026..3873a5d2a 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -57,7 +57,7 @@ async def send_automod_alert( ... -async def run_all_checks( +def run_all_checks( guild: discord.Guild, config, message: discord.Message ) -> list[AutoModPunishment]: # Automod will only ever be a framework to say something needs to be done @@ -66,31 +66,44 @@ async def run_all_checks( ... -async def run_only_string_checks( +def run_only_string_checks( guild: discord.Guild, config, content: str ) -> list[AutoModPunishment]: ... -async def handle_file_extensions( - guild: discord.Guild, config, attachments: list[discord.Attachment] +def handle_file_extensions( + config, attachments: list[discord.Attachment] ) -> list[AutoModPunishment]: - ... - - -async def handle_mentions( - guild: discord.Guild, config, message: discord.Message -) -> list[AutoModPunishment]: - ... - - -async def handle_exact_string( - guild: discord.Guild, config, content: str -) -> list[AutoModPunishment]: - ... - - -async def handle_regex_string( + violations = [] + for attachment in attachments: + if ( + attachment.filename.split(".")[-1] + in config.extensions.protect.banned_file_extensions.value + ): + violations.append( + AutoModPunishment( + f"{attachment.filename} has a suspicious file extension", True, True + ) + ) + return violations + + +def handle_mentions(config, message: discord.Message) -> list[AutoModPunishment]: + if len(message.mentions) > config.extensions.protect.max_mentions.value: + return [AutoModPunishment("Mass Mentions", True, True)] + return [] + + +def handle_exact_string(config, content: str) -> list[AutoModPunishment]: + violations = [] + for rule in config.extensions.protect.block_strings.value: + if rule.string.lower() in content.lower(): + violations.append(AutoModPunishment(rule.message, rule.delete, rule.warn)) + return violations + + +def handle_regex_string( guild: discord.Guild, config, content: str ) -> list[AutoModPunishment]: ... From 4307f6e3b5c5cf68f39da06121ad36720d92d569 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 22 Jul 2024 09:44:16 -0400 Subject: [PATCH 09/99] Add regex checks --- techsupport_bot/functions/automod.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 3873a5d2a..798d68e40 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -7,6 +7,7 @@ import munch from core import cogs from discord.ext import commands +import re if TYPE_CHECKING: import bot @@ -58,7 +59,7 @@ async def send_automod_alert( def run_all_checks( - guild: discord.Guild, config, message: discord.Message + config, message: discord.Message ) -> list[AutoModPunishment]: # Automod will only ever be a framework to say something needs to be done # Outside of running from the response function, NO ACTION will be taken @@ -67,7 +68,7 @@ def run_all_checks( def run_only_string_checks( - guild: discord.Guild, config, content: str + config, content: str ) -> list[AutoModPunishment]: ... @@ -104,6 +105,16 @@ def handle_exact_string(config, content: str) -> list[AutoModPunishment]: def handle_regex_string( - guild: discord.Guild, config, content: str + config, content: str ) -> list[AutoModPunishment]: - ... + violations = [] + for rule in config.extensions.protect.block_strings.value: + regex = rule.get("regex") + if regex: + try: + match = re.search(regex, content) + except re.error: + match = None + if match: + violations.append(AutoModPunishment(rule.message, rule.delete, rule.warn)) + return violations From bb8a3f910d96146264f5f4c0951baf8fe83560fe Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 22 Jul 2024 15:31:20 -0400 Subject: [PATCH 10/99] Formatting --- techsupport_bot/commands/modlog.py | 12 ++++------- techsupport_bot/commands/protect.py | 1 - techsupport_bot/commands/purge.py | 2 +- techsupport_bot/core/moderation.py | 1 - techsupport_bot/functions/automod.py | 31 ++++++++++------------------ techsupport_bot/functions/paste.py | 2 +- 6 files changed, 17 insertions(+), 32 deletions(-) diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index 6586c3946..c894b0741 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -1,14 +1,10 @@ class BanLogger: - async def high_score_command(self, interaction): - ... + async def high_score_command(self, interaction): ... - async def on_ban(self, guild, ban): - ... + async def on_ban(self, guild, ban): ... -async def log_ban(banned_member, banning_moderator, guild): - ... +async def log_ban(banned_member, banning_moderator, guild): ... -async def log_unban(unbanned_member, unbanning_moderator, guild): - ... +async def log_unban(unbanned_member, unbanning_moderator, guild): ... diff --git a/techsupport_bot/commands/protect.py b/techsupport_bot/commands/protect.py index c76a79976..3db30e843 100644 --- a/techsupport_bot/commands/protect.py +++ b/techsupport_bot/commands/protect.py @@ -17,7 +17,6 @@ Move all config over to specific new files """ - import re import discord diff --git a/techsupport_bot/commands/purge.py b/techsupport_bot/commands/purge.py index 02113f097..c570ecd0a 100644 --- a/techsupport_bot/commands/purge.py +++ b/techsupport_bot/commands/purge.py @@ -50,7 +50,7 @@ async def purge_amount(self, ctx: commands.context, amount: int = 1): if amount <= 0 or amount > config.extensions.protect.max_purge_amount.value: amount = config.extensions.protect.max_purge_amount.value - await ctx.channel.purge(limit=amount) + await ctx.channel.purge(limit=amount + 1) await self.send_alert(config, ctx, "Purge command") diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index dd5ac9ef7..5d520378a 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -1,7 +1,6 @@ """This file will hold the core moderation functions. These functions will: Do the proper moderative action and return true if successful, false if not.""" - from datetime import timedelta import discord diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 798d68e40..7537d580b 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -1,5 +1,6 @@ from __future__ import annotations +import re from dataclasses import dataclass from typing import TYPE_CHECKING @@ -7,7 +8,6 @@ import munch from core import cogs from discord.ext import commands -import re if TYPE_CHECKING: import bot @@ -41,36 +41,27 @@ class AutoModPunishment: class AutoMod(cogs.MatchCog): async def match( self, config: munch.Munch, ctx: commands.Context, content: str - ) -> bool: - ... + ) -> bool: ... async def response( self, config: munch.Munch, ctx: commands.Context, content: str, result: bool - ) -> None: - ... + ) -> None: ... - async def should_ban_instead_of_warn(self, member: discord.Member) -> bool: - ... + async def should_ban_instead_of_warn(self, member: discord.Member) -> bool: ... async def send_automod_alert( self, message: discord.Message, violation: AutoModPunishment - ) -> None: - ... + ) -> None: ... -def run_all_checks( - config, message: discord.Message -) -> list[AutoModPunishment]: +def run_all_checks(config, message: discord.Message) -> list[AutoModPunishment]: # Automod will only ever be a framework to say something needs to be done # Outside of running from the response function, NO ACTION will be taken # All checks will return a list of AutoModPunishment, which may be nothing ... -def run_only_string_checks( - config, content: str -) -> list[AutoModPunishment]: - ... +def run_only_string_checks(config, content: str) -> list[AutoModPunishment]: ... def handle_file_extensions( @@ -104,9 +95,7 @@ def handle_exact_string(config, content: str) -> list[AutoModPunishment]: return violations -def handle_regex_string( - config, content: str -) -> list[AutoModPunishment]: +def handle_regex_string(config, content: str) -> list[AutoModPunishment]: violations = [] for rule in config.extensions.protect.block_strings.value: regex = rule.get("regex") @@ -116,5 +105,7 @@ def handle_regex_string( except re.error: match = None if match: - violations.append(AutoModPunishment(rule.message, rule.delete, rule.warn)) + violations.append( + AutoModPunishment(rule.message, rule.delete, rule.warn) + ) return violations diff --git a/techsupport_bot/functions/paste.py b/techsupport_bot/functions/paste.py index 5fbb28a5b..45acf7329 100644 --- a/techsupport_bot/functions/paste.py +++ b/techsupport_bot/functions/paste.py @@ -103,7 +103,7 @@ async def handle_length_alert(self, config, ctx, content) -> None: message=( f"Protect did not reupload {lf} file(s) due to file size limit." ), - level=LogLevel.INFO, + level=LogLevel.WARNING, channel=log_channel, context=LogContext(guild=ctx.guild, channel=ctx.channel), ) From aabceab38c15eaa9f309045984ee9d50747105ca Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 22 Jul 2024 18:57:10 -0400 Subject: [PATCH 11/99] Mostly finish automod --- techsupport_bot/commands/__init__.py | 1 + techsupport_bot/commands/moderator.py | 87 ++++++-------- techsupport_bot/core/moderation.py | 21 ++++ techsupport_bot/functions/automod.py | 156 +++++++++++++++++++++++--- 4 files changed, 192 insertions(+), 73 deletions(-) diff --git a/techsupport_bot/commands/__init__.py b/techsupport_bot/commands/__init__.py index ab0153aab..fa1b9b139 100644 --- a/techsupport_bot/commands/__init__.py +++ b/techsupport_bot/commands/__init__.py @@ -16,5 +16,6 @@ from .linter import * from .listen import * from .mock import * +from .moderator import * from .roll import * from .wyr import * diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 763c7f61b..2466f295c 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -97,7 +97,7 @@ async def handle_ban_user( guild=interaction.guild, target=target, ) - embed = self.generate_response_embed(user=target, action="ban", reason=reason) + embed = generate_response_embed(user=target, action="ban", reason=reason) await interaction.response.send_message(content=target.mention, embed=embed) @app_commands.checks.has_permissions(ban_members=True) @@ -148,7 +148,7 @@ async def handle_unban_user( guild=interaction.guild, target=target, ) - embed = self.generate_response_embed(user=target, action="unban", reason=reason) + embed = generate_response_embed(user=target, action="unban", reason=reason) await interaction.response.send_message(content=target.mention, embed=embed) @app_commands.checks.has_permissions(kick_members=True) @@ -188,7 +188,7 @@ async def handle_kick_user( guild=interaction.guild, target=target, ) - embed = self.generate_response_embed(user=target, action="kick", reason=reason) + embed = generate_response_embed(user=target, action="kick", reason=reason) await interaction.response.send_message(content=target.mention, embed=embed) @app_commands.checks.has_permissions(moderate_members=True) @@ -266,7 +266,7 @@ async def handle_mute_user( guild=interaction.guild, target=target, ) - embed = self.generate_response_embed(user=target, action="mute", reason=reason) + embed = generate_response_embed(user=target, action="mute", reason=reason) await interaction.response.send_message(content=target.mention, embed=embed) @app_commands.checks.has_permissions(moderate_members=True) @@ -312,9 +312,7 @@ async def handle_unmute_user( guild=interaction.guild, target=target, ) - embed = self.generate_response_embed( - user=target, action="unmute", reason=reason - ) + embed = generate_response_embed(user=target, action="unmute", reason=reason) await interaction.response.send_message(content=target.mention, embed=embed) @app_commands.checks.has_permissions(kick_members=True) @@ -343,7 +341,8 @@ async def handle_warn_user( config = self.bot.guild_configs[str(interaction.guild.id)] new_count_of_warnings = ( - len(await self.get_all_warnings(target, interaction.guild)) + 1 + len(await moderation.get_all_warnings(self.bot, target, interaction.guild)) + + 1 ) should_ban = False @@ -405,7 +404,7 @@ async def handle_warn_user( target=target, ) - embed = self.generate_response_embed( + embed = generate_response_embed( user=target, action="warn", reason=f"{reason} ({new_count_of_warnings} total warnings)", @@ -474,9 +473,7 @@ async def handle_unwarn_user( guild=interaction.guild, target=target, ) - embed = self.generate_response_embed( - user=target, action="unwarn", reason=reason - ) + embed = generate_response_embed(user=target, action="unwarn", reason=reason) await interaction.response.send_message(content=target.mention, embed=embed) # Helper functions @@ -537,51 +534,8 @@ async def permission_check( return None - def generate_response_embed( - self, user: discord.Member, action: str, reason: str - ) -> discord.Embed: - """This generates a simple embed to be displayed in the chat where the command was called. - - Args: - user (discord.Member): The user who was actioned against - action (str): The string representation of the action type - reason (str): The reason the action was taken - - Returns: - discord.Embed: The formatted embed ready to be sent - """ - embed = discord.Embed( - title="Chat Protection", description=f"{action.upper()} `{user}`" - ) - embed.add_field(name="Reason", value=reason) - embed.set_thumbnail(url=user.display_avatar.url) - embed.color = discord.Color.gold() - - return embed - # Database functions - async def get_all_warnings( - self, user: discord.User, guild: discord.Guild - ) -> list[bot.models.Warning]: - """Gets a list of all warnings for a specific user in a specific guild - - Args: - user (discord.User): The user that we want warns from - guild (discord.Guild): The guild that we want warns from - - Returns: - list[bot.models.Warning]: The list of all warnings for the user/guild, if any exist - """ - warnings = ( - await self.bot.models.Warning.query.where( - self.bot.models.Warning.user_id == str(user.id) - ) - .where(self.bot.models.Warning.guild_id == str(guild.id)) - .gino.all() - ) - return warnings - async def get_warning( self, user: discord.Member, warning: str ) -> bot.models.Warning: @@ -603,3 +557,26 @@ async def get_warning( ) entry = await query.gino.first() return entry + + +def generate_response_embed( + user: discord.Member, action: str, reason: str +) -> discord.Embed: + """This generates a simple embed to be displayed in the chat where the command was called. + + Args: + user (discord.Member): The user who was actioned against + action (str): The string representation of the action type + reason (str): The reason the action was taken + + Returns: + discord.Embed: The formatted embed ready to be sent + """ + embed = discord.Embed( + title="Chat Protection", description=f"{action.upper()} `{user}`" + ) + embed.add_field(name="Reason", value=reason) + embed.set_thumbnail(url=user.display_avatar.url) + embed.color = discord.Color.gold() + + return embed diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index 5d520378a..86ab1dd7f 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -4,6 +4,7 @@ from datetime import timedelta import discord +import munch async def ban_user( @@ -140,6 +141,26 @@ async def unwarn_user(bot, user: discord.Member, warning: str) -> bool: return True +async def get_all_warnings( + bot, user: discord.User, guild: discord.Guild +) -> list[munch.Munch]: + """Gets a list of all warnings for a specific user in a specific guild + + Args: + user (discord.User): The user that we want warns from + guild (discord.Guild): The guild that we want warns from + + Returns: + list[bot.models.Warning]: The list of all warnings for the user/guild, if any exist + """ + warnings = ( + await bot.models.Warning.query.where(bot.models.Warning.user_id == str(user.id)) + .where(bot.models.Warning.guild_id == str(guild.id)) + .gino.all() + ) + return warnings + + async def send_command_usage_alert( bot, interaction: discord.Interaction, diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 7537d580b..f62b5492e 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -2,11 +2,14 @@ import re from dataclasses import dataclass +from datetime import timedelta from typing import TYPE_CHECKING import discord import munch -from core import cogs +from botlogging import LogContext, LogLevel +from commands import moderator +from core import cogs, moderation from discord.ext import commands if TYPE_CHECKING: @@ -19,7 +22,7 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cog with """ - await bot.add_cog(AutoMod(bot=bot)) + await bot.add_cog(AutoMod(bot=bot, extension_name="automod")) @dataclass @@ -30,38 +33,133 @@ class AutoModPunishment: violation_str - The string of the policy broken. Should be displayed to user recommend_delete - If the policy recommends deletion of the message recommend_warn - If the policy recommends warning the user + recommend_mute - If the policy recommends muting the user """ violation_str: str recommend_delete: bool recommend_warn: bool + recommend_mute: bool + + @property + def score(self) -> int: + score = 0 + if self.recommend_mute: + score += 4 + if self.recommend_warn: + score += 2 + if self.recommend_delete: + score += 1 + return score class AutoMod(cogs.MatchCog): async def match( self, config: munch.Munch, ctx: commands.Context, content: str - ) -> bool: ... + ) -> bool: + if not str(ctx.channel.id) in config.extensions.protect.channels.value: + await self.bot.logger.send_log( + message="Channel not in protected channels - ignoring protect check", + level=LogLevel.DEBUG, + context=LogContext(guild=ctx.guild, channel=ctx.channel), + ) + return False + + role_names = [role.name.lower() for role in getattr(ctx.author, "roles", [])] + + if any( + role_name.lower() in role_names + for role_name in config.extensions.protect.bypass_roles.value + ): + return False + + if ctx.author.id in config.extensions.protect.bypass_ids.value: + return False + + return True async def response( self, config: munch.Munch, ctx: commands.Context, content: str, result: bool - ) -> None: ... + ) -> None: + should_delete = False + should_warn = False + should_mute = False + + all_punishments = run_all_checks(config, ctx.message) + + if len(all_punishments) == 0: + return + + sorted_punishments = sorted( + all_punishments, key=lambda p: p.score, reverse=True + ) + for punishment in sorted_punishments: + should_delete = should_delete or punishment.recommend_delete + should_warn = should_warn or punishment.recommend_warn + should_mute = should_mute or punishment.recommend_mute + + actions = [] + if should_mute: + actions.append("mute") + if should_warn: + actions.append("warn") + if should_delete: + actions.append("delete") + + if len(actions) == 0: + actions.append("notice") + + actions_str = " & ".join(actions) + + embed = moderator.generate_response_embed( + ctx.author, actions_str, sorted_punishments[0].violation_str + ) + + if should_mute and not ctx.author.timed_out_until: + await ctx.author.timeout( + timedelta(hours=1), + reason=sorted_punishments[0].violation_str, + ) + + if should_delete: + await ctx.message.delete() - async def should_ban_instead_of_warn(self, member: discord.Member) -> bool: ... + if should_warn: + await moderation.warn_user( + self.bot, ctx.author, ctx.author, sorted_punishments[0].violation_str + ) + + count_of_warnings = ( + len(await moderation.get_all_warnings(self.bot, ctx.author, ctx.guild)) + + 1 + ) + + if count_of_warnings >= config.extensions.protect.max_warnings.value: + await moderation.ban_user( + ctx.guild, ctx.author, 7, sorted_punishments[0].violation_str + ) - async def send_automod_alert( - self, message: discord.Message, violation: AutoModPunishment - ) -> None: ... + await ctx.send(embed=embed) def run_all_checks(config, message: discord.Message) -> list[AutoModPunishment]: # Automod will only ever be a framework to say something needs to be done # Outside of running from the response function, NO ACTION will be taken # All checks will return a list of AutoModPunishment, which may be nothing - ... + all_violations = ( + run_only_string_checks(config, message.clean_content) + + handle_file_extensions(config, message.attachments) + + handle_mentions(config, message) + ) + return all_violations -def run_only_string_checks(config, content: str) -> list[AutoModPunishment]: ... +def run_only_string_checks(config, content: str) -> list[AutoModPunishment]: + all_violations = handle_exact_string(config, content) + handle_regex_string( + config, content + ) + return all_violations def handle_file_extensions( @@ -75,7 +173,10 @@ def handle_file_extensions( ): violations.append( AutoModPunishment( - f"{attachment.filename} has a suspicious file extension", True, True + f"{attachment.filename} has a suspicious file extension", + True, + True, + False, ) ) return violations @@ -83,22 +184,36 @@ def handle_file_extensions( def handle_mentions(config, message: discord.Message) -> list[AutoModPunishment]: if len(message.mentions) > config.extensions.protect.max_mentions.value: - return [AutoModPunishment("Mass Mentions", True, True)] + return [AutoModPunishment("Mass Mentions", True, True, False)] return [] def handle_exact_string(config, content: str) -> list[AutoModPunishment]: violations = [] - for rule in config.extensions.protect.block_strings.value: - if rule.string.lower() in content.lower(): - violations.append(AutoModPunishment(rule.message, rule.delete, rule.warn)) + for ( + keyword, + filter_config, + ) in config.extensions.protect.string_map.value.items(): + print(filter_config) + if keyword.lower() in content.lower(): + violations.append( + AutoModPunishment( + filter_config.message, + filter_config.delete, + filter_config.warn, + filter_config.mute, + ) + ) return violations def handle_regex_string(config, content: str) -> list[AutoModPunishment]: violations = [] - for rule in config.extensions.protect.block_strings.value: - regex = rule.get("regex") + for ( + keyword, + filter_config, + ) in config.extensions.protect.string_map.value.items(): + regex = filter_config.get("regex") if regex: try: match = re.search(regex, content) @@ -106,6 +221,11 @@ def handle_regex_string(config, content: str) -> list[AutoModPunishment]: match = None if match: violations.append( - AutoModPunishment(rule.message, rule.delete, rule.warn) + AutoModPunishment( + filter_config.message, + filter_config.delete, + filter_config.warn, + filter_config.mute, + ) ) return violations From 91c08e5a41f5488a205d46c79daa402da1bb51f0 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 22 Jul 2024 19:25:15 -0400 Subject: [PATCH 12/99] Update purge --- techsupport_bot/commands/purge.py | 108 +++++++++------------------ techsupport_bot/functions/automod.py | 1 - 2 files changed, 37 insertions(+), 72 deletions(-) diff --git a/techsupport_bot/commands/purge.py b/techsupport_bot/commands/purge.py index c570ecd0a..f8fe6730b 100644 --- a/techsupport_bot/commands/purge.py +++ b/techsupport_bot/commands/purge.py @@ -4,7 +4,8 @@ from typing import TYPE_CHECKING import discord -from core import auxiliary, cogs +from core import auxiliary, cogs, moderation +from discord import app_commands from discord.ext import commands if TYPE_CHECKING: @@ -26,86 +27,51 @@ class Purger(cogs.BaseCog): + "alert_danger_warning_notification_icon_124692.png" ) - @commands.has_permissions(manage_messages=True) - @commands.bot_has_permissions(manage_messages=True) - @commands.group( - brief="Executes a purge command", - description="Executes a purge command", + @app_commands.checks.has_permissions(manage_messages=True) + @app_commands.checks.bot_has_permissions(manage_messages=True) + @app_commands.command( + name="purge", + description="Purge by pure duration of messages", + extras={"module": "purge"}, ) - async def purge(self, ctx): - """Method to purge messages in discord.""" - await auxiliary.extension_help(self, ctx, self.__module__[9:]) - - @purge.command( - name="amount", - aliases=["x"], - brief="Purges messages by amount", - description="Purges the current channel's messages based on amount", - usage="[amount]", - ) - async def purge_amount(self, ctx: commands.context, amount: int = 1): - """Method to get the amount to purge messages in discord.""" - config = self.bot.guild_configs[str(ctx.guild.id)] + async def purge_command( + self, + interaction: discord.Interaction, + amount: int, + duration_minutes: int = None, + ): + """Method to purge a channel's message up to a time.""" + config = self.bot.guild_configs[str(interaction.guild.id)] if amount <= 0 or amount > config.extensions.protect.max_purge_amount.value: - amount = config.extensions.protect.max_purge_amount.value - - await ctx.channel.purge(limit=amount + 1) - - await self.send_alert(config, ctx, "Purge command") - - @purge.command( - name="duration", - aliases=["d"], - brief="Purges messages by duration", - description="Purges the current channel's messages up to a time", - usage="[duration (minutes)]", - ) - async def purge_duration(self, ctx, duration_minutes: int): - """Method to purge a channel's message up to a time.""" - if duration_minutes < 0: - await auxiliary.send_deny_embed( - message="I can't use that input", channel=ctx.channel + embed = auxiliary.prepare_deny_embed( + message="This is an invalid amount of messages to purge", ) + await interaction.response.send_message(embed=embed, ephemeral=True) return - timestamp = datetime.datetime.utcnow() - datetime.timedelta( - minutes=duration_minutes - ) - - config = self.bot.guild_configs[str(ctx.guild.id)] - - await ctx.channel.purge( - after=timestamp, limit=config.extensions.protect.max_purge_amount.value - ) - - await self.send_alert(config, ctx, "Purge command") - - async def send_alert(self, config, ctx: commands.Context, message: str): - """Method to send an alert to the channel about a protect command.""" - try: - alert_channel = ctx.guild.get_channel( - int(config.extensions.protect.alert_channel.value) + if duration_minutes and duration_minutes < 0: + embed = auxiliary.prepare_deny_embed( + message="This is an invalid duration", ) - except TypeError: - alert_channel = None - - if not alert_channel: + await interaction.response.send_message(embed=embed, ephemeral=True) return - embed = discord.Embed(title="Protect Alert", description=message) - - if len(ctx.message.content) >= 256: - message_content = ctx.message.content[0:256] + if duration_minutes: + timestamp = datetime.datetime.utcnow() - datetime.timedelta( + minutes=duration_minutes + ) else: - message_content = ctx.message.content + timestamp = None - embed.add_field(name="Channel", value=f"#{ctx.channel.name}") - embed.add_field(name="User", value=ctx.author.mention) - embed.add_field(name="Message", value=message_content, inline=False) - embed.add_field(name="URL", value=ctx.message.jump_url, inline=False) + await interaction.response.send_message("Purge Successful", ephemeral=True) - embed.set_thumbnail(url=self.ALERT_ICON_URL) - embed.color = discord.Color.red() + await interaction.channel.purge(after=timestamp, limit=amount) - await alert_channel.send(embed=embed) + await moderation.send_command_usage_alert( + bot=self.bot, + interaction=interaction, + command=f"/purge amount: {amount} duration: {duration_minutes}", + guild=interaction.guild, + target=interaction.user, + ) diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index f62b5492e..b5784f12a 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -194,7 +194,6 @@ def handle_exact_string(config, content: str) -> list[AutoModPunishment]: keyword, filter_config, ) in config.extensions.protect.string_map.value.items(): - print(filter_config) if keyword.lower() in content.lower(): violations.append( AutoModPunishment( From 5e9d47b3820c5ce55175d6201c378570d6671cd0 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 22 Jul 2024 20:09:22 -0400 Subject: [PATCH 13/99] Improve automod --- techsupport_bot/commands/protect.py | 153 --------------------------- techsupport_bot/functions/automod.py | 141 ++++++++++++++++++++---- 2 files changed, 118 insertions(+), 176 deletions(-) diff --git a/techsupport_bot/commands/protect.py b/techsupport_bot/commands/protect.py index 3db30e843..0448e3598 100644 --- a/techsupport_bot/commands/protect.py +++ b/techsupport_bot/commands/protect.py @@ -173,134 +173,6 @@ async def preconfig(self): max_len=100, max_age_seconds=3600 ) - async def match(self, config, ctx, content): - """Method to match roles for the protect command.""" - # exit the match based on exclusion parameters - if not str(ctx.channel.id) in config.extensions.protect.channels.value: - await self.bot.logger.send_log( - message="Channel not in protected channels - ignoring protect check", - level=LogLevel.DEBUG, - context=LogContext(guild=ctx.guild, channel=ctx.channel), - ) - return False - - role_names = [role.name.lower() for role in getattr(ctx.author, "roles", [])] - - if any( - role_name.lower() in role_names - for role_name in config.extensions.protect.bypass_roles.value - ): - return False - - if ctx.author.id in config.extensions.protect.bypass_ids.value: - return False - - return True - - @commands.Cog.listener() - async def on_raw_message_edit(self, payload: discord.RawMessageUpdateEvent): - """Method to edit the raw message.""" - guild = self.bot.get_guild(payload.guild_id) - if not guild: - return - - config = self.bot.guild_configs[str(guild.id)] - if not self.extension_enabled(config): - return - - channel = self.bot.get_channel(payload.channel_id) - if not channel: - return - - message = await channel.fetch_message(payload.message_id) - if not message: - return - - # Don't trigger if content hasn't changed - if payload.cached_message and payload.cached_message.content == message.content: - return - - ctx = await self.bot.get_context(message) - matched = await self.match(config, ctx, message.content) - if not matched: - return - - await self.response(config, ctx, message.content, None) - - def search_by_text_regex(self, config, content): - """Function to search given input by all - text and regex rules from the config""" - triggered_config = None - for ( - keyword, - filter_config, - ) in config.extensions.protect.string_map.value.items(): - filter_config = munch.munchify(filter_config) - search_keyword = keyword - search_content = content - - regex = filter_config.get("regex") - if regex: - try: - match = re.search(regex, search_content) - except re.error: - match = None - if match: - filter_config["trigger"] = keyword - triggered_config = filter_config - if triggered_config.get("delete"): - return triggered_config - else: - if filter_config.get("sensitive"): - search_keyword = search_keyword.lower() - search_content = search_content.lower() - if search_keyword in search_content: - filter_config["trigger"] = keyword - triggered_config = filter_config - if triggered_config.get("delete"): - return triggered_config - return triggered_config - - async def response(self, config, ctx, content, _): - """Method to define the response for the protect extension.""" - # check mass mentions first - return after handling - if len(ctx.message.mentions) > config.extensions.protect.max_mentions.value: - await self.handle_mass_mention_alert(config, ctx, content) - return - - # search the message against keyword strings - triggered_config = self.search_by_text_regex(config, content) - - for attachment in ctx.message.attachments: - if ( - attachment.filename.split(".")[-1] - in config.extensions.protect.banned_file_extensions.value - ): - await self.handle_file_extension_alert(config, ctx, attachment.filename) - return - - if triggered_config: - await self.handle_string_alert(config, ctx, content, triggered_config) - if triggered_config.get("delete"): - # the message is deleted, no need to pastebin it - return - - async def handle_mass_mention_alert(self, config, ctx, content): - """Method for handling mass mentions in an alert.""" - await ctx.message.delete() - await self.handle_warn(ctx, ctx.author, "mass mention", bypass=True) - await self.send_alert(config, ctx, f"Mass mentions from {ctx.author}") - - async def handle_file_extension_alert(self, config, ctx, filename): - """Method for handling suspicious file extensions.""" - await ctx.message.delete() - await self.handle_warn( - ctx, ctx.author, "Suspicious file extension", bypass=True - ) - await self.send_alert( - config, ctx, f"Suspicious file uploaded by {ctx.author}: {filename}" - ) - async def handle_string_alert(self, config, ctx, content, filter_config): """Method to handle a string alert for the protect extension.""" # If needed, delete the message @@ -411,31 +283,6 @@ async def handle_warn(self, ctx, user: discord.Member, reason: str, bypass=False user_id=str(user.id), guild_id=str(ctx.guild.id), reason=reason ).create() - async def handle_ban(self, ctx, user, reason, bypass=False): - """Method to handle the ban of a user.""" - if not bypass: - can_execute = await self.can_execute(ctx, user) - if not can_execute: - return - - async for ban in ctx.guild.bans(limit=None): - if user == ban.user: - await auxiliary.send_deny_embed( - message="User is already banned.", channel=ctx.channel - ) - return - - config = self.bot.guild_configs[str(ctx.guild.id)] - await ctx.guild.ban( - user, - reason=reason, - delete_message_days=config.extensions.protect.ban_delete_duration.value, - ) - - embed = await self.generate_user_modified_embed(user, "ban", reason) - - await ctx.send(embed=embed) - async def clear_warnings(self, user, guild): """Method to clear warnings of a user in discord.""" await self.bot.models.Warning.delete.where( diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index b5784f12a..3b39e4a61 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -100,47 +100,142 @@ async def response( should_mute = should_mute or punishment.recommend_mute actions = [] + + reason_str = sorted_punishments[0].violation_str + if should_mute: actions.append("mute") - if should_warn: - actions.append("warn") + if not ctx.author.timed_out_until: + await ctx.author.timeout( + timedelta(hours=1), + reason=sorted_punishments[0].violation_str, + ) + if should_delete: actions.append("delete") + await ctx.message.delete() + + if should_warn: + actions.append("warn") + count_of_warnings = ( + len(await moderation.get_all_warnings(self.bot, ctx.author, ctx.guild)) + + 1 + ) + reason_str += f" ({count_of_warnings} total warnings)" + await moderation.warn_user( + self.bot, ctx.author, ctx.author, sorted_punishments[0].violation_str + ) + if count_of_warnings >= config.extensions.protect.max_warnings.value: + ban_embed = moderator.generate_response_embed( + ctx.author, + "ban", + reason=( + f"Over max warning count {count_of_warnings} out of" + f" {config.extensions.protect.max_warnings.value} (final warning:" + f" {sorted_punishments[0].violation_str}) - banned by automod" + ), + ) + + await ctx.send(content=ctx.author.mention, embed=ban_embed) + try: + await ctx.author.send(embed=ban_embed) + except discord.Forbidden: + await self.bot.logger.send_log( + message=f"Could not DM {ctx.author} about being banned", + level=LogLevel.WARNING, + context=LogContext(guild=ctx.guild, channel=ctx.channel), + ) + + await moderation.ban_user( + ctx.guild, ctx.author, 7, sorted_punishments[0].violation_str + ) if len(actions) == 0: actions.append("notice") actions_str = " & ".join(actions) - embed = moderator.generate_response_embed( - ctx.author, actions_str, sorted_punishments[0].violation_str - ) + embed = moderator.generate_response_embed(ctx.author, actions_str, reason_str) - if should_mute and not ctx.author.timed_out_until: - await ctx.author.timeout( - timedelta(hours=1), - reason=sorted_punishments[0].violation_str, + await ctx.send(content=ctx.author.mention, embed=embed) + try: + await ctx.author.send(embed=embed) + except discord.Forbidden: + await self.bot.logger.send_log( + message=f"Could not DM {ctx.author} about being automodded", + level=LogLevel.WARNING, + context=LogContext(guild=ctx.guild, channel=ctx.channel), ) - if should_delete: - await ctx.message.delete() + alert_channel_embed = generate_automod_alert_embed(ctx, sorted_punishments) - if should_warn: - await moderation.warn_user( - self.bot, ctx.author, ctx.author, sorted_punishments[0].violation_str - ) + config = self.bot.guild_configs[str(ctx.guild.id)] - count_of_warnings = ( - len(await moderation.get_all_warnings(self.bot, ctx.author, ctx.guild)) - + 1 + try: + alert_channel = ctx.guild.get_channel( + int(config.extensions.protect.alert_channel.value) ) + except TypeError: + alert_channel = None - if count_of_warnings >= config.extensions.protect.max_warnings.value: - await moderation.ban_user( - ctx.guild, ctx.author, 7, sorted_punishments[0].violation_str - ) + if not alert_channel: + return + + await alert_channel.send(embed=alert_channel_embed) + + @commands.Cog.listener() + async def on_raw_message_edit(self, payload: discord.RawMessageUpdateEvent): + """Method to edit the raw message.""" + guild = self.bot.get_guild(payload.guild_id) + if not guild: + return + + config = self.bot.guild_configs[str(guild.id)] + if not self.extension_enabled(config): + return + + channel = self.bot.get_channel(payload.channel_id) + if not channel: + return + + message = await channel.fetch_message(payload.message_id) + if not message: + return + + # Don't trigger if content hasn't changed + if payload.cached_message and payload.cached_message.content == message.content: + return + + ctx = await self.bot.get_context(message) + matched = await self.match(config, ctx, message.content) + if not matched: + return + + await self.response(config, ctx, message.content, matched) + + +def generate_automod_alert_embed( + ctx: commands.Context, violations: list[AutoModPunishment] +): + + ALERT_ICON_URL = ( + "https://cdn.icon-icons.com/icons2/2063/PNG/512/" + + "alert_danger_warning_notification_icon_124692.png" + ) + + embed = discord.Embed( + title="Automod Violations", + description="\n".join(violation.violation_str for violation in violations), + ) + embed.add_field(name="Channel", value=f"{ctx.channel.mention} ({ctx.channel.name})") + embed.add_field(name="User", value=f"{ctx.author.mention} ({ctx.author.name})") + embed.add_field(name="Message", value=ctx.message.content, inline=False) + embed.add_field(name="URL", value=ctx.message.jump_url, inline=False) + + embed.set_thumbnail(url=ALERT_ICON_URL) + embed.color = discord.Color.red() - await ctx.send(embed=embed) + return embed def run_all_checks(config, message: discord.Message) -> list[AutoModPunishment]: From 7ea5942a78b74cd994a0569f089500959ed75f76 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 22 Jul 2024 21:14:12 -0400 Subject: [PATCH 14/99] Setup basic modlog logging, no commands --- techsupport_bot/commands/__init__.py | 1 + techsupport_bot/commands/moderator.py | 10 ++ techsupport_bot/commands/modlog.py | 149 +++++++++++++++++++++++++- techsupport_bot/commands/purge.py | 2 +- techsupport_bot/core/databases.py | 12 +++ techsupport_bot/functions/automod.py | 31 ++++-- 6 files changed, 192 insertions(+), 13 deletions(-) diff --git a/techsupport_bot/commands/__init__.py b/techsupport_bot/commands/__init__.py index fa1b9b139..9d6bca0b4 100644 --- a/techsupport_bot/commands/__init__.py +++ b/techsupport_bot/commands/__init__.py @@ -17,5 +17,6 @@ from .listen import * from .mock import * from .moderator import * +from .modlog import * from .roll import * from .wyr import * diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 2466f295c..2192c9632 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -9,6 +9,7 @@ import discord import ui from botlogging import LogContext, LogLevel +from commands import modlog from core import auxiliary, cogs, moderation from discord import app_commands @@ -87,6 +88,10 @@ async def handle_ban_user( await interaction.response.send_message(embed=embed) return + await modlog.log_ban( + self.bot, target, interaction.user, interaction.guild, reason + ) + await moderation.send_command_usage_alert( bot=self.bot, interaction=interaction, @@ -141,6 +146,8 @@ async def handle_unban_user( await interaction.response.send_message(embed=embed) return + await modlog.log_unban(target, interaction.user, interaction.guild, reason) + await moderation.send_command_usage_alert( bot=self.bot, interaction=interaction, @@ -385,6 +392,9 @@ async def handle_warn_user( else: await interaction.response.send_message(embed=embed) return + await modlog.log_ban( + self.bot, target, interaction.user, interaction.guild, reason + ) if not warn_result: embed = auxiliary.prepare_deny_embed( diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index c894b0741..98b8df100 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -1,10 +1,149 @@ -class BanLogger: - async def high_score_command(self, interaction): ... +from __future__ import annotations - async def on_ban(self, guild, ban): ... +import datetime +from typing import TYPE_CHECKING, Self +import discord +from core import cogs +from discord.ext import commands -async def log_ban(banned_member, banning_moderator, guild): ... +if TYPE_CHECKING: + import bot -async def log_unban(unbanned_member, unbanning_moderator, guild): ... +async def setup(bot: bot.TechSupportBot) -> None: + """Adds the cog to the bot. Setups config + + Args: + bot (bot.TechSupportBot): The bot object to register the cog with + """ + await bot.add_cog(BanLogger(bot=bot, extension_name="modlog")) + + +class BanLogger(cogs.BaseCog): + async def high_score_command(self: Self, interaction: discord.Interaction): ... + + async def lookup_user_command( + self: Self, interaction: discord.Interaction, user: discord.User + ): ... + + async def lookup_moderator_command( + self: Self, interaction: discord.Interaction, moderator: discord.Member + ): ... + + @commands.Cog.listener() + async def on_member_ban( + self: Self, guild: discord.Guild, user: discord.User | discord.Member + ) -> None: + await discord.utils.sleep_until( + discord.utils.utcnow() + datetime.timedelta(seconds=2) + ) + + # Fetch the audit logs for the ban action + async for entry in guild.audit_logs(limit=1, action=discord.AuditLogAction.ban): + if entry.target.id == user.id: + moderator = entry.user + + if moderator.bot: + return + + await log_ban(self.bot, user, moderator, guild, entry.reason) + + @commands.Cog.listener() + async def on_member_unban( + self: Self, guild: discord.Guild, user: discord.User + ) -> None: + # Wait a short time to ensure the audit log has been updated + await discord.utils.sleep_until( + discord.utils.utcnow() + datetime.timedelta(seconds=2) + ) + + # Fetch the audit logs for the unban action + async for entry in guild.audit_logs( + limit=1, action=discord.AuditLogAction.unban + ): + if entry.target.id == user.id: + moderator = entry.user + + if moderator.bot: + return + + await log_unban(self.bot, user, moderator, guild, entry.reason) + + +# Any bans initiated by TS will come through this +async def log_ban( + bot: bot.TechSupportBot, + banned_member: discord.User | discord.Member, + banning_moderator: discord.Member, + guild: discord.Guild, + reason: str, +): + if not reason: + reason = "No reason specified" + + ban = bot.models.BanLog( + guild_id=str(guild.id), + reason=reason, + banning_moderator=str(banning_moderator.id), + banned_member=str(banned_member.id), + ) + ban = await ban.create() + + embed = discord.Embed(title=f"ban | case {ban.pk}") + embed.description = ( + f"**Offender:** {banned_member.name} {banned_member.mention}\n" + f"**Reason:** {reason}\n" + f"**Responsible moderator:** {banning_moderator.name} {banning_moderator.mention}" + ) + embed.set_footer(text=f"ID: {banned_member.id}") + embed.timestamp = datetime.datetime.utcnow() + embed.color = discord.Color.red() + + config = bot.guild_configs[str(guild.id)] + + try: + alert_channel = guild.get_channel( + int(config.extensions.protect.alert_channel.value) + ) + except TypeError: + alert_channel = None + + if not alert_channel: + return + + await alert_channel.send(embed=embed) + + +async def log_unban( + unbanned_member: discord.User | discord.Member, + unbanning_moderator: discord.Member, + guild: discord.Guild, + reason: str, +): + if not reason: + reason = "No reason specified" + + embed = discord.Embed(title=f"unban") + embed.description = ( + f"**Offender:** {unbanned_member.name} {unbanned_member.mention}\n" + f"**Reason:** {reason}\n" + f"**Responsible moderator:** {unbanning_moderator.name} {unbanning_moderator.mention}" + ) + embed.set_footer(text=f"ID: {unbanned_member.id}") + embed.timestamp = datetime.datetime.utcnow() + embed.color = discord.Color.green() + + config = bot.guild_configs[str(guild.id)] + + try: + alert_channel = guild.get_channel( + int(config.extensions.protect.alert_channel.value) + ) + except TypeError: + alert_channel = None + + if not alert_channel: + return + + await alert_channel.send(embed=embed) diff --git a/techsupport_bot/commands/purge.py b/techsupport_bot/commands/purge.py index f8fe6730b..03ee87927 100644 --- a/techsupport_bot/commands/purge.py +++ b/techsupport_bot/commands/purge.py @@ -71,7 +71,7 @@ async def purge_command( await moderation.send_command_usage_alert( bot=self.bot, interaction=interaction, - command=f"/purge amount: {amount} duration: {duration_minutes}", + command=f"/purge amount: {amount}, duration: {duration_minutes}", guild=interaction.guild, target=interaction.user, ) diff --git a/techsupport_bot/core/databases.py b/techsupport_bot/core/databases.py index 20008b37e..96de81883 100644 --- a/techsupport_bot/core/databases.py +++ b/techsupport_bot/core/databases.py @@ -65,6 +65,17 @@ class ApplicationBans(bot.db.Model): guild_id = bot.db.Column(bot.db.String) applicant_id = bot.db.Column(bot.db.String) + class BanLog(bot.db.Model): + + __tablename__ = "banlog" + + pk = bot.db.Column(bot.db.Integer, primary_key=True, autoincrement=True) + guild_id = bot.db.Column(bot.db.String) + reason = bot.db.Column(bot.db.String) + banning_moderator = bot.db.Column(bot.db.String) + banned_member = bot.db.Column(bot.db.String) + ban_time = bot.db.Column(bot.db.DateTime, default=datetime.datetime.utcnow) + class DuckUser(bot.db.Model): """The postgres table for ducks Currently used in duck.py @@ -337,6 +348,7 @@ class Votes(bot.db.Model): bot.models.Applications = Applications bot.models.AppBans = ApplicationBans + bot.models.BanLog = BanLog bot.models.DuckUser = DuckUser bot.models.Factoid = Factoid bot.models.FactoidJob = FactoidJob diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 3b39e4a61..2cdaefa87 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -8,7 +8,7 @@ import discord import munch from botlogging import LogContext, LogLevel -from commands import moderator +from commands import moderator, modlog from core import cogs, moderation from discord.ext import commands @@ -149,6 +149,13 @@ async def response( await moderation.ban_user( ctx.guild, ctx.author, 7, sorted_punishments[0].violation_str ) + await modlog.log_ban( + self.bot, + ctx.author, + ctx.guild.me, + ctx.guild, + sorted_punishments[0].violation_str, + ) if len(actions) == 0: actions.append("notice") @@ -167,7 +174,9 @@ async def response( context=LogContext(guild=ctx.guild, channel=ctx.channel), ) - alert_channel_embed = generate_automod_alert_embed(ctx, sorted_punishments) + alert_channel_embed = generate_automod_alert_embed( + ctx, sorted_punishments, actions_str + ) config = self.bot.guild_configs[str(ctx.guild.id)] @@ -215,7 +224,7 @@ async def on_raw_message_edit(self, payload: discord.RawMessageUpdateEvent): def generate_automod_alert_embed( - ctx: commands.Context, violations: list[AutoModPunishment] + ctx: commands.Context, violations: list[AutoModPunishment], action_taken: str ): ALERT_ICON_URL = ( @@ -227,6 +236,7 @@ def generate_automod_alert_embed( title="Automod Violations", description="\n".join(violation.violation_str for violation in violations), ) + embed.add_field(name="Actions Taken", value=action_taken) embed.add_field(name="Channel", value=f"{ctx.channel.mention} ({ctx.channel.name})") embed.add_field(name="User", value=f"{ctx.author.mention} ({ctx.author.name})") embed.add_field(name="Message", value=ctx.message.content, inline=False) @@ -269,9 +279,9 @@ def handle_file_extensions( violations.append( AutoModPunishment( f"{attachment.filename} has a suspicious file extension", - True, - True, - False, + recommend_delete=True, + recommend_warn=True, + recommend_mute=False, ) ) return violations @@ -279,7 +289,14 @@ def handle_file_extensions( def handle_mentions(config, message: discord.Message) -> list[AutoModPunishment]: if len(message.mentions) > config.extensions.protect.max_mentions.value: - return [AutoModPunishment("Mass Mentions", True, True, False)] + return [ + AutoModPunishment( + "Mass Mentions", + recommend_delete=True, + recommend_warn=True, + recommend_mute=False, + ) + ] return [] From 0f502568f2669de40f7294938bcdd5ae625eee97 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 22 Jul 2024 21:15:11 -0400 Subject: [PATCH 15/99] Add layout of report --- techsupport_bot/commands/report.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 techsupport_bot/commands/report.py diff --git a/techsupport_bot/commands/report.py b/techsupport_bot/commands/report.py new file mode 100644 index 000000000..222e00ca0 --- /dev/null +++ b/techsupport_bot/commands/report.py @@ -0,0 +1,3 @@ +class Report: + async def report_command(self, interaction, report_str): + ... \ No newline at end of file From fda054c81b7febe25c4d21b70460c1dbcc5a5f9c Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 22 Jul 2024 23:12:54 -0400 Subject: [PATCH 16/99] Modlog commands done --- techsupport_bot/commands/moderator.py | 4 +- techsupport_bot/commands/modlog.py | 121 +++++++++++++++++++++++++- techsupport_bot/commands/report.py | 3 +- 3 files changed, 121 insertions(+), 7 deletions(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 2192c9632..a5714ba79 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -146,7 +146,9 @@ async def handle_unban_user( await interaction.response.send_message(embed=embed) return - await modlog.log_unban(target, interaction.user, interaction.guild, reason) + await modlog.log_unban( + self.bot, target, interaction.user, interaction.guild, reason + ) await moderation.send_command_usage_alert( bot=self.bot, diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index 98b8df100..16da2396a 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -1,10 +1,14 @@ from __future__ import annotations import datetime +from collections import Counter from typing import TYPE_CHECKING, Self import discord -from core import cogs +import munch +import ui +from core import auxiliary, cogs +from discord import app_commands from discord.ext import commands if TYPE_CHECKING: @@ -21,15 +25,123 @@ async def setup(bot: bot.TechSupportBot) -> None: class BanLogger(cogs.BaseCog): - async def high_score_command(self: Self, interaction: discord.Interaction): ... + modlog_group = app_commands.Group( + name="modlog", description="...", extras={"module": "modlog"} + ) + + @modlog_group.command( + name="highscores", + description="Unban someone and allow them to apply", + extras={"module": "modlog"}, + ) + async def high_score_command(self: Self, interaction: discord.Interaction): + all_bans = await self.bot.models.BanLog.query.where( + self.bot.models.BanLog.guild_id == str(interaction.guild.id) + ).gino.all() + ban_frequency_counter = Counter(ban.banning_moderator for ban in all_bans) + + sorted_ban_frequency = sorted( + ban_frequency_counter.items(), key=lambda x: x[1], reverse=True + ) + embed = discord.Embed(title="Most active moderators") + + final_string = "" + for index, (moderator_id, count) in enumerate(sorted_ban_frequency): + moderator = await interaction.guild.fetch_member(int(moderator_id)) + final_string += ( + f"{index+1}. {moderator.display_name} " + f"{moderator.mention} ({moderator.id}) - ({count})\n" + ) + embed.description = final_string + embed.color = discord.Color.blue() + await interaction.response.send_message(embed=embed) + + @modlog_group.command( + name="lookup-user", + description="Unban someone and allow them to apply", + extras={"module": "modlog"}, + ) async def lookup_user_command( self: Self, interaction: discord.Interaction, user: discord.User - ): ... + ): + recent_bans_by_user = ( + await self.bot.models.BanLog.query.where( + self.bot.models.BanLog.guild_id == str(interaction.guild.id) + ) + .where(self.bot.models.BanLog.banned_member == str(user.id)) + .order_by(self.bot.models.BanLog.ban_time.desc()) + .limit(10) + .gino.all() + ) + + embeds = [] + for ban in recent_bans_by_user: + embeds.append( + await self.convert_ban_to_pretty_string(ban, f"{user.name} bans") + ) + + if len(embeds) == 0: + embed = auxiliary.prepare_deny_embed( + f"No bans for the user {user.name} could be found" + ) + await interaction.response.send_message(embed=embed) + return + await interaction.response.defer(ephemeral=False) + view = ui.PaginateView() + await view.send(interaction.channel, interaction.user, embeds, interaction) + + @modlog_group.command( + name="lookup-moderator", + description="Unban someone and allow them to apply", + extras={"module": "modlog"}, + ) async def lookup_moderator_command( self: Self, interaction: discord.Interaction, moderator: discord.Member - ): ... + ): + recent_bans_by_user = ( + await self.bot.models.BanLog.query.where( + self.bot.models.BanLog.guild_id == str(interaction.guild.id) + ) + .where(self.bot.models.BanLog.banning_moderator == str(moderator.id)) + .order_by(self.bot.models.BanLog.ban_time.desc()) + .limit(10) + .gino.all() + ) + + embeds = [] + for ban in recent_bans_by_user: + embeds.append( + await self.convert_ban_to_pretty_string( + ban, f"Bans by {moderator.name}" + ) + ) + + if len(embeds) == 0: + embed = auxiliary.prepare_deny_embed( + f"No bans by the user {moderator.name} could be found" + ) + await interaction.response.send_message(embed=embed) + return + + await interaction.response.defer(ephemeral=False) + view = ui.PaginateView() + await view.send(interaction.channel, interaction.user, embeds, interaction) + + async def convert_ban_to_pretty_string(self, ban: munch.Munch, title: str): + member = await self.bot.fetch_user(int(ban.banned_member)) + moderator = await self.bot.fetch_user(int(ban.banning_moderator)) + embed = discord.Embed(title=title) + embed.description = ( + f"**Case:** {ban.pk}\n" + f"**Offender:** {member.name} {member.mention}\n" + f"**Reason:** {ban.reason}\n" + f"**Responsible moderator:** {moderator.name} {moderator.mention}" + ) + embed.timestamp = ban.ban_time + embed.color = discord.Color.red() + return embed @commands.Cog.listener() async def on_member_ban( @@ -116,6 +228,7 @@ async def log_ban( async def log_unban( + bot: bot.TechSupportBot, unbanned_member: discord.User | discord.Member, unbanning_moderator: discord.Member, guild: discord.Guild, diff --git a/techsupport_bot/commands/report.py b/techsupport_bot/commands/report.py index 222e00ca0..e29a6d55b 100644 --- a/techsupport_bot/commands/report.py +++ b/techsupport_bot/commands/report.py @@ -1,3 +1,2 @@ class Report: - async def report_command(self, interaction, report_str): - ... \ No newline at end of file + async def report_command(self, interaction, report_str): ... From d55a39b9ac04a3adca4d6d735bc8935a05c74621 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 23 Jul 2024 00:05:49 -0400 Subject: [PATCH 17/99] Formatting --- techsupport_bot/commands/moderator.py | 97 +++++++++++++++---- techsupport_bot/commands/modlog.py | 79 ++++++++++++++-- techsupport_bot/commands/purge.py | 21 +++-- techsupport_bot/commands/report.py | 22 ++++- techsupport_bot/core/databases.py | 13 +++ techsupport_bot/core/moderation.py | 20 +++- techsupport_bot/functions/automod.py | 131 +++++++++++++++++++++++--- techsupport_bot/functions/paste.py | 94 +++++++++++++++--- 8 files changed, 411 insertions(+), 66 deletions(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index a5714ba79..91e101c6b 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Self import dateparser import discord @@ -40,7 +40,7 @@ class ProtectCommands(cogs.BaseCog): extras={"module": "moderator"}, ) async def handle_ban_user( - self, + self: Self, interaction: discord.Interaction, target: discord.User, reason: str, @@ -113,8 +113,16 @@ async def handle_ban_user( extras={"module": "moderator"}, ) async def handle_unban_user( - self, interaction: discord.Interaction, target: discord.User, reason: str - ): + self: Self, interaction: discord.Interaction, target: discord.User, reason: str + ) -> None: + """The logic for the /unban command + + Args: + self (Self): _description_ + interaction (discord.Interaction): The interaction that triggered this command + target (discord.User): The target to be unbanned + reason (str): The reason for the user being unbanned + """ permission_check = await self.permission_check( invoker=interaction.user, target=target, action_name="unban" ) @@ -168,8 +176,19 @@ async def handle_unban_user( extras={"module": "moderator"}, ) async def handle_kick_user( - self, interaction: discord.Interaction, target: discord.Member, reason: str - ): + self: Self, + interaction: discord.Interaction, + target: discord.Member, + reason: str, + ) -> None: + """The core logic for the /kick command + + Args: + self (Self): _description_ + interaction (discord.Interaction): The interaction that triggered the command + target (discord.Member): The target for being kicked + reason (str): The reason for the user being kicked + """ permission_check = await self.permission_check( invoker=interaction.user, target=target, action_name="kick" ) @@ -206,12 +225,23 @@ async def handle_kick_user( name="mute", description="Times out a user", extras={"module": "moderator"} ) async def handle_mute_user( - self, + self: Self, interaction: discord.Interaction, target: discord.Member, reason: str, duration: str = None, - ): + ) -> None: + """The core logic for the /mute command + + Args: + interaction (discord.Interaction): The interaction that triggered this command + target (discord.Member): The target for being muted + reason (str): The reason for being muted + duration (str, optional): The human readable duration to be muted for. Defaults to None. + + Raises: + ValueError: Raised if the duration is invalid or cannot be parsed + """ permission_check = await self.permission_check( invoker=interaction.user, target=target, action_name="mute" ) @@ -286,8 +316,19 @@ async def handle_mute_user( extras={"module": "moderator"}, ) async def handle_unmute_user( - self, interaction: discord.Interaction, target: discord.Member, reason: str - ): + self: Self, + interaction: discord.Interaction, + target: discord.Member, + reason: str, + ) -> None: + """The core logic for the /unmute command + + Args: + self (Self): _description_ + interaction (discord.Interaction): The interaction that triggered this command + target (discord.Member): The target for being unmuted + reason (str): The reason for the unmute + """ permission_check = await self.permission_check( invoker=interaction.user, target=target, action_name="unmute" ) @@ -330,8 +371,19 @@ async def handle_unmute_user( name="warn", description="Warns a user", extras={"module": "moderator"} ) async def handle_warn_user( - self, interaction: discord.Interaction, target: discord.Member, reason: str - ): + self: Self, + interaction: discord.Interaction, + target: discord.Member, + reason: str, + ) -> None: + """The core logic for the /warn command + + Args: + self (Self): _description_ + interaction (discord.Interaction): The interaction that triggered this command + target (discord.Member): The target for being warned + reason (str): The reason the user is being warned + """ permission_check = await self.permission_check( invoker=interaction.user, target=target, action_name="warn" ) @@ -445,12 +497,21 @@ async def handle_warn_user( name="unwarn", description="Unwarns a user", extras={"module": "moderator"} ) async def handle_unwarn_user( - self, + self: Self, interaction: discord.Interaction, target: discord.Member, reason: str, warning: str, - ): + ) -> None: + """The core logic of the /unwarn command + + Args: + self (Self): _description_ + interaction (discord.Interaction): The interaction that triggered the command + target (discord.Member): The user being unwarned + reason (str): The reason for the unwarn + warning (str): The exact string of the warning + """ permission_check = await self.permission_check( invoker=interaction.user, target=target, action_name="unwarn" ) @@ -491,9 +552,9 @@ async def handle_unwarn_user( # Helper functions async def permission_check( - self, + self: Self, invoker: discord.Member, - target: Union[discord.User, discord.Member], + target: discord.User | discord.Member, action_name: str, ) -> str: """Checks permissions to ensure the command should be executed. This checks: @@ -507,7 +568,7 @@ async def permission_check( Args: invoker (discord.Member): The invoker of the action. Either will be the user who ran the command, or the bot itself - target (Union[discord.User, discord.Member]): The target of the command. + target (discord.User | discord.Member): The target of the command. Can be a user or member. action_name (str): The action name to be displayed in messages @@ -549,7 +610,7 @@ async def permission_check( # Database functions async def get_warning( - self, user: discord.Member, warning: str + self: Self, user: discord.Member, warning: str ) -> bot.models.Warning: """Gets a specific warning by string for a user diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index 16da2396a..e5b6e0170 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -1,3 +1,5 @@ +"""Commands and functions to log and interact with logs of bans and unbans""" + from __future__ import annotations import datetime @@ -35,7 +37,12 @@ class BanLogger(cogs.BaseCog): description="Unban someone and allow them to apply", extras={"module": "modlog"}, ) - async def high_score_command(self: Self, interaction: discord.Interaction): + async def high_score_command(self: Self, interaction: discord.Interaction) -> None: + """Gets the top 10 moderators based on banned user count + + Args: + interaction (discord.Interaction): The interaction that started this command + """ all_bans = await self.bot.models.BanLog.query.where( self.bot.models.BanLog.guild_id == str(interaction.guild.id) ).gino.all() @@ -64,7 +71,13 @@ async def high_score_command(self: Self, interaction: discord.Interaction): ) async def lookup_user_command( self: Self, interaction: discord.Interaction, user: discord.User - ): + ) -> None: + """This is the core of the /modlog lookup-user command + + Args: + interaction (discord.Interaction): The interaction that called the command + user (discord.User): The user to search for bans for + """ recent_bans_by_user = ( await self.bot.models.BanLog.query.where( self.bot.models.BanLog.guild_id == str(interaction.guild.id) @@ -100,6 +113,12 @@ async def lookup_user_command( async def lookup_moderator_command( self: Self, interaction: discord.Interaction, moderator: discord.Member ): + """This is the core of the /modlog lookup-moderator command + + Args: + interaction (discord.Interaction): The interaction that called the command + moderator (discord.Member): The moderator to search for bans for + """ recent_bans_by_user = ( await self.bot.models.BanLog.query.where( self.bot.models.BanLog.guild_id == str(interaction.guild.id) @@ -129,7 +148,19 @@ async def lookup_moderator_command( view = ui.PaginateView() await view.send(interaction.channel, interaction.user, embeds, interaction) - async def convert_ban_to_pretty_string(self, ban: munch.Munch, title: str): + async def convert_ban_to_pretty_string( + self: Self, ban: munch.Munch, title: str + ) -> discord.Embed: + """This converts a database ban entry into a shiny embed + + Args: + self (Self): _description_ + ban (munch.Munch): The ban database entry + title (str): The title to set the embeds to + + Returns: + discord.Embed: The fancy embed + """ member = await self.bot.fetch_user(int(ban.banned_member)) moderator = await self.bot.fetch_user(int(ban.banning_moderator)) embed = discord.Embed(title=title) @@ -147,6 +178,13 @@ async def convert_ban_to_pretty_string(self, ban: munch.Munch, title: str): async def on_member_ban( self: Self, guild: discord.Guild, user: discord.User | discord.Member ) -> None: + """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_member_ban + + Args: + guild (discord.Guild): The guild the user got banned from + user (discord.User | discord.Member): The user that got banned. Can be either User + or Member depending if the user was in the guild or not at the time of removal. + """ await discord.utils.sleep_until( discord.utils.utcnow() + datetime.timedelta(seconds=2) ) @@ -156,6 +194,9 @@ async def on_member_ban( if entry.target.id == user.id: moderator = entry.user + if not entry: + return + if moderator.bot: return @@ -165,6 +206,12 @@ async def on_member_ban( async def on_member_unban( self: Self, guild: discord.Guild, user: discord.User ) -> None: + """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_member_unban + + Args: + guild (discord.Guild): The guild the user got unbanned from + user (discord.User): The user that got unbanned + """ # Wait a short time to ensure the audit log has been updated await discord.utils.sleep_until( discord.utils.utcnow() + datetime.timedelta(seconds=2) @@ -176,6 +223,8 @@ async def on_member_unban( ): if entry.target.id == user.id: moderator = entry.user + if not entry: + return if moderator.bot: return @@ -190,7 +239,16 @@ async def log_ban( banning_moderator: discord.Member, guild: discord.Guild, reason: str, -): +) -> None: + """Logs a ban into the alert channel + + Args: + bot (bot.TechSupportBot): The bot object to use for the logging + banned_member (discord.User | discord.Member): The member who was banned + banning_moderator (discord.Member): The moderator who banned the member + guild (discord.Guild): The guild the member was banned from + reason (str): The reason for the ban + """ if not reason: reason = "No reason specified" @@ -233,11 +291,20 @@ async def log_unban( unbanning_moderator: discord.Member, guild: discord.Guild, reason: str, -): +) -> None: + """Logs an unban into the alert channel + + Args: + bot (bot.TechSupportBot): The bot object to use for the logging + unbanned_member (discord.User | discord.Member): The member who was unbanned + unbanning_moderator (discord.Member): The moderator who unbanned the member + guild (discord.Guild): The guild the member was unbanned from + reason (str): The reason for the unban + """ if not reason: reason = "No reason specified" - embed = discord.Embed(title=f"unban") + embed = discord.Embed(title="unban") embed.description = ( f"**Offender:** {unbanned_member.name} {unbanned_member.mention}\n" f"**Reason:** {reason}\n" diff --git a/techsupport_bot/commands/purge.py b/techsupport_bot/commands/purge.py index 03ee87927..772b7d571 100644 --- a/techsupport_bot/commands/purge.py +++ b/techsupport_bot/commands/purge.py @@ -1,7 +1,9 @@ +"""The file that holds the purge command""" + from __future__ import annotations import datetime -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Self import discord from core import auxiliary, cogs, moderation @@ -22,11 +24,6 @@ async def setup(bot: bot.TechSupportBot) -> None: class Purger(cogs.BaseCog): - ALERT_ICON_URL = ( - "https://cdn.icon-icons.com/icons2/2063/PNG/512/" - + "alert_danger_warning_notification_icon_124692.png" - ) - @app_commands.checks.has_permissions(manage_messages=True) @app_commands.checks.bot_has_permissions(manage_messages=True) @app_commands.command( @@ -35,12 +32,18 @@ class Purger(cogs.BaseCog): extras={"module": "purge"}, ) async def purge_command( - self, + self: Self, interaction: discord.Interaction, amount: int, duration_minutes: int = None, - ): - """Method to purge a channel's message up to a time.""" + ) -> None: + """The core purge command that can purge by either amount or duration + + Args: + interaction (discord.Interaction): The interaction that called this command + amount (int): The max amount of messages to purge + duration_minutes (int, optional): The max age of a message to purge. Defaults to None. + """ config = self.bot.guild_configs[str(interaction.guild.id)] if amount <= 0 or amount > config.extensions.protect.max_purge_amount.value: diff --git a/techsupport_bot/commands/report.py b/techsupport_bot/commands/report.py index e29a6d55b..1c45f27b5 100644 --- a/techsupport_bot/commands/report.py +++ b/techsupport_bot/commands/report.py @@ -1,2 +1,22 @@ +"""The report command""" + +from typing import Self + +import discord + + class Report: - async def report_command(self, interaction, report_str): ... + """The class that holds the report command and helper function""" + + async def report_command( + self: Self, interaction: discord.Interaction, report_str: str + ) -> None: + """This is the core of the /report command + Allows users to report potential moderation issues to staff + + Args: + self (Self): _description_ + interaction (discord.Interaction): The interaction that called this command + report_str (str): The report string that the user submitted + """ + ... diff --git a/techsupport_bot/core/databases.py b/techsupport_bot/core/databases.py index 96de81883..9718b7a89 100644 --- a/techsupport_bot/core/databases.py +++ b/techsupport_bot/core/databases.py @@ -66,6 +66,18 @@ class ApplicationBans(bot.db.Model): applicant_id = bot.db.Column(bot.db.String) class BanLog(bot.db.Model): + """The postgres table for banlogs + Currently used in modlog.py + + Attrs: + __tablename__ (str): The name of the table in postgres + pk (int): The automatic primary key + guild_id (str): The string of the guild ID the user was banned in + reason (str): The reason of the ban + banning_moderator (str): The ID of the moderator who banned + banned_member (str): The ID of the user who was banned + ban_time (datetime): The date and time of the ban + """ __tablename__ = "banlog" @@ -244,6 +256,7 @@ class Warning(bot.db.Model): guild_id (str): The guild this warn occured in reason (str): The reason for the warn time (datetime): The time the warning was given + invoker_id (str): The moderator who made the warning """ __tablename__ = "warnings" diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index 86ab1dd7f..459f51e3e 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -2,10 +2,14 @@ Do the proper moderative action and return true if successful, false if not.""" from datetime import timedelta +from typing import TYPE_CHECKING import discord import munch +if TYPE_CHECKING: + import bot + async def ban_user( guild: discord.Guild, user: discord.User, delete_days: int, reason: str @@ -96,11 +100,12 @@ async def unmute_user(user: discord.Member, reason: str) -> bool: async def warn_user( - bot, user: discord.Member, invoker: discord.Member, reason: str + bot: bot.TechSupportBot, user: discord.Member, invoker: discord.Member, reason: str ) -> bool: """Warns a user. Does NOT check config or how many warnings a user has Args: + bot (bot.TechSupportBot): The bot object to use user (discord.Member): The user to warn invoker (discord.Member): The person who warned the user reason (str): The reason for the warning @@ -117,10 +122,13 @@ async def warn_user( return True -async def unwarn_user(bot, user: discord.Member, warning: str) -> bool: +async def unwarn_user( + bot: bot.TechSupportBot, user: discord.Member, warning: str +) -> bool: """Removes a specific warning from a user by string Args: + bot (bot.TechSupportBot): The bot object to use user (discord.Member): The member to remove a warning from warning (str): The warning to remove @@ -142,16 +150,17 @@ async def unwarn_user(bot, user: discord.Member, warning: str) -> bool: async def get_all_warnings( - bot, user: discord.User, guild: discord.Guild + bot: bot.TechSupportBot, user: discord.User, guild: discord.Guild ) -> list[munch.Munch]: """Gets a list of all warnings for a specific user in a specific guild Args: + bot (bot.TechSupportBot): The bot object to use user (discord.User): The user that we want warns from guild (discord.Guild): The guild that we want warns from Returns: - list[bot.models.Warning]: The list of all warnings for the user/guild, if any exist + list[munch.Munch]: The list of all warnings for the user/guild, if any exist """ warnings = ( await bot.models.Warning.query.where(bot.models.Warning.user_id == str(user.id)) @@ -162,7 +171,7 @@ async def get_all_warnings( async def send_command_usage_alert( - bot, + bot: bot.TechSupportBot, interaction: discord.Interaction, command: str, guild: discord.Guild, @@ -171,6 +180,7 @@ async def send_command_usage_alert( """Sends a usage alert to the protect events channel, if configured Args: + bot (bot.TechSupportBot): The bot object to use interaction (discord.Interaction): The interaction that trigger the command command (str): The string representation of the command that was run guild (discord.Guild): The guild the command was run in diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 2cdaefa87..eaad5e5f7 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -1,9 +1,11 @@ +"""Handles the automod checks""" + from __future__ import annotations import re from dataclasses import dataclass from datetime import timedelta -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Self import discord import munch @@ -56,8 +58,18 @@ def score(self) -> int: class AutoMod(cogs.MatchCog): async def match( - self, config: munch.Munch, ctx: commands.Context, content: str + self: Self, config: munch.Munch, ctx: commands.Context, content: str ) -> bool: + """Checks to see if a message should be considered for automod violations + + Args: + config (munch.Munch): The config of the guild to check + ctx (commands.Context): The context of the original message + content (str): The string representation of the message + + Returns: + bool: Whether the message should be inspected for automod violations + """ if not str(ctx.channel.id) in config.extensions.protect.channels.value: await self.bot.logger.send_log( message="Channel not in protected channels - ignoring protect check", @@ -82,6 +94,14 @@ async def match( async def response( self, config: munch.Munch, ctx: commands.Context, content: str, result: bool ) -> None: + """Handles a discord automod violation + + Args: + config (munch.Munch): The config of the guild where the message was sent + ctx (commands.Context): The context the message was sent in + content (str): The string content of the message + result (bool): What the match() function returned + """ should_delete = False should_warn = False should_mute = False @@ -193,8 +213,13 @@ async def response( await alert_channel.send(embed=alert_channel_embed) @commands.Cog.listener() - async def on_raw_message_edit(self, payload: discord.RawMessageUpdateEvent): - """Method to edit the raw message.""" + async def on_raw_message_edit(self: Self, payload: discord.RawMessageUpdateEvent): + """This is called when any message is edited in any guild the bot is in. + There is no guarantee that the message exists or is used + + Args: + payload (discord.RawMessageUpdateEvent): The raw event that the edit generated + """ guild = self.bot.get_guild(payload.guild_id) if not guild: return @@ -226,6 +251,16 @@ async def on_raw_message_edit(self, payload: discord.RawMessageUpdateEvent): def generate_automod_alert_embed( ctx: commands.Context, violations: list[AutoModPunishment], action_taken: str ): + """Generates an alert embed for the automod rules that are broken + + Args: + ctx (commands.Context): The context of the message that violated the automod + violations (list[AutoModPunishment]): The list of all violations of the automod + action_taken (str): The text based action taken against the user + + Returns: + discord.Embed: The formatted embed ready to be sent to discord + """ ALERT_ICON_URL = ( "https://cdn.icon-icons.com/icons2/2063/PNG/512/" @@ -248,10 +283,27 @@ def generate_automod_alert_embed( return embed -def run_all_checks(config, message: discord.Message) -> list[AutoModPunishment]: - # Automod will only ever be a framework to say something needs to be done - # Outside of running from the response function, NO ACTION will be taken - # All checks will return a list of AutoModPunishment, which may be nothing +# Automod will only ever be a framework to say something needs to be done +# Outside of running from the response function, NO ACTION will be taken +# All checks will return a list of AutoModPunishment, which may be nothing + + +def run_all_checks( + config: munch.Munch, message: discord.Message +) -> list[AutoModPunishment]: + """This runs all 4 checks on a given discord.Message + handle_file_extensions + handle_mentions + handle_exact_string + handle_regex_string + + Args: + config (munch.Munch): The guild config to check with + message (discord.Message): The message object to use to search + + Returns: + list[AutoModPunishment]: The automod violations that the given message violated + """ all_violations = ( run_only_string_checks(config, message.clean_content) + handle_file_extensions(config, message.attachments) @@ -260,7 +312,20 @@ def run_all_checks(config, message: discord.Message) -> list[AutoModPunishment]: return all_violations -def run_only_string_checks(config, content: str) -> list[AutoModPunishment]: +def run_only_string_checks( + config: munch.Munch, content: str +) -> list[AutoModPunishment]: + """This runs the plaintext string texts and returns the combined list of violations + handle_exact_string + handle_regex_string + + Args: + config (munch.Munch): The guild config to check with + content (str): The content of the message to search + + Returns: + list[AutoModPunishment]: The automod violations that the given message violated + """ all_violations = handle_exact_string(config, content) + handle_regex_string( config, content ) @@ -268,8 +333,17 @@ def run_only_string_checks(config, content: str) -> list[AutoModPunishment]: def handle_file_extensions( - config, attachments: list[discord.Attachment] + config: munch.Munch, attachments: list[discord.Attachment] ) -> list[AutoModPunishment]: + """This checks a list of attachments for attachments that violate the automod rules + + Args: + config (munch.Munch): The guild config to check with + attachments (list[discord.Attachment]): The list of attachments to search + + Returns: + list[AutoModPunishment]: The automod violations that the given message violated + """ violations = [] for attachment in attachments: if ( @@ -287,7 +361,18 @@ def handle_file_extensions( return violations -def handle_mentions(config, message: discord.Message) -> list[AutoModPunishment]: +def handle_mentions( + config: munch.Munch, message: discord.Message +) -> list[AutoModPunishment]: + """This checks a given discord message to make sure it doesn't violate the mentions maximum + + Args: + config (munch.Munch): The guild config to check with + message (discord.Message): The message to check for mentions with + + Returns: + list[AutoModPunishment]: The automod violations that the given message violated + """ if len(message.mentions) > config.extensions.protect.max_mentions.value: return [ AutoModPunishment( @@ -300,7 +385,17 @@ def handle_mentions(config, message: discord.Message) -> list[AutoModPunishment] return [] -def handle_exact_string(config, content: str) -> list[AutoModPunishment]: +def handle_exact_string(config: munch.Munch, content: str) -> list[AutoModPunishment]: + """This checks the configued automod exact string blocks + If the content matches the string, it's added to a list + + Args: + config (munch.Munch): The guild config to check with + content (str): The content of the message to search + + Returns: + list[AutoModPunishment]: The automod violations that the given message violated + """ violations = [] for ( keyword, @@ -318,7 +413,17 @@ def handle_exact_string(config, content: str) -> list[AutoModPunishment]: return violations -def handle_regex_string(config, content: str) -> list[AutoModPunishment]: +def handle_regex_string(config: munch.Munch, content: str) -> list[AutoModPunishment]: + """This checks the configued automod regex blocks + If the content matches the regex, it's added to a list + + Args: + config (munch.Munch): The guild config to check with + content (str): The content of the message to search + + Returns: + list[AutoModPunishment]: The automod violations that the given message violated + """ violations = [] for ( keyword, diff --git a/techsupport_bot/functions/paste.py b/techsupport_bot/functions/paste.py index 45acf7329..bd7471a7c 100644 --- a/techsupport_bot/functions/paste.py +++ b/techsupport_bot/functions/paste.py @@ -1,9 +1,10 @@ from __future__ import annotations import io -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Self import discord +import munch from botlogging import LogContext, LogLevel from core import cogs from discord.ext import commands @@ -22,8 +23,19 @@ async def setup(bot: bot.TechSupportBot) -> None: class Paster(cogs.MatchCog): - async def match(self, config, ctx, content): - """Method to match roles for the protect command.""" + async def match( + self: Self, config: munch.Munch, ctx: commands.Context, content: str + ) -> bool: + """Checks to see if a message should be considered for a paste + + Args: + config (munch.Munch): The config of the guild to check + ctx (commands.Context): The context of the original message + content (str): The string representation of the message + + Returns: + bool: Whether the message should be inspected for a paste + """ # exit the match based on exclusion parameters if not str(ctx.channel.id) in config.extensions.protect.channels.value: await self.bot.logger.send_log( @@ -46,20 +58,41 @@ async def match(self, config, ctx, content): return True - async def response(self, config, ctx, content, _): - # check length of content + async def response( + self, config: munch.Munch, ctx: commands.Context, content: str, result: bool + ) -> None: + """Handles a paste check + + Args: + config (munch.Munch): The config of the guild where the message was sent + ctx (commands.Context): The context the message was sent in + content (str): The string content of the message + result (bool): What the match() function returned + """ if len(content) > config.extensions.protect.length_limit.value or content.count( "\n" ) > self.max_newlines(config.extensions.protect.length_limit.value): await self.handle_length_alert(config, ctx, content) - def max_newlines(self, max_length): - """Method to set up the number of max lines.""" + def max_newlines(self: Self, max_length: int) -> int: + """Gets a theoretical maximum number of new lines in a given message + + Args: + max_length (int): The max length of characters per theoretical line + + Returns: + int: The maximum number of new lines based on config + """ return int(max_length / 80) + 1 @commands.Cog.listener() async def on_raw_message_edit(self, payload: discord.RawMessageUpdateEvent): - """Method to edit the raw message.""" + """This is called when any message is edited in any guild the bot is in. + There is no guarantee that the message exists or is used + + Args: + payload (discord.RawMessageUpdateEvent): The raw event that the edit generated + """ guild = self.bot.get_guild(payload.guild_id) if not guild: return @@ -87,8 +120,16 @@ async def on_raw_message_edit(self, payload: discord.RawMessageUpdateEvent): await self.response(config, ctx, message.content, None) - async def handle_length_alert(self, config, ctx, content) -> None: - """Method to handle alert for the protect extension.""" + async def handle_length_alert( + self: Self, config: munch.Munch, ctx: commands.Context, content: str + ) -> None: + """Moves message into a linx paste if it's too long + + Args: + config (munch.Munch): The guild config where the too long message was sent + ctx (commands.Context): The context where the original message was sent + content (str): The string content of the flagged message + """ attachments: list[discord.File] = [] if ctx.message.attachments: total_attachment_size = 0 @@ -125,8 +166,21 @@ async def handle_length_alert(self, config, ctx, content) -> None: ctx.message.author.mention, embed=linx_embed, files=attachments[:10] ) - async def send_default_delete_response(self, config, ctx, content, reason): - """Method for the default delete of a message.""" + async def send_default_delete_response( + self: Self, + config: munch.Munch, + ctx: commands.Context, + content: str, + reason: str, + ) -> None: + """Sends a DM to a user containing a message that was deleted + + Args: + config (munch.Munch): The config of the guild where the message was sent + ctx (commands.Context): The context of the deleted message + content (str): The context of the deleted message + reason (str): The reason the message was deleted + """ embed = discord.Embed( title="Chat Protection", description=f"Message deleted. Reason: *{reason}*" ) @@ -134,8 +188,20 @@ async def send_default_delete_response(self, config, ctx, content, reason): await ctx.send(ctx.message.author.mention, embed=embed) await ctx.author.send(f"Deleted message: ```{content[:1994]}```") - async def create_linx_embed(self, config, ctx, content): - """Method to create a link for long messages.""" + async def create_linx_embed( + self: Self, config: munch.Munch, ctx: commands.Context, content: str + ) -> discord.Embed | None: + """This function sends a message to the linx url and puts the result in + an embed to be sent to the user + + Args: + config (munch.Munch): The guild config where the message was sent + ctx (commands.Context): The context that generated the need for a paste + content (str): The context of the message to be pasted + + Returns: + discord.Embed | None: The formatted embed, or None if there was an API error + """ if not content: return None From 2ee4f6ad0e29fe9a92aa09bd4bdf6d2c0ac4c227 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 23 Jul 2024 00:14:25 -0400 Subject: [PATCH 18/99] Formatting 2 --- techsupport_bot/commands/moderator.py | 16 ++++++------ techsupport_bot/commands/modlog.py | 5 ++-- techsupport_bot/commands/protect.py | 3 --- techsupport_bot/commands/purge.py | 5 ++-- techsupport_bot/commands/report.py | 1 - techsupport_bot/core/moderation.py | 37 +++++++++++++++------------ techsupport_bot/functions/automod.py | 10 ++++++-- techsupport_bot/functions/paste.py | 4 +++ 8 files changed, 47 insertions(+), 34 deletions(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 91e101c6b..662c0207b 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -93,7 +93,7 @@ async def handle_ban_user( ) await moderation.send_command_usage_alert( - bot=self.bot, + bot_object=self.bot, interaction=interaction, command=( f"/ban target: {target.display_name}, reason: {reason}, delete_days:" @@ -159,7 +159,7 @@ async def handle_unban_user( ) await moderation.send_command_usage_alert( - bot=self.bot, + bot_object=self.bot, interaction=interaction, command=f"/unban target: {target.display_name}, reason: {reason}", guild=interaction.guild, @@ -210,7 +210,7 @@ async def handle_kick_user( return await moderation.send_command_usage_alert( - bot=self.bot, + bot_object=self.bot, interaction=interaction, command=f"/kick target: {target.display_name}, reason: {reason}", guild=interaction.guild, @@ -296,7 +296,7 @@ async def handle_mute_user( return await moderation.send_command_usage_alert( - bot=self.bot, + bot_object=self.bot, interaction=interaction, command=( f"/mute target: {target.display_name}, reason: {reason}, duration:" @@ -356,7 +356,7 @@ async def handle_unmute_user( return await moderation.send_command_usage_alert( - bot=self.bot, + bot_object=self.bot, interaction=interaction, command=f"/unmute target: {target.display_name}, reason: {reason}", guild=interaction.guild, @@ -423,7 +423,7 @@ async def handle_warn_user( should_ban = True warn_result = await moderation.warn_user( - bot=self.bot, user=target, invoker=interaction.user, reason=reason + bot_object=self.bot, user=target, invoker=interaction.user, reason=reason ) if should_ban: @@ -461,7 +461,7 @@ async def handle_warn_user( return await moderation.send_command_usage_alert( - bot=self.bot, + bot_object=self.bot, interaction=interaction, command=f"/warn target: {target.display_name}, reason: {reason}", guild=interaction.guild, @@ -540,7 +540,7 @@ async def handle_unwarn_user( return await moderation.send_command_usage_alert( - bot=self.bot, + bot_object=self.bot, interaction=interaction, command=f"/unwarn target: {target.display_name}, reason: {reason}, warning: {warning}", guild=interaction.guild, diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index e5b6e0170..d5f60c5b9 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -27,6 +27,7 @@ async def setup(bot: bot.TechSupportBot) -> None: class BanLogger(cogs.BaseCog): + """The class that holds the /modlog commands""" modlog_group = app_commands.Group( name="modlog", description="...", extras={"module": "modlog"} @@ -189,7 +190,7 @@ async def on_member_ban( discord.utils.utcnow() + datetime.timedelta(seconds=2) ) - # Fetch the audit logs for the ban action + entry = None async for entry in guild.audit_logs(limit=1, action=discord.AuditLogAction.ban): if entry.target.id == user.id: moderator = entry.user @@ -217,7 +218,7 @@ async def on_member_unban( discord.utils.utcnow() + datetime.timedelta(seconds=2) ) - # Fetch the audit logs for the unban action + entry = None async for entry in guild.audit_logs( limit=1, action=discord.AuditLogAction.unban ): diff --git a/techsupport_bot/commands/protect.py b/techsupport_bot/commands/protect.py index 0448e3598..09282af35 100644 --- a/techsupport_bot/commands/protect.py +++ b/techsupport_bot/commands/protect.py @@ -17,11 +17,8 @@ Move all config over to specific new files """ -import re - import discord import expiringdict -import munch import ui from botlogging import LogContext, LogLevel from core import auxiliary, cogs, extensionconfig diff --git a/techsupport_bot/commands/purge.py b/techsupport_bot/commands/purge.py index 772b7d571..87007c931 100644 --- a/techsupport_bot/commands/purge.py +++ b/techsupport_bot/commands/purge.py @@ -8,7 +8,6 @@ import discord from core import auxiliary, cogs, moderation from discord import app_commands -from discord.ext import commands if TYPE_CHECKING: import bot @@ -24,6 +23,8 @@ async def setup(bot: bot.TechSupportBot) -> None: class Purger(cogs.BaseCog): + """The class that holds the /purge command""" + @app_commands.checks.has_permissions(manage_messages=True) @app_commands.checks.bot_has_permissions(manage_messages=True) @app_commands.command( @@ -72,7 +73,7 @@ async def purge_command( await interaction.channel.purge(after=timestamp, limit=amount) await moderation.send_command_usage_alert( - bot=self.bot, + bot_object=self.bot, interaction=interaction, command=f"/purge amount: {amount}, duration: {duration_minutes}", guild=interaction.guild, diff --git a/techsupport_bot/commands/report.py b/techsupport_bot/commands/report.py index 1c45f27b5..b354489ea 100644 --- a/techsupport_bot/commands/report.py +++ b/techsupport_bot/commands/report.py @@ -19,4 +19,3 @@ async def report_command( interaction (discord.Interaction): The interaction that called this command report_str (str): The report string that the user submitted """ - ... diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index 459f51e3e..e5d3adba8 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -100,12 +100,15 @@ async def unmute_user(user: discord.Member, reason: str) -> bool: async def warn_user( - bot: bot.TechSupportBot, user: discord.Member, invoker: discord.Member, reason: str + bot_object: bot.TechSupportBot, + user: discord.Member, + invoker: discord.Member, + reason: str, ) -> bool: """Warns a user. Does NOT check config or how many warnings a user has Args: - bot (bot.TechSupportBot): The bot object to use + bot_object (bot.TechSupportBot): The bot object to use user (discord.Member): The user to warn invoker (discord.Member): The person who warned the user reason (str): The reason for the warning @@ -113,7 +116,7 @@ async def warn_user( Returns: bool: True if warning was successful """ - await bot.models.Warning( + await bot_object.models.Warning( user_id=str(user.id), guild_id=str(invoker.guild.id), reason=reason, @@ -123,12 +126,12 @@ async def warn_user( async def unwarn_user( - bot: bot.TechSupportBot, user: discord.Member, warning: str + bot_object: bot.TechSupportBot, user: discord.Member, warning: str ) -> bool: """Removes a specific warning from a user by string Args: - bot (bot.TechSupportBot): The bot object to use + bot_object (bot.TechSupportBot): The bot object to use user (discord.Member): The member to remove a warning from warning (str): The warning to remove @@ -136,11 +139,11 @@ async def unwarn_user( bool: True if unwarning was successful """ query = ( - bot.models.Warning.query.where( - bot.models.Warning.guild_id == str(user.guild.id) + bot_object.models.Warning.query.where( + bot_object.models.Warning.guild_id == str(user.guild.id) ) - .where(bot.models.Warning.reason == warning) - .where(bot.models.Warning.user_id == str(user.id)) + .where(bot_object.models.Warning.reason == warning) + .where(bot_object.models.Warning.user_id == str(user.id)) ) entry = await query.gino.first() if not entry: @@ -150,12 +153,12 @@ async def unwarn_user( async def get_all_warnings( - bot: bot.TechSupportBot, user: discord.User, guild: discord.Guild + bot_object: bot.TechSupportBot, user: discord.User, guild: discord.Guild ) -> list[munch.Munch]: """Gets a list of all warnings for a specific user in a specific guild Args: - bot (bot.TechSupportBot): The bot object to use + bot_object (bot.TechSupportBot): The bot object to use user (discord.User): The user that we want warns from guild (discord.Guild): The guild that we want warns from @@ -163,15 +166,17 @@ async def get_all_warnings( list[munch.Munch]: The list of all warnings for the user/guild, if any exist """ warnings = ( - await bot.models.Warning.query.where(bot.models.Warning.user_id == str(user.id)) - .where(bot.models.Warning.guild_id == str(guild.id)) + await bot_object.models.Warning.query.where( + bot_object.models.Warning.user_id == str(user.id) + ) + .where(bot_object.models.Warning.guild_id == str(guild.id)) .gino.all() ) return warnings async def send_command_usage_alert( - bot: bot.TechSupportBot, + bot_object: bot.TechSupportBot, interaction: discord.Interaction, command: str, guild: discord.Guild, @@ -180,7 +185,7 @@ async def send_command_usage_alert( """Sends a usage alert to the protect events channel, if configured Args: - bot (bot.TechSupportBot): The bot object to use + bot_object (bot.TechSupportBot): The bot object to use interaction (discord.Interaction): The interaction that trigger the command command (str): The string representation of the command that was run guild (discord.Guild): The guild the command was run in @@ -192,7 +197,7 @@ async def send_command_usage_alert( + "alert_danger_warning_notification_icon_124692.png" ) - config = bot.guild_configs[str(guild.id)] + config = bot_object.guild_configs[str(guild.id)] try: alert_channel = guild.get_channel( diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index eaad5e5f7..bb05d84a9 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -45,7 +45,13 @@ class AutoModPunishment: recommend_mute: bool @property - def score(self) -> int: + def score(self: Self) -> int: + """A score so that the AutoModPunishment object is sortable + This sorts based on actions recommended to be taken + + Returns: + int: The score + """ score = 0 if self.recommend_mute: score += 4 @@ -426,7 +432,7 @@ def handle_regex_string(config: munch.Munch, content: str) -> list[AutoModPunish """ violations = [] for ( - keyword, + _, filter_config, ) in config.extensions.protect.string_map.value.items(): regex = filter_config.get("regex") diff --git a/techsupport_bot/functions/paste.py b/techsupport_bot/functions/paste.py index bd7471a7c..7dfc763b3 100644 --- a/techsupport_bot/functions/paste.py +++ b/techsupport_bot/functions/paste.py @@ -1,3 +1,5 @@ +"""The file that holds the paste function""" + from __future__ import annotations import io @@ -23,6 +25,8 @@ async def setup(bot: bot.TechSupportBot) -> None: class Paster(cogs.MatchCog): + """The pasting module""" + async def match( self: Self, config: munch.Munch, ctx: commands.Context, content: str ) -> bool: From be050467ff0951bfaf69e69fac31f3fdd75df333 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 23 Jul 2024 00:24:21 -0400 Subject: [PATCH 19/99] FOrmatting --- techsupport_bot/commands/moderator.py | 27 ++++++++++++++------------- techsupport_bot/commands/protect.py | 18 +++++++++--------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 662c0207b..cb8faed1c 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Self +from typing import Self import dateparser import discord @@ -13,15 +13,16 @@ from core import auxiliary, cogs, moderation from discord import app_commands -if TYPE_CHECKING: - import bot +class FakeTechSupportBot: + """This exists because of circular imports that I fucking hate""" -async def setup(bot: bot.TechSupportBot) -> None: + +async def setup(bot: FakeTechSupportBot) -> None: """Adds the cog to the bot. Setups config Args: - bot (bot.TechSupportBot): The bot object to register the cog with + bot (TechSupportBot): The bot object to register the cog with """ await bot.add_cog(ProtectCommands(bot=bot)) @@ -93,7 +94,7 @@ async def handle_ban_user( ) await moderation.send_command_usage_alert( - bot_object=self.bot, + bot=self.bot, interaction=interaction, command=( f"/ban target: {target.display_name}, reason: {reason}, delete_days:" @@ -159,7 +160,7 @@ async def handle_unban_user( ) await moderation.send_command_usage_alert( - bot_object=self.bot, + bot=self.bot, interaction=interaction, command=f"/unban target: {target.display_name}, reason: {reason}", guild=interaction.guild, @@ -210,7 +211,7 @@ async def handle_kick_user( return await moderation.send_command_usage_alert( - bot_object=self.bot, + bot=self.bot, interaction=interaction, command=f"/kick target: {target.display_name}, reason: {reason}", guild=interaction.guild, @@ -296,7 +297,7 @@ async def handle_mute_user( return await moderation.send_command_usage_alert( - bot_object=self.bot, + bot=self.bot, interaction=interaction, command=( f"/mute target: {target.display_name}, reason: {reason}, duration:" @@ -356,7 +357,7 @@ async def handle_unmute_user( return await moderation.send_command_usage_alert( - bot_object=self.bot, + bot=self.bot, interaction=interaction, command=f"/unmute target: {target.display_name}, reason: {reason}", guild=interaction.guild, @@ -461,7 +462,7 @@ async def handle_warn_user( return await moderation.send_command_usage_alert( - bot_object=self.bot, + bot=self.bot, interaction=interaction, command=f"/warn target: {target.display_name}, reason: {reason}", guild=interaction.guild, @@ -540,7 +541,7 @@ async def handle_unwarn_user( return await moderation.send_command_usage_alert( - bot_object=self.bot, + bot=self.bot, interaction=interaction, command=f"/unwarn target: {target.display_name}, reason: {reason}, warning: {warning}", guild=interaction.guild, @@ -611,7 +612,7 @@ async def permission_check( async def get_warning( self: Self, user: discord.Member, warning: str - ) -> bot.models.Warning: + ) -> FakeTechSupportBot.models.Warning: """Gets a specific warning by string for a user Args: diff --git a/techsupport_bot/commands/protect.py b/techsupport_bot/commands/protect.py index 09282af35..58310568b 100644 --- a/techsupport_bot/commands/protect.py +++ b/techsupport_bot/commands/protect.py @@ -1,18 +1,18 @@ """ Todo: - Purge to slash commands - Unwarn has autofill - Get all warnings command - - Make all of automod - Simplify paste + Purge to slash commands + Unwarn has autofill + Get all warnings command + + Make all of automod + Simplify paste Make paste not work if message would be DELETED by automod - Create a ban logging system like carl - Needs a database for ban history + Create a ban logging system like carl - Needs a database for ban history Central unban logging but NO database needed Ban logs need to be more centralized: - Auto bans, command bans, and manual bans all need to be logged with a single message - A modlog highscores command, in a modlog.py command + Auto bans, command bans, and manual bans all need to be logged with a single message + A modlog highscores command, in a modlog.py command Move all config over to specific new files """ From 02d993a7837c7b7d051b723a1ac1b0ecedc76e88 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 23 Jul 2024 00:28:43 -0400 Subject: [PATCH 20/99] Formatting --- techsupport_bot/commands/moderator.py | 15 +++++++-------- techsupport_bot/core/moderation.py | 22 ++++++++-------------- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index cb8faed1c..91e101c6b 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime, timedelta -from typing import Self +from typing import TYPE_CHECKING, Self import dateparser import discord @@ -13,16 +13,15 @@ from core import auxiliary, cogs, moderation from discord import app_commands +if TYPE_CHECKING: + import bot -class FakeTechSupportBot: - """This exists because of circular imports that I fucking hate""" - -async def setup(bot: FakeTechSupportBot) -> None: +async def setup(bot: bot.TechSupportBot) -> None: """Adds the cog to the bot. Setups config Args: - bot (TechSupportBot): The bot object to register the cog with + bot (bot.TechSupportBot): The bot object to register the cog with """ await bot.add_cog(ProtectCommands(bot=bot)) @@ -424,7 +423,7 @@ async def handle_warn_user( should_ban = True warn_result = await moderation.warn_user( - bot_object=self.bot, user=target, invoker=interaction.user, reason=reason + bot=self.bot, user=target, invoker=interaction.user, reason=reason ) if should_ban: @@ -612,7 +611,7 @@ async def permission_check( async def get_warning( self: Self, user: discord.Member, warning: str - ) -> FakeTechSupportBot.models.Warning: + ) -> bot.models.Warning: """Gets a specific warning by string for a user Args: diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index e5d3adba8..46ca27dde 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -2,14 +2,10 @@ Do the proper moderative action and return true if successful, false if not.""" from datetime import timedelta -from typing import TYPE_CHECKING import discord import munch -if TYPE_CHECKING: - import bot - async def ban_user( guild: discord.Guild, user: discord.User, delete_days: int, reason: str @@ -100,7 +96,7 @@ async def unmute_user(user: discord.Member, reason: str) -> bool: async def warn_user( - bot_object: bot.TechSupportBot, + bot_object: object, user: discord.Member, invoker: discord.Member, reason: str, @@ -108,7 +104,7 @@ async def warn_user( """Warns a user. Does NOT check config or how many warnings a user has Args: - bot_object (bot.TechSupportBot): The bot object to use + bot_object (object): The bot object to use user (discord.Member): The user to warn invoker (discord.Member): The person who warned the user reason (str): The reason for the warning @@ -125,13 +121,11 @@ async def warn_user( return True -async def unwarn_user( - bot_object: bot.TechSupportBot, user: discord.Member, warning: str -) -> bool: +async def unwarn_user(bot_object: object, user: discord.Member, warning: str) -> bool: """Removes a specific warning from a user by string Args: - bot_object (bot.TechSupportBot): The bot object to use + bot_object (object): The bot object to use user (discord.Member): The member to remove a warning from warning (str): The warning to remove @@ -153,12 +147,12 @@ async def unwarn_user( async def get_all_warnings( - bot_object: bot.TechSupportBot, user: discord.User, guild: discord.Guild + bot_object: object, user: discord.User, guild: discord.Guild ) -> list[munch.Munch]: """Gets a list of all warnings for a specific user in a specific guild Args: - bot_object (bot.TechSupportBot): The bot object to use + bot_object (object): The bot object to use user (discord.User): The user that we want warns from guild (discord.Guild): The guild that we want warns from @@ -176,7 +170,7 @@ async def get_all_warnings( async def send_command_usage_alert( - bot_object: bot.TechSupportBot, + bot_object: object, interaction: discord.Interaction, command: str, guild: discord.Guild, @@ -185,7 +179,7 @@ async def send_command_usage_alert( """Sends a usage alert to the protect events channel, if configured Args: - bot_object (bot.TechSupportBot): The bot object to use + bot_object (object): The bot object to use interaction (discord.Interaction): The interaction that trigger the command command (str): The string representation of the command that was run guild (discord.Guild): The guild the command was run in From 8be5e934fd2bcd8ed7e2336b3d524f570c5ed329 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 23 Jul 2024 00:34:59 -0400 Subject: [PATCH 21/99] Formatting --- techsupport_bot/commands/moderator.py | 5 ----- techsupport_bot/commands/modlog.py | 9 ++++++--- techsupport_bot/commands/report.py | 1 - techsupport_bot/functions/automod.py | 15 ++++++++++++--- techsupport_bot/functions/paste.py | 17 +++++++++++++---- 5 files changed, 31 insertions(+), 16 deletions(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 91e101c6b..cd275e716 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -118,7 +118,6 @@ async def handle_unban_user( """The logic for the /unban command Args: - self (Self): _description_ interaction (discord.Interaction): The interaction that triggered this command target (discord.User): The target to be unbanned reason (str): The reason for the user being unbanned @@ -184,7 +183,6 @@ async def handle_kick_user( """The core logic for the /kick command Args: - self (Self): _description_ interaction (discord.Interaction): The interaction that triggered the command target (discord.Member): The target for being kicked reason (str): The reason for the user being kicked @@ -324,7 +322,6 @@ async def handle_unmute_user( """The core logic for the /unmute command Args: - self (Self): _description_ interaction (discord.Interaction): The interaction that triggered this command target (discord.Member): The target for being unmuted reason (str): The reason for the unmute @@ -379,7 +376,6 @@ async def handle_warn_user( """The core logic for the /warn command Args: - self (Self): _description_ interaction (discord.Interaction): The interaction that triggered this command target (discord.Member): The target for being warned reason (str): The reason the user is being warned @@ -506,7 +502,6 @@ async def handle_unwarn_user( """The core logic of the /unwarn command Args: - self (Self): _description_ interaction (discord.Interaction): The interaction that triggered the command target (discord.Member): The user being unwarned reason (str): The reason for the unwarn diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index d5f60c5b9..d6ae90e02 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -27,7 +27,11 @@ async def setup(bot: bot.TechSupportBot) -> None: class BanLogger(cogs.BaseCog): - """The class that holds the /modlog commands""" + """The class that holds the /modlog commands + + Attrs: + modlog_group (app_commands.Group): The group for the /modlog commands + """ modlog_group = app_commands.Group( name="modlog", description="...", extras={"module": "modlog"} @@ -113,7 +117,7 @@ async def lookup_user_command( ) async def lookup_moderator_command( self: Self, interaction: discord.Interaction, moderator: discord.Member - ): + ) -> None: """This is the core of the /modlog lookup-moderator command Args: @@ -155,7 +159,6 @@ async def convert_ban_to_pretty_string( """This converts a database ban entry into a shiny embed Args: - self (Self): _description_ ban (munch.Munch): The ban database entry title (str): The title to set the embeds to diff --git a/techsupport_bot/commands/report.py b/techsupport_bot/commands/report.py index b354489ea..5d28a3b24 100644 --- a/techsupport_bot/commands/report.py +++ b/techsupport_bot/commands/report.py @@ -15,7 +15,6 @@ async def report_command( Allows users to report potential moderation issues to staff Args: - self (Self): _description_ interaction (discord.Interaction): The interaction that called this command report_str (str): The report string that the user submitted """ diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index bb05d84a9..4dab5d57a 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -63,6 +63,9 @@ def score(self: Self) -> int: class AutoMod(cogs.MatchCog): + """Holds all of the discord message specific automod functions + Most of the automod is a class function""" + async def match( self: Self, config: munch.Munch, ctx: commands.Context, content: str ) -> bool: @@ -98,7 +101,11 @@ async def match( return True async def response( - self, config: munch.Munch, ctx: commands.Context, content: str, result: bool + self: Self, + config: munch.Munch, + ctx: commands.Context, + content: str, + result: bool, ) -> None: """Handles a discord automod violation @@ -219,7 +226,9 @@ async def response( await alert_channel.send(embed=alert_channel_embed) @commands.Cog.listener() - async def on_raw_message_edit(self: Self, payload: discord.RawMessageUpdateEvent): + async def on_raw_message_edit( + self: Self, payload: discord.RawMessageUpdateEvent + ) -> None: """This is called when any message is edited in any guild the bot is in. There is no guarantee that the message exists or is used @@ -256,7 +265,7 @@ async def on_raw_message_edit(self: Self, payload: discord.RawMessageUpdateEvent def generate_automod_alert_embed( ctx: commands.Context, violations: list[AutoModPunishment], action_taken: str -): +) -> discord.Embed: """Generates an alert embed for the automod rules that are broken Args: diff --git a/techsupport_bot/functions/paste.py b/techsupport_bot/functions/paste.py index 7dfc763b3..986692a55 100644 --- a/techsupport_bot/functions/paste.py +++ b/techsupport_bot/functions/paste.py @@ -63,7 +63,11 @@ async def match( return True async def response( - self, config: munch.Munch, ctx: commands.Context, content: str, result: bool + self: Self, + config: munch.Munch, + ctx: commands.Context, + content: str, + result: bool, ) -> None: """Handles a paste check @@ -90,7 +94,9 @@ def max_newlines(self: Self, max_length: int) -> int: return int(max_length / 80) + 1 @commands.Cog.listener() - async def on_raw_message_edit(self, payload: discord.RawMessageUpdateEvent): + async def on_raw_message_edit( + self: Self, payload: discord.RawMessageUpdateEvent + ) -> None: """This is called when any message is edited in any guild the bot is in. There is no guarantee that the message exists or is used @@ -214,9 +220,12 @@ async def create_linx_embed( "Linx-Randomize": "yes", "Accept": "application/json", } - file = {"file": io.StringIO(content)} + html_file = {"file": io.StringIO(content)} response = await self.bot.http_functions.http_call( - "post", self.bot.file_config.api.api_url.linx, headers=headers, data=file + "post", + self.bot.file_config.api.api_url.linx, + headers=headers, + data=html_file, ) url = response.get("url") From dea76a2481639e023491fe241656a6cddd54a665 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 23 Jul 2024 10:08:45 -0400 Subject: [PATCH 22/99] warnings all/clear --- techsupport_bot/commands/moderator.py | 120 ++++++++++++++++++++++++-- techsupport_bot/commands/modlog.py | 6 +- techsupport_bot/core/moderation.py | 5 +- techsupport_bot/functions/automod.py | 5 +- 4 files changed, 120 insertions(+), 16 deletions(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index cd275e716..1c3157014 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -30,6 +30,10 @@ class ProtectCommands(cogs.BaseCog): """The cog for all manual moderation activities These are all slash commands""" + warnings_group = app_commands.Group( + name="warning", description="...", extras={"module": "moderator"} + ) + # Commands @app_commands.checks.has_permissions(ban_members=True) @@ -93,7 +97,7 @@ async def handle_ban_user( ) await moderation.send_command_usage_alert( - bot=self.bot, + bot_object=self.bot, interaction=interaction, command=( f"/ban target: {target.display_name}, reason: {reason}, delete_days:" @@ -158,7 +162,7 @@ async def handle_unban_user( ) await moderation.send_command_usage_alert( - bot=self.bot, + bot_object=self.bot, interaction=interaction, command=f"/unban target: {target.display_name}, reason: {reason}", guild=interaction.guild, @@ -208,7 +212,7 @@ async def handle_kick_user( return await moderation.send_command_usage_alert( - bot=self.bot, + bot_object=self.bot, interaction=interaction, command=f"/kick target: {target.display_name}, reason: {reason}", guild=interaction.guild, @@ -294,7 +298,7 @@ async def handle_mute_user( return await moderation.send_command_usage_alert( - bot=self.bot, + bot_object=self.bot, interaction=interaction, command=( f"/mute target: {target.display_name}, reason: {reason}, duration:" @@ -353,7 +357,7 @@ async def handle_unmute_user( return await moderation.send_command_usage_alert( - bot=self.bot, + bot_object=self.bot, interaction=interaction, command=f"/unmute target: {target.display_name}, reason: {reason}", guild=interaction.guild, @@ -419,7 +423,7 @@ async def handle_warn_user( should_ban = True warn_result = await moderation.warn_user( - bot=self.bot, user=target, invoker=interaction.user, reason=reason + bot_object=self.bot, user=target, invoker=interaction.user, reason=reason ) if should_ban: @@ -457,7 +461,7 @@ async def handle_warn_user( return await moderation.send_command_usage_alert( - bot=self.bot, + bot_object=self.bot, interaction=interaction, command=f"/warn target: {target.display_name}, reason: {reason}", guild=interaction.guild, @@ -525,7 +529,7 @@ async def handle_unwarn_user( return result = await moderation.unwarn_user( - bot=self.bot, user=target, warning=warning + bot_object=self.bot, user=target, warning=warning ) if not result: embed = auxiliary.prepare_deny_embed( @@ -535,7 +539,7 @@ async def handle_unwarn_user( return await moderation.send_command_usage_alert( - bot=self.bot, + bot_object=self.bot, interaction=interaction, command=f"/unwarn target: {target.display_name}, reason: {reason}, warning: {warning}", guild=interaction.guild, @@ -544,6 +548,104 @@ async def handle_unwarn_user( embed = generate_response_embed(user=target, action="unwarn", reason=reason) await interaction.response.send_message(content=target.mention, embed=embed) + @app_commands.checks.has_permissions(kick_members=True) + @app_commands.checks.bot_has_permissions(kick_members=True) + @warnings_group.command( + name="clear", + description="clears all warnings from a user", + extras={"module": "moderator"}, + ) + async def handle_warning_clear( + self: Self, + interaction: discord.Interaction, + target: discord.Member, + reason: str, + ) -> None: + """The core logic of the /warnings clear command + + Args: + interaction (discord.Interaction): The interaction that triggered the command + target (discord.Member): The user having warnings cleared + reason (str): The reason for the warnings being cleared + """ + permission_check = await self.permission_check( + invoker=interaction.user, target=target, action_name="unwarn" + ) + if permission_check: + embed = auxiliary.prepare_deny_embed(message=permission_check) + await interaction.response.send_message(embed=embed) + return + + warnings = await moderation.get_all_warnings( + self.bot, target, interaction.guild + ) + + if not warnings: + embed = auxiliary.prepare_deny_embed( + message=f"No warnings could be found on {target}" + ) + await interaction.response.send_message(embed=embed) + return + + for warning in warnings: + await moderation.unwarn_user(self.bot, target, warning.reason) + + await moderation.send_command_usage_alert( + bot_object=self.bot, + interaction=interaction, + command=f"/warnings clear target: {target.display_name}, reaason: {reason}", + guild=interaction.guild, + target=target, + ) + + embed = generate_response_embed( + user=target, action="warnings clear", reason=reason + ) + await interaction.response.send_message(content=target.mention, embed=embed) + + @app_commands.checks.has_permissions(kick_members=True) + @app_commands.checks.bot_has_permissions(kick_members=True) + @warnings_group.command( + name="all", + description="Shows all warnings to the invoker", + extras={"module": "moderator"}, + ) + async def handle_warning_clear( + self: Self, + interaction: discord.Interaction, + target: discord.User, + ) -> None: + """The core logic of the /warnings all command + + Args: + interaction (discord.Interaction): The interaction that triggered the command + target (discord.User): The user to lookup warnings for + """ + warnings = await moderation.get_all_warnings( + self.bot, target, interaction.guild + ) + + if not warnings: + embed = auxiliary.prepare_deny_embed( + message=f"No warnings could be found on {target}" + ) + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + embed = discord.Embed( + title=f"Warnings for {target.display_name} ({target.name})" + ) + for warning in warnings: + warning_moderator = await self.bot.fetch_user(int(warning.invoker_id)) + print(type(warning.time)) + embed.add_field( + name=f"Warning by {warning_moderator.display_name} ({warning_moderator.name})", + value=f"{warning.reason}\nWarned at {warning.time}", + ) + embed.color = discord.Color.blue() + + await interaction.response.send_message(embed=embed, ephemeral=True) + # Helper functions async def permission_check( diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index d6ae90e02..7d3576794 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -39,7 +39,7 @@ class BanLogger(cogs.BaseCog): @modlog_group.command( name="highscores", - description="Unban someone and allow them to apply", + description="Shows the top 10 moderators based on ban count", extras={"module": "modlog"}, ) async def high_score_command(self: Self, interaction: discord.Interaction) -> None: @@ -71,7 +71,7 @@ async def high_score_command(self: Self, interaction: discord.Interaction) -> No @modlog_group.command( name="lookup-user", - description="Unban someone and allow them to apply", + description="Looks up the 10 most recent bans for a given user", extras={"module": "modlog"}, ) async def lookup_user_command( @@ -112,7 +112,7 @@ async def lookup_user_command( @modlog_group.command( name="lookup-moderator", - description="Unban someone and allow them to apply", + description="Looks up the 10 most recent bans by a given moderator", extras={"module": "modlog"}, ) async def lookup_moderator_command( diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index 46ca27dde..7ff7a6ae6 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -75,7 +75,10 @@ async def mute_user(user: discord.Member, reason: str, duration: timedelta) -> b Returns: bool: True if the timeout was successful """ - await user.timeout(duration, reason=reason) + try: + await user.timeout(duration, reason=reason) + except discord.Forbidden: + return False return True diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 4dab5d57a..6e221b705 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -139,9 +139,8 @@ async def response( if should_mute: actions.append("mute") if not ctx.author.timed_out_until: - await ctx.author.timeout( - timedelta(hours=1), - reason=sorted_punishments[0].violation_str, + await moderation.mute_user( + ctx.author, sorted_punishments[0].violation_str, timedelta(hours=1) ) if should_delete: From 5a0e4a985b38a2649c64a9c324c1cc0301aec7a7 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 23 Jul 2024 11:07:07 -0400 Subject: [PATCH 23/99] Report command --- techsupport_bot/commands/htd.py | 1 - techsupport_bot/commands/moderator.py | 2 +- techsupport_bot/commands/report.py | 89 ++++++++++++++++++++++++++- 3 files changed, 88 insertions(+), 4 deletions(-) diff --git a/techsupport_bot/commands/htd.py b/techsupport_bot/commands/htd.py index 453dba3c3..0d78673a7 100644 --- a/techsupport_bot/commands/htd.py +++ b/techsupport_bot/commands/htd.py @@ -235,7 +235,6 @@ def custom_embed_generation(raw_input: str, val_to_convert: int) -> discord.Embe inline=False, ) - print(embed.fields[0].name) return embed diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 1c3157014..682e6eacc 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -610,7 +610,7 @@ async def handle_warning_clear( description="Shows all warnings to the invoker", extras={"module": "moderator"}, ) - async def handle_warning_clear( + async def handle_warning_all( self: Self, interaction: discord.Interaction, target: discord.User, diff --git a/techsupport_bot/commands/report.py b/techsupport_bot/commands/report.py index 5d28a3b24..22f94802f 100644 --- a/techsupport_bot/commands/report.py +++ b/techsupport_bot/commands/report.py @@ -1,13 +1,36 @@ """The report command""" -from typing import Self +from __future__ import annotations + +import datetime +import re +from typing import TYPE_CHECKING, Self import discord +from core import auxiliary, cogs, moderation +from discord import app_commands + +if TYPE_CHECKING: + import bot + + +async def setup(bot: bot.TechSupportBot) -> None: + """Adds the cog to the bot. Setups config + Args: + bot (bot.TechSupportBot): The bot object to register the cog with + """ + await bot.add_cog(Report(bot=bot)) -class Report: + +class Report(cogs.BaseCog): """The class that holds the report command and helper function""" + @app_commands.command( + name="report", + description="Reports something to the moderators", + extras={"module": "report"}, + ) async def report_command( self: Self, interaction: discord.Interaction, report_str: str ) -> None: @@ -18,3 +41,65 @@ async def report_command( interaction (discord.Interaction): The interaction that called this command report_str (str): The report string that the user submitted """ + embed = discord.Embed(title="New Report", description=report_str) + embed.color = discord.Color.red() + embed.set_author( + name=interaction.user.name, icon_url=interaction.user.avatar.url + ) + + embed.add_field( + name="User info", + value=( + f"**Name:** {interaction.user.name} ({interaction.user.mention})\n" + f"**Joined:** \n" + f"**Created:** \n" + f"**Sent from:** {interaction.channel.mention} [Jump to context](https://discord.com/channels/{interaction.guild.id}/{interaction.channel.id}/{discord.utils.time_snowflake(datetime.datetime.utcnow())})" + ), + ) + + mention_pattern = re.compile(r"<@!?(\d+)>") + mentioned_user_ids = mention_pattern.findall(report_str) + + mentioned_users = [] + for user_id in mentioned_user_ids: + user = await interaction.guild.fetch_member(int(user_id)) + if user: + mentioned_users.append(user) + mentioned_users: list[discord.Member] = set(mentioned_users) + + for index, user in enumerate(mentioned_users): + embed.add_field( + name=f"Mentioned user #{index+1}", + value=( + f"**Name:** {user.name} ({user.mention})\n" + f"**Joined:** \n" + f"**Created:** \n" + f"**ID:** {user.id}" + ), + ) + + embed.set_footer(text=f"Author ID: {interaction.user.id}") + embed.timestamp = datetime.datetime.utcnow() + + config = self.bot.guild_configs[str(interaction.guild.id)] + + try: + alert_channel = interaction.guild.get_channel( + int(config.extensions.protect.alert_channel.value) + ) + except TypeError: + alert_channel = None + + if not alert_channel: + user_embed = auxiliary.prepare_deny_embed( + message="An error occured while processing your report. It was not received." + ) + await interaction.response.send_message(embed=user_embed, ephemeral=True) + return + + await alert_channel.send(embed=embed) + + user_embed = auxiliary.prepare_confirm_embed( + message="Your report was successfully recieved" + ) + await interaction.response.send_message(embed=user_embed, ephemeral=True) From 6697956c2ac6d14fddd328966642a0a67766860c Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 23 Jul 2024 16:35:45 -0400 Subject: [PATCH 24/99] Automod silent punishment and variable mute --- techsupport_bot/functions/automod.py | 55 +++++++++++++++++----------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 6e221b705..5d707b5fe 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -35,14 +35,16 @@ class AutoModPunishment: violation_str - The string of the policy broken. Should be displayed to user recommend_delete - If the policy recommends deletion of the message recommend_warn - If the policy recommends warning the user - recommend_mute - If the policy recommends muting the user + recommend_mute - If the policy recommends muting the user. If so, the amount of seconds to mute for + is_silent - If the punishment should be silent """ violation_str: str recommend_delete: bool recommend_warn: bool - recommend_mute: bool + recommend_mute: int + is_silent: bool = False @property def score(self: Self) -> int: @@ -117,7 +119,9 @@ async def response( """ should_delete = False should_warn = False - should_mute = False + mute_duration = 0 + + silent = True all_punishments = run_all_checks(config, ctx.message) @@ -130,18 +134,22 @@ async def response( for punishment in sorted_punishments: should_delete = should_delete or punishment.recommend_delete should_warn = should_warn or punishment.recommend_warn - should_mute = should_mute or punishment.recommend_mute + mute_duration = max(mute_duration, punishment.recommend_mute) + + if not punishment.is_silent: + silent = False actions = [] reason_str = sorted_punishments[0].violation_str - if should_mute: + if mute_duration > 0: actions.append("mute") - if not ctx.author.timed_out_until: - await moderation.mute_user( - ctx.author, sorted_punishments[0].violation_str, timedelta(hours=1) - ) + await moderation.mute_user( + ctx.author, + sorted_punishments[0].violation_str, + timedelta(seconds=mute_duration), + ) if should_delete: actions.append("delete") @@ -167,16 +175,16 @@ async def response( f" {sorted_punishments[0].violation_str}) - banned by automod" ), ) - - await ctx.send(content=ctx.author.mention, embed=ban_embed) - try: - await ctx.author.send(embed=ban_embed) - except discord.Forbidden: - await self.bot.logger.send_log( - message=f"Could not DM {ctx.author} about being banned", - level=LogLevel.WARNING, - context=LogContext(guild=ctx.guild, channel=ctx.channel), - ) + if not silent: + await ctx.send(content=ctx.author.mention, embed=ban_embed) + try: + await ctx.author.send(embed=ban_embed) + except discord.Forbidden: + await self.bot.logger.send_log( + message=f"Could not DM {ctx.author} about being banned", + level=LogLevel.WARNING, + context=LogContext(guild=ctx.guild, channel=ctx.channel), + ) await moderation.ban_user( ctx.guild, ctx.author, 7, sorted_punishments[0].violation_str @@ -192,6 +200,9 @@ async def response( if len(actions) == 0: actions.append("notice") + if silent: + return + actions_str = " & ".join(actions) embed = moderator.generate_response_embed(ctx.author, actions_str, reason_str) @@ -369,7 +380,7 @@ def handle_file_extensions( f"{attachment.filename} has a suspicious file extension", recommend_delete=True, recommend_warn=True, - recommend_mute=False, + recommend_mute=0, ) ) return violations @@ -393,7 +404,7 @@ def handle_mentions( "Mass Mentions", recommend_delete=True, recommend_warn=True, - recommend_mute=False, + recommend_mute=0, ) ] return [] @@ -422,6 +433,7 @@ def handle_exact_string(config: munch.Munch, content: str) -> list[AutoModPunish filter_config.delete, filter_config.warn, filter_config.mute, + filter_config.silent_punishment, ) ) return violations @@ -456,6 +468,7 @@ def handle_regex_string(config: munch.Munch, content: str) -> list[AutoModPunish filter_config.delete, filter_config.warn, filter_config.mute, + filter_config.silent_punishment, ) ) return violations From 82305d8d31d666b07fcc8cf36510d9dfa7d51b28 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 23 Jul 2024 18:11:14 -0400 Subject: [PATCH 25/99] Fix config --- techsupport_bot/bot.py | 3 + techsupport_bot/commands/moderator.py | 34 ++- techsupport_bot/commands/modlog.py | 15 +- techsupport_bot/commands/protect.py | 423 -------------------------- techsupport_bot/commands/purge.py | 15 +- techsupport_bot/commands/report.py | 15 +- techsupport_bot/core/moderation.py | 4 +- techsupport_bot/functions/automod.py | 80 ++++- techsupport_bot/functions/paste.py | 54 +++- 9 files changed, 176 insertions(+), 467 deletions(-) delete mode 100644 techsupport_bot/commands/protect.py diff --git a/techsupport_bot/bot.py b/techsupport_bot/bot.py index 9909e9edb..b4eab9cc3 100644 --- a/techsupport_bot/bot.py +++ b/techsupport_bot/bot.py @@ -322,6 +322,9 @@ async def create_new_context_config(self: Self, guild_id: str) -> munch.Munch: config_.rate_limit.enabled = False config_.rate_limit.commands = 4 config_.rate_limit.time = 10 + config_.moderation = munch.DefaultMunch(None) + config_.moderation.max_warnings = 3 + config_.moderation.alert_channel = None config_.extensions = extensions_config diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 682e6eacc..82a536a7a 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -10,7 +10,7 @@ import ui from botlogging import LogContext, LogLevel from commands import modlog -from core import auxiliary, cogs, moderation +from core import auxiliary, cogs, extensionconfig, moderation from discord import app_commands if TYPE_CHECKING: @@ -23,7 +23,25 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cog with """ - await bot.add_cog(ProtectCommands(bot=bot)) + config = extensionconfig.ExtensionConfig() + config.add( + key="immune_roles", + datatype="list", + title="Immune role names", + description="The list of role names that are immune to protect commands", + default=[], + ) + config.add( + key="ban_delete_duration", + datatype="int", + title="Ban delete duration (days)", + description=( + "The amount of days to delete messages for a user after they are banned" + ), + default=7, + ) + await bot.add_cog(ProtectCommands(bot=bot, extension_name="moderator")) + bot.add_extension_config("moderator", config) class ProtectCommands(cogs.BaseCog): @@ -76,7 +94,7 @@ async def handle_ban_user( if not delete_days: config = self.bot.guild_configs[str(interaction.guild.id)] - delete_days = config.extensions.protect.ban_delete_duration.value + delete_days = config.extensions.moderator.ban_delete_duration.value # Ban the user using the core moderation cog result = await moderation.ban_user( @@ -407,12 +425,12 @@ async def handle_warn_user( ) should_ban = False - if new_count_of_warnings >= config.extensions.protect.max_warnings.value: + if new_count_of_warnings >= config.moderation.max_warnings: await interaction.response.defer(ephemeral=False) view = ui.Confirm() await view.send( message="This user has exceeded the max warnings of " - + f"{config.extensions.protect.max_warnings.value}. Would " + + f"{config.moderation.max_warnings}. Would " + "you like to ban them instead?", channel=interaction.channel, author=interaction.user, @@ -430,10 +448,10 @@ async def handle_warn_user( ban_result = await moderation.ban_user( guild=interaction.guild, user=target, - delete_days=config.extensions.protect.ban_delete_duration.value, + delete_days=config.extensions.moderator.ban_delete_duration.value, reason=( f"Over max warning count {new_count_of_warnings} out of" - f" {config.extensions.protect.max_warnings.value} (final warning:" + f" {config.moderation.max_warnings} (final warning:" f" {reason}) - banned by {interaction.user}" ), ) @@ -686,7 +704,7 @@ async def permission_check( return None # Check to see if target has any immune roles - for name in config.extensions.protect.immune_roles.value: + for name in config.extensions.moderator.immune_roles.value: role_check = discord.utils.get(target.guild.roles, name=name) if role_check and role_check in getattr(target, "roles", []): return ( diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index 7d3576794..c24a17029 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -9,7 +9,7 @@ import discord import munch import ui -from core import auxiliary, cogs +from core import auxiliary, cogs, extensionconfig from discord import app_commands from discord.ext import commands @@ -23,7 +23,16 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cog with """ + config = extensionconfig.ExtensionConfig() + config.add( + key="alert_channel", + datatype="int", + title="Alert channel ID", + description="The ID of the channel to send auto-protect alerts to", + default=None, + ) await bot.add_cog(BanLogger(bot=bot, extension_name="modlog")) + bot.add_extension_config("modlog", config) class BanLogger(cogs.BaseCog): @@ -278,7 +287,7 @@ async def log_ban( try: alert_channel = guild.get_channel( - int(config.extensions.protect.alert_channel.value) + int(config.extensions.modlog.alert_channel.value) ) except TypeError: alert_channel = None @@ -322,7 +331,7 @@ async def log_unban( try: alert_channel = guild.get_channel( - int(config.extensions.protect.alert_channel.value) + int(config.extensions.modlog.alert_channel.value) ) except TypeError: alert_channel = None diff --git a/techsupport_bot/commands/protect.py b/techsupport_bot/commands/protect.py deleted file mode 100644 index 58310568b..000000000 --- a/techsupport_bot/commands/protect.py +++ /dev/null @@ -1,423 +0,0 @@ -""" -Todo: - Purge to slash commands - Unwarn has autofill - Get all warnings command - - Make all of automod - Simplify paste - Make paste not work if message would be DELETED by automod - Create a ban logging system like carl - Needs a database for ban history - Central unban logging but NO database needed - -Ban logs need to be more centralized: - Auto bans, command bans, and manual bans all need to be logged with a single message - A modlog highscores command, in a modlog.py command - -Move all config over to specific new files -""" - -import discord -import expiringdict -import ui -from botlogging import LogContext, LogLevel -from core import auxiliary, cogs, extensionconfig -from discord.ext import commands - - -async def setup(bot): - """Class to set up the protect options in the config file.""" - - config = extensionconfig.ExtensionConfig() - config.add( - key="channels", - datatype="list", - title="Protected channels", - description=( - "The list of channel ID's associated with the channels to auto-protect" - ), - default=[], - ) - config.add( - key="bypass_roles", - datatype="list", - title="Bypassed role names", - description=( - "The list of role names associated with bypassed roles by the auto-protect" - ), - default=[], - ) - config.add( - key="immune_roles", - datatype="list", - title="Immune role names", - description="The list of role names that are immune to protect commands", - default=[], - ) - config.add( - key="bypass_ids", - datatype="list", - title="Bypassed member ID's", - description=( - "The list of member ID's associated with bypassed members by the" - " auto-protect" - ), - default=[], - ) - config.add( - key="length_limit", - datatype="int", - title="Max length limit", - description=( - "The max char limit on messages before they trigger an action by" - " auto-protect" - ), - default=500, - ) - config.add( - key="string_map", - datatype="dict", - title="Keyword string map", - description=( - "Mapping of keyword strings to data defining the action taken by" - " auto-protect" - ), - default={}, - ) - config.add( - key="banned_file_extensions", - datatype="dict", - title="List of banned file types", - description=( - "A list of all file extensions to be blocked and have a auto warning issued" - ), - default=[], - ) - config.add( - key="alert_channel", - datatype="int", - title="Alert channel ID", - description="The ID of the channel to send auto-protect alerts to", - default=None, - ) - config.add( - key="max_mentions", - datatype="int", - title="Max message mentions", - description=( - "Max number of mentions allowed in a message before triggering auto-protect" - ), - default=3, - ) - config.add( - key="max_warnings", - datatype="int", - title="Max Warnings", - description="The amount of warnings a user should be banned on", - default=3, - ) - config.add( - key="ban_delete_duration", - datatype="int", - title="Ban delete duration (days)", - description=( - "The amount of days to delete messages for a user after they are banned" - ), - default=7, - ) - config.add( - key="max_purge_amount", - datatype="int", - title="Max Purge Amount", - description="The max amount of messages allowed to be purged in one command", - default=50, - ) - config.add( - key="paste_footer_message", - datatype="str", - title="The linx embed footer", - description="The message used on the footer of the large message paste URL", - default="Note: Long messages are automatically pasted", - ) - - await bot.add_cog(Protector(bot=bot, extension_name="protect")) - bot.add_extension_config("protect", config) - - -class ProtectEmbed(discord.Embed): - """Class to make the embed for the protect command.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.title = "Chat Protection" - self.color = discord.Color.gold() - - -class Protector(cogs.MatchCog): - """Class for the protector command.""" - - ALERT_ICON_URL = ( - "https://cdn.icon-icons.com/icons2/2063/PNG/512/" - + "alert_danger_warning_notification_icon_124692.png" - ) - CLIPBOARD_ICON_URL = ( - "https://icon-icons.com/icons2/203/PNG/128/diagram-30_24487.png" - ) - - async def preconfig(self): - """Method to preconfig the protect.""" - self.string_alert_cache = expiringdict.ExpiringDict( - max_len=100, max_age_seconds=3600 - ) - - async def handle_string_alert(self, config, ctx, content, filter_config): - """Method to handle a string alert for the protect extension.""" - # If needed, delete the message - if filter_config.delete: - await ctx.message.delete() - - # Send only 1 response based on warn, deletion, or neither - if filter_config.warn: - await self.handle_warn(ctx, ctx.author, filter_config.message, bypass=True) - elif filter_config.delete: - await self.send_default_delete_response( - config, ctx, content, filter_config.message - ) - else: - # Ensure we don't trigger people more than once if the only trigger is a warning - cache_key = self.get_cache_key(ctx.guild, ctx.author, filter_config.trigger) - if self.string_alert_cache.get(cache_key): - return - - self.string_alert_cache[cache_key] = True - embed = ProtectEmbed(description=filter_config.message) - await ctx.send(ctx.message.author.mention, embed=embed) - - await self.send_alert( - config, - ctx, - f"Message contained trigger: {filter_config.trigger}", - ) - - async def handle_warn(self, ctx, user: discord.Member, reason: str, bypass=False): - """Method to handle the warn of a user.""" - if not bypass: - can_execute = await self.can_execute(ctx, user) - if not can_execute: - return - - warnings = await self.get_warnings(user, ctx.guild) - - new_count = len(warnings) + 1 - - config = self.bot.guild_configs[str(ctx.guild.id)] - - if new_count >= config.extensions.protect.max_warnings.value: - # Start by assuming we don't want to ban someone - should_ban = False - - # If there is no bypass, ask using ui.Confirm - # If there is a bypass, assume we want to ban - if not bypass: - view = ui.Confirm() - await view.send( - message="This user has exceeded the max warnings of " - + f"{config.extensions.protect.max_warnings.value}. Would " - + "you like to ban them instead?", - channel=ctx.channel, - author=ctx.author, - ) - await view.wait() - if view.value is ui.ConfirmResponse.CONFIRMED: - should_ban = True - else: - should_ban = True - - if should_ban: - await self.handle_ban( - ctx, - user, - f"Over max warning count {new_count} out " - + f"of {config.extensions.protect.max_warnings.value}" - + f" (final warning: {reason})", - bypass=True, - ) - await self.clear_warnings(user, ctx.guild) - return - - embed = await self.generate_user_modified_embed( - user, "warn", f"{reason} ({new_count} total warnings)" - ) - - # Attempt DM for manually initiated, non-banning warns - if ctx.command == self.bot.get_command("warn"): - # Cancel warns in channels invisible to user - if user not in ctx.channel.members: - await auxiliary.send_deny_embed( - message=f"{user} cannot see this warning.", channel=ctx.channel - ) - return - - try: - await user.send(embed=embed) - - except (discord.HTTPException, discord.Forbidden): - channel = config.get("logging_channel") - await self.bot.logger.send_log( - message=f"Failed to DM warning to {user}", - level=LogLevel.WARNING, - channel=channel, - context=LogContext(guild=ctx.guild, channel=ctx.channel), - ) - - finally: - await ctx.send(content=user.mention, embed=embed) - - else: - await ctx.send(ctx.message.author.mention, embed=embed) - - await self.bot.models.Warning( - user_id=str(user.id), guild_id=str(ctx.guild.id), reason=reason - ).create() - - async def clear_warnings(self, user, guild): - """Method to clear warnings of a user in discord.""" - await self.bot.models.Warning.delete.where( - self.bot.models.Warning.user_id == str(user.id) - ).where(self.bot.models.Warning.guild_id == str(guild.id)).gino.status() - - async def generate_user_modified_embed(self, user, action, reason): - """Method to generate the user embed with the reason.""" - embed = discord.Embed( - title="Chat Protection", description=f"{action.upper()} `{user}`" - ) - embed.set_footer(text=f"Reason: {reason}") - embed.set_thumbnail(url=user.display_avatar.url) - embed.color = discord.Color.gold() - - return embed - - def get_cache_key(self, guild, user, trigger): - """Method to get the cache key of the user.""" - return f"{guild.id}_{user.id}_{trigger}" - - async def can_execute(self, ctx: commands.Context, target: discord.User): - """Method to not execute on admin users.""" - action = ctx.command.name or "do that to" - config = self.bot.guild_configs[str(ctx.guild.id)] - - # Check to see if executed on author - if target == ctx.author: - await auxiliary.send_deny_embed( - message=f"You cannot {action} yourself", channel=ctx.channel - ) - return False - # Check to see if executed on bot - if target == self.bot.user: - await auxiliary.send_deny_embed( - message=f"It would be silly to {action} myself", channel=ctx.channel - ) - return False - # Check to see if target has a role. Will allow execution on Users outside of server - if not hasattr(target, "top_role"): - return True - # Check to see if target has any immune roles - for name in config.extensions.protect.immune_roles.value: - role_check = discord.utils.get(target.guild.roles, name=name) - if role_check and role_check in getattr(target, "roles", []): - await auxiliary.send_deny_embed( - message=( - f"You cannot {action} {target} because they have `{role_check}`" - " role" - ), - channel=ctx.channel, - ) - return False - # Check to see if the Bot can execute on the target - if ctx.guild.me.top_role <= target.top_role: - await auxiliary.send_deny_embed( - message=f"Bot does not have enough permissions to {action} `{target}`", - channel=ctx.channel, - ) - return False - # Check to see if author top role is higher than targets - if target.top_role >= ctx.author.top_role: - await auxiliary.send_deny_embed( - message=f"You do not have enough permissions to {action} `{target}`", - channel=ctx.channel, - ) - return False - return True - - async def send_alert(self, config, ctx, message): - """Method to send an alert to the channel about a protect command.""" - try: - alert_channel = ctx.guild.get_channel( - int(config.extensions.protect.alert_channel.value) - ) - except TypeError: - alert_channel = None - - if not alert_channel: - return - - embed = discord.Embed(title="Protect Alert", description=message) - - if len(ctx.message.content) >= 256: - message_content = ctx.message.content[0:256] - else: - message_content = ctx.message.content - - embed.add_field(name="Channel", value=f"#{ctx.channel.name}") - embed.add_field(name="User", value=ctx.author.mention) - embed.add_field(name="Message", value=message_content, inline=False) - embed.add_field(name="URL", value=ctx.message.jump_url, inline=False) - - embed.set_thumbnail(url=self.ALERT_ICON_URL) - embed.color = discord.Color.red() - - await alert_channel.send(embed=embed) - - async def send_default_delete_response(self, config, ctx, content, reason): - """Method for the default delete of a message.""" - embed = ProtectEmbed(description=f"Message deleted. Reason: *{reason}*") - await ctx.send(ctx.message.author.mention, embed=embed) - await ctx.author.send(f"Deleted message: ```{content[:1994]}```") - - async def get_warnings(self, user, guild): - """Method to get the warnings of a user.""" - warnings = ( - await self.bot.models.Warning.query.where( - self.bot.models.Warning.user_id == str(user.id) - ) - .where(self.bot.models.Warning.guild_id == str(guild.id)) - .gino.all() - ) - return warnings - - @commands.has_permissions(kick_members=True) - @commands.bot_has_permissions(kick_members=True) - @commands.command( - name="warnings", - brief="Gets warnings", - description="Gets warnings for a user", - usage="@user", - ) - async def get_warnings_command(self, ctx, user: discord.User): - """Method to get the warnings of a user in discord.""" - warnings = await self.get_warnings(user, ctx.guild) - if not warnings: - await auxiliary.send_deny_embed( - message="There are no warnings for that user", channel=ctx.channel - ) - return - - embed = discord.Embed(title=f"Warnings for {user}") - for warning in warnings: - embed.add_field(name=warning.time, value=warning.reason, inline=False) - - embed.set_thumbnail(url=user.display_avatar.url) - - embed.color = discord.Color.red() - - await ctx.send(embed=embed) diff --git a/techsupport_bot/commands/purge.py b/techsupport_bot/commands/purge.py index 87007c931..62a389515 100644 --- a/techsupport_bot/commands/purge.py +++ b/techsupport_bot/commands/purge.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Self import discord -from core import auxiliary, cogs, moderation +from core import auxiliary, cogs, extensionconfig, moderation from discord import app_commands if TYPE_CHECKING: @@ -19,7 +19,16 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cog with """ - await bot.add_cog(Purger(bot=bot)) + config = extensionconfig.ExtensionConfig() + config.add( + key="max_purge_amount", + datatype="int", + title="Max Purge Amount", + description="The max amount of messages allowed to be purged in one command", + default=50, + ) + await bot.add_cog(Purger(bot=bot, extension_name="purge")) + bot.add_extension_config("purge", config) class Purger(cogs.BaseCog): @@ -47,7 +56,7 @@ async def purge_command( """ config = self.bot.guild_configs[str(interaction.guild.id)] - if amount <= 0 or amount > config.extensions.protect.max_purge_amount.value: + if amount <= 0 or amount > config.extensions.purge.max_purge_amount.value: embed = auxiliary.prepare_deny_embed( message="This is an invalid amount of messages to purge", ) diff --git a/techsupport_bot/commands/report.py b/techsupport_bot/commands/report.py index 22f94802f..7e1526830 100644 --- a/techsupport_bot/commands/report.py +++ b/techsupport_bot/commands/report.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Self import discord -from core import auxiliary, cogs, moderation +from core import auxiliary, cogs, extensionconfig from discord import app_commands if TYPE_CHECKING: @@ -20,7 +20,16 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cog with """ - await bot.add_cog(Report(bot=bot)) + config = extensionconfig.ExtensionConfig() + config.add( + key="alert_channel", + datatype="int", + title="Alert channel ID", + description="The ID of the channel to send auto-protect alerts to", + default=None, + ) + await bot.add_cog(Report(bot=bot, extension_name="report")) + bot.add_extension_config("report", config) class Report(cogs.BaseCog): @@ -85,7 +94,7 @@ async def report_command( try: alert_channel = interaction.guild.get_channel( - int(config.extensions.protect.alert_channel.value) + int(config.extensions.report.alert_channel.value) ) except TypeError: alert_channel = None diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index 7ff7a6ae6..079cf5452 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -197,9 +197,7 @@ async def send_command_usage_alert( config = bot_object.guild_configs[str(guild.id)] try: - alert_channel = guild.get_channel( - int(config.extensions.protect.alert_channel.value) - ) + alert_channel = guild.get_channel(int(config.moderation.alert_channel)) except TypeError: alert_channel = None diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 5d707b5fe..c46227efa 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -11,7 +11,7 @@ import munch from botlogging import LogContext, LogLevel from commands import moderator, modlog -from core import cogs, moderation +from core import cogs, extensionconfig, moderation from discord.ext import commands if TYPE_CHECKING: @@ -24,7 +24,62 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cog with """ + config = extensionconfig.ExtensionConfig() + config.add( + key="channels", + datatype="list", + title="Protected channels", + description=( + "The list of channel ID's associated with the channels to auto-protect" + ), + default=[], + ) + config.add( + key="bypass_roles", + datatype="list", + title="Bypassed role names", + description=( + "The list of role names associated with bypassed roles by the auto-protect" + ), + default=[], + ) + config.add( + key="string_map", + datatype="dict", + title="Keyword string map", + description=( + "Mapping of keyword strings to data defining the action taken by" + " auto-protect" + ), + default={}, + ) + config.add( + key="banned_file_extensions", + datatype="dict", + title="List of banned file types", + description=( + "A list of all file extensions to be blocked and have a auto warning issued" + ), + default=[], + ) + config.add( + key="alert_channel", + datatype="int", + title="Alert channel ID", + description="The ID of the channel to send auto-protect alerts to", + default=None, + ) + config.add( + key="max_mentions", + datatype="int", + title="Max message mentions", + description=( + "Max number of mentions allowed in a message before triggering auto-protect" + ), + default=3, + ) await bot.add_cog(AutoMod(bot=bot, extension_name="automod")) + bot.add_extension_config("automod", config) @dataclass @@ -81,9 +136,9 @@ async def match( Returns: bool: Whether the message should be inspected for automod violations """ - if not str(ctx.channel.id) in config.extensions.protect.channels.value: + if not str(ctx.channel.id) in config.extensions.automod.channels.value: await self.bot.logger.send_log( - message="Channel not in protected channels - ignoring protect check", + message="Channel not in automod channels - ignoring automod check", level=LogLevel.DEBUG, context=LogContext(guild=ctx.guild, channel=ctx.channel), ) @@ -93,13 +148,10 @@ async def match( if any( role_name.lower() in role_names - for role_name in config.extensions.protect.bypass_roles.value + for role_name in config.extensions.automod.bypass_roles.value ): return False - if ctx.author.id in config.extensions.protect.bypass_ids.value: - return False - return True async def response( @@ -165,13 +217,13 @@ async def response( await moderation.warn_user( self.bot, ctx.author, ctx.author, sorted_punishments[0].violation_str ) - if count_of_warnings >= config.extensions.protect.max_warnings.value: + if count_of_warnings >= config.moderation.max_warnings: ban_embed = moderator.generate_response_embed( ctx.author, "ban", reason=( f"Over max warning count {count_of_warnings} out of" - f" {config.extensions.protect.max_warnings.value} (final warning:" + f" {config.moderation.max_warnings} (final warning:" f" {sorted_punishments[0].violation_str}) - banned by automod" ), ) @@ -225,7 +277,7 @@ async def response( try: alert_channel = ctx.guild.get_channel( - int(config.extensions.protect.alert_channel.value) + int(config.extensions.automod.alert_channel.value) ) except TypeError: alert_channel = None @@ -373,7 +425,7 @@ def handle_file_extensions( for attachment in attachments: if ( attachment.filename.split(".")[-1] - in config.extensions.protect.banned_file_extensions.value + in config.extensions.automod.banned_file_extensions.value ): violations.append( AutoModPunishment( @@ -398,7 +450,7 @@ def handle_mentions( Returns: list[AutoModPunishment]: The automod violations that the given message violated """ - if len(message.mentions) > config.extensions.protect.max_mentions.value: + if len(message.mentions) > config.extensions.automod.max_mentions.value: return [ AutoModPunishment( "Mass Mentions", @@ -425,7 +477,7 @@ def handle_exact_string(config: munch.Munch, content: str) -> list[AutoModPunish for ( keyword, filter_config, - ) in config.extensions.protect.string_map.value.items(): + ) in config.extensions.automod.string_map.value.items(): if keyword.lower() in content.lower(): violations.append( AutoModPunishment( @@ -454,7 +506,7 @@ def handle_regex_string(config: munch.Munch, content: str) -> list[AutoModPunish for ( _, filter_config, - ) in config.extensions.protect.string_map.value.items(): + ) in config.extensions.automod.string_map.value.items(): regex = filter_config.get("regex") if regex: try: diff --git a/techsupport_bot/functions/paste.py b/techsupport_bot/functions/paste.py index 986692a55..6c7fb69b8 100644 --- a/techsupport_bot/functions/paste.py +++ b/techsupport_bot/functions/paste.py @@ -8,7 +8,7 @@ import discord import munch from botlogging import LogContext, LogLevel -from core import cogs +from core import cogs, extensionconfig from discord.ext import commands if TYPE_CHECKING: @@ -21,7 +21,44 @@ async def setup(bot: bot.TechSupportBot) -> None: Args: bot (bot.TechSupportBot): The bot object to register the cog with """ - await bot.add_cog(Paster(bot=bot)) + config = extensionconfig.ExtensionConfig() + config.add( + key="channels", + datatype="list", + title="Protected channels", + description=( + "The list of channel ID's associated with the channels to auto-protect" + ), + default=[], + ) + config.add( + key="bypass_roles", + datatype="list", + title="Bypassed role names", + description=( + "The list of role names associated with bypassed roles by the auto-protect" + ), + default=[], + ) + config.add( + key="length_limit", + datatype="int", + title="Max length limit", + description=( + "The max char limit on messages before they trigger an action by" + " auto-protect" + ), + default=500, + ) + config.add( + key="paste_footer_message", + datatype="str", + title="The linx embed footer", + description="The message used on the footer of the large message paste URL", + default="Note: Long messages are automatically pasted", + ) + await bot.add_cog(Paster(bot=bot, extension_name="paste")) + bot.add_extension_config("paste", config) class Paster(cogs.MatchCog): @@ -41,7 +78,7 @@ async def match( bool: Whether the message should be inspected for a paste """ # exit the match based on exclusion parameters - if not str(ctx.channel.id) in config.extensions.protect.channels.value: + if not str(ctx.channel.id) in config.extensions.paste.channels.value: await self.bot.logger.send_log( message="Channel not in protected channels - ignoring protect check", level=LogLevel.DEBUG, @@ -53,13 +90,10 @@ async def match( if any( role_name.lower() in role_names - for role_name in config.extensions.protect.bypass_roles.value + for role_name in config.extensions.paste.bypass_roles.value ): return False - if ctx.author.id in config.extensions.protect.bypass_ids.value: - return False - return True async def response( @@ -77,9 +111,9 @@ async def response( content (str): The string content of the message result (bool): What the match() function returned """ - if len(content) > config.extensions.protect.length_limit.value or content.count( + if len(content) > config.extensions.paste.length_limit.value or content.count( "\n" - ) > self.max_newlines(config.extensions.protect.length_limit.value): + ) > self.max_newlines(config.extensions.paste.length_limit.value): await self.handle_length_alert(config, ctx, content) def max_newlines(self: Self, max_length: int) -> int: @@ -239,7 +273,7 @@ async def create_linx_embed( embed.set_author( name=f"Paste by {ctx.author}", icon_url=ctx.author.display_avatar.url ) - embed.set_footer(text=config.extensions.protect.paste_footer_message.value) + embed.set_footer(text=config.extensions.paste.paste_footer_message.value) embed.color = discord.Color.blue() return embed From 985158d22177eb7d7b1f8430809692a47792841c Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 24 Jul 2024 18:49:14 -0400 Subject: [PATCH 26/99] Make more framework out of automod, fix modlog still running when disabled --- techsupport_bot/commands/duck.py | 9 +- techsupport_bot/commands/modlog.py | 8 ++ techsupport_bot/functions/automod.py | 133 ++++++++++++++++++--------- 3 files changed, 102 insertions(+), 48 deletions(-) diff --git a/techsupport_bot/commands/duck.py b/techsupport_bot/commands/duck.py index 3dddd060a..73dee4c2e 100644 --- a/techsupport_bot/commands/duck.py +++ b/techsupport_bot/commands/duck.py @@ -13,7 +13,7 @@ import munch import ui from botlogging import LogContext, LogLevel -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs, extensionconfig, moderation from discord import Color as embed_colors from discord.ext import commands @@ -382,9 +382,12 @@ def message_check( and channel.guild.me.guild_permissions.moderate_members ): asyncio.create_task( - message.author.timeout( - timedelta(seconds=config.extensions.duck.cooldown.value), + moderation.mute_user( + user=message.author, reason="Missed a duck", + duration=timedelta( + seconds=config.extensions.duck.cooldown.value + ), ) ) asyncio.create_task( diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index c24a17029..f5e14ad5b 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -262,6 +262,10 @@ async def log_ban( guild (discord.Guild): The guild the member was banned from reason (str): The reason for the ban """ + config = bot.guild_configs[str(guild.id)] + if not "modlog" in config.get("enabled_extensions", []): + return + if not reason: reason = "No reason specified" @@ -314,6 +318,10 @@ async def log_unban( guild (discord.Guild): The guild the member was unbanned from reason (str): The reason for the unban """ + config = bot.guild_configs[str(guild.id)] + if not "modlog" in config.get("enabled_extensions", []): + return + if not reason: reason = "No reason specified" diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index c46227efa..1a76115d9 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -119,6 +119,19 @@ def score(self: Self) -> int: return score +@dataclass +class AutoModAction: + warn: bool + delete_message: bool + mute: bool + mute_duration: int + be_silent: bool + action_string: str + violation_string: str + total_punishments: str + violations_list: list[AutoModPunishment] + + class AutoMod(cogs.MatchCog): """Holds all of the discord message specific automod functions Most of the automod is a class function""" @@ -169,53 +182,33 @@ async def response( content (str): The string content of the message result (bool): What the match() function returned """ - should_delete = False - should_warn = False - mute_duration = 0 - - silent = True - all_punishments = run_all_checks(config, ctx.message) if len(all_punishments) == 0: return - sorted_punishments = sorted( - all_punishments, key=lambda p: p.score, reverse=True - ) - for punishment in sorted_punishments: - should_delete = should_delete or punishment.recommend_delete - should_warn = should_warn or punishment.recommend_warn - mute_duration = max(mute_duration, punishment.recommend_mute) - - if not punishment.is_silent: - silent = False + total_punishment = process_automod_violations(all_punishments=all_punishments) - actions = [] - - reason_str = sorted_punishments[0].violation_str - - if mute_duration > 0: - actions.append("mute") + if total_punishment.mute > 0: await moderation.mute_user( - ctx.author, - sorted_punishments[0].violation_str, - timedelta(seconds=mute_duration), + user=ctx.author, + reason=total_punishment.violation_string, + duration=timedelta(seconds=total_punishment.mute_duration), ) - if should_delete: - actions.append("delete") + if total_punishment.delete_message: await ctx.message.delete() - if should_warn: - actions.append("warn") + if total_punishment.warn: count_of_warnings = ( len(await moderation.get_all_warnings(self.bot, ctx.author, ctx.guild)) + 1 ) - reason_str += f" ({count_of_warnings} total warnings)" + total_punishment.violation_string += ( + f" ({count_of_warnings} total warnings)" + ) await moderation.warn_user( - self.bot, ctx.author, ctx.author, sorted_punishments[0].violation_str + self.bot, ctx.author, ctx.author, total_punishment.violation_string ) if count_of_warnings >= config.moderation.max_warnings: ban_embed = moderator.generate_response_embed( @@ -224,10 +217,10 @@ async def response( reason=( f"Over max warning count {count_of_warnings} out of" f" {config.moderation.max_warnings} (final warning:" - f" {sorted_punishments[0].violation_str}) - banned by automod" + f" {total_punishment.violation_string}) - banned by automod" ), ) - if not silent: + if not total_punishment.be_silent: await ctx.send(content=ctx.author.mention, embed=ban_embed) try: await ctx.author.send(embed=ban_embed) @@ -239,25 +232,24 @@ async def response( ) await moderation.ban_user( - ctx.guild, ctx.author, 7, sorted_punishments[0].violation_str + ctx.guild, ctx.author, 7, total_punishment.violation_string ) await modlog.log_ban( self.bot, ctx.author, ctx.guild.me, ctx.guild, - sorted_punishments[0].violation_str, + total_punishment.violation_string, ) - if len(actions) == 0: - actions.append("notice") - - if silent: + if total_punishment.be_silent: return - actions_str = " & ".join(actions) - - embed = moderator.generate_response_embed(ctx.author, actions_str, reason_str) + embed = moderator.generate_response_embed( + ctx.author, + total_punishment.action_string, + total_punishment.violation_string, + ) await ctx.send(content=ctx.author.mention, embed=embed) try: @@ -270,7 +262,7 @@ async def response( ) alert_channel_embed = generate_automod_alert_embed( - ctx, sorted_punishments, actions_str + ctx, total_punishment.total_punishments, total_punishment.action_string ) config = self.bot.guild_configs[str(ctx.guild.id)] @@ -325,8 +317,59 @@ async def on_raw_message_edit( await self.response(config, ctx, message.content, matched) +def process_automod_violations( + all_punishments: list[AutoModPunishment], +) -> AutoModAction: + should_delete = False + should_warn = False + mute_duration = 0 + + silent = True + + sorted_punishments = sorted(all_punishments, key=lambda p: p.score, reverse=True) + for punishment in sorted_punishments: + should_delete = should_delete or punishment.recommend_delete + should_warn = should_warn or punishment.recommend_warn + mute_duration = max(mute_duration, punishment.recommend_mute) + + if not punishment.is_silent: + silent = False + + actions = [] + + reason_str = sorted_punishments[0].violation_str + + if mute_duration > 0: + actions.append("mute") + + if should_delete: + actions.append("delete") + + if should_warn: + actions.append("warn") + + if len(actions) == 0: + actions.append("notice") + + actions_str = " & ".join(actions) + + all_alerts_str = "\n".join(violation.violation_str for violation in all_punishments) + + final_action = AutoModAction( + warn=should_warn, + delete_message=should_delete, + mute=mute_duration > 0, + mute_duration=mute_duration, + be_silent=silent, + action_string=actions_str, + violation_string=reason_str, + total_punishments=all_alerts_str, + violations_list=all_punishments, + ) + + def generate_automod_alert_embed( - ctx: commands.Context, violations: list[AutoModPunishment], action_taken: str + ctx: commands.Context, violations: str, action_taken: str ) -> discord.Embed: """Generates an alert embed for the automod rules that are broken @@ -346,7 +389,7 @@ def generate_automod_alert_embed( embed = discord.Embed( title="Automod Violations", - description="\n".join(violation.violation_str for violation in violations), + description=violations, ) embed.add_field(name="Actions Taken", value=action_taken) embed.add_field(name="Channel", value=f"{ctx.channel.mention} ({ctx.channel.name})") From e0c2b6b140221f24aad6cf8b4b002ebd1d2833a0 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 24 Jul 2024 19:30:12 -0400 Subject: [PATCH 27/99] Some small changes and fixes --- techsupport_bot/commands/moderator.py | 3 ++- techsupport_bot/commands/modlog.py | 32 ++++++++++++++++++++++----- techsupport_bot/commands/who.py | 3 ++- techsupport_bot/functions/automod.py | 2 ++ 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 82a536a7a..96b02cc1d 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -653,12 +653,13 @@ async def handle_warning_all( embed = discord.Embed( title=f"Warnings for {target.display_name} ({target.name})" ) + for warning in warnings: warning_moderator = await self.bot.fetch_user(int(warning.invoker_id)) print(type(warning.time)) embed.add_field( name=f"Warning by {warning_moderator.display_name} ({warning_moderator.name})", - value=f"{warning.reason}\nWarned at {warning.time}", + value=f"{warning.reason}\nWarned ", ) embed.color = discord.Color.blue() diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index f5e14ad5b..51c6ca961 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -102,11 +102,22 @@ async def lookup_user_command( .gino.all() ) + all_bans_by_user = ( + await self.bot.models.BanLog.query.where( + self.bot.models.BanLog.guild_id == str(interaction.guild.id) + ) + .where(self.bot.models.BanLog.banned_member == str(user.id)) + .order_by(self.bot.models.BanLog.ban_time.desc()) + .gino.all() + ) + embeds = [] for ban in recent_bans_by_user: - embeds.append( - await self.convert_ban_to_pretty_string(ban, f"{user.name} bans") + temp_embed = await self.convert_ban_to_pretty_string( + ban, f"{user.name} bans" ) + temp_embed.description += f"\n**Total bans:** {len(all_bans_by_user)}" + embeds.append(temp_embed) if len(embeds) == 0: embed = auxiliary.prepare_deny_embed( @@ -143,13 +154,22 @@ async def lookup_moderator_command( .gino.all() ) + all_bans_by_user = ( + await self.bot.models.BanLog.query.where( + self.bot.models.BanLog.guild_id == str(interaction.guild.id) + ) + .where(self.bot.models.BanLog.banning_moderator == str(moderator.id)) + .order_by(self.bot.models.BanLog.ban_time.desc()) + .gino.all() + ) + embeds = [] for ban in recent_bans_by_user: - embeds.append( - await self.convert_ban_to_pretty_string( - ban, f"Bans by {moderator.name}" - ) + temp_embed = await self.convert_ban_to_pretty_string( + ban, f"Bans by {moderator.name}" ) + temp_embed.description += f"\n**Total bans:** {len(all_bans_by_user)}" + embeds.append(temp_embed) if len(embeds) == 0: embed = auxiliary.prepare_deny_embed( diff --git a/techsupport_bot/commands/who.py b/techsupport_bot/commands/who.py index f26d1bd7c..c2806c9ad 100644 --- a/techsupport_bot/commands/who.py +++ b/techsupport_bot/commands/who.py @@ -224,7 +224,8 @@ async def modify_embed_for_mods( ) warning_str = "" for warning in warnings: - warning_str += f"{warning.reason} - {warning.time.date()}\n" + warning_moderator = await self.bot.fetch_user(int(warning.invoker_id)) + warning_str += f"{warning.reason} - \nWarned by: {warning_moderator.name}\n" if warning_str: embed.add_field( name="**Warnings**", diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 1a76115d9..3e8f24d52 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -367,6 +367,8 @@ def process_automod_violations( violations_list=all_punishments, ) + return final_action + def generate_automod_alert_embed( ctx: commands.Context, violations: str, action_taken: str From 706e6769fd1c6622a69591aaba7241d23999adfc Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 24 Jul 2024 20:08:22 -0400 Subject: [PATCH 28/99] Paste never fails, paste respected automod --- techsupport_bot/functions/__init__.py | 1 + techsupport_bot/functions/automod.py | 5 +- techsupport_bot/functions/paste.py | 72 +++++++++++++-------------- 3 files changed, 40 insertions(+), 38 deletions(-) diff --git a/techsupport_bot/functions/__init__.py b/techsupport_bot/functions/__init__.py index 26928a165..df256aa0b 100644 --- a/techsupport_bot/functions/__init__.py +++ b/techsupport_bot/functions/__init__.py @@ -1,3 +1,4 @@ """Functions are commandless cogs""" +from .automod import * from .nickname import * diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 3e8f24d52..4b2f85e10 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -320,6 +320,9 @@ async def on_raw_message_edit( def process_automod_violations( all_punishments: list[AutoModPunishment], ) -> AutoModAction: + if len(all_punishments) == 0: + return None + should_delete = False should_warn = False mute_duration = 0 @@ -396,7 +399,7 @@ def generate_automod_alert_embed( embed.add_field(name="Actions Taken", value=action_taken) embed.add_field(name="Channel", value=f"{ctx.channel.mention} ({ctx.channel.name})") embed.add_field(name="User", value=f"{ctx.author.mention} ({ctx.author.name})") - embed.add_field(name="Message", value=ctx.message.content, inline=False) + embed.add_field(name="Message", value=ctx.message.content[:1024], inline=False) embed.add_field(name="URL", value=ctx.message.jump_url, inline=False) embed.set_thumbnail(url=ALERT_ICON_URL) diff --git a/techsupport_bot/functions/paste.py b/techsupport_bot/functions/paste.py index 6c7fb69b8..955cf6e1e 100644 --- a/techsupport_bot/functions/paste.py +++ b/techsupport_bot/functions/paste.py @@ -10,6 +10,7 @@ from botlogging import LogContext, LogLevel from core import cogs, extensionconfig from discord.ext import commands +from functions import automod if TYPE_CHECKING: import bot @@ -114,7 +115,12 @@ async def response( if len(content) > config.extensions.paste.length_limit.value or content.count( "\n" ) > self.max_newlines(config.extensions.paste.length_limit.value): - await self.handle_length_alert(config, ctx, content) + if "automod" in config.get("enabled_extensions", []): + automod_actions = automod.run_all_checks(config, ctx.message) + automod_final = automod.process_automod_violations(automod_actions) + if automod_final and automod_final.delete_message: + return + await self.paste_message(config, ctx, content) def max_newlines(self: Self, max_length: int) -> int: """Gets a theoretical maximum number of new lines in a given message @@ -164,7 +170,7 @@ async def on_raw_message_edit( await self.response(config, ctx, message.content, None) - async def handle_length_alert( + async def paste_message( self: Self, config: munch.Munch, ctx: commands.Context, content: str ) -> None: """Moves message into a linx paste if it's too long @@ -174,6 +180,30 @@ async def handle_length_alert( ctx (commands.Context): The context where the original message was sent content (str): The string content of the flagged message """ + if not self.bot.file_config.api.api_url.linx: + await self.bot.logger.send_log( + message=( + f"Would have pasted message {ctx.message.id} but no linx url has been configured." + ), + level=LogLevel.WARNING, + channel=log_channel, + context=LogContext(guild=ctx.guild, channel=ctx.channel), + ) + return + + linx_embed = await self.create_linx_embed(config, ctx, content) + + if not linx_embed: + await self.bot.logger.send_log( + message=( + f"Would have pasted message {ctx.message.id} but uploading the file to linx failed." + ), + level=LogLevel.WARNING, + channel=log_channel, + context=LogContext(guild=ctx.guild, channel=ctx.channel), + ) + return + attachments: list[discord.File] = [] if ctx.message.attachments: total_attachment_size = 0 @@ -192,45 +222,13 @@ async def handle_length_alert( channel=log_channel, context=LogContext(guild=ctx.guild, channel=ctx.channel), ) - await ctx.message.delete() - - reason = "message too long (too many newlines or characters)" - - if not self.bot.file_config.api.api_url.linx: - await self.send_default_delete_response(config, ctx, content, reason) - return - - linx_embed = await self.create_linx_embed(config, ctx, content) - if not linx_embed: - await self.send_default_delete_response(config, ctx, content, reason) - # await self.send_alert(config, ctx, "Could not convert text to Linx paste") - return - await ctx.send( + message = await ctx.send( ctx.message.author.mention, embed=linx_embed, files=attachments[:10] ) - async def send_default_delete_response( - self: Self, - config: munch.Munch, - ctx: commands.Context, - content: str, - reason: str, - ) -> None: - """Sends a DM to a user containing a message that was deleted - - Args: - config (munch.Munch): The config of the guild where the message was sent - ctx (commands.Context): The context of the deleted message - content (str): The context of the deleted message - reason (str): The reason the message was deleted - """ - embed = discord.Embed( - title="Chat Protection", description=f"Message deleted. Reason: *{reason}*" - ) - embed.color = discord.Color.gold() - await ctx.send(ctx.message.author.mention, embed=embed) - await ctx.author.send(f"Deleted message: ```{content[:1994]}```") + if message: + await ctx.message.delete() async def create_linx_embed( self: Self, config: munch.Munch, ctx: commands.Context, content: str From 4f781104d8ea498361aa3138b26ef4ebf05c4c75 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 24 Jul 2024 20:28:48 -0400 Subject: [PATCH 29/99] Add automod to IRC --- techsupport_bot/commands/relay.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/techsupport_bot/commands/relay.py b/techsupport_bot/commands/relay.py index 481b0ff9d..9ec68a978 100644 --- a/techsupport_bot/commands/relay.py +++ b/techsupport_bot/commands/relay.py @@ -11,6 +11,7 @@ from bidict import bidict from core import auxiliary, cogs from discord.ext import commands +from functions import automod if TYPE_CHECKING: import bot @@ -411,6 +412,36 @@ async def send_message_from_irc(self: Self, split_message: dict[str, str]) -> No mentions = self.get_mentions( message=split_message["content"], channel=discord_channel ) + + config = self.bot.guild_configs[str(discord_channel.guild.id)] + if "automod" in config.get("enabled_extensions", []): + automod_actions = automod.run_only_string_checks( + config, split_message["content"] + ) + automod_final = automod.process_automod_violations(automod_actions) + if automod_final and automod_final.delete_message: + embed = discord.Embed(title="IRC Automod") + embed.description = ( + f"**Blocked message:** {split_message['content']}\n" + f"**Reason: ** {automod_final.violation_string}\n" + f"**Message sent by:** {split_message['username']} ({split_message['hostmask']})\n" + f"**In channel:** {split_message['channel']}" + ) + embed.color = discord.Color.red() + try: + alert_channel = discord_channel.guild.get_channel( + int(config.extensions.automod.alert_channel.value) + ) + except TypeError: + alert_channel = None + + if not alert_channel: + return + + await alert_channel.send(embed=embed) + + return + mentions_string = auxiliary.construct_mention_string(targets=mentions) embed = self.generate_sent_message_embed(split_message=split_message) From 0deda6a8f42878594813b436dd2f0b049f6d9912 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 24 Jul 2024 20:33:48 -0400 Subject: [PATCH 30/99] Final features --- techsupport_bot/core/moderation.py | 6 +++--- techsupport_bot/functions/automod.py | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index 079cf5452..6cab4e35f 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -204,7 +204,7 @@ async def send_command_usage_alert( if not alert_channel: return - embed = discord.Embed(title="Protect Alert") + embed = discord.Embed(title="Command Usage Alert") embed.add_field(name="Command", value=f"`{command}`", inline=False) embed.add_field( @@ -213,11 +213,11 @@ async def send_command_usage_alert( ) embed.add_field( name="Invoking User", - value=f"{interaction.user.display_name} ({interaction.user.mention})", + value=f"{interaction.user.display_name} ({interaction.user.mention}, {interaction.user.id})", ) embed.add_field( name="Target", - value=f"{target.display_name} ({target.mention})", + value=f"{target.display_name} ({target.mention}, {target.id})", ) embed.set_thumbnail(url=ALERT_ICON_URL) diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 4b2f85e10..6bdb8ef8e 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -398,7 +398,9 @@ def generate_automod_alert_embed( ) embed.add_field(name="Actions Taken", value=action_taken) embed.add_field(name="Channel", value=f"{ctx.channel.mention} ({ctx.channel.name})") - embed.add_field(name="User", value=f"{ctx.author.mention} ({ctx.author.name})") + embed.add_field( + name="User", value=f"{ctx.author.mention} ({ctx.author.name}, {ctx.author.id})" + ) embed.add_field(name="Message", value=ctx.message.content[:1024], inline=False) embed.add_field(name="URL", value=ctx.message.jump_url, inline=False) From aaa7fd33236f5d0db0fd23807c16757398eb8924 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 24 Jul 2024 20:42:33 -0400 Subject: [PATCH 31/99] Formatting --- techsupport_bot/commands/relay.py | 3 ++- techsupport_bot/commands/report.py | 4 +++- techsupport_bot/commands/who.py | 4 +++- techsupport_bot/core/moderation.py | 4 +++- techsupport_bot/functions/automod.py | 27 ++++++++++++++++++++++----- techsupport_bot/functions/paste.py | 8 +++++--- 6 files changed, 38 insertions(+), 12 deletions(-) diff --git a/techsupport_bot/commands/relay.py b/techsupport_bot/commands/relay.py index 9ec68a978..9e531f693 100644 --- a/techsupport_bot/commands/relay.py +++ b/techsupport_bot/commands/relay.py @@ -424,7 +424,8 @@ async def send_message_from_irc(self: Self, split_message: dict[str, str]) -> No embed.description = ( f"**Blocked message:** {split_message['content']}\n" f"**Reason: ** {automod_final.violation_string}\n" - f"**Message sent by:** {split_message['username']} ({split_message['hostmask']})\n" + f"**Message sent by:** {split_message['username']} " + f"({split_message['hostmask']})\n" f"**In channel:** {split_message['channel']}" ) embed.color = discord.Color.red() diff --git a/techsupport_bot/commands/report.py b/techsupport_bot/commands/report.py index 7e1526830..c1a9951c4 100644 --- a/techsupport_bot/commands/report.py +++ b/techsupport_bot/commands/report.py @@ -62,7 +62,9 @@ async def report_command( f"**Name:** {interaction.user.name} ({interaction.user.mention})\n" f"**Joined:** \n" f"**Created:** \n" - f"**Sent from:** {interaction.channel.mention} [Jump to context](https://discord.com/channels/{interaction.guild.id}/{interaction.channel.id}/{discord.utils.time_snowflake(datetime.datetime.utcnow())})" + f"**Sent from:** {interaction.channel.mention} [Jump to context]" + f"(https://discord.com/channels/{interaction.guild.id}/{interaction.channel.id}/" + f"{discord.utils.time_snowflake(datetime.datetime.utcnow())})" ), ) diff --git a/techsupport_bot/commands/who.py b/techsupport_bot/commands/who.py index c2806c9ad..9e8e4bf7f 100644 --- a/techsupport_bot/commands/who.py +++ b/techsupport_bot/commands/who.py @@ -225,7 +225,9 @@ async def modify_embed_for_mods( warning_str = "" for warning in warnings: warning_moderator = await self.bot.fetch_user(int(warning.invoker_id)) - warning_str += f"{warning.reason} - \nWarned by: {warning_moderator.name}\n" + warning_str += f"{warning.reason} - " + warning_str += f"\nWarned by: {warning_moderator.name}\n" + if warning_str: embed.add_field( name="**Warnings**", diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index 6cab4e35f..d6e189dad 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -213,7 +213,9 @@ async def send_command_usage_alert( ) embed.add_field( name="Invoking User", - value=f"{interaction.user.display_name} ({interaction.user.mention}, {interaction.user.id})", + value=( + f"{interaction.user.display_name} ({interaction.user.mention}, {interaction.user.id})" + ), ) embed.add_field( name="Target", diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 6bdb8ef8e..3eaf7651a 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -87,11 +87,13 @@ class AutoModPunishment: """This is a base class holding the violation and recommended actions Since automod is a framework, the actions can translate to different things - violation_str - The string of the policy broken. Should be displayed to user - recommend_delete - If the policy recommends deletion of the message - recommend_warn - If the policy recommends warning the user - recommend_mute - If the policy recommends muting the user. If so, the amount of seconds to mute for - is_silent - If the punishment should be silent + Attrs: + violation_str (str): The string of the policy broken. Should be displayed to user + recommend_delete (bool): If the policy recommends deletion of the message + recommend_warn (bool): If the policy recommends warning the user + recommend_mute (int): If the policy recommends muting the user. + If so, the amount of seconds to mute for. + is_silent (bool, optional): If the punishment should be silent. Defaults to False """ @@ -121,6 +123,21 @@ def score(self: Self) -> int: @dataclass class AutoModAction: + """The final summarized action for this automod violation + + Attrs: + warn (bool): Whether the user should be warned + delete_message (bool): Whether the message should be deleted + mute (bool): Whether the user should be muted + mute_duration (int): How many seconds to mute the user for + be_silent (bool): If the actions should be taken silently + action_string (str): The string of & separated actions taken + violation_string (str): The most severe punishment to be used as a reason + total_punishments (str): All the punishment reasons + violations_list (list[AutoModPunishment]): The list of original AutoModPunishment items + + """ + warn: bool delete_message: bool mute: bool diff --git a/techsupport_bot/functions/paste.py b/techsupport_bot/functions/paste.py index 955cf6e1e..8f9b4e946 100644 --- a/techsupport_bot/functions/paste.py +++ b/techsupport_bot/functions/paste.py @@ -180,10 +180,12 @@ async def paste_message( ctx (commands.Context): The context where the original message was sent content (str): The string content of the flagged message """ + log_channel = config.get("logging_channel") if not self.bot.file_config.api.api_url.linx: await self.bot.logger.send_log( message=( - f"Would have pasted message {ctx.message.id} but no linx url has been configured." + f"Would have pasted message {ctx.message.id}" + " but no linx url has been configured." ), level=LogLevel.WARNING, channel=log_channel, @@ -196,7 +198,8 @@ async def paste_message( if not linx_embed: await self.bot.logger.send_log( message=( - f"Would have pasted message {ctx.message.id} but uploading the file to linx failed." + f"Would have pasted message {ctx.message.id}" + " but uploading the file to linx failed." ), level=LogLevel.WARNING, channel=log_channel, @@ -213,7 +216,6 @@ async def paste_message( ) <= ctx.filesize_limit: attachments.append(await attch.to_file()) if (lf := len(ctx.message.attachments) - len(attachments)) != 0: - log_channel = config.get("logging_channel") await self.bot.logger.send_log( message=( f"Protect did not reupload {lf} file(s) due to file size limit." From f69e3ccd920101d521c187f170c63cf07a13aaec Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 24 Jul 2024 20:47:51 -0400 Subject: [PATCH 32/99] Formatting --- techsupport_bot/commands/moderator.py | 6 +++++- techsupport_bot/commands/modlog.py | 4 ++-- techsupport_bot/functions/automod.py | 12 +++++++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 96b02cc1d..fe63fd9a7 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -46,7 +46,11 @@ async def setup(bot: bot.TechSupportBot) -> None: class ProtectCommands(cogs.BaseCog): """The cog for all manual moderation activities - These are all slash commands""" + These are all slash commands + + Attrs: + warnings_group (app_commands.Group): The group for the /warning commands + """ warnings_group = app_commands.Group( name="warning", description="...", extras={"module": "moderator"} diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index 51c6ca961..1f2b6c023 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -283,7 +283,7 @@ async def log_ban( reason (str): The reason for the ban """ config = bot.guild_configs[str(guild.id)] - if not "modlog" in config.get("enabled_extensions", []): + if "modlog" not in config.get("enabled_extensions", []): return if not reason: @@ -339,7 +339,7 @@ async def log_unban( reason (str): The reason for the unban """ config = bot.guild_configs[str(guild.id)] - if not "modlog" in config.get("enabled_extensions", []): + if "modlog" not in config.get("enabled_extensions", []): return if not reason: diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 3eaf7651a..55c850ce5 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -94,6 +94,7 @@ class AutoModPunishment: recommend_mute (int): If the policy recommends muting the user. If so, the amount of seconds to mute for. is_silent (bool, optional): If the punishment should be silent. Defaults to False + score (int): The weighted score for sorting punishments """ @@ -337,6 +338,15 @@ async def on_raw_message_edit( def process_automod_violations( all_punishments: list[AutoModPunishment], ) -> AutoModAction: + """This processes a list of potentially many AutoModPunishments into a single + recommended action + + Args: + all_punishments (list[AutoModPunishment]): The list of punishments that should be taken + + Returns: + AutoModAction: The final summarized action that is recommended to be taken + """ if len(all_punishments) == 0: return None @@ -397,7 +407,7 @@ def generate_automod_alert_embed( Args: ctx (commands.Context): The context of the message that violated the automod - violations (list[AutoModPunishment]): The list of all violations of the automod + violations (str): The string form of ALL automod violations the user triggered action_taken (str): The text based action taken against the user Returns: From c49218e3fb525c24912ce8d4459d868b5febbe3f Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 24 Jul 2024 20:50:38 -0400 Subject: [PATCH 33/99] Removed print statement --- techsupport_bot/commands/moderator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index fe63fd9a7..f6ffe5c97 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -660,7 +660,6 @@ async def handle_warning_all( for warning in warnings: warning_moderator = await self.bot.fetch_user(int(warning.invoker_id)) - print(type(warning.time)) embed.add_field( name=f"Warning by {warning_moderator.display_name} ({warning_moderator.name})", value=f"{warning.reason}\nWarned ", From a534eddeaa611f172daed38dfe9df0ba717ee92a Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 27 Apr 2025 19:14:56 -0400 Subject: [PATCH 34/99] Fix some issues, rewrite note all --- techsupport_bot/commands/duck.py | 10 +- techsupport_bot/commands/modlog.py | 12 +- techsupport_bot/commands/notes.py | 410 +++++++++++++++++++++++++++ techsupport_bot/commands/purge.py | 5 +- techsupport_bot/commands/report.py | 3 +- techsupport_bot/commands/who.py | 17 +- techsupport_bot/commands/whois.py | 151 ++++++++++ techsupport_bot/core/moderation.py | 13 +- techsupport_bot/functions/automod.py | 5 + techsupport_bot/ui/pagination.py | 3 +- 10 files changed, 605 insertions(+), 24 deletions(-) create mode 100644 techsupport_bot/commands/notes.py create mode 100644 techsupport_bot/commands/whois.py diff --git a/techsupport_bot/commands/duck.py b/techsupport_bot/commands/duck.py index 761b1477d..8820e2ea5 100644 --- a/techsupport_bot/commands/duck.py +++ b/techsupport_bot/commands/duck.py @@ -395,11 +395,11 @@ def message_check( ): asyncio.create_task( moderation.mute_user( - user=message.author, - reason="Missed a duck", - duration=timedelta( - seconds=config.extensions.duck.cooldown.value - ), + user=message.author, + reason="Missed a duck", + duration=timedelta( + seconds=config.extensions.duck.cooldown.value + ), ) ) diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index 1f2b6c023..e6f5321cf 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -70,10 +70,14 @@ async def high_score_command(self: Self, interaction: discord.Interaction) -> No final_string = "" for index, (moderator_id, count) in enumerate(sorted_ban_frequency): moderator = await interaction.guild.fetch_member(int(moderator_id)) - final_string += ( - f"{index+1}. {moderator.display_name} " - f"{moderator.mention} ({moderator.id}) - ({count})\n" - ) + if moderator: + final_string += ( + f"{index+1}. {moderator.display_name} " + f"{moderator.mention} ({moderator.id}) - ({count})\n" + ) + else: + final_string += {f"{index+1}. Moderator left: {moderator_id}"} + embed.description = final_string embed.color = discord.Color.blue() await interaction.response.send_message(embed=embed) diff --git a/techsupport_bot/commands/notes.py b/techsupport_bot/commands/notes.py new file mode 100644 index 000000000..d9508f2bc --- /dev/null +++ b/techsupport_bot/commands/notes.py @@ -0,0 +1,410 @@ +"""Module for the who extension for the discord bot.""" + +from __future__ import annotations + +import datetime +import io +from typing import TYPE_CHECKING, Self + +import discord +import ui +import yaml +from botlogging import LogContext, LogLevel +from core import auxiliary, cogs, extensionconfig +from discord import app_commands +from discord.ext import commands + +if TYPE_CHECKING: + import bot + + +async def setup(bot: bot.TechSupportBot) -> None: + """Loading the Who plugin into the bot + + Args: + bot (bot.TechSupportBot): The bot object to register the cogs to + """ + + config = extensionconfig.ExtensionConfig() + config.add( + key="note_role", + datatype="str", + title="Note role", + description="The name of the role to be added when a note is added to a user", + default=None, + ) + config.add( + key="note_bypass", + datatype="list", + title="Note bypass list", + description=( + "A list of roles that shouldn't have notes set or the note role assigned" + ), + default=["Moderator"], + ) + config.add( + key="note_readers", + datatype="list", + title="Note Reader Roles", + description="Users with roles in this list will be able to use whois", + default=[], + ) + config.add( + key="note_writers", + datatype="list", + title="Note Writer Roles", + description="Users with roles in this list will be able to create or delete notes", + default=[], + ) + + await bot.add_cog(Notes(bot=bot, extension_name="notes")) + bot.add_extension_config("notes", config) + + +class Notes(cogs.BaseCog): + """Class to set up who for the extension. + + Attributes: + notes (app_commands.Group): The group for the /note commands + + """ + + notes: app_commands.Group = app_commands.Group( + name="notes", description="Command Group for the Notes Extension" + ) + + @staticmethod + async def is_reader(interaction: discord.Interaction) -> bool: + """Checks whether invoker can read notes. If at least one reader + role is not set, NO members can read notes + + Args: + interaction (discord.Interaction): The interaction in which the whois command occured + + Raises: + MissingAnyRole: Raised if the user is lacking any reader role, + but there are roles defined + AppCommandError: Raised if there are no note_readers set in the config + + Returns: + bool: True if the user can run, False if they cannot + """ + + config = interaction.client.guild_configs[str(interaction.guild.id)] + if reader_roles := config.extensions.who.note_readers.value: + roles = ( + discord.utils.get(interaction.guild.roles, name=role) + for role in reader_roles + ) + status = any((role in interaction.user.roles for role in roles)) + if not status: + raise app_commands.MissingAnyRole(reader_roles) + return True + + # Reader_roles are empty (not set) + message = "There aren't any `note_readers` roles set in the config!" + embed = auxiliary.prepare_deny_embed(message=message) + + await interaction.response.send_message(embed=embed, ephemeral=True) + + raise app_commands.AppCommandError(message) + + @staticmethod + async def is_writer(interaction: discord.Interaction) -> bool: + """Checks whether invoker can write notes. If at least one writer + role is not set, NO members can write notes + + Args: + interaction (discord.Interaction): The interaction in which the whois command occured + + Raises: + MissingAnyRole: Raised if the user is lacking any writer role, + but there are roles defined + AppCommandError: Raised if there are no note_writers set in the config + + Returns: + bool: True if the user can run, False if they cannot + """ + config = interaction.client.guild_configs[str(interaction.guild.id)] + if reader_roles := config.extensions.who.note_writers.value: + roles = ( + discord.utils.get(interaction.guild.roles, name=role) + for role in reader_roles + ) + status = any((role in interaction.user.roles for role in roles)) + if not status: + raise app_commands.MissingAnyRole(reader_roles) + return True + + # Reader_roles are empty (not set) + message = "There aren't any `note_writers` roles set in the config!" + embed = auxiliary.prepare_deny_embed(message=message) + + await interaction.response.send_message(embed=embed, ephemeral=True) + + raise app_commands.AppCommandError(message) + + @app_commands.check(is_writer) + @notes.command( + name="set", + description="Sets a note for a user, which can be read later from their whois", + extras={ + "brief": "Sets a note for a user", + "usage": "@user [note]", + "module": "notes", + }, + ) + async def set_note( + self: Self, interaction: discord.Interaction, user: discord.Member, body: str + ) -> None: + """Adds a new note to a user + This is the entrance for the /note set command + + Args: + interaction (discord.Interaction): The interaction that called this command + user (discord.Member): The member to add the note to + body (str): The contents of the note being created + """ + if interaction.user.id == user.id: + embed = auxiliary.prepare_deny_embed( + message="You cannot add a note for yourself" + ) + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + note = self.bot.models.UserNote( + user_id=str(user.id), + guild_id=str(interaction.guild.id), + author_id=str(interaction.user.id), + body=body, + ) + + config = self.bot.guild_configs[str(interaction.guild.id)] + + # Check to make sure notes are allowed to be assigned + for name in config.extensions.who.note_bypass.value: + role_check = discord.utils.get(interaction.guild.roles, name=name) + if not role_check: + continue + if role_check in getattr(user, "roles", []): + embed = auxiliary.prepare_deny_embed( + message=f"You cannot assign notes to `{user}` because " + + f"they have `{role_check}` role", + ) + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + await note.create() + + role = discord.utils.get( + interaction.guild.roles, name=config.extensions.who.note_role.value + ) + + if not role: + embed = auxiliary.prepare_confirm_embed( + message=f"Note created for `{user}`, but no note " + + "role is configured so no role was added", + ) + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + await user.add_roles(role, reason=f"First note was added by {interaction.user}") + + embed = auxiliary.prepare_confirm_embed(message=f"Note created for `{user}`") + await interaction.response.send_message(embed=embed, ephemeral=True) + + @app_commands.check(is_writer) + @notes.command( + name="clear", + description="Clears all existing notes for a user", + extras={ + "brief": "Clears all notes for a user", + "usage": "@user", + "module": "notes", + }, + ) + async def clear_notes( + self: Self, interaction: discord.Interaction, user: discord.Member + ) -> None: + """Clears all notes on a given user + This is the entrace for the /note clear command + + Args: + interaction (discord.Interaction): The interaction that called this command + user (discord.Member): The member to remove all notes from + """ + notes = await self.get_notes(user, interaction.guild) + + if not notes: + embed = auxiliary.prepare_deny_embed( + message="There are no notes for that user" + ) + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + await interaction.response.defer(ephemeral=True) + view = ui.Confirm() + + await view.send( + message=f"Are you sure you want to clear {len(notes)} notes?", + channel=interaction.channel, + author=interaction.user, + interaction=interaction, + ephemeral=True, + ) + + await view.wait() + if view.value is ui.ConfirmResponse.TIMEOUT: + return + if view.value is ui.ConfirmResponse.DENIED: + embed = auxiliary.prepare_deny_embed( + message=f"Notes for `{user}` were not cleared" + ) + await view.followup.send(embed=embed, ephemeral=True) + return + + for note in notes: + await note.delete() + + config = self.bot.guild_configs[str(interaction.guild.id)] + role = discord.utils.get( + interaction.guild.roles, name=config.extensions.who.note_role.value + ) + if role: + await user.remove_roles( + role, reason=f"Notes were cleared by {interaction.user}" + ) + + embed = auxiliary.prepare_confirm_embed(message=f"Notes cleared for `{user}`") + await view.followup.send(embed=embed, ephemeral=True) + + @app_commands.check(is_reader) + @notes.command( + name="all", + description="Gets all notes for a user instead of just new ones", + extras={ + "brief": "Gets all notes for a user", + "usage": "@user", + "module": "notes", + }, + ) + async def all_notes( + self: Self, interaction: discord.Interaction, member: discord.Member + ) -> None: + """Gets a file containing every note on a user + This is the entrance for the /note all command + + Args: + interaction (discord.Interaction): The interaction that called this command + member (discord.Member): The member to get all notes for + """ + notes = await self.get_notes(member, interaction.guild) + + embed = auxiliary.generate_basic_embed( + f"Notes for `{member.display_name}` (`{member.name}`)", + color=discord.Color.dark_blue(), + ) + embed.set_footer(text=f"{len(notes)} total notes.") + + embeds = [] + + if not notes: + embed.description = "No notes" + await interaction.response.send_message(embed=embed, ephemeral=True) + return + + for index, note in enumerate(notes): + if index % 10 == 0 and index > 0: + embeds.append(embed) + embed = auxiliary.generate_basic_embed( + f"Notes for `{member.display_name}` (`{member.name}`)", + color=discord.Color.dark_blue(), + ) + embed.set_footer(text=f"{len(notes)} total notes.") + author = interaction.guild.get_member(int(note.author_id)) or note.author_id + embed.add_field( + name=f"Note by {author}", + value=f"{note.body}\nNote added ", + ) + embeds.append(embed) + + await interaction.response.defer(ephemeral=True) + view = ui.PaginateView() + await view.send( + interaction.channel, interaction.user, embeds, interaction, True + ) + return + + note_output_data = [] + for note in notes: + author = interaction.guild.get_member(int(note.author_id)) or note.author_id + data = { + "body": note.body, + "from": str(author), + "at": str(note.updated), + } + note_output_data.append(data) + + yaml_file = discord.File( + io.StringIO(yaml.dump({"notes": note_output_data})), + filename=f"notes-for-{member.id}-{datetime.datetime.utcnow()}.yaml", + ) + + await interaction.response.send_message(file=yaml_file, ephemeral=True) + + async def get_notes( + self: Self, user: discord.Member, guild: discord.Guild + ) -> list[bot.models.UserNote]: + """Calls to the database to get a list of note database entries for a given user and guild + + Args: + user (discord.Member): The member to look for notes for + guild (discord.Guild): The guild to fetch the notes from + + Returns: + list[bot.models.UserNote]: The list of notes on the member/guild combo. + Will be an empty list if there are no notes + """ + user_notes = ( + await self.bot.models.UserNote.query.where( + self.bot.models.UserNote.user_id == str(user.id) + ) + .where(self.bot.models.UserNote.guild_id == str(guild.id)) + .order_by(self.bot.models.UserNote.updated.desc()) + .gino.all() + ) + + return user_notes + + # re-adds note role back to joining users + @commands.Cog.listener() + async def on_member_join(self: Self, member: discord.Member) -> None: + """Automatic listener to look at users when they join the guild. + This is to apply the note role back to joining users + + Args: + member (discord.Member): The member who has just joined + """ + config = self.bot.guild_configs[str(member.guild.id)] + if not self.extension_enabled(config): + return + + role = discord.utils.get( + member.guild.roles, name=config.extensions.who.note_role.value + ) + if not role: + return + + user_notes = await self.get_notes(member, member.guild) + if not user_notes: + return + + await member.add_roles(role, reason="Noted user has joined the guild") + + log_channel = config.get("logging_channel") + await self.bot.logger.send_log( + message=f"Found noted user with ID {member.id} joining - re-adding role", + level=LogLevel.INFO, + context=LogContext(guild=member.guild), + channel=log_channel, + ) diff --git a/techsupport_bot/commands/purge.py b/techsupport_bot/commands/purge.py index 62a389515..ed308dd51 100644 --- a/techsupport_bot/commands/purge.py +++ b/techsupport_bot/commands/purge.py @@ -58,14 +58,14 @@ async def purge_command( if amount <= 0 or amount > config.extensions.purge.max_purge_amount.value: embed = auxiliary.prepare_deny_embed( - message="This is an invalid amount of messages to purge", + message=f"Messages to purge must be between 0 and {config.extensions.purge.max_purge_amount.value}", ) await interaction.response.send_message(embed=embed, ephemeral=True) return if duration_minutes and duration_minutes < 0: embed = auxiliary.prepare_deny_embed( - message="This is an invalid duration", + message="Message age must be older than 0 minutes", ) await interaction.response.send_message(embed=embed, ephemeral=True) return @@ -86,5 +86,4 @@ async def purge_command( interaction=interaction, command=f"/purge amount: {amount}, duration: {duration_minutes}", guild=interaction.guild, - target=interaction.user, ) diff --git a/techsupport_bot/commands/report.py b/techsupport_bot/commands/report.py index c1a9951c4..f7ad98316 100644 --- a/techsupport_bot/commands/report.py +++ b/techsupport_bot/commands/report.py @@ -53,7 +53,8 @@ async def report_command( embed = discord.Embed(title="New Report", description=report_str) embed.color = discord.Color.red() embed.set_author( - name=interaction.user.name, icon_url=interaction.user.avatar.url + name=interaction.user.name, + icon_url=interaction.user.avatar.url or interaction.user.default_avatar.url, ) embed.add_field( diff --git a/techsupport_bot/commands/who.py b/techsupport_bot/commands/who.py index 6e1972672..e07c32e16 100644 --- a/techsupport_bot/commands/who.py +++ b/techsupport_bot/commands/who.py @@ -223,14 +223,21 @@ async def modify_embed_for_mods( .gino.all() ) warning_str = "" - for warning in warnings: - warning_moderator = await self.bot.fetch_user(int(warning.invoker_id)) - warning_str += f"{warning.reason} - " - warning_str += f"\nWarned by: {warning_moderator.name}\n" + for warning in warnings[-3:]: + warning_moderator_name = "unknown" + if warning.invoker_id: + warning_moderator = await self.bot.fetch_user(int(warning.invoker_id)) + if warning_moderator: + warning_moderator_name = warning_moderator.name + + warning_str += ( + f"- {warning.reason} - . " + ) + warning_str += f"Warned by: {warning_moderator_name}\n" if warning_str: embed.add_field( - name="**Warnings**", + name=f"**Warnings ({len(warnings)} total)**", value=warning_str, inline=True, ) diff --git a/techsupport_bot/commands/whois.py b/techsupport_bot/commands/whois.py new file mode 100644 index 000000000..1d333e71d --- /dev/null +++ b/techsupport_bot/commands/whois.py @@ -0,0 +1,151 @@ + + + @staticmethod + async def is_reader(interaction: discord.Interaction) -> bool: + """Checks whether invoker can read notes. If at least one reader + role is not set, NO members can read notes + + Args: + interaction (discord.Interaction): The interaction in which the whois command occured + + Raises: + MissingAnyRole: Raised if the user is lacking any reader role, + but there are roles defined + AppCommandError: Raised if there are no note_readers set in the config + + Returns: + bool: True if the user can run, False if they cannot + """ + + config = interaction.client.guild_configs[str(interaction.guild.id)] + if reader_roles := config.extensions.who.note_readers.value: + roles = ( + discord.utils.get(interaction.guild.roles, name=role) + for role in reader_roles + ) + status = any((role in interaction.user.roles for role in roles)) + if not status: + raise app_commands.MissingAnyRole(reader_roles) + return True + + # Reader_roles are empty (not set) + message = "There aren't any `note_readers` roles set in the config!" + embed = auxiliary.prepare_deny_embed(message=message) + + await interaction.response.send_message(embed=embed, ephemeral=True) + + raise app_commands.AppCommandError(message) + + @app_commands.check(is_reader) + @app_commands.command( + name="whois", + description="Gets Discord user information", + extras={"brief": "Gets user data", "usage": "@user", "module": "who"}, + ) + async def get_note( + self: Self, interaction: discord.Interaction, user: discord.Member + ) -> None: + """This is the base of the /whois command + + Args: + interaction (discord.Interaction): The interaction that called this command + user (discord.Member): The member to lookup. Will not work on discord.User + """ + embed = discord.Embed( + title=f"User info for `{user}`", + description="**Note: this is a bot account!**" if user.bot else "", + ) + + embed.set_thumbnail(url=user.display_avatar.url) + + embed.add_field(name="Created at", value=user.created_at.replace(microsecond=0)) + embed.add_field(name="Joined at", value=user.joined_at.replace(microsecond=0)) + embed.add_field( + name="Status", value=interaction.guild.get_member(user.id).status + ) + embed.add_field(name="Nickname", value=user.display_name) + + role_string = ", ".join(role.name for role in user.roles[1:]) + embed.add_field(name="Roles", value=role_string or "No roles") + + # Adds special information only visible to mods + if interaction.permissions.kick_members: + embed = await self.modify_embed_for_mods(interaction, user, embed) + + user_notes = await self.get_notes(user, interaction.guild) + total_notes = 0 + if user_notes: + total_notes = len(user_notes) + user_notes = user_notes[:3] + embed.set_footer(text=f"{total_notes} total notes") + embed.color = discord.Color.dark_blue() + + for note in user_notes: + author = interaction.guild.get_member(int(note.author_id)) or note.author_id + embed.add_field( + name=f"Note from {author} ({note.updated.date()})", + value=f"*{note.body}*" or "*None*", + inline=False, + ) + + await interaction.response.send_message(embed=embed, ephemeral=True) + + async def modify_embed_for_mods( + self: Self, + interaction: discord.Interaction, + user: discord.Member, + embed: discord.Embed, + ) -> discord.Embed: + """Makes modifications to the whois embed to add mod only information + + Args: + interaction (discord.Interaction): The interaction where the /whois command was called + user (discord.Member): The user being looked up + embed (discord.Embed): The embed already filled with whois information + + Returns: + discord.Embed: The embed with mod only information added + """ + # If the user has warnings, add them + warnings = ( + await self.bot.models.Warning.query.where( + self.bot.models.Warning.user_id == str(user.id) + ) + .where(self.bot.models.Warning.guild_id == str(interaction.guild.id)) + .gino.all() + ) + warning_str = "" + for warning in warnings[-3:]: + warning_moderator_name = "unknown" + if warning.invoker_id: + warning_moderator = await self.bot.fetch_user(int(warning.invoker_id)) + if warning_moderator: + warning_moderator_name = warning_moderator.name + + warning_str += ( + f"- {warning.reason} - . " + ) + warning_str += f"Warned by: {warning_moderator_name}\n" + + if warning_str: + embed.add_field( + name=f"**Warnings ({len(warnings)} total)**", + value=warning_str, + inline=True, + ) + + # If the user has a pending application, show it + # If the user is banned from making applications, show it + application_cog = interaction.client.get_cog("ApplicationManager") + if application_cog: + has_application = await application_cog.search_for_pending_application(user) + is_banned = await application_cog.get_ban_entry(user) + embed.add_field( + name="Application information:", + value=( + f"Has pending application: {bool(has_application)}\nIs banned from" + f" making applications: {bool(is_banned)}" + ), + inline=True, + ) + return embed diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index 52a5ffd0a..4b4668409 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -177,7 +177,7 @@ async def send_command_usage_alert( interaction: discord.Interaction, command: str, guild: discord.Guild, - target: discord.Member, + target: discord.Member = None, ) -> None: """Sends a usage alert to the protect events channel, if configured @@ -210,16 +210,19 @@ async def send_command_usage_alert( name="Channel", value=f"{interaction.channel.name} ({interaction.channel.mention})", ) + embed.add_field( name="Invoking User", value=( f"{interaction.user.display_name} ({interaction.user.mention}, {interaction.user.id})" ), ) - embed.add_field( - name="Target", - value=f"{target.display_name} ({target.mention}, {target.id})", - ) + + if target: + embed.add_field( + name="Target", + value=f"{target.display_name} ({target.mention}, {target.id})", + ) embed.set_thumbnail(url=ALERT_ICON_URL) embed.color = discord.Color.red() diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index cc32d79c5..3d237550d 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -200,6 +200,11 @@ async def response( content (str): The string content of the message result (bool): What the match() function returned """ + + # If user outranks bot, do nothing + if ctx.message.author.top_role >= ctx.channel.guild.me.top_role: + return + all_punishments = run_all_checks(config, ctx.message) if len(all_punishments) == 0: diff --git a/techsupport_bot/ui/pagination.py b/techsupport_bot/ui/pagination.py index 874290f7e..042884076 100644 --- a/techsupport_bot/ui/pagination.py +++ b/techsupport_bot/ui/pagination.py @@ -38,6 +38,7 @@ async def send( author: discord.Member, data: list[str | discord.Embed], interaction: discord.Interaction | None = None, + ephemeral: bool = False, ) -> None: """Entry point for PaginateView @@ -57,7 +58,7 @@ async def send( if interaction: self.followup = interaction.followup - self.message = await self.followup.send(view=self) + self.message = await self.followup.send(view=self, ephemeral=ephemeral) else: self.message = await channel.send(view=self) From 625f314d88edbd38f6807e0ebd1361cc298aca82 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 27 Apr 2025 21:20:32 -0400 Subject: [PATCH 35/99] Move some functions around, make whois have notes on page 2 --- techsupport_bot/commands/__init__.py | 1 + techsupport_bot/commands/moderator.py | 2 +- techsupport_bot/commands/notes.py | 256 +++++++++++++------------- techsupport_bot/commands/who.py | 2 + techsupport_bot/commands/whois.py | 110 +++++------ 5 files changed, 178 insertions(+), 193 deletions(-) diff --git a/techsupport_bot/commands/__init__.py b/techsupport_bot/commands/__init__.py index dafec06a1..a11c06a98 100644 --- a/techsupport_bot/commands/__init__.py +++ b/techsupport_bot/commands/__init__.py @@ -18,6 +18,7 @@ from .mock import * from .moderator import * from .modlog import * +from .notes import * from .relay import * from .roll import * from .wyr import * diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index f6ffe5c97..a20cf3fb1 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -655,7 +655,7 @@ async def handle_warning_all( return embed = discord.Embed( - title=f"Warnings for {target.display_name} ({target.name})" + title=f"Warnings for `{target.display_name}` (`{target.name}`)" ) for warning in warnings: diff --git a/techsupport_bot/commands/notes.py b/techsupport_bot/commands/notes.py index d9508f2bc..1b508c18c 100644 --- a/techsupport_bot/commands/notes.py +++ b/techsupport_bot/commands/notes.py @@ -61,88 +61,88 @@ async def setup(bot: bot.TechSupportBot) -> None: bot.add_extension_config("notes", config) -class Notes(cogs.BaseCog): - """Class to set up who for the extension. +async def is_reader(interaction: discord.Interaction) -> bool: + """Checks whether invoker can read notes. If at least one reader + role is not set, NO members can read notes - Attributes: - notes (app_commands.Group): The group for the /note commands + Args: + interaction (discord.Interaction): The interaction in which the whois command occured + Raises: + MissingAnyRole: Raised if the user is lacking any reader role, + but there are roles defined + AppCommandError: Raised if there are no note_readers set in the config + + Returns: + bool: True if the user can run, False if they cannot """ - notes: app_commands.Group = app_commands.Group( - name="notes", description="Command Group for the Notes Extension" - ) + config = interaction.client.guild_configs[str(interaction.guild.id)] + if reader_roles := config.extensions.who.note_readers.value: + roles = ( + discord.utils.get(interaction.guild.roles, name=role) + for role in reader_roles + ) + status = any((role in interaction.user.roles for role in roles)) + if not status: + raise app_commands.MissingAnyRole(reader_roles) + return True - @staticmethod - async def is_reader(interaction: discord.Interaction) -> bool: - """Checks whether invoker can read notes. If at least one reader - role is not set, NO members can read notes + # Reader_roles are empty (not set) + message = "There aren't any `note_readers` roles set in the config!" + embed = auxiliary.prepare_deny_embed(message=message) - Args: - interaction (discord.Interaction): The interaction in which the whois command occured + await interaction.response.send_message(embed=embed, ephemeral=True) - Raises: - MissingAnyRole: Raised if the user is lacking any reader role, - but there are roles defined - AppCommandError: Raised if there are no note_readers set in the config + raise app_commands.AppCommandError(message) - Returns: - bool: True if the user can run, False if they cannot - """ - config = interaction.client.guild_configs[str(interaction.guild.id)] - if reader_roles := config.extensions.who.note_readers.value: - roles = ( - discord.utils.get(interaction.guild.roles, name=role) - for role in reader_roles - ) - status = any((role in interaction.user.roles for role in roles)) - if not status: - raise app_commands.MissingAnyRole(reader_roles) - return True +async def is_writer(interaction: discord.Interaction) -> bool: + """Checks whether invoker can write notes. If at least one writer + role is not set, NO members can write notes - # Reader_roles are empty (not set) - message = "There aren't any `note_readers` roles set in the config!" - embed = auxiliary.prepare_deny_embed(message=message) + Args: + interaction (discord.Interaction): The interaction in which the whois command occured - await interaction.response.send_message(embed=embed, ephemeral=True) + Raises: + MissingAnyRole: Raised if the user is lacking any writer role, + but there are roles defined + AppCommandError: Raised if there are no note_writers set in the config + + Returns: + bool: True if the user can run, False if they cannot + """ + config = interaction.client.guild_configs[str(interaction.guild.id)] + if reader_roles := config.extensions.who.note_writers.value: + roles = ( + discord.utils.get(interaction.guild.roles, name=role) + for role in reader_roles + ) + status = any((role in interaction.user.roles for role in roles)) + if not status: + raise app_commands.MissingAnyRole(reader_roles) + return True - raise app_commands.AppCommandError(message) + # Reader_roles are empty (not set) + message = "There aren't any `note_writers` roles set in the config!" + embed = auxiliary.prepare_deny_embed(message=message) - @staticmethod - async def is_writer(interaction: discord.Interaction) -> bool: - """Checks whether invoker can write notes. If at least one writer - role is not set, NO members can write notes + await interaction.response.send_message(embed=embed, ephemeral=True) - Args: - interaction (discord.Interaction): The interaction in which the whois command occured + raise app_commands.AppCommandError(message) - Raises: - MissingAnyRole: Raised if the user is lacking any writer role, - but there are roles defined - AppCommandError: Raised if there are no note_writers set in the config - Returns: - bool: True if the user can run, False if they cannot - """ - config = interaction.client.guild_configs[str(interaction.guild.id)] - if reader_roles := config.extensions.who.note_writers.value: - roles = ( - discord.utils.get(interaction.guild.roles, name=role) - for role in reader_roles - ) - status = any((role in interaction.user.roles for role in roles)) - if not status: - raise app_commands.MissingAnyRole(reader_roles) - return True +class Notes(cogs.BaseCog): + """Class to set up who for the extension. - # Reader_roles are empty (not set) - message = "There aren't any `note_writers` roles set in the config!" - embed = auxiliary.prepare_deny_embed(message=message) + Attributes: + notes (app_commands.Group): The group for the /note commands - await interaction.response.send_message(embed=embed, ephemeral=True) + """ - raise app_commands.AppCommandError(message) + notes: app_commands.Group = app_commands.Group( + name="notes", description="Command Group for the Notes Extension" + ) @app_commands.check(is_writer) @notes.command( @@ -300,33 +300,7 @@ async def all_notes( """ notes = await self.get_notes(member, interaction.guild) - embed = auxiliary.generate_basic_embed( - f"Notes for `{member.display_name}` (`{member.name}`)", - color=discord.Color.dark_blue(), - ) - embed.set_footer(text=f"{len(notes)} total notes.") - - embeds = [] - - if not notes: - embed.description = "No notes" - await interaction.response.send_message(embed=embed, ephemeral=True) - return - - for index, note in enumerate(notes): - if index % 10 == 0 and index > 0: - embeds.append(embed) - embed = auxiliary.generate_basic_embed( - f"Notes for `{member.display_name}` (`{member.name}`)", - color=discord.Color.dark_blue(), - ) - embed.set_footer(text=f"{len(notes)} total notes.") - author = interaction.guild.get_member(int(note.author_id)) or note.author_id - embed.add_field( - name=f"Note by {author}", - value=f"{note.body}\nNote added ", - ) - embeds.append(embed) + embeds = build_embeds(interaction.guild, member, notes) await interaction.response.defer(ephemeral=True) view = ui.PaginateView() @@ -335,47 +309,6 @@ async def all_notes( ) return - note_output_data = [] - for note in notes: - author = interaction.guild.get_member(int(note.author_id)) or note.author_id - data = { - "body": note.body, - "from": str(author), - "at": str(note.updated), - } - note_output_data.append(data) - - yaml_file = discord.File( - io.StringIO(yaml.dump({"notes": note_output_data})), - filename=f"notes-for-{member.id}-{datetime.datetime.utcnow()}.yaml", - ) - - await interaction.response.send_message(file=yaml_file, ephemeral=True) - - async def get_notes( - self: Self, user: discord.Member, guild: discord.Guild - ) -> list[bot.models.UserNote]: - """Calls to the database to get a list of note database entries for a given user and guild - - Args: - user (discord.Member): The member to look for notes for - guild (discord.Guild): The guild to fetch the notes from - - Returns: - list[bot.models.UserNote]: The list of notes on the member/guild combo. - Will be an empty list if there are no notes - """ - user_notes = ( - await self.bot.models.UserNote.query.where( - self.bot.models.UserNote.user_id == str(user.id) - ) - .where(self.bot.models.UserNote.guild_id == str(guild.id)) - .order_by(self.bot.models.UserNote.updated.desc()) - .gino.all() - ) - - return user_notes - # re-adds note role back to joining users @commands.Cog.listener() async def on_member_join(self: Self, member: discord.Member) -> None: @@ -395,7 +328,7 @@ async def on_member_join(self: Self, member: discord.Member) -> None: if not role: return - user_notes = await self.get_notes(member, member.guild) + user_notes = await get_notes(bot, member, member.guild) if not user_notes: return @@ -408,3 +341,62 @@ async def on_member_join(self: Self, member: discord.Member) -> None: context=LogContext(guild=member.guild), channel=log_channel, ) + + +def build_embeds( + guild: discord.Guild, + member: discord.Member, + notes: list[bot.models.UserNote], +) -> list[discord.Embed]: + embed = auxiliary.generate_basic_embed( + f"Notes for `{member.display_name}` (`{member.name}`)", + color=discord.Color.dark_blue(), + ) + embed.set_footer(text=f"{len(notes)} total notes.") + + embeds = [] + + if not notes: + embed.description = "No notes" + return [embed] + + for index, note in enumerate(notes): + if index % 6 == 0 and index > 0: + embeds.append(embed) + embed = auxiliary.generate_basic_embed( + f"Notes for `{member.display_name}` (`{member.name}`)", + color=discord.Color.dark_blue(), + ) + embed.set_footer(text=f"{len(notes)} total notes.") + author = guild.get_member(int(note.author_id)) or note.author_id + embed.add_field( + name=f"Note by {author}", + value=f"{note.body}\nNote added ", + ) + embeds.append(embed) + return embeds + + +async def get_notes( + bot: bot.TechSupportBot, user: discord.Member, guild: discord.Guild +) -> list[bot.models.UserNote]: + """Calls to the database to get a list of note database entries for a given user and guild + + Args: + user (discord.Member): The member to look for notes for + guild (discord.Guild): The guild to fetch the notes from + + Returns: + list[bot.models.UserNote]: The list of notes on the member/guild combo. + Will be an empty list if there are no notes + """ + user_notes = ( + await bot.models.UserNote.query.where( + bot.models.UserNote.user_id == str(user.id) + ) + .where(bot.models.UserNote.guild_id == str(guild.id)) + .order_by(bot.models.UserNote.updated.desc()) + .gino.all() + ) + + return user_notes diff --git a/techsupport_bot/commands/who.py b/techsupport_bot/commands/who.py index e07c32e16..0cc851c11 100644 --- a/techsupport_bot/commands/who.py +++ b/techsupport_bot/commands/who.py @@ -25,6 +25,8 @@ async def setup(bot: bot.TechSupportBot) -> None: bot (bot.TechSupportBot): The bot object to register the cogs to """ + return + config = extensionconfig.ExtensionConfig() config.add( key="note_role", diff --git a/techsupport_bot/commands/whois.py b/techsupport_bot/commands/whois.py index 1d333e71d..dc7b9ee82 100644 --- a/techsupport_bot/commands/whois.py +++ b/techsupport_bot/commands/whois.py @@ -1,49 +1,42 @@ +"""Module for the who extension for the discord bot.""" +from __future__ import annotations - @staticmethod - async def is_reader(interaction: discord.Interaction) -> bool: - """Checks whether invoker can read notes. If at least one reader - role is not set, NO members can read notes +import datetime +import io +from typing import TYPE_CHECKING, Self - Args: - interaction (discord.Interaction): The interaction in which the whois command occured +import discord +import ui +import yaml +from botlogging import LogContext, LogLevel +from commands import notes +from core import auxiliary, cogs +from discord import app_commands +from discord.ext import commands - Raises: - MissingAnyRole: Raised if the user is lacking any reader role, - but there are roles defined - AppCommandError: Raised if there are no note_readers set in the config +if TYPE_CHECKING: + import bot - Returns: - bool: True if the user can run, False if they cannot - """ - config = interaction.client.guild_configs[str(interaction.guild.id)] - if reader_roles := config.extensions.who.note_readers.value: - roles = ( - discord.utils.get(interaction.guild.roles, name=role) - for role in reader_roles - ) - status = any((role in interaction.user.roles for role in roles)) - if not status: - raise app_commands.MissingAnyRole(reader_roles) - return True +async def setup(bot: bot.TechSupportBot) -> None: + """Loading the Who plugin into the bot - # Reader_roles are empty (not set) - message = "There aren't any `note_readers` roles set in the config!" - embed = auxiliary.prepare_deny_embed(message=message) + Args: + bot (bot.TechSupportBot): The bot object to register the cogs to + """ + await bot.add_cog(Whois(bot=bot, extension_name="whois")) - await interaction.response.send_message(embed=embed, ephemeral=True) - raise app_commands.AppCommandError(message) +class Whois(cogs.BaseCog): - @app_commands.check(is_reader) @app_commands.command( name="whois", description="Gets Discord user information", extras={"brief": "Gets user data", "usage": "@user", "module": "who"}, ) - async def get_note( - self: Self, interaction: discord.Interaction, user: discord.Member + async def whois_command( + self: Self, interaction: discord.Interaction, member: discord.Member ) -> None: """This is the base of the /whois command @@ -51,44 +44,41 @@ async def get_note( interaction (discord.Interaction): The interaction that called this command user (discord.Member): The member to lookup. Will not work on discord.User """ - embed = discord.Embed( - title=f"User info for `{user}`", - description="**Note: this is a bot account!**" if user.bot else "", + embed = auxiliary.generate_basic_embed( + title=f"User info for `{member.display_name}` (`{member.name}`)", + description="**Note: this is a bot account!**" if member.bot else "", + color=discord.Color.dark_blue(), + url=member.display_avatar.url, ) - embed.set_thumbnail(url=user.display_avatar.url) - - embed.add_field(name="Created at", value=user.created_at.replace(microsecond=0)) - embed.add_field(name="Joined at", value=user.joined_at.replace(microsecond=0)) embed.add_field( - name="Status", value=interaction.guild.get_member(user.id).status + name="Created at", value=member.created_at.replace(microsecond=0) + ) + embed.add_field(name="Joined at", value=member.joined_at.replace(microsecond=0)) + embed.add_field( + name="Status", value=interaction.guild.get_member(member.id).status ) - embed.add_field(name="Nickname", value=user.display_name) + embed.add_field(name="Nickname", value=member.display_name) - role_string = ", ".join(role.name for role in user.roles[1:]) + role_string = ", ".join(role.name for role in member.roles[1:]) embed.add_field(name="Roles", value=role_string or "No roles") - # Adds special information only visible to mods if interaction.permissions.kick_members: - embed = await self.modify_embed_for_mods(interaction, user, embed) - - user_notes = await self.get_notes(user, interaction.guild) - total_notes = 0 - if user_notes: - total_notes = len(user_notes) - user_notes = user_notes[:3] - embed.set_footer(text=f"{total_notes} total notes") - embed.color = discord.Color.dark_blue() - - for note in user_notes: - author = interaction.guild.get_member(int(note.author_id)) or note.author_id - embed.add_field( - name=f"Note from {author} ({note.updated.date()})", - value=f"*{note.body}*" or "*None*", - inline=False, - ) + embed = await self.modify_embed_for_mods(interaction, member, embed) - await interaction.response.send_message(embed=embed, ephemeral=True) + embeds = [embed] + + if await notes.is_reader(interaction): + all_notes = await notes.get_notes(self.bot, member, interaction.guild) + notes_embeds = notes.build_embeds(interaction.guild, member, all_notes) + embeds.append(notes_embeds[0]) + + await interaction.response.defer(ephemeral=True) + view = ui.PaginateView() + await view.send( + interaction.channel, interaction.user, embeds, interaction, True + ) + return async def modify_embed_for_mods( self: Self, From a44332c3baa2ed714b2f619148f4f588e01dff81 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 27 Apr 2025 21:56:52 -0400 Subject: [PATCH 36/99] Add warnings as page 3 in whois, fix some bugs --- techsupport_bot/commands/moderator.py | 62 +++++++++++++++++++-------- techsupport_bot/commands/notes.py | 34 ++------------- techsupport_bot/commands/whois.py | 53 +++++++++-------------- techsupport_bot/core/moderation.py | 25 +++++++++++ 4 files changed, 94 insertions(+), 80 deletions(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index a20cf3fb1..f46efcc1a 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -647,27 +647,14 @@ async def handle_warning_all( self.bot, target, interaction.guild ) - if not warnings: - embed = auxiliary.prepare_deny_embed( - message=f"No warnings could be found on {target}" - ) - await interaction.response.send_message(embed=embed, ephemeral=True) - return + embeds = build_warning_embeds(interaction.guild, target, warnings) - embed = discord.Embed( - title=f"Warnings for `{target.display_name}` (`{target.name}`)" + await interaction.response.defer(ephemeral=True) + view = ui.PaginateView() + await view.send( + interaction.channel, interaction.user, embeds, interaction, True ) - for warning in warnings: - warning_moderator = await self.bot.fetch_user(int(warning.invoker_id)) - embed.add_field( - name=f"Warning by {warning_moderator.display_name} ({warning_moderator.name})", - value=f"{warning.reason}\nWarned ", - ) - embed.color = discord.Color.blue() - - await interaction.response.send_message(embed=embed, ephemeral=True) - # Helper functions async def permission_check( @@ -772,3 +759,42 @@ def generate_response_embed( embed.color = discord.Color.gold() return embed + + +def build_warning_embeds( + guild: discord.Guild, + member: discord.Member, + warnings: list[bot.models.UserNote], +) -> list[discord.Embed]: + embed = auxiliary.generate_basic_embed( + f"Warnings for `{member.display_name}` (`{member.name}`)", + color=discord.Color.dark_blue(), + ) + embed.set_footer(text=f"{len(warnings)} total warns.") + + embeds = [] + + if not warnings: + embed.description = "No warnings" + return [embed] + + for index, warn in enumerate(warnings): + if index % 6 == 0 and index > 0: + embeds.append(embed) + embed = auxiliary.generate_basic_embed( + f"Warnings for `{member.display_name}` (`{member.name}`)", + color=discord.Color.dark_blue(), + ) + embed.set_footer(text=f"{len(warnings)} total warns.") + warn_author = "Unknown" + if warn.invoker_id: + warn_author = warn.invoker_id + author = guild.get_member(int(warn.invoker_id)) + if author: + warn_author = author.name + embed.add_field( + name=f"Warned by {warn_author}", + value=f"{warn.reason}\nWarned ", + ) + embeds.append(embed) + return embeds diff --git a/techsupport_bot/commands/notes.py b/techsupport_bot/commands/notes.py index 1b508c18c..0f137c435 100644 --- a/techsupport_bot/commands/notes.py +++ b/techsupport_bot/commands/notes.py @@ -10,7 +10,7 @@ import ui import yaml from botlogging import LogContext, LogLevel -from core import auxiliary, cogs, extensionconfig +from core import auxiliary, cogs, extensionconfig, moderation from discord import app_commands from discord.ext import commands @@ -300,14 +300,13 @@ async def all_notes( """ notes = await self.get_notes(member, interaction.guild) - embeds = build_embeds(interaction.guild, member, notes) + embeds = build_note_embeds(interaction.guild, member, notes) await interaction.response.defer(ephemeral=True) view = ui.PaginateView() await view.send( interaction.channel, interaction.user, embeds, interaction, True ) - return # re-adds note role back to joining users @commands.Cog.listener() @@ -328,7 +327,7 @@ async def on_member_join(self: Self, member: discord.Member) -> None: if not role: return - user_notes = await get_notes(bot, member, member.guild) + user_notes = await moderation.get_all_notes(bot, member, member.guild) if not user_notes: return @@ -343,7 +342,7 @@ async def on_member_join(self: Self, member: discord.Member) -> None: ) -def build_embeds( +def build_note_embeds( guild: discord.Guild, member: discord.Member, notes: list[bot.models.UserNote], @@ -375,28 +374,3 @@ def build_embeds( ) embeds.append(embed) return embeds - - -async def get_notes( - bot: bot.TechSupportBot, user: discord.Member, guild: discord.Guild -) -> list[bot.models.UserNote]: - """Calls to the database to get a list of note database entries for a given user and guild - - Args: - user (discord.Member): The member to look for notes for - guild (discord.Guild): The guild to fetch the notes from - - Returns: - list[bot.models.UserNote]: The list of notes on the member/guild combo. - Will be an empty list if there are no notes - """ - user_notes = ( - await bot.models.UserNote.query.where( - bot.models.UserNote.user_id == str(user.id) - ) - .where(bot.models.UserNote.guild_id == str(guild.id)) - .order_by(bot.models.UserNote.updated.desc()) - .gino.all() - ) - - return user_notes diff --git a/techsupport_bot/commands/whois.py b/techsupport_bot/commands/whois.py index dc7b9ee82..1eea077a0 100644 --- a/techsupport_bot/commands/whois.py +++ b/techsupport_bot/commands/whois.py @@ -10,8 +10,8 @@ import ui import yaml from botlogging import LogContext, LogLevel -from commands import notes -from core import auxiliary, cogs +from commands import moderator, notes +from core import auxiliary, cogs, moderation from discord import app_commands from discord.ext import commands @@ -69,10 +69,27 @@ async def whois_command( embeds = [embed] if await notes.is_reader(interaction): - all_notes = await notes.get_notes(self.bot, member, interaction.guild) - notes_embeds = notes.build_embeds(interaction.guild, member, all_notes) + all_notes = await moderation.get_all_notes( + self.bot, member, interaction.guild + ) + notes_embeds = notes.build_note_embeds(interaction.guild, member, all_notes) + notes_embeds[0].description = ( + f"Showing {min(len(all_notes), 6)}/{len(all_notes)} notes" + ) embeds.append(notes_embeds[0]) + if interaction.permissions.kick_members: + all_warnings = await moderation.get_all_warnings( + self.bot, member, interaction.guild + ) + warning_embeds = moderator.build_warning_embeds( + interaction.guild, member, all_warnings + ) + warning_embeds[0].description = ( + f"Showing {min(len(all_warnings), 6)}/{len(all_warnings)} warnings" + ) + embeds.append(warning_embeds[0]) + await interaction.response.defer(ephemeral=True) view = ui.PaginateView() await view.send( @@ -96,34 +113,6 @@ async def modify_embed_for_mods( Returns: discord.Embed: The embed with mod only information added """ - # If the user has warnings, add them - warnings = ( - await self.bot.models.Warning.query.where( - self.bot.models.Warning.user_id == str(user.id) - ) - .where(self.bot.models.Warning.guild_id == str(interaction.guild.id)) - .gino.all() - ) - warning_str = "" - for warning in warnings[-3:]: - warning_moderator_name = "unknown" - if warning.invoker_id: - warning_moderator = await self.bot.fetch_user(int(warning.invoker_id)) - if warning_moderator: - warning_moderator_name = warning_moderator.name - - warning_str += ( - f"- {warning.reason} - . " - ) - warning_str += f"Warned by: {warning_moderator_name}\n" - - if warning_str: - embed.add_field( - name=f"**Warnings ({len(warnings)} total)**", - value=warning_str, - inline=True, - ) - # If the user has a pending application, show it # If the user is banned from making applications, show it application_cog = interaction.client.get_cog("ApplicationManager") diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index 4b4668409..299ad77c5 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -172,6 +172,31 @@ async def get_all_warnings( return warnings +async def get_all_notes( + bot: object, user: discord.Member, guild: discord.Guild +) -> list[munch.Munch]: + """Calls to the database to get a list of note database entries for a given user and guild + + Args: + user (discord.Member): The member to look for notes for + guild (discord.Guild): The guild to fetch the notes from + + Returns: + list[bot.models.UserNote]: The list of notes on the member/guild combo. + Will be an empty list if there are no notes + """ + user_notes = ( + await bot.models.UserNote.query.where( + bot.models.UserNote.user_id == str(user.id) + ) + .where(bot.models.UserNote.guild_id == str(guild.id)) + .order_by(bot.models.UserNote.updated.desc()) + .gino.all() + ) + + return user_notes + + async def send_command_usage_alert( bot_object: object, interaction: discord.Interaction, From b92b982e9056439f68dccd305a3154ec1dd5fb5a Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sun, 27 Apr 2025 22:14:28 -0400 Subject: [PATCH 37/99] Update config location --- techsupport_bot/commands/notes.py | 14 +- techsupport_bot/commands/who.py | 498 ------------------------------ 2 files changed, 7 insertions(+), 505 deletions(-) delete mode 100644 techsupport_bot/commands/who.py diff --git a/techsupport_bot/commands/notes.py b/techsupport_bot/commands/notes.py index 0f137c435..b635a130a 100644 --- a/techsupport_bot/commands/notes.py +++ b/techsupport_bot/commands/notes.py @@ -78,7 +78,7 @@ async def is_reader(interaction: discord.Interaction) -> bool: """ config = interaction.client.guild_configs[str(interaction.guild.id)] - if reader_roles := config.extensions.who.note_readers.value: + if reader_roles := config.extensions.notes.note_readers.value: roles = ( discord.utils.get(interaction.guild.roles, name=role) for role in reader_roles @@ -113,7 +113,7 @@ async def is_writer(interaction: discord.Interaction) -> bool: bool: True if the user can run, False if they cannot """ config = interaction.client.guild_configs[str(interaction.guild.id)] - if reader_roles := config.extensions.who.note_writers.value: + if reader_roles := config.extensions.notes.note_writers.value: roles = ( discord.utils.get(interaction.guild.roles, name=role) for role in reader_roles @@ -182,7 +182,7 @@ async def set_note( config = self.bot.guild_configs[str(interaction.guild.id)] # Check to make sure notes are allowed to be assigned - for name in config.extensions.who.note_bypass.value: + for name in config.extensions.notes.note_bypass.value: role_check = discord.utils.get(interaction.guild.roles, name=name) if not role_check: continue @@ -197,7 +197,7 @@ async def set_note( await note.create() role = discord.utils.get( - interaction.guild.roles, name=config.extensions.who.note_role.value + interaction.guild.roles, name=config.extensions.notes.note_role.value ) if not role: @@ -268,7 +268,7 @@ async def clear_notes( config = self.bot.guild_configs[str(interaction.guild.id)] role = discord.utils.get( - interaction.guild.roles, name=config.extensions.who.note_role.value + interaction.guild.roles, name=config.extensions.notes.note_role.value ) if role: await user.remove_roles( @@ -298,7 +298,7 @@ async def all_notes( interaction (discord.Interaction): The interaction that called this command member (discord.Member): The member to get all notes for """ - notes = await self.get_notes(member, interaction.guild) + notes = await moderation.get_all_notes(self.bot, member, interaction.guild) embeds = build_note_embeds(interaction.guild, member, notes) @@ -322,7 +322,7 @@ async def on_member_join(self: Self, member: discord.Member) -> None: return role = discord.utils.get( - member.guild.roles, name=config.extensions.who.note_role.value + member.guild.roles, name=config.extensions.notes.note_role.value ) if not role: return diff --git a/techsupport_bot/commands/who.py b/techsupport_bot/commands/who.py deleted file mode 100644 index 0cc851c11..000000000 --- a/techsupport_bot/commands/who.py +++ /dev/null @@ -1,498 +0,0 @@ -"""Module for the who extension for the discord bot.""" - -from __future__ import annotations - -import datetime -import io -from typing import TYPE_CHECKING, Self - -import discord -import ui -import yaml -from botlogging import LogContext, LogLevel -from core import auxiliary, cogs, extensionconfig -from discord import app_commands -from discord.ext import commands - -if TYPE_CHECKING: - import bot - - -async def setup(bot: bot.TechSupportBot) -> None: - """Loading the Who plugin into the bot - - Args: - bot (bot.TechSupportBot): The bot object to register the cogs to - """ - - return - - config = extensionconfig.ExtensionConfig() - config.add( - key="note_role", - datatype="str", - title="Note role", - description="The name of the role to be added when a note is added to a user", - default=None, - ) - config.add( - key="note_bypass", - datatype="list", - title="Note bypass list", - description=( - "A list of roles that shouldn't have notes set or the note role assigned" - ), - default=["Moderator"], - ) - config.add( - key="note_readers", - datatype="list", - title="Note Reader Roles", - description="Users with roles in this list will be able to use whois", - default=[], - ) - config.add( - key="note_writers", - datatype="list", - title="Note Writer Roles", - description="Users with roles in this list will be able to create or delete notes", - default=[], - ) - - await bot.add_cog(Who(bot=bot, extension_name="who")) - bot.add_extension_config("who", config) - - -class Who(cogs.BaseCog): - """Class to set up who for the extension. - - Attributes: - notes (app_commands.Group): The group for the /note commands - - """ - - notes: app_commands.Group = app_commands.Group( - name="note", description="Command Group for the Notes Extension" - ) - - @staticmethod - async def is_writer(interaction: discord.Interaction) -> bool: - """Checks whether invoker can write notes. If at least one writer - role is not set, NO members can write notes - - Args: - interaction (discord.Interaction): The interaction in which the whois command occured - - Raises: - MissingAnyRole: Raised if the user is lacking any writer role, - but there are roles defined - AppCommandError: Raised if there are no note_writers set in the config - - Returns: - bool: True if the user can run, False if they cannot - """ - config = interaction.client.guild_configs[str(interaction.guild.id)] - if reader_roles := config.extensions.who.note_writers.value: - roles = ( - discord.utils.get(interaction.guild.roles, name=role) - for role in reader_roles - ) - status = any((role in interaction.user.roles for role in roles)) - if not status: - raise app_commands.MissingAnyRole(reader_roles) - return True - - # Reader_roles are empty (not set) - message = "There aren't any `note_writers` roles set in the config!" - embed = auxiliary.prepare_deny_embed(message=message) - - await interaction.response.send_message(embed=embed, ephemeral=True) - - raise app_commands.AppCommandError(message) - - @staticmethod - async def is_reader(interaction: discord.Interaction) -> bool: - """Checks whether invoker can read notes. If at least one reader - role is not set, NO members can read notes - - Args: - interaction (discord.Interaction): The interaction in which the whois command occured - - Raises: - MissingAnyRole: Raised if the user is lacking any reader role, - but there are roles defined - AppCommandError: Raised if there are no note_readers set in the config - - Returns: - bool: True if the user can run, False if they cannot - """ - - config = interaction.client.guild_configs[str(interaction.guild.id)] - if reader_roles := config.extensions.who.note_readers.value: - roles = ( - discord.utils.get(interaction.guild.roles, name=role) - for role in reader_roles - ) - status = any((role in interaction.user.roles for role in roles)) - if not status: - raise app_commands.MissingAnyRole(reader_roles) - return True - - # Reader_roles are empty (not set) - message = "There aren't any `note_readers` roles set in the config!" - embed = auxiliary.prepare_deny_embed(message=message) - - await interaction.response.send_message(embed=embed, ephemeral=True) - - raise app_commands.AppCommandError(message) - - @app_commands.check(is_reader) - @app_commands.command( - name="whois", - description="Gets Discord user information", - extras={"brief": "Gets user data", "usage": "@user", "module": "who"}, - ) - async def get_note( - self: Self, interaction: discord.Interaction, user: discord.Member - ) -> None: - """This is the base of the /whois command - - Args: - interaction (discord.Interaction): The interaction that called this command - user (discord.Member): The member to lookup. Will not work on discord.User - """ - embed = discord.Embed( - title=f"User info for `{user}`", - description="**Note: this is a bot account!**" if user.bot else "", - ) - - embed.set_thumbnail(url=user.display_avatar.url) - - embed.add_field(name="Created at", value=user.created_at.replace(microsecond=0)) - embed.add_field(name="Joined at", value=user.joined_at.replace(microsecond=0)) - embed.add_field( - name="Status", value=interaction.guild.get_member(user.id).status - ) - embed.add_field(name="Nickname", value=user.display_name) - - role_string = ", ".join(role.name for role in user.roles[1:]) - embed.add_field(name="Roles", value=role_string or "No roles") - - # Adds special information only visible to mods - if interaction.permissions.kick_members: - embed = await self.modify_embed_for_mods(interaction, user, embed) - - user_notes = await self.get_notes(user, interaction.guild) - total_notes = 0 - if user_notes: - total_notes = len(user_notes) - user_notes = user_notes[:3] - embed.set_footer(text=f"{total_notes} total notes") - embed.color = discord.Color.dark_blue() - - for note in user_notes: - author = interaction.guild.get_member(int(note.author_id)) or note.author_id - embed.add_field( - name=f"Note from {author} ({note.updated.date()})", - value=f"*{note.body}*" or "*None*", - inline=False, - ) - - await interaction.response.send_message(embed=embed, ephemeral=True) - - async def modify_embed_for_mods( - self: Self, - interaction: discord.Interaction, - user: discord.Member, - embed: discord.Embed, - ) -> discord.Embed: - """Makes modifications to the whois embed to add mod only information - - Args: - interaction (discord.Interaction): The interaction where the /whois command was called - user (discord.Member): The user being looked up - embed (discord.Embed): The embed already filled with whois information - - Returns: - discord.Embed: The embed with mod only information added - """ - # If the user has warnings, add them - warnings = ( - await self.bot.models.Warning.query.where( - self.bot.models.Warning.user_id == str(user.id) - ) - .where(self.bot.models.Warning.guild_id == str(interaction.guild.id)) - .gino.all() - ) - warning_str = "" - for warning in warnings[-3:]: - warning_moderator_name = "unknown" - if warning.invoker_id: - warning_moderator = await self.bot.fetch_user(int(warning.invoker_id)) - if warning_moderator: - warning_moderator_name = warning_moderator.name - - warning_str += ( - f"- {warning.reason} - . " - ) - warning_str += f"Warned by: {warning_moderator_name}\n" - - if warning_str: - embed.add_field( - name=f"**Warnings ({len(warnings)} total)**", - value=warning_str, - inline=True, - ) - - # If the user has a pending application, show it - # If the user is banned from making applications, show it - application_cog = interaction.client.get_cog("ApplicationManager") - if application_cog: - has_application = await application_cog.search_for_pending_application(user) - is_banned = await application_cog.get_ban_entry(user) - embed.add_field( - name="Application information:", - value=( - f"Has pending application: {bool(has_application)}\nIs banned from" - f" making applications: {bool(is_banned)}" - ), - inline=True, - ) - return embed - - @app_commands.check(is_writer) - @notes.command( - name="set", - description="Sets a note for a user, which can be read later from their whois", - extras={ - "brief": "Sets a note for a user", - "usage": "@user [note]", - "module": "who", - }, - ) - async def set_note( - self: Self, interaction: discord.Interaction, user: discord.Member, body: str - ) -> None: - """Adds a new note to a user - This is the entrance for the /note set command - - Args: - interaction (discord.Interaction): The interaction that called this command - user (discord.Member): The member to add the note to - body (str): The contents of the note being created - """ - if interaction.user.id == user.id: - embed = auxiliary.prepare_deny_embed( - message="You cannot add a note for yourself" - ) - await interaction.response.send_message(embed=embed, ephemeral=True) - return - - note = self.bot.models.UserNote( - user_id=str(user.id), - guild_id=str(interaction.guild.id), - author_id=str(interaction.user.id), - body=body, - ) - - config = self.bot.guild_configs[str(interaction.guild.id)] - - # Check to make sure notes are allowed to be assigned - for name in config.extensions.who.note_bypass.value: - role_check = discord.utils.get(interaction.guild.roles, name=name) - if not role_check: - continue - if role_check in getattr(user, "roles", []): - embed = auxiliary.prepare_deny_embed( - message=f"You cannot assign notes to `{user}` because " - + f"they have `{role_check}` role", - ) - await interaction.response.send_message(embed=embed, ephemeral=True) - return - - await note.create() - - role = discord.utils.get( - interaction.guild.roles, name=config.extensions.who.note_role.value - ) - - if not role: - embed = auxiliary.prepare_confirm_embed( - message=f"Note created for `{user}`, but no note " - + "role is configured so no role was added", - ) - await interaction.response.send_message(embed=embed, ephemeral=True) - return - - await user.add_roles(role, reason=f"First note was added by {interaction.user}") - - embed = auxiliary.prepare_confirm_embed(message=f"Note created for `{user}`") - await interaction.response.send_message(embed=embed, ephemeral=True) - - @app_commands.check(is_writer) - @notes.command( - name="clear", - description="Clears all existing notes for a user", - extras={ - "brief": "Clears all notes for a user", - "usage": "@user", - "module": "who", - }, - ) - async def clear_notes( - self: Self, interaction: discord.Interaction, user: discord.Member - ) -> None: - """Clears all notes on a given user - This is the entrace for the /note clear command - - Args: - interaction (discord.Interaction): The interaction that called this command - user (discord.Member): The member to remove all notes from - """ - notes = await self.get_notes(user, interaction.guild) - - if not notes: - embed = auxiliary.prepare_deny_embed( - message="There are no notes for that user" - ) - await interaction.response.send_message(embed=embed, ephemeral=True) - return - - await interaction.response.defer(ephemeral=True) - view = ui.Confirm() - - await view.send( - message=f"Are you sure you want to clear {len(notes)} notes?", - channel=interaction.channel, - author=interaction.user, - interaction=interaction, - ephemeral=True, - ) - - await view.wait() - if view.value is ui.ConfirmResponse.TIMEOUT: - return - if view.value is ui.ConfirmResponse.DENIED: - embed = auxiliary.prepare_deny_embed( - message=f"Notes for `{user}` were not cleared" - ) - await view.followup.send(embed=embed, ephemeral=True) - return - - for note in notes: - await note.delete() - - config = self.bot.guild_configs[str(interaction.guild.id)] - role = discord.utils.get( - interaction.guild.roles, name=config.extensions.who.note_role.value - ) - if role: - await user.remove_roles( - role, reason=f"Notes were cleared by {interaction.user}" - ) - - embed = auxiliary.prepare_confirm_embed(message=f"Notes cleared for `{user}`") - await view.followup.send(embed=embed, ephemeral=True) - - @app_commands.check(is_reader) - @notes.command( - name="all", - description="Gets all notes for a user instead of just new ones", - extras={ - "brief": "Gets all notes for a user", - "usage": "@user", - "module": "who", - }, - ) - async def all_notes( - self: Self, interaction: discord.Interaction, user: discord.Member - ) -> None: - """Gets a file containing every note on a user - This is the entrance for the /note all command - - Args: - interaction (discord.Interaction): The interaction that called this command - user (discord.Member): The member to get all notes for - """ - notes = await self.get_notes(user, interaction.guild) - - if not notes: - embed = auxiliary.prepare_deny_embed( - message=f"There are no notes for `{user}`" - ) - await interaction.response.send_message(embed=embed, ephemeral=True) - return - - note_output_data = [] - for note in notes: - author = interaction.guild.get_member(int(note.author_id)) or note.author_id - data = { - "body": note.body, - "from": str(author), - "at": str(note.updated), - } - note_output_data.append(data) - - yaml_file = discord.File( - io.StringIO(yaml.dump({"notes": note_output_data})), - filename=f"notes-for-{user.id}-{datetime.datetime.utcnow()}.yaml", - ) - - await interaction.response.send_message(file=yaml_file, ephemeral=True) - - async def get_notes( - self: Self, user: discord.Member, guild: discord.Guild - ) -> list[bot.models.UserNote]: - """Calls to the database to get a list of note database entries for a given user and guild - - Args: - user (discord.Member): The member to look for notes for - guild (discord.Guild): The guild to fetch the notes from - - Returns: - list[bot.models.UserNote]: The list of notes on the member/guild combo. - Will be an empty list if there are no notes - """ - user_notes = ( - await self.bot.models.UserNote.query.where( - self.bot.models.UserNote.user_id == str(user.id) - ) - .where(self.bot.models.UserNote.guild_id == str(guild.id)) - .order_by(self.bot.models.UserNote.updated.desc()) - .gino.all() - ) - - return user_notes - - # re-adds note role back to joining users - @commands.Cog.listener() - async def on_member_join(self: Self, member: discord.Member) -> None: - """Automatic listener to look at users when they join the guild. - This is to apply the note role back to joining users - - Args: - member (discord.Member): The member who has just joined - """ - config = self.bot.guild_configs[str(member.guild.id)] - if not self.extension_enabled(config): - return - - role = discord.utils.get( - member.guild.roles, name=config.extensions.who.note_role.value - ) - if not role: - return - - user_notes = await self.get_notes(member, member.guild) - if not user_notes: - return - - await member.add_roles(role, reason="Noted user has joined the guild") - - log_channel = config.get("logging_channel") - await self.bot.logger.send_log( - message=f"Found noted user with ID {member.id} joining - re-adding role", - level=LogLevel.INFO, - context=LogContext(guild=member.guild), - channel=log_channel, - ) From f2718900aeddff6bf6e2bdde6efcf9f8bf494fc9 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 28 Apr 2025 00:13:07 -0400 Subject: [PATCH 38/99] Invert warning order, flags on whois, fancy time stamps on whois --- techsupport_bot/commands/whois.py | 31 ++++++++++++++++++++++++++++-- techsupport_bot/core/moderation.py | 1 + 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/techsupport_bot/commands/whois.py b/techsupport_bot/commands/whois.py index 1eea077a0..96c5b13a6 100644 --- a/techsupport_bot/commands/whois.py +++ b/techsupport_bot/commands/whois.py @@ -52,9 +52,9 @@ async def whois_command( ) embed.add_field( - name="Created at", value=member.created_at.replace(microsecond=0) + name="Created", value=f"" ) - embed.add_field(name="Joined at", value=member.joined_at.replace(microsecond=0)) + embed.add_field(name="Joined", value=f"") embed.add_field( name="Status", value=interaction.guild.get_member(member.id).status ) @@ -66,6 +66,33 @@ async def whois_command( if interaction.permissions.kick_members: embed = await self.modify_embed_for_mods(interaction, member, embed) + flags = [] + if member.flags.automod_quarantined_username: + flags.append("Quarantined by Automod") + if not member.flags.completed_onboarding: + flags.append("Not completed onboarding") + if member.flags.did_rejoin: + flags.append("Has left and rejoined the server") + if member.flags.guest: + flags.append("Is a guest") + if member.public_flags.staff: + flags.append("Is discord staff") + if member.public_flags.spammer: + flags.append("Is a flagged spammer") + if ( + member.is_timed_out + and member.timed_out_until + and member.timed_out_until.astimezone(datetime.timezone.utc) + > datetime.datetime.now((datetime.timezone.utc)) + ): + flags.append( + f"Is timed out until " + ) + + flag_string = "\n - ".join(flag for flag in flags) + if flag_string: + embed.add_field(name="Flags", value=f"- {flag_string}") + embeds = [embed] if await notes.is_reader(interaction): diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index 299ad77c5..611bdcf09 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -167,6 +167,7 @@ async def get_all_warnings( bot_object.models.Warning.user_id == str(user.id) ) .where(bot_object.models.Warning.guild_id == str(guild.id)) + .order_by(bot_object.models.Warning.time.desc()) .gino.all() ) return warnings From 6caf7861fc4a9ff5391bcc607002c61cdeaae7c6 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 28 Apr 2025 00:19:51 -0400 Subject: [PATCH 39/99] Small changes to whois.py --- techsupport_bot/commands/whois.py | 64 +++++++++++++++---------------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/techsupport_bot/commands/whois.py b/techsupport_bot/commands/whois.py index 96c5b13a6..86e488153 100644 --- a/techsupport_bot/commands/whois.py +++ b/techsupport_bot/commands/whois.py @@ -3,17 +3,13 @@ from __future__ import annotations import datetime -import io from typing import TYPE_CHECKING, Self import discord import ui -import yaml -from botlogging import LogContext, LogLevel from commands import moderator, notes from core import auxiliary, cogs, moderation from discord import app_commands -from discord.ext import commands if TYPE_CHECKING: import bot @@ -64,7 +60,7 @@ async def whois_command( embed.add_field(name="Roles", value=role_string or "No roles") if interaction.permissions.kick_members: - embed = await self.modify_embed_for_mods(interaction, member, embed) + embed = await modify_embed_for_mods(interaction, member, embed) flags = [] if member.flags.automod_quarantined_username: @@ -124,34 +120,34 @@ async def whois_command( ) return - async def modify_embed_for_mods( - self: Self, - interaction: discord.Interaction, - user: discord.Member, - embed: discord.Embed, - ) -> discord.Embed: - """Makes modifications to the whois embed to add mod only information - Args: - interaction (discord.Interaction): The interaction where the /whois command was called - user (discord.Member): The user being looked up - embed (discord.Embed): The embed already filled with whois information +async def modify_embed_for_mods( + interaction: discord.Interaction, + user: discord.Member, + embed: discord.Embed, +) -> discord.Embed: + """Makes modifications to the whois embed to add mod only information - Returns: - discord.Embed: The embed with mod only information added - """ - # If the user has a pending application, show it - # If the user is banned from making applications, show it - application_cog = interaction.client.get_cog("ApplicationManager") - if application_cog: - has_application = await application_cog.search_for_pending_application(user) - is_banned = await application_cog.get_ban_entry(user) - embed.add_field( - name="Application information:", - value=( - f"Has pending application: {bool(has_application)}\nIs banned from" - f" making applications: {bool(is_banned)}" - ), - inline=True, - ) - return embed + Args: + interaction (discord.Interaction): The interaction where the /whois command was called + user (discord.Member): The user being looked up + embed (discord.Embed): The embed already filled with whois information + + Returns: + discord.Embed: The embed with mod only information added + """ + # If the user has a pending application, show it + # If the user is banned from making applications, show it + application_cog = interaction.client.get_cog("ApplicationManager") + if application_cog: + has_application = await application_cog.search_for_pending_application(user) + is_banned = await application_cog.get_ban_entry(user) + embed.add_field( + name="Application information:", + value=( + f"Has pending application: {bool(has_application)}\nIs banned from" + f" making applications: {bool(is_banned)}" + ), + inline=True, + ) + return embed From 3f698ced096bedafe9308308addc9f96448187cc Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 28 Apr 2025 08:45:16 -0400 Subject: [PATCH 40/99] rename function --- techsupport_bot/commands/whois.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/techsupport_bot/commands/whois.py b/techsupport_bot/commands/whois.py index 86e488153..e9206e179 100644 --- a/techsupport_bot/commands/whois.py +++ b/techsupport_bot/commands/whois.py @@ -60,7 +60,7 @@ async def whois_command( embed.add_field(name="Roles", value=role_string or "No roles") if interaction.permissions.kick_members: - embed = await modify_embed_for_mods(interaction, member, embed) + embed = await add_application_info_field(interaction, member, embed) flags = [] if member.flags.automod_quarantined_username: @@ -121,7 +121,7 @@ async def whois_command( return -async def modify_embed_for_mods( +async def add_application_info_field( interaction: discord.Interaction, user: discord.Member, embed: discord.Embed, From 42dc6cb1cb47e048f362d44798fbc01fac58b0a0 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 28 Apr 2025 08:46:16 -0400 Subject: [PATCH 41/99] Move around purge command --- techsupport_bot/commands/purge.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/techsupport_bot/commands/purge.py b/techsupport_bot/commands/purge.py index ed308dd51..b4780a982 100644 --- a/techsupport_bot/commands/purge.py +++ b/techsupport_bot/commands/purge.py @@ -77,10 +77,9 @@ async def purge_command( else: timestamp = None - await interaction.response.send_message("Purge Successful", ephemeral=True) - await interaction.channel.purge(after=timestamp, limit=amount) + await interaction.response.send_message("Purge Successful", ephemeral=True) await moderation.send_command_usage_alert( bot_object=self.bot, interaction=interaction, From 5f910c710a169f39f1e367760fed5681370c91ca Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 28 Apr 2025 08:49:43 -0400 Subject: [PATCH 42/99] remove unused imports --- techsupport_bot/commands/notes.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/techsupport_bot/commands/notes.py b/techsupport_bot/commands/notes.py index b635a130a..34c29a51d 100644 --- a/techsupport_bot/commands/notes.py +++ b/techsupport_bot/commands/notes.py @@ -2,13 +2,10 @@ from __future__ import annotations -import datetime -import io from typing import TYPE_CHECKING, Self import discord import ui -import yaml from botlogging import LogContext, LogLevel from core import auxiliary, cogs, extensionconfig, moderation from discord import app_commands From 566540cbf4b75699f58cea2ef647f16402d5c22c Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 28 Apr 2025 08:50:11 -0400 Subject: [PATCH 43/99] Fix small bug --- techsupport_bot/commands/purge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/purge.py b/techsupport_bot/commands/purge.py index b4780a982..95bcdd8d5 100644 --- a/techsupport_bot/commands/purge.py +++ b/techsupport_bot/commands/purge.py @@ -77,9 +77,9 @@ async def purge_command( else: timestamp = None + await interaction.response.send_message("Purge Successful", ephemeral=True) await interaction.channel.purge(after=timestamp, limit=amount) - await interaction.response.send_message("Purge Successful", ephemeral=True) await moderation.send_command_usage_alert( bot_object=self.bot, interaction=interaction, From aba5dbe3b03d70540b2758c9c2de29573627a69d Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 28 Apr 2025 10:44:53 -0400 Subject: [PATCH 44/99] Docstring changes --- techsupport_bot/commands/moderator.py | 12 +++++++++++- techsupport_bot/commands/modlog.py | 2 +- techsupport_bot/commands/notes.py | 10 ++++++++++ techsupport_bot/commands/whois.py | 2 +- techsupport_bot/core/databases.py | 4 ++-- techsupport_bot/core/moderation.py | 3 ++- techsupport_bot/functions/automod.py | 4 ++-- techsupport_bot/ui/pagination.py | 1 + 8 files changed, 30 insertions(+), 8 deletions(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index f46efcc1a..c5d4e3c8b 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -48,7 +48,7 @@ class ProtectCommands(cogs.BaseCog): """The cog for all manual moderation activities These are all slash commands - Attrs: + Attributes: warnings_group (app_commands.Group): The group for the /warning commands """ @@ -766,6 +766,16 @@ def build_warning_embeds( member: discord.Member, warnings: list[bot.models.UserNote], ) -> list[discord.Embed]: + """Makes a list of embeds with 6 warnings per page, for a given user + + Args: + guild (discord.Guild): The guild where the warnings occured + member (discord.Member): The member whose warnings are being looked for + warnings (list[bot.models.UserNote]): The list of warnings from the database + + Returns: + list[discord.Embed]: The list of well formatted embeds + """ embed = auxiliary.generate_basic_embed( f"Warnings for `{member.display_name}` (`{member.name}`)", color=discord.Color.dark_blue(), diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index e6f5321cf..4edba62eb 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -38,7 +38,7 @@ async def setup(bot: bot.TechSupportBot) -> None: class BanLogger(cogs.BaseCog): """The class that holds the /modlog commands - Attrs: + Attributes: modlog_group (app_commands.Group): The group for the /modlog commands """ diff --git a/techsupport_bot/commands/notes.py b/techsupport_bot/commands/notes.py index 34c29a51d..29917db89 100644 --- a/techsupport_bot/commands/notes.py +++ b/techsupport_bot/commands/notes.py @@ -344,6 +344,16 @@ def build_note_embeds( member: discord.Member, notes: list[bot.models.UserNote], ) -> list[discord.Embed]: + """Makes a list of embeds with 6 notes per page, for a given user + + Args: + guild (discord.Guild): The guild where the notes occured + member (discord.Member): The member whose notes are being looked for + notes (list[bot.models.UserNote]): The list of notes from the database + + Returns: + list[discord.Embed]: The list of well formatted embeds + """ embed = auxiliary.generate_basic_embed( f"Notes for `{member.display_name}` (`{member.name}`)", color=discord.Color.dark_blue(), diff --git a/techsupport_bot/commands/whois.py b/techsupport_bot/commands/whois.py index e9206e179..20d81bc24 100644 --- a/techsupport_bot/commands/whois.py +++ b/techsupport_bot/commands/whois.py @@ -38,7 +38,7 @@ async def whois_command( Args: interaction (discord.Interaction): The interaction that called this command - user (discord.Member): The member to lookup. Will not work on discord.User + member (discord.Member): The member to lookup. Will not work on discord.User """ embed = auxiliary.generate_basic_embed( title=f"User info for `{member.display_name}` (`{member.name}`)", diff --git a/techsupport_bot/core/databases.py b/techsupport_bot/core/databases.py index 2a3500230..7f5be522a 100644 --- a/techsupport_bot/core/databases.py +++ b/techsupport_bot/core/databases.py @@ -67,7 +67,7 @@ class BanLog(bot.db.Model): """The postgres table for banlogs Currently used in modlog.py - Attrs: + Attributes: __tablename__ (str): The name of the table in postgres pk (int): The automatic primary key guild_id (str): The string of the guild ID the user was banned in @@ -250,7 +250,7 @@ class Warning(bot.db.Model): """The postgres table for warnings Currently used in protect.py and who.py - Attrs: + Attributes: __tablename__ (str): The name of the table in postgres pk (int): The primary key for the database user_id (str): The user who got warned diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index 611bdcf09..037ddaf85 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -179,11 +179,12 @@ async def get_all_notes( """Calls to the database to get a list of note database entries for a given user and guild Args: + bot (object): The TS bot object to use for the database lookup user (discord.Member): The member to look for notes for guild (discord.Guild): The guild to fetch the notes from Returns: - list[bot.models.UserNote]: The list of notes on the member/guild combo. + list[munch.Munch]: The list of notes on the member/guild combo. Will be an empty list if there are no notes """ user_notes = ( diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 3d237550d..162386c6f 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -87,7 +87,7 @@ class AutoModPunishment: """This is a base class holding the violation and recommended actions Since automod is a framework, the actions can translate to different things - Attrs: + Attributes: violation_str (str): The string of the policy broken. Should be displayed to user recommend_delete (bool): If the policy recommends deletion of the message recommend_warn (bool): If the policy recommends warning the user @@ -126,7 +126,7 @@ def score(self: Self) -> int: class AutoModAction: """The final summarized action for this automod violation - Attrs: + Attributes: warn (bool): Whether the user should be warned delete_message (bool): Whether the message should be deleted mute (bool): Whether the user should be muted diff --git a/techsupport_bot/ui/pagination.py b/techsupport_bot/ui/pagination.py index 042884076..7ad384db7 100644 --- a/techsupport_bot/ui/pagination.py +++ b/techsupport_bot/ui/pagination.py @@ -49,6 +49,7 @@ async def send( with [0] being the first page interaction (discord.Interaction | None): The interaction this should followup with (Optional) + ephemeral (bool): Whether the response should be ephemeral (optional) """ self.author = author self.data = data From 9bd04279a054d4d64ecd4c6aaf68d29ee97f8ab9 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 28 Apr 2025 10:48:18 -0400 Subject: [PATCH 45/99] Small formatting changes --- techsupport_bot/commands/modlog.py | 13 +++++++++---- techsupport_bot/commands/purge.py | 5 ++++- techsupport_bot/commands/whois.py | 1 + 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index 4edba62eb..196ab26aa 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -227,14 +227,18 @@ async def on_member_ban( ) entry = None - async for entry in guild.audit_logs(limit=1, action=discord.AuditLogAction.ban): + moderator = None + async for entry in guild.audit_logs( + limit=10, action=discord.AuditLogAction.ban + ): if entry.target.id == user.id: moderator = entry.user + break if not entry: return - if moderator.bot: + if not moderator or moderator.bot: return await log_ban(self.bot, user, moderator, guild, entry.reason) @@ -255,15 +259,16 @@ async def on_member_unban( ) entry = None + moderator = None async for entry in guild.audit_logs( - limit=1, action=discord.AuditLogAction.unban + limit=10, action=discord.AuditLogAction.unban ): if entry.target.id == user.id: moderator = entry.user if not entry: return - if moderator.bot: + if not moderator or moderator.bot: return await log_unban(self.bot, user, moderator, guild, entry.reason) diff --git a/techsupport_bot/commands/purge.py b/techsupport_bot/commands/purge.py index 95bcdd8d5..700b1332b 100644 --- a/techsupport_bot/commands/purge.py +++ b/techsupport_bot/commands/purge.py @@ -58,7 +58,10 @@ async def purge_command( if amount <= 0 or amount > config.extensions.purge.max_purge_amount.value: embed = auxiliary.prepare_deny_embed( - message=f"Messages to purge must be between 0 and {config.extensions.purge.max_purge_amount.value}", + message=( + "Messages to purge must be between 0 " + f"and {config.extensions.purge.max_purge_amount.value}" + ), ) await interaction.response.send_message(embed=embed, ephemeral=True) return diff --git a/techsupport_bot/commands/whois.py b/techsupport_bot/commands/whois.py index 20d81bc24..63613d583 100644 --- a/techsupport_bot/commands/whois.py +++ b/techsupport_bot/commands/whois.py @@ -25,6 +25,7 @@ async def setup(bot: bot.TechSupportBot) -> None: class Whois(cogs.BaseCog): + """The class for the /whois command""" @app_commands.command( name="whois", From d944230767831e3d08052f73f5b15bfd41220560 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 28 Apr 2025 10:51:52 -0400 Subject: [PATCH 46/99] Formatting, bug fixes --- techsupport_bot/commands/moderator.py | 2 +- techsupport_bot/commands/modlog.py | 2 +- techsupport_bot/commands/notes.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index c5d4e3c8b..b7dfca525 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -52,7 +52,7 @@ class ProtectCommands(cogs.BaseCog): warnings_group (app_commands.Group): The group for the /warning commands """ - warnings_group = app_commands.Group( + warnings_group: app_commands.Group = app_commands.Group( name="warning", description="...", extras={"module": "moderator"} ) diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index 196ab26aa..6078aa26c 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -42,7 +42,7 @@ class BanLogger(cogs.BaseCog): modlog_group (app_commands.Group): The group for the /modlog commands """ - modlog_group = app_commands.Group( + modlog_group: app_commands.Group = app_commands.Group( name="modlog", description="...", extras={"module": "modlog"} ) diff --git a/techsupport_bot/commands/notes.py b/techsupport_bot/commands/notes.py index 29917db89..f918594da 100644 --- a/techsupport_bot/commands/notes.py +++ b/techsupport_bot/commands/notes.py @@ -324,7 +324,7 @@ async def on_member_join(self: Self, member: discord.Member) -> None: if not role: return - user_notes = await moderation.get_all_notes(bot, member, member.guild) + user_notes = await moderation.get_all_notes(self.bot, member, member.guild) if not user_notes: return From a4877b3c11f466553ae199b65ead459a5669633f Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 28 Apr 2025 18:46:09 -0400 Subject: [PATCH 47/99] Fix automod warning attribution --- techsupport_bot/functions/automod.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 162386c6f..9d8fd8912 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -231,7 +231,10 @@ async def response( f" ({count_of_warnings} total warnings)" ) await moderation.warn_user( - self.bot, ctx.author, ctx.author, total_punishment.violation_string + self.bot, + ctx.author, + ctx.channel.guild.me, + total_punishment.violation_string, ) if count_of_warnings >= config.moderation.max_warnings: ban_embed = moderator.generate_response_embed( From 725e48bf2e42d2a5c414edf58bc5043557dc6fe1 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 28 Apr 2025 19:18:13 -0400 Subject: [PATCH 48/99] Fix modlog lookup bugs --- techsupport_bot/commands/modlog.py | 31 +++++++----------------------- 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index 6078aa26c..de8117daf 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -96,15 +96,8 @@ async def lookup_user_command( interaction (discord.Interaction): The interaction that called the command user (discord.User): The user to search for bans for """ - recent_bans_by_user = ( - await self.bot.models.BanLog.query.where( - self.bot.models.BanLog.guild_id == str(interaction.guild.id) - ) - .where(self.bot.models.BanLog.banned_member == str(user.id)) - .order_by(self.bot.models.BanLog.ban_time.desc()) - .limit(10) - .gino.all() - ) + + await interaction.response.defer(ephemeral=False) all_bans_by_user = ( await self.bot.models.BanLog.query.where( @@ -116,7 +109,7 @@ async def lookup_user_command( ) embeds = [] - for ban in recent_bans_by_user: + for ban in all_bans_by_user[:10]: temp_embed = await self.convert_ban_to_pretty_string( ban, f"{user.name} bans" ) @@ -127,10 +120,9 @@ async def lookup_user_command( embed = auxiliary.prepare_deny_embed( f"No bans for the user {user.name} could be found" ) - await interaction.response.send_message(embed=embed) + await interaction.followup.send(embed=embed) return - await interaction.response.defer(ephemeral=False) view = ui.PaginateView() await view.send(interaction.channel, interaction.user, embeds, interaction) @@ -148,15 +140,7 @@ async def lookup_moderator_command( interaction (discord.Interaction): The interaction that called the command moderator (discord.Member): The moderator to search for bans for """ - recent_bans_by_user = ( - await self.bot.models.BanLog.query.where( - self.bot.models.BanLog.guild_id == str(interaction.guild.id) - ) - .where(self.bot.models.BanLog.banning_moderator == str(moderator.id)) - .order_by(self.bot.models.BanLog.ban_time.desc()) - .limit(10) - .gino.all() - ) + await interaction.response.defer(ephemeral=False) all_bans_by_user = ( await self.bot.models.BanLog.query.where( @@ -168,7 +152,7 @@ async def lookup_moderator_command( ) embeds = [] - for ban in recent_bans_by_user: + for ban in all_bans_by_user[:10]: temp_embed = await self.convert_ban_to_pretty_string( ban, f"Bans by {moderator.name}" ) @@ -179,10 +163,9 @@ async def lookup_moderator_command( embed = auxiliary.prepare_deny_embed( f"No bans by the user {moderator.name} could be found" ) - await interaction.response.send_message(embed=embed) + await interaction.followup.send(embed=embed) return - await interaction.response.defer(ephemeral=False) view = ui.PaginateView() await view.send(interaction.channel, interaction.user, embeds, interaction) From a31c526d26dd7db7a3476267eeacc6042d3ff01e Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 28 Apr 2025 19:23:02 -0400 Subject: [PATCH 49/99] Limit delete_days in ban to 7 days --- techsupport_bot/commands/moderator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index b7dfca525..5491b58ce 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -70,7 +70,7 @@ async def handle_ban_user( interaction: discord.Interaction, target: discord.User, reason: str, - delete_days: int = None, + delete_days: app_commands.Range[int, 0, 7] = None, ) -> None: """The ban slash command. This checks that the permissions are correct and that the user is not already banned From 26f7717cfee1b2763a02c5fca7a0a0e9923b33ce Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 28 Apr 2025 19:25:45 -0400 Subject: [PATCH 50/99] fix docstring, ignore MDA002 --- .flake8 | 2 +- techsupport_bot/commands/moderator.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.flake8 b/.flake8 index 3879e514d..f88cb435d 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] -ignore = DCO010, DCO023, DOC503, DOC602, DOC603, E203, E501, E712, F401, F403, F821, W503 +ignore = DCO010, DCO023, DOC503, DOC602, DOC603, MDA002, E203, E501, E712, F401, F403, F821, W503 style = google skip-checking-short-docstrings = False \ No newline at end of file diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 5491b58ce..cb170a114 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -79,7 +79,8 @@ async def handle_ban_user( interaction (discord.Interaction): The interaction that called this command target (discord.User): The target to ban reason (str): The reason the person is getting banned - delete_days (int, optional): How many days of messages to delete. Defaults to None. + delete_days (app_commands.Range[int, 0, 7], optional): How many days of + messages to delete. Defaults to None. """ # Ensure we can ban the person permission_check = await self.permission_check( From f683728952ef3672be9065650ef42c0a4d0af9f8 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 28 Apr 2025 19:30:53 -0400 Subject: [PATCH 51/99] Check if modlog is enabled in modlog ban/unban --- techsupport_bot/commands/moderator.py | 2 +- techsupport_bot/commands/modlog.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index cb170a114..e088231ec 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -79,7 +79,7 @@ async def handle_ban_user( interaction (discord.Interaction): The interaction that called this command target (discord.User): The target to ban reason (str): The reason the person is getting banned - delete_days (app_commands.Range[int, 0, 7], optional): How many days of + delete_days (app_commands.Range[int, 0, 7], optional): How many days of messages to delete. Defaults to None. """ # Ensure we can ban the person diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index de8117daf..07de04c7d 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -209,6 +209,10 @@ async def on_member_ban( discord.utils.utcnow() + datetime.timedelta(seconds=2) ) + config = self.bot.guild_configs[str(guild.id)] + if not self.extension_enabled(config): + return + entry = None moderator = None async for entry in guild.audit_logs( @@ -241,6 +245,10 @@ async def on_member_unban( discord.utils.utcnow() + datetime.timedelta(seconds=2) ) + config = self.bot.guild_configs[str(guild.id)] + if not self.extension_enabled(config): + return + entry = None moderator = None async for entry in guild.audit_logs( From 090eb120725d65f98b90a72ee9074f0c10a38321 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 28 Apr 2025 19:34:15 -0400 Subject: [PATCH 52/99] Cap reason at 500 characters --- techsupport_bot/commands/moderator.py | 48 +++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index e088231ec..f9c918927 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -91,6 +91,12 @@ async def handle_ban_user( await interaction.response.send_message(embed=embed) return + if len(reason) > 500: + embed = auxiliary.prepare_deny_embed( + message="Reason length is capped at 500 characters" + ) + await interaction.response.send_message(embed=embed) + async for ban in interaction.guild.bans(limit=None): if target == ban.user: embed = auxiliary.prepare_deny_embed(message="User is already banned.") @@ -157,6 +163,12 @@ async def handle_unban_user( await interaction.response.send_message(embed=embed) return + if len(reason) > 500: + embed = auxiliary.prepare_deny_embed( + message="Reason length is capped at 500 characters" + ) + await interaction.response.send_message(embed=embed) + is_banned = False async for ban in interaction.guild.bans(limit=None): @@ -222,6 +234,12 @@ async def handle_kick_user( await interaction.response.send_message(embed=embed) return + if len(reason) > 500: + embed = auxiliary.prepare_deny_embed( + message="Reason length is capped at 500 characters" + ) + await interaction.response.send_message(embed=embed) + result = await moderation.kick_user( guild=interaction.guild, user=target, @@ -275,6 +293,12 @@ async def handle_mute_user( await interaction.response.send_message(embed=embed) return + if len(reason) > 500: + embed = auxiliary.prepare_deny_embed( + message="Reason length is capped at 500 characters" + ) + await interaction.response.send_message(embed=embed) + # The API prevents administrators from being timed out. Check it here if target.guild_permissions.administrator: embed = auxiliary.prepare_deny_embed( @@ -361,6 +385,12 @@ async def handle_unmute_user( await interaction.response.send_message(embed=embed) return + if len(reason) > 500: + embed = auxiliary.prepare_deny_embed( + message="Reason length is capped at 500 characters" + ) + await interaction.response.send_message(embed=embed) + if not target.timed_out_until: embed = auxiliary.prepare_deny_embed( message=(f"{target} is not currently muted") @@ -415,6 +445,12 @@ async def handle_warn_user( await interaction.response.send_message(embed=embed) return + if len(reason) > 500: + embed = auxiliary.prepare_deny_embed( + message="Reason length is capped at 500 characters" + ) + await interaction.response.send_message(embed=embed) + if target not in interaction.channel.members: embed = auxiliary.prepare_deny_embed( message=f"{target} cannot see this warning. No warning was added." @@ -542,6 +578,12 @@ async def handle_unwarn_user( await interaction.response.send_message(embed=embed) return + if len(reason) > 500: + embed = auxiliary.prepare_deny_embed( + message="Reason length is capped at 500 characters" + ) + await interaction.response.send_message(embed=embed) + database_warning = await self.get_warning(user=target, warning=warning) if not database_warning: @@ -599,6 +641,12 @@ async def handle_warning_clear( await interaction.response.send_message(embed=embed) return + if len(reason) > 500: + embed = auxiliary.prepare_deny_embed( + message="Reason length is capped at 500 characters" + ) + await interaction.response.send_message(embed=embed) + warnings = await moderation.get_all_warnings( self.bot, target, interaction.guild ) From 6a849ed0add93f877ce122df1b276cdcd98d28ef Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 28 Apr 2025 20:04:04 -0400 Subject: [PATCH 53/99] Add mute duration to reason in response embed --- techsupport_bot/commands/moderator.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index f9c918927..1ac6b56f6 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -354,7 +354,14 @@ async def handle_mute_user( guild=interaction.guild, target=target, ) - embed = generate_response_embed(user=target, action="mute", reason=reason) + + muted_until_timestamp = ( + f"" + ) + + full_reason = f"{reason} (muted until {muted_until_timestamp})" + + embed = generate_response_embed(user=target, action="mute", reason=full_reason) await interaction.response.send_message(content=target.mention, embed=embed) @app_commands.checks.has_permissions(moderate_members=True) From a6e82babac392acdc24cfa5e8757da5f40680876 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 28 Apr 2025 20:11:53 -0400 Subject: [PATCH 54/99] Check permissions and enabled extensions in whois --- techsupport_bot/commands/__init__.py | 1 + techsupport_bot/commands/whois.py | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/techsupport_bot/commands/__init__.py b/techsupport_bot/commands/__init__.py index a11c06a98..720c19524 100644 --- a/techsupport_bot/commands/__init__.py +++ b/techsupport_bot/commands/__init__.py @@ -3,6 +3,7 @@ Both app and prefix commands are in this module """ +from .application import * from .burn import * from .conch import * from .config import * diff --git a/techsupport_bot/commands/whois.py b/techsupport_bot/commands/whois.py index 63613d583..054a195ac 100644 --- a/techsupport_bot/commands/whois.py +++ b/techsupport_bot/commands/whois.py @@ -7,7 +7,7 @@ import discord import ui -from commands import moderator, notes +from commands import application, moderator, notes from core import auxiliary, cogs, moderation from discord import app_commands @@ -60,9 +60,15 @@ async def whois_command( role_string = ", ".join(role.name for role in member.roles[1:]) embed.add_field(name="Roles", value=role_string or "No roles") - if interaction.permissions.kick_members: + config = self.bot.guild_configs[str(interaction.guild.id)] + + if ( + "application" in config.enabled_extensions + and await application.command_permission_check(interaction) + ): embed = await add_application_info_field(interaction, member, embed) + if interaction.permissions.kick_members: flags = [] if member.flags.automod_quarantined_username: flags.append("Quarantined by Automod") @@ -92,7 +98,7 @@ async def whois_command( embeds = [embed] - if await notes.is_reader(interaction): + if "notes" in config.enabled_extensions and await notes.is_reader(interaction): all_notes = await moderation.get_all_notes( self.bot, member, interaction.guild ) @@ -102,7 +108,10 @@ async def whois_command( ) embeds.append(notes_embeds[0]) - if interaction.permissions.kick_members: + if ( + "moderator" in config.enabled_extensions + and interaction.permissions.kick_members + ): all_warnings = await moderation.get_all_warnings( self.bot, member, interaction.guild ) From fc372808747b5f76771b431070c33133518b6fb7 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 28 Apr 2025 20:16:27 -0400 Subject: [PATCH 55/99] Add try catch when looking up users in report --- techsupport_bot/commands/report.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/techsupport_bot/commands/report.py b/techsupport_bot/commands/report.py index f7ad98316..531abea66 100644 --- a/techsupport_bot/commands/report.py +++ b/techsupport_bot/commands/report.py @@ -74,7 +74,11 @@ async def report_command( mentioned_users = [] for user_id in mentioned_user_ids: - user = await interaction.guild.fetch_member(int(user_id)) + user = None + try: + user = await interaction.guild.fetch_member(int(user_id)) + except discord.NotFound: + user = None if user: mentioned_users.append(user) mentioned_users: list[discord.Member] = set(mentioned_users) From db55e4be917793e31bfbb1012e8602b33d4a45d1 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Mon, 28 Apr 2025 21:04:55 -0400 Subject: [PATCH 56/99] fix note clear --- techsupport_bot/commands/notes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/notes.py b/techsupport_bot/commands/notes.py index f918594da..8e078a5a2 100644 --- a/techsupport_bot/commands/notes.py +++ b/techsupport_bot/commands/notes.py @@ -230,7 +230,7 @@ async def clear_notes( interaction (discord.Interaction): The interaction that called this command user (discord.Member): The member to remove all notes from """ - notes = await self.get_notes(user, interaction.guild) + notes = await moderation.get_all_notes(self.bot, user, interaction.guild) if not notes: embed = auxiliary.prepare_deny_embed( From 9dc56cbc81311ae3f8fccbfaf0dee31a0339af6c Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 09:37:35 -0400 Subject: [PATCH 57/99] Fix unrelated file --- techsupport_bot/commands/htd.py | 1 + 1 file changed, 1 insertion(+) diff --git a/techsupport_bot/commands/htd.py b/techsupport_bot/commands/htd.py index 0d78673a7..453dba3c3 100644 --- a/techsupport_bot/commands/htd.py +++ b/techsupport_bot/commands/htd.py @@ -235,6 +235,7 @@ def custom_embed_generation(raw_input: str, val_to_convert: int) -> discord.Embe inline=False, ) + print(embed.fields[0].name) return embed From 47438627af20b154c57265303e4641b26cad570c Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:17:56 -0400 Subject: [PATCH 58/99] Fix whois issue --- techsupport_bot/commands/whois.py | 37 ++++++++++++++++++------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/techsupport_bot/commands/whois.py b/techsupport_bot/commands/whois.py index 054a195ac..4a63b625a 100644 --- a/techsupport_bot/commands/whois.py +++ b/techsupport_bot/commands/whois.py @@ -62,11 +62,12 @@ async def whois_command( config = self.bot.guild_configs[str(interaction.guild.id)] - if ( - "application" in config.enabled_extensions - and await application.command_permission_check(interaction) - ): - embed = await add_application_info_field(interaction, member, embed) + if "application" in config.enabled_extensions: + try: + await application.command_permission_check(interaction) + embed = await add_application_info_field(interaction, member, embed) + except app_commands.MissingAnyRole or app_commands.AppCommandError: + pass if interaction.permissions.kick_members: flags = [] @@ -94,19 +95,25 @@ async def whois_command( flag_string = "\n - ".join(flag for flag in flags) if flag_string: - embed.add_field(name="Flags", value=f"- {flag_string}") + embed.add_field(name="Flags", value=f"- {flag_string}", inline=False) embeds = [embed] - if "notes" in config.enabled_extensions and await notes.is_reader(interaction): - all_notes = await moderation.get_all_notes( - self.bot, member, interaction.guild - ) - notes_embeds = notes.build_note_embeds(interaction.guild, member, all_notes) - notes_embeds[0].description = ( - f"Showing {min(len(all_notes), 6)}/{len(all_notes)} notes" - ) - embeds.append(notes_embeds[0]) + if "notes" in config.enabled_extensions: + try: + await notes.is_reader(interaction) + all_notes = await moderation.get_all_notes( + self.bot, member, interaction.guild + ) + notes_embeds = notes.build_note_embeds( + interaction.guild, member, all_notes + ) + notes_embeds[0].description = ( + f"Showing {min(len(all_notes), 6)}/{len(all_notes)} notes" + ) + embeds.append(notes_embeds[0]) + except app_commands.MissingAnyRole or app_commands.AppCommandError: + pass if ( "moderator" in config.enabled_extensions From 99001eb92fdb6152ab21281d997dcde5810daa10 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 11:20:51 -0700 Subject: [PATCH 59/99] Update techsupport_bot/commands/modlog.py Co-authored-by: dkay --- techsupport_bot/commands/modlog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/techsupport_bot/commands/modlog.py b/techsupport_bot/commands/modlog.py index 07de04c7d..fa55633ef 100644 --- a/techsupport_bot/commands/modlog.py +++ b/techsupport_bot/commands/modlog.py @@ -205,6 +205,7 @@ async def on_member_ban( user (discord.User | discord.Member): The user that got banned. Can be either User or Member depending if the user was in the guild or not at the time of removal. """ + # Wait a short time to ensure the audit log has been updated await discord.utils.sleep_until( discord.utils.utcnow() + datetime.timedelta(seconds=2) ) From 995673218db494919a85802d5b6f69c73ab19125 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:22:27 -0400 Subject: [PATCH 60/99] Update except whois --- techsupport_bot/commands/whois.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/techsupport_bot/commands/whois.py b/techsupport_bot/commands/whois.py index 4a63b625a..756895358 100644 --- a/techsupport_bot/commands/whois.py +++ b/techsupport_bot/commands/whois.py @@ -66,7 +66,7 @@ async def whois_command( try: await application.command_permission_check(interaction) embed = await add_application_info_field(interaction, member, embed) - except app_commands.MissingAnyRole or app_commands.AppCommandError: + except (app_commands.MissingAnyRole, app_commands.AppCommandError): pass if interaction.permissions.kick_members: @@ -112,7 +112,7 @@ async def whois_command( f"Showing {min(len(all_notes), 6)}/{len(all_notes)} notes" ) embeds.append(notes_embeds[0]) - except app_commands.MissingAnyRole or app_commands.AppCommandError: + except (app_commands.MissingAnyRole, app_commands.AppCommandError): pass if ( From adfee94e1676689176bbef692f8f6d2ed3e87ab2 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:27:08 -0400 Subject: [PATCH 61/99] Replace ban_days with ban_seconds --- techsupport_bot/commands/moderator.py | 6 ++++-- techsupport_bot/core/moderation.py | 4 ++-- techsupport_bot/functions/automod.py | 7 ++++++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 1ac6b56f6..8f829dd36 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -111,7 +111,7 @@ async def handle_ban_user( result = await moderation.ban_user( guild=interaction.guild, user=target, - delete_days=delete_days, + delete_seconds=delete_days * 86400, reason=f"{reason} - banned by {interaction.user}", ) if not result: @@ -496,7 +496,9 @@ async def handle_warn_user( ban_result = await moderation.ban_user( guild=interaction.guild, user=target, - delete_days=config.extensions.moderator.ban_delete_duration.value, + delete_seconds=( + config.extensions.moderator.ban_delete_duration.value * 86400 + ), reason=( f"Over max warning count {new_count_of_warnings} out of" f" {config.moderation.max_warnings} (final warning:" diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index 037ddaf85..935ecd9fe 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -8,7 +8,7 @@ async def ban_user( - guild: discord.Guild, user: discord.User, delete_days: int, reason: str + guild: discord.Guild, user: discord.User, delete_seconds: int, reason: str ) -> bool: """A very simple function that bans a given user from the passed guild @@ -25,7 +25,7 @@ async def ban_user( await guild.ban( user, reason=reason, - delete_message_days=delete_days, + delete_message_seconds=delete_seconds, ) return True diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 9d8fd8912..758e3a5bc 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -258,7 +258,12 @@ async def response( ) await moderation.ban_user( - ctx.guild, ctx.author, 7, total_punishment.violation_string + ctx.guild, + ctx.author, + delete_seconds=( + config.extensions.moderator.ban_delete_duration.value * 86400 + ), + reason=total_punishment.violation_string, ) await modlog.log_ban( self.bot, From f0cd09e484595442e810bb2aa394962634fdeb5a Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:30:39 -0400 Subject: [PATCH 62/99] Update docstring --- techsupport_bot/core/moderation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index 935ecd9fe..7f3f291c6 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -15,7 +15,7 @@ async def ban_user( Args: guild (discord.Guild): The guild to ban from user (discord.User): The user who needs to be banned - delete_days (int): The numbers of days of past messages to delete + delete_seconds (int): The numbers of seconds of past messages to delete reason (str): The reason for banning Returns: From 61f74ff980eb35e8f3c524ddd300e8f09e5209ec Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:14:56 -0400 Subject: [PATCH 63/99] Move ban detection, make it better --- techsupport_bot/commands/moderator.py | 16 ++++++---------- techsupport_bot/core/moderation.py | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 8f829dd36..7bece9af5 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -97,11 +97,11 @@ async def handle_ban_user( ) await interaction.response.send_message(embed=embed) - async for ban in interaction.guild.bans(limit=None): - if target == ban.user: - embed = auxiliary.prepare_deny_embed(message="User is already banned.") - await interaction.response.send_message(embed=embed) - return + is_banned = await moderation.check_if_user_banned(target, interaction.guild) + if is_banned: + embed = auxiliary.prepare_deny_embed(message="User is already banned.") + await interaction.response.send_message(embed=embed) + return if not delete_days: config = self.bot.guild_configs[str(interaction.guild.id)] @@ -169,11 +169,7 @@ async def handle_unban_user( ) await interaction.response.send_message(embed=embed) - is_banned = False - - async for ban in interaction.guild.bans(limit=None): - if target == ban.user: - is_banned = True + is_banned = await moderation.check_if_user_banned(target, interaction.guild) if not is_banned: embed = auxiliary.prepare_deny_embed(message=f"{target} is not banned") diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index 7f3f291c6..f6b68f613 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -255,3 +255,22 @@ async def send_command_usage_alert( embed.color = discord.Color.red() await alert_channel.send(embed=embed) + + +async def check_if_user_banned(user: discord.User, guild: discord.Guild) -> bool: + """Queries the given guild to find if the given discord.User is banned or not + + Args: + user (discord.User): The user to search for being banned + guild (discord.Guild): The guild to search the bans for + + Returns: + bool: Whether the user is banned or not + """ + + try: + ban = await guild.fetch_ban(user) + except discord.NotFound: + return False + + return True From 364b56d29acc6d17a32f4399857f6adaf50b7414 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:16:52 -0400 Subject: [PATCH 64/99] Fix check if banned function --- techsupport_bot/core/moderation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index f6b68f613..f15ecb406 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -269,7 +269,7 @@ async def check_if_user_banned(user: discord.User, guild: discord.Guild) -> bool """ try: - ban = await guild.fetch_ban(user) + await guild.fetch_ban(user) except discord.NotFound: return False From eec41070c00f3fc6d2e3b4d8613eb8e09c833811 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:20:13 -0700 Subject: [PATCH 65/99] Add default to the ban delete days config description Co-authored-by: dkay --- techsupport_bot/commands/moderator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 7bece9af5..df1f0b949 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -36,7 +36,7 @@ async def setup(bot: bot.TechSupportBot) -> None: datatype="int", title="Ban delete duration (days)", description=( - "The amount of days to delete messages for a user after they are banned" + "The default amount of days to delete messages for a user after they are banned" ), default=7, ) From a44cceb89f55f0a67ac2dae6f71f1f2924c87ee4 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:20:26 -0700 Subject: [PATCH 66/99] Update techsupport_bot/commands/report.py Co-authored-by: dkay --- techsupport_bot/commands/report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/report.py b/techsupport_bot/commands/report.py index 531abea66..9bb95c791 100644 --- a/techsupport_bot/commands/report.py +++ b/techsupport_bot/commands/report.py @@ -116,6 +116,6 @@ async def report_command( await alert_channel.send(embed=embed) user_embed = auxiliary.prepare_confirm_embed( - message="Your report was successfully recieved" + message="Your report was successfully sent" ) await interaction.response.send_message(embed=user_embed, ephemeral=True) From 8d6e0ffbc3c5d10a0aa9eb7d1aa25afc2432ee7a Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:20:37 -0700 Subject: [PATCH 67/99] Update techsupport_bot/commands/whois.py Co-authored-by: dkay --- techsupport_bot/commands/whois.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/whois.py b/techsupport_bot/commands/whois.py index 756895358..e220fbf6e 100644 --- a/techsupport_bot/commands/whois.py +++ b/techsupport_bot/commands/whois.py @@ -74,7 +74,7 @@ async def whois_command( if member.flags.automod_quarantined_username: flags.append("Quarantined by Automod") if not member.flags.completed_onboarding: - flags.append("Not completed onboarding") + flags.append("Has not completed onboarding") if member.flags.did_rejoin: flags.append("Has left and rejoined the server") if member.flags.guest: From ff9cd0131b74204f190cad0700a1343fabb44804 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:20:49 -0700 Subject: [PATCH 68/99] Update techsupport_bot/commands/report.py Co-authored-by: dkay --- techsupport_bot/commands/report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/report.py b/techsupport_bot/commands/report.py index 9bb95c791..d6efb1012 100644 --- a/techsupport_bot/commands/report.py +++ b/techsupport_bot/commands/report.py @@ -108,7 +108,7 @@ async def report_command( if not alert_channel: user_embed = auxiliary.prepare_deny_embed( - message="An error occured while processing your report. It was not received." + message="An error occurred while processing your report. It was not sent." ) await interaction.response.send_message(embed=user_embed, ephemeral=True) return From e0d08505cd65d7d131b55e04ed4628083c23e5ca Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:21:07 -0700 Subject: [PATCH 69/99] Update techsupport_bot/commands/moderator.py Co-authored-by: dkay --- techsupport_bot/commands/moderator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index df1f0b949..fca4a79fd 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -390,7 +390,7 @@ async def handle_unmute_user( if len(reason) > 500: embed = auxiliary.prepare_deny_embed( - message="Reason length is capped at 500 characters" + message="Unmute reason must be below 500 characters" ) await interaction.response.send_message(embed=embed) From 6c8059077c15b3a76787b849d5c7158cf7a3b0fa Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:22:10 -0700 Subject: [PATCH 70/99] Update techsupport_bot/commands/moderator.py Co-authored-by: dkay --- techsupport_bot/commands/moderator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index fca4a79fd..1de7dc7a8 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -291,7 +291,7 @@ async def handle_mute_user( if len(reason) > 500: embed = auxiliary.prepare_deny_embed( - message="Reason length is capped at 500 characters" + message="Mute reason must be below 500 characters" ) await interaction.response.send_message(embed=embed) From 94a4d413b740fcf35d0bdead1fa266fc1dd699fc Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:22:21 -0700 Subject: [PATCH 71/99] Update techsupport_bot/commands/moderator.py Co-authored-by: dkay --- techsupport_bot/commands/moderator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 1de7dc7a8..ae0f39dbf 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -93,7 +93,7 @@ async def handle_ban_user( if len(reason) > 500: embed = auxiliary.prepare_deny_embed( - message="Reason length is capped at 500 characters" + message="Ban reason must be under 500 characters" ) await interaction.response.send_message(embed=embed) From ec7bafaad0781bb53bf17067764afbd2a61bd783 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:22:31 -0700 Subject: [PATCH 72/99] Update techsupport_bot/commands/moderator.py Co-authored-by: dkay --- techsupport_bot/commands/moderator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index ae0f39dbf..58ca46356 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -165,7 +165,7 @@ async def handle_unban_user( if len(reason) > 500: embed = auxiliary.prepare_deny_embed( - message="Reason length is capped at 500 characters" + message="Unban reason must be under 500 characters" ) await interaction.response.send_message(embed=embed) From 225451b812698ce51671e75143dc72715165c492 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:22:42 -0700 Subject: [PATCH 73/99] Update techsupport_bot/commands/moderator.py Co-authored-by: dkay --- techsupport_bot/commands/moderator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 58ca46356..a075e8b5b 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -450,7 +450,7 @@ async def handle_warn_user( if len(reason) > 500: embed = auxiliary.prepare_deny_embed( - message="Reason length is capped at 500 characters" + message="Warn reason must be below 500 characters" ) await interaction.response.send_message(embed=embed) From a69e790170f41d1f3301587d0085c043e3415efe Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:22:59 -0700 Subject: [PATCH 74/99] Update techsupport_bot/commands/moderator.py Co-authored-by: dkay --- techsupport_bot/commands/moderator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index a075e8b5b..e514e3df2 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -648,7 +648,7 @@ async def handle_warning_clear( if len(reason) > 500: embed = auxiliary.prepare_deny_embed( - message="Reason length is capped at 500 characters" + message="Reason must be below 500 characters" ) await interaction.response.send_message(embed=embed) From a8d75c1a0c29e0f422cce691732ec0d864466075 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:23:16 -0700 Subject: [PATCH 75/99] Update techsupport_bot/commands/moderator.py Co-authored-by: dkay --- techsupport_bot/commands/moderator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index e514e3df2..b037a7330 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -658,7 +658,7 @@ async def handle_warning_clear( if not warnings: embed = auxiliary.prepare_deny_embed( - message=f"No warnings could be found on {target}" + message=f"{target} has no warnings" ) await interaction.response.send_message(embed=embed) return From 59bc7a24425d28c0485c76c44fbbaf8c71113103 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:23:31 -0700 Subject: [PATCH 76/99] Update techsupport_bot/commands/moderator.py Co-authored-by: dkay --- techsupport_bot/commands/moderator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index b037a7330..655b7c5e5 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -585,7 +585,7 @@ async def handle_unwarn_user( if len(reason) > 500: embed = auxiliary.prepare_deny_embed( - message="Reason length is capped at 500 characters" + message="Unwarn reason must be below 500 characters" ) await interaction.response.send_message(embed=embed) From da3e3e69afbd3dd2ff5479153f3c158d529e20d3 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 17:24:20 -0400 Subject: [PATCH 77/99] Formatting changes --- techsupport_bot/commands/moderator.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 655b7c5e5..58d800b01 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -657,9 +657,7 @@ async def handle_warning_clear( ) if not warnings: - embed = auxiliary.prepare_deny_embed( - message=f"{target} has no warnings" - ) + embed = auxiliary.prepare_deny_embed(message=f"{target} has no warnings") await interaction.response.send_message(embed=embed) return From 5f120edc9f3bf941906e8b343526ee13ce6e9ebd Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 17:31:41 -0400 Subject: [PATCH 78/99] You must have reader role to be writer for notes --- techsupport_bot/commands/notes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/techsupport_bot/commands/notes.py b/techsupport_bot/commands/notes.py index 8e078a5a2..354027d76 100644 --- a/techsupport_bot/commands/notes.py +++ b/techsupport_bot/commands/notes.py @@ -109,6 +109,7 @@ async def is_writer(interaction: discord.Interaction) -> bool: Returns: bool: True if the user can run, False if they cannot """ + await is_reader(interaction) config = interaction.client.guild_configs[str(interaction.guild.id)] if reader_roles := config.extensions.notes.note_writers.value: roles = ( From 53741d80dfa45a41c1f0a2056e52d9b7faa3a02c Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 18:20:36 -0400 Subject: [PATCH 79/99] Update module on whois command --- techsupport_bot/commands/whois.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/whois.py b/techsupport_bot/commands/whois.py index e220fbf6e..e78d8ed92 100644 --- a/techsupport_bot/commands/whois.py +++ b/techsupport_bot/commands/whois.py @@ -30,7 +30,7 @@ class Whois(cogs.BaseCog): @app_commands.command( name="whois", description="Gets Discord user information", - extras={"brief": "Gets user data", "usage": "@user", "module": "who"}, + extras={"brief": "Gets user data", "usage": "@user", "module": "whois"}, ) async def whois_command( self: Self, interaction: discord.Interaction, member: discord.Member From 1f70d654648fcecc3ea888160b00d20e37a0d66e Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 18:36:33 -0400 Subject: [PATCH 80/99] Fix shitty config causing bug --- techsupport_bot/commands/moderator.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 58d800b01..96b14f205 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -747,13 +747,16 @@ async def permission_check( return None # Check to see if target has any immune roles - for name in config.extensions.moderator.immune_roles.value: - role_check = discord.utils.get(target.guild.roles, name=name) - if role_check and role_check in getattr(target, "roles", []): - return ( - f"You cannot {action_name} {target} because they have" - f" `{role_check}` role" - ) + try: + for name in config.extensions.moderator.immune_roles.value: + role_check = discord.utils.get(target.guild.roles, name=name) + if role_check and role_check in getattr(target, "roles", []): + return ( + f"You cannot {action_name} {target} because they have" + f" `{role_check}` role" + ) + except AttributeError: + pass # Check to see if the Bot can execute on the target if invoker.guild.get_member(int(self.bot.user.id)).top_role <= target.top_role: From de96e6fbe04abcb8a3903c3e87f2cc213382cf20 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 19:20:50 -0400 Subject: [PATCH 81/99] Really fix reader/writer roles in notes --- techsupport_bot/commands/notes.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/techsupport_bot/commands/notes.py b/techsupport_bot/commands/notes.py index 354027d76..e50fca3ea 100644 --- a/techsupport_bot/commands/notes.py +++ b/techsupport_bot/commands/notes.py @@ -109,16 +109,30 @@ async def is_writer(interaction: discord.Interaction) -> bool: Returns: bool: True if the user can run, False if they cannot """ - await is_reader(interaction) config = interaction.client.guild_configs[str(interaction.guild.id)] - if reader_roles := config.extensions.notes.note_writers.value: + reader_roles = config.extensions.notes.note_readers.value + writer_roles = config.extensions.notes.note_writers.value + + try: + await is_reader(interaction) + except app_commands.MissingAnyRole: + raise app_commands.MissingAnyRole(reader_roles) + except app_commands.AppCommandError: + message = "There aren't any `note_readers` roles set in the config!" + embed = auxiliary.prepare_deny_embed(message=message) + + await interaction.response.send_message(embed=embed, ephemeral=True) + + raise app_commands.AppCommandError(message) + + if writer_roles: roles = ( discord.utils.get(interaction.guild.roles, name=role) - for role in reader_roles + for role in writer_roles ) status = any((role in interaction.user.roles for role in roles)) if not status: - raise app_commands.MissingAnyRole(reader_roles) + raise app_commands.MissingAnyRole(writer_roles) return True # Reader_roles are empty (not set) From 3ffc9ef5bda8ae3647155a5c1a50e02f942a635d Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 29 Apr 2025 19:23:00 -0400 Subject: [PATCH 82/99] Raise error chain --- techsupport_bot/commands/notes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/techsupport_bot/commands/notes.py b/techsupport_bot/commands/notes.py index e50fca3ea..76c0c9134 100644 --- a/techsupport_bot/commands/notes.py +++ b/techsupport_bot/commands/notes.py @@ -115,15 +115,15 @@ async def is_writer(interaction: discord.Interaction) -> bool: try: await is_reader(interaction) - except app_commands.MissingAnyRole: - raise app_commands.MissingAnyRole(reader_roles) - except app_commands.AppCommandError: + except app_commands.MissingAnyRole as e: + raise app_commands.MissingAnyRole(reader_roles) from e + except app_commands.AppCommandError as e: message = "There aren't any `note_readers` roles set in the config!" embed = auxiliary.prepare_deny_embed(message=message) await interaction.response.send_message(embed=embed, ephemeral=True) - raise app_commands.AppCommandError(message) + raise app_commands.AppCommandError(message) from e if writer_roles: roles = ( From 2b34a39c76065f6696683368c4cc694a6b7e9c8a Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:44:42 -0400 Subject: [PATCH 83/99] Fix bug in application perm check --- techsupport_bot/commands/application.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/techsupport_bot/commands/application.py b/techsupport_bot/commands/application.py index 5cfca5887..493146df8 100644 --- a/techsupport_bot/commands/application.py +++ b/techsupport_bot/commands/application.py @@ -105,7 +105,7 @@ async def setup(bot: bot.TechSupportBot) -> None: description=( "The role IDs required to manage the applications (not required to apply)" ), - default=[""], + default=[], ) config.add( key="ping_role", @@ -142,6 +142,9 @@ async def command_permission_check(interaction: discord.Interaction) -> bool: # Gets permitted roles allowed_roles = [] for role_id in config.extensions.application.manage_roles.value: + if not role: + continue + role = interaction.guild.get_role(int(role_id)) if not role: continue From c4ad546438fc0b6bf60fa07e71a5bff898f5b7d0 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:46:32 -0400 Subject: [PATCH 84/99] use the right variable name --- techsupport_bot/commands/application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/application.py b/techsupport_bot/commands/application.py index 493146df8..c7361f8f1 100644 --- a/techsupport_bot/commands/application.py +++ b/techsupport_bot/commands/application.py @@ -142,7 +142,7 @@ async def command_permission_check(interaction: discord.Interaction) -> bool: # Gets permitted roles allowed_roles = [] for role_id in config.extensions.application.manage_roles.value: - if not role: + if not role_id: continue role = interaction.guild.get_role(int(role_id)) From 5c7a8a6125e1c65ab8a55f1dd0ea1224ccf2b908 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:47:08 -0400 Subject: [PATCH 85/99] Better whitespce --- techsupport_bot/commands/application.py | 1 - 1 file changed, 1 deletion(-) diff --git a/techsupport_bot/commands/application.py b/techsupport_bot/commands/application.py index c7361f8f1..296499f03 100644 --- a/techsupport_bot/commands/application.py +++ b/techsupport_bot/commands/application.py @@ -144,7 +144,6 @@ async def command_permission_check(interaction: discord.Interaction) -> bool: for role_id in config.extensions.application.manage_roles.value: if not role_id: continue - role = interaction.guild.get_role(int(role_id)) if not role: continue From 174c4cb8c88b5b3b0f0c2c9065805b8e1e5cb24f Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:56:33 -0400 Subject: [PATCH 86/99] Add length limit to report --- techsupport_bot/commands/report.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/techsupport_bot/commands/report.py b/techsupport_bot/commands/report.py index d6efb1012..f3c4730f5 100644 --- a/techsupport_bot/commands/report.py +++ b/techsupport_bot/commands/report.py @@ -50,6 +50,13 @@ async def report_command( interaction (discord.Interaction): The interaction that called this command report_str (str): The report string that the user submitted """ + if len(report_str) > 2000: + embed = auxiliary.prepare_deny_embed( + "Your report cannot be longer than 2000 characters." + ) + await interaction.response.send_message(embed=embed) + return + embed = discord.Embed(title="New Report", description=report_str) embed.color = discord.Color.red() embed.set_author( From e4d1aba85b44ac23abe3c4ee7d91941edd333cdb Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 30 Apr 2025 10:59:58 -0400 Subject: [PATCH 87/99] Add timestamp to automod/command alerts --- techsupport_bot/core/moderation.py | 9 ++++++--- techsupport_bot/functions/automod.py | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index f15ecb406..41db94a74 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -1,7 +1,7 @@ """This file will hold the core moderation functions. These functions will: Do the proper moderative action and return true if successful, false if not.""" -from datetime import timedelta +import datetime import discord import munch @@ -64,13 +64,15 @@ async def kick_user(guild: discord.Guild, user: discord.Member, reason: str) -> return True -async def mute_user(user: discord.Member, reason: str, duration: timedelta) -> bool: +async def mute_user( + user: discord.Member, reason: str, duration: datetime.timedelta +) -> bool: """Times out a given user Args: user (discord.Member): The user to timeout reason (str): The reason they are being timed out - duration (timedelta): How long to timeout the user for + duration (datetime.timedelta): How long to timeout the user for Returns: bool: True if the timeout was successful @@ -253,6 +255,7 @@ async def send_command_usage_alert( embed.set_thumbnail(url=ALERT_ICON_URL) embed.color = discord.Color.red() + embed.timestamp = datetime.datetime.utcnow() await alert_channel.send(embed=embed) diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 758e3a5bc..051e4d88b 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -2,6 +2,7 @@ from __future__ import annotations +import datetime import re from dataclasses import dataclass from datetime import timedelta @@ -216,7 +217,7 @@ async def response( await moderation.mute_user( user=ctx.author, reason=total_punishment.violation_string, - duration=timedelta(seconds=total_punishment.mute_duration), + duration=datetime.timedelta(seconds=total_punishment.mute_duration), ) if total_punishment.delete_message: @@ -445,6 +446,7 @@ def generate_automod_alert_embed( embed.set_thumbnail(url=ALERT_ICON_URL) embed.color = discord.Color.red() + embed.timestamp = datetime.datetime.utcnow() return embed From cfc368c5fa8bd12ad479d943a09df19086e99664 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:01:37 -0400 Subject: [PATCH 88/99] Remove timedelta --- techsupport_bot/functions/automod.py | 1 - 1 file changed, 1 deletion(-) diff --git a/techsupport_bot/functions/automod.py b/techsupport_bot/functions/automod.py index 051e4d88b..84c243700 100644 --- a/techsupport_bot/functions/automod.py +++ b/techsupport_bot/functions/automod.py @@ -5,7 +5,6 @@ import datetime import re from dataclasses import dataclass -from datetime import timedelta from typing import TYPE_CHECKING, Self import discord From 104e63834be8947bed779a8794af450257dbbab2 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:33:01 -0400 Subject: [PATCH 89/99] Move whois response defer --- techsupport_bot/commands/whois.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/techsupport_bot/commands/whois.py b/techsupport_bot/commands/whois.py index e78d8ed92..26f0008c5 100644 --- a/techsupport_bot/commands/whois.py +++ b/techsupport_bot/commands/whois.py @@ -41,6 +41,8 @@ async def whois_command( interaction (discord.Interaction): The interaction that called this command member (discord.Member): The member to lookup. Will not work on discord.User """ + await interaction.response.defer(ephemeral=True) + embed = auxiliary.generate_basic_embed( title=f"User info for `{member.display_name}` (`{member.name}`)", description="**Note: this is a bot account!**" if member.bot else "", @@ -130,7 +132,6 @@ async def whois_command( ) embeds.append(warning_embeds[0]) - await interaction.response.defer(ephemeral=True) view = ui.PaginateView() await view.send( interaction.channel, interaction.user, embeds, interaction, True From 2d091488790ae8b5d89938d77c4477d76cc9e806 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:33:57 -0400 Subject: [PATCH 90/99] Update desc for notes set command --- techsupport_bot/commands/notes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/notes.py b/techsupport_bot/commands/notes.py index 76c0c9134..8cc01ff71 100644 --- a/techsupport_bot/commands/notes.py +++ b/techsupport_bot/commands/notes.py @@ -159,7 +159,7 @@ class Notes(cogs.BaseCog): @app_commands.check(is_writer) @notes.command( name="set", - description="Sets a note for a user, which can be read later from their whois", + description="Adds a note to a given user.", extras={ "brief": "Sets a note for a user", "usage": "@user [note]", From 7a785f74de1f83fa5612d1b51b859392e95c7672 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 30 Apr 2025 12:14:49 -0400 Subject: [PATCH 91/99] Add jump to context to command usage alerts --- techsupport_bot/core/moderation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index 41db94a74..a653ebd49 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -237,7 +237,9 @@ async def send_command_usage_alert( embed.add_field(name="Command", value=f"`{command}`", inline=False) embed.add_field( name="Channel", - value=f"{interaction.channel.name} ({interaction.channel.mention})", + value=f"{interaction.channel.name} ({interaction.channel.mention}) [Jump to context]" + f"(https://discord.com/channels/{interaction.guild.id}/{interaction.channel.id}/" + f"{discord.utils.time_snowflake(datetime.datetime.utcnow())})", ) embed.add_field( From 71a292e4bb10e65a49a5d4f99a60ad42970ef247 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 30 Apr 2025 13:07:27 -0400 Subject: [PATCH 92/99] Make mute respond instead of raise --- techsupport_bot/commands/moderator.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 96b14f205..70b03c7f4 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -277,9 +277,6 @@ async def handle_mute_user( target (discord.Member): The target for being muted reason (str): The reason for being muted duration (str, optional): The human readable duration to be muted for. Defaults to None. - - Raises: - ValueError: Raised if the duration is invalid or cannot be parsed """ permission_check = await self.permission_check( invoker=interaction.user, target=target, action_name="mute" @@ -316,17 +313,29 @@ async def handle_mute_user( seconds=round(delta_duration.total_seconds()) ) except TypeError as exc: - raise ValueError("Invalid duration") from exc + embed = auxiliary.prepare_deny_embed(message="Invalid duration") + await interaction.response.send_message(embed=embed) + return if not delta_duration: - raise ValueError("Invalid duration") + embed = auxiliary.prepare_deny_embed(message="Invalid duration") + await interaction.response.send_message(embed=embed) + return else: delta_duration = timedelta(hours=1) # Checks to ensure time is valid and within the scope of the API if delta_duration > timedelta(days=28): - raise ValueError("Timeout duration cannot be more than 28 days") + embed = auxiliary.prepare_deny_embed( + message="Timeout duration cannot be more than 28 days" + ) + await interaction.response.send_message(embed=embed) + return if delta_duration < timedelta(seconds=1): - raise ValueError("Timeout duration cannot be less than 1 second") + embed = auxiliary.prepare_deny_embed( + message="Timeout duration cannot be less than 1 second" + ) + await interaction.response.send_message(embed=embed) + return result = await moderation.mute_user( user=target, From 19cb032512368e97cb220f6fd11a8d78cb7dbb14 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 30 Apr 2025 13:09:51 -0400 Subject: [PATCH 93/99] Remove unused variable --- techsupport_bot/commands/moderator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index 70b03c7f4..e4407a76b 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -312,7 +312,7 @@ async def handle_mute_user( delta_duration = timedelta( seconds=round(delta_duration.total_seconds()) ) - except TypeError as exc: + except TypeError: embed = auxiliary.prepare_deny_embed(message="Invalid duration") await interaction.response.send_message(embed=embed) return From ea4087166bb05de41749eeca783a624cd7bb6439 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 30 Apr 2025 13:14:20 -0400 Subject: [PATCH 94/99] Add return statements, make 500 character reason message consistent --- techsupport_bot/commands/moderator.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/techsupport_bot/commands/moderator.py b/techsupport_bot/commands/moderator.py index e4407a76b..79322df6d 100644 --- a/techsupport_bot/commands/moderator.py +++ b/techsupport_bot/commands/moderator.py @@ -96,6 +96,7 @@ async def handle_ban_user( message="Ban reason must be under 500 characters" ) await interaction.response.send_message(embed=embed) + return is_banned = await moderation.check_if_user_banned(target, interaction.guild) if is_banned: @@ -168,6 +169,7 @@ async def handle_unban_user( message="Unban reason must be under 500 characters" ) await interaction.response.send_message(embed=embed) + return is_banned = await moderation.check_if_user_banned(target, interaction.guild) @@ -232,9 +234,10 @@ async def handle_kick_user( if len(reason) > 500: embed = auxiliary.prepare_deny_embed( - message="Reason length is capped at 500 characters" + message="Kick reason must be under 500 characters" ) await interaction.response.send_message(embed=embed) + return result = await moderation.kick_user( guild=interaction.guild, @@ -288,9 +291,10 @@ async def handle_mute_user( if len(reason) > 500: embed = auxiliary.prepare_deny_embed( - message="Mute reason must be below 500 characters" + message="Mute reason must be under 500 characters" ) await interaction.response.send_message(embed=embed) + return # The API prevents administrators from being timed out. Check it here if target.guild_permissions.administrator: @@ -399,9 +403,10 @@ async def handle_unmute_user( if len(reason) > 500: embed = auxiliary.prepare_deny_embed( - message="Unmute reason must be below 500 characters" + message="Unmute reason must be under 500 characters" ) await interaction.response.send_message(embed=embed) + return if not target.timed_out_until: embed = auxiliary.prepare_deny_embed( @@ -459,9 +464,10 @@ async def handle_warn_user( if len(reason) > 500: embed = auxiliary.prepare_deny_embed( - message="Warn reason must be below 500 characters" + message="Warn reason must be under 500 characters" ) await interaction.response.send_message(embed=embed) + return if target not in interaction.channel.members: embed = auxiliary.prepare_deny_embed( @@ -594,9 +600,10 @@ async def handle_unwarn_user( if len(reason) > 500: embed = auxiliary.prepare_deny_embed( - message="Unwarn reason must be below 500 characters" + message="Unwarn reason must be under 500 characters" ) await interaction.response.send_message(embed=embed) + return database_warning = await self.get_warning(user=target, warning=warning) @@ -657,9 +664,10 @@ async def handle_warning_clear( if len(reason) > 500: embed = auxiliary.prepare_deny_embed( - message="Reason must be below 500 characters" + message="Reason must be under 500 characters" ) await interaction.response.send_message(embed=embed) + return warnings = await moderation.get_all_warnings( self.bot, target, interaction.guild From e7f6785924d587cfd4eb9d1db0cbb622c79b5853 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 30 Apr 2025 13:17:49 -0400 Subject: [PATCH 95/99] Fix protect command logging --- techsupport_bot/core/moderation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/techsupport_bot/core/moderation.py b/techsupport_bot/core/moderation.py index a653ebd49..c87859e7c 100644 --- a/techsupport_bot/core/moderation.py +++ b/techsupport_bot/core/moderation.py @@ -234,7 +234,7 @@ async def send_command_usage_alert( embed = discord.Embed(title="Command Usage Alert") - embed.add_field(name="Command", value=f"`{command}`", inline=False) + embed.description = f"**Command**\n`{command}`" embed.add_field( name="Channel", value=f"{interaction.channel.name} ({interaction.channel.mention}) [Jump to context]" From 2d5268256d44fc49ff564d2d849b90f661bd44ee Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 30 Apr 2025 18:00:43 -0400 Subject: [PATCH 96/99] Fix notes reader/writer in an actually smart way --- techsupport_bot/commands/notes.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/techsupport_bot/commands/notes.py b/techsupport_bot/commands/notes.py index 8cc01ff71..3f4877153 100644 --- a/techsupport_bot/commands/notes.py +++ b/techsupport_bot/commands/notes.py @@ -110,22 +110,7 @@ async def is_writer(interaction: discord.Interaction) -> bool: bool: True if the user can run, False if they cannot """ config = interaction.client.guild_configs[str(interaction.guild.id)] - reader_roles = config.extensions.notes.note_readers.value - writer_roles = config.extensions.notes.note_writers.value - - try: - await is_reader(interaction) - except app_commands.MissingAnyRole as e: - raise app_commands.MissingAnyRole(reader_roles) from e - except app_commands.AppCommandError as e: - message = "There aren't any `note_readers` roles set in the config!" - embed = auxiliary.prepare_deny_embed(message=message) - - await interaction.response.send_message(embed=embed, ephemeral=True) - - raise app_commands.AppCommandError(message) from e - - if writer_roles: + if writer_roles := config.extensions.notes.note_writers.value: roles = ( discord.utils.get(interaction.guild.roles, name=role) for role in writer_roles @@ -156,6 +141,7 @@ class Notes(cogs.BaseCog): name="notes", description="Command Group for the Notes Extension" ) + @app_commands.check(is_reader) @app_commands.check(is_writer) @notes.command( name="set", @@ -225,6 +211,7 @@ async def set_note( embed = auxiliary.prepare_confirm_embed(message=f"Note created for `{user}`") await interaction.response.send_message(embed=embed, ephemeral=True) + @app_commands.check(is_reader) @app_commands.check(is_writer) @notes.command( name="clear", From c8ea89a6dad8672dd80ab2279dfe2aaacc58b068 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Thu, 1 May 2025 09:36:16 -0400 Subject: [PATCH 97/99] Add empty line to flake8 --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index f88cb435d..2c5c893c1 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] ignore = DCO010, DCO023, DOC503, DOC602, DOC603, MDA002, E203, E501, E712, F401, F403, F821, W503 style = google -skip-checking-short-docstrings = False \ No newline at end of file +skip-checking-short-docstrings = False From 9d8357f89bd3b80c5b7b41259e7db31ab4d6c9cc Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 3 May 2025 20:22:33 -0400 Subject: [PATCH 98/99] Minor fixes --- techsupport_bot/commands/notes.py | 6 ------ techsupport_bot/commands/purge.py | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/techsupport_bot/commands/notes.py b/techsupport_bot/commands/notes.py index 3f4877153..2afe3dd6a 100644 --- a/techsupport_bot/commands/notes.py +++ b/techsupport_bot/commands/notes.py @@ -87,9 +87,6 @@ async def is_reader(interaction: discord.Interaction) -> bool: # Reader_roles are empty (not set) message = "There aren't any `note_readers` roles set in the config!" - embed = auxiliary.prepare_deny_embed(message=message) - - await interaction.response.send_message(embed=embed, ephemeral=True) raise app_commands.AppCommandError(message) @@ -122,9 +119,6 @@ async def is_writer(interaction: discord.Interaction) -> bool: # Reader_roles are empty (not set) message = "There aren't any `note_writers` roles set in the config!" - embed = auxiliary.prepare_deny_embed(message=message) - - await interaction.response.send_message(embed=embed, ephemeral=True) raise app_commands.AppCommandError(message) diff --git a/techsupport_bot/commands/purge.py b/techsupport_bot/commands/purge.py index 700b1332b..353f4c4e9 100644 --- a/techsupport_bot/commands/purge.py +++ b/techsupport_bot/commands/purge.py @@ -59,7 +59,7 @@ async def purge_command( if amount <= 0 or amount > config.extensions.purge.max_purge_amount.value: embed = auxiliary.prepare_deny_embed( message=( - "Messages to purge must be between 0 " + "Messages to purge must be between 1 " f"and {config.extensions.purge.max_purge_amount.value}" ), ) From d4eb0a726fcc36f2b8e97c82d4a7ffe3b0b3f08b Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Sat, 3 May 2025 20:32:25 -0400 Subject: [PATCH 99/99] Edit purge message to show how many were deleted --- techsupport_bot/commands/purge.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/techsupport_bot/commands/purge.py b/techsupport_bot/commands/purge.py index 353f4c4e9..0a25fb8c8 100644 --- a/techsupport_bot/commands/purge.py +++ b/techsupport_bot/commands/purge.py @@ -81,7 +81,12 @@ async def purge_command( timestamp = None await interaction.response.send_message("Purge Successful", ephemeral=True) - await interaction.channel.purge(after=timestamp, limit=amount) + sent_message = await interaction.original_response() + deleted = await interaction.channel.purge(after=timestamp, limit=amount) + await interaction.followup.edit_message( + message_id=sent_message.id, + content=f"Purge Successful. Deleted {len(deleted)} messages.", + ) await moderation.send_command_usage_alert( bot_object=self.bot,