v3.5.2 (beta in other servers) #28
No files matched your search
@@ -84,7 +84,7 @@ Some commands have a response that takes longer than three seconds (anything tha
|
||||
| Schedule reminders | `events/schedule_reminders.go` | 1 min | DMs participants 10 minutes before a session |
|
||||
| Raid watch | `security` | 30 s | Ends raid mode after a quiet period |
|
||||
| Snapshots | `security` | 6 h | Saves roles and channels while anti-nuke is on (keeps three) |
|
||||
| Beta settings | `bot/bot.go` (`WatchBeta`) | 30 s | Re-reads who the beta testers are |
|
||||
| Beta settings | `bot/bot.go` (`WatchBeta`) | 30 s | Re-reads who the beta testers are and which servers opted in, and adds or removes beta commands to match |
|
||||
| Command listeners | music, tickets, security | on `NOTIFY` | Carry out dashboard requests |
|
||||
|
||||
## The link to the dashboard
|
||||
|
||||
@@ -37,7 +37,7 @@ Code: `internal/beta` (the rules), `internal/commands/commands.go` (`RegisterBet
|
||||
- For a person using a command in another server, the bot looks up their roles in the beta server: first in its gateway cache, otherwise it asks Discord (remembered for 30 seconds). If anything fails, the answer is no.
|
||||
- If the owner changes the beta server on the dashboard, the beta commands move within 30 seconds. In the beta server, commands this bot left there that no longer exist are removed too. If the beta server is also `GUILD_ID`, the regular commands are kept.
|
||||
- `/beta` shows the bot's owner how access is set up. Only the owner of the Discord application can run it, and it is hidden from everyone but administrators.
|
||||
- A server that opted out while the bot was **offline** keeps its beta commands in the menu until someone removes them. Using one still fails, because the gate checks every time.
|
||||
- The bot remembers the servers where it registered beta commands (`beta_registered` table). A server that opted out while the bot was **offline** is cleaned up on the next start, and a cleanup that fails is retried every 30 seconds. A server the bot has left is forgotten.
|
||||
|
||||
### Add a beta command
|
||||
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ Two things only exist in the API's migrations, because the bot never inserts int
|
||||
| Music | `music_settings`, `music_now_playing`, `music_queue`, `music_commands` | State for the dashboard's player, plus requests from the dashboard |
|
||||
| Tickets | `ticket_settings`, `ticket_categories`, `tickets`, `ticket_commands` | |
|
||||
| Security | `security_settings`, `security_trusted`, `security_quarantines`, `security_lockdowns`, `security_raids`, `security_raid_caught`, `security_snapshots`, `security_events`, `security_commands` | Settings, who is trusted, quarantines (with the roles to give back), locked channels (with the permission to restore), raids in progress, snapshots, an event history, and dashboard requests |
|
||||
| Beta | `beta_settings`, `beta_guilds` | `beta_settings` is one row: the beta server and the Beta Tester role. `beta_guilds` lists servers whose owner opted in to beta features. Written by the dashboard, read by the bot |
|
||||
| Beta | `beta_settings`, `beta_guilds` | `beta_settings` is one row: the beta server and the Beta Tester role. `beta_guilds` lists servers whose owner opted in to beta features. Both are written by the dashboard and read by the bot. `beta_registered` is the bot's own note of where it registered beta commands |
|
||||
|
||||
Discord IDs are `BIGINT` (snowflakes fit in 64 bits). In Go they are `int64`; Discord's own API gives them to you as strings, so convert at the edge.
|
||||
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"velox-bot/internal/beta"
|
||||
"velox-bot/internal/commands"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
const (
|
||||
appID = "100"
|
||||
betaServer = "1170934850157547600"
|
||||
optedIn = "222"
|
||||
leftServer = "333"
|
||||
)
|
||||
|
||||
// fakeDiscord answers the application-command calls the beta sync makes, from
|
||||
// memory, and records what it was asked to change.
|
||||
var registerOnce sync.Once
|
||||
|
||||
type fakeDiscord struct {
|
||||
mu sync.Mutex
|
||||
cmds map[string][]*discordgo.ApplicationCommand // guild -> commands
|
||||
failures map[string]int // guild -> status for every call
|
||||
nextID int
|
||||
created []string // "guild/name"
|
||||
deleted []string // "guild/name"
|
||||
}
|
||||
|
||||
var cmdPath = regexp.MustCompile(`/applications/` + appID + `/guilds/(\d+)/commands(?:/(\d+))?$`)
|
||||
|
||||
func (f *fakeDiscord) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
reply := func(code int, v any) (*http.Response, error) {
|
||||
b, _ := json.Marshal(v)
|
||||
return &http.Response{StatusCode: code, Body: io.NopCloser(strings.NewReader(string(b))), Header: http.Header{"Content-Type": {"application/json"}}, Request: r}, nil
|
||||
}
|
||||
m := cmdPath.FindStringSubmatch(r.URL.Path)
|
||||
if m == nil {
|
||||
return reply(404, map[string]any{"message": "unknown route"})
|
||||
}
|
||||
guild, id := m[1], m[2]
|
||||
if code := f.failures[guild]; code != 0 {
|
||||
return reply(code, map[string]any{"message": "failure", "code": 0})
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
return reply(200, f.cmds[guild])
|
||||
case http.MethodPost:
|
||||
var c discordgo.ApplicationCommand
|
||||
_ = json.NewDecoder(r.Body).Decode(&c)
|
||||
f.nextID++
|
||||
c.ID = strconv.Itoa(1000 + f.nextID)
|
||||
f.cmds[guild] = append(f.cmds[guild], &c)
|
||||
f.created = append(f.created, guild+"/"+c.Name)
|
||||
return reply(200, c)
|
||||
case http.MethodDelete:
|
||||
kept := f.cmds[guild][:0]
|
||||
for _, c := range f.cmds[guild] {
|
||||
if c.ID == id {
|
||||
f.deleted = append(f.deleted, guild+"/"+c.Name)
|
||||
continue
|
||||
}
|
||||
kept = append(kept, c)
|
||||
}
|
||||
f.cmds[guild] = kept
|
||||
return reply(204, nil)
|
||||
}
|
||||
return reply(405, nil)
|
||||
}
|
||||
|
||||
func (f *fakeDiscord) names(guild string) []string {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
var out []string
|
||||
for _, c := range f.cmds[guild] {
|
||||
out = append(out, c.Name)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
type fakeStore struct {
|
||||
registered map[string]bool
|
||||
servers []string
|
||||
failWrite bool
|
||||
}
|
||||
|
||||
func (s *fakeStore) GetBetaSettings(context.Context) (string, string, error) {
|
||||
return betaServer, "555", nil
|
||||
}
|
||||
func (s *fakeStore) ListBetaGuilds(context.Context) ([]string, error) { return s.servers, nil }
|
||||
func (s *fakeStore) ListBetaRegistered(context.Context) ([]string, error) {
|
||||
var out []string
|
||||
for id := range s.registered {
|
||||
out = append(out, id)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func (s *fakeStore) SetBetaRegistered(_ context.Context, id string, on bool) error {
|
||||
if s.failWrite {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
if on {
|
||||
s.registered[id] = true
|
||||
} else {
|
||||
delete(s.registered, id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newTestBot(t *testing.T, d *fakeDiscord) *Bot {
|
||||
t.Helper()
|
||||
sess, err := discordgo.New("Bot x")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sess.Client = &http.Client{Transport: d}
|
||||
sess.MaxRestRetries = 0
|
||||
return &Bot{
|
||||
Session: sess,
|
||||
AppID: appID,
|
||||
betaRegistered: map[string]bool{},
|
||||
betaKnown: map[string]bool{},
|
||||
}
|
||||
}
|
||||
|
||||
func contains(list []string, v string) bool {
|
||||
for _, x := range list {
|
||||
if x == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestBetaCommandsFollowTheServers(t *testing.T) {
|
||||
registerOnce.Do(func() {
|
||||
commands.RegisterBeta(&discordgo.ApplicationCommand{Name: "betatest", Description: "test"}, func(*discordgo.Session, *discordgo.InteractionCreate) {})
|
||||
})
|
||||
d := &fakeDiscord{cmds: map[string][]*discordgo.ApplicationCommand{
|
||||
// A server that opted out while the bot was offline still has them,
|
||||
// next to a command that isn't a beta one.
|
||||
leftServer: {{ID: "1", Name: "betatest"}, {ID: "2", Name: "ping"}},
|
||||
// A stale command in the beta server, which the bot cleans up.
|
||||
betaServer: {{ID: "3", Name: "oldthing"}},
|
||||
}, failures: map[string]int{}}
|
||||
store := &fakeStore{registered: map[string]bool{leftServer: true}, servers: []string{optedIn}}
|
||||
b := newTestBot(t, d)
|
||||
ctx := context.Background()
|
||||
|
||||
beta.Configure(betaServer, "555", store.servers)
|
||||
b.syncBetaCommands(ctx, store)
|
||||
|
||||
if got := d.names(leftServer); len(got) != 1 || got[0] != "ping" {
|
||||
t.Errorf("the beta command is removed from the server that left, nothing else: %v", got)
|
||||
}
|
||||
if got := d.names(optedIn); len(got) != 1 || got[0] != "betatest" {
|
||||
t.Errorf("an opted-in server gets the beta commands but not /beta: %v", got)
|
||||
}
|
||||
got := d.names(betaServer)
|
||||
if !contains(got, "beta") || !contains(got, "betatest") || contains(got, "oldthing") {
|
||||
t.Errorf("the beta server gets /beta and the beta commands, and loses stale ones: %v", got)
|
||||
}
|
||||
if store.registered[leftServer] || !store.registered[optedIn] || !store.registered[betaServer] {
|
||||
t.Errorf("the database remembers where the commands are now: %v", store.registered)
|
||||
}
|
||||
|
||||
// The server opts out.
|
||||
store.servers = nil
|
||||
beta.Configure(betaServer, "555", store.servers)
|
||||
b.syncBetaCommands(ctx, store)
|
||||
if got := d.names(optedIn); len(got) != 0 {
|
||||
t.Errorf("an opted-out server loses them within one round: %v", got)
|
||||
}
|
||||
if store.registered[optedIn] || !store.registered[betaServer] {
|
||||
t.Errorf("only the beta server stays remembered: %v", store.registered)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBetaCleanupIsRetriedAndGivesUpWhenTheBotIsGone(t *testing.T) {
|
||||
d := &fakeDiscord{cmds: map[string][]*discordgo.ApplicationCommand{
|
||||
leftServer: {{ID: "1", Name: "betatest"}},
|
||||
}, failures: map[string]int{leftServer: 500}}
|
||||
store := &fakeStore{registered: map[string]bool{leftServer: true}}
|
||||
b := newTestBot(t, d)
|
||||
ctx := context.Background()
|
||||
beta.Configure(betaServer, "555", nil)
|
||||
|
||||
b.syncBetaCommands(ctx, store)
|
||||
if !store.registered[leftServer] {
|
||||
t.Fatal("a failed cleanup is remembered so it is tried again")
|
||||
}
|
||||
|
||||
d.failures[leftServer] = 0
|
||||
b.syncBetaCommands(ctx, store)
|
||||
if store.registered[leftServer] || len(d.names(leftServer)) != 0 {
|
||||
t.Errorf("the retry cleans up: %v, %v", store.registered, d.names(leftServer))
|
||||
}
|
||||
|
||||
// The bot was removed from a server: nothing to clean, don't try forever.
|
||||
store.registered["444"] = true
|
||||
b.betaKnown["444"] = true
|
||||
d.failures["444"] = 403
|
||||
b.syncBetaCommands(ctx, store)
|
||||
if store.registered["444"] {
|
||||
t.Error("a server the bot can't reach any more is forgotten")
|
||||
}
|
||||
}
|
||||
+86
-19
@@ -2,7 +2,9 @@ package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
"velox-bot/internal/beta"
|
||||
@@ -29,8 +31,10 @@ type Bot struct {
|
||||
DBHost string
|
||||
LavalinkHost string
|
||||
LavalinkPass string
|
||||
betaRegistered map[string]bool // servers where the beta commands are registered right now
|
||||
betaRoles sync.Map // "guild:user" -> cachedRoles
|
||||
betaRegistered map[string]bool // servers where this run registered the beta commands
|
||||
betaKnown map[string]bool // servers where they were registered, now or in an earlier run
|
||||
betaLoaded bool
|
||||
betaRoles sync.Map // "guild:user" -> cachedRoles
|
||||
}
|
||||
|
||||
func NewBot(token, appID, guildID, lavalinkHost, lavalinkPass string, cmds []*discordgo.ApplicationCommand, services *services.Services, musicRepo *musicrepo.Repo, dbHost string) (*Bot, error) {
|
||||
@@ -54,6 +58,7 @@ func NewBot(token, appID, guildID, lavalinkHost, lavalinkPass string, cmds []*di
|
||||
|
||||
return &Bot{
|
||||
betaRegistered: map[string]bool{},
|
||||
betaKnown: map[string]bool{},
|
||||
Session: session,
|
||||
AppID: appID,
|
||||
GuildID: guildID,
|
||||
@@ -210,21 +215,37 @@ func (b *Bot) Start() error {
|
||||
// made on the dashboard.
|
||||
const betaPollEvery = 30 * time.Second
|
||||
|
||||
// BetaStore is where the bot reads the beta settings the dashboard writes,
|
||||
// and remembers which servers it registered beta commands in.
|
||||
type BetaStore interface {
|
||||
GetBetaSettings(ctx context.Context) (guildID, roleID string, err error)
|
||||
ListBetaGuilds(ctx context.Context) ([]string, error)
|
||||
ListBetaRegistered(ctx context.Context) ([]string, error)
|
||||
SetBetaRegistered(ctx context.Context, guildID string, registered bool) error
|
||||
}
|
||||
|
||||
// WatchBeta keeps the bot's idea of who the beta testers are, and which
|
||||
// servers opted in, in step with the dashboard. When the list of servers
|
||||
// changes, the beta commands follow: they appear in servers that opted in and
|
||||
// are removed from servers that opted out. If the settings can't be read, the
|
||||
// last known ones stay in force (and before the first read, beta stays off).
|
||||
func (b *Bot) WatchBeta(ctx context.Context, load func(context.Context) (guildID, roleID string, servers []string, err error)) {
|
||||
// are removed from servers that opted out, also if that happened while the bot
|
||||
// was offline (it remembers where it registered them). If the settings can't
|
||||
// be read, the last known ones stay in force (and before the first read, beta
|
||||
// stays off).
|
||||
func (b *Bot) WatchBeta(ctx context.Context, store BetaStore) {
|
||||
beta.SetRolesLookup(b.betaMemberRoles)
|
||||
sync := func() {
|
||||
guildID, roleID, servers, err := load(ctx)
|
||||
guildID, roleID, err := store.GetBetaSettings(ctx)
|
||||
if err != nil {
|
||||
log.Printf("beta: cannot read settings, keeping the last ones: %v", err)
|
||||
return
|
||||
}
|
||||
servers, err := store.ListBetaGuilds(ctx)
|
||||
if err != nil {
|
||||
log.Printf("beta: cannot read the opted-in servers, keeping the last ones: %v", err)
|
||||
return
|
||||
}
|
||||
beta.Configure(guildID, roleID, servers)
|
||||
b.syncBetaCommands()
|
||||
b.syncBetaCommands(ctx, store)
|
||||
}
|
||||
sync()
|
||||
go func() {
|
||||
@@ -274,8 +295,22 @@ func (b *Bot) betaMemberRoles(guildID, userID string) ([]string, error) {
|
||||
|
||||
// syncBetaCommands makes the beta commands exist in exactly the servers where
|
||||
// beta features are on (the beta server and the ones that opted in), and
|
||||
// nowhere else. A server whose registration failed is retried next time.
|
||||
func (b *Bot) syncBetaCommands() {
|
||||
// nowhere else. The servers it registered them in are remembered in the
|
||||
// database, so a server that opted out while the bot was offline is cleaned up
|
||||
// at the next start. A step that failed is retried on the next round.
|
||||
func (b *Bot) syncBetaCommands(ctx context.Context, store BetaStore) {
|
||||
if !b.betaLoaded {
|
||||
known, err := store.ListBetaRegistered(ctx)
|
||||
if err != nil {
|
||||
log.Printf("beta: cannot read where beta commands were registered: %v", err)
|
||||
return
|
||||
}
|
||||
for _, id := range known {
|
||||
b.betaKnown[id] = true
|
||||
}
|
||||
b.betaLoaded = true
|
||||
}
|
||||
|
||||
want := map[string]bool{}
|
||||
if g := beta.GuildID(); g != "" {
|
||||
want[g] = true
|
||||
@@ -283,16 +318,30 @@ func (b *Bot) syncBetaCommands() {
|
||||
for _, id := range beta.Servers() {
|
||||
want[id] = true
|
||||
}
|
||||
for id := range b.betaRegistered {
|
||||
if !want[id] {
|
||||
b.removeBetaCommands(id)
|
||||
delete(b.betaRegistered, id)
|
||||
for id := range b.betaKnown {
|
||||
if want[id] {
|
||||
continue
|
||||
}
|
||||
if !b.removeBetaCommands(id) {
|
||||
continue
|
||||
}
|
||||
if err := store.SetBetaRegistered(ctx, id, false); err != nil {
|
||||
log.Printf("beta: cannot forget server %s: %v", id, err)
|
||||
continue
|
||||
}
|
||||
delete(b.betaKnown, id)
|
||||
delete(b.betaRegistered, id)
|
||||
}
|
||||
for id := range want {
|
||||
if !b.betaRegistered[id] && b.addBetaCommands(id) {
|
||||
b.betaRegistered[id] = true
|
||||
if b.betaRegistered[id] || !b.addBetaCommands(id) {
|
||||
continue
|
||||
}
|
||||
b.betaRegistered[id] = true
|
||||
if err := store.SetBetaRegistered(ctx, id, true); err != nil {
|
||||
log.Printf("beta: cannot remember server %s: %v", id, err)
|
||||
continue
|
||||
}
|
||||
b.betaKnown[id] = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,26 +388,44 @@ func (b *Bot) addBetaCommands(guildID string) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// goneFromServer reports whether an error from Discord means the bot isn't in
|
||||
// that server any more (or it no longer exists), so there is nothing left to
|
||||
// clean up there.
|
||||
func goneFromServer(err error) bool {
|
||||
var rest *discordgo.RESTError
|
||||
if errors.As(err, &rest) && rest.Response != nil {
|
||||
return rest.Response.StatusCode == http.StatusForbidden || rest.Response.StatusCode == http.StatusNotFound
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// removeBetaCommands takes the beta commands out of a server that is no longer
|
||||
// part of the beta, and leaves everything else there alone.
|
||||
func (b *Bot) removeBetaCommands(guildID string) {
|
||||
// part of the beta, and leaves everything else there alone. It reports whether
|
||||
// the server is clean (or out of reach for good).
|
||||
func (b *Bot) removeBetaCommands(guildID string) bool {
|
||||
isBeta := map[string]bool{}
|
||||
for _, cmd := range commands.BetaCommands {
|
||||
isBeta[cmd.Name] = true
|
||||
}
|
||||
existing, err := b.Session.ApplicationCommands(b.AppID, guildID)
|
||||
if err != nil {
|
||||
if goneFromServer(err) {
|
||||
return true
|
||||
}
|
||||
log.Printf("beta: cannot list commands in %s: %v", guildID, err)
|
||||
return
|
||||
return false
|
||||
}
|
||||
clean := true
|
||||
for _, cmd := range existing {
|
||||
if !isBeta[cmd.Name] {
|
||||
continue
|
||||
}
|
||||
if err := b.Session.ApplicationCommandDelete(b.AppID, guildID, cmd.ID); err != nil {
|
||||
if err := b.Session.ApplicationCommandDelete(b.AppID, guildID, cmd.ID); err != nil && !goneFromServer(err) {
|
||||
log.Printf("beta: cannot remove '%v' from %s: %v", cmd.Name, guildID, err)
|
||||
clean = false
|
||||
}
|
||||
}
|
||||
return clean
|
||||
}
|
||||
|
||||
func (b *Bot) Close() error {
|
||||
|
||||
@@ -215,3 +215,44 @@ func TestListBetaGuilds(t *testing.T) {
|
||||
t.Fatal("listed after opting in")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBetaRegistered(t *testing.T) {
|
||||
r, _ := testRepo(t)
|
||||
ctx := context.Background()
|
||||
if _, err := r.db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS beta_registered (guild_id BIGINT PRIMARY KEY)`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const g = "8800000000000005151"
|
||||
_ = r.SetBetaRegistered(ctx, g, false)
|
||||
t.Cleanup(func() { _ = r.SetBetaRegistered(ctx, g, false) })
|
||||
|
||||
has := func() bool {
|
||||
ids, err := r.ListBetaRegistered(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, id := range ids {
|
||||
if id == g {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if has() {
|
||||
t.Fatal("not remembered at first")
|
||||
}
|
||||
for i := 0; i < 2; i++ { // twice: harmless
|
||||
if err := r.SetBetaRegistered(ctx, g, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if !has() {
|
||||
t.Fatal("remembered after registering")
|
||||
}
|
||||
if err := r.SetBetaRegistered(ctx, g, false); err != nil || has() {
|
||||
t.Fatalf("forgotten after removing: %v", err)
|
||||
}
|
||||
if err := r.SetBetaRegistered(ctx, "not-a-number", true); err == nil {
|
||||
t.Error("a bad ID is refused")
|
||||
}
|
||||
}
|
||||
@@ -441,3 +441,37 @@ func (r *Repo) ListBetaGuilds(ctx context.Context) ([]string, error) {
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// ListBetaRegistered returns the servers where the bot registered beta
|
||||
// commands, so it can clean up after itself even if a server opted out while
|
||||
// it was offline.
|
||||
func (r *Repo) ListBetaRegistered(ctx context.Context) ([]string, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `SELECT guild_id FROM beta_registered`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []string
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, strconv.FormatInt(id, 10))
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// SetBetaRegistered records (or forgets) that beta commands exist in a server.
|
||||
func (r *Repo) SetBetaRegistered(ctx context.Context, guildID string, registered bool) error {
|
||||
id, err := strconv.ParseInt(guildID, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if registered {
|
||||
_, err = r.db.ExecContext(ctx, `INSERT INTO beta_registered (guild_id) VALUES ($1) ON CONFLICT DO NOTHING`, id)
|
||||
} else {
|
||||
_, err = r.db.ExecContext(ctx, `DELETE FROM beta_registered WHERE guild_id = $1`, id)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -94,14 +94,7 @@ func main() {
|
||||
defer bot.Close()
|
||||
|
||||
// Who the beta testers are is set on the dashboard; follow it.
|
||||
bot.WatchBeta(context.Background(), func(ctx context.Context) (string, string, []string, error) {
|
||||
guildID, roleID, err := settingsRepo.GetBetaSettings(ctx)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
servers, err := settingsRepo.ListBetaGuilds(ctx)
|
||||
return guildID, roleID, servers, err
|
||||
})
|
||||
bot.WatchBeta(context.Background(), settingsRepo)
|
||||
|
||||
fmt.Println("Bot is running...")
|
||||
|
||||
|
||||
@@ -92,6 +92,13 @@ CREATE TABLE IF NOT EXISTS beta_guilds (
|
||||
enabled_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Servers where the bot registered beta commands, so it can remove them even
|
||||
-- if the server opted out while the bot was offline. Bot-owned (the API's
|
||||
-- migration 000022 creates it for a combined database).
|
||||
CREATE TABLE IF NOT EXISTS beta_registered (
|
||||
guild_id BIGINT PRIMARY KEY
|
||||
);
|
||||
|
||||
-- A server can have several lobbies (at most 10; the trigger enforces it, and
|
||||
-- is the same as the API's migration 000020). meeting_settings.lobby_channel_id
|
||||
-- is the old single-lobby column, no longer read.
|
||||
|
||||
Reference in new issue
Block a user