- /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]>
613 lines
17 KiB
Go
613 lines
17 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) 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
|
|
}
|