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]>
425 lines
11 KiB
Go
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
|
|
}
|