Implement slash commands for playback control
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -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.")
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -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.")
|
||||||
|
}
|
||||||
@@ -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."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -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.")
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user