Merge pull request 'feat(music): add /music toggle and gate playback behind it' (#12) from ft/music-toggle into dev
Reviewed-on: #12
This commit was merged in pull request #12.
This commit is contained in:
@@ -33,6 +33,7 @@ var AllCommands = []*discordgo.ApplicationCommand{
|
|||||||
cmdmusic.Play,
|
cmdmusic.Play,
|
||||||
cmdmusic.Queue,
|
cmdmusic.Queue,
|
||||||
cmdmusic.Volume,
|
cmdmusic.Volume,
|
||||||
|
cmdmusic.Music,
|
||||||
meeting.Meeting,
|
meeting.Meeting,
|
||||||
moderation.Moderation,
|
moderation.Moderation,
|
||||||
timezone.Timezone,
|
timezone.Timezone,
|
||||||
@@ -62,6 +63,7 @@ var handlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCre
|
|||||||
"play": cmdmusic.PlayHandler,
|
"play": cmdmusic.PlayHandler,
|
||||||
"queue": cmdmusic.QueueHandler,
|
"queue": cmdmusic.QueueHandler,
|
||||||
"volume": cmdmusic.VolumeHandler,
|
"volume": cmdmusic.VolumeHandler,
|
||||||
|
"music": cmdmusic.MusicHandler,
|
||||||
"config": config.ConfigHandler,
|
"config": config.ConfigHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"velox-bot/internal/commands/music/shared"
|
||||||
"velox-bot/internal/music"
|
"velox-bot/internal/music"
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
@@ -23,6 +24,10 @@ var Play = &discordgo.ApplicationCommand{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func PlayHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
func PlayHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
|
if !shared.RequireMusicEnabled(s, i) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
data := i.ApplicationCommandData()
|
data := i.ApplicationCommandData()
|
||||||
if len(data.Options) == 0 {
|
if len(data.Options) == 0 {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package public
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"velox-bot/internal/commands/music/shared"
|
||||||
"velox-bot/internal/music"
|
"velox-bot/internal/music"
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
@@ -14,6 +15,9 @@ var Queue = &discordgo.ApplicationCommand{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func QueueHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
func QueueHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
|
if !shared.RequireMusicEnabled(s, i) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if !memberHasDJRole(s, i.GuildID, i.Member) {
|
if !memberHasDJRole(s, i.GuildID, i.Member) {
|
||||||
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package public
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"velox-bot/internal/commands/music/shared"
|
||||||
"velox-bot/internal/music"
|
"velox-bot/internal/music"
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
@@ -23,6 +24,9 @@ var Volume = &discordgo.ApplicationCommand{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func VolumeHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
func VolumeHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
|
if !shared.RequireMusicEnabled(s, i) {
|
||||||
|
return
|
||||||
|
}
|
||||||
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||||
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
|
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
|
||||||
Data: &discordgo.InteractionResponseData{
|
Data: &discordgo.InteractionResponseData{
|
||||||
@@ -60,4 +64,3 @@ func VolumeHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ptrFloat(v float64) *float64 { return &v }
|
func ptrFloat(v float64) *float64 { return &v }
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Play *discordgo.ApplicationCommand = public.Play
|
Play *discordgo.ApplicationCommand = public.Play
|
||||||
Queue *discordgo.ApplicationCommand = public.Queue
|
Queue *discordgo.ApplicationCommand = public.Queue
|
||||||
Volume *discordgo.ApplicationCommand = public.Volume
|
Volume *discordgo.ApplicationCommand = public.Volume
|
||||||
|
Music *discordgo.ApplicationCommand = public.Music
|
||||||
)
|
)
|
||||||
|
|
||||||
func PlayHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { public.PlayHandler(s, i) }
|
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)
|
public.QueueHandler(s, i)
|
||||||
}
|
}
|
||||||
func VolumeHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { public.VolumeHandler(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) }
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -340,3 +340,22 @@ func (r *Repo) GetLevelUpMessage(ctx context.Context, guildID int64) (string, er
|
|||||||
return "", err
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"velox-bot/internal/db/services/levelsettings"
|
"velox-bot/internal/db/services/levelsettings"
|
||||||
"velox-bot/internal/db/services/logsettings"
|
"velox-bot/internal/db/services/logsettings"
|
||||||
"velox-bot/internal/db/services/meeting"
|
"velox-bot/internal/db/services/meeting"
|
||||||
|
"velox-bot/internal/db/services/musicsettings"
|
||||||
"velox-bot/internal/db/services/projects"
|
"velox-bot/internal/db/services/projects"
|
||||||
"velox-bot/internal/db/services/rps"
|
"velox-bot/internal/db/services/rps"
|
||||||
"velox-bot/internal/db/services/schedule"
|
"velox-bot/internal/db/services/schedule"
|
||||||
@@ -26,11 +27,12 @@ type Services struct {
|
|||||||
Welcome *welcome.Service
|
Welcome *welcome.Service
|
||||||
DefaultRole *defaultrole.Service
|
DefaultRole *defaultrole.Service
|
||||||
LogSettings *logsettings.Service
|
LogSettings *logsettings.Service
|
||||||
|
MusicSettings *musicsettings.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
var Global *Services
|
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{
|
s := &Services{
|
||||||
Level: level,
|
Level: level,
|
||||||
LevelSettings: levelSettings,
|
LevelSettings: levelSettings,
|
||||||
@@ -43,6 +45,7 @@ func NewServices(level *level.Service, levelSettings *levelsettings.Service, mee
|
|||||||
Welcome: welcomeSvc,
|
Welcome: welcomeSvc,
|
||||||
DefaultRole: defaultRoleSvc,
|
DefaultRole: defaultRoleSvc,
|
||||||
LogSettings: logSettingsSvc,
|
LogSettings: logSettingsSvc,
|
||||||
|
MusicSettings: musicSvc,
|
||||||
}
|
}
|
||||||
Global = s
|
Global = s
|
||||||
return s
|
return s
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"velox-bot/internal/db/services/levelsettings"
|
"velox-bot/internal/db/services/levelsettings"
|
||||||
"velox-bot/internal/db/services/logsettings"
|
"velox-bot/internal/db/services/logsettings"
|
||||||
"velox-bot/internal/db/services/meeting"
|
"velox-bot/internal/db/services/meeting"
|
||||||
|
"velox-bot/internal/db/services/musicsettings"
|
||||||
"velox-bot/internal/db/services/projects"
|
"velox-bot/internal/db/services/projects"
|
||||||
"velox-bot/internal/db/services/rps"
|
"velox-bot/internal/db/services/rps"
|
||||||
"velox-bot/internal/db/services/schedule"
|
"velox-bot/internal/db/services/schedule"
|
||||||
@@ -67,7 +68,8 @@ func main() {
|
|||||||
twitchService := twitch.New(twitchRepo, config.TwitchClientID)
|
twitchService := twitch.New(twitchRepo, config.TwitchClientID)
|
||||||
welcomeService := welcome.New(welcomeRepo)
|
welcomeService := welcome.New(welcomeRepo)
|
||||||
defaultRoleService := defaultrole.New(defaultRoleRepo)
|
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)
|
bot, err := bot.NewBot(config.BotToken, config.AppID, config.GuildID, config.LavalinkHost, config.LavalinkPass, commands.AllCommands, services)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+39
@@ -133,3 +133,42 @@ CREATE TABLE IF NOT EXISTS user_settings (
|
|||||||
user_id BIGINT PRIMARY KEY,
|
user_id BIGINT PRIMARY KEY,
|
||||||
timezone TEXT NOT NULL
|
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
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user