Compare commits

...

27 commits

Author SHA1 Message Date
2aadf5134d
fix(aurora): use f-strings
Some checks failed
Actions / Lint Code (Ruff & Pylint) (pull_request) Failing after 39s
Actions / Build Documentation (MkDocs) (pull_request) Failing after 26s
2024-07-05 19:03:44 -04:00
3b102debac
fix(aurora): fixed a PermissionError 2024-06-30 05:04:21 -04:00
c28b089edb
fix(aurora): fixed an issue with the /case command preventing the cog from loading 2024-06-30 05:02:32 -04:00
987fc0dbf8
feat(aurora): add scoped export functionality to aurora's /history command 2024-06-30 04:56:42 -04:00
87942213a5
fix(aurora): typehints 2024-06-11 03:17:38 -04:00
60d4afc6f3
fix(aurora): fixed AuroraBaseModel.to_json() not using AuroraBaseModel.dump() 2024-06-11 03:11:45 -04:00
0bcbcd6c0c
feat(aurora): bunch of changes 2024-06-08 20:12:22 -04:00
f6a42b97d9
misc(aurora): various model changes 2024-06-05 23:13:23 -04:00
5cb61ecd65
fix(aurora): awaited a coroutine 2024-06-05 01:39:34 -04:00
9622e037c9
fix(aurora): fixed another sql syntax error 2024-06-05 01:38:27 -04:00
e9a64e5a39
fix(aurora): fixed an sql syntax error 2024-06-05 01:37:41 -04:00
afac274978
fix(aurora): removed a useless try/except block 2024-06-05 01:36:45 -04:00
e988917319
feat(aurora): added a return_obj parameter to Moderation.execute() 2024-06-05 01:31:40 -04:00
42f7f9f69b
feat(aurora): migrated Aurora.handle_expiry() to use Moderation.execute() instead of opening its own connections 2024-06-05 01:29:47 -04:00
d07e5ed804
misc(aurora): changing around some logging levels 2024-06-05 01:12:49 -04:00
df465e5ba6
fix(aurora): awaited another coroutine 2024-06-05 01:11:18 -04:00
76572e2281
fix(aurora): awaited a coroutine 2024-06-05 01:05:11 -04:00
d629f1a5a2
fix(aurora): awaited two coroutines 2024-06-05 01:02:09 -04:00
ca4510d3a5
fix(aurora): why was I CLOSING THE CONNECTION THERE 😭 2024-06-05 01:01:17 -04:00
3247e6fb82
fix(aurora): lmao i'm dumb 2024-06-05 00:59:33 -04:00
67e3abf5ce
fix(aurora): use interaction.channel.send instead 2024-06-05 00:54:05 -04:00
d1b5346396
fix(aurora): fixed failed_cases in the aurora importer 2024-06-05 00:51:13 -04:00
fe5823b637
fix(aurora): awaited a coroutine 2024-06-05 00:41:41 -04:00
3383e84221
fix(aurora): fixed some issues with aiosqlite 2024-06-05 00:39:56 -04:00
56a2f96a2d
feat(aurora): made database.connect() into an async context manager 2024-06-05 00:36:12 -04:00
eebddd6e89
fix(aurora): override aiosqlite's logging level to warning 2024-06-05 00:25:19 -04:00
5cbf4e7e47
feat(aurora): migrated to aiosqlite 2024-06-05 00:14:43 -04:00
12 changed files with 319 additions and 327 deletions

View file

