Files
velox-bot/internal/security/fakes_test.go
FernandoJVideiraandClaude Sonnet 5.5 132a2cd986 Commands for everything a dashboard can do
- /music skip, pause, resume, stop, repeat, remove, move (DJ role)
- /config view, clear, toggletwitch, streamers, beta-features (server owner)
- /slvl status, remove-reward; /moderation bans, timeouts; /security history
- /beta is now the bot owner's DM-only command: set, off, servers, remove-server, status
- betasettings service and repo methods, with integration tests

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-23 02:10:05 +01:00

624 lines
18 KiB
Go

package security
import (
"context"
"errors"
"fmt"
"io"
"sort"
"sync"
"time"
"velox-bot/internal/db/repos/securityrepo"
"github.com/bwmarrin/discordgo"
)
// fakeStore keeps everything in memory with the same rules as the SQL.
type fakeStore struct {
mu sync.Mutex
settings map[int64]*securityrepo.Settings
trusted map[int64][]securityrepo.Trusted
quarantines map[string]securityrepo.Quarantine
lockdowns map[string]securityrepo.Lockdown
events []securityrepo.Event
cmds []securityrepo.Command
done map[int64]bool
raids map[int64]securityrepo.Raid
caught map[int64][]securityrepo.Caught
snapshots []securityrepo.Snapshot
}
func newStore() *fakeStore {
return &fakeStore{
settings: map[int64]*securityrepo.Settings{}, trusted: map[int64][]securityrepo.Trusted{},
quarantines: map[string]securityrepo.Quarantine{}, lockdowns: map[string]securityrepo.Lockdown{}, done: map[int64]bool{},
raids: map[int64]securityrepo.Raid{}, caught: map[int64][]securityrepo.Caught{},
}
}
func (f *fakeStore) SaveRaid(_ context.Context, x securityrepo.Raid) error {
f.mu.Lock()
defer f.mu.Unlock()
if old, ok := f.raids[x.GuildID]; ok {
x.StartedAt = old.StartedAt
}
f.raids[x.GuildID] = x
return nil
}
func (f *fakeStore) ListRaids(context.Context) ([]securityrepo.Raid, error) {
f.mu.Lock()
defer f.mu.Unlock()
var out []securityrepo.Raid
for _, x := range f.raids {
out = append(out, x)
}
return out, nil
}
func (f *fakeStore) DeleteRaid(_ context.Context, g int64) error {
f.mu.Lock()
defer f.mu.Unlock()
delete(f.raids, g)
delete(f.caught, g)
return nil
}
func (f *fakeStore) AddRaidCaught(_ context.Context, g int64, c securityrepo.Caught) error {
f.mu.Lock()
defer f.mu.Unlock()
for _, x := range f.caught[g] {
if x.UserID == c.UserID {
return nil
}
}
f.caught[g] = append(f.caught[g], c)
return nil
}
func (f *fakeStore) ListRaidCaught(_ context.Context, g int64, limit int) ([]securityrepo.Caught, error) {
f.mu.Lock()
defer f.mu.Unlock()
out := f.caught[g]
if len(out) > limit {
out = out[:limit]
}
return append([]securityrepo.Caught(nil), out...), nil
}
func (f *fakeStore) SaveSnapshot(_ context.Context, s securityrepo.Snapshot) error {
f.mu.Lock()
defer f.mu.Unlock()
s.ID = int64(len(f.snapshots) + 1)
s.CreatedAt = time.Date(2026, 10, 20, 6, 0, 0, 0, time.UTC)
f.snapshots = append(f.snapshots, s)
return nil
}
func (f *fakeStore) LatestSnapshot(_ context.Context, g int64) (*securityrepo.Snapshot, error) {
f.mu.Lock()
defer f.mu.Unlock()
for i := len(f.snapshots) - 1; i >= 0; i-- {
if f.snapshots[i].GuildID == g {
s := f.snapshots[i]
return &s, nil
}
}
return nil, nil
}
func (f *fakeStore) GuildsDueForSnapshot(context.Context, time.Duration) ([]int64, error) {
return nil, nil
}
func key(a, b int64) string { return fmt.Sprint(a, "/", b) }
func (f *fakeStore) set(g int64) *securityrepo.Settings {
f.mu.Lock()
defer f.mu.Unlock()
s, ok := f.settings[g]
if !ok {
s = securityrepo.Defaults(g)
s.Configured = true
f.settings[g] = s
}
return s
}
func (f *fakeStore) GetSettings(_ context.Context, g int64) (*securityrepo.Settings, error) {
f.mu.Lock()
defer f.mu.Unlock()
if s, ok := f.settings[g]; ok {
cp := *s
return &cp, nil
}
return securityrepo.Defaults(g), nil
}
func (f *fakeStore) SetEnabled(_ context.Context, g int64, feature string, on bool) error {
s := f.set(g)
switch feature {
case securityrepo.FeatureJoinGate:
s.JoinGateEnabled = on
case securityrepo.FeatureAntiRaid:
s.AntiRaidEnabled = on
case securityrepo.FeatureAntiNuke:
s.AntiNukeEnabled = on
}
return nil
}
func (f *fakeStore) SetAlerts(_ context.Context, g, ch, role int64) error {
s := f.set(g)
s.AlertChannelID, s.AlertRoleID = ch, role
return nil
}
func (f *fakeStore) SetQuarantineRole(_ context.Context, g, role int64) error {
f.set(g).QuarantineRoleID = role
return nil
}
func (f *fakeStore) SetJoinLock(_ context.Context, g int64, mode string) error {
f.set(g).JoinLock = mode
return nil
}
func (f *fakeStore) UpdateTuning(_ context.Context, g int64, t securityrepo.Tuning) error {
s := f.set(g)
str := func(dst *string, v *string) {
if v != nil {
*dst = *v
}
}
num := func(dst *int, v *int) {
if v != nil {
*dst = *v
}
}
flag := func(dst *bool, v *bool) {
if v != nil {
*dst = *v
}
}
if t.AlertDM != nil {
s.AlertDM = *t.AlertDM
}
str(&s.NoAvatar, t.NoAvatar)
str(&s.NewAccount, t.NewAccount)
num(&s.MinAccountAgeDays, t.MinAccountAgeDays)
str(&s.AdvertisingName, t.AdvertisingName)
str(&s.UsernameFilter, t.UsernameFilter)
if t.UsernamePatterns != nil {
s.UsernamePatterns = *t.UsernamePatterns
}
str(&s.UnverifiedBot, t.UnverifiedBot)
str(&s.UnauthorizedBot, t.UnauthorizedBot)
num(&s.RaidJoinCount, t.RaidJoinCount)
num(&s.RaidJoinSeconds, t.RaidJoinSeconds)
str(&s.RaidAction, t.RaidAction)
flag(&s.RaidPauseInvites, t.RaidPauseInvites)
num(&s.RaidQuietMinutes, t.RaidQuietMinutes)
num(&s.AntiNukeWindowSeconds, t.AntiNukeWindowSeconds)
if s.AntiNukeLimits == nil {
s.AntiNukeLimits = securityrepo.DefaultLimits()
}
for k, v := range t.AntiNukeLimits {
s.AntiNukeLimits[k] = v
}
flag(&s.AntiNukeLockServer, t.AntiNukeLockServer)
flag(&s.AntiNukeAutoRestore, t.AntiNukeAutoRestore)
str(&s.HeatStrictness, t.HeatStrictness)
str(&s.HeatAction, t.HeatAction)
num(&s.HeatTimeoutMinutes, t.HeatTimeoutMinutes)
flag(&s.HeatEscalate, t.HeatEscalate)
if t.HeatBlockedWords != nil {
s.HeatBlockedWords = *t.HeatBlockedWords
}
num(&s.HeatPanicMembers, t.HeatPanicMembers)
num(&s.HeatLockdownMentions, t.HeatLockdownMentions)
return nil
}
func (f *fakeStore) ListTrusted(_ context.Context, g int64) ([]securityrepo.Trusted, error) {
f.mu.Lock()
defer f.mu.Unlock()
return append([]securityrepo.Trusted(nil), f.trusted[g]...), nil
}
func (f *fakeStore) AddTrusted(_ context.Context, g int64, t securityrepo.Trusted) error {
f.mu.Lock()
defer f.mu.Unlock()
f.trusted[g] = append(f.trusted[g], t)
return nil
}
func (f *fakeStore) RemoveTrusted(_ context.Context, g, target int64) (bool, error) {
f.mu.Lock()
defer f.mu.Unlock()
var keep []securityrepo.Trusted
found := false
for _, t := range f.trusted[g] {
if t.TargetID == target {
found = true
continue
}
keep = append(keep, t)
}
f.trusted[g] = keep
return found, nil
}
func (f *fakeStore) SaveQuarantine(_ context.Context, q securityrepo.Quarantine) error {
f.mu.Lock()
defer f.mu.Unlock()
if _, ok := f.quarantines[key(q.GuildID, q.UserID)]; !ok {
f.quarantines[key(q.GuildID, q.UserID)] = q
}
return nil
}
func (f *fakeStore) GetQuarantine(_ context.Context, g, u int64) (*securityrepo.Quarantine, error) {
f.mu.Lock()
defer f.mu.Unlock()
if q, ok := f.quarantines[key(g, u)]; ok {
return &q, nil
}
return nil, nil
}
func (f *fakeStore) ListQuarantines(_ context.Context, g int64) ([]securityrepo.Quarantine, error) {
f.mu.Lock()
defer f.mu.Unlock()
var out []securityrepo.Quarantine
for _, q := range f.quarantines {
if q.GuildID == g {
out = append(out, q)
}
}
return out, nil
}
func (f *fakeStore) DeleteQuarantine(_ context.Context, g, u int64) error {
f.mu.Lock()
defer f.mu.Unlock()
delete(f.quarantines, key(g, u))
return nil
}
func (f *fakeStore) SaveLockdown(_ context.Context, l securityrepo.Lockdown) error {
f.mu.Lock()
defer f.mu.Unlock()
if _, ok := f.lockdowns[key(l.GuildID, l.ChannelID)]; !ok {
f.lockdowns[key(l.GuildID, l.ChannelID)] = l
}
return nil
}
func (f *fakeStore) GetLockdown(_ context.Context, g, ch int64) (*securityrepo.Lockdown, error) {
f.mu.Lock()
defer f.mu.Unlock()
if l, ok := f.lockdowns[key(g, ch)]; ok {
return &l, nil
}
return nil, nil
}
func (f *fakeStore) ListLockdowns(_ context.Context, g int64, serverOnly bool) ([]securityrepo.Lockdown, error) {
f.mu.Lock()
defer f.mu.Unlock()
var out []securityrepo.Lockdown
for _, l := range f.lockdowns {
if l.GuildID == g && (!serverOnly || l.ServerWide) {
out = append(out, l)
}
}
sort.Slice(out, func(i, j int) bool { return out[i].ChannelID < out[j].ChannelID })
return out, nil
}
func (f *fakeStore) DeleteLockdown(_ context.Context, g, ch int64) error {
f.mu.Lock()
defer f.mu.Unlock()
delete(f.lockdowns, key(g, ch))
return nil
}
func (f *fakeStore) AddEvent(_ context.Context, e securityrepo.Event) error {
f.mu.Lock()
defer f.mu.Unlock()
f.events = append(f.events, e)
return nil
}
func (f *fakeStore) ListEvents(_ context.Context, g int64, limit int) ([]securityrepo.Event, error) {
f.mu.Lock()
defer f.mu.Unlock()
var out []securityrepo.Event
for k := len(f.events) - 1; k >= 0 && len(out) < limit; k-- {
if f.events[k].GuildID == g {
out = append(out, f.events[k])
}
}
return out, nil
}
func (f *fakeStore) eventKinds() []string {
f.mu.Lock()
defer f.mu.Unlock()
var out []string
for _, e := range f.events {
out = append(out, e.Kind)
}
return out
}
func (f *fakeStore) ListUnprocessedCommands(context.Context) ([]securityrepo.Command, error) {
f.mu.Lock()
defer f.mu.Unlock()
var out []securityrepo.Command
for _, c := range f.cmds {
if !f.done[c.ID] {
out = append(out, c)
}
}
return out, nil
}
func (f *fakeStore) MarkCommandProcessed(_ context.Context, id int64) error {
f.mu.Lock()
defer f.mu.Unlock()
f.done[id] = true
return nil
}
// fakeDiscord is a tiny in-memory guild.
type fakeDiscord struct {
mu sync.Mutex
ownerID string
roles []*discordgo.Role
members map[string]*discordgo.Member
users map[string]*discordgo.User
channels map[string]*discordgo.Channel
kicked []string
banned []string
timeouts map[string]*time.Time
sent []*discordgo.MessageSend
sentTo []string
files map[string]string
requests []string // "METHOD url body"
deletedMsgs []string
nextID int
failEdits error
rejectRole string // edits that assign this role fail, like a role above Velox's
lookups int // member and role lookups, to check harmless entries cost nothing
closedDMs map[string]bool
guildName string
}
const guild = "100"
func newDiscord() *fakeDiscord {
return &fakeDiscord{
ownerID: "1",
roles: []*discordgo.Role{
{ID: guild, Name: "@everyone", Position: 0},
{ID: "200", Name: "Admin", Position: 5, Permissions: discordgo.PermissionAdministrator},
{ID: "201", Name: "Mod", Position: 3, Permissions: discordgo.PermissionKickMembers},
{ID: "202", Name: "Member", Position: 1},
{ID: "203", Name: "Quarantine", Position: 2},
{ID: "204", Name: "Booster", Position: 1, Managed: true},
{ID: "299", Name: "Velox", Position: 9, Managed: true, Permissions: discordgo.PermissionAdministrator},
},
members: map[string]*discordgo.Member{
"1": {User: &discordgo.User{ID: "1", Username: "owner"}},
"999": {User: &discordgo.User{ID: "999", Username: "velox", Bot: true}, Roles: []string{"299"}},
},
users: map[string]*discordgo.User{},
channels: map[string]*discordgo.Channel{},
timeouts: map[string]*time.Time{},
files: map[string]string{},
}
}
func (f *fakeDiscord) addMember(id string, roles ...string) *discordgo.Member {
f.mu.Lock()
defer f.mu.Unlock()
m := &discordgo.Member{User: &discordgo.User{ID: id, Username: "user" + id, Avatar: "a"}, Roles: roles}
f.members[id] = m
return m
}
func (f *fakeDiscord) Guild(g string, _ ...discordgo.RequestOption) (*discordgo.Guild, error) {
return &discordgo.Guild{ID: g, OwnerID: f.ownerID, Name: f.guildName}, nil
}
func (f *fakeDiscord) UserChannelCreate(u string, _ ...discordgo.RequestOption) (*discordgo.Channel, error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.closedDMs[u] {
return nil, errors.New("Cannot send messages to this user")
}
return &discordgo.Channel{ID: "dm-" + u, Type: discordgo.ChannelTypeDM}, nil
}
// dmsTo lists who got a DM, in order.
func (f *fakeDiscord) dmsTo() []string {
f.mu.Lock()
defer f.mu.Unlock()
var out []string
for _, ch := range f.sentTo {
if len(ch) > 3 && ch[:3] == "dm-" {
out = append(out, ch[3:])
}
}
return out
}
func (f *fakeDiscord) User(id string, _ ...discordgo.RequestOption) (*discordgo.User, error) {
f.mu.Lock()
defer f.mu.Unlock()
if u, ok := f.users[id]; ok {
return u, nil
}
return nil, errors.New("unknown user")
}
func (f *fakeDiscord) GuildRoles(string, ...discordgo.RequestOption) ([]*discordgo.Role, error) {
f.mu.Lock()
defer f.mu.Unlock()
f.lookups++
return f.roles, nil
}
func (f *fakeDiscord) GuildChannels(g string, _ ...discordgo.RequestOption) ([]*discordgo.Channel, error) {
f.mu.Lock()
defer f.mu.Unlock()
var out []*discordgo.Channel
for _, c := range f.channels {
if c.GuildID == g {
out = append(out, c)
}
}
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
return out, nil
}
func (f *fakeDiscord) GuildMember(_, u string, _ ...discordgo.RequestOption) (*discordgo.Member, error) {
f.mu.Lock()
defer f.mu.Unlock()
f.lookups++
if m, ok := f.members[u]; ok {
cp := *m
cp.Roles = append([]string(nil), m.Roles...)
return &cp, nil
}
return nil, errors.New("unknown member")
}
func (f *fakeDiscord) GuildMemberEdit(_, u string, data *discordgo.GuildMemberParams, _ ...discordgo.RequestOption) (*discordgo.Member, error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.failEdits != nil {
return nil, f.failEdits
}
if data.Roles != nil && f.rejectRole != "" {
for _, r := range *data.Roles {
if r == f.rejectRole {
return nil, errors.New("403 Missing Permissions")
}
}
}
m, ok := f.members[u]
if !ok {
return nil, errors.New("unknown member")
}
if data.Roles != nil {
m.Roles = append([]string(nil), (*data.Roles)...)
}
return m, nil
}
func (f *fakeDiscord) GuildMemberTimeout(_, u string, until *time.Time, _ ...discordgo.RequestOption) error {
f.mu.Lock()
defer f.mu.Unlock()
f.timeouts[u] = until
return nil
}
func (f *fakeDiscord) GuildMemberDeleteWithReason(_, u, _ string, _ ...discordgo.RequestOption) error {
f.mu.Lock()
defer f.mu.Unlock()
f.kicked = append(f.kicked, u)
delete(f.members, u)
return nil
}
func (f *fakeDiscord) GuildBanCreateWithReason(_, u, _ string, _ int, _ ...discordgo.RequestOption) error {
f.mu.Lock()
defer f.mu.Unlock()
f.banned = append(f.banned, u)
delete(f.members, u)
return nil
}
func (f *fakeDiscord) Channel(id string, _ ...discordgo.RequestOption) (*discordgo.Channel, error) {
f.mu.Lock()
defer f.mu.Unlock()
if c, ok := f.channels[id]; ok {
cp := *c
cp.PermissionOverwrites = nil
for _, o := range c.PermissionOverwrites {
oc := *o
cp.PermissionOverwrites = append(cp.PermissionOverwrites, &oc)
}
return &cp, nil
}
return nil, errors.New("unknown channel")
}
func (f *fakeDiscord) ChannelPermissionSet(ch, target string, t discordgo.PermissionOverwriteType, allow, deny int64, _ ...discordgo.RequestOption) error {
f.mu.Lock()
defer f.mu.Unlock()
c := f.channels[ch]
for _, o := range c.PermissionOverwrites {
if o.ID == target {
o.Allow, o.Deny = allow, deny
return nil
}
}
c.PermissionOverwrites = append(c.PermissionOverwrites, &discordgo.PermissionOverwrite{ID: target, Type: t, Allow: allow, Deny: deny})
return nil
}
func (f *fakeDiscord) ChannelPermissionDelete(ch, target string, _ ...discordgo.RequestOption) error {
f.mu.Lock()
defer f.mu.Unlock()
c := f.channels[ch]
var keep []*discordgo.PermissionOverwrite
for _, o := range c.PermissionOverwrites {
if o.ID != target {
keep = append(keep, o)
}
}
c.PermissionOverwrites = keep
return nil
}
func (f *fakeDiscord) ChannelMessageSendComplex(ch string, data *discordgo.MessageSend, _ ...discordgo.RequestOption) (*discordgo.Message, error) {
f.mu.Lock()
defer f.mu.Unlock()
f.sentTo = append(f.sentTo, ch)
for _, file := range data.Files {
raw, _ := io.ReadAll(file.Reader)
f.files[file.Name] = string(raw)
}
f.sent = append(f.sent, data)
return &discordgo.Message{ID: "m"}, nil
}
func (f *fakeDiscord) RequestWithBucketID(method, url string, data interface{}, _ string, _ ...discordgo.RequestOption) ([]byte, error) {
f.mu.Lock()
defer f.mu.Unlock()
f.requests = append(f.requests, fmt.Sprintf("%s %s %v", method, url, data))
return nil, nil
}
// sentCount counts channel alerts (not DMs).
func (f *fakeDiscord) sentCount() int {
return len(f.channelSends())
}
// channelSends lists messages sent to channels, leaving out DMs.
func (f *fakeDiscord) channelSends() []*discordgo.MessageSend {
f.mu.Lock()
defer f.mu.Unlock()
var out []*discordgo.MessageSend
for i, m := range f.sent {
if i >= len(f.sentTo) || len(f.sentTo[i]) < 3 || f.sentTo[i][:3] != "dm-" {
out = append(out, m)
}
}
return out
}
func (f *fakeDiscord) newID() string {
f.nextID++
return fmt.Sprint(8000 + f.nextID)
}
func (f *fakeDiscord) ChannelMessageDelete(ch, msg string, _ ...discordgo.RequestOption) error {
f.mu.Lock()
defer f.mu.Unlock()
f.deletedMsgs = append(f.deletedMsgs, msg)
return nil
}
func (f *fakeDiscord) GuildRoleCreate(_ string, p *discordgo.RoleParams, _ ...discordgo.RequestOption) (*discordgo.Role, error) {
f.mu.Lock()
defer f.mu.Unlock()
r := &discordgo.Role{ID: f.newID(), Name: p.Name}
if p.Permissions != nil {
r.Permissions = *p.Permissions
}
f.roles = append(f.roles, r)
return r, nil
}
func (f *fakeDiscord) GuildRoleEdit(_, roleID string, p *discordgo.RoleParams, _ ...discordgo.RequestOption) (*discordgo.Role, error) {
f.mu.Lock()
defer f.mu.Unlock()
for _, r := range f.roles {
if r.ID == roleID {
if p.Permissions != nil {
r.Permissions = *p.Permissions
}
return r, nil
}
}
return nil, errors.New("unknown role")
}
func (f *fakeDiscord) GuildChannelCreateComplex(g string, d discordgo.GuildChannelCreateData, _ ...discordgo.RequestOption) (*discordgo.Channel, error) {
f.mu.Lock()
defer f.mu.Unlock()
c := &discordgo.Channel{ID: f.newID(), GuildID: g, Name: d.Name, Type: d.Type, ParentID: d.ParentID, PermissionOverwrites: d.PermissionOverwrites}
f.channels[c.ID] = c
return c, nil
}