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:
1 parent
5657d0789a
commit
d95ebf473e
10 files changed
+1020
-4
No files matched your search
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user