Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,14 @@
| `src/modules/events.js` | Event handler registration (wires modules to Discord events) |
| `src/api/server.js` | Express API server setup (createApp, startServer, stopServer) |
| `src/api/index.js` | API route mounting |
| `src/api/middleware/auditLog.js` | Audit logging middleware for authenticated mutating API requests |
| `src/api/routes/guilds.js` | Guild REST API endpoints (info, channels, roles, config, stats, members, moderation, analytics, actions) |
| `src/api/routes/auditLog.js` | Audit log retrieval endpoint (filters + pagination) |
| `web/src/components/dashboard/analytics-dashboard.tsx` | Analytics dashboard React component — charts, KPIs, date range controls |
| `web/src/types/analytics.ts` | Shared analytics TypeScript contracts used by dashboard UI and analytics API responses |
| `web/src/app/api/guilds/[guildId]/analytics/route.ts` | Next.js API route — proxies analytics requests to bot API with param allowlisting |
| `web/src/app/dashboard/audit-log/page.tsx` | Audit log dashboard page (filterable audit timeline + detail rows) |
| `web/src/app/api/guilds/[guildId]/audit-log/route.ts` | Next.js API route — proxies guild audit log queries to bot API |
| `web/src/components/dashboard/channel-selector.tsx` | Channel picker component — single or multi-select Discord channel picker with Zustand store integration |
| `web/src/components/dashboard/role-selector.tsx` | Role picker component — single or multi-select Discord role picker with color dots |
| `web/src/components/dashboard/array-editor.tsx` | Tag-input component for editing string arrays (Enter to add, Backspace to remove) |
Expand All @@ -64,6 +68,7 @@
| `src/modules/afkHandler.js` | AFK message handler — detects AFK mentions, sends inline notices (rate-limited), auto-clears AFK on return, DMs ping summary |
| `src/modules/scheduler.js` | Scheduled message poller — cron expression parser (`parseCron`, `getNextCronRun`), due-message dispatcher via `safeSend`, 60s interval started/stopped via `startScheduler`/`stopScheduler` |
| `migrations/002_scheduled-messages.cjs` | Migration — creates `scheduled_messages` table (id, guild_id, channel_id, content, cron_expression, next_run, is_one_time, created_by) |
| `migrations/015_audit_logs.cjs` | Migration — creates `audit_logs` table + indexes for guild timeline and retention cleanup |
| `config.json` | Default configuration (seeded to DB on first run) |
| `.env.example` | Environment variable template |

Expand Down Expand Up @@ -141,6 +146,7 @@ Duration-based commands (timeout, tempban, slowmode) use `parseDuration()` from
| `mod_scheduled_actions` | Scheduled operations (tempban expiry). Polled every 60s by the tempban scheduler |
| `afk_status` | Active AFK records — one row per (guild_id, user_id); upserted on `/afk set`, deleted on return or `/afk clear` |
| `afk_pings` | Pings logged while a user is AFK — accumulated until the user returns, then DM-summarised and deleted |
| `audit_logs` | Admin audit trail for mutating API actions (guild/user/action/target + JSON details + IP + created_at) |

