Files
velox-bot/internal/db/repos/settingsrepo/repo.go
T
FernandoJVideiraandClaude Sonnet 5.5 1ee6280d91 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]>
2026-10-23 00:55:09 +01:00

425 lines
11 KiB
Go

package settingsrepo
import (
"context"
"database/sql"
"errors"
"strconv"
"github.com/jackc/pgx/v5/pgconn"
)
type Repo struct {
db *sql.DB
}
type LevelReward struct {
GuildID int64
Level int
RoleID int64
}
type LogSettings struct {
GuildID int64
ChannelID int64
IsEnabled bool
}
func NewRepo(db *sql.DB) *Repo {
return &Repo{db: db}
}
// MaxMeetingLobbies is how many lobby channels one server can have. The
// database enforces it too (a trigger from the API's migration 000020), so
// the command and the dashboard can't disagree.
const MaxMeetingLobbies = 10
// ErrMeetingLobbyLimit means the server already has MaxMeetingLobbies.
var ErrMeetingLobbyLimit = errors.New("meeting lobby limit reached")
// pgLimitExceeded is the error code the database raises for one lobby too many.
const pgLimitExceeded = "54000"
// AddMeetingLobby makes channelID a lobby. added is false when it already
// was one; going over the limit is ErrMeetingLobbyLimit.
func (r *Repo) AddMeetingLobby(ctx context.Context, guildID, channelID int64) (added bool, err error) {
const q = `
INSERT INTO meeting_lobbies (guild_id, channel_id)
VALUES ($1, $2)
ON CONFLICT DO NOTHING
`
res, err := r.db.ExecContext(ctx, q, guildID, channelID)
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == pgLimitExceeded {
return false, ErrMeetingLobbyLimit
}
if err != nil {
return false, err
}
n, err := res.RowsAffected()
return n > 0, err
}
// RemoveMeetingLobby takes a channel off the lobby list; removed is false
// when it wasn't one.
func (r *Repo) RemoveMeetingLobby(ctx context.Context, guildID, channelID int64) (removed bool, err error) {
const q = `DELETE FROM meeting_lobbies WHERE guild_id = $1 AND channel_id = $2`
res, err := r.db.ExecContext(ctx, q, guildID, channelID)
if err != nil {
return false, err
}
n, err := res.RowsAffected()
return n > 0, err
}
// ListMeetingLobbies returns a server's lobbies, oldest first.
func (r *Repo) ListMeetingLobbies(ctx context.Context, guildID int64) ([]int64, error) {
const q = `SELECT channel_id FROM meeting_lobbies WHERE guild_id = $1 ORDER BY created_at, channel_id`
rows, err := r.db.QueryContext(ctx, q, guildID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []int64
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
out = append(out, id)
}
return out, rows.Err()
}
// IsMeetingLobby reports whether channelID is one of the server's lobbies.
// It runs whenever someone joins a voice channel, so it's one indexed lookup.
func (r *Repo) IsMeetingLobby(ctx context.Context, guildID, channelID int64) (bool, error) {
const q = `SELECT EXISTS (SELECT 1 FROM meeting_lobbies WHERE guild_id = $1 AND channel_id = $2)`
var ok bool
err := r.db.QueryRowContext(ctx, q, guildID, channelID).Scan(&ok)
return ok, err
}
func (r *Repo) AddMeetingTempChannel(ctx context.Context, guildID, channelID, createdBy int64) error {
const q = `
INSERT INTO meeting_temp_channels (guild_id, channel_id, created_by)
VALUES ($1, $2, $3)
ON CONFLICT (guild_id, channel_id) DO NOTHING
`
_, err := r.db.ExecContext(ctx, q, guildID, channelID, createdBy)
return err
}
func (r *Repo) RemoveMeetingTempChannel(ctx context.Context, guildID, channelID int64) error {
const q = `
DELETE FROM meeting_temp_channels
WHERE guild_id = $1 AND channel_id = $2
`
_, err := r.db.ExecContext(ctx, q, guildID, channelID)
return err
}
func (r *Repo) IsMeetingTempChannel(ctx context.Context, guildID, channelID int64) (bool, error) {
const q = `
SELECT 1
FROM meeting_temp_channels
WHERE guild_id = $1 AND channel_id = $2
`
row := r.db.QueryRowContext(ctx, q, guildID, channelID)
var one int
switch err := row.Scan(&one); err {
case nil:
return true, nil
case sql.ErrNoRows:
return false, nil
default:
return false, err
}
}
func (r *Repo) GetMeetingTempChannelOwner(ctx context.Context, guildID, channelID int64) (createdBy int64, ok bool, err error) {
const q = `
SELECT created_by
FROM meeting_temp_channels
WHERE guild_id = $1 AND channel_id = $2
`
row := r.db.QueryRowContext(ctx, q, guildID, channelID)
var cb sql.NullInt64
switch err := row.Scan(&cb); err {
case nil:
if !cb.Valid {
return 0, false, nil
}
return cb.Int64, true, nil
case sql.ErrNoRows:
return 0, false, nil
default:
return 0, false, err
}
}
func (r *Repo) SetLevelSystemEnabled(ctx context.Context, guildID int64, enabled bool) error {
const q = `
INSERT INTO levelsettings (guild_id, levelsys)
VALUES ($1, $2)
ON CONFLICT (guild_id)
DO UPDATE SET levelsys = EXCLUDED.levelsys
`
_, err := r.db.ExecContext(ctx, q, guildID, enabled)
return err
}
func (r *Repo) IsLevelSystemEnabled(ctx context.Context, guildID int64) (bool, error) {
const q = `
SELECT levelsys
FROM levelsettings
WHERE guild_id = $1
`
row := r.db.QueryRowContext(ctx, q, guildID)
var enabled bool
switch err := row.Scan(&enabled); err {
case nil:
return enabled, nil
case sql.ErrNoRows:
return false, nil
default:
return false, err
}
}
func (r *Repo) GetLevelupChannelID(ctx context.Context, guildID int64) (int64, bool, error) {
const q = `
SELECT levelup_channel_id
FROM levelup
WHERE guild_id = $1
`
row := r.db.QueryRowContext(ctx, q, guildID)
var channelID sql.NullInt64
switch err := row.Scan(&channelID); err {
case nil:
if !channelID.Valid {
return 0, false, nil
}
return channelID.Int64, true, nil
case sql.ErrNoRows:
return 0, false, nil
default:
return 0, false, err
}
}
func (r *Repo) GetRoleRewardForLevel(ctx context.Context, guildID int64, level int) (roleID int64, ok bool, err error) {
const q = `
SELECT role
FROM levelrewards
WHERE guild_id = $1 AND levelreq = $2
`
row := r.db.QueryRowContext(ctx, q, guildID, level)
var role sql.NullInt64
switch err := row.Scan(&role); err {
case nil:
if !role.Valid {
return 0, false, nil
}
return role.Int64, true, nil
case sql.ErrNoRows:
return 0, false, nil
default:
return 0, false, err
}
}
func (r *Repo) SetRoleRewardForLevel(ctx context.Context, guildID int64, level int, roleID int64) error {
const q = `
INSERT INTO levelrewards (guild_id, levelreq, role)
VALUES ($1, $2, $3)
ON CONFLICT (guild_id, levelreq)
DO UPDATE SET role = EXCLUDED.role
`
_, err := r.db.ExecContext(ctx, q, guildID, level, roleID)
return err
}
func (r *Repo) ListRoleRewards(ctx context.Context, guildID int64, limit int) ([]*LevelReward, error) {
const q = `
SELECT levelreq, role
FROM levelrewards
WHERE guild_id = $1
ORDER BY levelreq ASC
LIMIT $2
`
rows, err := r.db.QueryContext(ctx, q, guildID, limit)
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]*LevelReward, 0, limit)
for rows.Next() {
rw := &LevelReward{GuildID: guildID}
if err := rows.Scan(&rw.Level, &rw.RoleID); err != nil {
return nil, err
}
out = append(out, rw)
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
}
func (r *Repo) SetLevelUpChannel(ctx context.Context, guildID int64, channelID int64) error {
const q = `
INSERT INTO levelup (guild_id, levelup_channel_id)
VALUES ($1, $2)
ON CONFLICT (guild_id)
DO UPDATE SET levelup_channel_id = EXCLUDED.levelup_channel_id
`
_, err := r.db.ExecContext(ctx, q, guildID, channelID)
return err
}
func (r *Repo) SetLevelUpMessage(ctx context.Context, guildID int64, message string) error {
const q = `
UPDATE levelsettings
SET message = $1
WHERE guild_id = $2
`
_, err := r.db.ExecContext(ctx, q, message, guildID)
return err
}
func (r *Repo) SetLogChannel(ctx context.Context, guildID int64, channelID int64) error {
const q = `
INSERT INTO logsettings (guild_id, log_channel_id, is_enabled)
VALUES ($1, $2, TRUE)
ON CONFLICT (guild_id)
DO UPDATE SET log_channel_id = EXCLUDED.log_channel_id
`
_, err := r.db.ExecContext(ctx, q, guildID, channelID)
return err
}
func (r *Repo) SetLogEnabled(ctx context.Context, guildID int64, enabled bool) error {
const q = `
INSERT INTO logsettings (guild_id, is_enabled)
VALUES ($1, $2)
ON CONFLICT (guild_id)
DO UPDATE SET is_enabled = EXCLUDED.is_enabled
`
_, err := r.db.ExecContext(ctx, q, guildID, enabled)
return err
}
func (r *Repo) GetLogSettings(ctx context.Context, guildID int64) (*LogSettings, error) {
const q = `
SELECT log_channel_id, is_enabled
FROM logsettings
WHERE guild_id = $1
`
row := r.db.QueryRowContext(ctx, q, guildID)
var channelID sql.NullInt64
var enabled sql.NullBool
switch err := row.Scan(&channelID, &enabled); err {
case nil:
out := &LogSettings{
GuildID: guildID,
ChannelID: 0,
IsEnabled: false,
}
if channelID.Valid {
out.ChannelID = channelID.Int64
}
if enabled.Valid {
out.IsEnabled = enabled.Bool
}
return out, nil
case sql.ErrNoRows:
return &LogSettings{
GuildID: guildID,
ChannelID: 0,
IsEnabled: false,
}, nil
default:
return nil, err
}
}
func (r *Repo) HasLogSettings(ctx context.Context, guildID int64) (bool, error) {
const q = `
SELECT 1
FROM logsettings
WHERE guild_id = $1
`
row := r.db.QueryRowContext(ctx, q, guildID)
var one int
switch err := row.Scan(&one); err {
case nil:
return true, nil
case sql.ErrNoRows:
return false, nil
default:
return false, err
}
}
func (r *Repo) GetLevelUpMessage(ctx context.Context, guildID int64) (string, error) {
const q = `
SELECT message
FROM levelsettings
WHERE guild_id = $1
`
row := r.db.QueryRowContext(ctx, q, guildID)
var message sql.NullString
switch err := row.Scan(&message); err {
case nil:
if !message.Valid {
return "GG {user}, you reached level {level}!", nil
}
return message.String, nil
case sql.ErrNoRows:
return "GG {user}, you reached level {level}!", nil
default:
return "", err
}
}
func (r *Repo) IsMusicEnabled(ctx context.Context, guildID int64) (bool, error) {
const q = `SELECT is_enabled FROM music_settings WHERE guild_id = $1`
var enabled bool
err := r.db.QueryRowContext(ctx, q, guildID).Scan(&enabled)
if err == sql.ErrNoRows {
return false, nil
}
if err != nil {
return false, err
}
return enabled, nil
}
func (r *Repo) SetMusicEnabled(ctx context.Context, guildID int64, enabled bool) error {
const q = `INSERT INTO music_settings (guild_id, is_enabled) VALUES ($1, $2) ON CONFLICT (guild_id) DO UPDATE SET is_enabled = $2`
_, 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
}