@ -6,8 +6,8 @@
# |_____/ \___|\__,_|___/ \_/\_/ |_|_| |_| |_|_| |_| |_|\___|_| # |_____/ \___|\__,_|___/ \_/\_/ |_|_| |_| |_|_| |_| |_|\___|_|
import json import json
import logging as py_logging
import os import os
import sqlite3
import time import time
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from math import ceil from math import ceil
@ -19,7 +19,7 @@ from redbot.core import app_commands, commands, data_manager
from redbot.core.app_commands import Choice from redbot.core.app_commands import Choice
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.commands.converter import parse_relativedelta, parse_timedelta from redbot.core.commands.converter import parse_relativedelta, parse_timedelta
from redbot.core.utils.chat_formatting import box, error, humanize_list, humanize_timedelta, warning from redbot.core.utils.chat_formatting import bold, box, error, humanize_list, humanize_timedelta, warning
from .importers.aurora import ImportAuroraView from .importers.aurora import ImportAuroraView
from .importers.galacticbot import ImportGalacticBotView from .importers.galacticbot import ImportGalacticBotView
@ -30,11 +30,10 @@ from .menus.overrides import Overrides
from .models.change import Change from .models.change import Change
from .models.moderation import Moderation from .models.moderation import Moderation
from .utilities.config import config, register_config from .utilities.config import config, register_config
from .utilities.database import connect, create_guild_table
from .utilities.factory import addrole_embed, case_factory, changes_factory, evidenceformat_factory, guild_embed, immune_embed, message_factory, overrides_embed from .utilities.factory import addrole_embed, case_factory, changes_factory, evidenceformat_factory, guild_embed, immune_embed, message_factory, overrides_embed
from .utilities.json import dump from .utilities.json import dump
from .utilities.logger import logger from .utilities.logger import logger
from .utilities.utils import check_moddable, check_permissions, get_footer_image, log, send_evidenceformat, timedelta_from_relativedelta from .utilities.utils import check_moddable, check_permissions, create_guild_table, get_footer_image, log, send_evidenceformat, timedelta_from_relativedelta
class Aurora(commands.Cog): class Aurora(commands.Cog):
@ -43,28 +42,22 @@ class Aurora(commands.Cog):
This cog stores all of its data in an SQLite database.""" This cog stores all of its data in an SQLite database."""
__author__ = ["SeaswimmerTheFsh"] __author__ = ["SeaswimmerTheFsh"]
__version__ = "2.2.0" __version__ = "2.3.0"
__documentation__ = "https://seacogs.coastalcommits.com/aurora/" __documentation__ = "https://seacogs.coastalcommits.com/aurora/"
async def red_delete_data_for_user(self, *, requester, user_id: int): async def red_delete_data_for_user(self, *, requester, user_id: int):
if requester == "discord_deleted_user": if requester == "discord_deleted_user":
await config.user_from_id(user_id).clear() await config.user_from_id(user_id).clear()
database = connect() results = await Moderation.execute(query="SHOW TABLES;", return_obj=False)
cursor = database.cursor() tables = [table[0] for table in results]
cursor.execute("SHOW TABLES;")
tables = [table[0] for table in cursor.fetchall()]
condition = "target_id = %s OR moderator_id = %s;" condition = "target_id = %s OR moderator_id = %s;"
for table in tables: for table in tables:
delete_query = f"DELETE FROM {table[0]} WHERE {condition}" delete_query = f"DELETE FROM {table[0]} WHERE {condition}"
cursor.execute(delete_query, (user_id, user_id)) await Moderation.execute(query=delete_query, parameters=(user_id, user_id), return_obj=False)
database.commit()
cursor.close()
database.close()
if requester == "owner": if requester == "owner":
await config.user_from_id(user_id).clear() await config.user_from_id(user_id).clear()
if requester == "user": if requester == "user":
@ -76,20 +69,26 @@ class Aurora(commands.Cog):
"Invalid requester passed to red_delete_data_for_user: %s", requester "Invalid requester passed to red_delete_data_for_user: %s", requester
) )
def __init__(self, bot: Red): def __init__(self, bot: Red) -> None:
super().__init__() super().__init__()
self.bot = bot self.bot = bot
register_config(config) register_config(config)
self.handle_expiry.start() 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.
py_logging.getLogger('aiosqlite').setLevel(py_logging.INFO)
def format_help_for_context(self, ctx: commands.Context) -> str: def format_help_for_context(self, ctx: commands.Context) -> str:
pre_processed = super().format_help_for_context(ctx) or "" pre_processed = super().format_help_for_context(ctx) or ""
n = "\n" if "\n\n" not in pre_processed else "" n = "\n" if "\n\n" not in pre_processed else ""
text = [ text = [
f"{pre_processed}{n}", f"{pre_processed}{n}",
f"Cog Version: **{self.__version__}**", f"{bold('Cog Version:')} {self.__version__}",
f"Author: {humanize_list(self.__author__)}", f"{bold('Author:')} {humanize_list(self.__author__)}",
f"Documentation: {self.__documentation__}", f"{bold('Documentation:')} {self.__documentation__}",
] ]
return "\n".join(text) return "\n".join(text)
@ -122,14 +121,11 @@ class Aurora(commands.Cog):
"""This method automatically adds roles to users when they join the server.""" """This method automatically adds roles to users when they join the server."""
if not await self.bot.cog_disabled_in_guild(self, member.guild): 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;""" 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;"""
database = connect() results = Moderation.execute(query, (member.id,))
cursor = database.cursor() for row in results:
cursor.execute(query, (member.id,)) role = member.guild.get_role(row[1])
results = cursor.fetchall() reason = row[2]
for result in results: await member.add_roles(role, reason=f"Role automatically added on member rejoin for: {reason} (Case #{row[0]:,})")
role = member.guild.get_role(result[1])
reason = result[2]
await member.add_roles(role, reason=f"Role automatically added on member rejoin for: {reason} (Case #{result[0]:,})")
@commands.Cog.listener("on_audit_log_entry_create") @commands.Cog.listener("on_audit_log_entry_create")
async def autologger(self, entry: discord.AuditLogEntry): async def autologger(self, entry: discord.AuditLogEntry):
@ -175,7 +171,7 @@ class Aurora(commands.Cog):
else: else:
return return
Moderation.log( await Moderation.log(
self.bot, self.bot,
entry.guild.id, entry.guild.id,
entry.user.id, entry.user.id,
@ -233,7 +229,7 @@ class Aurora(commands.Cog):
except discord.errors.HTTPException: except discord.errors.HTTPException:
pass pass
moderation = Moderation.log( moderation = await Moderation.log(
interaction.client, interaction.client,
interaction.guild.id, interaction.guild.id,
interaction.user.id, interaction.user.id,
@ -292,7 +288,7 @@ class Aurora(commands.Cog):
except discord.errors.HTTPException: except discord.errors.HTTPException:
pass pass
moderation = Moderation.log( moderation = await Moderation.log(
interaction.client, interaction.client,
interaction.guild.id, interaction.guild.id,
interaction.user.id, interaction.user.id,
@ -398,7 +394,7 @@ class Aurora(commands.Cog):
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}`" 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}`"
) )
moderation = Moderation.log( moderation = await Moderation.log(
interaction.client, interaction.client,
interaction.guild.id, interaction.guild.id,
interaction.user.id, interaction.user.id,
@ -504,7 +500,7 @@ class Aurora(commands.Cog):
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}`" 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}`"
) )
moderation = Moderation.log( moderation = await Moderation.log(
interaction.client, interaction.client,
interaction.guild.id, interaction.guild.id,
interaction.user.id, interaction.user.id,
@ -592,7 +588,7 @@ class Aurora(commands.Cog):
except discord.errors.HTTPException: except discord.errors.HTTPException:
pass pass
moderation = Moderation.log( moderation = await Moderation.log(
interaction.client, interaction.client,
interaction.guild.id, interaction.guild.id,
interaction.user.id, interaction.user.id,
@ -728,7 +724,7 @@ class Aurora(commands.Cog):
await target.kick(reason=f"Kicked by {interaction.user.id} for: {reason}") await target.kick(reason=f"Kicked by {interaction.user.id} for: {reason}")
moderation = Moderation.log( moderation = await Moderation.log(
interaction.client, interaction.client,
interaction.guild.id, interaction.guild.id,
interaction.user.id, interaction.user.id,
@ -836,7 +832,7 @@ class Aurora(commands.Cog):
delete_message_seconds=delete_messages_seconds, delete_message_seconds=delete_messages_seconds,
) )
moderation = Moderation.log( moderation = await Moderation.log(
interaction.client, interaction.client,
interaction.guild.id, interaction.guild.id,
interaction.user.id, interaction.user.id,
@ -880,7 +876,7 @@ class Aurora(commands.Cog):
delete_message_seconds=delete_messages_seconds, delete_message_seconds=delete_messages_seconds,
) )
moderation = Moderation.log( moderation = await Moderation.log(
interaction.client, interaction.client,
interaction.guild.id, interaction.guild.id,
interaction.user.id, interaction.user.id,
@ -957,7 +953,7 @@ class Aurora(commands.Cog):
except discord.errors.HTTPException: except discord.errors.HTTPException:
pass pass
moderation = Moderation.log( moderation = await Moderation.log(
interaction.client, interaction.client,
interaction.guild.id, interaction.guild.id,
interaction.user.id, interaction.user.id,
@ -1001,7 +997,7 @@ class Aurora(commands.Cog):
await channel.edit(slowmode_delay=interval) 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}`") await interaction.response.send_message(f"Slowmode in {channel.mention} has been set to {interval} seconds!\n**Reason** - `{reason}`")
moderation = Moderation.log( moderation = await Moderation.log(
interaction.client, interaction.client,
interaction.guild.id, interaction.guild.id,
interaction.user.id, interaction.user.id,
@ -1046,7 +1042,7 @@ class Aurora(commands.Cog):
inline: bool inline: bool
Display infractions in a grid arrangement (does not look very good) Display infractions in a grid arrangement (does not look very good)
export: bool export: bool
Exports the server's entire moderation history to a JSON file""" Exports the server's moderation history to a JSON file"""
if ephemeral is None: if ephemeral is None:
ephemeral = ( ephemeral = (
await config.user(interaction.user).history_ephemeral() await config.user(interaction.user).history_ephemeral()
@ -1089,47 +1085,45 @@ class Aurora(commands.Cog):
) )
return return
database = connect() if target:
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:
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:
filename = f"moderation_{str(interaction.guild.id)}.json"
moderations = await Moderation.get_latest(interaction.client, interaction.guild.id)
if export: if export:
try: try:
filename = ( filepath = (
str(data_manager.cog_data_path(cog_instance=self)) str(data_manager.cog_data_path(cog_instance=self))
+ str(os.sep) + str(os.sep)
+ f"moderation_{interaction.guild.id}.json" + filename
) )
cases = Moderation.get_latest(bot=interaction.client, guild_id=interaction.guild.id) with open(filepath, "w", encoding="utf-8") as f:
dump(obj=moderations, fp=f, indent=2)
with open(filename, "w", encoding="utf-8") as f:
dump(obj=cases, fp=f, indent=2)
await interaction.followup.send( await interaction.followup.send(
file=discord.File( file=discord.File(
filename, f"moderation_{interaction.guild.id}.json" fp=filepath, filename=filename
), ),
ephemeral=ephemeral, ephemeral=ephemeral,
) )
os.remove(filename) os.remove(filepath)
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
await interaction.followup.send( await interaction.followup.send(
content=error( content=error(
"An error occured while exporting the moderation history.\nError:\n" "An error occured while exporting the moderation history.\nError:\n"
) )
+ box(e, "py"), + box(text=e, lang="py"),
ephemeral=ephemeral, ephemeral=ephemeral,
) )
database.close()
return return
if target:
moderations = Moderation.find_by_target(interaction.client, interaction.guild.id, target.id)
elif moderator:
moderations = Moderation.find_by_moderator(interaction.client, interaction.guild.id, moderator.id)
else:
moderations = Moderation.get_latest(interaction.client, interaction.guild.id)
case_quantity = len(moderations) case_quantity = len(moderations)
page_quantity = ceil(case_quantity / pagesize) page_quantity = ceil(case_quantity / pagesize)
start_index = (page - 1) * pagesize start_index = (page - 1) * pagesize
@ -1214,7 +1208,7 @@ class Aurora(commands.Cog):
return return
try: try:
moderation = Moderation.find_by_id(interaction.client, case, interaction.guild.id) moderation = await Moderation.find_by_id(interaction.client, case, interaction.guild.id)
except ValueError: except ValueError:
await interaction.response.send_message( await interaction.response.send_message(
content=error(f"Case #{case:,} does not exist!"), ephemeral=True content=error(f"Case #{case:,} does not exist!"), ephemeral=True
@ -1252,9 +1246,9 @@ class Aurora(commands.Cog):
@app_commands.command(name="case") @app_commands.command(name="case")
@app_commands.choices( @app_commands.choices(
export=[ raw=[
Choice(name="Export as File", value="file"),
Choice(name="Export as Codeblock", value="codeblock"), Choice(name="Export as Codeblock", value="codeblock"),
Choice(name="Export as File", value="file"),
] ]
) )
async def case( async def case(
@ -1264,7 +1258,7 @@ class Aurora(commands.Cog):
ephemeral: bool | None = None, ephemeral: bool | None = None,
evidenceformat: bool = False, evidenceformat: bool = False,
changes: bool = False, changes: bool = False,
export: Choice[str] | None = None, raw: Choice[str] | None = None,
): ):
"""Check the details of a specific case. """Check the details of a specific case.
@ -1274,9 +1268,11 @@ class Aurora(commands.Cog):
What case are you looking up? What case are you looking up?
ephemeral: bool ephemeral: bool
Hide the command response Hide the command response
evidenceformat: bool
Display the evidence format of the case
changes: bool changes: bool
List the changes made to the case List the changes made to the case
export: bool raw: bool
Export the case to a JSON file or codeblock""" Export the case to a JSON file or codeblock"""
permissions = check_permissions( permissions = check_permissions(
interaction.client.user, ["embed_links"], interaction interaction.client.user, ["embed_links"], interaction
@ -1298,15 +1294,15 @@ class Aurora(commands.Cog):
) )
try: try:
mod = Moderation.find_by_id(interaction.client, case, interaction.guild.id) mod = await Moderation.find_by_id(interaction.client, case, interaction.guild.id)
except ValueError: except ValueError:
await interaction.response.send_message( await interaction.response.send_message(
content=error(f"Case #{case:,} does not exist!"), ephemeral=True content=error(f"Case #{case:,} does not exist!"), ephemeral=True
) )
return return
if export: if raw:
if export.value == "file" or len(mod.to_json(2)) > 1800: if raw.value == "file" or len(mod.to_json(2)) > 1800:
filename = ( filename = (
str(data_manager.cog_data_path(cog_instance=self)) str(data_manager.cog_data_path(cog_instance=self))
+ str(os.sep) + str(os.sep)
@ -1315,7 +1311,7 @@ class Aurora(commands.Cog):
with open(filename, "w", encoding="utf-8") as f: with open(filename, "w", encoding="utf-8") as f:
mod.to_json(2, f) mod.to_json(2, f)
if export.value == "codeblock": if raw.value == "codeblock":
content = f"Case #{case:,} exported.\n" + warning( content = f"Case #{case:,} exported.\n" + warning(
"Case was too large to export as codeblock, so it has been uploaded as a `.json` file." "Case was too large to export as codeblock, so it has been uploaded as a `.json` file."
) )
@ -1391,7 +1387,7 @@ class Aurora(commands.Cog):
return return
try: try:
moderation = Moderation.find_by_id(interaction.client, case, interaction.guild.id) moderation = await Moderation.find_by_id(interaction.client, case, interaction.guild.id)
old_moderation = moderation old_moderation = moderation
except ValueError: except ValueError:
await interaction.response.send_message( await interaction.response.send_message(
@ -1469,7 +1465,7 @@ class Aurora(commands.Cog):
"end_timestamp": moderation.end_timestamp, "end_timestamp": moderation.end_timestamp,
})) }))
moderation.update() await moderation.update()
embed = await case_factory(interaction=interaction, moderation=moderation) embed = await case_factory(interaction=interaction, moderation=moderation)
await interaction.response.send_message( await interaction.response.send_message(
@ -1485,8 +1481,6 @@ class Aurora(commands.Cog):
async def handle_expiry(self): async def handle_expiry(self):
await self.bot.wait_until_red_ready() await self.bot.wait_until_red_ready()
current_time = time.time() current_time = time.time()
database = connect()
cursor = database.cursor()
global_unban_num = 0 global_unban_num = 0
global_addrole_num = 0 global_addrole_num = 0
global_removerole_num = 0 global_removerole_num = 0
@ -1496,20 +1490,17 @@ class Aurora(commands.Cog):
if not await self.bot.cog_disabled_in_guild(self, guild): if not await self.bot.cog_disabled_in_guild(self, guild):
time_per_guild = time.time() time_per_guild = time.time()
tempban_query = f"SELECT target_id, moderation_id FROM moderation_{guild.id} WHERE end_timestamp IS NOT NULL 0 AND end_timestamp <= ? AND moderation_type = 'TEMPBAN' AND expired = 0" 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(),))
try:
cursor.execute(tempban_query, (time.time(),))
result = cursor.fetchall()
except sqlite3.OperationalError:
continue
target_ids = [row[0] for row in result]
moderation_ids = [row[1] for row in result]
unban_num = 0 unban_num = 0
for target_id, moderation_id in zip(target_ids, moderation_ids): for moderation in tempbans:
user: discord.User = await self.bot.fetch_user(target_id) user = self.bot.get_user(moderation.target_id)
if user is None:
try:
user = await self.bot.fetch_user(moderation.target_id)
except discord.errors.NotFound:
continue
name = ( name = (
f"{user.name}#{user.discriminator}" f"{user.name}#{user.discriminator}"
if user.discriminator != "0" if user.discriminator != "0"
@ -1517,14 +1508,14 @@ class Aurora(commands.Cog):
) )
try: try:
await guild.unban( await guild.unban(
user, reason=f"Automatic unban from case #{moderation_id}" user, reason=f"Automatic unban from case #{moderation.id}"
) )
embed = await message_factory( embed = await message_factory(
bot=self.bot, bot=self.bot,
color=await self.bot.get_embed_color(guild.channels[0]), color=await self.bot.get_embed_color(guild.channels[0]),
guild=guild, guild=guild,
reason=f"Automatic unban from case #{moderation_id}", reason=f"Automatic unban from case #{moderation.id}",
moderation_type="unbanned", moderation_type="unbanned",
) )
@ -1533,14 +1524,14 @@ class Aurora(commands.Cog):
except discord.errors.HTTPException: except discord.errors.HTTPException:
pass pass
logger.debug( logger.trace(
"Unbanned %s (%s) from %s (%s)", "Unbanned %s (%s) from %s (%s)",
name, name,
user.id, user.id,
guild.name, guild.name,
guild.id, guild.id,
) )
unban_num = unban_num + 1 unban_num += 1
except ( except (
discord.errors.NotFound, discord.errors.NotFound,
discord.errors.Forbidden, discord.errors.Forbidden,
@ -1556,26 +1547,23 @@ class Aurora(commands.Cog):
) )
removerole_num = 0 removerole_num = 0
addrole_query = f"SELECT target_id, moderation_id, role_id FROM moderation_{guild.id} WHERE end_timestamp IS NOT NULL AND end_timestamp <= ? AND moderation_type = 'ADDROLE' AND expired = 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"
try: addroles = await Moderation.execute(bot=self.bot, guild_id=guild.id, query=addrole_query, parameters=(time.time(),))
cursor.execute(addrole_query, (time.time(),))
result = cursor.fetchall()
except sqlite3.OperationalError:
continue
target_ids = [row[0] for row in result]
moderation_ids = [row[1] for row in result]
role_ids = [row[2] for row in result]
for target_id, moderation_id, role_id in zip( for moderation in addroles:
target_ids, moderation_ids, role_ids
):
try: try:
member = await guild.fetch_member(target_id) member = await guild.fetch_member(moderation.target_id)
await member.remove_roles( await member.remove_roles(
Object(role_id), reason=f"Automatic role removal from case #{moderation_id}" 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 removerole_num = removerole_num + 1
except ( except (
discord.errors.NotFound, discord.errors.NotFound,
@ -1584,44 +1572,36 @@ class Aurora(commands.Cog):
) as e: ) as e:
logger.error( logger.error(
"Removing the role %s from user %s failed due to: \n%s", "Removing the role %s from user %s failed due to: \n%s",
role_id, moderation.role_id,
target_id, moderation.target_id,
e, e,
) )
continue continue
addrole_num = 0 addrole_num = 0
removerole_query = f"SELECT target_id, moderation_id, role_id FROM moderation_{guild.id} WHERE end_timestamp IS NOT NULL 0 AND end_timestamp <= ? AND moderation_type = 'REMOVEROLE' AND expired = 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"
try: removeroles = await Moderation.execute(bot=self.bot, guild_id=guild.id, query=removerole_query, parameters=(time.time(),))
cursor.execute(removerole_query, (time.time(),))
result = cursor.fetchall()
except sqlite3.OperationalError:
continue
target_ids = [row[0] for row in result]
moderation_ids = [row[1] for row in result]
role_ids = [row[2] for row in result]
for target_id, moderation_id, role_id in zip( for moderation in removeroles:
target_ids, moderation_ids, role_ids
):
try: try:
member = await guild.fetch_member(target_id) member = await guild.fetch_member(moderation.target_id)
await member.add_roles( await member.add_roles(
Object(role_id), reason=f"Automatic role addition from case #{moderation_id}" 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 addrole_num = addrole_num + 1
except ( except (
discord.errors.NotFound, discord.errors.NotFound,
discord.errors.Forbidden, discord.errors.Forbidden,
discord.errors.HTTPException, discord.errors.HTTPException,
) as e: ) as e:
logger.error("Adding the role %s to user %s failed due to: \n%s", role_id, target_id, e) logger.error("Adding the role %s to user %s failed due to: \n%s", moderation.role_id, moderation.target_id, e)
continue 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);" 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);"
cursor.execute(expiry_query, (time.time(),)) 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 per_guild_completion_time = (time.time() - time_per_guild) * 1000
logger.debug( logger.debug(
@ -1637,9 +1617,6 @@ class Aurora(commands.Cog):
global_addrole_num = global_addrole_num + addrole_num global_addrole_num = global_addrole_num + addrole_num
global_removerole_num = global_removerole_num + removerole_num global_removerole_num = global_removerole_num + removerole_num
database.commit()
cursor.close()
database.close()
completion_time = (time.time() - current_time) * 1000 completion_time = (time.time() - current_time) * 1000
logger.debug( logger.debug(

View file

@ -1,15 +1,16 @@
# pylint: disable=duplicate-code # pylint: disable=duplicate-code
import json import json
import os
from time import time from time import time
from typing import Dict from typing import Dict
from discord import ButtonStyle, Interaction, Message, ui from discord import ButtonStyle, File, Interaction, Message, ui
from redbot.core import commands from redbot.core import commands, data_manager
from redbot.core.utils.chat_formatting import box, warning from redbot.core.utils.chat_formatting import warning
from ..models.moderation import Moderation from ..models.moderation import Moderation
from ..utilities.database import connect, create_guild_table from ..utilities.json import dump
from ..utilities.utils import timedelta_from_string from ..utilities.utils import create_guild_table, timedelta_from_string
class ImportAuroraView(ui.View): class ImportAuroraView(ui.View):
@ -27,14 +28,8 @@ class ImportAuroraView(ui.View):
"Deleting original table...", ephemeral=True "Deleting original table...", ephemeral=True
) )
database = connect()
cursor = database.cursor()
query = f"DROP TABLE IF EXISTS moderation_{self.ctx.guild.id};" query = f"DROP TABLE IF EXISTS moderation_{self.ctx.guild.id};"
cursor.execute(query) await Moderation.execute(query=query, return_obj=False)
cursor.close()
database.commit()
await interaction.edit_original_response(content="Creating new table...") await interaction.edit_original_response(content="Creating new table...")
@ -96,7 +91,7 @@ class ImportAuroraView(ui.View):
duration = None duration = None
try: try:
Moderation.log( await Moderation.log(
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"],
@ -113,7 +108,6 @@ class ImportAuroraView(ui.View):
expired=case["expired"], expired=case["expired"],
changes=changes, changes=changes,
metadata=metadata, metadata=metadata,
database=database,
return_obj=False return_obj=False
) )
except Exception as e: # pylint: disable=broad-exception-caught except Exception as e: # pylint: disable=broad-exception-caught
@ -121,12 +115,25 @@ class ImportAuroraView(ui.View):
await interaction.edit_original_response(content="Import complete.") await interaction.edit_original_response(content="Import complete.")
if failed_cases: if failed_cases:
await interaction.edit_original_response( filename = (
content="Import complete.\n" str(data_manager.cog_data_path(cog_instance=self))
+ warning("Failed to import the following cases:\n") + str(os.sep)
+ box(failed_cases) + f"failed_cases_{interaction.guild.id}.json"
) )
with open(filename, "w", encoding="utf-8") as f:
dump(obj=failed_cases, fp=f, indent=2)
await interaction.channel.send(
content="Import complete.\n"
+ warning("Failed to import the following cases:\n"),
file=File(
filename, f"failed_cases_{interaction.guild.id}.json"
)
)
os.remove(filename)
@ui.button(label="No", style=ButtonStyle.danger) @ui.button(label="No", style=ButtonStyle.danger)
async def import_button_n( async def import_button_n(
self, interaction: Interaction, button: ui.Button self, interaction: Interaction, button: ui.Button

View file

@ -8,13 +8,14 @@ from redbot.core import commands
from redbot.core.utils.chat_formatting import box, warning from redbot.core.utils.chat_formatting import box, warning
from ..models.moderation import Change, Moderation from ..models.moderation import Change, Moderation
from ..utilities.database import connect, create_guild_table from ..utilities.database import create_guild_table
class ImportGalacticBotView(ui.View): class ImportGalacticBotView(ui.View):
def __init__(self, timeout, ctx, message): def __init__(self, timeout, ctx, message):
super().__init__() super().__init__()
self.ctx: commands.Context = ctx self.ctx: commands.Context = ctx
self.timeout = timeout
self.message: Message = message self.message: Message = message
@ui.button(label="Yes", style=ButtonStyle.success) @ui.button(label="Yes", style=ButtonStyle.success)
@ -26,14 +27,14 @@ class ImportGalacticBotView(ui.View):
"Deleting original table...", ephemeral=True "Deleting original table...", ephemeral=True
) )
database = connect() database = await Moderation.connect()
cursor = database.cursor() cursor = await database.cursor()
query = f"DROP TABLE IF EXISTS moderation_{self.ctx.guild.id};" query = f"DROP TABLE IF EXISTS moderation_{self.ctx.guild.id};"
cursor.execute(query) await cursor.execute(query)
cursor.close() await cursor.close()
database.commit() await database.commit()
await interaction.edit_original_response(content="Creating new table...") await interaction.edit_original_response(content="Creating new table...")
@ -124,7 +125,7 @@ class ImportGalacticBotView(ui.View):
else: else:
reason = None reason = None
Moderation.log( await Moderation.log(
self.ctx.guild.id, self.ctx.guild.id,
case["executor"], case["executor"],
case["type"], case["type"],

View file

@ -9,7 +9,7 @@
"disabled": false, "disabled": false,
"min_bot_version": "3.5.0", "min_bot_version": "3.5.0",
"min_python_version": [3, 10, 0], "min_python_version": [3, 10, 0],
"requirements": ["pydantic"], "requirements": ["pydantic", "aiosqlite"],
"tags": [ "tags": [
"mod", "mod",
"moderate", "moderate",

View file

@ -1,5 +1,6 @@
from typing import Any from typing import Any, Optional
from discord import Guild
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from redbot.core.bot import Red from redbot.core.bot import Red
@ -12,17 +13,19 @@ class AuroraBaseModel(BaseModel):
def dump(self) -> dict: def dump(self) -> dict:
return self.model_dump(exclude={"bot"}) return self.model_dump(exclude={"bot"})
def to_json(self, indent: int | None = None, file: Any | None = None, **kwargs): def to_json(self, indent: int | None = None, file: Any | None = None, **kwargs) -> str:
from ..utilities.json import dump, dumps # pylint: disable=cyclic-import from ..utilities.json import dump, dumps # pylint: disable=cyclic-import
return dump(self.dump(), file, indent=indent, **kwargs) if file else dumps(self.model_dump(exclude={"bot"}), indent=indent, **kwargs) return dump(self.dump(), file, indent=indent, **kwargs) if file else dumps(self.dump(), indent=indent, **kwargs)
class AuroraGuildModel(AuroraBaseModel): class AuroraGuildModel(AuroraBaseModel):
"""Subclass of AuroraBaseModel that includes a guild_id attribute, and a modified to_json() method to match.""" """Subclass of AuroraBaseModel that includes a guild_id attribute and a guild attribute, and a modified to_json() method to match."""
model_config = ConfigDict(ignored_types=(Red, Guild), arbitrary_types_allowed=True)
guild_id: int guild_id: int
guild: Optional[Guild] = None
def dump(self) -> dict: def dump(self) -> dict:
return self.model_dump(exclude={"bot", "guild_id"}) return self.model_dump(exclude={"bot", "guild_id", "guild"})
def to_json(self, indent: int | None = None, file: Any | None = None, **kwargs): def to_json(self, indent: int | None = None, file: Any | None = None, **kwargs) -> str:
from ..utilities.json import dump, dumps # pylint: disable=cyclic-import from ..utilities.json import dump, dumps # pylint: disable=cyclic-import
return dump(self.dump(), file, indent=indent, **kwargs) if file else dumps(self.model_dump(exclude={"bot", "guild_id"}), indent=indent, **kwargs) return dump(self.dump(), file, indent=indent, **kwargs) if file else dumps(self.dump(), indent=indent, **kwargs)

View file

@ -1,12 +1,14 @@
import json import json
import sqlite3 import sqlite3
from datetime import datetime, timedelta from datetime import datetime, timedelta
from sqlite3 import Cursor
from time import time from time import time
from typing import Dict, Iterable, List, Optional, Tuple, Union from typing import Dict, Iterable, List, Optional, Tuple, Union
import discord import discord
from aiosqlite import Connection, Cursor, OperationalError, Row
from aiosqlite import connect as aiosqlite_connect
from discord import NotFound from discord import NotFound
from redbot.core import data_manager
from redbot.core.bot import Red from redbot.core.bot import Red
from ..utilities.logger import logger from ..utilities.logger import logger
@ -52,7 +54,7 @@ class Moderation(AuroraGuildModel):
async def get_target(self) -> Union["PartialUser", "PartialChannel"]: async def get_target(self) -> Union["PartialUser", "PartialChannel"]:
if self.target_type == "USER": if self.target_type == "USER":
return await PartialUser.from_id(self.bot, self.target_id) return await PartialUser.from_id(self.bot, self.target_id)
return await PartialChannel.from_id(self.bot, self.target_id) return await PartialChannel.from_id(self.bot, self.target_id, self.guild)
async def get_resolved_by(self) -> Optional["PartialUser"]: async def get_resolved_by(self) -> Optional["PartialUser"]:
if self.resolved_by: if self.resolved_by:
@ -61,7 +63,7 @@ class Moderation(AuroraGuildModel):
async def get_role(self) -> Optional["PartialRole"]: async def get_role(self) -> Optional["PartialRole"]:
if self.role_id: if self.role_id:
return await PartialRole.from_id(self.bot, self.guild_id, self.role_id) return await PartialRole.from_id(self.bot, self.guild, self.role_id)
return None return None
def __str__(self) -> str: def __str__(self) -> str:
@ -115,33 +117,31 @@ class Moderation(AuroraGuildModel):
"user_id": resolved_by, "user_id": resolved_by,
})) }))
self.update() await self.update()
def update(self) -> None: async def update(self) -> None:
from ..utilities.database import connect
from ..utilities.json import dumps from ..utilities.json import dumps
query = f"UPDATE moderation_{self.guild_id} SET timestamp = ?, moderation_type = ?, target_type = ?, moderator_id = ?, role_id = ?, duration = ?, end_timestamp = ?, reason = ?, resolved = ?, resolved_by = ?, resolve_reason = ?, expired = ?, changes = ?, metadata = ? WHERE moderation_id = ?;" query = f"UPDATE moderation_{self.guild_id} SET timestamp = ?, moderation_type = ?, target_type = ?, moderator_id = ?, role_id = ?, duration = ?, end_timestamp = ?, reason = ?, resolved = ?, resolved_by = ?, resolve_reason = ?, expired = ?, changes = ?, metadata = ? WHERE moderation_id = ?;"
with connect() as database: await self.execute(query, (
database.execute(query, ( self.timestamp.timestamp(),
self.timestamp.timestamp(), self.moderation_type,
self.moderation_type, self.target_type,
self.target_type, self.moderator_id,
self.moderator_id, self.role_id,
self.role_id, str(self.duration) if self.duration else None,
str(self.duration) if self.duration else None, self.end_timestamp.timestamp() if self.end_timestamp else None,
self.end_timestamp.timestamp() if self.end_timestamp else None, self.reason,
self.reason, self.resolved,
self.resolved, self.resolved_by,
self.resolved_by, self.resolve_reason,
self.resolve_reason, self.expired,
self.expired, dumps(self.changes),
dumps(self.changes), dumps(self.metadata),
dumps(self.metadata), self.moderation_id,
self.moderation_id, ))
))
logger.debug("Row updated in moderation_%s!\n%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s", logger.verbose("Row updated in moderation_%s!\n%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s",
self.moderation_id, self.moderation_id,
self.guild_id, self.guild_id,
self.timestamp.timestamp(), self.timestamp.timestamp(),
@ -162,6 +162,14 @@ class Moderation(AuroraGuildModel):
@classmethod @classmethod
def from_dict(cls, bot: Red, data: dict) -> "Moderation": def from_dict(cls, bot: Red, data: dict) -> "Moderation":
if data.get("guild_id"):
try:
guild: discord.Guild = bot.get_guild(data["guild_id"])
if not guild:
guild = bot.fetch_guild(data["guild_id"])
except (discord.Forbidden, discord.HTTPException):
guild = None
data.update({"guild": guild})
return cls(bot=bot, **data) return cls(bot=bot, **data)
@classmethod @classmethod
@ -205,37 +213,51 @@ class Moderation(AuroraGuildModel):
} }
return cls.from_dict(bot=bot, data=case) return cls.from_dict(bot=bot, data=case)
@staticmethod
async def connect() -> Connection:
"""Connects to the SQLite database, and returns a connection object."""
try:
connection = await aiosqlite_connect(
database=data_manager.cog_data_path(raw_name="Aurora") / "aurora.db"
)
return connection
except OperationalError as e:
logger.error("Unable to access the SQLite database!\nError:\n%s", e.msg)
raise ConnectionRefusedError(
f"Unable to access the SQLite Database!\n{e.msg}"
) from e
@classmethod @classmethod
def execute(cls, bot: Red, guild_id: int, query: str, parameters: tuple | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]: async def execute(cls, query: str, parameters: tuple | None = None, bot: Red | None = None, guild_id: int | None = None, cursor: Cursor | None = None, return_obj: bool = True) -> Union[Tuple["Moderation"], Iterable[Row]]:
from ..utilities.database import connect logger.trace("Executing query: \"%s\" with parameters \"%s\"", query, parameters)
logger.trace("Executing query: %s", query)
logger.trace("With parameters: %s", parameters)
if not parameters: if not parameters:
parameters = () parameters = ()
if not cursor: if not cursor:
no_cursor = True no_cursor = True
database = connect() database = await cls.connect()
cursor = database.cursor() cursor = await database.cursor()
else: else:
no_cursor = False no_cursor = False
cursor.execute(query, parameters) await cursor.execute(query, parameters)
results = cursor.fetchall() results = await cursor.fetchall()
await database.commit()
if no_cursor: if no_cursor:
cursor.close() await cursor.close()
database.close() await database.close()
if results: if results and return_obj and bot and guild_id:
cases = [] cases = []
for result in results: for result in results:
case = cls.from_result(bot=bot, result=result, guild_id=guild_id) case = cls.from_result(bot=bot, result=result, guild_id=guild_id)
if case.moderation_id != 0: if case.moderation_id != 0:
cases.append(case) cases.append(case)
return tuple(cases) return tuple(cases)
return () return results
@classmethod @classmethod
def get_latest(cls, bot: Red, guild_id: int, limit: int | None = None, offset: int = 0, types: Iterable | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]: async def get_latest(cls, bot: Red, guild_id: int, limit: int | None = None, offset: int = 0, types: Iterable | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]:
params = [] params = []
query = f"SELECT * FROM moderation_{guild_id} ORDER BY moderation_id DESC" query = f"SELECT * FROM moderation_{guild_id} ORDER BY moderation_id DESC"
if types: if types:
@ -245,41 +267,41 @@ class Moderation(AuroraGuildModel):
query += " LIMIT ? OFFSET ?" query += " LIMIT ? OFFSET ?"
params.extend((limit, offset)) params.extend((limit, offset))
query += ";" query += ";"
return cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=tuple(params) if params else (), cursor=cursor) return await cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=tuple(params) if params else (), cursor=cursor)
@classmethod @classmethod
def get_next_case_number(cls, bot: Red, guild_id: int, cursor: Cursor | None = None) -> int: async def get_next_case_number(cls, bot: Red, guild_id: int, cursor: Cursor | None = None) -> int:
result = cls.get_latest(bot=bot, guild_id=guild_id, cursor=cursor, limit=1) result = await cls.get_latest(bot=bot, guild_id=guild_id, cursor=cursor, limit=1)
return (result[0].moderation_id + 1) if result else 1 return (result[0].moderation_id + 1) if result else 1
@classmethod @classmethod
def find_by_id(cls, bot: Red, moderation_id: int, guild_id: int, cursor: Cursor | None = None) -> "Moderation": async def find_by_id(cls, bot: Red, moderation_id: int, guild_id: int, cursor: Cursor | None = None) -> "Moderation":
query = f"SELECT * FROM moderation_{guild_id} WHERE moderation_id = ?;" query = f"SELECT * FROM moderation_{guild_id} WHERE moderation_id = ?;"
case = cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=(moderation_id,), cursor=cursor) case = await cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=(moderation_id,), cursor=cursor)
if case: if case:
return case[0] return case[0]
raise ValueError(f"Case {moderation_id} not found in moderation_{guild_id}!") raise ValueError(f"Case {moderation_id} not found in moderation_{guild_id}!")
@classmethod @classmethod
def find_by_target(cls, bot: Red, guild_id: int, target: int, types: Iterable | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]: async def find_by_target(cls, bot: Red, guild_id: int, target: int, types: Iterable | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]:
query = f"SELECT * FROM moderation_{guild_id} WHERE target_id = ?" query = f"SELECT * FROM moderation_{guild_id} WHERE target_id = ?"
if types: if types:
query += f" AND moderation_type IN ({', '.join(['?' for _ in types])})" query += f" AND moderation_type IN ({', '.join(['?' for _ in types])})"
query += " ORDER BY moderation_id DESC;" query += " ORDER BY moderation_id DESC;"
return cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=(target, *types) if types else (target,), cursor=cursor) return await cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=(target, *types) if types else (target,), cursor=cursor)
@classmethod @classmethod
def find_by_moderator(cls, bot: Red, guild_id: int, moderator: int, types: Iterable | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]: async def find_by_moderator(cls, bot: Red, guild_id: int, moderator: int, types: Iterable | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]:
query = f"SELECT * FROM moderation_{guild_id} WHERE moderator_id = ?" query = f"SELECT * FROM moderation_{guild_id} WHERE moderator_id = ?"
if types: if types:
query += f" AND moderation_type IN ({', '.join(['?' for _ in types])})" query += f" AND moderation_type IN ({', '.join(['?' for _ in types])})"
query += " ORDER BY moderation_id DESC;" query += " ORDER BY moderation_id DESC;"
return cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=(moderator, *types) if types else (moderator,), cursor=cursor) return await cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=(moderator, *types) if types else (moderator,), cursor=cursor)
@classmethod @classmethod
def log( async def log(
cls, cls,
bot: Red, bot: Red,
guild_id: int, guild_id: int,
@ -300,7 +322,6 @@ class Moderation(AuroraGuildModel):
metadata: dict | None = None, metadata: dict | None = None,
return_obj: bool = True, return_obj: bool = True,
) -> Union["Moderation", int]: ) -> Union["Moderation", int]:
from ..utilities.database import connect
from ..utilities.json import dumps from ..utilities.json import dumps
if not timestamp: if not timestamp:
timestamp = datetime.fromtimestamp(time()) timestamp = datetime.fromtimestamp(time())
@ -335,13 +356,12 @@ class Moderation(AuroraGuildModel):
role_id = None role_id = None
if not database: if not database:
database = connect() database = await cls.connect()
close_db = True close_db = True
else: else:
close_db = False close_db = False
cursor = database.cursor()
moderation_id = cls.get_next_case_number(bot=bot, guild_id=guild_id, cursor=cursor) moderation_id = await cls.get_next_case_number(bot=bot, guild_id=guild_id)
case = { case = {
"moderation_id": moderation_id, "moderation_id": moderation_id,
@ -363,14 +383,13 @@ class Moderation(AuroraGuildModel):
} }
sql = f"INSERT INTO `moderation_{guild_id}` (moderation_id, timestamp, moderation_type, target_type, target_id, moderator_id, role_id, duration, end_timestamp, reason, resolved, resolved_by, resolve_reason, expired, changes, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" sql = f"INSERT INTO `moderation_{guild_id}` (moderation_id, timestamp, moderation_type, target_type, target_id, moderator_id, role_id, duration, end_timestamp, reason, resolved, resolved_by, resolve_reason, expired, changes, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
cursor.execute(sql, tuple(case.values())) await database.execute(sql, tuple(case.values()))
cursor.close() await database.commit()
database.commit()
if close_db: if close_db:
database.close() await database.close()
logger.debug( logger.verbose(
"Row inserted into moderation_%s!\n%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s", "Row inserted into moderation_%s!\n%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s",
guild_id, guild_id,
case["moderation_id"], case["moderation_id"],
@ -392,5 +411,5 @@ class Moderation(AuroraGuildModel):
) )
if return_obj: if return_obj:
return cls.find_by_id(bot=bot, moderation_id=moderation_id, guild_id=guild_id) return await cls.find_by_id(bot=bot, moderation_id=moderation_id, guild_id=guild_id)
return moderation_id return moderation_id

View file

@ -1,4 +1,4 @@
from discord import Forbidden, HTTPException, InvalidData, NotFound from discord import ChannelType, Forbidden, Guild, HTTPException, InvalidData, NotFound
from redbot.core.bot import Red from redbot.core.bot import Red
from .base import AuroraBaseModel, AuroraGuildModel from .base import AuroraBaseModel, AuroraGuildModel
@ -31,6 +31,7 @@ class PartialUser(AuroraBaseModel):
class PartialChannel(AuroraGuildModel): class PartialChannel(AuroraGuildModel):
id: int id: int
name: str name: str
type: ChannelType
@property @property
def mention(self): def mention(self):
@ -42,17 +43,17 @@ class PartialChannel(AuroraGuildModel):
return self.mention return self.mention
@classmethod @classmethod
async def from_id(cls, bot: Red, channel_id: int) -> "PartialChannel": async def from_id(cls, bot: Red, channel_id: int, guild: Guild) -> "PartialChannel":
channel = bot.get_channel(channel_id) channel = bot.get_channel(channel_id)
if not channel: if not channel:
try: try:
channel = await bot.fetch_channel(channel_id) channel = await bot.fetch_channel(channel_id)
return cls(bot=bot, guild_id=channel.guild.id, id=channel.id, name=channel.name) return cls(bot=bot, guild_id=channel.guild.id, guild=guild, id=channel.id, name=channel.name, type=channel.type)
except (NotFound, InvalidData, HTTPException, Forbidden) as e: except (NotFound, InvalidData, HTTPException, Forbidden) as e:
if e == Forbidden: if e == Forbidden:
return cls(bot=bot, guild_id=0, id=channel_id, name="Forbidden Channel") return cls(bot=bot, guild_id=0, id=channel_id, name="Forbidden Channel")
return cls(bot=bot, guild_id=0, id=channel_id, name="Deleted Channel") return cls(bot=bot, guild_id=0, id=channel_id, name="Deleted Channel", type=ChannelType.text)
return cls(bot=bot, guild_id=channel.guild.id, id=channel.id, name=channel.name) return cls(bot=bot, guild_id=channel.guild.id, guild=guild, id=channel.id, name=channel.name, type=channel.type)
class PartialRole(AuroraGuildModel): class PartialRole(AuroraGuildModel):
id: int id: int
@ -68,12 +69,8 @@ class PartialRole(AuroraGuildModel):
return self.mention return self.mention
@classmethod @classmethod
async def from_id(cls, bot: Red, guild_id: int, role_id: int) -> "PartialRole": async def from_id(cls, bot: Red, guild: Guild, role_id: int) -> "PartialRole":
try:
guild = await bot.fetch_guild(guild_id, with_counts=False)
except (Forbidden, HTTPException):
return cls(bot=bot, guild_id=guild_id, id=role_id, name="Forbidden Role")
role = guild.get_role(role_id) role = guild.get_role(role_id)
if not role: if not role:
return cls(bot=bot, guild_id=guild_id, id=role_id, name="Deleted Role") return cls(bot=bot, guild_id=guild.id, id=role_id, name="Deleted Role")
return cls(bot=bot, guild_id=guild_id, id=role.id, name=role.name) return cls(bot=bot, guild_id=guild.id, id=role.id, name=role.name)

View file

@ -1,100 +0,0 @@
# pylint: disable=cyclic-import
import json
import sqlite3
from discord import Guild
from redbot.core import data_manager
from .logger import logger
def connect() -> sqlite3.Connection:
"""Connects to the SQLite database, and returns a connection object."""
try:
connection = sqlite3.connect(
database=data_manager.cog_data_path(raw_name="Aurora") / "aurora.db"
)
return connection
except sqlite3.OperationalError as e:
logger.error("Unable to access the SQLite database!\nError:\n%s", e.msg)
raise ConnectionRefusedError(
f"Unable to access the SQLite Database!\n{e.msg}"
) from e
async def create_guild_table(guild: Guild):
database = connect()
cursor = database.cursor()
try:
cursor.execute(f"SELECT * FROM `moderation_{guild.id}`")
logger.debug("SQLite Table exists for server %s (%s)", guild.name, guild.id)
except sqlite3.OperationalError:
query = f"""
CREATE TABLE `moderation_{guild.id}` (
moderation_id INTEGER PRIMARY KEY,
timestamp INTEGER NOT NULL,
moderation_type TEXT NOT NULL,
target_type TEXT NOT NULL,
target_id INTEGER NOT NULL,
moderator_id INTEGER NOT NULL,
role_id INTEGER,
duration TEXT,
end_timestamp INTEGER,
reason TEXT,
resolved INTEGER NOT NULL,
resolved_by TEXT,
resolve_reason TEXT,
expired INTEGER NOT NULL,
changes JSON NOT NULL,
metadata JSON NOT NULL
)
"""
cursor.execute(query)
index_query_1 = f"CREATE INDEX IF NOT EXISTS idx_target_id ON moderation_{guild.id}(target_id);"
cursor.execute(index_query_1)
index_query_2 = f"CREATE INDEX IF NOT EXISTS idx_moderator_id ON moderation_{guild.id}(moderator_id);"
cursor.execute(index_query_2)
index_query_3 = f"CREATE INDEX IF NOT EXISTS idx_moderation_id ON moderation_{guild.id}(moderation_id);"
cursor.execute(index_query_3)
insert_query = f"""
INSERT INTO `moderation_{guild.id}`
(moderation_id, timestamp, moderation_type, target_type, target_id, moderator_id, role_id, duration, end_timestamp, reason, resolved, resolved_by, resolve_reason, expired, changes, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
insert_values = (
0,
0,
"NULL",
"NULL",
0,
0,
None,
None,
None,
None,
0,
None,
None,
0,
json.dumps([]),
json.dumps({}),
)
cursor.execute(insert_query, insert_values)
database.commit()
logger.debug(
"SQLite Table (moderation_%s) created for %s (%s)",
guild.id,
guild.name,
guild.id,
)
database.close()

View file

@ -91,7 +91,7 @@ async def message_factory(
embed.set_author(name=guild.name) embed.set_author(name=guild.name)
embed.set_footer( embed.set_footer(
text=f"Case #{Moderation.get_next_case_number(bot=bot, guild_id=guild.id):,}", text=f"Case #{await Moderation.get_next_case_number(bot=bot, guild_id=guild.id):,}",
icon_url="attachment://arrow.png", icon_url="attachment://arrow.png",
) )

View file

@ -2,6 +2,7 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional, Tuple, Union from typing import Optional, Tuple, Union
import aiosqlite
from dateutil.relativedelta import relativedelta as rd from dateutil.relativedelta import relativedelta as rd
from discord import File, Guild, Interaction, Member, SelectOption, TextChannel, User from discord import File, Guild, Interaction, Member, SelectOption, TextChannel, User
from discord.errors import Forbidden from discord.errors import Forbidden
@ -9,6 +10,8 @@ from redbot.core import commands, data_manager
from redbot.core.utils.chat_formatting import error from redbot.core.utils.chat_formatting import error
from ..utilities.config import config from ..utilities.config import config
from ..utilities.json import dumps
from ..utilities.logger import logger
def check_permissions( def check_permissions(
@ -120,7 +123,7 @@ async def log(interaction: Interaction, moderation_id: int, resolved: bool = Fal
logging_channel = interaction.guild.get_channel(logging_channel_id) logging_channel = interaction.guild.get_channel(logging_channel_id)
try: try:
moderation = Moderation.find_by_id(interaction.client, moderation_id, interaction.guild_id) moderation = await Moderation.find_by_id(interaction.client, moderation_id, interaction.guild_id)
embed = await log_factory( embed = await log_factory(
interaction=interaction, moderation=moderation, resolved=resolved interaction=interaction, moderation=moderation, resolved=resolved
) )
@ -145,7 +148,7 @@ async def send_evidenceformat(interaction: Interaction, moderation_id: int) -> N
if send_evidence_bool is False: if send_evidence_bool is False:
return return
moderation = Moderation.find_by_id(interaction.client, moderation_id, interaction.guild.id) moderation = await Moderation.find_by_id(interaction.client, moderation_id, interaction.guild.id)
content = await evidenceformat_factory(moderation=moderation) content = await evidenceformat_factory(moderation=moderation)
await interaction.followup.send(content=content, ephemeral=True) await interaction.followup.send(content=content, ephemeral=True)
@ -210,3 +213,69 @@ def get_footer_image(coginstance: commands.Cog) -> File:
"""Returns the footer image for the embeds.""" """Returns the footer image for the embeds."""
image_path = data_manager.bundled_data_path(coginstance) / "arrow.png" image_path = data_manager.bundled_data_path(coginstance) / "arrow.png"
return File(image_path, filename="arrow.png", description="arrow") return File(image_path, filename="arrow.png", description="arrow")
async def create_guild_table(guild: Guild) -> None:
from ..models.moderation import Moderation
try:
await Moderation.execute(f"SELECT * FROM `moderation_{guild.id}`", return_obj=False)
logger.trace("SQLite Table exists for server %s (%s)", guild.name, guild.id)
except aiosqlite.OperationalError:
query = f"""
CREATE TABLE `moderation_{guild.id}` (
moderation_id INTEGER PRIMARY KEY,
timestamp INTEGER NOT NULL,
moderation_type TEXT NOT NULL,
target_type TEXT NOT NULL,
target_id INTEGER NOT NULL,
moderator_id INTEGER NOT NULL,
role_id INTEGER,
duration TEXT,
end_timestamp INTEGER,
reason TEXT,
resolved INTEGER NOT NULL,
resolved_by TEXT,
resolve_reason TEXT,
expired INTEGER NOT NULL,
changes JSON NOT NULL,
metadata JSON NOT NULL
)
"""
await Moderation.execute(query=query, return_obj=False)
index_query_1 = f"CREATE INDEX IF NOT EXISTS idx_target_id ON moderation_{guild.id}(target_id);"
await Moderation.execute(query=index_query_1, return_obj=False)
index_query_2 = f"CREATE INDEX IF NOT EXISTS idx_moderator_id ON moderation_{guild.id}(moderator_id);"
await Moderation.execute(query=index_query_2, return_obj=False)
index_query_3 = f"CREATE INDEX IF NOT EXISTS idx_moderation_id ON moderation_{guild.id}(moderation_id);"
await Moderation.execute(query=index_query_3, return_obj=False)
insert_query = f"""
INSERT INTO `moderation_{guild.id}`
(moderation_id, timestamp, moderation_type, target_type, target_id, moderator_id, role_id, duration, end_timestamp, reason, resolved, resolved_by, resolve_reason, expired, changes, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"""
insert_values = (
0,
0,
"NULL",
"NULL",
0,
0,
None,
None,
None,
None,
0,
None,
None,
0,
dumps([]),
dumps({}),
)
await Moderation.execute(query=insert_query, parameters=insert_values, return_obj=False)
logger.trace("SQLite Table created for server %s (%s)", guild.name, guild.id)

20
poetry.lock generated
View file

@ -123,6 +123,24 @@ files = [
[package.dependencies] [package.dependencies]
frozenlist = ">=1.1.0" frozenlist = ">=1.1.0"
[[package]]
name = "aiosqlite"
version = "0.20.0"
description = "asyncio bridge to the standard sqlite3 module"
optional = false
python-versions = ">=3.8"
files = [
{file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"},
{file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"},
]
[package.dependencies]
typing_extensions = ">=4.0"
[package.extras]
dev = ["attribution (==1.7.0)", "black (==24.2.0)", "coverage[toml] (==7.4.1)", "flake8 (==7.0.0)", "flake8-bugbear (==24.2.6)", "flit (==3.9.0)", "mypy (==1.8.0)", "ufmt (==2.3.0)", "usort (==1.0.8.post1)"]
docs = ["sphinx (==7.2.6)", "sphinx-mdinclude (==0.5.3)"]
[[package]] [[package]]
name = "annotated-types" name = "annotated-types"
version = "0.7.0" version = "0.7.0"
@ -2655,4 +2673,4 @@ multidict = ">=4.0"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.11,<3.12" python-versions = ">=3.11,<3.12"
content-hash = "3f732c0b0b0eb2a31fb9484c7cf699cd3c26474d6779528102af4c509a48351e" content-hash = "22b824824f73dc3dc1a9a0a01060371ee1f6414e5bef39cb7455d21121988b47"

View file

@ -18,6 +18,7 @@ pydantic = "^2.7.1"
colorthief = "^0.2.1" colorthief = "^0.2.1"
beautifulsoup4 = "^4.12.3" beautifulsoup4 = "^4.12.3"
markdownify = "^0.12.1" markdownify = "^0.12.1"
aiosqlite = "^0.20.0"
[tool.poetry.group.dev] [tool.poetry.group.dev]
optional = true optional = true