feat: integrate music commands with Lavalink support
- Added new music commands: play, queue, and volume. - Implemented music management using disgolink for Lavalink integration. - Updated bot initialization to include Lavalink host and password. - Enhanced interaction handling for music commands, requiring DJ role for usage. - Introduced now playing message with interactive buttons for controlling playback. - Updated dependencies in go.mod for disgolink and snowflake.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
package public
|
||||
|
||||
import "github.com/bwmarrin/discordgo"
|
||||
|
||||
func memberHasDJRole(s *discordgo.Session, guildID string, m *discordgo.Member) bool {
|
||||
if s == nil || guildID == "" || m == nil {
|
||||
return false
|
||||
}
|
||||
if len(m.Roles) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
roles, err := s.GuildRoles(guildID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
djRoleID := ""
|
||||
for _, r := range roles {
|
||||
if r != nil && r.Name == "DJ" {
|
||||
djRoleID = r.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
if djRoleID == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, rid := range m.Roles {
|
||||
if rid == djRoleID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"velox-bot/internal/music"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
var Play = &discordgo.ApplicationCommand{
|
||||
Name: "play",
|
||||
Description: "Play a song via Lavalink",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Name: "query",
|
||||
Description: "Song name or URL",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func PlayHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
data := i.ApplicationCommandData()
|
||||
if len(data.Options) == 0 {
|
||||
return
|
||||
}
|
||||
query := data.Options[0].StringValue()
|
||||
|
||||
// Always acknowledge quickly to avoid "application did not respond".
|
||||
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: "Searching...",
|
||||
},
|
||||
})
|
||||
|
||||
if !memberHasDJRole(s, i.GuildID, i.Member) {
|
||||
_, _ = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
|
||||
Content: ptr("You need the **DJ** role to use music commands."),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// If user provided plain text, turn it into a YouTube search
|
||||
if !strings.HasPrefix(query, "http://") && !strings.HasPrefix(query, "https://") && !strings.HasPrefix(query, "ytsearch:") {
|
||||
query = "ytsearch:" + query
|
||||
} else {
|
||||
query = normalizeYouTubeRadioURL(query)
|
||||
}
|
||||
|
||||
vs, err := findUserVoiceState(s, i.GuildID, i.Member.User.ID)
|
||||
if err != nil {
|
||||
_, _ = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
|
||||
Content: ptr(fmt.Sprintf("Error: %v", err)),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
entry, started, err := music.EnqueueAndPlay(i.GuildID, vs.ChannelID, i.ChannelID, query, i.Member.User.Username)
|
||||
if err != nil {
|
||||
_, _ = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
|
||||
Content: ptr(fmt.Sprintf("Error: %v", err)),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
title := entry.Track.Info.Title
|
||||
author := entry.Track.Info.Author
|
||||
length := entry.Track.Info.Length
|
||||
|
||||
if started {
|
||||
// manager will post now-playing message & manage invalidating old buttons
|
||||
_, _ = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
|
||||
Content: ptr(fmt.Sprintf("Starting: **%s** by **%s** `[%ds]`", title, author, length/1000)),
|
||||
})
|
||||
} else {
|
||||
_, _ = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
|
||||
Content: ptr(fmt.Sprintf("Added to queue: **%s** by **%s** `[%ds]`", title, author, length/1000)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ptr[T any](v T) *T { return new(v) }
|
||||
|
||||
// normalizeYouTubeRadioURL strips auto-generated "radio/mix" params like:
|
||||
// https://www.youtube.com/watch?v=ID&list=RDID&start_radio=1 -> https://www.youtube.com/watch?v=ID
|
||||
// It intentionally does NOT strip normal playlist URLs.
|
||||
func normalizeYouTubeRadioURL(raw string) string {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil || u == nil {
|
||||
return raw
|
||||
}
|
||||
|
||||
host := strings.ToLower(u.Host)
|
||||
if !strings.Contains(host, "youtube.com") || u.Path != "/watch" {
|
||||
return raw
|
||||
}
|
||||
|
||||
q := u.Query()
|
||||
v := q.Get("v")
|
||||
if v == "" {
|
||||
return raw
|
||||
}
|
||||
|
||||
list := q.Get("list")
|
||||
_, hasStartRadio := q["start_radio"]
|
||||
if !hasStartRadio && !strings.HasPrefix(list, "RD") {
|
||||
return raw
|
||||
}
|
||||
|
||||
u.RawQuery = url.Values{"v": []string{v}}.Encode()
|
||||
u.Fragment = ""
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func findUserVoiceState(s *discordgo.Session, guildID, userID string) (*discordgo.VoiceState, error) {
|
||||
g, err := s.State.Guild(guildID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot find guild voice state")
|
||||
}
|
||||
for _, vs := range g.VoiceStates {
|
||||
if vs.UserID == userID {
|
||||
return vs, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("you must be in a voice channel")
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"velox-bot/internal/music"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
var Queue = &discordgo.ApplicationCommand{
|
||||
Name: "queue",
|
||||
Description: "Show the current music queue",
|
||||
}
|
||||
|
||||
func QueueHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
if !memberHasDJRole(s, i.GuildID, i.Member) {
|
||||
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: "You need the **DJ** role to use music commands.",
|
||||
Flags: discordgo.MessageFlagsEphemeral,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
current, rest := music.GetQueue(i.GuildID)
|
||||
|
||||
if current == nil {
|
||||
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: "The queue is currently empty.",
|
||||
Flags: discordgo.MessageFlagsEphemeral,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
nowPlaying := fmt.Sprintf("%s — %s", current.Track.Info.Title, current.Track.Info.Author)
|
||||
|
||||
var descBuilder strings.Builder
|
||||
if len(rest) > 0 {
|
||||
limit := len(rest)
|
||||
if limit > 10 {
|
||||
limit = 10
|
||||
}
|
||||
for idx, entry := range rest[:limit] {
|
||||
fmt.Fprintf(&descBuilder, "%d. %s — %s (requested by %s)\n", idx+1, entry.Track.Info.Title, entry.Track.Info.Author, entry.RequestedBy)
|
||||
}
|
||||
if len(rest) > limit {
|
||||
fmt.Fprintf(&descBuilder, "...and %d more.", len(rest)-limit)
|
||||
}
|
||||
} else {
|
||||
descBuilder.WriteString("Nothing else in the queue.")
|
||||
}
|
||||
|
||||
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Embeds: []*discordgo.MessageEmbed{
|
||||
{
|
||||
Title: "Music Queue",
|
||||
Description: descBuilder.String(),
|
||||
Fields: []*discordgo.MessageEmbedField{
|
||||
{
|
||||
Name: "Now Playing",
|
||||
Value: nowPlaying,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"velox-bot/internal/music"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
var Volume = &discordgo.ApplicationCommand{
|
||||
Name: "volume",
|
||||
Description: "Set playback volume (0-150)",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionInteger,
|
||||
Name: "value",
|
||||
Description: "Volume percent (0-150)",
|
||||
Required: true,
|
||||
MinValue: ptrFloat(0),
|
||||
MaxValue: 150,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func VolumeHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: "Updating volume...",
|
||||
Flags: discordgo.MessageFlagsEphemeral,
|
||||
},
|
||||
})
|
||||
|
||||
if !memberHasDJRole(s, i.GuildID, i.Member) {
|
||||
_, _ = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
|
||||
Content: ptr("You need the **DJ** role to use music commands."),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
data := i.ApplicationCommandData()
|
||||
if len(data.Options) == 0 {
|
||||
_, _ = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
|
||||
Content: ptr("Missing volume value."),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
vol := int(data.Options[0].IntValue())
|
||||
if err := music.SetVolume(i.GuildID, vol); err != nil {
|
||||
_, _ = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
|
||||
Content: ptr(fmt.Sprintf("Error: %v", err)),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
|
||||
Content: ptr(fmt.Sprintf("Volume set to **%d%%**.", vol)),
|
||||
})
|
||||
}
|
||||
|
||||
func ptrFloat(v float64) *float64 { return &v }
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package music
|
||||
|
||||
import (
|
||||
"velox-bot/internal/commands/music/public"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
var (
|
||||
Play *discordgo.ApplicationCommand = public.Play
|
||||
Queue *discordgo.ApplicationCommand = public.Queue
|
||||
Volume *discordgo.ApplicationCommand = public.Volume
|
||||
)
|
||||
|
||||
func PlayHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { public.PlayHandler(s, i) }
|
||||
func QueueHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
public.QueueHandler(s, i)
|
||||
}
|
||||
func VolumeHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { public.VolumeHandler(s, i) }
|
||||
|
||||
Reference in New Issue
Block a user