## How to Add a Module

Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,13 @@ All configuration lives in `config.json` and can be updated at runtime via the `

**Escalation thresholds** are objects with: `warns` (count), `withinDays` (window), `action` ("timeout" or "ban"), `duration` (for timeout, e.g. "1h").

### Audit Log (`auditLog`)

| Key | Type | Description |
|-----|------|-------------|
| `enabled` | boolean | Enable/disable audit logging for mutating authenticated API requests |
| `retentionDays` | number | Data retention window in days for scheduled cleanup (default: 90, `<= 0` disables purge) |

### Starboard (`starboard`)

| Key | Type | Description |
Expand Down
16 changes: 10 additions & 6 deletions config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"ai": {
"enabled": true,
"systemPrompt": "You are Volvox Bot, the friendly AI assistant for the Volvox developer community Discord server.\n\nYou're witty, snarky (but warm), and deeply knowledgeable about programming, software development, and tech.\n\nKey traits:\n- Helpful but not boring\n- Can roast people lightly when appropriate\n- Enthusiastic about cool tech and projects\n- Supportive of beginners learning to code\n- Concise - this is Discord, not an essay\n\nIf asked about your own infrastructure, model, or internals say you don't know the specifics\nand suggest asking a server admin. Don't guess or speculate about what you run on.\n\nCRITICAL RULES:\n- NEVER type @everyone or @here these ping hundreds of people\n- NEVER use mass mention pings under any circumstances\n- If you need to address the group, say \"everyone\" or \"folks\" without the @ symbol\n\nKeep responses under 2000 chars. Use Discord markdown when helpful.",
"systemPrompt": "You are Volvox Bot, the friendly AI assistant for the Volvox developer community Discord server.\n\nYou're witty, snarky (but warm), and deeply knowledgeable about programming, software development, and tech.\n\nKey traits:\n- Helpful but not boring\n- Can roast people lightly when appropriate\n- Enthusiastic about cool tech and projects\n- Supportive of beginners learning to code\n- Concise - this is Discord, not an essay\n\nIf asked about your own infrastructure, model, or internals \u2014 say you don't know the specifics\nand suggest asking a server admin. Don't guess or speculate about what you run on.\n\nCRITICAL RULES:\n- NEVER type @everyone or @here \u2014 these ping hundreds of people\n- NEVER use mass mention pings under any circumstances\n- If you need to address the group, say \"everyone\" or \"folks\" without the @ symbol\n\nKeep responses under 2000 chars. Use Discord markdown when helpful.",
"channels": [],
"historyLength": 20,
"historyTTLDays": 30,
Expand Down Expand Up @@ -43,7 +43,7 @@
"welcome": {
"enabled": true,
"channelId": "1438631182379253814",
"message": "Welcome to Volvox, {user}! 🌱 You're member #{memberCount}!\n\nWe're a community of developers building cool stuff together. Feel free to introduce yourself!\n\nCheck out <#1446317676988465242> to see what we're working on, share your projects in <#1444154471704957069>, or just say hi in <#1438631182379253814>.\n\nHave questions? Just ask we're here to help. 💚",
"message": "Welcome to Volvox, {user}! \ud83c\udf31 You're member #{memberCount}!\n\nWe're a community of developers building cool stuff together. Feel free to introduce yourself!\n\nCheck out <#1446317676988465242> to see what we're working on, share your projects in <#1444154471704957069>, or just say hi in <#1438631182379253814>.\n\nHave questions? Just ask \u2014 we're here to help. \ud83d\udc9a",
"dynamic": {
"enabled": true,
"timezone": "America/New_York",
Expand Down Expand Up @@ -211,19 +211,19 @@
"activityBadges": [
{
"days": 90,
"label": "👑 Legend"
"label": "\ud83d\udc51 Legend"
},
{
"days": 30,
"label": "🌳 Veteran"
"label": "\ud83c\udf33 Veteran"
},
{
"days": 7,
"label": "🌿 Regular"
"label": "\ud83c\udf3f Regular"
},
{
"days": 0,
"label": "🌱 Newcomer"
"label": "\ud83c\udf31 Newcomer"
}
]
},
Expand Down Expand Up @@ -260,5 +260,9 @@
"channelId": null,
"staleAfterDays": 7,
"xpReward": 50
},
"auditLog": {
"enabled": true,
"retentionDays": 90
}
}
39 changes: 39 additions & 0 deletions migrations/015_audit_logs.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Migration 015 — Audit Logs
* Creates the audit_logs table for tracking admin actions in the web dashboard.
*
* @see https://github.com/VolvoxLLC/volvox-bot/issues/123
*/

'use strict';

/**
* @param {import('pg').Pool} pool
*/
async function up(pool) {
await pool.query(`
CREATE TABLE IF NOT EXISTS audit_logs (
id SERIAL PRIMARY KEY,
guild_id VARCHAR(32) NOT NULL,
user_id VARCHAR(32) NOT NULL,
action VARCHAR(128) NOT NULL,
target_type VARCHAR(64),
target_id VARCHAR(64),
details JSONB,
ip_address VARCHAR(45),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
`);

await pool.query(`
CREATE INDEX IF NOT EXISTS idx_audit_logs_guild_created
ON audit_logs(guild_id, created_at DESC);
`);

await pool.query(`
CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at
ON audit_logs(created_at);
`);
}

module.exports = { up };
18 changes: 12 additions & 6 deletions src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
*/

import { Router } from 'express';
import { auditLogMiddleware } from './middleware/auditLog.js';
import { requireAuth } from './middleware/auth.js';
import auditLogRouter from './routes/auditLog.js';
import authRouter from './routes/auth.js';
import communityRouter from './routes/community.js';
import configRouter from './routes/config.js';
Expand All @@ -28,25 +30,29 @@ router.use('/community', communityRouter);
router.use('/auth', authRouter);

// Global config routes — require API secret or OAuth2 JWT
router.use('/config', requireAuth(), configRouter);
router.use('/config', requireAuth(), auditLogMiddleware(), configRouter);

// Member management routes — require API secret or OAuth2 JWT
// (mounted before guilds to handle /:id/members/* before the basic guilds endpoint)
router.use('/guilds', requireAuth(), membersRouter);
router.use('/guilds', requireAuth(), auditLogMiddleware(), membersRouter);

// Conversation routes — require API secret or OAuth2 JWT
// (mounted before guilds to handle /:id/conversations/* before the catch-all guild endpoint)
router.use('/guilds/:id/conversations', requireAuth(), conversationsRouter);
router.use('/guilds/:id/conversations', requireAuth(), auditLogMiddleware(), conversationsRouter);

// Ticket routes — require API secret or OAuth2 JWT
// (mounted before guilds to handle /:id/tickets/* before the catch-all guild endpoint)
router.use('/guilds', requireAuth(), ticketsRouter);
router.use('/guilds', requireAuth(), auditLogMiddleware(), ticketsRouter);

// Guild routes — require API secret or OAuth2 JWT
router.use('/guilds', requireAuth(), guildsRouter);
router.use('/guilds', requireAuth(), auditLogMiddleware(), guildsRouter);

// Moderation routes — require API secret or OAuth2 JWT
router.use('/moderation', requireAuth(), moderationRouter);
router.use('/moderation', requireAuth(), auditLogMiddleware(), moderationRouter);

// Audit log routes — require API secret or OAuth2 JWT
// GET-only; no audit middleware needed (reads are not mutating actions)
router.use('/guilds', requireAuth(), auditLogRouter);

// Webhook routes — require API secret or OAuth2 JWT (endpoint further restricts to api-secret)
router.use('/webhooks', requireAuth(), webhooksRouter);
Expand Down
Loading