From d05e7212845407c725526f487b758891b809b0b6 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 21 Sep 2026 18:48:17 +0200 Subject: [PATCH] Implement slash commands for playback control --- internal/commands/dependencies.go | 14 +++ internal/commands/helpers.go | 109 ++++++++++++++++++++ internal/commands/loop.go | 62 ++++++++++++ internal/commands/nowplaying.go | 37 +++++++ internal/commands/pause.go | 35 +++++++ internal/commands/play.go | 160 ++++++++++++++++++++++++++++++ internal/commands/queue.go | 44 ++++++++ internal/commands/registry.go | 38 +++++++ internal/commands/remove.go | 47 +++++++++ internal/commands/resume.go | 35 +++++++ internal/commands/seek.go | 63 ++++++++++++ internal/commands/shuffle.go | 37 +++++++ internal/commands/skip.go | 41 ++++++++ internal/commands/stop.go | 40 ++++++++ internal/commands/volume.go | 47 +++++++++ 15 files changed, 809 insertions(+) create mode 100644 internal/commands/dependencies.go create mode 100644 internal/commands/helpers.go create mode 100644 internal/commands/loop.go create mode 100644 internal/commands/nowplaying.go create mode 100644 internal/commands/pause.go create mode 100644 internal/commands/play.go create mode 100644 internal/commands/queue.go create mode 100644 internal/commands/registry.go create mode 100644 internal/commands/remove.go create mode 100644 internal/commands/resume.go create mode 100644 internal/commands/seek.go create mode 100644 internal/commands/shuffle.go create mode 100644 internal/commands/skip.go create mode 100644 internal/commands/stop.go create mode 100644 internal/commands/volume.go diff --git a/internal/commands/dependencies.go b/internal/commands/dependencies.go new file mode 100644 index 0000000..932d172 --- /dev/null +++ b/internal/commands/dependencies.go @@ -0,0 +1,14 @@ +package commands + +import ( + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgolink/v4/disgolink" + + "discord-music-bot/internal/music" +) + +type Dependencies struct { + Discord *bot.Client + Lavalink *disgolink.Client + Music *music.Manager +} diff --git a/internal/commands/helpers.go b/internal/commands/helpers.go new file mode 100644 index 0000000..63cd89d --- /dev/null +++ b/internal/commands/helpers.go @@ -0,0 +1,109 @@ +package commands + +import ( + "errors" + "fmt" + "strconv" + "strings" + + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" + "github.com/disgoorg/disgolink/v4/lavalink" + "github.com/disgoorg/snowflake/v2" +) + +func guildID(event *events.ApplicationCommandInteractionCreate) (snowflake.ID, bool) { + id := event.GuildID() + if id == nil { + return 0, false + } + return *id, true +} + +func memberVoiceChannel(discordClient *bot.Client, guild snowflake.ID, userID snowflake.ID) (snowflake.ID, bool) { + state, ok := discordClient.Caches.VoiceState(guild, userID) + if !ok || state.ChannelID == nil { + return 0, false + } + return *state.ChannelID, true +} + +func ensureCanControlPlayback(discordClient *bot.Client, guild snowflake.ID, userID snowflake.ID) (bool, string) { + botChannelID, botConnected := memberVoiceChannel(discordClient, guild, discordClient.ApplicationID) + if !botConnected { + return true, "" + } + + userChannelID, userConnected := memberVoiceChannel(discordClient, guild, userID) + if !userConnected || userChannelID != botChannelID { + return false, fmt.Sprintf("I'm currently active in <#%s>. Join that channel to control playback.", botChannelID) + } + return true, "" +} + +func requirePlaybackAccess(discordClient *bot.Client, event *events.ApplicationCommandInteractionCreate) (snowflake.ID, error) { + gid, ok := guildID(event) + if !ok { + return 0, guildOnlyError(event) + } + + if allowed, reason := ensureCanControlPlayback(discordClient, gid, event.User().ID); !allowed { + return 0, rejectEphemeral(event, reason) + } + + return gid, nil +} + +func reply(event *events.ApplicationCommandInteractionCreate, content string) error { + return event.CreateMessage(discord.NewMessageCreate().WithContent(content)) +} + +func replyEphemeral(event *events.ApplicationCommandInteractionCreate, content string) error { + return event.CreateMessage(discord.NewMessageCreate().WithContent(content).WithEphemeral(true)) +} + +func guildOnlyError(event *events.ApplicationCommandInteractionCreate) error { + return rejectEphemeral(event, "This command only works inside a server.") +} + +func rejectEphemeral(event *events.ApplicationCommandInteractionCreate, content string) error { + if err := replyEphemeral(event, content); err != nil { + return err + } + return errors.New(content) +} + +func replyWithError(event *events.ApplicationCommandInteractionCreate, userMessage string, cause error) error { + if err := replyEphemeral(event, userMessage); err != nil { + return err + } + return fmt.Errorf("%s: %w", userMessage, cause) +} + +func intPtr(v int) *int { + return &v +} + +func parsePosition(raw string) (lavalink.Duration, error) { + parts := strings.Split(raw, ":") + if len(parts) == 0 || len(parts) > 3 { + return 0, errors.New("expected seconds, mm:ss or hh:mm:ss") + } + + segments := make([]int64, len(parts)) + for i, part := range parts { + n, err := strconv.ParseInt(strings.TrimSpace(part), 10, 64) + if err != nil || n < 0 { + return 0, errors.New("expected seconds, mm:ss or hh:mm:ss") + } + segments[i] = n + } + + var totalSeconds int64 + for _, segment := range segments { + totalSeconds = totalSeconds*60 + segment + } + + return lavalink.Duration(totalSeconds) * lavalink.Second, nil +} diff --git a/internal/commands/loop.go b/internal/commands/loop.go new file mode 100644 index 0000000..feb7fb5 --- /dev/null +++ b/internal/commands/loop.go @@ -0,0 +1,62 @@ +package commands + +import ( + "context" + "fmt" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" + + "discord-music-bot/internal/music" +) + +type LoopCommand struct { + deps *Dependencies +} + +func NewLoopCommand(deps *Dependencies) *LoopCommand { + return &LoopCommand{deps: deps} +} + +func (c *LoopCommand) Definition() discord.ApplicationCommandCreate { + return discord.SlashCommandCreate{ + Name: "loop", + Description: "Sets what happens when a track finishes", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionString{ + Name: "mode", + Description: "Loop mode", + Required: true, + Choices: []discord.ApplicationCommandOptionChoiceString{ + {Name: "Off", Value: "off"}, + {Name: "Track", Value: "track"}, + {Name: "Queue", Value: "queue"}, + }, + }, + }, + } +} + +func (c *LoopCommand) Execute(ctx context.Context, event *events.ApplicationCommandInteractionCreate) error { + gid, err := requirePlaybackAccess(c.deps.Discord, event) + if err != nil { + return err + } + + value := event.SlashCommandInteractionData().String("mode") + + var mode music.LoopMode + switch value { + case "off": + mode = music.LoopOff + case "track": + mode = music.LoopTrack + case "queue": + mode = music.LoopQueue + default: + return rejectEphemeral(event, fmt.Sprintf("Unknown loop mode %q.", value)) + } + + c.deps.Music.Guild(gid).SetLoopMode(mode) + return reply(event, fmt.Sprintf("Loop mode set to **%s**.", mode)) +} diff --git a/internal/commands/nowplaying.go b/internal/commands/nowplaying.go new file mode 100644 index 0000000..f84a95f --- /dev/null +++ b/internal/commands/nowplaying.go @@ -0,0 +1,37 @@ +package commands + +import ( + "context" + "fmt" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" +) + +type NowPlayingCommand struct { + deps *Dependencies +} + +func NewNowPlayingCommand(deps *Dependencies) *NowPlayingCommand { + return &NowPlayingCommand{deps: deps} +} + +func (c *NowPlayingCommand) Definition() discord.ApplicationCommandCreate { + return discord.SlashCommandCreate{ + Name: "nowplaying", + Description: "Shows the currently playing track", + } +} + +func (c *NowPlayingCommand) Execute(ctx context.Context, event *events.ApplicationCommandInteractionCreate) error { + gid, ok := guildID(event) + if !ok { + return guildOnlyError(event) + } + + track, playing := c.deps.Music.Guild(gid).NowPlaying() + if !playing { + return reply(event, "Nothing is playing right now.") + } + return reply(event, fmt.Sprintf("Now playing: **%s** (%s)", track.Info.Title, track.Info.Length)) +} diff --git a/internal/commands/pause.go b/internal/commands/pause.go new file mode 100644 index 0000000..9c3740f --- /dev/null +++ b/internal/commands/pause.go @@ -0,0 +1,35 @@ +package commands + +import ( + "context" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" +) + +type PauseCommand struct { + deps *Dependencies +} + +func NewPauseCommand(deps *Dependencies) *PauseCommand { + return &PauseCommand{deps: deps} +} + +func (c *PauseCommand) Definition() discord.ApplicationCommandCreate { + return discord.SlashCommandCreate{ + Name: "pause", + Description: "Pauses the current track", + } +} + +func (c *PauseCommand) Execute(ctx context.Context, event *events.ApplicationCommandInteractionCreate) error { + gid, err := requirePlaybackAccess(c.deps.Discord, event) + if err != nil { + return err + } + + if err := c.deps.Music.Guild(gid).SetPaused(ctx, true); err != nil { + return replyWithError(event, "Could not pause playback.", err) + } + return reply(event, "Paused.") +} diff --git a/internal/commands/play.go b/internal/commands/play.go new file mode 100644 index 0000000..1492ae3 --- /dev/null +++ b/internal/commands/play.go @@ -0,0 +1,160 @@ +package commands + +import ( + "context" + "fmt" + "strings" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" + "github.com/disgoorg/disgolink/v4/disgolink" + "github.com/disgoorg/disgolink/v4/lavalink" + + "discord-music-bot/internal/music" +) + +type PlayCommand struct { + deps *Dependencies +} + +func NewPlayCommand(deps *Dependencies) *PlayCommand { + return &PlayCommand{deps: deps} +} + +func (c *PlayCommand) Definition() discord.ApplicationCommandCreate { + return discord.SlashCommandCreate{ + Name: "play", + Description: "Plays a song or playlist, or adds it to the queue", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionString{ + Name: "query", + Description: "Song name, song URL, or playlist URL", + Required: true, + }, + }, + } +} + +func (c *PlayCommand) Execute(ctx context.Context, event *events.ApplicationCommandInteractionCreate) error { + gid, ok := guildID(event) + if !ok { + return guildOnlyError(event) + } + + channelID, ok := memberVoiceChannel(c.deps.Discord, gid, event.User().ID) + if !ok { + return rejectEphemeral(event, "Join a voice channel first.") + } + + if allowed, reason := ensureCanControlPlayback(c.deps.Discord, gid, event.User().ID); !allowed { + return rejectEphemeral(event, reason) + } + + query := event.SlashCommandInteractionData().String("query") + + if err := event.DeferCreateMessage(false); err != nil { + return fmt.Errorf("deferring response: %w", err) + } + + loaded, err := loadTracks(ctx, c.deps.Lavalink, query) + if err != nil { + return c.followUp(event, err.Error()) + } + + if err := c.deps.Discord.UpdateVoiceState(ctx, gid, &channelID, false, false); err != nil { + return c.followUp(event, fmt.Sprintf("Could not join your voice channel: %s", err)) + } + + result, err := c.deps.Music.Guild(gid).EnqueueMany(ctx, loaded.tracks) + if err != nil { + return c.followUp(event, fmt.Sprintf("Could not play that track: %s", err)) + } + + return c.followUp(event, playSummary(loaded, result)) +} + +func playSummary(loaded trackLoadResult, result music.EnqueueResult) string { + if len(loaded.tracks) <= 1 { + title := "that track" + if len(loaded.tracks) == 1 { + title = loaded.tracks[0].Info.Title + } + if result.PlayingNow { + return fmt.Sprintf("Now playing: **%s**", title) + } + return fmt.Sprintf("Added to queue: **%s**", title) + } + + name := loaded.playlistName + if name == "" { + name = "the playlist" + } + + if result.PlayingNow { + return fmt.Sprintf("Now playing **%s** and queued %d more track(s) from **%s**.", + result.Started.Info.Title, result.Queued, name) + } + return fmt.Sprintf("Added %d tracks from **%s** to the queue.", len(loaded.tracks), name) +} + +func (c *PlayCommand) followUp(event *events.ApplicationCommandInteractionCreate, content string) error { + _, err := event.Client().Rest.UpdateInteractionResponse(event.ApplicationID(), event.Token(), discord.NewMessageUpdate().WithContent(content)) + return err +} + +func normalizeQuery(query string) string { + if strings.HasPrefix(query, "http://") || strings.HasPrefix(query, "https://") { + return query + } + return "ytsearch:" + query +} + +type trackLoadResult struct { + tracks []lavalink.Track + playlistName string +} + +func loadTracks(ctx context.Context, lavalinkClient *disgolink.Client, query string) (trackLoadResult, error) { + node := lavalinkClient.BestNode() + if node == nil { + return trackLoadResult{}, fmt.Errorf("no music node is available right now") + } + + type outcome struct { + result trackLoadResult + err error + } + outcomeCh := make(chan outcome, 1) + + node.Rest.LoadTracksHandler(ctx, normalizeQuery(query), disgolink.NewTrackLoadingResultHandler( + func(track lavalink.Track) { + outcomeCh <- outcome{result: trackLoadResult{tracks: []lavalink.Track{track}}} + }, + func(playlist lavalink.Playlist) { + if len(playlist.Tracks) == 0 { + outcomeCh <- outcome{err: fmt.Errorf("that playlist is empty")} + return + } + outcomeCh <- outcome{result: trackLoadResult{ + tracks: playlist.Tracks, + playlistName: playlist.Info.Name, + }} + }, + func(tracks []lavalink.Track) { + if len(tracks) == 0 { + outcomeCh <- outcome{err: fmt.Errorf("no results found for %q", query)} + return + } + outcomeCh <- outcome{result: trackLoadResult{tracks: []lavalink.Track{tracks[0]}}} + }, + func() { + outcomeCh <- outcome{err: fmt.Errorf("no results found for %q", query)} + }, + func(err error) { + outcomeCh <- outcome{err: fmt.Errorf("failed to load track: %w", err)} + }, + )) + + res := <-outcomeCh + return res.result, res.err +} diff --git a/internal/commands/queue.go b/internal/commands/queue.go new file mode 100644 index 0000000..3c5b2e0 --- /dev/null +++ b/internal/commands/queue.go @@ -0,0 +1,44 @@ +package commands + +import ( + "context" + "fmt" + "strings" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" +) + +type QueueCommand struct { + deps *Dependencies +} + +func NewQueueCommand(deps *Dependencies) *QueueCommand { + return &QueueCommand{deps: deps} +} + +func (c *QueueCommand) Definition() discord.ApplicationCommandCreate { + return discord.SlashCommandCreate{ + Name: "queue", + Description: "Shows the upcoming tracks", + } +} + +func (c *QueueCommand) Execute(ctx context.Context, event *events.ApplicationCommandInteractionCreate) error { + gid, ok := guildID(event) + if !ok { + return guildOnlyError(event) + } + + tracks := c.deps.Music.Guild(gid).Queued() + if len(tracks) == 0 { + return reply(event, "The queue is empty.") + } + + var builder strings.Builder + builder.WriteString("Up next:\n") + for i, track := range tracks { + fmt.Fprintf(&builder, "%d. **%s** (%s)\n", i+1, track.Info.Title, track.Info.Length) + } + return reply(event, builder.String()) +} diff --git a/internal/commands/registry.go b/internal/commands/registry.go new file mode 100644 index 0000000..033f0b5 --- /dev/null +++ b/internal/commands/registry.go @@ -0,0 +1,38 @@ +package commands + +import ( + "context" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" +) + +type Command interface { + Definition() discord.ApplicationCommandCreate + Execute(ctx context.Context, event *events.ApplicationCommandInteractionCreate) error +} + +type Registry struct { + commands map[string]Command +} + +func NewRegistry(cmds ...Command) *Registry { + registry := &Registry{commands: make(map[string]Command, len(cmds))} + for _, cmd := range cmds { + registry.commands[cmd.Definition().CommandName()] = cmd + } + return registry +} + +func (r *Registry) Get(name string) (Command, bool) { + cmd, ok := r.commands[name] + return cmd, ok +} + +func (r *Registry) Definitions() []discord.ApplicationCommandCreate { + definitions := make([]discord.ApplicationCommandCreate, 0, len(r.commands)) + for _, cmd := range r.commands { + definitions = append(definitions, cmd.Definition()) + } + return definitions +} diff --git a/internal/commands/remove.go b/internal/commands/remove.go new file mode 100644 index 0000000..6576f73 --- /dev/null +++ b/internal/commands/remove.go @@ -0,0 +1,47 @@ +package commands + +import ( + "context" + "fmt" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" +) + +type RemoveCommand struct { + deps *Dependencies +} + +func NewRemoveCommand(deps *Dependencies) *RemoveCommand { + return &RemoveCommand{deps: deps} +} + +func (c *RemoveCommand) Definition() discord.ApplicationCommandCreate { + return discord.SlashCommandCreate{ + Name: "remove", + Description: "Removes a track from the queue", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionInt{ + Name: "position", + Description: "Position in the queue, as shown by /queue (1 is next)", + Required: true, + MinValue: intPtr(1), + }, + }, + } +} + +func (c *RemoveCommand) Execute(ctx context.Context, event *events.ApplicationCommandInteractionCreate) error { + gid, err := requirePlaybackAccess(c.deps.Discord, event) + if err != nil { + return err + } + + position := event.SlashCommandInteractionData().Int("position") + + track, ok := c.deps.Music.Guild(gid).RemoveQueued(position - 1) + if !ok { + return rejectEphemeral(event, fmt.Sprintf("There's no track at position %d in the queue.", position)) + } + return reply(event, fmt.Sprintf("Removed **%s** from the queue.", track.Info.Title)) +} diff --git a/internal/commands/resume.go b/internal/commands/resume.go new file mode 100644 index 0000000..753a7ae --- /dev/null +++ b/internal/commands/resume.go @@ -0,0 +1,35 @@ +package commands + +import ( + "context" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" +) + +type ResumeCommand struct { + deps *Dependencies +} + +func NewResumeCommand(deps *Dependencies) *ResumeCommand { + return &ResumeCommand{deps: deps} +} + +func (c *ResumeCommand) Definition() discord.ApplicationCommandCreate { + return discord.SlashCommandCreate{ + Name: "resume", + Description: "Resumes the current track", + } +} + +func (c *ResumeCommand) Execute(ctx context.Context, event *events.ApplicationCommandInteractionCreate) error { + gid, err := requirePlaybackAccess(c.deps.Discord, event) + if err != nil { + return err + } + + if err := c.deps.Music.Guild(gid).SetPaused(ctx, false); err != nil { + return replyWithError(event, "Could not resume playback.", err) + } + return reply(event, "Resumed.") +} diff --git a/internal/commands/seek.go b/internal/commands/seek.go new file mode 100644 index 0000000..f874458 --- /dev/null +++ b/internal/commands/seek.go @@ -0,0 +1,63 @@ +package commands + +import ( + "context" + "errors" + "fmt" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" + + "discord-music-bot/internal/music" +) + +type SeekCommand struct { + deps *Dependencies +} + +func NewSeekCommand(deps *Dependencies) *SeekCommand { + return &SeekCommand{deps: deps} +} + +func (c *SeekCommand) Definition() discord.ApplicationCommandCreate { + return discord.SlashCommandCreate{ + Name: "seek", + Description: "Jumps to a position in the current track", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionString{ + Name: "position", + Description: "Position to seek to: seconds, mm:ss or hh:mm:ss (e.g. 90 or 1:30)", + Required: true, + }, + }, + } +} + +func (c *SeekCommand) Execute(ctx context.Context, event *events.ApplicationCommandInteractionCreate) error { + gid, err := requirePlaybackAccess(c.deps.Discord, event) + if err != nil { + return err + } + + raw := event.SlashCommandInteractionData().String("position") + position, err := parsePosition(raw) + if err != nil { + return rejectEphemeral(event, fmt.Sprintf("Invalid position %q: %s", raw, err)) + } + + if err := c.deps.Music.Guild(gid).Seek(ctx, position); err != nil { + return replyWithError(event, seekErrorMessage(err), err) + } + return reply(event, fmt.Sprintf("Seeked to **%s**.", position)) +} + +func seekErrorMessage(err error) string { + switch { + case errors.Is(err, music.ErrNoTrackPlaying): + return "Nothing is playing right now." + case errors.Is(err, music.ErrStreamNotSeekable): + return "Can't seek within a live stream." + default: + return "Could not seek." + } +} diff --git a/internal/commands/shuffle.go b/internal/commands/shuffle.go new file mode 100644 index 0000000..564a746 --- /dev/null +++ b/internal/commands/shuffle.go @@ -0,0 +1,37 @@ +package commands + +import ( + "context" + "fmt" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" +) + +type ShuffleCommand struct { + deps *Dependencies +} + +func NewShuffleCommand(deps *Dependencies) *ShuffleCommand { + return &ShuffleCommand{deps: deps} +} + +func (c *ShuffleCommand) Definition() discord.ApplicationCommandCreate { + return discord.SlashCommandCreate{ + Name: "shuffle", + Description: "Shuffles the upcoming tracks in the queue", + } +} + +func (c *ShuffleCommand) Execute(ctx context.Context, event *events.ApplicationCommandInteractionCreate) error { + gid, err := requirePlaybackAccess(c.deps.Discord, event) + if err != nil { + return err + } + + count := c.deps.Music.Guild(gid).Shuffle() + if count == 0 { + return reply(event, "The queue is empty, nothing to shuffle.") + } + return reply(event, fmt.Sprintf("Shuffled **%d** queued track(s).", count)) +} diff --git a/internal/commands/skip.go b/internal/commands/skip.go new file mode 100644 index 0000000..e069c03 --- /dev/null +++ b/internal/commands/skip.go @@ -0,0 +1,41 @@ +package commands + +import ( + "context" + "fmt" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" +) + +type SkipCommand struct { + deps *Dependencies +} + +func NewSkipCommand(deps *Dependencies) *SkipCommand { + return &SkipCommand{deps: deps} +} + +func (c *SkipCommand) Definition() discord.ApplicationCommandCreate { + return discord.SlashCommandCreate{ + Name: "skip", + Description: "Skips the current track", + } +} + +func (c *SkipCommand) Execute(ctx context.Context, event *events.ApplicationCommandInteractionCreate) error { + gid, err := requirePlaybackAccess(c.deps.Discord, event) + if err != nil { + return err + } + + track, hasNext, err := c.deps.Music.Guild(gid).Skip(ctx) + if err != nil { + return replyWithError(event, "Could not skip the current track.", err) + } + + if !hasNext { + return reply(event, "Skipped. Queue is empty.") + } + return reply(event, fmt.Sprintf("Skipped. Now playing: **%s**", track.Info.Title)) +} diff --git a/internal/commands/stop.go b/internal/commands/stop.go new file mode 100644 index 0000000..101f51a --- /dev/null +++ b/internal/commands/stop.go @@ -0,0 +1,40 @@ +package commands + +import ( + "context" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" +) + +type StopCommand struct { + deps *Dependencies +} + +func NewStopCommand(deps *Dependencies) *StopCommand { + return &StopCommand{deps: deps} +} + +func (c *StopCommand) Definition() discord.ApplicationCommandCreate { + return discord.SlashCommandCreate{ + Name: "stop", + Description: "Stops playback, clears the queue and leaves the voice channel", + } +} + +func (c *StopCommand) Execute(ctx context.Context, event *events.ApplicationCommandInteractionCreate) error { + gid, err := requirePlaybackAccess(c.deps.Discord, event) + if err != nil { + return err + } + + if err := c.deps.Music.Guild(gid).Stop(ctx); err != nil { + return replyWithError(event, "Could not stop playback.", err) + } + + if err := c.deps.Discord.UpdateVoiceState(ctx, gid, nil, false, false); err != nil { + return replyWithError(event, "Stopped playback, but could not leave the voice channel.", err) + } + + return reply(event, "Stopped playback and left the voice channel.") +} diff --git a/internal/commands/volume.go b/internal/commands/volume.go new file mode 100644 index 0000000..25e8fcb --- /dev/null +++ b/internal/commands/volume.go @@ -0,0 +1,47 @@ +package commands + +import ( + "context" + "fmt" + + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/events" +) + +type VolumeCommand struct { + deps *Dependencies +} + +func NewVolumeCommand(deps *Dependencies) *VolumeCommand { + return &VolumeCommand{deps: deps} +} + +func (c *VolumeCommand) Definition() discord.ApplicationCommandCreate { + return discord.SlashCommandCreate{ + Name: "volume", + Description: "Sets the playback volume", + Options: []discord.ApplicationCommandOption{ + discord.ApplicationCommandOptionInt{ + Name: "level", + Description: "Volume percentage, 0-200 (default 100)", + Required: true, + MinValue: intPtr(0), + MaxValue: intPtr(200), + }, + }, + } +} + +func (c *VolumeCommand) Execute(ctx context.Context, event *events.ApplicationCommandInteractionCreate) error { + gid, err := requirePlaybackAccess(c.deps.Discord, event) + if err != nil { + return err + } + + level := event.SlashCommandInteractionData().Int("level") + + if err := c.deps.Music.Guild(gid).SetVolume(ctx, level); err != nil { + return replyWithError(event, "Could not change the volume.", err) + } + return reply(event, fmt.Sprintf("Volume set to **%d%%**.", level)) +}