v3.2.0 #13
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
"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 }
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ var (
|
||||
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) }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
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/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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+39
@@ -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
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user