// Business logic for the moderation dashboard: reshapes the raw mod_actions // reads into the shapes the admin UI consumes, and annotates each action with // whether it was an automated (bot) action. For a Discord bot the application_id // IS the bot's user id, and the filter/spam pipeline records automated actions // with staff_user_id = the bot user (see bot/src/discord/messageFilter.js), so // staff_user_id === bot_config.application_id reliably flags automated actions // without needing new columns on mod_actions. const moderationDb = require('./moderation.db') const botConfigDb = require('../botConfig/botConfig.db') const { zeroCounts, annotate, reshapeWindows, windowValue } = require('./moderation.pure') const DAY_MS = 24 * 60 * 60 * 1000 const WINDOW_KEYS = ['24h', '7d', '30d'] async function botApplicationId() { try { const cfg = await botConfigDb.get() return cfg ? cfg.application_id : null } catch { return null } } // Counts by type across 24h / 7d / 30d windows for the overview tiles. Covers // moderation actions (mod_actions) plus the Phase 6b event streams: member // joins/leaves, attributed invite joins, and filter/spam hits. async function summary() { const now = Date.now() const cutoffs = { cutoff24h: new Date(now - DAY_MS), cutoff7d: new Date(now - 7 * DAY_MS), cutoff30d: new Date(now - 30 * DAY_MS), } const [modRows, memberRows, inviteRow, filterRow, spamRow] = await Promise.all([ moderationDb.countsByWindow(cutoffs), moderationDb.memberCountsByWindow(cutoffs), moderationDb.inviteJoinCountsByWindow(cutoffs), moderationDb.tableCountsByWindow('filter_hits', cutoffs), moderationDb.tableCountsByWindow('spam_hits', cutoffs), ]) const windows = reshapeWindows(modRows).windows const joinRow = memberRows.find((r) => r.event_type === 'join') const leaveRow = memberRows.find((r) => r.event_type === 'leave') for (const w of WINDOW_KEYS) { windows[w].joins = windowValue(joinRow, w) windows[w].leaves = windowValue(leaveRow, w) windows[w].invite_joins = windowValue(inviteRow, w) windows[w].filter_hits = windowValue(filterRow, w) windows[w].spam_hits = windowValue(spamRow, w) } return { windows } } // Recent event feeds for the overview's secondary panel (Phase 6b). async function members(opts) { return moderationDb.recentMemberEvents(opts) } async function filterHits(opts) { return moderationDb.recentFilterHits(opts) } async function spamHits(opts) { return moderationDb.recentSpamHits(opts) } async function recent(opts) { const appId = await botApplicationId() return annotate(await moderationDb.recentActions(opts), appId) } async function userActions(discordId, opts) { const appId = await botApplicationId() return annotate(await moderationDb.userActions(discordId, opts), appId) } // Header data for the per-user history page: latest known tag, linked site // account (if any), and all-time counts per action type. async function userSummary(discordId) { const [countRows, tag, linked] = await Promise.all([ moderationDb.userCounts(discordId), moderationDb.latestTag(discordId), moderationDb.linkedAccount(discordId), ]) const counts = zeroCounts() let total = 0 for (const row of countRows) { const c = Number(row.c) || 0 if (counts[row.action_type] !== undefined) counts[row.action_type] = c total += c } return { discord_user_id: discordId, tag, linked_account: linked, counts, total_actions: total, } } async function search(term, opts) { return moderationDb.searchTargets(term, opts) } module.exports = { summary, recent, userActions, userSummary, search, members, filterHits, spamHits }