Beta server and role come from the dashboard

The owner sets them on the admin page; the bot polls the shared beta_settings
row (30s) and moves the beta commands if the server changes. BETA_* env vars
are gone.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
FernandoJVideiraandClaude Sonnet 5.5 committed 2026-10-23 00:55:09 +01:00
1 parent a364435bab
commit 1ee6280d91
7 files changed
+157 -24

No files matched your search

+57 -5
View File
@@ -27,6 +27,7 @@ type Bot struct {
DBHost string
LavalinkHost string
LavalinkPass string
betaGuild string // where beta commands are registered right now
}
func NewBot(token, appID, guildID, lavalinkHost, lavalinkPass string, cmds []*discordgo.ApplicationCommand, services *services.Services, musicRepo *musicrepo.Repo, dbHost string) (*Bot, error) {
@@ -114,8 +115,6 @@ func (b *Bot) Start() error {
b.registeredCommands = append(b.registeredCommands, created)
}
b.registerBetaCommands()
b.Session.AddHandler(commands.HandleInteraction)
b.Session.AddHandler(func(s *discordgo.Session, c *discordgo.ChannelDelete) {
@@ -203,12 +202,65 @@ func (b *Bot) Start() error {
return nil
}
// betaPollEvery is how often the bot looks for a change to the beta settings
// made on the dashboard.
const betaPollEvery = 30 * time.Second
// WatchBeta keeps the bot's idea of who the beta testers are in step with the
// settings the owner saves on the dashboard. When the beta server changes, the
// beta commands move with it. If the settings can't be read, the last known
// ones stay in force (and before the first read, beta stays locked).
func (b *Bot) WatchBeta(ctx context.Context, load func(context.Context) (guildID, roleID string, err error)) {
sync := func() {
guildID, roleID, err := load(ctx)
if err != nil {
log.Printf("beta: cannot read settings, keeping the last ones: %v", err)
return
}
beta.Configure(guildID, roleID)
if now := beta.GuildID(); now != b.betaGuild {
b.registerBetaCommands(b.betaGuild, now)
b.betaGuild = now
}
}
sync()
go func() {
t := time.NewTicker(betaPollEvery)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
sync()
}
}
}()
}
// registerBetaCommands puts the beta commands in the beta server, and only
// there, so nobody else sees them. Commands this bot left there that are no
// longer beta (or no longer exist) are removed. When the beta server is also
// the bot's main server, its regular commands are kept.
func (b *Bot) registerBetaCommands() {
guildID := beta.GuildID()
// the bot's main server, its regular commands are kept. If the beta server
// changed, the beta commands are taken out of the old one.
func (b *Bot) registerBetaCommands(previous, guildID string) {
if previous != "" && previous != guildID {
isBeta := map[string]bool{}
for _, cmd := range commands.BetaCommands {
isBeta[cmd.Name] = true
}
if old, err := b.Session.ApplicationCommands(b.AppID, previous); err != nil {
log.Printf("beta: cannot list commands in the previous server: %v", err)
} else {
for _, cmd := range old {
if isBeta[cmd.Name] {
if err := b.Session.ApplicationCommandDelete(b.AppID, previous, cmd.ID); err != nil {
log.Printf("beta: cannot remove '%v' from the previous server: %v", cmd.Name, err)
}
}
}
}
}
if guildID == "" {
return
}
-14
View File
@@ -7,16 +7,10 @@ import (
"github.com/joho/godotenv"
)
// DefaultBetaGuildID is the server that beta features are tried in, unless
// BETA_GUILD_ID says otherwise.
const DefaultBetaGuildID = "1170934850157547600"
type Config struct {
BotToken string
AppID string
GuildID string
BetaGuildID string // BETA_GUILD_ID: where beta features exist
BetaRoleID string // BETA_ROLE_ID: the Beta Tester role; empty keeps beta features locked for everyone
DBHost string
LavalinkHost string
LavalinkPass string
@@ -39,12 +33,6 @@ func LoadConfig() (*Config, error) {
guildID := os.Getenv("GUILD_ID")
betaGuildID := os.Getenv("BETA_GUILD_ID")
if betaGuildID == "" {
betaGuildID = DefaultBetaGuildID
}
betaRoleID := os.Getenv("BETA_ROLE_ID")
dbHost := os.Getenv("DB_HOST")
if dbHost == "" {
return nil, fmt.Errorf("DB_HOST is not set")
@@ -62,8 +50,6 @@ func LoadConfig() (*Config, error) {
BotToken: token,
AppID: appID,
GuildID: guildID,
BetaGuildID: betaGuildID,
BetaRoleID: betaRoleID,
DBHost: dbHost,
LavalinkHost: lavalinkHost,
LavalinkPass: lavalinkPass,
@@ -117,3 +117,66 @@ func TestMeetingLobbiesConcurrent(t *testing.T) {
t.Errorf("have %d lobbies, want %d", len(ids), MaxMeetingLobbies)
}
}
func TestGetBetaSettings(t *testing.T) {
r, _ := testRepo(t)
db := r.db
ctx := context.Background()
_, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS beta_settings (
id BOOLEAN PRIMARY KEY DEFAULT TRUE CHECK (id), guild_id BIGINT, role_id BIGINT, updated_by BIGINT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW())`)
if err != nil {
t.Fatal(err)
}
var wasG, wasR sql.NullInt64
hadRow := db.QueryRowContext(ctx, `SELECT guild_id, role_id FROM beta_settings WHERE id`).Scan(&wasG, &wasR) == nil
t.Cleanup(func() {
if hadRow {
_, _ = db.ExecContext(ctx, `UPDATE beta_settings SET guild_id=$1, role_id=$2 WHERE id`, wasG, wasR)
} else {
_, _ = db.ExecContext(ctx, `DELETE FROM beta_settings`)
}
})
_, err = db.ExecContext(ctx, `DELETE FROM beta_settings`)
if err != nil {
t.Fatal(err)
}
g, role, err := r.GetBetaSettings(ctx)
if err != nil {
t.Fatal(err)
}
if g != "" {
t.Fatalf("g = %q, want empty", g)
}
if role != "" {
t.Fatalf("role = %q, want empty", role)
}
_, err = db.ExecContext(ctx, `INSERT INTO beta_settings (id, guild_id) VALUES (TRUE, 1170934850157547600)`)
if err != nil {
t.Fatal(err)
}
g, role, err = r.GetBetaSettings(ctx)
if err != nil {
t.Fatal(err)
}
if g != "1170934850157547600" {
t.Fatalf("g = %q, want %q", g, "1170934850157547600")
}
if role != "" {
t.Fatalf("role = %q, want empty", role)
}
_, err = db.ExecContext(ctx, `UPDATE beta_settings SET role_id = 42`)
if err != nil {
t.Fatal(err)
}
_, role, err = r.GetBetaSettings(ctx)
if err != nil {
t.Fatal(err)
}
if role != "42" {
t.Fatalf("role = %q, want %q", role, "42")
}
}
+21
View File
@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"errors"
"strconv"
"github.com/jackc/pgx/v5/pgconn"
)
@@ -401,3 +402,23 @@ func (r *Repo) SetMusicEnabled(ctx context.Context, guildID int64, enabled bool)
_, err := r.db.ExecContext(ctx, q, guildID, enabled)
return err
}
// GetBetaSettings returns the beta server and Beta Tester role as set on the
// dashboard. Either is "" when not set (or when there's no row yet).
func (r *Repo) GetBetaSettings(ctx context.Context) (guildID, roleID string, err error) {
var g, role sql.NullInt64
err = r.db.QueryRowContext(ctx, `SELECT guild_id, role_id FROM beta_settings WHERE id = TRUE`).Scan(&g, &role)
if errors.Is(err, sql.ErrNoRows) {
return "", "", nil
}
if err != nil {
return "", "", err
}
if g.Valid {
guildID = strconv.FormatInt(g.Int64, 10)
}
if role.Valid {
roleID = strconv.FormatInt(role.Int64, 10)
}
return guildID, roleID, nil
}