Add moderation system with deterministic detection and strike ladder

Deterministic-only detection (no LLM): word list with precompiled
leetspeak variation matching, text/image flood, wall of text, repeated
messages (Levenshtein similarity), mention flooding, link rate limiting
for new members, invite flooding, join/leave cycling detection.

Three-strike response ladder: warn + redact → temp mute → permanent ban.
Strikes expire after configurable period. Admin room notifications with
context cards. DMs over public callouts.

Admin commands: !mod warn/mute/unmute/ban/strikes/forgive/history/reload/
status/test. All require ADMIN_USERS membership.

Feature-flagged: disabled by default, set FEATURE_MODERATION=true to
enable. All thresholds configurable via env vars. New member grace
period with stricter thresholds. Word list hot-reload via !mod reload.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-03-11 17:49:35 -07:00
parent 56f69cd4b7
commit 996bb18566
5 changed files with 1420 additions and 3 deletions

18
main.go
View File

@@ -71,6 +71,10 @@ func main() {
// ---- Register plugins in dependency order ----
// Moderation (runs first in dispatch order)
modPlugin := plugin.NewModerationPlugin(client)
registry.Register(modPlugin)
// Foundation (passive tracking)
xpPlugin := plugin.NewXPPlugin(client)
registry.Register(xpPlugin)
@@ -154,10 +158,15 @@ func main() {
syncer := client.Syncer.(*mautrix.DefaultSyncer)
// Auto-join on invite
// Auto-join on invite + moderation member tracking
syncer.OnEventType(event.StateMember, func(ctx context.Context, evt *event.Event) {
mem := evt.Content.AsMember()
if mem == nil {
return
}
// Auto-join invites for the bot
if evt.GetStateKey() == string(client.UserID) {
mem := evt.Content.AsMember()
if mem.Membership == event.MembershipInvite {
_, err := client.JoinRoomByID(ctx, evt.RoomID)
if err != nil {
@@ -166,7 +175,12 @@ func main() {
slog.Info("joined room", "room", evt.RoomID)
}
}
return
}
// Track join/leave/invite for moderation
targetUser := id.UserID(evt.GetStateKey())
modPlugin.OnMemberEvent(evt.RoomID, targetUser, mem.Membership)
})
// Message handler