Add Discord bot (moderation, filters, scheduling, roles, invites, site integration)

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>
This commit is contained in:
2026-07-04 15:54:41 -05:00
parent 0318d6fe9f
commit 7a21cc636c
77 changed files with 4800 additions and 3 deletions

52
bot/src/server.js Normal file
View File

@@ -0,0 +1,52 @@
require('dotenv').config()
const app = require('./app')
const bootstrap = require('./bootstrap')
const createLogger = require('./utils/logger')
const discordManager = require('./discord/discordManager')
const pkg = require('../package.json')
const log = createLogger('server')
const PORT = Number(process.env.PORT) || 4100
const HOST = '0.0.0.0'
async function start() {
log.info(`starting UOMysticmoon bot v${pkg.version}`, {
node: process.version,
logFile: createLogger.logFilePath || 'disabled (console only)',
})
const server = app.listen(PORT, HOST, () => {
log.info(`internal API listening on http://${HOST}:${PORT}`)
})
await bootstrap()
setupShutdown(server)
}
function setupShutdown(server) {
let closing = false
const shutdown = async (signal) => {
if (closing) return
closing = true
log.warn(`${signal} received — shutting down gracefully`)
server.close(() => log.info('internal API closed'))
await discordManager.stop()
await createLogger.close()
process.exit(0)
}
process.on('SIGINT', () => shutdown('SIGINT'))
process.on('SIGTERM', () => shutdown('SIGTERM'))
process.on('unhandledRejection', (reason) => log.error('unhandledRejection', { reason: String(reason) }))
process.on('uncaughtException', (err) => {
log.error('uncaughtException', err)
process.exit(1)
})
}
start().catch((err) => {
log.error('failed to start bot', err)
process.exit(1)
})