Add Discord bot: moderation, filters, scheduling, roles, invites, site integration #29

Merged
whitlocktech merged 2 commits from feature/discord-bot into main 2026-07-04 21:19:43 +00:00
Member

Summary

Adds a standalone Discord bot for the UOMysticMoon community server, built and verified in phases against a live Discord guild. The bot runs as its own service (bot/, own package.json/Dockerfile/DB tables) so a bot crash or restart never affects the website.

Admin control, not env vars. The bot token is entered in a new admin-only Discord Bot panel, encrypted at rest (same pattern as the existing Google/Discord login-SSO secrets), and pushed to the bot process in-memory over a shared-secret internal API — never written to disk on the bot side, never an environment variable. The bot self-reconnects on its own restart by pulling current config from the site (verified: killing the bot container and restarting it reconnects with zero admin-panel interaction).

Phase 1 — Bot skeleton

  • Discord gateway connection lifecycle (start/stop/status), slash-command registration + dispatch
  • Internal shared-secret API between site and bot (/internal/config, /internal/status, /internal/bot-config)
  • New "Discord Bot" admin panel page (masked token field, live status, admin-only via existing requireRole('admin'))

Phase 2 — Moderation core

  • /ban, /kick, /mute, /warn, /warnings — each permission-gated via Discord's own permission system
  • /modlog — configurable mod-log channel; every action posts an embed there and logs to mod_actions/warnings

Phase 3 — Word / message filtering

  • /filter add|remove|list — banned-word list with severity tiers (delete / delete+warn / delete+mute), leetspeak + repeated-character normalization, word-boundary matching to avoid false positives
  • Foreign Discord invite-link blocking (allows invites that resolve back to the same guild)
  • Basic spam detection: per-user rate limiting, mass-mention, mass-emoji
  • /filterallow role|channel|list — staff/channel bypass list

Phase 4 — Scheduled messages

  • /schedule recurring|once|remove|list — recurring messages via node-cron, one-off messages via a once-a-minute due-message sweep

