-
Notifications
You must be signed in to change notification settings - Fork 2
feat(moderation): comprehensive warning system with severity, decay, and expiry #251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
ca4a580
5391fd6
e31417b
6ada6e2
d1dffe0
6705986
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,3 +42,4 @@ web/tsconfig.tsbuildinfo | |
| openclaw-studio/ | ||
| .codex/ | ||
| .turbo/ | ||
| worktrees/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| # Task: Comprehensive Warning System (#250) | ||
|
|
||
| ## Branch: `feat/issue-250-warning-system` | ||
| ## Closes: #250 | ||
|
|
||
| ## Context | ||
| There's already a basic `/warn` command (`src/commands/warn.js`) that creates `mod_cases` entries and has auto-escalation in `src/modules/moderation.js`. We need to build out the full warning lifecycle. | ||
|
|
||
| ## What Exists | ||
| - `/warn` command → creates a mod_case with action='warn' | ||
| - `mod_cases` table (guild_id, target_id, moderator_id, action, reason, case_number, created_at) | ||
| - `checkEscalation()` in moderation.js — checks warn count against thresholds | ||
| - `executeModAction()` utility in `src/utils/modAction.js` | ||
| - Audit log integration (`src/api/routes/auditLog.js`) | ||
| - Members API already queries warning counts | ||
|
|
||
| ## Deliverables | ||
|
|
||
| ### 1. Database Migration | ||
| Create migration `007_warnings.cjs`: | ||
| - Add `expires_at TIMESTAMPTZ` column to `mod_cases` (nullable, for decay/expiry) | ||
| - Add `expired BOOLEAN DEFAULT false` column to `mod_cases` | ||
| - Add `edited_at TIMESTAMPTZ` column to `mod_cases` | ||
| - Add `edited_by VARCHAR(20)` column to `mod_cases` | ||
| - Add `original_reason TEXT` column to `mod_cases` (stores original before edit) | ||
| - Add `removed BOOLEAN DEFAULT false` column to `mod_cases` | ||
| - Add `removed_by VARCHAR(20)` column to `mod_cases` | ||
| - Add `removed_at TIMESTAMPTZ` column to `mod_cases` | ||
| - Add index on `(guild_id, target_id, action, expired, removed)` for active warning queries | ||
|
|
||
| ### 2. New Commands | ||
|
|
||
| #### `/warnings <user>` — View warning history | ||
| - Shows paginated warning list for a user (embed with fields) | ||
| - Shows active (non-expired, non-removed) count prominently | ||
| - Each warning: case #, reason, moderator, date, status (active/expired/removed) | ||
| - Mod or admin | ||
|
|
||
| #### `/editwarn <case_number> <new_reason>` — Edit a warning reason | ||
| - Updates reason, stores original in `original_reason`, sets `edited_at` and `edited_by` | ||
| - Creates audit log entry | ||
| - Mod or admin | ||
|
|
||
| #### `/removewarn <case_number> [reason]` — Remove/void a warning | ||
| - Soft-deletes: sets `removed=true`, `removed_by`, `removed_at` | ||
| - Does NOT delete the record (audit trail) | ||
| - Creates audit log entry | ||
| - Mod or admin | ||
|
|
||
| #### `/clearwarnings <user> [reason]` — Clear all active warnings for a user | ||
| - Bulk soft-delete all active warnings | ||
| - Creates audit log entry | ||
| - Mod or admin, requires confirmation | ||
|
|
||
| **IMPORTANT:** Also update `src/commands/warn.js` to change `adminOnly = true` to `adminOnly = false` (or use moderator permission check). All warning commands should be usable by moderators, not just admins. | ||
|
|
||
| ### 3. Warning Decay/Expiry Engine | ||
| In `src/modules/moderation.js` or new `src/modules/warningDecay.js`: | ||
| - On bot startup and on a configurable interval (default: every hour), scan for expired warnings | ||
| - Mark `expired=true` where `expires_at < NOW()` and `expired=false` and `removed=false` | ||
| - `checkEscalation()` should only count active warnings (not expired/removed) | ||
| - Config: `moderation.warnings.decayDays` (default: null = never expire) | ||
| - When a new `/warn` is issued, set `expires_at = NOW() + decayDays` if configured | ||
|
|
||
| ### 4. Escalation Improvements | ||
| Update `checkEscalation()` in `src/modules/moderation.js`: | ||
| - Only count active warnings (`expired=false AND removed=false`) | ||
| - Support configurable escalation actions: `timeout`, `kick`, `ban` | ||
| - Config schema: `moderation.escalation.thresholds[].action` (currently hardcoded) | ||
|
|
||
| ### 5. DM Notification | ||
| When a warning is issued: | ||
| - If `moderation.warnings.dmNotification` is true (default: true), DM the user | ||
| - Message: "You have been warned in {server} for: {reason}. You now have {count} active warning(s)." | ||
| - Gracefully handle blocked DMs (log warning, don't fail) | ||
|
|
||
| ### 6. API Routes | ||
| Create `src/api/routes/warnings.js`: | ||
| - `GET /api/v1/guilds/:guildId/warnings` — list warnings (paginated, filterable by user) | ||
| - `GET /api/v1/guilds/:guildId/warnings/:caseNumber` — single warning detail | ||
| - `PATCH /api/v1/guilds/:guildId/warnings/:caseNumber` — edit reason | ||
| - `DELETE /api/v1/guilds/:guildId/warnings/:caseNumber` — soft-remove | ||
| - `DELETE /api/v1/guilds/:guildId/users/:userId/warnings` — clear all for user | ||
| - All routes require auth + admin permission check | ||
|
|
||
| ### 7. Config Schema | ||
| Add to config defaults: | ||
| ```json | ||
| { | ||
| "moderation": { | ||
| "warnings": { | ||
| "dmNotification": true, | ||
| "decayDays": null, | ||
| "maxPerPage": 10 | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### 8. Tests | ||
| Create `tests/commands/warnings.test.js`: | ||
| - Test `/warn` creates case with correct fields | ||
| - Test `/warnings` shows history | ||
| - Test `/editwarn` updates reason + audit trail | ||
| - Test `/removewarn` soft-deletes + audit trail | ||
| - Test `/clearwarnings` bulk removes | ||
| - Test escalation only counts active warnings | ||
| - Test decay engine marks expired warnings | ||
| - Test DM notification (success + blocked DM) | ||
| - Test API routes (CRUD + auth) | ||
| - Test pagination | ||
|
|
||
| ## Important Notes | ||
| - Follow existing patterns in `src/commands/` and `src/utils/modAction.js` | ||
| - Use Winston logger from `src/logger.js`, NEVER `console.*` | ||
| - Use existing `checkPermissions` patterns for admin checks | ||
| - Keep the migration backward-compatible (all new columns nullable/default) | ||
| - Run `pnpm lint && pnpm test` before committing | ||
BillChirico marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| - Commit progressively (migration first, then commands, then tests) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,7 +9,8 @@ | |
| "!coverage", | ||
| "!logs", | ||
| "!data", | ||
| "!feat-issue-164" | ||
| "!feat-issue-164", | ||
| "!worktrees" | ||
| ] | ||
| }, | ||
| "linter": { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| /** | ||
| * Migration: Comprehensive Warning System | ||
| * | ||
| * Adds a dedicated `warnings` table that tracks individual warnings with | ||
| * severity, points, expiry (auto-removal after a configurable period), | ||
| * and decay (points reduce over time). Warnings reference the parent | ||
| * mod_case for traceability. | ||
| * | ||
| * Also adds an index on mod_cases for active-warning escalation queries. | ||
| * | ||
| * @see https://github.com/VolvoxLLC/volvox-bot/issues/250 | ||
| */ | ||
|
|
||
| 'use strict'; | ||
|
|
||
| /** @param {import('node-pg-migrate').MigrationBuilder} pgm */ | ||
| exports.up = (pgm) => { | ||
| // ── warnings table ───────────────────────────────────────────────── | ||
| pgm.sql(` | ||
| CREATE TABLE IF NOT EXISTS warnings ( | ||
| id SERIAL PRIMARY KEY, | ||
| guild_id TEXT NOT NULL, | ||
| user_id TEXT NOT NULL, | ||
| moderator_id TEXT NOT NULL, | ||
| moderator_tag TEXT NOT NULL, | ||
| reason TEXT, | ||
| severity TEXT NOT NULL DEFAULT 'low' | ||
| CHECK (severity IN ('low', 'medium', 'high')), | ||
| points INTEGER NOT NULL DEFAULT 1, | ||
| active BOOLEAN NOT NULL DEFAULT TRUE, | ||
| expires_at TIMESTAMPTZ, | ||
| removed_at TIMESTAMPTZ, | ||
| removed_by TEXT, | ||
| removal_reason TEXT, | ||
| case_id INTEGER REFERENCES mod_cases(id) ON DELETE SET NULL, | ||
| created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), | ||
| updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() | ||
| ) | ||
| `); | ||
|
|
||
| // Fast lookup: all warnings for a user in a guild | ||
| pgm.sql( | ||
| 'CREATE INDEX IF NOT EXISTS idx_warnings_guild_user ON warnings(guild_id, user_id, created_at DESC)', | ||
| ); | ||
|
|
||
| // Fast lookup: active warnings only (for escalation + /warnings display) | ||
| pgm.sql( | ||
| 'CREATE INDEX IF NOT EXISTS idx_warnings_active ON warnings(guild_id, user_id) WHERE active = TRUE', | ||
| ); | ||
|
|
||
| // Expiry polling: find warnings that need to be deactivated | ||
| pgm.sql( | ||
| 'CREATE INDEX IF NOT EXISTS idx_warnings_expires ON warnings(expires_at) WHERE active = TRUE AND expires_at IS NOT NULL', | ||
| ); | ||
| }; | ||
|
|
||
| /** @param {import('node-pg-migrate').MigrationBuilder} pgm */ | ||
| exports.down = (pgm) => { | ||
| pgm.sql('DROP TABLE IF EXISTS warnings CASCADE'); | ||
| }; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -21,6 +21,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import performanceRouter from './routes/performance.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import tempRolesRouter from './routes/tempRoles.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ticketsRouter from './routes/tickets.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import warningsRouter from './routes/warnings.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import webhooksRouter from './routes/webhooks.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import welcomeRouter from './routes/welcome.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -58,6 +59,9 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Moderation routes — require API secret or OAuth2 JWT | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| router.use('/moderation', requireAuth(), auditLogMiddleware(), moderationRouter); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Warning routes — require API secret or OAuth2 JWT | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| router.use('/warnings', requireAuth(), auditLogMiddleware(), warningsRouter); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Check failureCode scanning / CodeQL Missing rate limiting High
This route handler performs
authorization Error loading related location Loading This route handler performs authorization Error loading related location Loading
Copilot AutofixAI 9 days ago In general, the fix is to introduce a rate-limiting middleware (e.g., using The best single approach here is to (1) import Concretely:
Suggested changeset
2
src/api/index.js
package.json
Outside changed files
This fix introduces these dependencies
Copilot is powered by AI and may make mistakes. Always verify output.
Refresh and try again.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Temp role routes — require API secret or OAuth2 JWT | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| router.use('/temp-roles', requireAuth(), auditLogMiddleware(), tempRolesRouter); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.