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:
2026-01-16 19:27:48 +01:00
parent ffe42b6d51
commit 4e16777f25
45 changed files with 5802 additions and 1 deletions

View File

@@ -0,0 +1,267 @@
"""Automod cog for automatic content moderation."""
import logging
from datetime import datetime, timedelta, timezone
import discord
from discord.ext import commands
from guardden.bot import GuardDen
from guardden.services.automod import AutomodResult, AutomodService
logger = logging.getLogger(__name__)
class Automod(commands.Cog):
"""Automatic content moderation."""
def __init__(self, bot: GuardDen) -> None:
self.bot = bot
self.automod = AutomodService()
async def _handle_violation(
self,
message: discord.Message,
result: AutomodResult,
) -> None:
"""Handle an automod violation."""
# Delete the message
if result.should_delete:
try:
await message.delete()
except discord.Forbidden:
logger.warning(f"Cannot delete message in {message.guild}: missing permissions")
except discord.NotFound:
pass # Already deleted
# Apply timeout
if result.should_timeout and result.timeout_duration > 0:
try:
await message.author.timeout(
timedelta(seconds=result.timeout_duration),
reason=f"Automod: {result.reason}",
)
except discord.Forbidden:
logger.warning(f"Cannot timeout {message.author}: missing permissions")
# Log the action
await self._log_automod_action(message, result)
# Notify the user via DM
try:
embed = discord.Embed(
title=f"Message Removed in {message.guild.name}",
description=result.reason,
color=discord.Color.orange(),
timestamp=datetime.now(timezone.utc),
)
if result.should_timeout:
embed.add_field(
name="Timeout",
value=f"You have been timed out for {result.timeout_duration} seconds.",
)
await message.author.send(embed=embed)
except discord.Forbidden:
pass # User has DMs disabled
async def _log_automod_action(
self,
message: discord.Message,
result: AutomodResult,
) -> None:
"""Log an automod action to the mod log channel."""
config = await self.bot.guild_config.get_config(message.guild.id)
if not config or not config.mod_log_channel_id:
return
channel = message.guild.get_channel(config.mod_log_channel_id)
if not channel or not isinstance(channel, discord.TextChannel):
return
embed = discord.Embed(
title="Automod Action",
color=discord.Color.orange(),
timestamp=datetime.now(timezone.utc),
)
embed.set_author(
name=str(message.author),
icon_url=message.author.display_avatar.url,
)
embed.add_field(name="Filter", value=result.matched_filter, inline=True)
embed.add_field(name="Channel", value=message.channel.mention, inline=True)
embed.add_field(name="Reason", value=result.reason, inline=False)
if message.content:
content = (
message.content[:500] + "..." if len(message.content) > 500 else message.content
)
embed.add_field(name="Message Content", value=f"```{content}```", inline=False)
actions = []
if result.should_delete:
actions.append("Message deleted")
if result.should_warn:
actions.append("User warned")
if result.should_strike:
actions.append("Strike added")
if result.should_timeout:
actions.append(f"Timeout ({result.timeout_duration}s)")
embed.add_field(name="Actions Taken", value=", ".join(actions) or "None", inline=False)
embed.set_footer(text=f"User ID: {message.author.id}")
await channel.send(embed=embed)
@commands.Cog.listener()
async def on_message(self, message: discord.Message) -> None:
"""Check all messages for automod violations."""
# Ignore DMs, bots, and empty messages
if not message.guild or message.author.bot or not message.content:
return
# Ignore users with manage_messages permission
if isinstance(message.author, discord.Member):
if message.author.guild_permissions.manage_messages:
return
# Get guild config
config = await self.bot.guild_config.get_config(message.guild.id)
if not config or not config.automod_enabled:
return
result: AutomodResult | None = None
# Check banned words
banned_words = await self.bot.guild_config.get_banned_words(message.guild.id)
if banned_words:
result = self.automod.check_banned_words(message.content, banned_words)
# Check scam links (if link filter enabled)
if not result and config.link_filter_enabled:
result = self.automod.check_scam_links(message.content)
# Check spam
if not result and config.anti_spam_enabled:
result = self.automod.check_spam(message, anti_spam_enabled=True)
# Check invite links (if link filter enabled)
if not result and config.link_filter_enabled:
result = self.automod.check_invite_links(message.content, allow_invites=False)
# Handle violation if found
if result:
logger.info(
f"Automod triggered in {message.guild.name}: "
f"{result.matched_filter} by {message.author}"
)
await self._handle_violation(message, result)
@commands.Cog.listener()
async def on_message_edit(self, before: discord.Message, after: discord.Message) -> None:
"""Check edited messages for automod violations."""
# Only check if content changed
if before.content == after.content:
return
# Reuse on_message logic
await self.on_message(after)
@commands.group(name="automod", invoke_without_command=True)
@commands.has_permissions(administrator=True)
@commands.guild_only()
async def automod_cmd(self, ctx: commands.Context) -> None:
"""View automod status and configuration."""
config = await self.bot.guild_config.get_config(ctx.guild.id)
embed = discord.Embed(
title="Automod Configuration",
color=discord.Color.blue(),
)
embed.add_field(
name="Automod Enabled",
value="✅ Yes" if config and config.automod_enabled else "❌ No",
inline=True,
)
embed.add_field(
name="Anti-Spam",
value="✅ Yes" if config and config.anti_spam_enabled else "❌ No",
inline=True,
)
embed.add_field(
name="Link Filter",
value="✅ Yes" if config and config.link_filter_enabled else "❌ No",
inline=True,
)
# Show thresholds
embed.add_field(
name="Rate Limit",
value=f"{self.automod.message_rate_limit} msgs / {self.automod.message_rate_window}s",
inline=True,
)
embed.add_field(
name="Duplicate Threshold",
value=f"{self.automod.duplicate_threshold} same messages",
inline=True,
)
embed.add_field(
name="Mention Limit",
value=f"{self.automod.mention_limit} per message",
inline=True,
)
banned_words = await self.bot.guild_config.get_banned_words(ctx.guild.id)
embed.add_field(
name="Banned Words",
value=f"{len(banned_words)} configured",
inline=True,
)
await ctx.send(embed=embed)
@automod_cmd.command(name="test")
@commands.has_permissions(administrator=True)
@commands.guild_only()
async def automod_test(self, ctx: commands.Context, *, text: str) -> None:
"""Test a message against automod filters (does not take action)."""
config = await self.bot.guild_config.get_config(ctx.guild.id)
results = []
# Check banned words
banned_words = await self.bot.guild_config.get_banned_words(ctx.guild.id)
result = self.automod.check_banned_words(text, banned_words)
if result:
results.append(f"**Banned Words**: {result.reason}")
# Check scam links
result = self.automod.check_scam_links(text)
if result:
results.append(f"**Scam Detection**: {result.reason}")
# Check invite links
result = self.automod.check_invite_links(text, allow_invites=False)
if result:
results.append(f"**Invite Links**: {result.reason}")
# Check caps
result = self.automod.check_all_caps(text)
if result:
results.append(f"**Excessive Caps**: {result.reason}")
embed = discord.Embed(
title="Automod Test Results",
color=discord.Color.red() if results else discord.Color.green(),
)
if results:
embed.description = "\n".join(results)
else:
embed.description = "✅ No violations detected"
await ctx.send(embed=embed)
async def setup(bot: GuardDen) -> None:
"""Load the Automod cog."""
await bot.add_cog(Automod(bot))