Phase 5 — Role assignment

  • /rolemenu create — button-based self-assignable role menus (not reaction-based, avoids an extra privileged intent)
  • /autorole — role auto-assigned on join
  • /role add|remove — single-target assign/remove, optionally temporary (swept on expiry)
  • /roles bulk-assign|bulk-remove — bulk ops targeted "by existing role" (the spec's "explicit member list" variant is deferred — no good Discord slash UX for a multi-user picker)

Phase 6 — Invite rotation

  • /invite channel|rotate|log — auto-rotating primary invite (weekly cron + on-demand), full audit trail, previous invite is revoked on each rotation

Phase 7 — Site integration

  • Publishing a news-category post now fires a fire-and-forget announce to Discord (only on a genuine unpublished→published transition, not every save)
  • /announce <post> — manually re-post/boost an existing news item
  • /wiki <query> — public, read-only full-text search over the wiki, never writes

Also fixed

A real, pre-existing bug in both MariaDB pools (server/src/utils/db.js and the new bot/src/db.js): the mariadb driver defaults to timezone: 'local', silently serializing bound Date query parameters using the host machine's local offset instead of the DB's actual UTC session — e.g. a temp role scheduled "1 second from now" showed up as already expired. Fixed by setting timezone: 'auto' on both pools; verified with a direct round-trip showing 0-second drift.

Explicitly deferred (flagged during design, not silently dropped)

  • Warn-escalation ("N warns → auto-mute") and warning decay/expiry — warnings.expires_at column exists and is ready for this
  • Category-scoped wiki search (/wiki spells fireball) — the site's search endpoint currently ignores category filters whenever a text query is given
  • Bulk role assignment by an explicit list of members (only "by existing role" is implemented)
  • Rich embed content for scheduled messages (spec mentions "content/embed JSON") — plain text only, since authoring embed JSON through a single slash-command option isn't practical without a modal
  • Screenshot Friday reminder (spec's Phase 8) — not started

Test plan

Every phase was verified against a real, live Discord guild and the real dev database (not mocks), including:

  • Bot connects, all 18 slash commands register correctly
  • Admin-only gating: editor-role account gets 403 on Discord Bot admin endpoints
  • Bot self-reconnects after its own restart with no admin-panel interaction (and stays disconnected when disabled)
  • /ban /kick /mute /warn /warnings /modlog exercised live; mod-log embeds post correctly
  • Word filter: leetspeak/repeat-char normalization confirmed directly; word-boundary matching confirmed to avoid false positives
  • Invite-link filter deletes foreign invites in a live channel
  • /schedule once — inserted a due one-off message, confirmed the cron sweep posted it to a real channel and marked it sent
  • /rolemenu button toggle validated against stored mapping; auto-role on join wired with Server Members intent
  • Temp role expiry sweep confirmed correct timing after the timezone fix
  • /invite rotate — two consecutive rotations confirmed the first invite gets revoked and logged
  • News publish → Discord announce confirmed end-to-end with a real published post (embed posted with title/excerpt/link/image), then cleaned up
  • /wiki search and /announce lookup confirmed against real wiki/post data

Not yet tested: production Docker Compose deployment (docker-compose.yml changes are structurally verified via docker compose config and a successful docker build of the bot image, but not run end-to-end in compose).

## Summary Adds a standalone Discord bot for the UOMysticMoon community server, built and verified in phases against a live Discord guild. The bot runs as its own service (`bot/`, own `package.json`/`Dockerfile`/DB tables) so a bot crash or restart never affects the website. **Admin control, not env vars.** The bot token is entered in a new admin-only **Discord Bot** panel, encrypted at rest (same pattern as the existing Google/Discord login-SSO secrets), and pushed to the bot process in-memory over a shared-secret internal API — never written to disk on the bot side, never an environment variable. The bot self-reconnects on its own restart by pulling current config from the site (verified: killing the bot container and restarting it reconnects with zero admin-panel interaction). ### Phase 1 — Bot skeleton - Discord gateway connection lifecycle (start/stop/status), slash-command registration + dispatch - Internal shared-secret API between site and bot (`/internal/config`, `/internal/status`, `/internal/bot-config`) - New "Discord Bot" admin panel page (masked token field, live status, admin-only via existing `requireRole('admin')`) ### Phase 2 — Moderation core - `/ban`, `/kick`, `/mute`, `/warn`, `/warnings` — each permission-gated via Discord's own permission system - `/modlog` — configurable mod-log channel; every action posts an embed there and logs to `mod_actions`/`warnings` ### Phase 3 — Word / message filtering - `/filter add|remove|list` — banned-word list with severity tiers (delete / delete+warn / delete+mute), leetspeak + repeated-character normalization, word-boundary matching to avoid false positives - Foreign Discord invite-link blocking (allows invites that resolve back to the same guild) - Basic spam detection: per-user rate limiting, mass-mention, mass-emoji - `/filterallow role|channel|list` — staff/channel bypass list ### Phase 4 — Scheduled messages - `/schedule recurring|once|remove|list` — recurring messages via `node-cron`, one-off messages via a once-a-minute due-message sweep ### Phase 5 — Role assignment - `/rolemenu create` — button-based self-assignable role menus (not reaction-based, avoids an extra privileged intent) - `/autorole` — role auto-assigned on join - `/role add|remove` — single-target assign/remove, optionally temporary (swept on expiry) - `/roles bulk-assign|bulk-remove` — bulk ops targeted "by existing role" (the spec's "explicit member list" variant is deferred — no good Discord slash UX for a multi-user picker) ### Phase 6 — Invite rotation - `/invite channel|rotate|log` — auto-rotating primary invite (weekly cron + on-demand), full audit trail, previous invite is revoked on each rotation ### Phase 7 — Site integration - Publishing a `news`-category post now fires a fire-and-forget announce to Discord (only on a genuine unpublished→published transition, not every save) - `/announce <post>` — manually re-post/boost an existing news item - `/wiki <query>` — public, read-only full-text search over the wiki, never writes ### Also fixed A real, pre-existing bug in **both** MariaDB pools (`server/src/utils/db.js` and the new `bot/src/db.js`): the `mariadb` driver defaults to `timezone: 'local'`, silently serializing bound `Date` query parameters using the host machine's local offset instead of the DB's actual UTC session — e.g. a temp role scheduled "1 second from now" showed up as already expired. Fixed by setting `timezone: 'auto'` on both pools; verified with a direct round-trip showing 0-second drift. ### Explicitly deferred (flagged during design, not silently dropped) - Warn-escalation ("N warns → auto-mute") and warning decay/expiry — `warnings.expires_at` column exists and is ready for this - Category-scoped wiki search (`/wiki spells fireball`) — the site's search endpoint currently ignores category filters whenever a text query is given - Bulk role assignment by an explicit list of members (only "by existing role" is implemented) - Rich embed content for scheduled messages (spec mentions "content/embed JSON") — plain text only, since authoring embed JSON through a single slash-command option isn't practical without a modal - Screenshot Friday reminder (spec's Phase 8) — not started ## Test plan Every phase was verified against a real, live Discord guild and the real dev database (not mocks), including: - [x] Bot connects, all 18 slash commands register correctly - [x] Admin-only gating: editor-role account gets 403 on Discord Bot admin endpoints - [x] Bot self-reconnects after its own restart with no admin-panel interaction (and stays disconnected when disabled) - [x] `/ban` `/kick` `/mute` `/warn` `/warnings` `/modlog` exercised live; mod-log embeds post correctly - [x] Word filter: leetspeak/repeat-char normalization confirmed directly; word-boundary matching confirmed to avoid false positives - [x] Invite-link filter deletes foreign invites in a live channel - [x] `/schedule once` — inserted a due one-off message, confirmed the cron sweep posted it to a real channel and marked it sent - [x] `/rolemenu` button toggle validated against stored mapping; auto-role on join wired with Server Members intent - [x] Temp role expiry sweep confirmed correct timing after the timezone fix - [x] `/invite rotate` — two consecutive rotations confirmed the first invite gets revoked and logged - [x] News publish → Discord announce confirmed end-to-end with a real published post (embed posted with title/excerpt/link/image), then cleaned up - [x] `/wiki` search and `/announce` lookup confirmed against real wiki/post data Not yet tested: production Docker Compose deployment (`docker-compose.yml` changes are structurally verified via `docker compose config` and a successful `docker build` of the bot image, but not run end-to-end in compose).
wtclaude added 1 commit 2026-07-04 20:56:52 +00:00
Standalone bot/ service (its own package.json/Dockerfile) managed entirely
through a new admin-only Discord Bot panel — token stored encrypted in the
DB and pushed to the bot process in-memory, never an env var. Built in
phases, each independently verified against a live Discord guild:

- Bot skeleton: gateway connection, internal shared-secret API, self-heals
  on its own restart by pulling config from the site
- Moderation core: /ban /kick /mute /warn /warnings + mod-log channel
- Word/invite/spam filtering with leetspeak-resistant normalization and a
  staff role/channel allowlist
- Scheduled messages: recurring (cron) and one-off channel posts
- Role assignment: button role menus, auto-role on join, temp roles,
  bulk role ops
- Auto-rotating primary invite with an audit log
- Site integration: news-publish -> Discord announce webhook, manual
  /announce, read-only /wiki search

Also fixes a pre-existing bug in both DB pools (server + bot): the mariadb
driver defaulted to timezone 'local', silently mis-serializing bound Date
params by the host's local offset instead of the DB's UTC session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
whitlocktech approved these changes 2026-07-04 21:19:16 +00:00
whitlocktech added 1 commit 2026-07-04 21:19:28 +00:00
whitlocktech merged commit bb5cc68c54 into main 2026-07-04 21:19:43 +00:00
whitlocktech deleted branch feature/discord-bot 2026-07-04 21:19:44 +00:00
Sign in to join this conversation.
No description provided.