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 }