From 8f0425456c6aef415c63c5811cd2c54ea42b710c Mon Sep 17 00:00:00 2001 From: Seaswimmer Date: Sat, 6 Jul 2024 13:03:59 -0400 Subject: [PATCH] feat(aurora): boilerplate for all currently added moderation types --- aurora/importers/aurora.py | 19 ++- aurora/models/moderation_types.py | 191 +++++++++++++++++++++++++++++- aurora/models/type.py | 8 +- aurora/utilities/moderate.py | 4 +- 4 files changed, 201 insertions(+), 21 deletions(-) diff --git a/aurora/importers/aurora.py b/aurora/importers/aurora.py index 2500993..0e1c7a9 100644 --- a/aurora/importers/aurora.py +++ b/aurora/importers/aurora.py @@ -9,7 +9,9 @@ from redbot.core import commands, data_manager from redbot.core.utils.chat_formatting import warning from ..models.moderation import Moderation +from ..models.type import Type from ..utilities.json import dump +from ..utilities.registry import type_registry from ..utilities.utils import create_guild_table, timedelta_from_string @@ -40,23 +42,18 @@ class ImportAuroraView(ui.View): file = await self.ctx.message.attachments[0].read() data: list[dict] = sorted(json.loads(file), key=lambda x: x["moderation_id"]) - user_mod_types = ["NOTE", "WARN", "ADDROLE", "REMOVEROLE", "MUTE", "UNMUTE", "KICK", "TEMPBAN", "BAN", "UNBAN"] - - channel_mod_types = ["SLOWMODE", "LOCKDOWN"] - failed_cases = [] for case in data: if case["moderation_id"] == 0: continue + moderation_type: Type = type_registry[str.lower(case["moderation_type"])] if "target_type" not in case or not case["target_type"]: - if case["moderation_type"] in user_mod_types: - case["target_type"] = "USER" - elif case["moderation_type"] in channel_mod_types: - case["target_type"] = "CHANNEL" + if moderation_type.channel: + case["target_type"] = "channel" else: - case["target_type"] = "USER" + case["target_type"] = "user" if "role_id" not in case or not case["role_id"]: case["role_id"] = None @@ -95,8 +92,8 @@ class ImportAuroraView(ui.View): bot=interaction.client, guild_id=self.ctx.guild.id, moderator_id=case["moderator_id"], - moderation_type=case["moderation_type"], - target_type=case["target_type"], + moderation_type=moderation_type.key, + target_type=case["target_type"].lower(), target_id=case["target_id"], role_id=case["role_id"], duration=duration, diff --git a/aurora/models/moderation_types.py b/aurora/models/moderation_types.py index 5022698..db36dd0 100644 --- a/aurora/models/moderation_types.py +++ b/aurora/models/moderation_types.py @@ -1,5 +1,8 @@ -from discord import File, Guild, Member, User +from math import ceil + +from discord import File, Guild, Member, TextChannel, User +from discord.abc import Messageable from discord.errors import HTTPException, NotFound from redbot.core import app_commands, commands from redbot.core.bot import Red @@ -19,6 +22,86 @@ def get_icon(bot: Red) -> File: return get_footer_image(cog) raise ValueError("Aurora cog not found. How was this managed?") +@type_registry.register(key="note") +class Note(Type): + key="note" + string="note" + verb="noted" + +@type_registry.register(key="warn") +class Warn(Type): + key="warn" + string="warn" + verb="warned" + +@type_registry.register(key="addrole") +class AddRole(Type): + key="addrole" + string="addrole" + verb="added a role to" + +@type_registry.register(key="removerole") +class RemoveRole(Type): + key="removerole" + string="removerole" + verb="removed a role from" + +@type_registry.register(key="mute") +class Mute(Type): + key="mute" + string="mute" + verb="muted" + +@type_registry.register(key="unmute") +class Unmute(Type): + key="unmute" + string="unmute" + verb="unmuted" + +@type_registry.register(key="kick") +class Kick(Type): + key="kick" + string="kick" + verb="kicked" + + @classmethod + async def handler(cls, ctx: commands.Context, target: Member | User, silent: bool, reason: str = None) -> 'Kick': + """Kick a user.""" + bot = ctx.bot + response_message = await ctx.send(f"{target.mention} has been {cls.verb}!\n{bold('Reason:')} {inline(reason)}") + + if silent is False: + try: + embed = await message_factory( + bot, + await bot.get_embed_color(ctx.channel), + ctx.guild, + reason, + cls(), + ctx.author, + None, + response_message + ) + await target.send(embed=embed, file=get_icon(bot)) + except HTTPException: + pass + + await target.kick(reason=f"{str.title(cls.verb)} by {ctx.author.id} for: {reason}") + moderation = await Moderation.log( + bot, + ctx.guild.id, + ctx.author.id, + cls(), + 'user', + target.id, + None, + None, + reason + ) + await response_message.edit(content=f"{target.mention} has been {cls.verb}! (Case {inline(f'#{moderation.id}')})\n{bold('Reason:')} {inline(reason)}") + await log(ctx, moderation.id) + await send_evidenceformat(ctx, moderation.id) + return cls @type_registry.register(key="ban") class Ban(Type): key="ban" @@ -31,7 +114,7 @@ class Ban(Type): bot = ctx.bot try: await ctx.guild.fetch_ban(target) - await ctx.send(content=error(f"{target.mention} is already banned!"), ephemeral=True) + await ctx.send(content=error(f"{target.mention} is already {cls.verb}!"), ephemeral=True) except NotFound: pass @@ -58,7 +141,7 @@ class Ban(Type): except HTTPException: pass - await ctx.guild.ban(target, reason=f"Banned by {ctx.author.id} for: {reason}", delete_message_seconds=delete_messages_seconds) + await ctx.guild.ban(target, reason=f"{str.title(cls.verb)} by {ctx.author.id} for: {reason}", delete_message_seconds=delete_messages_seconds) moderation = await Moderation.log( bot, ctx.guild.id, @@ -110,7 +193,7 @@ class Tempban(Ban): bot = ctx.bot try: await ctx.guild.fetch_ban(target) - await ctx.send(content=error(f"{target.mention} is already banned!"), ephemeral=True) + await ctx.send(content=error(f"{target.mention} is already {Ban.verb}!"), ephemeral=True) except NotFound: pass @@ -145,7 +228,7 @@ class Tempban(Ban): except HTTPException: pass - await ctx.guild.ban(target, reason=f"Tempbanned by {ctx.author.id} for: {reason} (Duration: {parsed_time})", delete_message_seconds=delete_messages_seconds) + await ctx.guild.ban(target, reason=f"{str.title(cls.verb)} by {ctx.author.id} for: {reason} (Duration: {parsed_time})", delete_message_seconds=delete_messages_seconds) moderation = await Moderation.log( bot, ctx.guild.id, @@ -161,3 +244,101 @@ class Tempban(Ban): await log(ctx, moderation.id) await send_evidenceformat(ctx, moderation.id) return cls + +@type_registry.register(key="unban") +class Unban(Type): + key="unban" + string="unban" + verb="unbanned" + + @classmethod + async def handler(cls, ctx: commands.Context, target: Member | User, silent: bool, reason: str = None) -> 'Unban': + """Unban a user.""" + bot = ctx.bot + try: + await ctx.guild.fetch_ban(target) + except NotFound: + await ctx.send(content=error(f"{target.mention} is not {Ban.verb}!"), ephemeral=True) + return + + response_message = await ctx.send(f"{target.mention} has been {cls.verb}!\n{bold('Reason:')} {inline(reason)}") + + if silent is False: + try: + embed = await message_factory( + bot, + await bot.get_embed_color(ctx.channel), + ctx.guild, + reason, + cls(), + ctx.author, + None, + response_message + ) + await target.send(embed=embed, file=get_icon(bot)) + except HTTPException: + pass + + await ctx.guild.unban(target, reason=f"{str.title(cls.verb)} by {ctx.author.id} for: {reason}") + moderation = await Moderation.log( + bot, + ctx.guild.id, + ctx.author.id, + cls(), + 'user', + target.id, + None, + None, + reason + ) + await response_message.edit(content=f"{target.mention} has been {cls.verb}! (Case {inline(f'#{moderation.id}')})\n{bold('Reason:')} {inline(reason)}") + await log(ctx, moderation.id) + await send_evidenceformat(ctx, moderation.id) + return cls + +@type_registry.register(key="slowmode") +class Slowmode(Type): + key="slowmode" + string="slowmode" + verb="set the slowmode in" + channel=True + + @classmethod + async def handler(cls, ctx: commands.Context, target: Messageable, silent: bool, duration: str, reason: str) -> 'Slowmode': # pylint: disable=unused-argument + """Set the slowmode in a channel.""" + bot = ctx.bot + parsed_time = parse_relativedelta(duration) + if not parsed_time: + await ctx.send(content=error("Please provide a valid duration!"), ephemeral=True) + try: + parsed_time = timedelta_from_relativedelta(parsed_time) + except ValueError: + await ctx.send(content=error("Please provide a valid duration!"), ephemeral=True) + + if ceil(parsed_time.total_seconds()) > 21600: + await ctx.send(content=error("The slowmode duration cannot exceed 6 hours!"), ephemeral=True) + return + + if isinstance(target, TextChannel): + await target.edit(slowmode_delay=ceil(parsed_time.total_seconds())) + moderation = await Moderation.log( + bot, + ctx.guild.id, + ctx.author.id, + cls(), + 'channel', + target.id, + None, + parsed_time, + None + ) + await ctx.send(f"{ctx.author.mention} has {cls.verb} {target.mention} to {humanize_timedelta(parsed_time)}!\n{bold('Reason:')} {inline(reason)}") + await log(ctx, moderation.id) + return cls + +@type_registry.register(key="lockdown") +class Lockdown(Type): + key="lockdown" + string="lockdown" + verb="locked down" + channel=True diff --git a/aurora/models/type.py b/aurora/models/type.py index 11a0eae..92e734b 100644 --- a/aurora/models/type.py +++ b/aurora/models/type.py @@ -2,6 +2,7 @@ from typing import Any from discord import Guild, Member, User +from discord.abc import Messageable from redbot.core import commands from redbot.core.bot import Red @@ -12,23 +13,24 @@ class Type(object): string = "type" verb = "typed" embed_desc = "been" + channel = False # if this is True, the overridden handler methods should be typed with `discord.abc.Messageable` instead of `discord.Member | discord.User` def __str__(self) -> str: return self.string @classmethod - async def handler(cls, ctx: commands.Context, target: Member | User, silent: bool, **kwargs) -> 'Type': # pylint: disable=unused-argument + async def handler(cls, ctx: commands.Context, target: Member | User | Messageable, silent: bool, **kwargs) -> 'Type': # pylint: disable=unused-argument """This method should be overridden by any child classes, but should retain the same starting keyword arguments.""" raise NotImplementedError @classmethod - async def resolve_handler(cls, bot: Red, guild: Guild, target: Member | User, reason: str | None = None) -> Any: # pylint: disable=unused-argument + async def resolve_handler(cls, bot: Red, guild: Guild, target: Member | User | Messageable, reason: str | None = None, **kwargs) -> Any: # pylint: disable=unused-argument """This method should be overridden by any resolvable child classes, but should retain the same keyword arguments. If your moderation type should not be resolvable, do not override this.""" raise NotImplementedError @classmethod - async def expiry_handler(cls, bot: Red, guild: Guild, target: Member | User) -> Any: # pylint: disable=unused-argument + async def expiry_handler(cls, bot: Red, guild: Guild, target: Member | User | Messageable, **kwargs) -> Any: # pylint: disable=unused-argument """This method should be overridden by any expirable child classes, but should retain the same keyword arguments. If your moderation type should not expire, do not override this.""" raise NotImplementedError diff --git a/aurora/utilities/moderate.py b/aurora/utilities/moderate.py index b0f6ca1..05fc245 100644 --- a/aurora/utilities/moderate.py +++ b/aurora/utilities/moderate.py @@ -9,14 +9,14 @@ from .registry import type_registry from .utils import check_moddable -async def moderate(ctx: Union[commands.Context, discord.Interaction], target: discord.Member, silent: bool | None, permissions: List[str], moderation_type: Type | str, **kwargs) -> None | Type: +async def moderate(ctx: Union[commands.Context, discord.Interaction], target: discord.Member | discord.User | discord.abc.Messageable, silent: bool | None, permissions: List[str], moderation_type: Type | str, **kwargs) -> None | Type: """This function is used to moderate users. It checks if the target can be moderated, then calls the handler method of the moderation type specified. Args: bot (Red): The bot instance. ctx (Union[commands.Context, discord.Interaction]): The context of the command. If this is a `discord.Interaction` object, it will be converted to a `commands.Context` object. Additionally, if the interaction orignated from a context menu, the `ctx.author` attribute will be overriden to `interaction.user`. - target (discord.Member): The target user to moderate. + target (discord.Member, discord.User, discord.abc.Messageable): The target user or channel to moderate. silent (bool | None): Whether to send the moderation action to the target. permissions (List[str]): The permissions required to moderate the target. moderation_type (Type): The moderation type (handler) to use. See `aurora.models.moderation_types` for some examples.