feat(aurora): allow for querying moderation history by date (before/after)
Some checks failed
Actions / Build Documentation (MkDocs) (pull_request) Failing after 28s
Actions / Lint Code (Ruff & Pylint) (pull_request) Successful in 42s

This commit is contained in:
Seaswimmer 2024-08-10 13:17:16 -04:00
parent 7f0bd8c1a8
commit 1a3af342df
Signed by: cswimr
GPG key ID: 3813315477F26F82
5 changed files with 108 additions and 10 deletions

View file

@ -13,6 +13,7 @@ from datetime import datetime, timedelta, timezone
from math import ceil from math import ceil
import discord import discord
from dateparser import parse
from discord.ext import tasks from discord.ext import tasks
from redbot.core import app_commands, commands, data_manager from redbot.core import app_commands, commands, data_manager
from redbot.core.app_commands import Choice from redbot.core.app_commands import Choice
@ -520,6 +521,8 @@ class Aurora(commands.Cog):
moderator: discord.User | None = None, moderator: discord.User | None = None,
pagesize: app_commands.Range[int, 1, 20] | None = None, pagesize: app_commands.Range[int, 1, 20] | None = None,
page: int = 1, page: int = 1,
before: str | None = None,
after: str | None = None,
ephemeral: bool | None = None, ephemeral: bool | None = None,
inline: bool | None = None, inline: bool | None = None,
export: bool = False, export: bool = False,
@ -536,6 +539,10 @@ class Aurora(commands.Cog):
Amount of infractions to list per page Amount of infractions to list per page
page: int page: int
Page to select Page to select
before: str
List infractions before a certain date
after: str
List infractions after a certain date
ephemeral: bool ephemeral: bool
Hide the command response Hide the command response
inline: bool inline: bool
@ -570,6 +577,22 @@ class Aurora(commands.Cog):
or 5 or 5
) )
if before:
before = parse(before)
if before is None:
return await interaction.response.send_message(
content=error("Invalid date format for `before`."), ephemeral=True
)
before = before.replace(tzinfo=timezone.utc)
if after:
after = parse(after)
if after is None:
return await interaction.response.send_message(
content=error("Invalid date format for `after`."), ephemeral=True
)
after = after.replace(tzinfo=timezone.utc)
await interaction.response.defer(ephemeral=ephemeral) await interaction.response.defer(ephemeral=ephemeral)
permissions = check_permissions( permissions = check_permissions(
@ -586,13 +609,13 @@ class Aurora(commands.Cog):
if target: if target:
filename = f"moderation_target_{str(target.id)}_{str(interaction.guild.id)}.json" 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) moderations = await Moderation.find_by_target(bot=interaction.client, guild_id=interaction.guild.id, target=target.id, before=before, after=after)
elif moderator: elif moderator:
filename = f"moderation_moderator_{str(moderator.id)}_{str(interaction.guild.id)}.json" 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) moderations = await Moderation.find_by_moderator(bot=interaction.client, guild_id=interaction.guild.id, moderator=moderator.id, before=before, after=after)
else: else:
filename = f"moderation_{str(interaction.guild.id)}.json" filename = f"moderation_{str(interaction.guild.id)}.json"
moderations = await Moderation.get_latest(interaction.client, interaction.guild.id) moderations = await Moderation.get_latest(bot=interaction.client, guild_id=interaction.guild.id, before=before, after=after)
if export: if export:
try: try:

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", "aiosqlite", "phx-class-registry"], "requirements": ["pydantic", "aiosqlite", "phx-class-registry", "dateparser"],
"tags": [ "tags": [
"mod", "mod",
"moderate", "moderate",

View file

@ -307,13 +307,19 @@ class Moderation(AuroraGuildModel):
return results return results
@classmethod @classmethod
async def get_latest(cls, bot: Red, guild_id: int, limit: int | None = None, offset: int = 0, types: Iterable[Type] | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]: async def get_latest(cls, bot: Red, guild_id: int, before: datetime = None, after: datetime = None, limit: int | None = None, offset: int = 0, types: Iterable[Type] | 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:
query += f" WHERE moderation_type IN ({', '.join(['?' for _ in types])})" query += f" WHERE moderation_type IN ({', '.join(['?' for _ in types])})"
for t in types: for t in types:
params.append(t.key) params.append(t.key)
if before:
query += " WHERE timestamp < ?"
params.append(before.timestamp())
if after:
query += " WHERE timestamp > ?"
params.append(after.timestamp())
if limit: if limit:
query += " LIMIT ? OFFSET ?" query += " LIMIT ? OFFSET ?"
params.extend((limit, offset)) params.extend((limit, offset))
@ -334,22 +340,40 @@ class Moderation(AuroraGuildModel):
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
async def find_by_target(cls, bot: Red, guild_id: int, target: int, types: Iterable[Type] | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]: async def find_by_target(cls, bot: Red, guild_id: int, target: int, before: datetime = None, after: datetime = None, types: Iterable[Type] | 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 = ?"
params = [target]
if types: if types:
query += f" AND moderation_type IN ({', '.join(['?' for _ in types])})" query += f" AND moderation_type IN ({', '.join(['?' for _ in types])})"
for t in types:
params.append(t.key)
if before:
query += " AND timestamp < ?"
params.append(before.timestamp())
if after:
query += " AND timestamp > ?"
params.append(after.timestamp())
query += " ORDER BY moderation_id DESC;" query += " ORDER BY moderation_id DESC;"
return await cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=(target, *[t.key for t in types]) if types else (target,), cursor=cursor) return await cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=params, cursor=cursor)
@classmethod @classmethod
async def find_by_moderator(cls, bot: Red, guild_id: int, moderator: int, types: Iterable[Type] | None = None, cursor: Cursor | None = None) -> Tuple["Moderation"]: async def find_by_moderator(cls, bot: Red, guild_id: int, moderator: int, before: datetime = None, after: datetime = None, types: Iterable[Type] | 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 = ?"
params = [moderator]
if types: if types:
query += f" AND moderation_type IN ({', '.join(['?' for _ in types])})" query += f" AND moderation_type IN ({', '.join(['?' for _ in types])})"
for t in types:
params.append(t.key)
if before:
query += " AND timestamp < ?"
params.append(before.timestamp())
if after:
query += " AND timestamp > ?"
params.append(after.timestamp())
query += " ORDER BY moderation_id DESC;" query += " ORDER BY moderation_id DESC;"
return await cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=(moderator, *[t.key for t in types]) if types else (moderator,), cursor=cursor) return await cls.execute(bot=bot, guild_id=guild_id, query=query, parameters=params, cursor=cursor)
@classmethod @classmethod
async def log( async def log(

52
poetry.lock generated
View file

@ -668,6 +668,28 @@ webencodings = "*"
doc = ["sphinx", "sphinx_rtd_theme"] doc = ["sphinx", "sphinx_rtd_theme"]
test = ["flake8", "isort", "pytest"] test = ["flake8", "isort", "pytest"]
[[package]]
name = "dateparser"
version = "1.2.0"
description = "Date parsing library designed to parse dates from HTML pages"
optional = false
python-versions = ">=3.7"
files = [
{file = "dateparser-1.2.0-py2.py3-none-any.whl", hash = "sha256:0b21ad96534e562920a0083e97fd45fa959882d4162acc358705144520a35830"},
{file = "dateparser-1.2.0.tar.gz", hash = "sha256:7975b43a4222283e0ae15be7b4999d08c9a70e2d378ac87385b1ccf2cffbbb30"},
]
[package.dependencies]
python-dateutil = "*"
pytz = "*"
regex = "<2019.02.19 || >2019.02.19,<2021.8.27 || >2021.8.27"
tzlocal = "*"
[package.extras]
calendars = ["convertdate", "hijri-converter"]
fasttext = ["fasttext"]
langdetect = ["langdetect"]
[[package]] [[package]]
name = "defusedxml" name = "defusedxml"
version = "0.7.1" version = "0.7.1"
@ -2373,6 +2395,34 @@ files = [
{file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"},
] ]
[[package]]
name = "tzdata"
version = "2024.1"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
files = [
{file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
{file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
]
[[package]]
name = "tzlocal"
version = "5.2"
description = "tzinfo object for the local timezone"
optional = false
python-versions = ">=3.8"
files = [
{file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"},
{file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"},
]
[package.dependencies]
tzdata = {version = "*", markers = "platform_system == \"Windows\""}
[package.extras]
devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"]
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.2.1" version = "2.2.1"
@ -2690,4 +2740,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 = "6d09969e0ad7adff5aa105915c5c9e0354b4b7aceb80b9c2b3fb800bb08f21d4" content-hash = "bc58f1f72ca73806c414042a78294e73c4119906b57fceb045ab48414928849e"

View file

@ -20,6 +20,7 @@ beautifulsoup4 = "^4.12.3"
markdownify = "^0.12.1" markdownify = "^0.12.1"
aiosqlite = "^0.20.0" aiosqlite = "^0.20.0"
phx-class-registry = "^4.1.0" phx-class-registry = "^4.1.0"
dateparser = "^1.2.0"
[tool.poetry.group.dev] [tool.poetry.group.dev]
optional = true optional = true