2024-05-06 14:15:05 -04:00
|
|
|
import sqlite3
|
2024-05-04 14:08:53 -04:00
|
|
|
from datetime import datetime, timedelta
|
2024-05-06 14:29:09 -04:00
|
|
|
from itertools import chain
|
2024-05-06 14:15:05 -04:00
|
|
|
from time import time
|
2024-05-04 18:27:12 -04:00
|
|
|
from typing import Any, Dict, List, Literal, Optional, Union
|
2024-05-04 13:41:11 -04:00
|
|
|
|
2024-05-04 16:54:12 -04:00
|
|
|
from discord import Forbidden, HTTPException, InvalidData, NotFound
|
2024-05-04 18:27:12 -04:00
|
|
|
from pydantic import BaseModel, ConfigDict
|
2024-05-04 16:54:12 -04:00
|
|
|
from redbot.core.bot import Red
|
2024-05-04 13:42:58 -04:00
|
|
|
|
2024-05-04 22:17:52 -04:00
|
|
|
from aurora.utilities.logger import logger
|
2024-05-06 14:15:05 -04:00
|
|
|
from aurora.utilities.utils import generate_dict, get_next_case_number
|
2024-05-04 13:41:11 -04:00
|
|
|
|
2024-05-04 15:37:07 -04:00
|
|
|
|
|
|
|
class AuroraBaseModel(BaseModel):
|
|
|
|
"""Base class for all models in Aurora."""
|
2024-05-04 18:28:03 -04:00
|
|
|
model_config = ConfigDict(ignored_types=(Red,), arbitrary_types_allowed=True)
|
2024-05-04 18:27:12 -04:00
|
|
|
bot: Red
|
2024-05-04 18:17:21 -04:00
|
|
|
|
|
|
|
def to_json(self, indent: int = None, file: Any = None, **kwargs):
|
|
|
|
from aurora.utilities.json import dump, dumps
|
|
|
|
return dump(self.model_dump(exclude={"bot"}), file, indent=indent, **kwargs) if file else dumps(self.model_dump(exclude={"bot"}), indent=indent, **kwargs)
|
|
|
|
|
2024-05-04 18:27:12 -04:00
|
|
|
class AuroraGuildModel(AuroraBaseModel):
|
|
|
|
"""Subclass of AuroraBaseModel that includes a guild_id attribute, and a modified to_json() method to match."""
|
2024-05-04 18:05:20 -04:00
|
|
|
guild_id: int
|
2024-05-04 17:48:08 -04:00
|
|
|
|
|
|
|
def to_json(self, indent: int = None, file: Any = None, **kwargs):
|
|
|
|
from aurora.utilities.json import dump, dumps
|
2024-05-04 18:05:20 -04:00
|
|
|
return dump(self.model_dump(exclude={"bot", "guild_id"}), file, indent=indent, **kwargs) if file else dumps(self.model_dump(exclude={"bot", "guild_id"}), indent=indent, **kwargs)
|
2024-05-04 17:48:08 -04:00
|
|
|
|
2024-05-04 18:17:21 -04:00
|
|
|
class Moderation(AuroraGuildModel):
|
2024-05-04 13:41:11 -04:00
|
|
|
moderation_id: int
|
2024-05-04 14:08:53 -04:00
|
|
|
timestamp: datetime
|
2024-05-04 13:41:11 -04:00
|
|
|
moderation_type: str
|
|
|
|
target_type: str
|
|
|
|
target_id: int
|
|
|
|
moderator_id: int
|
2024-05-04 18:05:20 -04:00
|
|
|
role_id: Optional[int] = None
|
|
|
|
duration: Optional[timedelta] = None
|
|
|
|
end_timestamp: Optional[datetime] = None
|
|
|
|
reason: Optional[str] = None
|
2024-05-04 13:41:11 -04:00
|
|
|
resolved: bool
|
2024-05-04 18:05:20 -04:00
|
|
|
resolved_by: Optional[int] = None
|
|
|
|
resolve_reason: Optional[str] = None
|
2024-05-04 13:41:11 -04:00
|
|
|
expired: bool
|
2024-05-04 18:05:20 -04:00
|
|
|
changes: List["Change"]
|
2024-05-04 13:41:11 -04:00
|
|
|
metadata: Dict
|
|
|
|
|
2024-05-04 16:54:12 -04:00
|
|
|
@property
|
|
|
|
def id(self) -> int:
|
|
|
|
return self.moderation_id
|
|
|
|
|
|
|
|
@property
|
|
|
|
def type(self) -> str:
|
|
|
|
return self.moderation_type
|
|
|
|
|
2024-05-04 21:06:29 -04:00
|
|
|
@property
|
|
|
|
def unix_timestamp(self) -> int:
|
|
|
|
return int(self.timestamp.timestamp())
|
|
|
|
|
2024-05-04 17:09:22 -04:00
|
|
|
async def get_moderator(self) -> "PartialUser":
|
2024-05-04 16:54:12 -04:00
|
|
|
return await PartialUser.from_id(self.bot, self.moderator_id)
|
|
|
|
|
2024-05-04 17:09:22 -04:00
|
|
|
async def get_target(self) -> Union["PartialUser", "PartialChannel"]:
|
2024-05-04 20:59:23 -04:00
|
|
|
if self.target_type == "USER":
|
2024-05-04 16:54:12 -04:00
|
|
|
return await PartialUser.from_id(self.bot, self.target_id)
|
|
|
|
else:
|
|
|
|
return await PartialChannel.from_id(self.bot, self.target_id)
|
|
|
|
|
2024-05-04 17:09:22 -04:00
|
|
|
async def get_resolved_by(self) -> Optional["PartialUser"]:
|
2024-05-04 16:54:12 -04:00
|
|
|
if self.resolved_by:
|
|
|
|
return await PartialUser.from_id(self.bot, self.resolved_by)
|
|
|
|
return None
|
|
|
|
|
2024-05-04 17:31:16 -04:00
|
|
|
async def get_role(self) -> Optional["PartialRole"]:
|
|
|
|
if self.role_id:
|
|
|
|
return await PartialRole.from_id(self.bot, self.guild_id, self.role_id)
|
|
|
|
return None
|
|
|
|
|
2024-05-04 13:41:11 -04:00
|
|
|
def __str__(self):
|
|
|
|
return f"{self.moderation_type} {self.target_type} {self.target_id} {self.reason}"
|
|
|
|
|
2024-05-04 22:01:32 -04:00
|
|
|
def update(self):
|
|
|
|
from aurora.utilities.database import connect
|
|
|
|
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:
|
|
|
|
cursor = database.cursor()
|
2024-05-04 22:12:00 -04:00
|
|
|
cursor.execute(query, (
|
|
|
|
self.timestamp,
|
|
|
|
self.moderation_type,
|
|
|
|
self.target_type,
|
|
|
|
self.moderator_id,
|
|
|
|
self.role_id,
|
|
|
|
self.duration,
|
|
|
|
self.end_timestamp,
|
|
|
|
self.reason,
|
|
|
|
self.resolved,
|
|
|
|
self.resolved_by,
|
|
|
|
self.resolve_reason,
|
|
|
|
self.expired,
|
2024-05-06 14:15:05 -04:00
|
|
|
self.changes,
|
|
|
|
self.metadata,
|
2024-05-04 22:12:00 -04:00
|
|
|
self.moderation_id
|
|
|
|
))
|
2024-05-04 22:01:32 -04:00
|
|
|
cursor.close()
|
|
|
|
|
2024-05-04 22:19:29 -04:00
|
|
|
logger.info("Updated moderation case %s in guild %s with the following data:\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s",
|
2024-05-04 22:17:52 -04:00
|
|
|
self.moderation_id,
|
|
|
|
self.guild_id,
|
|
|
|
self.timestamp,
|
|
|
|
self.moderation_type,
|
|
|
|
self.target_type,
|
|
|
|
self.moderator_id,
|
|
|
|
self.role_id,
|
|
|
|
self.duration,
|
|
|
|
self.end_timestamp,
|
|
|
|
self.reason,
|
|
|
|
self.resolved,
|
|
|
|
self.resolved_by,
|
|
|
|
self.resolve_reason,
|
|
|
|
self.expired,
|
2024-05-06 14:15:05 -04:00
|
|
|
self.changes,
|
|
|
|
self.metadata,
|
2024-05-04 22:17:52 -04:00
|
|
|
)
|
|
|
|
|
2024-05-04 13:47:07 -04:00
|
|
|
@classmethod
|
2024-05-04 16:54:12 -04:00
|
|
|
def from_sql(cls, bot: Red, moderation_id: int, guild_id: int) -> Optional["Moderation"]:
|
2024-05-04 14:08:53 -04:00
|
|
|
from aurora.utilities.database import connect
|
|
|
|
query = f"SELECT * FROM moderation_{guild_id} WHERE moderation_id = ?;"
|
2024-05-04 13:47:07 -04:00
|
|
|
|
2024-05-04 13:48:57 -04:00
|
|
|
with connect() as database:
|
|
|
|
cursor = database.cursor()
|
2024-05-04 13:47:07 -04:00
|
|
|
cursor.execute(query, (moderation_id,))
|
|
|
|
result = cursor.fetchone()
|
|
|
|
|
|
|
|
if result:
|
2024-05-04 18:05:20 -04:00
|
|
|
case = generate_dict(bot, result, guild_id)
|
2024-05-04 13:48:57 -04:00
|
|
|
cursor.close()
|
2024-05-04 16:54:12 -04:00
|
|
|
return cls.from_dict(bot, case)
|
2024-05-04 13:47:07 -04:00
|
|
|
|
|
|
|
return None
|
2024-05-04 15:08:08 -04:00
|
|
|
|
2024-05-04 16:54:12 -04:00
|
|
|
@classmethod
|
|
|
|
def from_dict(cls, bot: Red, data: dict) -> "Moderation":
|
|
|
|
return cls(bot=bot, **data)
|
|
|
|
|
2024-05-06 14:15:05 -04:00
|
|
|
@classmethod
|
|
|
|
def log(
|
|
|
|
cls,
|
|
|
|
bot: Red,
|
|
|
|
guild_id: int,
|
|
|
|
author_id: int,
|
|
|
|
moderation_type: str,
|
|
|
|
target_type: str,
|
|
|
|
target_id: int,
|
|
|
|
role_id: int,
|
|
|
|
duration: timedelta = None,
|
|
|
|
reason: str = None,
|
|
|
|
database: sqlite3.Connection = None,
|
|
|
|
timestamp: datetime = None,
|
|
|
|
resolved: bool = False,
|
|
|
|
resolved_by: int = None,
|
|
|
|
resolved_reason: str = None,
|
|
|
|
expired: bool = None,
|
|
|
|
changes: list = None,
|
|
|
|
metadata: dict = None,
|
|
|
|
) -> "Moderation":
|
|
|
|
from aurora.utilities.database import connect
|
2024-05-06 14:21:57 -04:00
|
|
|
from aurora.utilities.json import dumps
|
2024-05-06 14:15:05 -04:00
|
|
|
if not timestamp:
|
|
|
|
timestamp = datetime.fromtimestamp(time())
|
|
|
|
elif not isinstance(timestamp, datetime):
|
|
|
|
timestamp = datetime.fromtimestamp(timestamp)
|
|
|
|
|
|
|
|
if duration != "NULL" and duration is not None:
|
|
|
|
end_timedelta = timestamp + duration
|
|
|
|
end_timestamp = int(end_timedelta.timestamp())
|
|
|
|
else:
|
|
|
|
duration = None
|
|
|
|
end_timestamp = None
|
|
|
|
|
|
|
|
if not expired:
|
|
|
|
if timestamp.timestamp() > end_timestamp:
|
|
|
|
expired = True
|
|
|
|
else:
|
|
|
|
expired = False
|
|
|
|
|
|
|
|
if reason == "NULL":
|
|
|
|
reason = None
|
|
|
|
|
|
|
|
if resolved_by == "NULL":
|
|
|
|
resolved_by = None
|
|
|
|
|
|
|
|
if resolved_reason == "NULL":
|
|
|
|
resolved_reason = None
|
|
|
|
|
|
|
|
if role_id == 0:
|
|
|
|
role_id = None
|
|
|
|
|
|
|
|
if not database:
|
|
|
|
database = connect()
|
|
|
|
close_db = True
|
|
|
|
else:
|
|
|
|
close_db = False
|
|
|
|
cursor = database.cursor()
|
|
|
|
|
|
|
|
moderation_id = get_next_case_number(guild_id=guild_id, cursor=cursor)
|
|
|
|
|
|
|
|
case = {
|
|
|
|
"guild_id": guild_id,
|
|
|
|
"moderation_id": moderation_id,
|
|
|
|
"timestamp": timestamp,
|
|
|
|
"moderation_type": moderation_type,
|
|
|
|
"target_type": target_type,
|
|
|
|
"target_id": target_id,
|
|
|
|
"moderator_id": author_id,
|
|
|
|
"role_id": role_id,
|
|
|
|
"duration": duration,
|
|
|
|
"end_timestamp": end_timestamp,
|
|
|
|
"reason": reason,
|
|
|
|
"resolved": resolved,
|
|
|
|
"resolved_by": resolved_by,
|
|
|
|
"resolve_reason": resolved_reason,
|
|
|
|
"expired": expired,
|
|
|
|
"changes": changes,
|
|
|
|
"metadata": metadata
|
|
|
|
}
|
|
|
|
|
|
|
|
case_safe = case.copy()
|
|
|
|
case_safe.pop("guild_id")
|
2024-05-06 14:20:49 -04:00
|
|
|
case_safe["timestamp"] = case_safe["timestamp"].timestamp()
|
|
|
|
case_safe["end_timestamp"] = case_safe["end_timestamp"].timestamp() if case_safe["end_timestamp"] else None
|
2024-05-06 14:21:57 -04:00
|
|
|
case_safe["changes"] = dumps(case_safe["changes"])
|
|
|
|
case_safe["metadata"] = dumps(case_safe["metadata"])
|
2024-05-06 14:15:05 -04:00
|
|
|
|
|
|
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
2024-05-06 14:20:49 -04:00
|
|
|
cursor.execute(sql, tuple(case_safe.values()))
|
2024-05-06 14:15:05 -04:00
|
|
|
|
|
|
|
cursor.close()
|
|
|
|
database.commit()
|
|
|
|
if close_db:
|
|
|
|
database.close()
|
|
|
|
|
|
|
|
logger.debug(
|
|
|
|
"Row inserted into moderation_%s!\n%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s",
|
2024-05-06 14:29:57 -04:00
|
|
|
tuple(chain({"guild_id": guild_id}.values(), case_safe.values()))
|
2024-05-06 14:15:05 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
return cls.from_dict(bot=bot, **case)
|
|
|
|
|
2024-05-04 17:48:08 -04:00
|
|
|
class Change(AuroraBaseModel):
|
|
|
|
type: Literal["ORIGINAL", "RESOLVE", "EDIT"]
|
|
|
|
timestamp: datetime
|
|
|
|
reason: str
|
|
|
|
user_id: int
|
2024-05-04 18:05:20 -04:00
|
|
|
duration: Optional[timedelta] = None
|
|
|
|
end_timestamp: Optional[datetime] = None
|
2024-05-04 17:48:08 -04:00
|
|
|
|
2024-05-04 21:15:39 -04:00
|
|
|
@property
|
|
|
|
def unix_timestamp(self) -> int:
|
|
|
|
return int(self.timestamp.timestamp())
|
|
|
|
|
2024-05-04 17:48:08 -04:00
|
|
|
def __str__(self):
|
|
|
|
return f"{self.type} {self.user_id} {self.reason}"
|
|
|
|
|
2024-05-04 18:05:20 -04:00
|
|
|
async def get_user(self) -> "PartialUser":
|
|
|
|
return await PartialUser.from_id(self.bot, self.user_id)
|
|
|
|
|
2024-05-04 17:48:08 -04:00
|
|
|
@classmethod
|
2024-05-04 18:17:21 -04:00
|
|
|
def from_dict(cls, bot: Red, data: dict) -> "Change":
|
2024-05-04 22:01:32 -04:00
|
|
|
if "duration" in data and data["duration"] and not isinstance(data["duration"], timedelta):
|
2024-05-04 21:15:39 -04:00
|
|
|
hours, minutes, seconds = map(int, data["duration"].split(':'))
|
|
|
|
duration = timedelta(hours=hours, minutes=minutes, seconds=seconds)
|
2024-05-04 22:02:31 -04:00
|
|
|
elif "duration" in data and isinstance(data["duration"], timedelta):
|
2024-05-04 22:01:32 -04:00
|
|
|
duration = data["duration"]
|
2024-05-04 21:15:39 -04:00
|
|
|
else:
|
|
|
|
duration = None
|
2024-05-04 22:01:32 -04:00
|
|
|
|
|
|
|
if "end_timestamp" in data and data["end_timestamp"] and not isinstance(data["end_timestamp"], datetime):
|
|
|
|
end_timestamp = datetime.fromtimestamp(data["end_timestamp"])
|
|
|
|
elif "end_timestamp" in data and isinstance(data["end_timestamp"], datetime):
|
|
|
|
end_timestamp = data["end_timestamp"]
|
|
|
|
else:
|
|
|
|
end_timestamp = None
|
|
|
|
|
|
|
|
if not isinstance(data["timestamp"], datetime):
|
|
|
|
timestamp = datetime.fromtimestamp(data["timestamp"])
|
|
|
|
else:
|
|
|
|
timestamp = data["timestamp"]
|
|
|
|
|
2024-05-04 21:15:39 -04:00
|
|
|
data.update({
|
2024-05-04 22:01:32 -04:00
|
|
|
"timestamp": timestamp,
|
|
|
|
"end_timestamp": end_timestamp,
|
2024-05-04 21:15:39 -04:00
|
|
|
"duration": duration
|
|
|
|
})
|
2024-05-04 18:18:57 -04:00
|
|
|
return cls(bot=bot, **data)
|
2024-05-04 17:48:08 -04:00
|
|
|
|
2024-05-04 16:54:12 -04:00
|
|
|
class PartialUser(AuroraBaseModel):
|
|
|
|
id: int
|
|
|
|
username: str
|
|
|
|
discriminator: int
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
2024-05-04 21:07:37 -04:00
|
|
|
return f"{self.username}#{self.discriminator}" if self.discriminator != 0 else self.username
|
2024-05-04 16:54:12 -04:00
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
async def from_id(cls, bot: Red, user_id: int) -> "PartialUser":
|
|
|
|
user = bot.get_user(user_id)
|
|
|
|
if not user:
|
|
|
|
try:
|
|
|
|
user = await bot.fetch_user(user_id)
|
2024-05-04 18:17:21 -04:00
|
|
|
return cls(bot=bot, id=user.id, username=user.name, discriminator=user.discriminator)
|
2024-05-04 16:54:12 -04:00
|
|
|
except NotFound:
|
2024-05-04 18:17:21 -04:00
|
|
|
return cls(bot=bot, id=user_id, username="Deleted User", discriminator=0)
|
2024-05-04 21:02:18 -04:00
|
|
|
return cls(bot=bot, id=user.id, username=user.name, discriminator=user.discriminator)
|
2024-05-04 16:54:12 -04:00
|
|
|
|
2024-05-04 17:48:08 -04:00
|
|
|
|
2024-05-04 18:17:21 -04:00
|
|
|
class PartialChannel(AuroraGuildModel):
|
2024-05-04 16:54:12 -04:00
|
|
|
id: int
|
|
|
|
name: str
|
|
|
|
|
|
|
|
@property
|
|
|
|
def mention(self):
|
|
|
|
if self.name == "Deleted Channel" or self.name == "Forbidden Channel":
|
|
|
|
return self.name
|
|
|
|
return f"<#{self.id}>"
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.mention
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
async def from_id(cls, bot: Red, channel_id: int) -> "PartialChannel":
|
2024-05-04 18:05:20 -04:00
|
|
|
channel = bot.get_channel(channel_id)
|
|
|
|
if not channel:
|
2024-05-04 16:54:12 -04:00
|
|
|
try:
|
2024-05-04 18:05:20 -04:00
|
|
|
channel = await bot.fetch_channel(channel_id)
|
|
|
|
return cls(bot=bot, guild_id=channel.guild.id, id=channel.id, username=channel.name)
|
2024-05-04 16:54:12 -04:00
|
|
|
except (NotFound, InvalidData, HTTPException, Forbidden) as e:
|
|
|
|
if e == Forbidden:
|
2024-05-04 20:59:23 -04:00
|
|
|
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")
|
2024-05-04 21:02:18 -04:00
|
|
|
return cls(bot=bot, guild_id=channel.guild.id, id=channel.id, username=channel.name)
|
2024-05-04 17:31:16 -04:00
|
|
|
|
2024-05-04 18:17:21 -04:00
|
|
|
class PartialRole(AuroraGuildModel):
|
2024-05-04 17:31:16 -04:00
|
|
|
id: int
|
|
|
|
name: str
|
|
|
|
|
|
|
|
@property
|
|
|
|
def mention(self):
|
|
|
|
return f"<@&{self.id}>"
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.mention
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
async def from_id(cls, bot: Red, guild_id: int, role_id: int) -> "PartialRole":
|
|
|
|
try:
|
|
|
|
guild = await bot.fetch_guild(guild_id, with_counts=False)
|
|
|
|
except (Forbidden, HTTPException):
|
2024-05-04 18:05:20 -04:00
|
|
|
return cls(bot=bot, guild_id=guild_id, id=role_id, name="Forbidden Role")
|
2024-05-04 17:31:16 -04:00
|
|
|
role = guild.get_role(role_id)
|
|
|
|
if not role:
|
2024-05-04 18:05:20 -04:00
|
|
|
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)
|