Security: tune the Join Gate, raid mode, anti-nuke and heat with commands

- /security settings, joingate check|account-age, words add|remove|list, raid, antinuke set|limit, heat, alert-dms
- Same rule as the rest of the configuration: owner or trusted, validated in the engine
- schema.sql was missing security_settings.alert_dm, so the bot couldn't read security settings on a database built from it. Fixed, with an ALTER for existing installs
- A test checks every command against Discord's limits

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
FernandoJVideiraandClaude Sonnet 5.5 committed 2026-10-23 02:03:03 +01:00
1 parent 5657d0789a
commit d95ebf473e
10 files changed
+1020 -4

No files matched your search

+134
View File
@@ -8,6 +8,8 @@ import (
"database/sql"
"encoding/json"
"errors"
"strconv"
"strings"
"time"
)
@@ -197,6 +199,138 @@ func (r *Repo) SetJoinLock(ctx context.Context, guildID int64, mode string) erro
return r.update(ctx, guildID, `join_lock = $2`, mode)
}
// Tuning is a partial update of a server's thresholds, filters and actions.
// A nil field is left as it is. The security package validates it before it
// gets here; the column constraints are the last line of defence.
type Tuning struct {
AlertDM *bool
NoAvatar *string
NewAccount *string
MinAccountAgeDays *int
AdvertisingName *string
UsernameFilter *string
UsernamePatterns *[]string
UnverifiedBot *string
UnauthorizedBot *string
RaidJoinCount *int
RaidJoinSeconds *int
RaidAction *string
RaidPauseInvites *bool
RaidQuietMinutes *int
AntiNukeWindowSeconds *int
AntiNukeLimits map[string]int // only the keys present change
AntiNukeLockServer *bool
AntiNukeAutoRestore *bool
HeatStrictness *string
HeatAction *string
HeatTimeoutMinutes *int
HeatEscalate *bool
HeatBlockedWords *[]string
HeatPanicMembers *int
HeatLockdownMentions *int
}
// UpdateTuning applies the fields that are set, in one statement.
func (r *Repo) UpdateTuning(ctx context.Context, guildID int64, t Tuning) error {
var sets []string
args := []any{}
add := func(col string, v any) {
args = append(args, v)
sets = append(sets, col+` = $`+strconv.Itoa(len(args)+1)) // $1 is the guild
}
addList := func(col string, v []string) {
b, _ := json.Marshal(v)
args = append(args, string(b))
sets = append(sets, col+` = ARRAY(SELECT jsonb_array_elements_text($`+strconv.Itoa(len(args)+1)+`::jsonb))`)
}
if t.AlertDM != nil {
add("alert_dm", *t.AlertDM)
}
if t.NoAvatar != nil {
add("joingate_no_avatar", *t.NoAvatar)
}
if t.NewAccount != nil {
add("joingate_new_account", *t.NewAccount)
}
if t.MinAccountAgeDays != nil {
add("joingate_min_account_age_days", *t.MinAccountAgeDays)
}
if t.AdvertisingName != nil {
add("joingate_advertising_name", *t.AdvertisingName)
}
if t.UsernameFilter != nil {
add("joingate_username_filter", *t.UsernameFilter)
}
if t.UsernamePatterns != nil {
addList("joingate_username_patterns", *t.UsernamePatterns)
}
if t.UnverifiedBot != nil {
add("joingate_unverified_bot", *t.UnverifiedBot)
}
if t.UnauthorizedBot != nil {
add("joingate_unauthorized_bot", *t.UnauthorizedBot)
}
if t.RaidJoinCount != nil {
add("raid_join_count", *t.RaidJoinCount)
}
if t.RaidJoinSeconds != nil {
add("raid_join_seconds", *t.RaidJoinSeconds)
}
if t.RaidAction != nil {
add("raid_action", *t.RaidAction)
}
if t.RaidPauseInvites != nil {
add("raid_pause_invites", *t.RaidPauseInvites)
}
if t.RaidQuietMinutes != nil {
add("raid_quiet_minutes", *t.RaidQuietMinutes)
}
if t.AntiNukeWindowSeconds != nil {
add("antinuke_window_seconds", *t.AntiNukeWindowSeconds)
}
if len(t.AntiNukeLimits) > 0 {
b, _ := json.Marshal(t.AntiNukeLimits)
args = append(args, string(b))
// Merge into what is saved, so setting one limit keeps the others.
sets = append(sets, `antinuke_limits = antinuke_limits || $`+strconv.Itoa(len(args)+1)+`::jsonb`)
}
if t.AntiNukeLockServer != nil {
add("antinuke_lock_server", *t.AntiNukeLockServer)
}
if t.AntiNukeAutoRestore != nil {
add("antinuke_auto_restore", *t.AntiNukeAutoRestore)
}
if t.HeatStrictness != nil {
add("heat_strictness", *t.HeatStrictness)
}
if t.HeatAction != nil {
add("heat_action", *t.HeatAction)
}
if t.HeatTimeoutMinutes != nil {
add("heat_timeout_minutes", *t.HeatTimeoutMinutes)
}
if t.HeatEscalate != nil {
add("heat_escalate", *t.HeatEscalate)
}
if t.HeatBlockedWords != nil {
addList("heat_blocked_words", *t.HeatBlockedWords)
}
if t.HeatPanicMembers != nil {
add("heat_panic_members", *t.HeatPanicMembers)
}
if t.HeatLockdownMentions != nil {
add("heat_lockdown_mentions", *t.HeatLockdownMentions)
}
if len(sets) == 0 {
return nil
}
return r.update(ctx, guildID, strings.Join(sets, ", "), args...)
}
// ---- Trusted ----
type Trusted struct {