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
@@ -0,0 +1,106 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
// Discord refuses a command that is over its limits, and then the whole
|
||||
// command silently doesn't exist. These checks catch that in a test instead
|
||||
// of after a deploy. See
|
||||
// https://discord.com/developers/docs/interactions/application-commands
|
||||
|
||||
var commandName = regexp.MustCompile(`^[-_\p{L}\p{N}]{1,32}$`)
|
||||
|
||||
func checkOptions(t *testing.T, path string, options []*discordgo.ApplicationCommandOption, size *int) {
|
||||
t.Helper()
|
||||
if len(options) > 25 {
|
||||
t.Errorf("%s: %d options, at most 25 allowed", path, len(options))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
sawOptional := false
|
||||
for _, o := range options {
|
||||
where := path + " " + o.Name
|
||||
if !commandName.MatchString(o.Name) || o.Name != lower(o.Name) {
|
||||
t.Errorf("%s: option names are 1 to 32 lowercase letters, digits, - or _", where)
|
||||
}
|
||||
if seen[o.Name] {
|
||||
t.Errorf("%s: duplicate option name", where)
|
||||
}
|
||||
seen[o.Name] = true
|
||||
if n := utf8.RuneCountInString(o.Description); n < 1 || n > 100 {
|
||||
t.Errorf("%s: description is %d characters, must be 1 to 100", where, n)
|
||||
}
|
||||
*size += utf8.RuneCountInString(o.Name) + utf8.RuneCountInString(o.Description)
|
||||
if len(o.Choices) > 25 {
|
||||
t.Errorf("%s: %d choices, at most 25", where, len(o.Choices))
|
||||
}
|
||||
for _, c := range o.Choices {
|
||||
*size += utf8.RuneCountInString(c.Name)
|
||||
if s, ok := c.Value.(string); ok {
|
||||
*size += utf8.RuneCountInString(s)
|
||||
if utf8.RuneCountInString(s) > 100 || utf8.RuneCountInString(c.Name) > 100 {
|
||||
t.Errorf("%s: a choice is over 100 characters", where)
|
||||
}
|
||||
}
|
||||
}
|
||||
switch o.Type {
|
||||
case discordgo.ApplicationCommandOptionSubCommand, discordgo.ApplicationCommandOptionSubCommandGroup:
|
||||
if len(o.Options) == 0 && o.Type == discordgo.ApplicationCommandOptionSubCommandGroup {
|
||||
t.Errorf("%s: a group needs subcommands", where)
|
||||
}
|
||||
checkOptions(t, where, o.Options, size)
|
||||
default:
|
||||
// Required options come before optional ones.
|
||||
if o.Required && sawOptional {
|
||||
t.Errorf("%s: a required option after an optional one", where)
|
||||
}
|
||||
if !o.Required {
|
||||
sawOptional = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lower(s string) string {
|
||||
out := []rune(s)
|
||||
for i, r := range out {
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
out[i] = r + 32
|
||||
}
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func TestCommandsStayWithinDiscordLimits(t *testing.T) {
|
||||
all := append(append([]*discordgo.ApplicationCommand{}, AllCommands...), BetaCommands...)
|
||||
names := map[string]bool{}
|
||||
for _, c := range all {
|
||||
if names[c.Name] {
|
||||
t.Errorf("/%s is defined twice", c.Name)
|
||||
}
|
||||
names[c.Name] = true
|
||||
if !commandName.MatchString(c.Name) || c.Name != lower(c.Name) {
|
||||
t.Errorf("/%s: names are 1 to 32 lowercase letters, digits, - or _", c.Name)
|
||||
}
|
||||
if n := utf8.RuneCountInString(c.Description); n < 1 || n > 100 {
|
||||
t.Errorf("/%s: description is %d characters, must be 1 to 100", c.Name, n)
|
||||
}
|
||||
size := utf8.RuneCountInString(c.Name) + utf8.RuneCountInString(c.Description)
|
||||
checkOptions(t, "/"+c.Name, c.Options, &size)
|
||||
if size > 8000 {
|
||||
t.Errorf("/%s: %d characters in total, Discord allows 8000", c.Name, size)
|
||||
}
|
||||
if _, ok := handlers[c.Name]; !ok {
|
||||
t.Errorf("/%s has no handler", c.Name)
|
||||
}
|
||||
}
|
||||
for name := range handlers {
|
||||
if !names[name] {
|
||||
t.Errorf("a handler for /%s exists but the command is never registered", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user