SeaCogs/aurora/aurora.py

1658 lines
63 KiB
Python
Raw Normal View History

2023-12-14 18:55:35 -05:00
# _____ _
# / ____| (_)
# | (___ ___ __ _ _____ ___ _ __ ___ _ __ ___ ___ _ __
# \___ \ / _ \/ _` / __\ \ /\ / / | '_ ` _ \| '_ ` _ \ / _ \ '__|
# ____) | __/ (_| \__ \\ V V /| | | | | | | | | | | | __/ |
# |_____/ \___|\__,_|___/ \_/\_/ |_|_| |_| |_|_| |_| |_|\___|_|
2023-12-14 18:35:25 -05:00
import json
import logging as py_logging
2023-12-14 18:35:25 -05:00
import os
2024-02-02 11:21:56 -05:00
import time
2023-12-14 18:35:25 -05:00
from datetime import datetime, timedelta, timezone
from math import ceil
2023-12-14 18:35:25 -05:00
import discord
from discord import Object
2023-12-14 18:35:25 -05:00
from discord.ext import tasks
2024-01-15 06:49:20 -05:00
from redbot.core import app_commands, commands, data_manager
2023-12-14 18:35:25 -05:00
from redbot.core.app_commands import Choice
from redbot.core.bot import Red
from redbot.core.commands.converter import parse_relativedelta, parse_timedelta
2024-06-08 20:12:22 -04:00
from redbot.core.utils.chat_formatting import bold, box, error, humanize_list, humanize_timedelta, warning
2024-05-06 21:39:43 -04:00
from .importers.aurora import ImportAuroraView
from .importers.galacticbot import ImportGalacticBotView
from .menus.addrole import Addrole
from .menus.guild import Guild
from .menus.immune import Immune
from .menus.overrides import Overrides
from .models.change import Change
from .models.moderation import Moderation
from .models.moderation_types import Ban, Tempban
2024-05-06 21:39:43 -04:00
from .utilities.config import config, register_config
2024-05-24 03:46:20 -04:00
from .utilities.factory import addrole_embed, case_factory, changes_factory, evidenceformat_factory, guild_embed, immune_embed, message_factory, overrides_embed
2024-05-06 21:39:43 -04:00
from .utilities.json import dump
from .utilities.logger import logger
from .utilities.moderate import moderate
from .utilities.registry import type_registry
from .utilities.utils import check_moddable, check_permissions, create_guild_table, get_footer_image, log, send_evidenceformat
2024-05-06 20:45:22 -04:00
class Aurora(commands.Cog):
"""Aurora is a fully-featured moderation system.
It is heavily inspired by GalacticBot, and is designed to be a more user-friendly alternative to Red's core Mod cogs.
This cog stores all of its data in an SQLite database."""
__author__ = ["SeaswimmerTheFsh"]
__version__ = "2.4.0"
__documentation__ = "https://seacogs.coastalcommits.com/aurora/"
2023-12-14 18:35:25 -05:00
async def red_delete_data_for_user(self, *, requester, user_id: int):
if requester == "discord_deleted_user":
await config.user_from_id(user_id).clear()
2024-06-08 20:12:22 -04:00
results = await Moderation.execute(query="SHOW TABLES;", return_obj=False)
tables = [table[0] for table in results]
condition = "target_id = %s OR moderator_id = %s;"
for table in tables:
delete_query = f"DELETE FROM {table[0]} WHERE {condition}"
2024-06-08 20:12:22 -04:00
await Moderation.execute(query=delete_query, parameters=(user_id, user_id), return_obj=False)
if requester == "owner":
await config.user_from_id(user_id).clear()
if requester == "user":
await config.user_from_id(user_id).clear()
if requester == "user_strict":
await config.user_from_id(user_id).clear()
else:
2024-02-02 11:21:56 -05:00
logger.warning(
"Invalid requester passed to red_delete_data_for_user: %s", requester
)
def __init__(self, bot: Red) -> None:
super().__init__()
2023-12-14 18:35:25 -05:00
self.bot = bot
self.type_registry = type_registry
register_config(config)
self.handle_expiry.start()
# If we don't override aiosqlite's logging level, it will spam the console with dozens of debug messages per query.
# This is unnecessary because Aurora already logs all of its SQL queries (or at least, most of them),
# and the information that aiosqlite logs is not useful to the bot owner.
# This is a bad solution though as it overrides it for any other cogs that are using aiosqlite too.
# If there's a better solution that you're aware of, please let me know in Discord or in a CoastalCommits issue.
2024-06-08 20:12:22 -04:00
py_logging.getLogger('aiosqlite').setLevel(py_logging.INFO)
2023-12-14 18:35:25 -05:00
def format_help_for_context(self, ctx: commands.Context) -> str:
pre_processed = super().format_help_for_context(ctx) or ""
n = "\n" if "\n\n" not in pre_processed else ""
text = [
f"{pre_processed}{n}",
2024-06-08 20:12:22 -04:00
f"{bold('Cog Version:')} {self.__version__}",
f"{bold('Author:')} {humanize_list(self.__author__)}",
f"{bold('Documentation:')} {self.__documentation__}",
]
return "\n".join(text)
2023-12-14 18:35:25 -05:00
async def cog_load(self):
"""This method prepares the database schema for all of the guilds the bot is currently in."""
2023-12-14 18:35:25 -05:00
guilds: list[discord.Guild] = self.bot.guilds
try:
for guild in guilds:
if not await self.bot.cog_disabled_in_guild(self, guild):
2023-12-17 02:36:18 -05:00
await create_guild_table(guild)
2023-12-14 18:35:25 -05:00
except ConnectionRefusedError:
return
async def cog_unload(self):
self.handle_expiry.cancel()
2023-12-14 18:35:25 -05:00
2024-02-02 11:21:56 -05:00
@commands.Cog.listener("on_guild_join")
2023-12-14 18:35:25 -05:00
async def db_generate_guild_join(self, guild: discord.Guild):
"""This method prepares the database schema whenever the bot joins a guild."""
if not await self.bot.cog_disabled_in_guild(self, guild):
try:
await create_guild_table(guild)
2023-12-14 18:35:25 -05:00
except ConnectionRefusedError:
return
@commands.Cog.listener("on_member_join")
async def addrole_on_member_join(self, member: discord.Member):
"""This method automatically adds roles to users when they join the server."""
if not await self.bot.cog_disabled_in_guild(self, member.guild):
query = f"""SELECT moderation_id, role_id, reason FROM moderation_{member.guild.id} WHERE target_id = ? AND moderation_type = 'ADDROLE' AND expired = 0 AND resolved = 0;"""
2024-06-08 20:12:22 -04:00
results = Moderation.execute(query, (member.id,))
for row in results:
role = member.guild.get_role(row[1])
reason = row[2]
await member.add_roles(role, reason=f"Role automatically added on member rejoin for: {reason} (Case #{row[0]:,})")
2024-02-02 11:21:56 -05:00
@commands.Cog.listener("on_audit_log_entry_create")
2023-12-14 18:35:25 -05:00
async def autologger(self, entry: discord.AuditLogEntry):
"""This method automatically logs moderations done by users manually ("right clicks")."""
if not await self.bot.cog_disabled_in_guild(self, entry.guild):
if await config.guild(entry.guild).ignore_other_bots() is True:
2023-12-14 18:35:25 -05:00
if entry.user.bot or entry.target.bot:
return
else:
if entry.user.id == self.bot.user.id:
return
duration = None
2023-12-14 18:35:25 -05:00
if entry.reason:
reason = entry.reason + " (This action was performed without the bot.)"
else:
reason = "This action was performed without the bot."
if entry.action == discord.AuditLogAction.kick:
2024-02-02 11:21:56 -05:00
moderation_type = "KICK"
2023-12-14 18:35:25 -05:00
elif entry.action == discord.AuditLogAction.ban:
2024-02-02 11:21:56 -05:00
moderation_type = "BAN"
2023-12-14 18:35:25 -05:00
elif entry.action == discord.AuditLogAction.unban:
2024-02-02 11:21:56 -05:00
moderation_type = "UNBAN"
2023-12-14 18:35:25 -05:00
elif entry.action == discord.AuditLogAction.member_update:
if entry.after.timed_out_until is not None:
2024-02-02 11:21:56 -05:00
timed_out_until_aware = entry.after.timed_out_until.replace(
tzinfo=timezone.utc
)
duration_datetime = timed_out_until_aware - datetime.now(
tz=timezone.utc
)
2023-12-14 18:35:25 -05:00
minutes = round(duration_datetime.total_seconds() / 60)
duration = timedelta(minutes=minutes)
2024-02-02 11:21:56 -05:00
moderation_type = "MUTE"
2023-12-14 18:35:25 -05:00
else:
2024-02-02 11:21:56 -05:00
moderation_type = "UNMUTE"
2023-12-14 18:35:25 -05:00
else:
return
2024-06-05 00:14:43 -04:00
await Moderation.log(
self.bot,
2024-02-02 11:21:56 -05:00
entry.guild.id,
entry.user.id,
moderation_type,
"USER",
entry.target.id,
None,
2024-02-02 11:21:56 -05:00
duration,
reason,
)
2023-12-14 18:35:25 -05:00
#######################################################################################################################
### COMMANDS
#######################################################################################################################
@app_commands.command(name="note")
2024-02-02 11:21:56 -05:00
async def note(
self,
interaction: discord.Interaction,
target: discord.User,
reason: str,
2024-05-06 21:04:08 -04:00
silent: bool | None = None,
2024-02-02 11:21:56 -05:00
):
2023-12-14 18:35:25 -05:00
"""Add a note to a user.
Parameters
-----------
target: discord.User
Who are you noting?
reason: str
Why are you noting this user?
silent: bool
Should the user be messaged?"""
2024-02-02 11:21:56 -05:00
if not await check_moddable(target, interaction, ["moderate_members"]):
2023-12-15 13:54:58 -05:00
return
2023-12-14 18:35:25 -05:00
2024-02-02 11:21:56 -05:00
await interaction.response.send_message(
content=f"{target.mention} has recieved a note!\n**Reason** - `{reason}`"
)
2023-12-14 18:35:25 -05:00
if silent is None:
silent = not await config.guild(interaction.guild).dm_users()
2023-12-14 18:35:25 -05:00
if silent is False:
try:
2024-02-02 11:21:56 -05:00
embed = await message_factory(
bot=interaction.client,
color=await self.bot.get_embed_color(interaction.channel),
2024-02-02 11:21:56 -05:00
guild=interaction.guild,
moderator=interaction.user,
reason=reason,
moderation_type="note",
response=await interaction.original_response(),
)
await target.send(embed=embed, file=get_footer_image(self))
2023-12-14 18:35:25 -05:00
except discord.errors.HTTPException:
pass
2024-06-05 00:14:43 -04:00
moderation = await Moderation.log(
interaction.client,
2024-02-02 11:21:56 -05:00
interaction.guild.id,
interaction.user.id,
"NOTE",
"USER",
target.id,
None,
None,
2024-02-02 11:21:56 -05:00
reason,
)
await interaction.edit_original_response(
content=f"{target.mention} has received a note! (Case `#{moderation.id:,}`)\n**Reason** - `{reason}`"
2024-02-02 11:21:56 -05:00
)
await log(interaction, moderation.id)
await send_evidenceformat(interaction, moderation.id)
2023-12-14 18:35:25 -05:00
@app_commands.command(name="warn")
2024-02-02 11:21:56 -05:00
async def warn(
self,
interaction: discord.Interaction,
target: discord.Member,
reason: str,
2024-05-06 21:04:08 -04:00
silent: bool | None = None,
2024-02-02 11:21:56 -05:00
):
2023-12-14 18:35:25 -05:00
"""Warn a user.
Parameters
-----------
target: discord.Member
Who are you warning?
reason: str
Why are you warning this user?
silent: bool
Should the user be messaged?"""
2024-02-02 11:21:56 -05:00
if not await check_moddable(target, interaction, ["moderate_members"]):
2023-12-15 13:54:58 -05:00
return
2023-12-14 18:35:25 -05:00
2024-02-02 11:21:56 -05:00
await interaction.response.send_message(
content=f"{target.mention} has been warned!\n**Reason** - `{reason}`"
)
2023-12-14 18:35:25 -05:00
if silent is None:
silent = not await config.guild(interaction.guild).dm_users()
2023-12-14 18:35:25 -05:00
if silent is False:
try:
2024-02-02 11:21:56 -05:00
embed = await message_factory(
bot=interaction.client,
color=await self.bot.get_embed_color(interaction.channel),
2024-02-02 11:21:56 -05:00
guild=interaction.guild,
moderator=interaction.user,
reason=reason,
moderation_type="warned",
response=await interaction.original_response(),
)
await target.send(embed=embed, file=get_footer_image(self))
2023-12-14 18:35:25 -05:00
except discord.errors.HTTPException:
pass
2024-06-05 00:14:43 -04:00
moderation = await Moderation.log(
interaction.client,
2024-02-02 11:21:56 -05:00
interaction.guild.id,
interaction.user.id,
"WARN",
"USER",
target.id,
None,
None,
2024-02-02 11:21:56 -05:00
reason,
)
await interaction.edit_original_response(
content=f"{target.mention} has been warned! (Case `#{moderation.id:,}`)\n**Reason** - `{reason}`"
2024-02-02 11:21:56 -05:00
)
await log(interaction, moderation.id)
await send_evidenceformat(interaction, moderation.id)
2024-02-02 11:21:56 -05:00
@app_commands.command(name="addrole")
async def addrole(
self,
interaction: discord.Interaction,
target: discord.Member,
role: discord.Role,
reason: str,
2024-05-06 21:04:08 -04:00
duration: str | None = None,
silent: bool | None = None,
2024-02-02 11:21:56 -05:00
):
"""Add a role to a user.
Parameters
-----------
target: discord.Member
2024-02-02 11:21:56 -05:00
Who are you adding a role to?
role: discord.Role
2024-02-02 11:21:56 -05:00
What role are you adding to the target?
reason: str
2024-02-02 11:21:56 -05:00
Why are you adding a role to this user?
2024-01-06 13:31:59 -05:00
duration: str
2024-02-02 11:21:56 -05:00
How long are you adding this role for?
silent: bool
Should the user be messaged?"""
2024-02-02 11:21:56 -05:00
addrole_whitelist = await config.guild(interaction.guild).addrole_whitelist()
2024-02-02 11:21:56 -05:00
if not addrole_whitelist:
await interaction.response.send_message(
content=error("There are no whitelisted roles set for this server!"),
ephemeral=True,
)
return
2024-02-02 11:21:56 -05:00
if duration is not None:
parsed_time = parse_timedelta(duration)
if parsed_time is None:
2024-02-02 11:21:56 -05:00
await interaction.response.send_message(
content=error("Please provide a valid duration!"), ephemeral=True
2024-02-02 11:21:56 -05:00
)
return
else:
parsed_time = None
2024-02-02 11:21:56 -05:00
2024-02-14 10:39:26 -05:00
if role.id not in addrole_whitelist:
2024-02-02 11:21:56 -05:00
await interaction.response.send_message(
content=error("That role isn't whitelisted!"), ephemeral=True
)
return
2024-02-02 11:21:56 -05:00
if not await check_moddable(
target, interaction, ["moderate_members", "manage_roles"]
):
return
2024-02-02 11:43:27 -05:00
if role.id in [user_role.id for user_role in target.roles]:
2024-02-02 11:21:56 -05:00
await interaction.response.send_message(
content=error(f"{target.mention} already has this role!"),
ephemeral=True,
)
return
await interaction.response.defer()
2023-12-17 02:36:18 -05:00
if silent is None:
silent = not await config.guild(interaction.guild).dm_users()
2023-12-17 02:36:18 -05:00
if silent is False:
try:
2024-02-02 11:21:56 -05:00
embed = await message_factory(
bot=interaction.client,
color=await self.bot.get_embed_color(interaction.channel),
2024-02-02 11:21:56 -05:00
guild=interaction.guild,
moderator=interaction.user,
reason=reason,
2024-02-02 12:55:34 -05:00
moderation_type="addrole",
2024-02-02 11:21:56 -05:00
response=await interaction.original_response(),
duration=parsed_time,
role=role,
2024-02-02 11:21:56 -05:00
)
await target.send(embed=embed, file=get_footer_image(self))
2023-12-17 02:36:18 -05:00
except discord.errors.HTTPException:
pass
2024-02-02 11:21:56 -05:00
await target.add_roles(
role,
reason=f"Role added by {interaction.user.id}{' for ' + humanize_timedelta(timedelta=parsed_time) if parsed_time != 'NULL' else ''} for: {reason}",
2024-02-02 11:21:56 -05:00
)
response: discord.WebhookMessage = await interaction.followup.send(
content=f"{target.mention} has been given the {role.mention} role{' for ' + humanize_timedelta(timedelta=parsed_time) if parsed_time != 'NULL' else ''}!\n**Reason** - `{reason}`"
2024-02-02 11:21:56 -05:00
)
2024-06-05 00:14:43 -04:00
moderation = await Moderation.log(
interaction.client,
2024-02-02 11:21:56 -05:00
interaction.guild.id,
interaction.user.id,
"ADDROLE",
"USER",
target.id,
role.id,
parsed_time,
2024-02-02 11:21:56 -05:00
reason,
)
await response.edit(
content=f"{target.mention} has been given the {role.mention} role{' for ' + humanize_timedelta(timedelta=parsed_time) if parsed_time != 'NULL' else ''}! (Case `#{moderation.id:,}`)\n**Reason** - `{reason}`",
2024-02-02 11:21:56 -05:00
)
await log(interaction, moderation.id)
await send_evidenceformat(interaction, moderation.id)
@app_commands.command(name="removerole")
async def removerole(
self,
interaction: discord.Interaction,
target: discord.Member,
role: discord.Role,
reason: str,
2024-05-06 21:04:08 -04:00
duration: str | None = None,
silent: bool | None = None,
):
"""Remove a role from a user.
Parameters
-----------
target: discord.Member
Who are you removing a role from?
role: discord.Role
What role are you removing from the target?
reason: str
Why are you removing a role from this user?
duration: str
How long are you removing this role for?
silent: bool
Should the user be messaged?"""
addrole_whitelist = await config.guild(interaction.guild).addrole_whitelist()
if not addrole_whitelist:
await interaction.response.send_message(
content=error("There are no whitelisted roles set for this server!"),
ephemeral=True,
)
return
if duration is not None:
parsed_time = parse_timedelta(duration)
if parsed_time is None:
await interaction.response.send_message(
content=error("Please provide a valid duration!"), ephemeral=True
)
return
else:
parsed_time = None
if role.id not in addrole_whitelist:
await interaction.response.send_message(
content=error("That role isn't whitelisted!"), ephemeral=True
)
return
if not await check_moddable(
target, interaction, ["moderate_members", "manage_roles"]
):
return
if role.id not in [user_role.id for user_role in target.roles]:
await interaction.response.send_message(
content=error(f"{target.mention} does not have this role!"),
ephemeral=True,
)
return
await interaction.response.defer()
if silent is None:
silent = not await config.guild(interaction.guild).dm_users()
if silent is False:
try:
embed = await message_factory(
bot=interaction.client,
color=await self.bot.get_embed_color(interaction.channel),
guild=interaction.guild,
moderator=interaction.user,
reason=reason,
moderation_type="removerole",
response=await interaction.original_response(),
duration=parsed_time,
role=role,
)
await target.send(embed=embed, file=get_footer_image(self))
except discord.errors.HTTPException:
pass
await target.remove_roles(
role,
reason=f"Role removed by {interaction.user.id}{' for ' + humanize_timedelta(timedelta=parsed_time) if parsed_time != 'NULL' else ''} for: {reason}",
)
response: discord.WebhookMessage = await interaction.followup.send(
content=f"{target.mention} has had the {role.mention} role removed{' for ' + humanize_timedelta(timedelta=parsed_time) if parsed_time != 'NULL' else ''}!\n**Reason** - `{reason}`"
)
2024-06-05 00:14:43 -04:00
moderation = await Moderation.log(
interaction.client,
interaction.guild.id,
interaction.user.id,
"REMOVEROLE",
"USER",
target.id,
role.id,
parsed_time,
reason,
)
await response.edit(
content=f"{target.mention} has had the {role.mention} role removed{' for ' + humanize_timedelta(timedelta=parsed_time) if parsed_time != 'NULL' else ''}! (Case `#{moderation.id:,}`)\n**Reason** - `{reason}`",
)
await log(interaction, moderation.id)
await send_evidenceformat(interaction, moderation.id)
2023-12-14 18:35:25 -05:00
@app_commands.command(name="mute")
2024-02-02 11:21:56 -05:00
async def mute(
self,
interaction: discord.Interaction,
target: discord.Member,
duration: str,
reason: str,
2024-05-06 21:04:08 -04:00
silent: bool | None = None,
2024-02-02 11:21:56 -05:00
):
2023-12-14 18:35:25 -05:00
"""Mute a user.
Parameters
-----------
target: discord.Member
Who are you unbanning?
duration: str
How long are you muting this user for?
reason: str
Why are you unbanning this user?
silent: bool
Should the user be messaged?"""
2024-02-02 11:21:56 -05:00
if not await check_moddable(target, interaction, ["moderate_members"]):
2023-12-14 18:35:25 -05:00
return
2023-12-15 13:54:58 -05:00
2023-12-14 18:35:25 -05:00
if target.is_timed_out() is True:
2024-02-02 11:21:56 -05:00
await interaction.response.send_message(
error(f"{target.mention} is already muted!"),
allowed_mentions=discord.AllowedMentions(users=False),
ephemeral=True,
)
2023-12-14 18:35:25 -05:00
return
try:
parsed_time = parse_timedelta(duration, maximum=timedelta(days=28))
if parsed_time is None:
await interaction.response.send_message(
error("Please provide a valid duration!"), ephemeral=True
)
return
except commands.BadArgument:
2024-02-02 11:21:56 -05:00
await interaction.response.send_message(
error("Please provide a duration that is less than 28 days."), ephemeral=True
2024-02-02 11:21:56 -05:00
)
2023-12-14 18:35:25 -05:00
return
2024-02-02 11:21:56 -05:00
await target.timeout(
parsed_time, reason=f"Muted by {interaction.user.id} for: {reason}"
)
2023-12-14 18:35:25 -05:00
2024-02-02 11:21:56 -05:00
await interaction.response.send_message(
2024-03-08 14:56:50 -05:00
content=f"{target.mention} has been muted for {humanize_timedelta(timedelta=parsed_time)}!\n**Reason** - `{reason}`"
2024-02-02 11:21:56 -05:00
)
2023-12-14 18:35:25 -05:00
if silent is None:
silent = not await config.guild(interaction.guild).dm_users()
2023-12-14 18:35:25 -05:00
if silent is False:
try:
2024-02-02 11:21:56 -05:00
embed = await message_factory(
bot=interaction.client,
color=await self.bot.get_embed_color(interaction.channel),
2024-02-02 11:21:56 -05:00
guild=interaction.guild,
moderator=interaction.user,
reason=reason,
moderation_type="muted",
response=await interaction.original_response(),
duration=parsed_time,
)
await target.send(embed=embed, file=get_footer_image(self))
2023-12-14 18:35:25 -05:00
except discord.errors.HTTPException:
pass
2024-06-05 00:14:43 -04:00
moderation = await Moderation.log(
interaction.client,
2024-02-02 11:21:56 -05:00
interaction.guild.id,
interaction.user.id,
"MUTE",
"USER",
target.id,
None,
2024-02-02 11:21:56 -05:00
parsed_time,
reason,
)
await interaction.edit_original_response(
content=f"{target.mention} has been muted for {humanize_timedelta(timedelta=parsed_time)}! (Case `#{moderation.id:,}`)\n**Reason** - `{reason}`"
2024-02-02 11:21:56 -05:00
)
await log(interaction, moderation.id)
await send_evidenceformat(interaction, moderation.id)
2023-12-14 18:35:25 -05:00
@app_commands.command(name="unmute")
2024-02-02 11:21:56 -05:00
async def unmute(
self,
interaction: discord.Interaction,
target: discord.Member,
2024-05-06 21:04:08 -04:00
reason: str | None = None,
silent: bool | None = None,
2024-02-02 11:21:56 -05:00
):
2023-12-14 18:35:25 -05:00
"""Unmute a user.
Parameters
-----------
target: discord.user
Who are you unmuting?
reason: str
Why are you unmuting this user?
silent: bool
Should the user be messaged?"""
2024-02-02 11:21:56 -05:00
if not await check_moddable(target, interaction, ["moderate_members"]):
2023-12-14 18:35:25 -05:00
return
2023-12-15 13:54:58 -05:00
2023-12-14 18:35:25 -05:00
if target.is_timed_out() is False:
2024-02-02 11:21:56 -05:00
await interaction.response.send_message(
error(f"{target.mention} is not muted!"),
allowed_mentions=discord.AllowedMentions(users=False),
ephemeral=True,
)
2023-12-14 18:35:25 -05:00
return
2023-12-15 13:54:58 -05:00
2023-12-14 18:35:25 -05:00
if reason:
2024-02-02 11:21:56 -05:00
await target.timeout(
None, reason=f"Unmuted by {interaction.user.id} for: {reason}"
)
2023-12-14 18:35:25 -05:00
else:
await target.timeout(None, reason=f"Unbanned by {interaction.user.id}")
reason = "No reason given."
2024-02-02 11:21:56 -05:00
await interaction.response.send_message(
content=f"{target.mention} has been unmuted!\n**Reason** - `{reason}`"
)
2023-12-14 18:35:25 -05:00
if silent is None:
silent = not await config.guild(interaction.guild).dm_users()
2023-12-14 18:35:25 -05:00
if silent is False:
try:
2024-02-02 11:21:56 -05:00
embed = await message_factory(
bot=interaction.client,
color=await self.bot.get_embed_color(interaction.channel),
2024-02-02 11:21:56 -05:00
guild=interaction.guild,
moderator=interaction.user,
reason=reason,
moderation_type="unmuted",
response=await interaction.original_response(),
)
await target.send(embed=embed, file=get_footer_image(self))
2023-12-14 18:35:25 -05:00
except discord.errors.HTTPException:
pass
moderation = await Moderation.log(
interaction.client,
2024-02-02 11:21:56 -05:00
interaction.guild.id,
interaction.user.id,
"UNMUTE",
"USER",
target.id,
None,
None,
2024-02-02 11:21:56 -05:00
reason,
)
await interaction.edit_original_response(
content=f"{target.mention} has been unmuted! (Case `#{moderation.id:,}`)\n**Reason** - `{reason}`"
2024-02-02 11:21:56 -05:00
)
await log(interaction, moderation.id)
await send_evidenceformat(interaction, moderation.id)
2023-12-14 18:35:25 -05:00
@app_commands.command(name="kick")
2024-02-02 11:21:56 -05:00
async def kick(
self,
interaction: discord.Interaction,
target: discord.Member,
reason: str,
2024-05-06 21:04:08 -04:00
silent: bool | None = None,
2024-02-02 11:21:56 -05:00
):
2023-12-14 18:35:25 -05:00
"""Kick a user.
Parameters
-----------
target: discord.user
Who are you kicking?
reason: str
Why are you kicking this user?
silent: bool
Should the user be messaged?"""
2024-02-02 11:21:56 -05:00
if not await check_moddable(target, interaction, ["kick_members"]):
2023-12-14 18:35:25 -05:00
return
2024-02-02 11:21:56 -05:00
await interaction.response.send_message(
content=f"{target.mention} has been kicked!\n**Reason** - `{reason}`"
)
2023-12-14 18:35:25 -05:00
if silent is None:
silent = not await config.guild(interaction.guild).dm_users()
2023-12-14 18:35:25 -05:00
if silent is False:
try:
2024-02-02 11:21:56 -05:00
embed = await message_factory(
bot=interaction.client,
color=await self.bot.get_embed_color(interaction.channel),
2024-02-02 11:21:56 -05:00
guild=interaction.guild,
moderator=interaction.user,
reason=reason,
moderation_type="kicked",
response=await interaction.original_response(),
)
await target.send(embed=embed, file=get_footer_image(self))
2023-12-14 18:35:25 -05:00
except discord.errors.HTTPException:
pass
2023-12-16 20:29:54 -05:00
await target.kick(reason=f"Kicked by {interaction.user.id} for: {reason}")
2023-12-14 18:35:25 -05:00
2024-06-05 00:14:43 -04:00
moderation = await Moderation.log(
interaction.client,
2024-02-02 11:21:56 -05:00
interaction.guild.id,
interaction.user.id,
"KICK",
"USER",
target.id,
None,
None,
2024-02-02 11:21:56 -05:00
reason,
)
await interaction.edit_original_response(
content=f"{target.mention} has been kicked! (Case `#{moderation.id:,}`)\n**Reason** - `{reason}`"
2024-02-02 11:21:56 -05:00
)
await log(interaction, moderation.id)
await send_evidenceformat(interaction, moderation.id)
2023-12-14 18:35:25 -05:00
@app_commands.command(name="ban")
2024-02-02 11:21:56 -05:00
@app_commands.choices(
delete_messages=[
Choice(name="None", value=0),
Choice(name="1 Hour", value=3600),
Choice(name="12 Hours", value=43200),
Choice(name="1 Day", value=86400),
Choice(name="3 Days", value=259200),
Choice(name="7 Days", value=604800),
]
)
async def ban(
self,
interaction: discord.Interaction,
target: discord.User,
reason: str,
2024-05-06 21:04:08 -04:00
duration: str | None = None,
delete_messages: Choice[int] | None = None,
silent: bool | None = None,
2024-02-02 11:21:56 -05:00
):
2023-12-14 18:35:25 -05:00
"""Ban a user.
Parameters
-----------
target: discord.user
Who are you banning?
duration: str
How long are you banning this user for?
reason: str
Why are you banning this user?
delete_messages: Choices[int]
How many days of messages to delete?
silent: bool
Should the user be messaged?"""
if duration:
await moderate(
interaction,
2024-02-02 11:21:56 -05:00
target,
silent,
["ban_members"],
Tempban,
reason=reason,
duration=duration,
delete_messages=delete_messages,
2024-02-02 11:21:56 -05:00
)
2023-12-14 18:35:25 -05:00
else:
await moderate(
interaction,
2024-02-02 11:21:56 -05:00
target,
silent,
["ban_members"],
Ban,
reason=reason,
delete_messages=delete_messages,
2024-02-02 11:21:56 -05:00
)
2023-12-14 18:35:25 -05:00
@app_commands.command(name="unban")
2024-02-02 11:21:56 -05:00
async def unban(
self,
interaction: discord.Interaction,
target: discord.User,
2024-05-06 21:04:08 -04:00
reason: str | None = None,
silent: bool | None = None,
2024-02-02 11:21:56 -05:00
):
2023-12-14 18:35:25 -05:00
"""Unban a user.
Parameters
-----------
target: discord.user
Who are you unbanning?
reason: str
Why are you unbanning this user?
silent: bool
Should the user be messaged?"""
2024-02-02 11:21:56 -05:00
if not await check_moddable(target, interaction, ["ban_members"]):
2023-12-14 18:35:25 -05:00
return
2023-12-15 13:54:58 -05:00
2023-12-14 18:35:25 -05:00
try:
await interaction.guild.fetch_ban(target)
except discord.errors.NotFound:
2024-02-02 11:21:56 -05:00
await interaction.response.send_message(
content=error(f"{target.mention} is not banned!"), ephemeral=True
)
2023-12-14 18:35:25 -05:00
return
if reason:
2024-02-02 11:21:56 -05:00
await interaction.guild.unban(
target, reason=f"Unbanned by {interaction.user.id} for: {reason}"
)
2023-12-14 18:35:25 -05:00
else:
2024-02-02 11:21:56 -05:00
await interaction.guild.unban(
target, reason=f"Unbanned by {interaction.user.id}"
)
2023-12-14 18:35:25 -05:00
reason = "No reason given."
2024-02-02 11:21:56 -05:00
await interaction.response.send_message(
content=f"{target.mention} has been unbanned!\n**Reason** - `{reason}`"
)
2023-12-14 18:35:25 -05:00
if silent is None:
silent = not await config.guild(interaction.guild).dm_users()
2023-12-14 18:35:25 -05:00
if silent is False:
try:
2024-02-02 11:21:56 -05:00
embed = await message_factory(
bot=interaction.client,
color=await self.bot.get_embed_color(interaction.channel),
2024-02-02 11:21:56 -05:00
guild=interaction.guild,
moderator=interaction.user,
reason=reason,
moderation_type="unbanned",
response=await interaction.original_response(),
)
await target.send(embed=embed, file=get_footer_image(self))
2023-12-14 18:35:25 -05:00
except discord.errors.HTTPException:
pass
2024-06-05 00:14:43 -04:00
moderation = await Moderation.log(
interaction.client,
2024-02-02 11:21:56 -05:00
interaction.guild.id,
interaction.user.id,
"UNBAN",
"USER",
target.id,
None,
None,
2024-02-02 11:21:56 -05:00
reason,
)
await interaction.edit_original_response(
content=f"{target.mention} has been unbanned! (Case `#{moderation.id:,}`)\n**Reason** - `{reason}`"
2024-02-02 11:21:56 -05:00
)
await log(interaction, moderation.id)
await send_evidenceformat(interaction, moderation.id)
2024-05-24 03:46:20 -04:00
@app_commands.command(name="slowmode")
async def slowmode(
self,
interaction: discord.Interaction,
interval: int,
channel: discord.TextChannel | None = None,
reason: str | None = None,
):
"""Set the slowmode of a channel.
Parameters
-----------
interval: int
The slowmode interval in seconds
channel: discord.TextChannel
The channel to set the slowmode in
reason: str
Why are you setting the slowmode?"""
if channel is None:
channel = interaction.channel
if not await check_moddable(channel, interaction, ["manage_channels"]):
return
await channel.edit(slowmode_delay=interval)
await interaction.response.send_message(f"Slowmode in {channel.mention} has been set to {interval} seconds!\n**Reason** - `{reason}`")
2024-06-05 00:14:43 -04:00
moderation = await Moderation.log(
2024-05-24 03:46:20 -04:00
interaction.client,
interaction.guild.id,
interaction.user.id,
"SLOWMODE",
"CHANNEL",
channel.id,
None,
None,
reason,
2024-05-24 04:16:48 -04:00
metadata={"interval": f"{interval} seconds"}
2024-05-24 03:46:20 -04:00
)
await interaction.edit_original_response(content=f"Slowmode in {channel.mention} has been set to {interval} seconds! (Case `#{moderation.id:,}`)\n**Reason** - `{reason}`")
await log(interaction, moderation.id)
await send_evidenceformat(interaction, moderation.id)
2023-12-14 18:35:25 -05:00
@app_commands.command(name="history")
2024-02-02 11:21:56 -05:00
async def history(
self,
interaction: discord.Interaction,
2024-05-06 21:04:08 -04:00
target: discord.User | None = None,
moderator: discord.User | None = None,
pagesize: app_commands.Range[int, 1, 20] | None = None,
2024-02-02 11:21:56 -05:00
page: int = 1,
2024-05-06 21:04:08 -04:00
ephemeral: bool | None = None,
inline: bool | None = None,
2024-02-02 11:21:56 -05:00
export: bool = False,
):
2023-12-14 18:35:25 -05:00
"""List previous infractions.
Parameters
-----------
target: discord.User
User whose infractions to query, overrides moderator if both are given
moderator: discord.User
Query by moderator
pagesize: app_commands.Range[int, 1, 25]
Amount of infractions to list per page
page: int
Page to select
ephemeral: bool
Hide the command response
inline: bool
Display infractions in a grid arrangement (does not look very good)
export: bool
Exports the server's moderation history to a JSON file"""
if ephemeral is None:
2024-02-02 11:21:56 -05:00
ephemeral = (
await config.user(interaction.user).history_ephemeral()
or await config.guild(interaction.guild).history_ephemeral()
2024-02-02 11:21:56 -05:00
or False
)
2023-12-14 18:35:25 -05:00
if inline is None:
2024-02-02 11:21:56 -05:00
inline = (
await config.user(interaction.user).history_inline()
or await config.guild(interaction.guild).history_inline()
2024-02-02 11:21:56 -05:00
or False
)
2023-12-14 18:35:25 -05:00
if pagesize is None:
if inline is True:
2024-02-02 11:21:56 -05:00
pagesize = (
await config.user(interaction.user).history_inline_pagesize()
or await config.guild(interaction.guild).history_inline_pagesize()
2024-02-02 11:21:56 -05:00
or 6
)
2023-12-14 18:35:25 -05:00
else:
2024-02-02 11:21:56 -05:00
pagesize = (
await config.user(interaction.user).history_pagesize()
or await config.guild(interaction.guild).history_pagesize()
2024-02-02 11:21:56 -05:00
or 5
)
2023-12-14 18:35:25 -05:00
await interaction.response.defer(ephemeral=ephemeral)
2024-02-02 11:21:56 -05:00
permissions = check_permissions(
interaction.client.user, ["embed_links"], interaction
)
if permissions:
2024-02-02 11:21:56 -05:00
await interaction.followup.send(
error(
f"I do not have the `{permissions}` permission, required for this action."
),
ephemeral=True,
)
return
if target:
2024-07-05 19:03:44 -04:00
filename = f"moderation_target_{str(target.id)}_{str(interaction.guild.id)}.json"
moderations = await Moderation.find_by_target(interaction.client, interaction.guild.id, target.id)
elif moderator:
2024-07-05 19:03:44 -04:00
filename = f"moderation_moderator_{str(moderator.id)}_{str(interaction.guild.id)}.json"
moderations = await Moderation.find_by_moderator(interaction.client, interaction.guild.id, moderator.id)
else:
2024-07-05 19:03:44 -04:00
filename = f"moderation_{str(interaction.guild.id)}.json"
moderations = await Moderation.get_latest(interaction.client, interaction.guild.id)
if export:
try:
filepath = (
2024-02-02 11:21:56 -05:00
str(data_manager.cog_data_path(cog_instance=self))
+ str(os.sep)
+ filename
2024-02-02 11:21:56 -05:00
)
2024-06-30 05:04:21 -04:00
with open(filepath, "w", encoding="utf-8") as f:
dump(obj=moderations, fp=f, indent=2)
2024-02-02 11:21:56 -05:00
await interaction.followup.send(
file=discord.File(
fp=filepath, filename=filename
2024-02-02 11:21:56 -05:00
),
ephemeral=ephemeral,
)
os.remove(filepath)
except json.JSONDecodeError as e:
2024-02-02 11:21:56 -05:00
await interaction.followup.send(
content=error(
"An error occured while exporting the moderation history.\nError:\n"
)
+ box(text=e, lang="py"),
2024-02-02 11:21:56 -05:00
ephemeral=ephemeral,
)
2023-12-18 16:35:38 -05:00
return
case_quantity = len(moderations)
page_quantity = ceil(case_quantity / pagesize)
2023-12-14 18:35:25 -05:00
start_index = (page - 1) * pagesize
end_index = page * pagesize
embed = discord.Embed(color=await self.bot.get_embed_color(interaction.channel))
2024-02-02 11:21:56 -05:00
embed.set_author(icon_url=interaction.guild.icon.url, name="Infraction History")
embed.set_footer(
text=f"Page {page:,}/{page_quantity:,} | {case_quantity:,} Results"
)
2023-12-14 18:35:25 -05:00
memory_dict = {}
for mod in moderations[start_index:end_index]:
if mod.target_id not in memory_dict:
memory_dict.update({
str(mod.target_id): await mod.get_target()
})
target = memory_dict[str(mod.target_id)]
2024-02-02 11:21:56 -05:00
if mod.moderator_id not in memory_dict:
memory_dict.update({
str(mod.moderator_id): await mod.get_moderator()
})
moderator = memory_dict[str(mod.moderator_id)]
2023-12-14 18:35:25 -05:00
field_name = f"Case #{mod.id:,} ({mod.type.string.title()})"
field_value = f"**Target:** `{target.name}` ({target.id})\n**Moderator:** `{moderator.name}` ({moderator.id})"
2023-12-14 18:35:25 -05:00
if len(str(mod.reason)) > 125:
field_value += f"\n**Reason:** `{str(mod.reason)[:125]}...`"
2023-12-14 18:35:25 -05:00
else:
field_value += f"\n**Reason:** `{str(mod.reason)}`"
if mod.duration:
2024-02-02 11:21:56 -05:00
duration_embed = (
f"{humanize_timedelta(timedelta=mod.duration)} | <t:{int(mod.end_timestamp.timestamp())}:R>"
if mod.expired is False
else f"{humanize_timedelta(timedelta=mod.duration)} | Expired"
2024-02-02 11:21:56 -05:00
)
2023-12-14 18:35:25 -05:00
field_value += f"\n**Duration:** {duration_embed}"
2024-02-02 11:21:56 -05:00
field_value += (
f"\n**Timestamp:** <t:{int(mod.timestamp.timestamp())}> | <t:{int(mod.timestamp.timestamp())}:R>"
2024-02-02 11:21:56 -05:00
)
2023-12-14 18:35:25 -05:00
if mod.role_id:
role = await mod.get_role()
field_value += f"\n**Role:** {role.mention} ({role.id})"
if mod.resolved:
2023-12-14 18:35:25 -05:00
field_value += "\n**Resolved:** True"
embed.add_field(name=field_name, value=field_value, inline=inline)
await interaction.followup.send(embed=embed, ephemeral=ephemeral)
@app_commands.command(name="resolve")
2024-02-02 11:21:56 -05:00
async def resolve(
2024-05-06 21:04:08 -04:00
self, interaction: discord.Interaction, case: int, reason: str | None = None
2024-02-02 11:21:56 -05:00
):
2023-12-14 18:35:25 -05:00
"""Resolve a specific case.
Parameters
-----------
case: int
2023-12-14 18:35:25 -05:00
Case number of the case you're trying to resolve
reason: str
Reason for resolving case"""
2024-02-02 11:21:56 -05:00
permissions = check_permissions(
interaction.client.user,
("embed_links", "moderate_members", "ban_members"),
2024-02-02 11:21:56 -05:00
interaction,
)
2023-12-14 18:35:25 -05:00
if permissions:
2024-02-02 11:21:56 -05:00
await interaction.response.send_message(
error(
f"I do not have the `{permissions}` permission, required for this action."
),
ephemeral=True,
)
2023-12-14 18:35:25 -05:00
return
try:
2024-06-05 00:14:43 -04:00
moderation = await Moderation.find_by_id(interaction.client, case, interaction.guild.id)
except ValueError:
2024-02-02 11:21:56 -05:00
await interaction.response.send_message(
content=error(f"Case #{case:,} does not exist!"), ephemeral=True
2024-02-02 11:21:56 -05:00
)
2023-12-14 18:35:25 -05:00
return
if len(moderation.changes) > 25:
2024-02-02 11:21:56 -05:00
await interaction.response.send_message(
content=error(
"Due to limitations with Discord's embed system, you cannot edit a case more than 25 times."
),
ephemeral=True,
)
return
2023-12-14 18:35:25 -05:00
try:
await moderation.resolve(interaction.user.id, reason)
except (ValueError, TypeError) as e:
if e == ValueError:
await interaction.response.send_message(
content=error("This case has already been resolved!"), ephemeral=True
)
elif e == TypeError:
await interaction.response.send_message(
content=error("This case type cannot be resolved!"), ephemeral=True
)
2023-12-14 18:35:25 -05:00
2024-02-02 11:21:56 -05:00
embed = await case_factory(
interaction=interaction,
moderation=moderation,
2024-02-02 11:21:56 -05:00
)
await interaction.response.send_message(
content=f"✅ Moderation #{case:,} resolved!", embed=embed
)
await log(interaction, case, resolved=True)
2023-12-14 18:35:25 -05:00
@app_commands.command(name="case")
2024-02-02 11:21:56 -05:00
@app_commands.choices(
raw=[
2024-02-02 11:21:56 -05:00
Choice(name="Export as Codeblock", value="codeblock"),
Choice(name="Export as File", value="file"),
2024-02-02 11:21:56 -05:00
]
)
async def case(
self,
interaction: discord.Interaction,
case: int,
2024-05-06 21:04:08 -04:00
ephemeral: bool | None = None,
2024-02-02 11:21:56 -05:00
evidenceformat: bool = False,
changes: bool = False,
2024-06-08 20:12:22 -04:00
raw: Choice[str] | None = None,
2024-02-02 11:21:56 -05:00
):
2023-12-14 18:35:25 -05:00
"""Check the details of a specific case.
Parameters
-----------
case: int
2023-12-14 18:35:25 -05:00
What case are you looking up?
ephemeral: bool
Hide the command response
2024-06-08 20:12:22 -04:00
evidenceformat: bool
Display the evidence format of the case
changes: bool
List the changes made to the case
2024-06-08 20:12:22 -04:00
raw: bool
2023-12-15 10:41:38 -05:00
Export the case to a JSON file or codeblock"""
2024-02-02 11:21:56 -05:00
permissions = check_permissions(
interaction.client.user, ["embed_links"], interaction
)
2023-12-14 18:35:25 -05:00
if permissions:
2024-02-02 11:21:56 -05:00
await interaction.response.send_message(
error(
f"I do not have the `{permissions}` permission, required for this action."
),
ephemeral=True,
)
2023-12-14 18:35:25 -05:00
return
if ephemeral is None:
2024-02-02 11:21:56 -05:00
ephemeral = (
await config.user(interaction.user).history_ephemeral()
or await config.guild(interaction.guild).history_ephemeral()
2024-02-02 11:21:56 -05:00
or False
)
try:
2024-06-05 00:14:43 -04:00
mod = await Moderation.find_by_id(interaction.client, case, interaction.guild.id)
except ValueError:
await interaction.response.send_message(
content=error(f"Case #{case:,} does not exist!"), ephemeral=True
)
return
2024-06-08 20:12:22 -04:00
if raw:
if raw.value == "file" or len(mod.to_json(2)) > 1800:
filename = (
str(data_manager.cog_data_path(cog_instance=self))
+ str(os.sep)
+ f"moderation_{interaction.guild.id}_case_{case}.json"
)
with open(filename, "w", encoding="utf-8") as f:
mod.to_json(2, f)
2024-06-08 20:12:22 -04:00
if raw.value == "codeblock":
content = f"Case #{case:,} exported.\n" + warning(
"Case was too large to export as codeblock, so it has been uploaded as a `.json` file."
2024-02-02 11:21:56 -05:00
)
else:
content = f"Case #{case:,} exported."
await interaction.response.send_message(
content=content,
file=discord.File(
filename,
f"moderation_{interaction.guild.id}_case_{case}.json",
),
ephemeral=ephemeral,
)
os.remove(filename)
2023-12-14 18:35:25 -05:00
return
await interaction.response.send_message(
content=box(mod.to_json(2), 'json'),
ephemeral=ephemeral,
)
return
if changes:
embed = await changes_factory(
interaction=interaction, moderation=mod
)
await interaction.response.send_message(
embed=embed, ephemeral=ephemeral
)
elif evidenceformat:
2024-05-06 16:47:21 -04:00
content = await evidenceformat_factory(moderation=mod)
await interaction.response.send_message(
content=content, ephemeral=ephemeral
)
else:
embed = await case_factory(
interaction=interaction, moderation=mod
)
await interaction.response.send_message(
embed=embed, ephemeral=ephemeral
)
return
2023-12-14 19:38:35 -05:00
@app_commands.command(name="edit")
2024-02-02 11:21:56 -05:00
async def edit(
self,
interaction: discord.Interaction,
case: int,
reason: str,
2024-05-06 21:04:08 -04:00
duration: str | None = None,
2024-02-02 11:21:56 -05:00
):
2023-12-14 19:38:35 -05:00
"""Edit the reason of a specific case.
Parameters
-----------
case: int
2023-12-14 19:38:35 -05:00
What case are you editing?
reason: str
What is the new reason?
duration: str
2024-02-02 11:21:56 -05:00
What is the new duration? Does not reapply the moderation if it has already expired.
"""
permissions = check_permissions(
interaction.client.user, ["embed_links"], interaction
)
2023-12-14 19:38:35 -05:00
if permissions:
2024-02-02 11:21:56 -05:00
await interaction.response.send_message(
error(
f"I do not have the `{permissions}` permission, required for this action."
),
ephemeral=True,
)
2023-12-14 19:38:35 -05:00
return
try:
2024-06-05 00:14:43 -04:00
moderation = await Moderation.find_by_id(interaction.client, case, interaction.guild.id)
old_moderation = moderation
except ValueError:
await interaction.response.send_message(
content=error(f"Case #{case:,} does not exist!"), ephemeral=True
)
return
if len(moderation.changes) > 25:
return await interaction.response.send_message(
content=error(
"Due to limitations with Discord's embed system, you cannot edit a case more than 25 times."
),
ephemeral=True,
)
if duration:
moderation.duration = parse_timedelta(duration)
if moderation.duration is None:
return await interaction.response.send_message(
error("Please provide a valid duration!"), ephemeral=True
)
moderation.end_timestamp = moderation.timestamp + moderation.duration.total_seconds()
if moderation.type == "MUTE":
if (
time.time() - moderation.unix_timestamp
) + moderation.duration.total_seconds() > 2419200:
return await interaction.response.send_message(
error(
"Please provide a duration that is less than 28 days from the initial moderation."
2024-02-02 11:21:56 -05:00
)
)
try:
member = await interaction.guild.fetch_member(
moderation.target_id
)
2023-12-14 19:38:35 -05:00
await member.timeout(
moderation.duration,
reason=f"Case #{case:,} edited by {interaction.user.id}",
)
except discord.NotFound:
pass
if reason:
moderation.reason = reason
if not moderation.changes:
moderation.changes.append(Change.from_dict(interaction.client, {
"type": "ORIGINAL",
"timestamp": old_moderation.timestamp,
"reason": old_moderation.reason,
"user_id": old_moderation.moderator_id,
"duration": old_moderation.duration,
"end_timestamp": old_moderation.end_timestamp,
}))
if duration:
moderation.changes.append(Change.from_dict(interaction.client, {
"type": "EDIT",
"timestamp": int(time.time()),
"reason": reason,
"user_id": interaction.user.id,
"duration": moderation.duration,
"end_timestamp": moderation.end_timestamp,
}))
else:
moderation.changes.append(Change.from_dict(interaction.client, {
"type": "EDIT",
"timestamp": int(time.time()),
"reason": reason,
"user_id": interaction.user.id,
"duration": moderation.duration,
"end_timestamp": moderation.end_timestamp,
}))
2024-06-05 00:14:43 -04:00
await moderation.update()
embed = await case_factory(interaction=interaction, moderation=moderation)
2023-12-14 19:38:35 -05:00
2024-02-02 11:21:56 -05:00
await interaction.response.send_message(
content=f"✅ Moderation #{case:,} edited!",
embed=embed,
ephemeral=True,
2024-02-02 11:21:56 -05:00
)
await log(interaction, case)
return
2023-12-14 18:35:25 -05:00
@tasks.loop(minutes=1)
async def handle_expiry(self):
await self.bot.wait_until_red_ready()
current_time = time.time()
global_unban_num = 0
global_addrole_num = 0
global_removerole_num = 0
2023-12-14 18:35:25 -05:00
guilds: list[discord.Guild] = self.bot.guilds
for guild in guilds:
if not await self.bot.cog_disabled_in_guild(self, guild):
time_per_guild = time.time()
2023-12-14 18:35:25 -05:00
2024-06-05 01:37:41 -04:00
tempban_query = f"SELECT * FROM moderation_{guild.id} WHERE end_timestamp IS NOT NULL AND end_timestamp <= ? AND moderation_type = 'TEMPBAN' AND expired = 0"
tempbans = await Moderation.execute(bot=self.bot, guild_id=guild.id, query=tempban_query, parameters=(time.time(),))
2023-12-14 18:35:25 -05:00
unban_num = 0
for moderation in tempbans:
user = self.bot.get_user(moderation.target_id)
if user is None:
try:
2024-06-05 01:39:34 -04:00
user = await self.bot.fetch_user(moderation.target_id)
except discord.errors.NotFound:
continue
2024-02-02 11:21:56 -05:00
name = (
f"{user.name}#{user.discriminator}"
if user.discriminator != "0"
else user.name
)
2023-12-14 18:35:25 -05:00
try:
2024-02-02 11:21:56 -05:00
await guild.unban(
user, reason=f"Automatic unban from case #{moderation.id}"
2024-02-02 11:21:56 -05:00
)
2023-12-14 18:35:25 -05:00
2024-02-02 11:21:56 -05:00
embed = await message_factory(
bot=self.bot,
color=await self.bot.get_embed_color(guild.channels[0]),
2024-02-02 11:21:56 -05:00
guild=guild,
reason=f"Automatic unban from case #{moderation.id}",
2024-02-02 11:21:56 -05:00
moderation_type="unbanned",
)
2023-12-14 18:35:25 -05:00
try:
2024-05-03 21:45:04 -04:00
await user.send(embed=embed, file=get_footer_image(self))
2023-12-14 18:35:25 -05:00
except discord.errors.HTTPException:
pass
logger.trace(
2024-02-02 11:21:56 -05:00
"Unbanned %s (%s) from %s (%s)",
name,
user.id,
guild.name,
guild.id,
)
unban_num += 1
2024-02-02 11:21:56 -05:00
except (
discord.errors.NotFound,
discord.errors.Forbidden,
discord.errors.HTTPException,
) as e:
logger.error(
"Failed to unban %s (%s) from %s (%s)\n%s",
name,
user.id,
guild.name,
guild.id,
e,
)
2023-12-14 18:35:25 -05:00
removerole_num = 0
addrole_query = f"SELECT * FROM moderation_{guild.id} WHERE end_timestamp IS NOT NULL AND end_timestamp <= ? AND moderation_type = 'ADDROLE' AND expired = 0"
addroles = await Moderation.execute(bot=self.bot, guild_id=guild.id, query=addrole_query, parameters=(time.time(),))
for moderation in addroles:
try:
member = await guild.fetch_member(moderation.target_id)
await member.remove_roles(
Object(moderation.role_id), reason=f"Automatic role removal from case #{moderation.id}"
)
logger.trace(
"Removed role %s from %s (%s)",
moderation.role_id,
member.name,
member.id,
)
removerole_num = removerole_num + 1
except (
discord.errors.NotFound,
discord.errors.Forbidden,
discord.errors.HTTPException,
) as e:
logger.error(
"Removing the role %s from user %s failed due to: \n%s",
moderation.role_id,
moderation.target_id,
e,
)
continue
2023-12-14 18:35:25 -05:00
addrole_num = 0
removerole_query = f"SELECT * FROM moderation_{guild.id} WHERE end_timestamp IS NOT NULL AND end_timestamp <= ? AND moderation_type = 'REMOVEROLE' AND expired = 0"
removeroles = await Moderation.execute(bot=self.bot, guild_id=guild.id, query=removerole_query, parameters=(time.time(),))
2023-12-14 18:35:25 -05:00
for moderation in removeroles:
2023-12-14 18:35:25 -05:00
try:
member = await guild.fetch_member(moderation.target_id)
2023-12-14 18:35:25 -05:00
await member.add_roles(
Object(moderation.role_id), reason=f"Automatic role addition from case #{moderation.id}"
)
logger.trace("Added role %s to %s (%s)", moderation.role_id, member.name, member.id)
addrole_num = addrole_num + 1
2024-02-02 11:21:56 -05:00
except (
discord.errors.NotFound,
discord.errors.Forbidden,
discord.errors.HTTPException,
) as e:
logger.error("Adding the role %s to user %s failed due to: \n%s", moderation.role_id, moderation.target_id, e)
2023-12-14 18:35:25 -05:00
continue
expiry_query = f"UPDATE `moderation_{guild.id}` SET expired = 1 WHERE (end_timestamp IS NOT NULL AND end_timestamp <= ? AND expired = 0) OR (expired = 0 AND resolved = 1);"
await Moderation.execute(bot=self.bot, guild_id=guild.id, query=expiry_query, parameters=(time.time(),), return_obj=False)
per_guild_completion_time = (time.time() - time_per_guild) * 1000
2024-02-02 11:21:56 -05:00
logger.debug(
"Completed expiry loop for %s (%s) in %sms with %s users unbanned, %s roles added, and %s roles removed",
2024-02-02 11:21:56 -05:00
guild.name,
guild.id,
f"{per_guild_completion_time:.6f}",
unban_num,
addrole_num,
removerole_num,
2024-02-02 11:21:56 -05:00
)
global_unban_num = global_unban_num + unban_num
global_addrole_num = global_addrole_num + addrole_num
global_removerole_num = global_removerole_num + removerole_num
2023-12-14 18:35:25 -05:00
completion_time = (time.time() - current_time) * 1000
2024-02-02 11:21:56 -05:00
logger.debug(
"Completed expiry loop in %sms with %s users unbanned, %s roles added, and %s roles removed",
2024-02-02 11:21:56 -05:00
f"{completion_time:.6f}",
global_unban_num,
global_addrole_num,
global_removerole_num,
2024-02-02 11:21:56 -05:00
)
2024-02-02 11:21:56 -05:00
########################################################################################################################
### Configuration Commands #
########################################################################################################################
@commands.group(autohelp=True, aliases=["moderation", "mod"])
async def aurora(self, ctx: commands.Context):
"""Settings and miscellaneous commands for Aurora."""
@aurora.group(autohelp=True, name="settings", aliases=["config", "options", "set"])
async def aurora_settings(self, ctx: commands.Context):
"""Configure Aurora's settings."""
@aurora_settings.command(name="overrides", aliases=["override", "user"])
async def aurora_settings_overrides(self, ctx: commands.Context):
"""Manage Aurora's user overriddable settings."""
msg = await ctx.send(embed=await overrides_embed(ctx))
await msg.edit(view=Overrides(ctx, msg, 60))
@aurora_settings.command(name="guild", aliases=["server"])
@commands.admin_or_permissions(manage_guild=True)
@commands.guild_only()
async def aurora_settings_guild(self, ctx: commands.Context):
"""Manage Aurora's guild settings."""
msg = await ctx.send(embed=await guild_embed(ctx))
await msg.edit(view=Guild(ctx, msg, 60))
@aurora_settings.command(name="addrole", aliases=["removerole"])
@commands.admin_or_permissions(manage_guild=True)
@commands.guild_only()
async def aurora_settings_addrole(self, ctx: commands.Context):
"""Manage the addrole whitelist.
Roles added to this list are also applied to `/removerole`."""
msg = await ctx.send(embed=await addrole_embed(ctx))
await msg.edit(view=Addrole(ctx, msg, 60))
@aurora_settings.command(name="immunity")
@commands.admin_or_permissions(manage_guild=True)
@commands.guild_only()
async def aurora_settings_immunity(self, ctx: commands.Context):
"""Manage the immunity whitelist."""
msg = await ctx.send(embed=await immune_embed(ctx))
await msg.edit(view=Immune(ctx, msg, 60))
@aurora.group(autohelp=True, name="import")
@commands.admin()
@commands.guild_only()
async def aurora_import(self, ctx: commands.Context):
"""Import moderation history from other bots."""
@aurora_import.command(name="aurora")
@commands.admin()
async def aurora_import_aurora(self, ctx: commands.Context):
"""Import moderation history from another bot using Aurora."""
if (
ctx.message.attachments
and ctx.message.attachments[0].content_type
== "application/json; charset=utf-8"
):
message = await ctx.send(
warning(
"Are you sure you want to import moderations from another bot?\n**This will overwrite any moderations that already exist in this guild's moderation table.**\n*The import process will block the rest of your bot until it is complete.*"
)
)
await message.edit(view=ImportAuroraView(60, ctx, message))
else:
await ctx.send(error("Please provide a valid Aurora export file."))
@aurora_import.command(name="galacticbot")
@commands.admin()
async def aurora_import_galacticbot(self, ctx: commands.Context):
"""Import moderation history from GalacticBot."""
if (
ctx.message.attachments
and ctx.message.attachments[0].content_type
== "application/json; charset=utf-8"
):
message = await ctx.send(
warning(
"Are you sure you want to import GalacticBot moderations?\n**This will overwrite any moderations that already exist in this guild's moderation table.**\n*The import process will block the rest of your bot until it is complete.*"
)
)
await message.edit(view=ImportGalacticBotView(60, ctx, message))
else:
await ctx.send(
error("Please provide a valid GalacticBot moderation export file.")
)
2024-02-02 11:21:56 -05:00
@aurora.command(aliases=["tdc", "td", "timedeltaconvert"])
async def timedelta(self, ctx: commands.Context, *, duration: str) -> None:
"""Convert a string to a timedelta.
This command converts a duration to a [`timedelta`](https://docs.python.org/3/library/datetime.html#datetime.timedelta) Python object.
You cannot convert years or months as they are not fixed units. Use `[p]aurora relativedelta` for that.
2023-12-14 18:35:25 -05:00
**Example usage**
`[p]aurora timedelta 1 day 15hr 82 minutes 52s`
2023-12-14 18:35:25 -05:00
**Output**
`1 day, 16:22:52`"""
parsed_time = parse_timedelta(duration)
if parsed_time is None:
await ctx.send(error("Please provide a convertible value!"))
return
await ctx.send(f"`{parsed_time}`")
@aurora.command(aliases=["rdc", "rd", "relativedeltaconvert"])
async def relativedelta(self, ctx: commands.Context, *, duration: str) -> None:
"""Convert a string to a relativedelta.
This command converts a duration to a [`relativedelta`](https://dateutil.readthedocs.io/en/stable/relativedelta.html) Python object.
**Example usage**
`[p]aurora relativedelta 3 years 1 day 15hr 82 minutes 52s`
**Output**
`relativedelta(years=+3, days=+1, hours=+15, minutes=+82, seconds=+52)`"""
parsed_time = parse_relativedelta(duration)
if parsed_time is None:
await ctx.send(error("Please provide a convertible value!"))
return
await ctx.send(f"`{parsed_time}`")