Enhance AutoMod configuration by introducing event-specific settings and thresholds. Refactor Automod schemas to include event configurations, thresholds, and limits for ignored roles and channels. Update database interactions to support new structure and improve handling of AutoMod rules in cogs, ensuring dynamic thresholds for various events. Update dashboard components to accommodate new configurations and improve user experience in managing AutoMod settings.
This commit is contained in:
@@ -17,7 +17,7 @@ from api.dependencies import get_bot, limiter
|
||||
from api.discord_auth import get_manageable_guild_ids, get_discord_headers
|
||||
from api.db_manager import db_manager
|
||||
from api.schemas import (
|
||||
GuildSummary, GuildDetails, PrefixConfig, AutomodConfig,
|
||||
GuildSummary, GuildDetails, PrefixConfig, AutomodConfig, AutomodEventConfig,
|
||||
TicketConfig, LevelingConfig, LoggingConfig, TicketEmbed,
|
||||
TicketCategory, LevelingEmbedStyle, PrefixUpdate,
|
||||
AutomodUpdate, LevelingUpdate, LoggingUpdate, TicketUpdate,
|
||||
@@ -30,11 +30,23 @@ from api.schemas import (
|
||||
RRConfig, RRUpdate, ReactionRoleEntry,
|
||||
InviteStat, InvitesLeaderboard
|
||||
)
|
||||
from utils.automod_settings import (
|
||||
EVENT_ID_TO_DB,
|
||||
THRESHOLD_META,
|
||||
MAX_IGNORED,
|
||||
NSFW_KEYWORDS,
|
||||
normalize_event_id,
|
||||
normalize_punishment,
|
||||
ensure_thresholds_table,
|
||||
get_all_thresholds,
|
||||
set_thresholds,
|
||||
)
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
import math
|
||||
import aiosqlite
|
||||
import json
|
||||
import os
|
||||
import discord
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.axiom import Axiom
|
||||
@@ -121,78 +133,264 @@ async def get_guild_automod(guild_id: int):
|
||||
Retrieves the AutoMod configuration for a specific guild.
|
||||
"""
|
||||
db = await db_manager.get_connection(db_path('automod.db'))
|
||||
# Check enabled status
|
||||
await ensure_thresholds_table(db)
|
||||
|
||||
cursor = await db.execute("SELECT enabled FROM automod WHERE guild_id = ?", (guild_id,))
|
||||
enabled_row = await cursor.fetchone()
|
||||
enabled = bool(enabled_row[0]) if enabled_row else False
|
||||
|
||||
# Get punishments
|
||||
cursor = await db.execute("SELECT event, punishment FROM automod_punishments WHERE guild_id = ?", (guild_id,))
|
||||
punishments = {row[0]: row[1] for row in await cursor.fetchall()}
|
||||
cursor = await db.execute(
|
||||
"SELECT event, punishment FROM automod_punishments WHERE guild_id = ?",
|
||||
(guild_id,),
|
||||
)
|
||||
raw_punishments = {row[0]: row[1] for row in await cursor.fetchall()}
|
||||
|
||||
# Get ignored items
|
||||
cursor = await db.execute("SELECT type, id FROM automod_ignored WHERE guild_id = ?", (guild_id,))
|
||||
events: dict[str, AutomodEventConfig] = {}
|
||||
punishments_out: dict[str, str] = {}
|
||||
for event_id, db_name in EVENT_ID_TO_DB.items():
|
||||
present = db_name in raw_punishments
|
||||
pun = raw_punishments.get(db_name)
|
||||
if event_id == "anti_nsfw_link":
|
||||
events[event_id] = AutomodEventConfig(
|
||||
enabled=present,
|
||||
punishment="block_message",
|
||||
readonly_punishment=True,
|
||||
)
|
||||
if present:
|
||||
punishments_out[event_id] = "block_message"
|
||||
else:
|
||||
norm = normalize_punishment(pun) if present else None
|
||||
events[event_id] = AutomodEventConfig(
|
||||
enabled=present,
|
||||
punishment=norm or ("Mute" if present else None),
|
||||
readonly_punishment=False,
|
||||
)
|
||||
if present and norm:
|
||||
punishments_out[event_id] = norm
|
||||
|
||||
cursor = await db.execute(
|
||||
"SELECT type, id FROM automod_ignored WHERE guild_id = ?", (guild_id,)
|
||||
)
|
||||
ignored_items = await cursor.fetchall()
|
||||
ignored_roles = [row[1] for row in ignored_items if row[0] == 'role']
|
||||
ignored_channels = [row[1] for row in ignored_items if row[0] == 'channel']
|
||||
ignored_roles = [row[1] for row in ignored_items if row[0] == "role"]
|
||||
ignored_channels = [row[1] for row in ignored_items if row[0] == "channel"]
|
||||
|
||||
# Get logging channel
|
||||
cursor = await db.execute("SELECT log_channel FROM automod_logging WHERE guild_id = ?", (guild_id,))
|
||||
cursor = await db.execute(
|
||||
"SELECT log_channel FROM automod_logging WHERE guild_id = ?", (guild_id,)
|
||||
)
|
||||
logging_row = await cursor.fetchone()
|
||||
logging_channel = logging_row[0] if logging_row else None
|
||||
|
||||
thresholds = await get_all_thresholds(guild_id)
|
||||
|
||||
return AutomodConfig(
|
||||
guild_id=guild_id,
|
||||
enabled=enabled,
|
||||
punishments=punishments,
|
||||
punishments=punishments_out,
|
||||
events=events,
|
||||
thresholds=thresholds,
|
||||
threshold_meta=THRESHOLD_META,
|
||||
ignored_roles=ignored_roles,
|
||||
ignored_channels=ignored_channels,
|
||||
logging_channel=logging_channel
|
||||
logging_channel=logging_channel,
|
||||
limits={"max_ignored_roles": MAX_IGNORED, "max_ignored_channels": MAX_IGNORED},
|
||||
)
|
||||
|
||||
|
||||
async def _sync_nsfw_rule(
|
||||
bot: "Axiom",
|
||||
guild_id: int,
|
||||
*,
|
||||
enabled: bool,
|
||||
ignored_roles: list[int],
|
||||
ignored_channels: list[int],
|
||||
) -> None:
|
||||
guild = bot.get_guild(guild_id)
|
||||
if not guild:
|
||||
return
|
||||
|
||||
existing = None
|
||||
try:
|
||||
rules = await guild.fetch_automod_rules()
|
||||
existing = next((r for r in rules if r.name == "Anti NSFW Links"), None)
|
||||
except (discord.Forbidden, discord.HTTPException):
|
||||
return
|
||||
|
||||
if not enabled:
|
||||
if existing:
|
||||
try:
|
||||
await existing.delete(reason="Automod NSFW disabled via dashboard")
|
||||
except (discord.Forbidden, discord.HTTPException):
|
||||
pass
|
||||
return
|
||||
|
||||
exempt_roles = [discord.Object(i) for i in ignored_roles[:MAX_IGNORED]]
|
||||
exempt_channels = [discord.Object(i) for i in ignored_channels[:MAX_IGNORED]]
|
||||
try:
|
||||
if existing:
|
||||
await existing.edit(
|
||||
exempt_roles=exempt_roles,
|
||||
exempt_channels=exempt_channels,
|
||||
enabled=True,
|
||||
reason="Automod NSFW updated via dashboard",
|
||||
)
|
||||
else:
|
||||
await guild.create_automod_rule(
|
||||
name="Anti NSFW Links",
|
||||
event_type=discord.AutoModRuleEventType.message_send,
|
||||
trigger=discord.AutoModTrigger(
|
||||
type=discord.AutoModRuleTriggerType.keyword,
|
||||
keyword_filter=NSFW_KEYWORDS,
|
||||
),
|
||||
actions=[
|
||||
discord.AutoModRuleAction(type=discord.AutoModRuleActionType.block_message),
|
||||
],
|
||||
enabled=True,
|
||||
exempt_roles=exempt_roles,
|
||||
exempt_channels=exempt_channels,
|
||||
reason="Automod NSFW enabled via dashboard",
|
||||
)
|
||||
except (discord.Forbidden, discord.HTTPException) as e:
|
||||
print(f"Automod NSFW sync error for guild {guild_id}: {e}")
|
||||
|
||||
|
||||
@router.patch("/{guild_id}/automod", summary="Update AutoMod config", description="Partially updates the AutoMod configuration components.")
|
||||
async def patch_guild_automod(guild_id: int, data: AutomodUpdate):
|
||||
async def patch_guild_automod(
|
||||
guild_id: int,
|
||||
data: AutomodUpdate,
|
||||
bot: "Axiom" = Depends(get_bot),
|
||||
):
|
||||
"""
|
||||
Updates parts of the AutoMod configuration for a specific guild.
|
||||
"""
|
||||
db = await db_manager.get_connection(db_path('automod.db'))
|
||||
await ensure_thresholds_table(db)
|
||||
|
||||
if data.enabled is not None:
|
||||
await db.execute(
|
||||
"INSERT OR REPLACE INTO automod (guild_id, enabled) VALUES (?, ?)",
|
||||
(guild_id, 1 if data.enabled else 0)
|
||||
(guild_id, 1 if data.enabled else 0),
|
||||
)
|
||||
|
||||
if data.punishments is not None:
|
||||
for event, punishment in data.punishments.items():
|
||||
await db.execute(
|
||||
"INSERT OR REPLACE INTO automod_punishments (guild_id, event, punishment) VALUES (?, ?, ?)",
|
||||
(guild_id, event, punishment)
|
||||
)
|
||||
# Build canonical enabled events from events{} and/or legacy punishments{}
|
||||
events_payload = data.events
|
||||
if events_payload is None and data.punishments is not None:
|
||||
events_payload = {}
|
||||
for raw_key, raw_pun in data.punishments.items():
|
||||
eid = normalize_event_id(raw_key)
|
||||
if not eid:
|
||||
continue
|
||||
if eid == "anti_nsfw_link":
|
||||
events_payload[eid] = AutomodEventConfig(
|
||||
enabled=True, punishment="block_message", readonly_punishment=True
|
||||
)
|
||||
else:
|
||||
pun = normalize_punishment(raw_pun) or "Mute"
|
||||
events_payload[eid] = AutomodEventConfig(enabled=True, punishment=pun)
|
||||
|
||||
if data.ignored_roles is not None:
|
||||
await db.execute("DELETE FROM automod_ignored WHERE guild_id = ? AND type = 'role'", (guild_id,))
|
||||
for role_id in data.ignored_roles:
|
||||
nsfw_enabled: bool | None = None
|
||||
if events_payload is not None:
|
||||
await db.execute(
|
||||
"DELETE FROM automod_punishments WHERE guild_id = ?", (guild_id,)
|
||||
)
|
||||
for raw_id, cfg in events_payload.items():
|
||||
eid = normalize_event_id(raw_id)
|
||||
if not eid or not cfg.enabled:
|
||||
if eid == "anti_nsfw_link":
|
||||
nsfw_enabled = False
|
||||
continue
|
||||
db_name = EVENT_ID_TO_DB[eid]
|
||||
if eid == "anti_nsfw_link":
|
||||
await db.execute(
|
||||
"INSERT INTO automod_punishments (guild_id, event, punishment) VALUES (?, ?, ?)",
|
||||
(guild_id, db_name, "Block message"),
|
||||
)
|
||||
nsfw_enabled = True
|
||||
else:
|
||||
pun = normalize_punishment(cfg.punishment) or "Mute"
|
||||
await db.execute(
|
||||
"INSERT INTO automod_punishments (guild_id, event, punishment) VALUES (?, ?, ?)",
|
||||
(guild_id, db_name, pun),
|
||||
)
|
||||
# Full-replace: omitted/disabled NSFW must drop the Discord rule
|
||||
if nsfw_enabled is None:
|
||||
nsfw_enabled = False
|
||||
|
||||
if data.thresholds is not None:
|
||||
await set_thresholds(db, guild_id, data.thresholds)
|
||||
|
||||
ignored_roles = data.ignored_roles
|
||||
ignored_channels = data.ignored_channels
|
||||
|
||||
if ignored_roles is not None:
|
||||
ignored_roles = list(dict.fromkeys(ignored_roles))[:MAX_IGNORED]
|
||||
await db.execute(
|
||||
"DELETE FROM automod_ignored WHERE guild_id = ? AND type = 'role'",
|
||||
(guild_id,),
|
||||
)
|
||||
for role_id in ignored_roles:
|
||||
await db.execute(
|
||||
"INSERT OR REPLACE INTO automod_ignored (guild_id, type, id) VALUES (?, 'role', ?)",
|
||||
(guild_id, role_id)
|
||||
(guild_id, role_id),
|
||||
)
|
||||
|
||||
if data.ignored_channels is not None:
|
||||
await db.execute("DELETE FROM automod_ignored WHERE guild_id = ? AND type = 'channel'", (guild_id,))
|
||||
for channel_id in data.ignored_channels:
|
||||
if ignored_channels is not None:
|
||||
ignored_channels = list(dict.fromkeys(ignored_channels))[:MAX_IGNORED]
|
||||
await db.execute(
|
||||
"DELETE FROM automod_ignored WHERE guild_id = ? AND type = 'channel'",
|
||||
(guild_id,),
|
||||
)
|
||||
for channel_id in ignored_channels:
|
||||
await db.execute(
|
||||
"INSERT OR REPLACE INTO automod_ignored (guild_id, type, id) VALUES (?, 'channel', ?)",
|
||||
(guild_id, channel_id)
|
||||
(guild_id, channel_id),
|
||||
)
|
||||
|
||||
if data.logging_channel is not None:
|
||||
if data.clear_logging:
|
||||
await db.execute(
|
||||
"INSERT OR REPLACE INTO automod_logging (guild_id, log_channel) VALUES (?, ?)",
|
||||
(guild_id, data.logging_channel)
|
||||
"DELETE FROM automod_logging WHERE guild_id = ?", (guild_id,)
|
||||
)
|
||||
elif data.logging_channel is not None:
|
||||
if data.logging_channel == 0:
|
||||
await db.execute(
|
||||
"DELETE FROM automod_logging WHERE guild_id = ?", (guild_id,)
|
||||
)
|
||||
else:
|
||||
await db.execute(
|
||||
"INSERT OR REPLACE INTO automod_logging (guild_id, log_channel) VALUES (?, ?)",
|
||||
(guild_id, data.logging_channel),
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
|
||||
# Resolve current ignore lists for NSFW sync if not in this payload
|
||||
if ignored_roles is None or ignored_channels is None:
|
||||
cursor = await db.execute(
|
||||
"SELECT type, id FROM automod_ignored WHERE guild_id = ?", (guild_id,)
|
||||
)
|
||||
items = await cursor.fetchall()
|
||||
if ignored_roles is None:
|
||||
ignored_roles = [r[1] for r in items if r[0] == "role"]
|
||||
if ignored_channels is None:
|
||||
ignored_channels = [r[1] for r in items if r[0] == "channel"]
|
||||
|
||||
if nsfw_enabled is None and (data.ignored_roles is not None or data.ignored_channels is not None):
|
||||
# Keep Discord rule exempts in sync when ignores change
|
||||
cursor = await db.execute(
|
||||
"SELECT 1 FROM automod_punishments WHERE guild_id = ? AND event = ?",
|
||||
(guild_id, EVENT_ID_TO_DB["anti_nsfw_link"]),
|
||||
)
|
||||
nsfw_enabled = bool(await cursor.fetchone())
|
||||
|
||||
if nsfw_enabled is not None:
|
||||
await _sync_nsfw_rule(
|
||||
bot,
|
||||
guild_id,
|
||||
enabled=nsfw_enabled,
|
||||
ignored_roles=ignored_roles or [],
|
||||
ignored_channels=ignored_channels or [],
|
||||
)
|
||||
|
||||
return {"status": "success", "guild_id": guild_id}
|
||||
|
||||
@router.get("/{guild_id}/tickets", response_model=TicketConfig, summary="Get Ticket config", description="Retrieves the support ticket system setup, categories, and staff roles.")
|
||||
|
||||
Reference in New Issue
Block a user