diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 13d5bb0..422f762 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -33,6 +33,7 @@ var AllCommands = []*discordgo.ApplicationCommand{ cmdmusic.Play, cmdmusic.Queue, cmdmusic.Volume, + cmdmusic.Music, meeting.Meeting, moderation.Moderation, timezone.Timezone, @@ -62,6 +63,7 @@ var handlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCre "play": cmdmusic.PlayHandler, "queue": cmdmusic.QueueHandler, "volume": cmdmusic.VolumeHandler, + "music": cmdmusic.MusicHandler, "config": config.ConfigHandler, } diff --git a/internal/commands/music/public/play.go b/internal/commands/music/public/play.go index d4abaf4..921dd51 100644 --- a/internal/commands/music/public/play.go +++ b/internal/commands/music/public/play.go @@ -4,6 +4,7 @@ import ( "fmt" "net/url" "strings" + "velox-bot/internal/commands/music/shared" "velox-bot/internal/music" "github.com/bwmarrin/discordgo" @@ -23,6 +24,10 @@ var Play = &discordgo.ApplicationCommand{ } func PlayHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { + if !shared.RequireMusicEnabled(s, i) { + return + } + data := i.ApplicationCommandData() if len(data.Options) == 0 { return diff --git a/internal/commands/music/public/queue.go b/internal/commands/music/public/queue.go index b6d30c1..ce4026d 100644 --- a/internal/commands/music/public/queue.go +++ b/internal/commands/music/public/queue.go @@ -3,6 +3,7 @@ package public import ( "fmt" "strings" + "velox-bot/internal/commands/music/shared" "velox-bot/internal/music" "github.com/bwmarrin/discordgo" @@ -14,6 +15,9 @@ var Queue = &discordgo.ApplicationCommand{ } func QueueHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { + if !shared.RequireMusicEnabled(s, i) { + return + } if !memberHasDJRole(s, i.GuildID, i.Member) { _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, diff --git a/internal/commands/music/public/toggle.go b/internal/commands/music/public/toggle.go new file mode 100644 index 0000000..0a61941 --- /dev/null +++ b/internal/commands/music/public/toggle.go @@ -0,0 +1,63 @@ +package public + +import ( + "context" + "velox-bot/internal/commands/music/shared" + "velox-bot/internal/db/services" + + "github.com/bwmarrin/discordgo" +) + +var Music = &discordgo.ApplicationCommand{ + Name: "music", + Description: "Music settings", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "toggle", + Description: "Turn the music player on or off for this server", + }, + }, +} + +func MusicHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { + if i.GuildID == "" { + shared.RespondEphemeral(s, i, "This command can only be used in a server.") + return + } + if !shared.RequireManageGuild(s, i) { + return + } + if !shared.RequireMusicService(s, i) { + return + } + + data := i.ApplicationCommandData() + if len(data.Options) == 0 { + shared.RespondEphemeral(s, i, "Missing subcommand.") + return + } + + switch data.Options[0].Name { + case "toggle": + handleToggle(s, i) + } +} + +func handleToggle(s *discordgo.Session, i *discordgo.InteractionCreate) { + guildID, ok := shared.ParseGuildID(s, i) + if !ok { + return + } + + enabled, err := services.Global.MusicSettings.ToggleMusic(context.Background(), guildID) + if err != nil { + shared.RespondEphemeral(s, i, "Failed to toggle music playback.") + return + } + status := "disabled" + if enabled { + status = "enabled" + } + shared.RespondEphemeral(s, i, "Music playback "+status+" for this server.") +} diff --git a/internal/commands/music/public/volume.go b/internal/commands/music/public/volume.go index 4221b17..12233c9 100644 --- a/internal/commands/music/public/volume.go +++ b/internal/commands/music/public/volume.go @@ -2,6 +2,7 @@ package public import ( "fmt" + "velox-bot/internal/commands/music/shared" "velox-bot/internal/music" "github.com/bwmarrin/discordgo" @@ -23,6 +24,9 @@ var Volume = &discordgo.ApplicationCommand{ } func VolumeHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { + if !shared.RequireMusicEnabled(s, i) { + return + } _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseDeferredChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ @@ -60,4 +64,3 @@ func VolumeHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { } func ptrFloat(v float64) *float64 { return &v } - diff --git a/internal/commands/music/registry.go b/internal/commands/music/registry.go index fd22966..7728a77 100644 --- a/internal/commands/music/registry.go +++ b/internal/commands/music/registry.go @@ -7,9 +7,10 @@ import ( ) var ( - Play *discordgo.ApplicationCommand = public.Play - Queue *discordgo.ApplicationCommand = public.Queue + Play *discordgo.ApplicationCommand = public.Play + Queue *discordgo.ApplicationCommand = public.Queue Volume *discordgo.ApplicationCommand = public.Volume + Music *discordgo.ApplicationCommand = public.Music ) func PlayHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { public.PlayHandler(s, i) } @@ -17,4 +18,4 @@ func QueueHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { public.QueueHandler(s, i) } func VolumeHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { public.VolumeHandler(s, i) } - +func MusicHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { public.MusicHandler(s, i) } diff --git a/internal/commands/music/shared/shared.go b/internal/commands/music/shared/shared.go new file mode 100644 index 0000000..b9ea90b --- /dev/null +++ b/internal/commands/music/shared/shared.go @@ -0,0 +1,61 @@ +package shared + +import ( + "context" + "strconv" + "velox-bot/internal/db/services" + + "github.com/bwmarrin/discordgo" +) + +func RequireMusicEnabled(s *discordgo.Session, i *discordgo.InteractionCreate) bool { + guildID, ok := ParseGuildID(s, i) + if !ok { + return false + } + enabled, err := services.Global.MusicSettings.IsMusicEnabled(context.Background(), guildID) + if err != nil { + RespondEphemeral(s, i, "Failed to load music settings.") + return false + } + if !enabled { + RespondEphemeral(s, i, "Music is disabled on this server. Use /music toggle to enable it.") + return false + } + return true +} + +func RequireManageGuild(s *discordgo.Session, i *discordgo.InteractionCreate) bool { + if i.Member == nil || (i.Member.Permissions&discordgo.PermissionManageGuild) == 0 { + RespondEphemeral(s, i, "You need the **Manage Server** permission to use this.") + return false + } + return true +} + +func RequireMusicService(s *discordgo.Session, i *discordgo.InteractionCreate) bool { + if services.Global == nil || services.Global.MusicSettings == nil { + RespondEphemeral(s, i, "Music service is not available.") + return false + } + return true +} + +func ParseGuildID(s *discordgo.Session, i *discordgo.InteractionCreate) (int64, bool) { + guildID, err := strconv.ParseInt(i.GuildID, 10, 64) + if err != nil { + RespondEphemeral(s, i, "Invalid guild ID.") + return 0, false + } + return guildID, true +} + +func RespondEphemeral(s *discordgo.Session, i *discordgo.InteractionCreate, msg string) { + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: msg, + Flags: discordgo.MessageFlagsEphemeral, + }, + }) +} diff --git a/internal/db/repos/settingsrepo/repo.go b/internal/db/repos/settingsrepo/repo.go index e895604..36cd8f1 100644 --- a/internal/db/repos/settingsrepo/repo.go +++ b/internal/db/repos/settingsrepo/repo.go @@ -340,3 +340,22 @@ func (r *Repo) GetLevelUpMessage(ctx context.Context, guildID int64) (string, er 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 +} diff --git a/internal/db/services/musicsettings/service.go b/internal/db/services/musicsettings/service.go new file mode 100644 index 0000000..798f772 --- /dev/null +++ b/internal/db/services/musicsettings/service.go @@ -0,0 +1,32 @@ +package musicsettings + +import ( + "context" + "velox-bot/internal/db/repos/settingsrepo" +) + +type Service struct { + settings *settingsrepo.Repo +} + +func New(settings *settingsrepo.Repo) *Service { + return &Service{settings: settings} +} + +func (s *Service) ToggleMusic(ctx context.Context, guildID int64) (bool, error) { + enabled, err := s.settings.IsMusicEnabled(ctx, guildID) + if err != nil { + return false, err + } + + newEnabled := !enabled + if err := s.settings.SetMusicEnabled(ctx, guildID, newEnabled); err != nil { + return false, err + } + + return newEnabled, nil +} + +func (s *Service) IsMusicEnabled(ctx context.Context, guildID int64) (bool, error) { + return s.settings.IsMusicEnabled(ctx, guildID) +} diff --git a/internal/db/services/services.go b/internal/db/services/services.go index d4b21b7..abec50d 100644 --- a/internal/db/services/services.go +++ b/internal/db/services/services.go @@ -6,6 +6,7 @@ import ( "velox-bot/internal/db/services/levelsettings" "velox-bot/internal/db/services/logsettings" "velox-bot/internal/db/services/meeting" + "velox-bot/internal/db/services/musicsettings" "velox-bot/internal/db/services/projects" "velox-bot/internal/db/services/rps" "velox-bot/internal/db/services/schedule" @@ -26,11 +27,12 @@ type Services struct { Welcome *welcome.Service DefaultRole *defaultrole.Service LogSettings *logsettings.Service + MusicSettings *musicsettings.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) *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 { s := &Services{ Level: level, LevelSettings: levelSettings, @@ -43,6 +45,7 @@ func NewServices(level *level.Service, levelSettings *levelsettings.Service, mee Welcome: welcomeSvc, DefaultRole: defaultRoleSvc, LogSettings: logSettingsSvc, + MusicSettings: musicSvc, } Global = s return s diff --git a/main.go b/main.go index c009b14..7de0ac5 100644 --- a/main.go +++ b/main.go @@ -24,6 +24,7 @@ import ( "velox-bot/internal/db/services/levelsettings" "velox-bot/internal/db/services/logsettings" "velox-bot/internal/db/services/meeting" + "velox-bot/internal/db/services/musicsettings" "velox-bot/internal/db/services/projects" "velox-bot/internal/db/services/rps" "velox-bot/internal/db/services/schedule" @@ -67,7 +68,8 @@ func main() { twitchService := twitch.New(twitchRepo, config.TwitchClientID) welcomeService := welcome.New(welcomeRepo) defaultRoleService := defaultrole.New(defaultRoleRepo) - services := services.NewServices(levelService, levelSettingsService, meetingService, scheduleService, userSettingsService, projectsService, rpsService, twitchService, welcomeService, defaultRoleService, logSettingsService) + musicSettingsService := musicsettings.New(settingsRepo) + services := services.NewServices(levelService, levelSettingsService, meetingService, scheduleService, userSettingsService, projectsService, rpsService, twitchService, welcomeService, defaultRoleService, logSettingsService, musicSettingsService) bot, err := bot.NewBot(config.BotToken, config.AppID, config.GuildID, config.LavalinkHost, config.LavalinkPass, commands.AllCommands, services) if err != nil { diff --git a/schema.sql b/schema.sql index a970a6d..db31c9d 100644 --- a/schema.sql +++ b/schema.sql @@ -133,3 +133,42 @@ CREATE TABLE IF NOT EXISTS user_settings ( user_id BIGINT PRIMARY KEY, timezone TEXT NOT NULL ); + +-- +-- Music (dashboard queue view/control - see velox-dashboard-api's +-- 000010_music migration, this file mirrors it) +-- +CREATE TABLE IF NOT EXISTS music_settings ( + guild_id BIGINT PRIMARY KEY, + is_enabled BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE TABLE IF NOT EXISTS music_now_playing ( + guild_id BIGINT PRIMARY KEY, + track_title TEXT NOT NULL, + track_url TEXT NOT NULL, + duration_seconds INT NOT NULL, + started_at TIMESTAMPTZ NOT NULL, + paused BOOLEAN NOT NULL DEFAULT FALSE, + volume INT NOT NULL DEFAULT 100 +); + +CREATE TABLE IF NOT EXISTS music_queue ( + id BIGSERIAL PRIMARY KEY, + guild_id BIGINT NOT NULL, + position INT NOT NULL, + track_title TEXT NOT NULL, + track_url TEXT NOT NULL, + requested_by BIGINT NOT NULL, + added_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS music_commands ( + id BIGSERIAL PRIMARY KEY, + guild_id BIGINT NOT NULL, + command_type TEXT NOT NULL CHECK (command_type IN ('skip', 'pause', 'resume', 'set_volume', 'remove_track', 'reorder')), + payload JSONB, + requested_by BIGINT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + processed_at TIMESTAMPTZ +);