feat(aurora): bunch of changes

This commit is contained in:
Seaswimmer 2024-06-08 20:12:22 -04:00
parent f6a42b97d9
commit 0bcbcd6c0c
Signed by untrusted user: cswimr
GPG key ID: 5D671B5D03D65A7F
6 changed files with 122 additions and 149 deletions

View file

@ -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 = await connect() results = await Moderation.execute(query="SHOW TABLES;", return_obj=False)
cursor = await database.cursor() tables = [table[0] for table in results]
await 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}"
await cursor.execute(delete_query, (user_id, user_id)) await Moderation.execute(query=delete_query, parameters=(user_id, user_id), return_obj=False)
await database.commit()
await cursor.close()
await 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":
@ -86,16 +79,16 @@ class Aurora(commands.Cog):
# and the information that aiosqlite logs is not useful to the bot owner. # 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. # 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. # 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.WARNING) 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)
@ -127,13 +120,12 @@ class Aurora(commands.Cog):
async def addrole_on_member_join(self, member: discord.Member): async def addrole_on_member_join(self, member: discord.Member):
"""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):
database = await connect()
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;"""
async with database.execute(query, (member.id,)) as cursor: results = Moderation.execute(query, (member.id,))
async for row in cursor: for row in results:
role = member.guild.get_role(row[1]) role = member.guild.get_role(row[1])
reason = row[2] reason = row[2]
await member.add_roles(role, reason=f"Role automatically added on member rejoin for: {reason} (Case #{row[0]:,})") await member.add_roles(role, reason=f"Role automatically added on member rejoin for: {reason} (Case #{row[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):
@ -1093,7 +1085,7 @@ class Aurora(commands.Cog):
) )
return return
database = await connect() database = await Moderation.connect()
if export: if export:
try: try:
@ -1268,7 +1260,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.
@ -1278,9 +1270,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
@ -1309,8 +1303,8 @@ class Aurora(commands.Cog):
) )
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)
@ -1319,7 +1313,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."
) )

View file

@ -9,9 +9,8 @@ from redbot.core import commands, data_manager
from redbot.core.utils.chat_formatting import warning from redbot.core.utils.chat_formatting import warning
from ..models.moderation import Moderation from ..models.moderation import Moderation
from ..utilities.database import connect, create_guild_table
from ..utilities.json import dump 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):
@ -29,10 +28,8 @@ class ImportAuroraView(ui.View):
"Deleting original table...", ephemeral=True "Deleting original table...", ephemeral=True
) )
database = await connect()
query = f"DROP TABLE IF EXISTS moderation_{self.ctx.guild.id};" query = f"DROP TABLE IF EXISTS moderation_{self.ctx.guild.id};"
await database.execute(query) await Moderation.execute(query=query, return_obj=False)
await database.commit()
await interaction.edit_original_response(content="Creating new table...") await interaction.edit_original_response(content="Creating new table...")
@ -111,15 +108,11 @@ 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
failed_cases.append(str(case["moderation_id"]) + f": {e}") failed_cases.append(str(case["moderation_id"]) + f": {e}")
await database.commit()
await database.close()
await interaction.edit_original_response(content="Import complete.") await interaction.edit_original_response(content="Import complete.")
if failed_cases: if failed_cases:
filename = ( filename = (

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,7 +27,7 @@ class ImportGalacticBotView(ui.View):
"Deleting original table...", ephemeral=True "Deleting original table...", ephemeral=True
) )
database = await connect() database = await Moderation.connect()
cursor = await 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};"

View file

@ -5,8 +5,10 @@ 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 Cursor 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
@ -211,34 +213,48 @@ 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
async def execute(cls, bot: Red, guild_id: int, query: str, parameters: tuple | None = None, cursor: Cursor | None = None, return_obj: bool = True) -> 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 = await connect() database = await cls.connect()
cursor = await database.cursor() cursor = await database.cursor()
else: else:
no_cursor = False no_cursor = False
await cursor.execute(query, parameters) await cursor.execute(query, parameters)
results = await cursor.fetchall() results = await cursor.fetchall()
await database.commit()
if no_cursor: if no_cursor:
await cursor.close() await cursor.close()
await database.close() await database.close()
if results and return_obj: 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
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"]: 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"]:
@ -306,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())
@ -341,7 +356,7 @@ class Moderation(AuroraGuildModel):
role_id = None role_id = None
if not database: if not database:
database = await connect() database = await cls.connect()
close_db = True close_db = True
else: else:
close_db = False close_db = False

View file

@ -1,99 +0,0 @@
# pylint: disable=cyclic-import
import json
import aiosqlite
from discord import Guild
from redbot.core import data_manager
from .logger import logger
async def connect() -> aiosqlite.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 aiosqlite.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 = await connect()
try:
await database.execute(f"SELECT * FROM `moderation_{guild.id}`")
logger.verbose("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 database.execute(query)
index_query_1 = f"CREATE INDEX IF NOT EXISTS idx_target_id ON moderation_{guild.id}(target_id);"
await database.execute(index_query_1)
index_query_2 = f"CREATE INDEX IF NOT EXISTS idx_moderator_id ON moderation_{guild.id}(moderator_id);"
await database.execute(index_query_2)
index_query_3 = f"CREATE INDEX IF NOT EXISTS idx_moderation_id ON moderation_{guild.id}(moderation_id);"
await database.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({}),
)
await database.execute(insert_query, insert_values)
await database.commit()
logger.debug(
"SQLite Table (moderation_%s) created for %s (%s)",
guild.id,
guild.name,
guild.id,
)
await database.close()

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(
@ -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)