feat(aurora): boilerplate for all currently added moderation types
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 26s
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 40s

This commit is contained in:
Seaswimmer 2024-07-06 13:03:59 -04:00
parent a05e957dde
commit 8f0425456c
Signed by: cswimr
GPG key ID: 3813315477F26F82
4 changed files with 201 additions and 21 deletions

View file

@ -9,7 +9,9 @@ from redbot.core import commands, data_manager
from redbot.core.utils.chat_formatting import warning from redbot.core.utils.chat_formatting import warning
from ..models.moderation import Moderation from ..models.moderation import Moderation
from ..models.type import Type
from ..utilities.json import dump from ..utilities.json import dump
from ..utilities.registry import type_registry
from ..utilities.utils import create_guild_table, timedelta_from_string 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() file = await self.ctx.message.attachments[0].read()
data: list[dict] = sorted(json.loads(file), key=lambda x: x["moderation_id"]) 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 = [] failed_cases = []
for case in data: for case in data:
if case["moderation_id"] == 0: if case["moderation_id"] == 0:
continue continue
moderation_type: Type = type_registry[str.lower(case["moderation_type"])]
if "target_type" not in case or not case["target_type"]: if "target_type" not in case or not case["target_type"]:
if case["moderation_type"] in user_mod_types: if moderation_type.channel:
case["target_type"] = "USER" case["target_type"] = "channel"
elif case["moderation_type"] in channel_mod_types:
case["target_type"] = "CHANNEL"
else: else:
case["target_type"] = "USER" case["target_type"] = "user"
if "role_id" not in case or not case["role_id"]: if "role_id" not in case or not case["role_id"]:
case["role_id"] = None case["role_id"] = None
@ -95,8 +92,8 @@ class ImportAuroraView(ui.View):
bot=interaction.client, bot=interaction.client,
guild_id=self.ctx.guild.id, guild_id=self.ctx.guild.id,
moderator_id=case["moderator_id"], moderator_id=case["moderator_id"],
moderation_type=case["moderation_type"], moderation_type=moderation_type.key,
target_type=case["target_type"], target_type=case["target_type"].lower(),
target_id=case["target_id"], target_id=case["target_id"],
role_id=case["role_id"], role_id=case["role_id"],
duration=duration, duration=duration,

View file

@ -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 discord.errors import HTTPException, NotFound
from redbot.core import app_commands, commands from redbot.core import app_commands, commands
from redbot.core.bot import Red from redbot.core.bot import Red
@ -19,6 +22,86 @@ def get_icon(bot: Red) -> File:
return get_footer_image(cog) return get_footer_image(cog)
raise ValueError("Aurora cog not found. How was this managed?") 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") @type_registry.register(key="ban")
class Ban(Type): class Ban(Type):
key="ban" key="ban"
@ -31,7 +114,7 @@ class Ban(Type):
bot = ctx.bot bot = ctx.bot
try: try:
await ctx.guild.fetch_ban(target) 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: except NotFound:
pass pass
@ -58,7 +141,7 @@ class Ban(Type):
except HTTPException: except HTTPException:
pass 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( moderation = await Moderation.log(
bot, bot,
ctx.guild.id, ctx.guild.id,
@ -110,7 +193,7 @@ class Tempban(Ban):
bot = ctx.bot bot = ctx.bot
try: try:
await ctx.guild.fetch_ban(target) 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: except NotFound:
pass pass
@ -145,7 +228,7 @@ class Tempban(Ban):
except HTTPException: except HTTPException:
pass 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( moderation = await Moderation.log(
bot, bot,
ctx.guild.id, ctx.guild.id,
@ -161,3 +244,101 @@ class Tempban(Ban):
await log(ctx, moderation.id) await log(ctx, moderation.id)
await send_evidenceformat(ctx, moderation.id) await send_evidenceformat(ctx, moderation.id)
return cls 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

View file

@ -2,6 +2,7 @@
from typing import Any from typing import Any
from discord import Guild, Member, User from discord import Guild, Member, User
from discord.abc import Messageable
from redbot.core import commands from redbot.core import commands
from redbot.core.bot import Red from redbot.core.bot import Red
@ -12,23 +13,24 @@ class Type(object):
string = "type" string = "type"
verb = "typed" verb = "typed"
embed_desc = "been" 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: def __str__(self) -> str:
return self.string return self.string
@classmethod @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.""" """This method should be overridden by any child classes, but should retain the same starting keyword arguments."""
raise NotImplementedError raise NotImplementedError
@classmethod @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. """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.""" If your moderation type should not be resolvable, do not override this."""
raise NotImplementedError raise NotImplementedError
@classmethod @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. """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.""" If your moderation type should not expire, do not override this."""
raise NotImplementedError raise NotImplementedError

View file

@ -9,14 +9,14 @@ from .registry import type_registry
from .utils import check_moddable 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. """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. It checks if the target can be moderated, then calls the handler method of the moderation type specified.
Args: Args:
bot (Red): The bot instance. 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`. 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. silent (bool | None): Whether to send the moderation action to the target.
permissions (List[str]): The permissions required to moderate 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. moderation_type (Type): The moderation type (handler) to use. See `aurora.models.moderation_types` for some examples.