diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index a8c09df..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,36 +0,0 @@ -# Repository Guidelines - -## Project Structure & Module Organization -- `src/guardden/` is the main package: `bot.py` (bot lifecycle), `cogs/` (Discord commands/events), `services/` (business logic), `models/` (SQLAlchemy models), `config.py` (settings), and `utils/` (shared helpers). -- `tests/` holds pytest suites (`test_*.py`) for services and utilities. -- `migrations/` and `alembic.ini` define database migrations. -- `docker-compose.yml` and `Dockerfile` support containerized development/deployments. -- `.env.example` provides the configuration template. - -## Build, Test, and Development Commands -- `pip install -e ".[dev,ai]"` installs dev tooling plus optional AI providers. -- `python -m guardden` runs the bot locally. -- `pytest` runs the full test suite; `pytest tests/test_verification.py::TestVerificationService::test_verify_correct` runs a single test. -- `ruff check src tests` lints; `ruff format src tests` formats. -- `mypy src` runs strict type checks. -- `docker compose up -d` starts the full stack; `docker compose up db -d` starts only Postgres. - -## Coding Style & Naming Conventions -- Python 3.11 with 4-space indentation; keep lines within 100 chars (Ruff config). -- Prefer type hints and clean async patterns; mypy runs in strict mode. -- Naming: `snake_case` for modules/functions, `CamelCase` for classes, `UPPER_SNAKE` for constants. -- New cogs live in `src/guardden/cogs/` and should be wired in `_load_cogs()` in `src/guardden/bot.py`. - -## Testing Guidelines -- Tests use pytest + pytest-asyncio (`asyncio_mode=auto`). -- Follow `test_*.py` file names and `test_*` function names; group related cases in `Test*` classes. -- Add or update tests for new services, automod rules, or AI provider behavior. - -## Commit & Pull Request Guidelines -- Commit messages are short, imperative, and capitalized (e.g., `Fix: initialize guild config...`, `Add Discord bot setup...`). -- PRs should include a concise summary, tests run, and any config or migration notes; link related issues when available. - -## Security & Configuration Tips -- Store secrets in `.env` (never commit); configuration keys are prefixed with `GUARDDEN_`. -- PostgreSQL is required; default URL is `postgresql://guardden:guardden@localhost:5432/guardden`. -- AI features require `GUARDDEN_AI_PROVIDER` plus the matching API key. diff --git a/CLAUDE.md b/CLAUDE.md index b77241a..8d484b8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,6 +55,8 @@ docker compose up -d - Spam tracking uses per-guild, per-user trackers with automatic cleanup - Scam detection uses compiled regex patterns in `SCAM_PATTERNS` list - Results return `AutomodResult` dataclass with actions to take +- **Whitelist**: Users in `GuildSettings.whitelisted_user_ids` bypass ALL automod checks +- Users with "Manage Messages" permission also bypass automod ## AI Moderation System @@ -67,6 +69,7 @@ docker compose up -d - Sensitivity setting (0-100) adjusts thresholds per guild - **NSFW-Only Filtering** (default: `True`): When enabled, only sexual content is filtered; violence, harassment, etc. are allowed - Filtering controlled by `nsfw_only_filtering` field in `GuildSettings` +- **Whitelist**: Users in `GuildSettings.whitelisted_user_ids` bypass ALL AI moderation checks ## Verification System diff --git a/DEV_GUIDE.md b/DEV_GUIDE.md deleted file mode 100644 index 7a82c0e..0000000 --- a/DEV_GUIDE.md +++ /dev/null @@ -1,311 +0,0 @@ -# GuardDen Development Guide - -This guide provides everything you need to start developing GuardDen locally. - -## ๐Ÿš€ Quick Start - -```bash -# 1. Clone the repository -git clone -cd GuardDen - -# 2. Set up development environment -./scripts/dev.sh setup - -# 3. Configure environment variables -cp .env.example .env -# Edit .env with your Discord bot token and other settings - -# 4. Start development environment -./scripts/dev.sh up - -# 5. Run tests to verify setup -./scripts/dev.sh test -``` - -## ๐Ÿ“‹ Development Commands - -The `./scripts/dev.sh` script provides comprehensive development automation: - -### Environment Management -```bash -./scripts/dev.sh setup # Set up development environment -./scripts/dev.sh up # Start development containers -./scripts/dev.sh down # Stop development containers -./scripts/dev.sh logs [service] # Show logs (optional service filter) -./scripts/dev.sh clean # Clean up development artifacts -``` - -### Code Quality -```bash -./scripts/dev.sh test # Run tests with coverage -./scripts/dev.sh lint # Run code quality checks -./scripts/dev.sh format # Format code with ruff -./scripts/dev.sh security # Run security scans -``` - -### Database Management -```bash -./scripts/dev.sh db migrate # Run database migrations -./scripts/dev.sh db revision "description" # Create new migration -./scripts/dev.sh db reset # Reset database (destructive) -``` - -### Health & Monitoring -```bash -./scripts/dev.sh health check # Run health checks -./scripts/dev.sh health json # Health checks with JSON output -``` - -### Docker Operations -```bash -./scripts/dev.sh build # Build Docker images (bot + dashboard) -``` - -## ๐Ÿณ Development Services - -When you run `./scripts/dev.sh up`, the following services are available: - -| Service | URL | Purpose | -|---------|-----|---------| -| GuardDen Bot | - | Discord bot with hot reloading | -| Dashboard | http://localhost:8080 | Web interface | -| PostgreSQL | localhost:5432 | Database | -| Redis | localhost:6379 | Caching & sessions | -| PgAdmin | http://localhost:5050 | Database administration | -| Redis Commander | http://localhost:8081 | Redis administration | -| MailHog | http://localhost:8025 | Email testing | - -## ๐Ÿ–ฅ๏ธ Dashboard Frontend - -The dashboard backend serves static assets from `dashboard/frontend/dist`. - -Build the static assets: -```bash -cd dashboard/frontend -npm install -npm run build -``` - -Or run the Vite dev server for UI iteration: -```bash -cd dashboard/frontend -npm install -npm run dev -``` - -The Vite dev server runs at `http://localhost:5173`. - -The Vite dev server proxies `/api` and `/auth` to `http://localhost:8000`. If you're -using the Docker dev stack (dashboard at `http://localhost:8080`), either run the -dashboard backend locally on port 8000 or update the proxy target. - -## ๐Ÿงช Testing - -### Running Tests -```bash -# Run all tests with coverage -./scripts/dev.sh test - -# Run specific test files -pytest tests/test_config.py - -# Run tests with verbose output -pytest -v - -# Run tests in parallel (faster) -pytest -n auto -``` - -### Test Structure -- `tests/conftest.py` - Test fixtures and configuration -- `tests/test_*.py` - Test modules -- Test coverage reports in `htmlcov/` - -### Writing Tests -- Use pytest with async support (`pytest-asyncio`) -- Comprehensive fixtures available for database, Discord mocks, etc. -- Follow naming convention: `test_*` functions in `Test*` classes - -## ๐Ÿ”ง Code Quality - -### Pre-commit Hooks -Pre-commit hooks are automatically installed during setup: -- **Ruff**: Code formatting and linting -- **MyPy**: Type checking -- **Tests**: Run tests on relevant changes - -### Manual Quality Checks -```bash -# Run all quality checks -./scripts/dev.sh lint - -# Format code -./scripts/dev.sh format - -# Type checking only -mypy src - -# Security scanning -./scripts/dev.sh security -``` - -### Code Style -- **Line Length**: 100 characters (configured in pyproject.toml) -- **Imports**: Sorted with ruff -- **Type Hints**: Required for all public functions -- **Docstrings**: Google style for modules and classes - -## ๐Ÿ“Š Monitoring & Debugging - -### Structured Logging -```python -from guardden.utils.logging import get_logger, bind_context - -logger = get_logger(__name__) - -# Log with context -bind_context(user_id=123, guild_id=456) -logger.info("User performed action", action="kick", target="user#1234") -``` - -### Metrics Collection -```python -from guardden.utils.metrics import get_metrics - -metrics = get_metrics() -metrics.record_command("ban", guild_id=123, status="success", duration=0.5) -``` - -### Health Checks -```bash -# Check application health -./scripts/dev.sh health check - -# Get detailed JSON health report -./scripts/dev.sh health json -``` - -## ๐Ÿ—„๏ธ Database Development - -### Migrations -```bash -# Create new migration -./scripts/dev.sh db revision "add new table" - -# Run migrations -./scripts/dev.sh db migrate - -# Rollback one migration -./scripts/dev.sh db downgrade -``` - -### Database Access -- **PgAdmin**: http://localhost:5050 (admin@guardden.dev / admin) -- **Direct connection**: localhost:5432 (guardden / guardden_dev) -- **Test database**: In-memory SQLite for tests - -## ๐Ÿ› Debugging - -### Debug Mode -Development containers include debugging support: -- **Bot**: Debug port 5678 -- **Dashboard**: Debug port 5679 - -### VS Code Configuration -Add to `.vscode/launch.json`: -```json -{ - "name": "Attach to Bot", - "type": "python", - "request": "attach", - "host": "localhost", - "port": 5678 -} -``` - -### Log Analysis -```bash -# Follow all logs -./scripts/dev.sh logs - -# Follow specific service logs -./scripts/dev.sh logs bot -./scripts/dev.sh logs dashboard -``` - -## ๐Ÿ” Security - -### Environment Variables -- **Required**: `GUARDDEN_DISCORD_TOKEN` -- **Database**: `GUARDDEN_DATABASE_URL` (auto-configured for development) -- **AI**: `GUARDDEN_ANTHROPIC_API_KEY` or `GUARDDEN_OPENAI_API_KEY` - -### Security Best Practices -- Never commit secrets to version control -- Use `.env` for local development secrets -- Run security scans regularly: `./scripts/dev.sh security` -- Keep dependencies updated - -## ๐Ÿš€ Deployment - -### Building for Production -```bash -# Build optimized image -docker build -t guardden:latest . - -# Build with AI dependencies -docker build --build-arg INSTALL_AI=true -t guardden:ai . -``` - -### CI/CD Pipeline -- **GitHub Actions**: Automated testing, security scanning, and deployment -- **Quality Gates**: 75%+ test coverage, type checking, security scans -- **Automated Deployments**: Staging (develop branch) and production (releases) - -## ๐Ÿ†˜ Troubleshooting - -### Common Issues - -**Port conflicts**: -```bash -# Check what's using a port -lsof -i :5432 - -# Use different ports in .env -POSTGRES_PORT=5433 -REDIS_PORT=6380 -``` - -**Permission errors**: -```bash -# Fix Docker permissions -sudo chown -R $USER:$USER data logs -``` - -**Database connection errors**: -```bash -# Reset development environment -./scripts/dev.sh down -./scripts/dev.sh clean -./scripts/dev.sh up -``` - -**Test failures**: -```bash -# Run tests with more verbose output -pytest -vvs - -# Run specific failing test -pytest tests/test_config.py::TestSettingsValidation::test_discord_token_validation_valid -vvs -``` - -### Getting Help -1. Check logs: `./scripts/dev.sh logs` -2. Run health check: `./scripts/dev.sh health check` -3. Verify environment: `./scripts/dev.sh setup` -4. Check GitHub Issues for known problems - ---- - -Happy coding! ๐ŸŽ‰ diff --git a/README.md b/README.md index be8e5d2..76e5b94 100644 --- a/README.md +++ b/README.md @@ -401,6 +401,20 @@ Edit config/wordlists/domain-allowlists.yml Edit config/wordlists/banned-words.yml ``` +### Whitelist Management (Admin only) + +| Command | Description | +|---------|-------------| +| `!whitelist` | View all whitelisted users | +| `!whitelist add @user` | Add a user to the whitelist (bypasses all moderation) | +| `!whitelist remove @user` | Remove a user from the whitelist | +| `!whitelist clear` | Clear the entire whitelist | + +**What is the whitelist?** +- Whitelisted users bypass **ALL** moderation checks (automod and AI moderation) +- Useful for trusted members, bots, or staff who need to post content that might trigger filters +- Users with "Manage Messages" permission are already exempt from moderation + ### Diagnostics (Admin only) | Command | Description | diff --git a/migrations/versions/20260125_add_whitelist.py b/migrations/versions/20260125_add_whitelist.py new file mode 100644 index 0000000..42779ff --- /dev/null +++ b/migrations/versions/20260125_add_whitelist.py @@ -0,0 +1,34 @@ +"""Add whitelisted_user_ids column to guild_settings table. + +Revision ID: 20260125_add_whitelist +Revises: 20260125_add_in_channel_warnings +Create Date: 2026-01-25 01:00:00.000000 +""" + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects.postgresql import JSONB + +# revision identifiers, used by Alembic. +revision = "20260125_add_whitelist" +down_revision = "20260125_add_in_channel_warnings" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """Add whitelisted_user_ids column to guild_settings table.""" + op.add_column( + "guild_settings", + sa.Column( + "whitelisted_user_ids", + JSONB().with_variant(sa.JSON(), "sqlite"), + nullable=False, + server_default="[]", + ), + ) + + +def downgrade() -> None: + """Remove whitelisted_user_ids column from guild_settings table.""" + op.drop_column("guild_settings", "whitelisted_user_ids") diff --git a/src/guardden/cogs/admin.py b/src/guardden/cogs/admin.py index 5291b23..885fa4b 100644 --- a/src/guardden/cogs/admin.py +++ b/src/guardden/cogs/admin.py @@ -311,6 +311,126 @@ class Admin(commands.Cog): await ctx.send(embed=embed) + @commands.group(name="whitelist", invoke_without_command=True) + @commands.guild_only() + async def whitelist_cmd(self, ctx: commands.Context) -> None: + """Manage the moderation whitelist.""" + config = await self.bot.guild_config.get_config(ctx.guild.id) + whitelisted_ids = config.whitelisted_user_ids if config else [] + + if not whitelisted_ids: + await ctx.send("No users are whitelisted.") + return + + embed = discord.Embed( + title="Whitelisted Users", + description="These users bypass all moderation checks:", + color=discord.Color.blue(), + ) + + users_text = [] + for user_id in whitelisted_ids[:25]: # Limit to 25 to avoid embed limits + user = ctx.guild.get_member(user_id) + if user: + users_text.append(f"โ€ข {user.mention} (`{user_id}`)") + else: + users_text.append(f"โ€ข Unknown User (`{user_id}`)") + + embed.add_field( + name=f"Total: {len(whitelisted_ids)} users", + value="\n".join(users_text) if users_text else "None", + inline=False, + ) + + if len(whitelisted_ids) > 25: + embed.set_footer(text=f"Showing 25 of {len(whitelisted_ids)} users") + + await ctx.send(embed=embed) + + @whitelist_cmd.command(name="add") + @commands.guild_only() + async def whitelist_add(self, ctx: commands.Context, user: discord.Member) -> None: + """Add a user to the whitelist. + + Whitelisted users bypass ALL moderation checks (automod and AI moderation). + """ + config = await self.bot.guild_config.get_config(ctx.guild.id) + whitelisted_ids = list(config.whitelisted_user_ids) if config else [] + + if user.id in whitelisted_ids: + await ctx.send(f"{user.mention} is already whitelisted.") + return + + whitelisted_ids.append(user.id) + await self.bot.guild_config.update_settings( + ctx.guild.id, whitelisted_user_ids=whitelisted_ids + ) + + embed = discord.Embed( + title="โœ… User Whitelisted", + description=f"{user.mention} has been added to the whitelist.", + color=discord.Color.green(), + ) + embed.add_field( + name="What this means", + value="This user will bypass all automod and AI moderation checks.", + inline=False, + ) + await ctx.send(embed=embed) + + @whitelist_cmd.command(name="remove") + @commands.guild_only() + async def whitelist_remove(self, ctx: commands.Context, user: discord.Member) -> None: + """Remove a user from the whitelist.""" + config = await self.bot.guild_config.get_config(ctx.guild.id) + whitelisted_ids = list(config.whitelisted_user_ids) if config else [] + + if user.id not in whitelisted_ids: + await ctx.send(f"{user.mention} is not whitelisted.") + return + + whitelisted_ids.remove(user.id) + await self.bot.guild_config.update_settings( + ctx.guild.id, whitelisted_user_ids=whitelisted_ids + ) + + embed = discord.Embed( + title="๐Ÿšซ User Removed from Whitelist", + description=f"{user.mention} has been removed from the whitelist.", + color=discord.Color.orange(), + ) + embed.add_field( + name="What this means", + value="This user will now be subject to normal moderation checks.", + inline=False, + ) + await ctx.send(embed=embed) + + @whitelist_cmd.command(name="clear") + @commands.guild_only() + async def whitelist_clear(self, ctx: commands.Context) -> None: + """Clear the entire whitelist.""" + config = await self.bot.guild_config.get_config(ctx.guild.id) + count = len(config.whitelisted_user_ids) if config else 0 + + if count == 0: + await ctx.send("The whitelist is already empty.") + return + + await self.bot.guild_config.update_settings(ctx.guild.id, whitelisted_user_ids=[]) + + embed = discord.Embed( + title="๐Ÿงน Whitelist Cleared", + description=f"Removed {count} user(s) from the whitelist.", + color=discord.Color.red(), + ) + embed.add_field( + name="What this means", + value="All users will now be subject to normal moderation checks.", + inline=False, + ) + await ctx.send(embed=embed) + @commands.command(name="sync") @commands.is_owner() async def sync_commands(self, ctx: commands.Context) -> None: diff --git a/src/guardden/cogs/ai_moderation.py b/src/guardden/cogs/ai_moderation.py index c0e2a84..47c3941 100644 --- a/src/guardden/cogs/ai_moderation.py +++ b/src/guardden/cogs/ai_moderation.py @@ -287,6 +287,11 @@ class AIModeration(commands.Cog): logger.debug(f"AI moderation disabled for guild {message.guild.id}") return + # Check if user is whitelisted + if message.author.id in config.whitelisted_user_ids: + logger.debug(f"Skipping whitelisted user {message.author}") + return + # Skip users with manage_messages permission (disabled for testing) # if isinstance(message.author, discord.Member): # if message.author.guild_permissions.manage_messages: diff --git a/src/guardden/cogs/automod.py b/src/guardden/cogs/automod.py index e9b1b35..35898da 100644 --- a/src/guardden/cogs/automod.py +++ b/src/guardden/cogs/automod.py @@ -283,6 +283,10 @@ class Automod(commands.Cog): if not config or not config.automod_enabled: return + # Check if user is whitelisted + if message.author.id in config.whitelisted_user_ids: + return + result: AutomodResult | None = None # Check banned words diff --git a/src/guardden/models/guild.py b/src/guardden/models/guild.py index 60057f9..ec42bb3 100644 --- a/src/guardden/models/guild.py +++ b/src/guardden/models/guild.py @@ -102,6 +102,11 @@ class GuildSettings(Base, TimestampMixin): # Notification settings send_in_channel_warnings: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + # Whitelist settings + whitelisted_user_ids: Mapped[list[int]] = mapped_column( + JSONB().with_variant(JSON(), "sqlite"), default=list, nullable=False + ) + # Verification settings verification_enabled: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) verification_type: Mapped[str] = mapped_column(