Implement GuardDen Discord moderation bot
Features: - Core moderation: warn, kick, ban, timeout, strike system - Automod: banned words filter, scam detection, anti-spam, link filtering - AI moderation: Claude/OpenAI integration, NSFW detection, phishing analysis - Verification system: button, captcha, math, emoji challenges - Rate limiting system with configurable scopes - Event logging: joins, leaves, message edits/deletes, voice activity - Per-guild configuration with caching - Docker deployment support Bug fixes applied: - Fixed await on session.delete() in guild_config.py - Fixed memory leak in AI moderation message tracking (use deque) - Added error handling to bot shutdown - Added error handling to timeout command - Removed unused Literal import - Added prefix validation - Added image analysis limit (3 per message) - Fixed test mock for SQLAlchemy model
This commit is contained in:
255
src/guardden/cogs/admin.py
Normal file
255
src/guardden/cogs/admin.py
Normal file
@@ -0,0 +1,255 @@
|
||||
"""Admin commands for bot configuration."""
|
||||
|
||||
import logging
|
||||
from typing import Literal
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from guardden.bot import GuardDen
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Admin(commands.Cog):
|
||||
"""Administrative commands for bot configuration."""
|
||||
|
||||
def __init__(self, bot: GuardDen) -> None:
|
||||
self.bot = bot
|
||||
|
||||
async def cog_check(self, ctx: commands.Context) -> bool:
|
||||
"""Ensure only administrators can use these commands."""
|
||||
if not ctx.guild:
|
||||
return False
|
||||
return ctx.author.guild_permissions.administrator
|
||||
|
||||
@commands.group(name="config", invoke_without_command=True)
|
||||
@commands.guild_only()
|
||||
async def config(self, ctx: commands.Context) -> None:
|
||||
"""View or modify bot configuration."""
|
||||
config = await self.bot.guild_config.get_config(ctx.guild.id)
|
||||
|
||||
if not config:
|
||||
await ctx.send("No configuration found. Run a config command to initialize.")
|
||||
return
|
||||
|
||||
embed = discord.Embed(
|
||||
title=f"Configuration for {ctx.guild.name}",
|
||||
color=discord.Color.blue(),
|
||||
)
|
||||
|
||||
# General settings
|
||||
embed.add_field(name="Prefix", value=f"`{config.prefix}`", inline=True)
|
||||
embed.add_field(name="Locale", value=config.locale, inline=True)
|
||||
embed.add_field(name="\u200b", value="\u200b", inline=True)
|
||||
|
||||
# Channels
|
||||
log_ch = ctx.guild.get_channel(config.log_channel_id) if config.log_channel_id else None
|
||||
mod_log_ch = (
|
||||
ctx.guild.get_channel(config.mod_log_channel_id) if config.mod_log_channel_id else None
|
||||
)
|
||||
welcome_ch = (
|
||||
ctx.guild.get_channel(config.welcome_channel_id) if config.welcome_channel_id else None
|
||||
)
|
||||
|
||||
embed.add_field(
|
||||
name="Log Channel", value=log_ch.mention if log_ch else "Not set", inline=True
|
||||
)
|
||||
embed.add_field(
|
||||
name="Mod Log Channel",
|
||||
value=mod_log_ch.mention if mod_log_ch else "Not set",
|
||||
inline=True,
|
||||
)
|
||||
embed.add_field(
|
||||
name="Welcome Channel",
|
||||
value=welcome_ch.mention if welcome_ch else "Not set",
|
||||
inline=True,
|
||||
)
|
||||
|
||||
# Features
|
||||
features = []
|
||||
if config.automod_enabled:
|
||||
features.append("AutoMod")
|
||||
if config.anti_spam_enabled:
|
||||
features.append("Anti-Spam")
|
||||
if config.link_filter_enabled:
|
||||
features.append("Link Filter")
|
||||
if config.ai_moderation_enabled:
|
||||
features.append("AI Moderation")
|
||||
if config.verification_enabled:
|
||||
features.append("Verification")
|
||||
|
||||
embed.add_field(
|
||||
name="Enabled Features",
|
||||
value=", ".join(features) if features else "None",
|
||||
inline=False,
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@config.command(name="prefix")
|
||||
@commands.guild_only()
|
||||
async def config_prefix(self, ctx: commands.Context, prefix: str) -> None:
|
||||
"""Set the command prefix for this server."""
|
||||
if not prefix or not prefix.strip():
|
||||
await ctx.send("Prefix cannot be empty or whitespace only.")
|
||||
return
|
||||
|
||||
if len(prefix) > 10:
|
||||
await ctx.send("Prefix must be 10 characters or less.")
|
||||
return
|
||||
|
||||
await self.bot.guild_config.update_settings(ctx.guild.id, prefix=prefix)
|
||||
await ctx.send(f"Command prefix set to `{prefix}`")
|
||||
|
||||
@config.command(name="logchannel")
|
||||
@commands.guild_only()
|
||||
async def config_log_channel(
|
||||
self, ctx: commands.Context, channel: discord.TextChannel | None = None
|
||||
) -> None:
|
||||
"""Set the channel for general event logs."""
|
||||
channel_id = channel.id if channel else None
|
||||
await self.bot.guild_config.update_settings(ctx.guild.id, log_channel_id=channel_id)
|
||||
|
||||
if channel:
|
||||
await ctx.send(f"Log channel set to {channel.mention}")
|
||||
else:
|
||||
await ctx.send("Log channel has been disabled.")
|
||||
|
||||
@config.command(name="modlogchannel")
|
||||
@commands.guild_only()
|
||||
async def config_mod_log_channel(
|
||||
self, ctx: commands.Context, channel: discord.TextChannel | None = None
|
||||
) -> None:
|
||||
"""Set the channel for moderation action logs."""
|
||||
channel_id = channel.id if channel else None
|
||||
await self.bot.guild_config.update_settings(ctx.guild.id, mod_log_channel_id=channel_id)
|
||||
|
||||
if channel:
|
||||
await ctx.send(f"Moderation log channel set to {channel.mention}")
|
||||
else:
|
||||
await ctx.send("Moderation log channel has been disabled.")
|
||||
|
||||
@config.command(name="welcomechannel")
|
||||
@commands.guild_only()
|
||||
async def config_welcome_channel(
|
||||
self, ctx: commands.Context, channel: discord.TextChannel | None = None
|
||||
) -> None:
|
||||
"""Set the welcome channel for new members."""
|
||||
channel_id = channel.id if channel else None
|
||||
await self.bot.guild_config.update_settings(ctx.guild.id, welcome_channel_id=channel_id)
|
||||
|
||||
if channel:
|
||||
await ctx.send(f"Welcome channel set to {channel.mention}")
|
||||
else:
|
||||
await ctx.send("Welcome channel has been disabled.")
|
||||
|
||||
@config.command(name="muterole")
|
||||
@commands.guild_only()
|
||||
async def config_mute_role(
|
||||
self, ctx: commands.Context, role: discord.Role | None = None
|
||||
) -> None:
|
||||
"""Set the role to assign when muting members."""
|
||||
role_id = role.id if role else None
|
||||
await self.bot.guild_config.update_settings(ctx.guild.id, mute_role_id=role_id)
|
||||
|
||||
if role:
|
||||
await ctx.send(f"Mute role set to {role.mention}")
|
||||
else:
|
||||
await ctx.send("Mute role has been cleared.")
|
||||
|
||||
@config.command(name="automod")
|
||||
@commands.guild_only()
|
||||
async def config_automod(self, ctx: commands.Context, enabled: bool) -> None:
|
||||
"""Enable or disable automod features."""
|
||||
await self.bot.guild_config.update_settings(ctx.guild.id, automod_enabled=enabled)
|
||||
status = "enabled" if enabled else "disabled"
|
||||
await ctx.send(f"AutoMod has been {status}.")
|
||||
|
||||
@config.command(name="antispam")
|
||||
@commands.guild_only()
|
||||
async def config_antispam(self, ctx: commands.Context, enabled: bool) -> None:
|
||||
"""Enable or disable anti-spam protection."""
|
||||
await self.bot.guild_config.update_settings(ctx.guild.id, anti_spam_enabled=enabled)
|
||||
status = "enabled" if enabled else "disabled"
|
||||
await ctx.send(f"Anti-spam has been {status}.")
|
||||
|
||||
@config.command(name="linkfilter")
|
||||
@commands.guild_only()
|
||||
async def config_linkfilter(self, ctx: commands.Context, enabled: bool) -> None:
|
||||
"""Enable or disable link filtering."""
|
||||
await self.bot.guild_config.update_settings(ctx.guild.id, link_filter_enabled=enabled)
|
||||
status = "enabled" if enabled else "disabled"
|
||||
await ctx.send(f"Link filter has been {status}.")
|
||||
|
||||
@commands.group(name="bannedwords", aliases=["bw"], invoke_without_command=True)
|
||||
@commands.guild_only()
|
||||
async def banned_words(self, ctx: commands.Context) -> None:
|
||||
"""Manage banned words list."""
|
||||
words = await self.bot.guild_config.get_banned_words(ctx.guild.id)
|
||||
|
||||
if not words:
|
||||
await ctx.send("No banned words configured.")
|
||||
return
|
||||
|
||||
embed = discord.Embed(
|
||||
title="Banned Words",
|
||||
color=discord.Color.red(),
|
||||
)
|
||||
|
||||
for word in words[:25]: # Discord embed limit
|
||||
word_type = "Regex" if word.is_regex else "Text"
|
||||
embed.add_field(
|
||||
name=f"#{word.id}: {word.pattern[:30]}",
|
||||
value=f"Type: {word_type} | Action: {word.action}",
|
||||
inline=True,
|
||||
)
|
||||
|
||||
if len(words) > 25:
|
||||
embed.set_footer(text=f"Showing 25 of {len(words)} banned words")
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@banned_words.command(name="add")
|
||||
@commands.guild_only()
|
||||
async def banned_words_add(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
pattern: str,
|
||||
action: Literal["delete", "warn", "strike"] = "delete",
|
||||
is_regex: bool = False,
|
||||
) -> None:
|
||||
"""Add a banned word or pattern."""
|
||||
word = await self.bot.guild_config.add_banned_word(
|
||||
guild_id=ctx.guild.id,
|
||||
pattern=pattern,
|
||||
added_by=ctx.author.id,
|
||||
is_regex=is_regex,
|
||||
action=action,
|
||||
)
|
||||
|
||||
word_type = "regex pattern" if is_regex else "word"
|
||||
await ctx.send(f"Added banned {word_type}: `{pattern}` (ID: {word.id}, Action: {action})")
|
||||
|
||||
@banned_words.command(name="remove", aliases=["delete"])
|
||||
@commands.guild_only()
|
||||
async def banned_words_remove(self, ctx: commands.Context, word_id: int) -> None:
|
||||
"""Remove a banned word by ID."""
|
||||
success = await self.bot.guild_config.remove_banned_word(ctx.guild.id, word_id)
|
||||
|
||||
if success:
|
||||
await ctx.send(f"Removed banned word #{word_id}")
|
||||
else:
|
||||
await ctx.send(f"Banned word #{word_id} not found.")
|
||||
|
||||
@commands.command(name="sync")
|
||||
@commands.is_owner()
|
||||
async def sync_commands(self, ctx: commands.Context) -> None:
|
||||
"""Sync slash commands (bot owner only)."""
|
||||
await self.bot.tree.sync()
|
||||
await ctx.send("Slash commands synced.")
|
||||
|
||||
|
||||
async def setup(bot: GuardDen) -> None:
|
||||
"""Load the Admin cog."""
|
||||
await bot.add_cog(Admin(bot))
|
||||
Reference in New Issue
Block a user