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
46 lines
1.7 KiB
JavaScript
46 lines
1.7 KiB
JavaScript
// Member join handling: record the join event (with best-effort invite
|
|
// attribution, Phase 6b) then apply the configured auto-role. Requires the
|
|
// Server Members privileged intent (already enabled per the Phase 1 setup notes)
|
|
// and, for invite attribution, the GuildInvites intent.
|
|
const guildConfig = require('../model/guildConfig')
|
|
const memberEvents = require('../model/memberEvents')
|
|
const inviteTracker = require('./inviteTracker')
|
|
const createLogger = require('../utils/logger')
|
|
|
|
const log = createLogger('members')
|
|
|
|
async function handleGuildMemberAdd(member) {
|
|
// Attribute the invite first (diffs the invite-use cache), then record the join.
|
|
// Both are best-effort — a failure here must never block the auto-role below.
|
|
let invite = { code: null, inviterId: null, inviterTag: null }
|
|
try {
|
|
invite = await inviteTracker.attribute(member)
|
|
} catch (err) {
|
|
log.warn('invite attribution threw', { userId: member.id, message: err.message })
|
|
}
|
|
try {
|
|
await memberEvents.record({
|
|
guildId: member.guild.id,
|
|
eventType: 'join',
|
|
discordUserId: member.id,
|
|
username: member.user?.tag,
|
|
inviteCode: invite.code,
|
|
inviterId: invite.inviterId,
|
|
inviterTag: invite.inviterTag,
|
|
})
|
|
} catch (err) {
|
|
log.warn('member join record failed', { userId: member.id, message: err.message })
|
|
}
|
|
|
|
try {
|
|
const roleId = await guildConfig.getAutoRoleId(member.guild.id)
|
|
if (!roleId) return
|
|
await member.roles.add(roleId)
|
|
log.info('auto-role assigned', { userId: member.id, roleId })
|
|
} catch (err) {
|
|
log.warn('auto-role assignment failed', { userId: member.id, message: err.message })
|
|
}
|
|
}
|
|
|
|
module.exports = { handleGuildMemberAdd }
|