Commands for everything a dashboard can do
- /music skip, pause, resume, stop, repeat, remove, move (DJ role) - /config view, clear, toggletwitch, streamers, beta-features (server owner) - /slvl status, remove-reward; /moderation bans, timeouts; /security history - /beta is now the bot owner's DM-only command: set, off, servers, remove-server, status - betasettings service and repo methods, with integration tests Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
acdb5ead35
commit
132a2cd986
26 files changed
+1062
-65
No files matched your search
@@ -509,11 +509,12 @@ func (r *Repo) DeleteLockdown(ctx context.Context, guildID, channelID int64) err
|
||||
// ---- Events ----
|
||||
|
||||
type Event struct {
|
||||
GuildID int64
|
||||
Kind string
|
||||
ActorID int64
|
||||
TargetID int64
|
||||
Detail string
|
||||
GuildID int64
|
||||
Kind string
|
||||
ActorID int64
|
||||
TargetID int64
|
||||
Detail string
|
||||
CreatedAt time.Time // set when read
|
||||
}
|
||||
|
||||
// Event kinds.
|
||||
@@ -716,3 +717,26 @@ func (r *Repo) GuildsDueForSnapshot(ctx context.Context, maxAge time.Duration) (
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// ListEvents returns a server's most recent security events, newest first.
|
||||
func (r *Repo) ListEvents(ctx context.Context, guildID int64, limit int) ([]Event, error) {
|
||||
if limit < 1 || limit > 100 {
|
||||
limit = 20
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT kind, COALESCE(actor_id, 0), COALESCE(target_id, 0), detail, created_at
|
||||
FROM security_events WHERE guild_id = $1 ORDER BY id DESC LIMIT $2`, guildID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []Event
|
||||
for rows.Next() {
|
||||
e := Event{GuildID: guildID}
|
||||
if err := rows.Scan(&e.Kind, &e.ActorID, &e.TargetID, &e.Detail, &e.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, e)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
@@ -229,3 +229,27 @@ func TestRaidsSnapshotsAndHeatSettings(t *testing.T) {
|
||||
t.Error("with a zero max age every anti-nuke guild is due")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListEvents(t *testing.T) {
|
||||
r, g := testRepo(t)
|
||||
ctx := context.Background()
|
||||
for _, kind := range []string{"settings", "raid_started", "quarantine"} {
|
||||
if err := r.AddEvent(ctx, Event{GuildID: g, Kind: kind, ActorID: 5, Detail: "d"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := r.AddEvent(ctx, Event{GuildID: g + 1, Kind: "other server"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _, _ = r.db.Exec(`DELETE FROM security_events WHERE guild_id = $1`, g+1) })
|
||||
got, err := r.ListEvents(ctx, g, 2)
|
||||
if err != nil || len(got) != 2 || got[0].Kind != "quarantine" || got[1].Kind != "raid_started" {
|
||||
t.Fatalf("newest first, limited: %+v %v", got, err)
|
||||
}
|
||||
if got[0].CreatedAt.IsZero() || got[0].ActorID != 5 || got[0].TargetID != 0 {
|
||||
t.Errorf("fields: %+v", got[0])
|
||||
}
|
||||
if all, _ := r.ListEvents(ctx, g, 0); len(all) != 3 {
|
||||
t.Errorf("only this server's events: %v", all)
|
||||
}
|
||||
}
|
||||
@@ -256,3 +256,66 @@ func TestBetaRegistered(t *testing.T) {
|
||||
t.Error("a bad ID is refused")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBetaSettingsFromCommands(t *testing.T) {
|
||||
r, _ := testRepo(t)
|
||||
ctx := context.Background()
|
||||
var wasG, wasR sql.NullInt64
|
||||
hadRow := r.db.QueryRowContext(ctx, `SELECT guild_id, role_id FROM beta_settings WHERE id`).Scan(&wasG, &wasR) == nil
|
||||
t.Cleanup(func() {
|
||||
if hadRow {
|
||||
_, _ = r.db.ExecContext(ctx, `UPDATE beta_settings SET guild_id=$1, role_id=$2 WHERE id`, wasG, wasR)
|
||||
} else {
|
||||
_, _ = r.db.ExecContext(ctx, `DELETE FROM beta_settings`)
|
||||
}
|
||||
_ = r.SetBetaGuild(ctx, 8800000000000007171, 1, false)
|
||||
})
|
||||
|
||||
if err := r.SetBetaServer(ctx, 700000000000000001, 800000000000000002, 5); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
g, role, err := r.GetBetaSettings(ctx)
|
||||
if err != nil || g != "700000000000000001" || role != "800000000000000002" {
|
||||
t.Fatalf("settings: %q %q %v", g, role, err)
|
||||
}
|
||||
if err := r.SetBetaServer(ctx, 700000000000000001, 0, 5); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g, role, _ = r.GetBetaSettings(ctx); g != "700000000000000001" || role != "" {
|
||||
t.Errorf("a zero role clears the role and keeps the server: %q %q", g, role)
|
||||
}
|
||||
|
||||
const guild = 8800000000000007171
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := r.SetBetaGuild(ctx, guild, 9, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if on, _ := r.IsBetaGuild(ctx, guild); !on {
|
||||
t.Error("opted in")
|
||||
}
|
||||
if err := r.SetBetaGuild(ctx, guild, 9, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if on, _ := r.IsBetaGuild(ctx, guild); on {
|
||||
t.Error("opted out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteRoleReward(t *testing.T) {
|
||||
r, g := testRepo(t)
|
||||
ctx := context.Background()
|
||||
if err := r.SetRoleRewardForLevel(ctx, g, 5, 77); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _, _ = r.db.ExecContext(ctx, `DELETE FROM levelrewards WHERE guild_id = $1`, g) })
|
||||
if removed, err := r.DeleteRoleRewardForLevel(ctx, g, 6); err != nil || removed {
|
||||
t.Errorf("nothing at level 6: %v %v", removed, err)
|
||||
}
|
||||
if removed, err := r.DeleteRoleRewardForLevel(ctx, g, 5); err != nil || !removed {
|
||||
t.Errorf("remove level 5: %v %v", removed, err)
|
||||
}
|
||||
if _, ok, _ := r.GetRoleRewardForLevel(ctx, g, 5); ok {
|
||||
t.Error("the reward is gone")
|
||||
}
|
||||
}
|
||||
@@ -475,3 +475,43 @@ func (r *Repo) SetBetaRegistered(ctx context.Context, guildID string, registered
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// SetBetaServer saves the beta server and the Beta Tester role (0 clears the
|
||||
// role, which turns beta features off for everyone).
|
||||
func (r *Repo) SetBetaServer(ctx context.Context, guildID, roleID, updatedBy int64) error {
|
||||
const q = `
|
||||
INSERT INTO beta_settings (id, guild_id, role_id, updated_by, updated_at)
|
||||
VALUES (TRUE, $1, $2, $3, NOW())
|
||||
ON CONFLICT (id) DO UPDATE SET guild_id = EXCLUDED.guild_id, role_id = EXCLUDED.role_id,
|
||||
updated_by = EXCLUDED.updated_by, updated_at = NOW()`
|
||||
_, err := r.db.ExecContext(ctx, q, guildID, sql.NullInt64{Int64: roleID, Valid: roleID != 0}, updatedBy)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetBetaGuild opts a server in or out of beta features.
|
||||
func (r *Repo) SetBetaGuild(ctx context.Context, guildID, enabledBy int64, enabled bool) error {
|
||||
if enabled {
|
||||
_, err := r.db.ExecContext(ctx, `INSERT INTO beta_guilds (guild_id, enabled_by) VALUES ($1, $2) ON CONFLICT (guild_id) DO NOTHING`, guildID, enabledBy)
|
||||
return err
|
||||
}
|
||||
_, err := r.db.ExecContext(ctx, `DELETE FROM beta_guilds WHERE guild_id = $1`, guildID)
|
||||
return err
|
||||
}
|
||||
|
||||
// IsBetaGuild reports whether a server opted in to beta features.
|
||||
func (r *Repo) IsBetaGuild(ctx context.Context, guildID int64) (bool, error) {
|
||||
var on bool
|
||||
err := r.db.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM beta_guilds WHERE guild_id = $1)`, guildID).Scan(&on)
|
||||
return on, err
|
||||
}
|
||||
|
||||
// DeleteRoleRewardForLevel removes the role reward of a level and reports
|
||||
// whether there was one.
|
||||
func (r *Repo) DeleteRoleRewardForLevel(ctx context.Context, guildID int64, level int) (bool, error) {
|
||||
res, err := r.db.ExecContext(ctx, `DELETE FROM levelrewards WHERE guild_id = $1 AND levelreq = $2`, guildID, level)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
n, err := res.RowsAffected()
|
||||
return n > 0, err
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Package betasettings is where the beta server, the Beta Tester role and the
|
||||
// servers that opted in to beta features are read and changed. The bot polls
|
||||
// the same tables, so a change here reaches the beta gate within 30 seconds.
|
||||
package betasettings
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"velox-bot/internal/db/repos/settingsrepo"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
settings *settingsrepo.Repo
|
||||
}
|
||||
|
||||
func New(settings *settingsrepo.Repo) *Service {
|
||||
return &Service{settings: settings}
|
||||
}
|
||||
|
||||
// Get returns the beta server and role as ID strings ("" when not set).
|
||||
func (s *Service) Get(ctx context.Context) (guildID, roleID string, err error) {
|
||||
return s.settings.GetBetaSettings(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) SetServer(ctx context.Context, guildID, roleID, updatedBy int64) error {
|
||||
return s.settings.SetBetaServer(ctx, guildID, roleID, updatedBy)
|
||||
}
|
||||
|
||||
func (s *Service) OptedIn(ctx context.Context) ([]string, error) {
|
||||
return s.settings.ListBetaGuilds(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) IsOptedIn(ctx context.Context, guildID int64) (bool, error) {
|
||||
return s.settings.IsBetaGuild(ctx, guildID)
|
||||
}
|
||||
|
||||
func (s *Service) SetOptedIn(ctx context.Context, guildID, by int64, on bool) error {
|
||||
return s.settings.SetBetaGuild(ctx, guildID, by, on)
|
||||
}
|
||||
@@ -55,3 +55,11 @@ func (s *Service) ListRoleRewards(ctx context.Context, guildID int64, limit int)
|
||||
func (s *Service) IsLevelSystemEnabled(ctx context.Context, guildID int64) (bool, error) {
|
||||
return s.settings.IsLevelSystemEnabled(ctx, guildID)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteRoleRewardForLevel(ctx context.Context, guildID int64, level int) (bool, error) {
|
||||
return s.settings.DeleteRoleRewardForLevel(ctx, guildID, level)
|
||||
}
|
||||
|
||||
func (s *Service) GetLevelUpChannelID(ctx context.Context, guildID int64) (int64, bool, error) {
|
||||
return s.settings.GetLevelupChannelID(ctx, guildID)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"velox-bot/internal/db/services/betasettings"
|
||||
"velox-bot/internal/db/services/defaultrole"
|
||||
"velox-bot/internal/db/services/level"
|
||||
"velox-bot/internal/db/services/levelsettings"
|
||||
@@ -28,11 +29,12 @@ type Services struct {
|
||||
DefaultRole *defaultrole.Service
|
||||
LogSettings *logsettings.Service
|
||||
MusicSettings *musicsettings.Service
|
||||
BetaSettings *betasettings.Service
|
||||
}
|
||||
|
||||
var Global *Services
|
||||
|
||||
func NewServices(level *level.Service, levelSettings *levelsettings.Service, meeting *meeting.Service, schedule *schedule.Service, userSettings *usersettings.Service, projects *projects.Service, rps *rps.Service, twitchSvc *twitch.Service, welcomeSvc *welcome.Service, defaultRoleSvc *defaultrole.Service, logSettingsSvc *logsettings.Service, musicSvc *musicsettings.Service) *Services {
|
||||
func NewServices(level *level.Service, levelSettings *levelsettings.Service, meeting *meeting.Service, schedule *schedule.Service, userSettings *usersettings.Service, projects *projects.Service, rps *rps.Service, twitchSvc *twitch.Service, welcomeSvc *welcome.Service, defaultRoleSvc *defaultrole.Service, logSettingsSvc *logsettings.Service, musicSvc *musicsettings.Service, betaSvc *betasettings.Service) *Services {
|
||||
s := &Services{
|
||||
Level: level,
|
||||
LevelSettings: levelSettings,
|
||||
@@ -46,6 +48,7 @@ func NewServices(level *level.Service, levelSettings *levelsettings.Service, mee
|
||||
DefaultRole: defaultRoleSvc,
|
||||
LogSettings: logSettingsSvc,
|
||||
MusicSettings: musicSvc,
|
||||
BetaSettings: betaSvc,
|
||||
}
|
||||
Global = s
|
||||
return s
|
||||
|
||||
Reference in new issue
Block a user