Capture member/filter/spam events for the dashboard (Phase 6b)
Light up the moderation dashboard's previously-empty widgets by persisting the
event streams the bot only reacted to in-memory before.
Schema (bot-owned)
- member_events: join/leave, with invite_code/inviter_* for best-effort invite
attribution on joins
- filter_hits: word / foreign-invite filter deletions (matched + action_taken)
- spam_hits: rate_limit / mass_mention / mass_emoji detections
Bot
- new models memberEvents/filterHits/spamHits
- guildMemberAdd records the join with invite attribution; new inviteTracker.js
keeps an invite-use cache (GuildInvites intent + inviteCreate/inviteDelete) and
diffs it on join to find which invite was used — best-effort, never blocks
auto-role
- new guildMemberRemove records leaves
- messageFilter records filter/spam hits alongside the existing warn/mute;
inviteFilter now returns the offending code; detectSpam identifies which spam
rule tripped (preserving the rate-limit-first side-effect order)
- mod_actions still logs the resulting warn/mute — the new tables are additive
Server
- summary extended with joins/leaves/invite_joins/filter_hits/spam_hits per window
- new feeds: /api/v1/admin/moderation/{members,filter-hits,spam-hits}
Client
- overview now shows 8 tiles (mod actions + joins/leaves/filter/spam, joins tile
notes "N via invite") plus an Events panel with Members/Filter/Spam tabs;
removed the coming-soon note
Verified: 119 server unit tests, client build, 14-check DB-backed smoke, and a
browser click-through of every tile and events tab (incl. invite attribution).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rao86n5cXpwAyjdBFEshV
This commit is contained in:
@@ -368,6 +368,64 @@ CREATE TABLE IF NOT EXISTS invite_log (
|
||||
INDEX idx_invite_log_guild (guild_id, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Guild member join/leave events (Phase 6b). Powers the dashboard's joins/leaves
|
||||
-- feeds and the invite-usage view. Bot-owned (written by bot/src/discord/
|
||||
-- guildMemberAdd.js + guildMemberRemove.js). For joins, invite_code/inviter_*
|
||||
-- record which invite was used when the bot could attribute it (best-effort, see
|
||||
-- bot/src/discord/inviteTracker.js) — NULL when undeterminable or for leaves.
|
||||
-- These are member lifecycle events, not moderation actions, hence separate from
|
||||
-- mod_actions.
|
||||
CREATE TABLE IF NOT EXISTS member_events (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
guild_id VARCHAR(32) NOT NULL,
|
||||
event_type ENUM('join','leave') NOT NULL,
|
||||
discord_user_id VARCHAR(32) NOT NULL,
|
||||
username VARCHAR(120) NULL,
|
||||
invite_code VARCHAR(20) NULL,
|
||||
inviter_id VARCHAR(32) NULL,
|
||||
inviter_tag VARCHAR(120) NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_member_events_guild (guild_id, created_at),
|
||||
INDEX idx_member_events_user (guild_id, discord_user_id, created_at),
|
||||
INDEX idx_member_events_invite (guild_id, invite_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Automated content-filter hits (Phase 6b): one row per message the word filter
|
||||
-- or the foreign-invite filter deleted. Separate from mod_actions (which still
|
||||
-- records the resulting warn/mute) so the dashboard can show filter volume in
|
||||
-- its own right. `matched` holds the offending word (word hits) or the blocked
|
||||
-- invite code (invite hits); `action_taken` is what the pipeline did. Bot-owned
|
||||
-- (bot/src/discord/messageFilter.js).
|
||||
CREATE TABLE IF NOT EXISTS filter_hits (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
guild_id VARCHAR(32) NOT NULL,
|
||||
hit_type ENUM('word','invite') NOT NULL,
|
||||
discord_user_id VARCHAR(32) NOT NULL,
|
||||
username VARCHAR(120) NULL,
|
||||
channel_id VARCHAR(32) NULL,
|
||||
matched VARCHAR(200) NULL,
|
||||
action_taken ENUM('delete','warn','mute') NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_filter_hits_guild (guild_id, created_at),
|
||||
INDEX idx_filter_hits_user (guild_id, discord_user_id, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Automated spam-detection hits (Phase 6b): rate-limit / mass-mention /
|
||||
-- mass-emoji triggers. As with filter_hits, mod_actions still logs the resulting
|
||||
-- warn; this records the detection itself for the dashboard's spam feed.
|
||||
-- Bot-owned (bot/src/discord/messageFilter.js via bot/src/filter/spamFilter.js).
|
||||
CREATE TABLE IF NOT EXISTS spam_hits (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
guild_id VARCHAR(32) NOT NULL,
|
||||
spam_type ENUM('rate_limit','mass_mention','mass_emoji') NOT NULL,
|
||||
discord_user_id VARCHAR(32) NOT NULL,
|
||||
username VARCHAR(120) NULL,
|
||||
channel_id VARCHAR(32) NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_spam_hits_guild (guild_id, created_at),
|
||||
INDEX idx_spam_hits_user (guild_id, discord_user_id, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Staff notes on a Discord user, surfaced in the admin moderation dashboard
|
||||
-- (Phase 6). Unlike the tables above, this one is SERVER-owned — it is written
|
||||
-- and read only by the main site (moderation.controller), never by the bot.
|
||||
|
||||
Reference in New Issue
Block a user