1205 lines
32 KiB
Go
1205 lines
32 KiB
Go
|
package main
|
|||
|
|
|||
|
import (
|
|||
|
dc "github.com/bwmarrin/discordgo"
|
|||
|
|
|||
|
"git.nobrain.org/r4/dischord/config"
|
|||
|
"git.nobrain.org/r4/dischord/extractor"
|
|||
|
_ "git.nobrain.org/r4/dischord/extractor/builtins"
|
|||
|
"git.nobrain.org/r4/dischord/extractor/ytdl"
|
|||
|
"git.nobrain.org/r4/dischord/player"
|
|||
|
"git.nobrain.org/r4/dischord/util"
|
|||
|
|
|||
|
"errors"
|
|||
|
"flag"
|
|||
|
"fmt"
|
|||
|
"os"
|
|||
|
"os/signal"
|
|||
|
"runtime"
|
|||
|
"sort"
|
|||
|
"strconv"
|
|||
|
"strings"
|
|||
|
"sync"
|
|||
|
"syscall"
|
|||
|
_ "embed"
|
|||
|
)
|
|||
|
|
|||
|
var copyright bool
|
|||
|
var autoconf bool
|
|||
|
var registerCommands bool
|
|||
|
var unregisterCommands bool
|
|||
|
|
|||
|
//go:embed bye.opus
|
|||
|
var resByeOpus []byte
|
|||
|
|
|||
|
func init() {
|
|||
|
flag.BoolVar(©right, "copyright", false, "print copyright info and quit")
|
|||
|
flag.BoolVar(&autoconf, "autoconf", false, "launch automatic configurator program (overwriting any existing configuration)")
|
|||
|
flag.BoolVar(®isterCommands, "register_commands", true, "register commands with Discord upon startup")
|
|||
|
flag.BoolVar(&unregisterCommands, "unregister_commands", false, "unregister registered commands with Discord upon shutdown")
|
|||
|
}
|
|||
|
|
|||
|
// A UserError is shown to the user
|
|||
|
type UserError struct {
|
|||
|
error
|
|||
|
}
|
|||
|
|
|||
|
const (
|
|||
|
interactionFlags = uint64(dc.MessageFlagsEphemeral)
|
|||
|
)
|
|||
|
|
|||
|
var (
|
|||
|
ErrVoiceNotConnected = UserError{errors.New("bot is currently not connected to a voice channel")}
|
|||
|
ErrUnsupportedUrl = UserError{errors.New("unsupported URL")}
|
|||
|
ErrStartThinkingNotInitialResponse = errors.New("StartThinking() must be the initial response")
|
|||
|
ErrInvalidAutocompleteCall = errors.New("invalid autocomplete call")
|
|||
|
)
|
|||
|
|
|||
|
type MessageData struct {
|
|||
|
Content string
|
|||
|
Files []*dc.File
|
|||
|
Components []dc.MessageComponent
|
|||
|
Embeds []*dc.MessageEmbed
|
|||
|
}
|
|||
|
|
|||
|
type MessageWriter struct {
|
|||
|
session *dc.Session
|
|||
|
interaction *dc.Interaction
|
|||
|
first bool
|
|||
|
thinking bool
|
|||
|
}
|
|||
|
|
|||
|
func NewMessageWriter(s *dc.Session, ia *dc.Interaction) *MessageWriter {
|
|||
|
return &MessageWriter{
|
|||
|
session: s,
|
|||
|
interaction: ia,
|
|||
|
first: true,
|
|||
|
thinking: false,
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func (m *MessageWriter) StartThinking() error {
|
|||
|
if !m.first {
|
|||
|
return ErrStartThinkingNotInitialResponse
|
|||
|
}
|
|||
|
err := m.session.InteractionRespond(m.interaction, &dc.InteractionResponse{
|
|||
|
Type: dc.InteractionResponseDeferredChannelMessageWithSource,
|
|||
|
Data: &dc.InteractionResponseData{
|
|||
|
Flags: interactionFlags,
|
|||
|
},
|
|||
|
})
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
m.first = false
|
|||
|
m.thinking = true
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
func (m *MessageWriter) Message(d *MessageData) error {
|
|||
|
var err error
|
|||
|
if m.first {
|
|||
|
err = m.session.InteractionRespond(m.interaction, &dc.InteractionResponse{
|
|||
|
Type: dc.InteractionResponseChannelMessageWithSource,
|
|||
|
Data: &dc.InteractionResponseData{
|
|||
|
Content: d.Content,
|
|||
|
Flags: interactionFlags,
|
|||
|
Files: d.Files,
|
|||
|
Components: d.Components,
|
|||
|
Embeds: d.Embeds,
|
|||
|
},
|
|||
|
})
|
|||
|
} else if m.thinking {
|
|||
|
_, err = m.session.InteractionResponseEdit(m.interaction, &dc.WebhookEdit{
|
|||
|
Content: d.Content,
|
|||
|
Files: d.Files,
|
|||
|
Components: d.Components,
|
|||
|
Embeds: d.Embeds,
|
|||
|
})
|
|||
|
} else {
|
|||
|
_, err = m.session.FollowupMessageCreate(m.interaction, true, &dc.WebhookParams{
|
|||
|
Content: d.Content,
|
|||
|
Flags: interactionFlags,
|
|||
|
Files: d.Files,
|
|||
|
Components: d.Components,
|
|||
|
Embeds: d.Embeds,
|
|||
|
})
|
|||
|
}
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
m.first = false
|
|||
|
m.thinking = false
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
func main() {
|
|||
|
flag.Parse()
|
|||
|
|
|||
|
if copyright {
|
|||
|
fmt.Println(copyrightText)
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
var clients sync.Map // guild ID string to player.Client
|
|||
|
|
|||
|
// Load / create configuration file
|
|||
|
cfgfile := "config.toml"
|
|||
|
var cfg *config.Config
|
|||
|
var err error
|
|||
|
if autoconf || func() bool {cfg, err = config.Load(cfgfile); return err != nil}() {
|
|||
|
if err != nil {
|
|||
|
if os.IsNotExist(err) {
|
|||
|
fmt.Println("Configuration file not found, launching automatic configurator.")
|
|||
|
fmt.Println("Hit Ctrl+C to cancel anytime.")
|
|||
|
} else {
|
|||
|
fmt.Println("Error:", err)
|
|||
|
return
|
|||
|
}
|
|||
|
}
|
|||
|
cfg, err = config.Autoconf(cfgfile)
|
|||
|
if err != nil {
|
|||
|
if err == config.ErrPythonNotInstalled {
|
|||
|
if runtime.GOOS == "darwin" {
|
|||
|
fmt.Println("Python is required to run youtube-dl, but no python installation was found. To fix this, please install Xcode Command Line Tools.")
|
|||
|
} else {
|
|||
|
fmt.Println("Python is required to run youtube-dl, but no python installation was found. To fix this, please install Python from your package manager.")
|
|||
|
}
|
|||
|
} else {
|
|||
|
fmt.Println("Error:", err)
|
|||
|
}
|
|||
|
return
|
|||
|
}
|
|||
|
if runtime.GOOS == "windows" {
|
|||
|
fmt.Println("Hit Enter to close this window.")
|
|||
|
fmt.Scanln()
|
|||
|
}
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
getClient := func(s *dc.Session, ia *dc.Interaction, create bool) (client player.Client, err error, created bool) {
|
|||
|
clI, exists := clients.Load(ia.GuildID)
|
|||
|
if exists {
|
|||
|
return clI.(player.Client), nil, false
|
|||
|
}
|
|||
|
|
|||
|
if !create {
|
|||
|
return player.Client{}, ErrVoiceNotConnected, false
|
|||
|
}
|
|||
|
|
|||
|
g, err := s.State.Guild(ia.GuildID)
|
|||
|
if err != nil {
|
|||
|
return player.Client{}, err, false
|
|||
|
}
|
|||
|
|
|||
|
voiceChannelId := ""
|
|||
|
for _, v := range g.VoiceStates {
|
|||
|
if v.UserID == ia.Member.User.ID {
|
|||
|
voiceChannelId = v.ChannelID
|
|||
|
break
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if voiceChannelId == "" {
|
|||
|
return player.Client{}, UserError{errors.New("bot doesn't know where to join, please enter a voice channel")}, false
|
|||
|
}
|
|||
|
|
|||
|
vc, err := s.ChannelVoiceJoin(ia.GuildID, voiceChannelId, false, true)
|
|||
|
if err != nil {
|
|||
|
return player.Client{}, err, false
|
|||
|
}
|
|||
|
|
|||
|
cl := player.NewClient(cfg.Extractors, cfg.FfmpegPath, vc.OpusSend, func(e player.EventStreamUpdated) {
|
|||
|
if err := vc.Speaking(true); err != nil {
|
|||
|
fmt.Println("Unable to speak:", err)
|
|||
|
}
|
|||
|
}, func(e player.EventKilled) {
|
|||
|
vc.Disconnect()
|
|||
|
})
|
|||
|
|
|||
|
clients.Store(ia.GuildID, cl)
|
|||
|
|
|||
|
go func() {
|
|||
|
for err := range cl.ErrCh {
|
|||
|
fmt.Println("Playback error:", err)
|
|||
|
}
|
|||
|
}()
|
|||
|
|
|||
|
return cl, nil, true
|
|||
|
}
|
|||
|
|
|||
|
getOptions := func(d *dc.ApplicationCommandInteractionData) map[string]*dc.ApplicationCommandInteractionDataOption {
|
|||
|
opts := make(map[string]*dc.ApplicationCommandInteractionDataOption, len(d.Options))
|
|||
|
for _, v := range d.Options {
|
|||
|
opts[v.Name] = v
|
|||
|
}
|
|||
|
return opts
|
|||
|
}
|
|||
|
|
|||
|
floatptr := func(f float64) *float64 {
|
|||
|
res := new(float64)
|
|||
|
*res = f
|
|||
|
return res
|
|||
|
}
|
|||
|
|
|||
|
commands := []*dc.ApplicationCommand{
|
|||
|
{
|
|||
|
Name: "queue",
|
|||
|
Description: "Show current playing queue",
|
|||
|
},
|
|||
|
{
|
|||
|
Name: "play",
|
|||
|
Description: "Resume playback or replace playing queue",
|
|||
|
Options: []*dc.ApplicationCommandOption{
|
|||
|
{
|
|||
|
Type: dc.ApplicationCommandOptionString,
|
|||
|
Name: "url-or-query",
|
|||
|
Description: "Video/music/playlist URL or search query to start playing",
|
|||
|
Required: false,
|
|||
|
Autocomplete: true,
|
|||
|
},
|
|||
|
},
|
|||
|
},
|
|||
|
{
|
|||
|
Name: "add",
|
|||
|
Description: "Add a track or playlist to queue",
|
|||
|
Options: []*dc.ApplicationCommandOption{
|
|||
|
{
|
|||
|
Type: dc.ApplicationCommandOptionString,
|
|||
|
Name: "url-or-query",
|
|||
|
Description: "Video/music/playlist URL or search query to add to queue",
|
|||
|
Required: true,
|
|||
|
Autocomplete: true,
|
|||
|
},
|
|||
|
},
|
|||
|
},
|
|||
|
{
|
|||
|
Name: "pause",
|
|||
|
Description: "Pause playback",
|
|||
|
},
|
|||
|
{
|
|||
|
Name: "loop",
|
|||
|
Description: "Toggle loop",
|
|||
|
},
|
|||
|
{
|
|||
|
Name: "stop",
|
|||
|
Description: "Stop playback and disconnect",
|
|||
|
},
|
|||
|
{
|
|||
|
Name: "disconnect",
|
|||
|
Description: "Alias for stop (stop playback and disconnect)",
|
|||
|
},
|
|||
|
{
|
|||
|
Name: "dc",
|
|||
|
Description: "Alias for stop (stop playback and disconnect)",
|
|||
|
},
|
|||
|
{
|
|||
|
Name: "jump",
|
|||
|
Description: "Jump to a track by number or name",
|
|||
|
Options: []*dc.ApplicationCommandOption{
|
|||
|
{
|
|||
|
Type: dc.ApplicationCommandOptionString,
|
|||
|
Name: "track",
|
|||
|
Description: "Track number or matching string",
|
|||
|
Required: true,
|
|||
|
Autocomplete: true,
|
|||
|
},
|
|||
|
},
|
|||
|
},
|
|||
|
{
|
|||
|
Name: "seek",
|
|||
|
Description: "Jump to a playback position. Format is ss, mm:ss or hh:mm:ss. Use prefixes +/- to jump relatively",
|
|||
|
Options: []*dc.ApplicationCommandOption{
|
|||
|
{
|
|||
|
Type: dc.ApplicationCommandOptionString,
|
|||
|
Name: "pos",
|
|||
|
Description: "Target playback position. Format is ss, mm:ss or hh:mm:ss. Use prefixes +/- to jump relatively",
|
|||
|
Required: true,
|
|||
|
},
|
|||
|
},
|
|||
|
},
|
|||
|
{
|
|||
|
Name: "pos",
|
|||
|
Description: "Get current playback position (time)",
|
|||
|
},
|
|||
|
{
|
|||
|
Name: "speed",
|
|||
|
Description: "Get or set the playback speed",
|
|||
|
Options: []*dc.ApplicationCommandOption{
|
|||
|
{
|
|||
|
Type: dc.ApplicationCommandOptionNumber,
|
|||
|
Name: "speed",
|
|||
|
Description: "New playback speed",
|
|||
|
Required: false,
|
|||
|
MinValue: floatptr(0.5),
|
|||
|
MaxValue: 3.0,
|
|||
|
},
|
|||
|
},
|
|||
|
},
|
|||
|
{
|
|||
|
Name: "shuffle",
|
|||
|
Description: "Shuffle all items in the queue",
|
|||
|
Options: []*dc.ApplicationCommandOption{},
|
|||
|
},
|
|||
|
{
|
|||
|
Name: "unshuffle",
|
|||
|
Description: "Undoes what shuffle did (may no longer be available after certain queue modifications)",
|
|||
|
Options: []*dc.ApplicationCommandOption{},
|
|||
|
},
|
|||
|
{
|
|||
|
Name: "swap",
|
|||
|
Description: "Swap two items' positions in the queue",
|
|||
|
Options: []*dc.ApplicationCommandOption{
|
|||
|
{
|
|||
|
Type: dc.ApplicationCommandOptionString,
|
|||
|
Name: "track-1",
|
|||
|
Description: "Track number or matching string",
|
|||
|
Required: true,
|
|||
|
Autocomplete: true,
|
|||
|
},
|
|||
|
{
|
|||
|
Type: dc.ApplicationCommandOptionString,
|
|||
|
Name: "track-2",
|
|||
|
Description: "Track number or matching string",
|
|||
|
Required: true,
|
|||
|
Autocomplete: true,
|
|||
|
},
|
|||
|
},
|
|||
|
},
|
|||
|
{
|
|||
|
Name: "delete",
|
|||
|
Description: "Delete up to five items from the queue (use delete-from to delete even more at once)",
|
|||
|
Options: []*dc.ApplicationCommandOption{
|
|||
|
{
|
|||
|
Type: dc.ApplicationCommandOptionString,
|
|||
|
Name: "track-1",
|
|||
|
Description: "Track number or matching string",
|
|||
|
Required: true,
|
|||
|
Autocomplete: true,
|
|||
|
},
|
|||
|
{
|
|||
|
Type: dc.ApplicationCommandOptionString,
|
|||
|
Name: "track-2",
|
|||
|
Description: "Track number or matching string",
|
|||
|
Required: false,
|
|||
|
Autocomplete: true,
|
|||
|
},
|
|||
|
{
|
|||
|
Type: dc.ApplicationCommandOptionString,
|
|||
|
Name: "track-3",
|
|||
|
Description: "Track number or matching string",
|
|||
|
Required: false,
|
|||
|
Autocomplete: true,
|
|||
|
},
|
|||
|
{
|
|||
|
Type: dc.ApplicationCommandOptionString,
|
|||
|
Name: "track-4",
|
|||
|
Description: "Track number or matching string",
|
|||
|
Required: false,
|
|||
|
Autocomplete: true,
|
|||
|
},
|
|||
|
{
|
|||
|
Type: dc.ApplicationCommandOptionString,
|
|||
|
Name: "track-5",
|
|||
|
Description: "Track number or matching string",
|
|||
|
Required: false,
|
|||
|
Autocomplete: true,
|
|||
|
},
|
|||
|
},
|
|||
|
},
|
|||
|
{
|
|||
|
Name: "delete-from",
|
|||
|
Description: "Delete the specified track along with all tracks following it from the queue",
|
|||
|
Options: []*dc.ApplicationCommandOption{
|
|||
|
{
|
|||
|
Type: dc.ApplicationCommandOptionString,
|
|||
|
Name: "track-1",
|
|||
|
Description: "Track number or matching string",
|
|||
|
Required: true,
|
|||
|
Autocomplete: true,
|
|||
|
},
|
|||
|
},
|
|||
|
},
|
|||
|
}
|
|||
|
|
|||
|
addToQueue := func(s *dc.Session, m *MessageWriter, cl player.Client, input string) error {
|
|||
|
if err := m.StartThinking(); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
|
|||
|
data, err := extractor.Extract(cfg.Extractors, input)
|
|||
|
if err != nil {
|
|||
|
if exerr, ok := err.(*extractor.Error); ok && exerr.Err == ytdl.ErrUnsupportedUrl {
|
|||
|
return ErrUnsupportedUrl
|
|||
|
}
|
|||
|
return err
|
|||
|
}
|
|||
|
|
|||
|
cl.CmdCh <- player.CmdAddBack(data)
|
|||
|
|
|||
|
var msg string
|
|||
|
if len(data) == 1 {
|
|||
|
msg = fmt.Sprintf("Added %v to queue", data[0].Title)
|
|||
|
} else if len(data) > 0 {
|
|||
|
msg = fmt.Sprintf("Added playlist %v to queue (%v items)", data[0].PlaylistTitle, len(data))
|
|||
|
} else {
|
|||
|
return UserError{errors.New("extractor returned no results")}
|
|||
|
}
|
|||
|
|
|||
|
if err := m.Message(&MessageData{Content: msg}); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
matchTracks := func(cl player.Client, search string, n int) []struct {
|
|||
|
title string
|
|||
|
relIdx int
|
|||
|
} {
|
|||
|
var res []struct {
|
|||
|
title string
|
|||
|
relIdx int
|
|||
|
}
|
|||
|
maybeAdd := func(title string, relIdx int) {
|
|||
|
if strings.Contains(strings.ToLower(title), strings.ToLower(search)) {
|
|||
|
res = append(res, struct {
|
|||
|
title string
|
|||
|
relIdx int
|
|||
|
}{
|
|||
|
title: title,
|
|||
|
relIdx: relIdx,
|
|||
|
})
|
|||
|
}
|
|||
|
}
|
|||
|
queue := cl.GetQueue()
|
|||
|
for i, v := range queue.Done {
|
|||
|
maybeAdd(v.Title, i-len(queue.Done))
|
|||
|
}
|
|||
|
if queue.Playing != nil {
|
|||
|
maybeAdd(queue.Playing.Title, 0)
|
|||
|
}
|
|||
|
for i, v := range queue.Ahead {
|
|||
|
maybeAdd(v.Title, i+1)
|
|||
|
}
|
|||
|
sort.Slice(res, func(i, j int) bool {
|
|||
|
cost := func(s string) int {
|
|||
|
return strings.Index(strings.ToLower(s), strings.ToLower(search))
|
|||
|
}
|
|||
|
return cost(res[i].title) < cost(res[j].title)
|
|||
|
})
|
|||
|
if n < len(res) {
|
|||
|
return res[:n]
|
|||
|
} else {
|
|||
|
return res
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
checkQueueBounds := func(queue *player.Queue, i int) error {
|
|||
|
if i == 0 && queue.Playing == nil {
|
|||
|
return UserError{errors.New("track index 0 is invalid when no track is playing")}
|
|||
|
}
|
|||
|
if i < 0 && -i-1 >= len(queue.Done) {
|
|||
|
return UserError{fmt.Errorf("track index %v is too low (minimum is %v)", i, -len(queue.Done))}
|
|||
|
}
|
|||
|
if i > 0 && i-1 >= len(queue.Ahead) {
|
|||
|
return UserError{fmt.Errorf("track index %v is too high (maximum is %v)", i, len(queue.Ahead))}
|
|||
|
}
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
getTrackNum := func(cl player.Client, input string) (int, error) {
|
|||
|
n, err := strconv.Atoi(input)
|
|||
|
if err != nil {
|
|||
|
tracks := matchTracks(cl, input, 1)
|
|||
|
if len(tracks) > 0 {
|
|||
|
return tracks[0].relIdx, nil
|
|||
|
} else {
|
|||
|
return 0, UserError{errors.New("no matching track found")}
|
|||
|
}
|
|||
|
}
|
|||
|
return n, nil
|
|||
|
}
|
|||
|
|
|||
|
getTrackEmbed := func(queue *player.Queue, i int) *dc.MessageEmbed {
|
|||
|
if !queue.InBounds(i) {
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
track := queue.At(i)
|
|||
|
url := track.SourceUrl
|
|||
|
id := strings.TrimSuffix(strings.TrimPrefix(url, "https://www.youtube.com/watch?v="), "/")
|
|||
|
var desc string
|
|||
|
if i == 0 {
|
|||
|
if queue.Paused {
|
|||
|
desc = "Paused"
|
|||
|
} else {
|
|||
|
desc = "Playing"
|
|||
|
}
|
|||
|
if queue.Loop {
|
|||
|
desc += " (loop)"
|
|||
|
}
|
|||
|
} else {
|
|||
|
desc = strconv.Itoa(i)
|
|||
|
}
|
|||
|
return &dc.MessageEmbed{
|
|||
|
Title: track.Title,
|
|||
|
Description: desc,
|
|||
|
URL: url + "/" + strconv.Itoa(i),
|
|||
|
Thumbnail: &dc.MessageEmbedThumbnail{
|
|||
|
URL: "https://i.ytimg.com/vi/" + id + "/mqdefault.jpg",
|
|||
|
},
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
var commandHandlers map[string]func(s *dc.Session, m *MessageWriter, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error
|
|||
|
commandHandlers = map[string]func(s *dc.Session, m *MessageWriter, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error{
|
|||
|
"queue": func(s *dc.Session, m *MessageWriter, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
cl, err, _ := getClient(s, ia, false)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
|
|||
|
queue := cl.GetQueue()
|
|||
|
|
|||
|
if len(queue.Done) == 0 && queue.Playing == nil && len(queue.Ahead) == 0 {
|
|||
|
if err := m.Message(&MessageData{Content: "Queue is empty"}); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
var embeds []*dc.MessageEmbed
|
|||
|
trySend := func(flush bool) error {
|
|||
|
if len(embeds) >= 10 || (len(embeds) > 0 && flush) {
|
|||
|
err := m.Message(&MessageData{
|
|||
|
Embeds: embeds,
|
|||
|
})
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
embeds = nil
|
|||
|
}
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
for i := -len(queue.Done); i <= len(queue.Ahead); i++ {
|
|||
|
if i == 0 && queue.Playing == nil {
|
|||
|
continue
|
|||
|
}
|
|||
|
embeds = append(embeds, getTrackEmbed(queue, i))
|
|||
|
if err := trySend(false); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
}
|
|||
|
if err := trySend(true); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
return nil
|
|||
|
},
|
|||
|
"play": func(s *dc.Session, m *MessageWriter, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
opts := getOptions(d)
|
|||
|
inputI, exists := opts["url-or-query"]
|
|||
|
if exists {
|
|||
|
cl, err, _ := getClient(s, ia, true)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
|
|||
|
input := inputI.StringValue()
|
|||
|
|
|||
|
cl.CmdCh <- player.CmdSkipAll{}
|
|||
|
|
|||
|
err = addToQueue(s, m, cl, input)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
|
|||
|
cl.CmdCh <- player.CmdPlay{}
|
|||
|
} else {
|
|||
|
cl, err, _ := getClient(s, ia, false)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
|
|||
|
queue := cl.GetQueue()
|
|||
|
|
|||
|
if queue.Paused {
|
|||
|
cl.CmdCh <- player.CmdPlay{}
|
|||
|
if err := m.Message(&MessageData{Content: "Playback resumed"}); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
} else if queue.Playing == nil && len(queue.Ahead) > 0 {
|
|||
|
cl.CmdCh <- player.CmdPlay{}
|
|||
|
if err := m.Message(&MessageData{Content: "Started playing"}); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
} else if queue.Playing == nil && len(queue.Ahead) == 0 {
|
|||
|
return UserError{errors.New("nothing in queue to resume from")}
|
|||
|
} else {
|
|||
|
return UserError{errors.New("already playing")}
|
|||
|
}
|
|||
|
}
|
|||
|
return nil
|
|||
|
},
|
|||
|
"add": func(s *dc.Session, m *MessageWriter, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
cl, err, created := getClient(s, ia, true)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
|
|||
|
err = addToQueue(s, m, cl, d.Options[0].StringValue())
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
|
|||
|
if created {
|
|||
|
if err := m.Message(&MessageData{Content: "Use /play to start playing"}); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
}
|
|||
|
return nil
|
|||
|
},
|
|||
|
"pause": func(s *dc.Session, m *MessageWriter, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
cl, err, _ := getClient(s, ia, false)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
if cl.GetQueue().Paused {
|
|||
|
return UserError{errors.New("already paused")}
|
|||
|
} else {
|
|||
|
cl.CmdCh <- player.CmdPause{}
|
|||
|
if err := m.Message(&MessageData{Content: "Playback paused"}); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
}
|
|||
|
return nil
|
|||
|
},
|
|||
|
"loop": func(s *dc.Session, m *MessageWriter, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
cl, err, _ := getClient(s, ia, false)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
loop := cl.GetQueue().Loop
|
|||
|
cl.CmdCh <- player.CmdLoop(!loop)
|
|||
|
var msg string
|
|||
|
if loop {
|
|||
|
msg = "Loop disabled"
|
|||
|
} else {
|
|||
|
msg = "Loop enabled"
|
|||
|
}
|
|||
|
if err := m.Message(&MessageData{Content: msg}); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
return nil
|
|||
|
},
|
|||
|
"stop": func(s *dc.Session, m *MessageWriter, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
cl, err, _ := getClient(s, ia, false)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
ch := make(chan struct{})
|
|||
|
cl.CmdCh <- player.CmdPlayFileAndStop{ch, resByeOpus}
|
|||
|
if err := m.Message(&MessageData{Content: "Bye, have a great time"}); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
<-ch
|
|||
|
clients.Delete(ia.GuildID)
|
|||
|
close(cl.CmdCh)
|
|||
|
return nil
|
|||
|
},
|
|||
|
"disconnect": func(s *dc.Session, m *MessageWriter, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
return commandHandlers["stop"](s, m, ia, d)
|
|||
|
},
|
|||
|
"dc": func(s *dc.Session, m *MessageWriter, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
return commandHandlers["stop"](s, m, ia, d)
|
|||
|
},
|
|||
|
"jump": func(s *dc.Session, m *MessageWriter, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
track := d.Options[0].StringValue()
|
|||
|
cl, err, _ := getClient(s, ia, false)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
|
|||
|
n, err := getTrackNum(cl, track)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
|
|||
|
cl.CmdCh <- player.CmdJump(n)
|
|||
|
|
|||
|
var msg string
|
|||
|
queue := cl.GetQueue()
|
|||
|
if queue.Playing != nil {
|
|||
|
msg = fmt.Sprintf("Jumped to %v", queue.Playing.Title)
|
|||
|
} else {
|
|||
|
msg = "Playback finished"
|
|||
|
}
|
|||
|
if err := m.Message(&MessageData{Content: msg}); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
return nil
|
|||
|
},
|
|||
|
"seek": func(s *dc.Session, m *MessageWriter, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
cl, err, _ := getClient(s, ia, false)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
|
|||
|
var relFactor int
|
|||
|
input := d.Options[0].StringValue()
|
|||
|
if strings.HasPrefix(input, "+") {
|
|||
|
relFactor = 1
|
|||
|
input = strings.TrimPrefix(input, "+")
|
|||
|
} else if strings.HasPrefix(input, "-") {
|
|||
|
relFactor = -1
|
|||
|
input = strings.TrimPrefix(input, "-")
|
|||
|
}
|
|||
|
|
|||
|
ntI, err := util.ParseDurationSeconds(input)
|
|||
|
if err != nil {
|
|||
|
return UserError{errors.New("invalid time format")}
|
|||
|
}
|
|||
|
|
|||
|
queue := cl.GetQueue()
|
|||
|
|
|||
|
if queue.Playing != nil {
|
|||
|
time := int(cl.GetTime())
|
|||
|
d := util.FormatDurationSeconds(int(queue.Playing.Duration))
|
|||
|
if relFactor != 0 {
|
|||
|
ntI = time + relFactor*ntI
|
|||
|
}
|
|||
|
if ntI < 0 || ntI >= queue.Playing.Duration {
|
|||
|
return UserError{errors.New("time out of range")}
|
|||
|
}
|
|||
|
nt := util.FormatDurationSeconds(ntI)
|
|||
|
relI := ntI - time
|
|||
|
var rel string
|
|||
|
if relI < 0 {
|
|||
|
rel = "-" + util.FormatDurationSeconds(-relI)
|
|||
|
} else {
|
|||
|
rel = "+" + util.FormatDurationSeconds(relI)
|
|||
|
}
|
|||
|
cl.CmdCh <- player.CmdSeek(int64(ntI))
|
|||
|
if err := m.Message(&MessageData{Content: fmt.Sprintf("Sought to: %v/%v (%v)", nt, d, rel)}); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
} else {
|
|||
|
return UserError{errors.New("not playing anything")}
|
|||
|
}
|
|||
|
return nil
|
|||
|
},
|
|||
|
"pos": func(s *dc.Session, m *MessageWriter, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
cl, err, _ := getClient(s, ia, false)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
|
|||
|
queue := cl.GetQueue()
|
|||
|
|
|||
|
if queue.Playing != nil {
|
|||
|
time := cl.GetTime()
|
|||
|
t := util.FormatDurationSeconds(int(time))
|
|||
|
d := util.FormatDurationSeconds(int(queue.Playing.Duration))
|
|||
|
|
|||
|
err := m.Message(&MessageData{
|
|||
|
Content: fmt.Sprintf("Position: %v/%v", t, d),
|
|||
|
Embeds: []*dc.MessageEmbed{
|
|||
|
getTrackEmbed(queue, 0),
|
|||
|
},
|
|||
|
})
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
} else {
|
|||
|
return UserError{errors.New("not playing anything")}
|
|||
|
}
|
|||
|
return nil
|
|||
|
},
|
|||
|
"speed": func(s *dc.Session, m *MessageWriter, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
opts := getOptions(d)
|
|||
|
inputI, exists := opts["speed"]
|
|||
|
|
|||
|
cl, err, _ := getClient(s, ia, false)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
|
|||
|
if exists {
|
|||
|
speed := inputI.FloatValue()
|
|||
|
cl.CmdCh <- player.CmdSpeed(speed)
|
|||
|
if err := m.Message(&MessageData{Content: fmt.Sprintf("Playing at %vx speed", speed)}); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
return nil
|
|||
|
} else {
|
|||
|
if err := m.Message(&MessageData{Content: fmt.Sprintf("Current playback speed: %vx", cl.GetSpeed())}); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
return nil
|
|||
|
}
|
|||
|
},
|
|||
|
"shuffle": func(s *dc.Session, m *MessageWriter, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
cl, err, _ := getClient(s, ia, false)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
cl.CmdCh <- player.CmdShuffle{}
|
|||
|
if err := m.Message(&MessageData{Content: fmt.Sprintf("Shuffled queue (%v items)", len(cl.GetQueue().Ahead))}); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
return nil
|
|||
|
},
|
|||
|
"unshuffle": func(s *dc.Session, m *MessageWriter, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
cl, err, _ := getClient(s, ia, false)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
queue := cl.GetQueue()
|
|||
|
if queue.AheadUnshuffled == nil {
|
|||
|
return UserError{errors.New("cannot unshuffle queue: either it is not shuffled, or too many modifications have been made to reverse the shuffle")}
|
|||
|
}
|
|||
|
cl.CmdCh <- player.CmdUnshuffle{}
|
|||
|
if err := m.Message(&MessageData{Content: fmt.Sprintf("Unshuffled queue (%v items)", len(queue.Ahead))}); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
return nil
|
|||
|
},
|
|||
|
"swap": func(s *dc.Session, m *MessageWriter, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
cl, err, _ := getClient(s, ia, false)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
queue := cl.GetQueue()
|
|||
|
sa, sb := d.Options[0].StringValue(), d.Options[1].StringValue()
|
|||
|
|
|||
|
var a, b int
|
|||
|
a, err = getTrackNum(cl, sa)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
b, err = getTrackNum(cl, sb)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
|
|||
|
if err := checkQueueBounds(queue, a); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
if err := checkQueueBounds(queue, b); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
|
|||
|
ta, tb := queue.At(a), queue.At(b)
|
|||
|
cl.CmdCh <- player.CmdSwap{a, b}
|
|||
|
if err := m.Message(&MessageData{Content: fmt.Sprintf("Swapped item %v: '%v' with %v: '%v'", ta.Title, tb.Title)}); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
return nil
|
|||
|
},
|
|||
|
"delete": func(s *dc.Session, m *MessageWriter, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
cl, err, _ := getClient(s, ia, false)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
queue := cl.GetQueue()
|
|||
|
var toDel []int
|
|||
|
for _, opt := range d.Options {
|
|||
|
a, err := getTrackNum(cl, opt.StringValue())
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
if err := checkQueueBounds(queue, a); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
toDel = append(toDel, a)
|
|||
|
}
|
|||
|
cl.CmdCh <- player.CmdDelete(toDel)
|
|||
|
var msg string
|
|||
|
msg = "Deleted "
|
|||
|
for _, i := range toDel {
|
|||
|
msg += fmt.Sprintf("%v: '%v'", i, queue.At(i).Title)
|
|||
|
if i == len(toDel)-2 {
|
|||
|
msg += "and "
|
|||
|
} else if i != len(toDel)-1 {
|
|||
|
msg += ", "
|
|||
|
}
|
|||
|
}
|
|||
|
msg += " from queue"
|
|||
|
if err := m.Message(&MessageData{Content: msg}); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
return nil
|
|||
|
},
|
|||
|
"delete-from": func(s *dc.Session, m *MessageWriter, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
cl, err, _ := getClient(s, ia, false)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
queue := cl.GetQueue()
|
|||
|
a, err := getTrackNum(cl, d.Options[0].StringValue())
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
if err := checkQueueBounds(queue, a); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
var toDel []int
|
|||
|
i := a
|
|||
|
for {
|
|||
|
if i != 0 && queue.At(i) == nil {
|
|||
|
break
|
|||
|
}
|
|||
|
toDel = append(toDel, i)
|
|||
|
i++
|
|||
|
}
|
|||
|
cl.CmdCh <- player.CmdDelete(toDel)
|
|||
|
if err := m.Message(&MessageData{Content: fmt.Sprintf("Deleted %v items starting with %v: '%v'", len(toDel), a, queue.At(a).Title)}); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
return nil
|
|||
|
},
|
|||
|
}
|
|||
|
|
|||
|
autocompleteBySearch := func(s *dc.Session, ia *dc.Interaction, input string) error {
|
|||
|
var choices []*dc.ApplicationCommandOptionChoice
|
|||
|
if strings.HasPrefix(input, "http://") || strings.HasPrefix(input, "https://") {
|
|||
|
choices = []*dc.ApplicationCommandOptionChoice{
|
|||
|
{
|
|||
|
Name: input,
|
|||
|
Value: input,
|
|||
|
},
|
|||
|
}
|
|||
|
} else if input != "" {
|
|||
|
res, err := extractor.Search(cfg.Extractors, input)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
|
|||
|
choices = make([]*dc.ApplicationCommandOptionChoice, len(res))
|
|||
|
for i, v := range res {
|
|||
|
switch {
|
|||
|
case v.Title != "":
|
|||
|
var prefix string
|
|||
|
if v.OfficialArtist {
|
|||
|
prefix = "🎵 "
|
|||
|
}
|
|||
|
choices[i] = &dc.ApplicationCommandOptionChoice{
|
|||
|
Name: prefix + v.Title,
|
|||
|
Value: v.SourceUrl,
|
|||
|
}
|
|||
|
case v.PlaylistTitle != "":
|
|||
|
choices[i] = &dc.ApplicationCommandOptionChoice{
|
|||
|
Name: "𝘗𝘭𝘢𝘺𝘭𝘪𝘴𝘵: " + v.PlaylistTitle,
|
|||
|
Value: v.PlaylistUrl,
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
err = s.InteractionRespond(ia, &dc.InteractionResponse{
|
|||
|
Type: dc.InteractionApplicationCommandAutocompleteResult,
|
|||
|
Data: &dc.InteractionResponseData{
|
|||
|
Choices: choices,
|
|||
|
},
|
|||
|
})
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
autocompleteTrack := func(s *dc.Session, ia *dc.Interaction, input string) error {
|
|||
|
cl, err, _ := getClient(s, ia, false)
|
|||
|
if err != nil {
|
|||
|
if errors.Is(err, ErrVoiceNotConnected) {
|
|||
|
err = s.InteractionRespond(ia, &dc.InteractionResponse{
|
|||
|
Type: dc.InteractionApplicationCommandAutocompleteResult,
|
|||
|
Data: &dc.InteractionResponseData{},
|
|||
|
})
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
return nil
|
|||
|
}
|
|||
|
return err
|
|||
|
}
|
|||
|
|
|||
|
tracks := matchTracks(cl, input, 10)
|
|||
|
|
|||
|
choices := make([]*dc.ApplicationCommandOptionChoice, len(tracks))
|
|||
|
for i := range tracks {
|
|||
|
choices[i] = &dc.ApplicationCommandOptionChoice{
|
|||
|
Name: tracks[i].title,
|
|||
|
Value: strconv.Itoa(tracks[i].relIdx),
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
err = s.InteractionRespond(ia, &dc.InteractionResponse{
|
|||
|
Type: dc.InteractionApplicationCommandAutocompleteResult,
|
|||
|
Data: &dc.InteractionResponseData{
|
|||
|
Choices: choices,
|
|||
|
},
|
|||
|
})
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
autocompleteHandlers := map[string]func(s *dc.Session, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error{
|
|||
|
"play": func(s *dc.Session, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
opts := getOptions(d)
|
|||
|
inputI, exists := opts["url-or-query"]
|
|||
|
if !exists {
|
|||
|
return nil
|
|||
|
}
|
|||
|
input := inputI.StringValue()
|
|||
|
|
|||
|
return autocompleteBySearch(s, ia, input)
|
|||
|
},
|
|||
|
"add": func(s *dc.Session, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
return autocompleteBySearch(s, ia, d.Options[0].StringValue())
|
|||
|
},
|
|||
|
"jump": func(s *dc.Session, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
return autocompleteTrack(s, ia, d.Options[0].StringValue())
|
|||
|
},
|
|||
|
"swap": func(s *dc.Session, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
for i := 0; i < 5; i++ {
|
|||
|
if d.Options[i].Focused {
|
|||
|
return autocompleteTrack(s, ia, d.Options[i].StringValue())
|
|||
|
}
|
|||
|
}
|
|||
|
return ErrInvalidAutocompleteCall
|
|||
|
},
|
|||
|
"delete": func(s *dc.Session, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
for i := 0; i < 5; i++ {
|
|||
|
if d.Options[i].Focused {
|
|||
|
return autocompleteTrack(s, ia, d.Options[i].StringValue())
|
|||
|
}
|
|||
|
}
|
|||
|
return ErrInvalidAutocompleteCall
|
|||
|
},
|
|||
|
"delete-from": func(s *dc.Session, ia *dc.Interaction, d *dc.ApplicationCommandInteractionData) error {
|
|||
|
return autocompleteTrack(s, ia, d.Options[0].StringValue())
|
|||
|
},
|
|||
|
}
|
|||
|
|
|||
|
componentHandlers := map[string]func(s *dc.Session, m *MessageWriter, ia *dc.Interaction, d *dc.MessageComponentInteractionData) error{}
|
|||
|
|
|||
|
// Create Discord session
|
|||
|
dg, err := dc.New("Bot " + cfg.Token)
|
|||
|
if err != nil {
|
|||
|
fmt.Println("Error creating Discord session:", err)
|
|||
|
return
|
|||
|
}
|
|||
|
dg.Identify.Intents = dc.IntentsAllWithoutPrivileged
|
|||
|
|
|||
|
// Set up handlers
|
|||
|
readyCh := make(chan string)
|
|||
|
dg.AddHandler(func(s *dc.Session, e *dc.Ready) {
|
|||
|
u := s.State.User
|
|||
|
readyCh <- u.Username + "#" + u.Discriminator
|
|||
|
})
|
|||
|
dg.AddHandler(func(s *dc.Session, e *dc.InteractionCreate) {
|
|||
|
switch e.Type {
|
|||
|
case dc.InteractionApplicationCommand:
|
|||
|
d := e.ApplicationCommandData()
|
|||
|
m := NewMessageWriter(s, e.Interaction)
|
|||
|
if e.GuildID == "" {
|
|||
|
if err := m.Message(&MessageData{Content: "This bot only works on servers"}); err != nil {
|
|||
|
fmt.Printf("Error: %v\n", err)
|
|||
|
}
|
|||
|
return
|
|||
|
}
|
|||
|
if h, exists := commandHandlers[d.Name]; exists {
|
|||
|
if err := h(s, m, e.Interaction, &d); err != nil {
|
|||
|
if _, ok := err.(UserError); ok {
|
|||
|
if err := m.Message(&MessageData{Content: util.CapitalizeFirst(err.Error())}); err != nil {
|
|||
|
fmt.Printf("Error: %v\n", err)
|
|||
|
}
|
|||
|
} else {
|
|||
|
fmt.Printf("Error in commandHandlers[%v]: %v\n", d.Name, err)
|
|||
|
if err := m.Message(&MessageData{Content: "An internal error occurred :("}); err != nil {
|
|||
|
fmt.Printf("Error: %v\n", err)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
case dc.InteractionApplicationCommandAutocomplete:
|
|||
|
d := e.ApplicationCommandData()
|
|||
|
if h, exists := autocompleteHandlers[d.Name]; exists {
|
|||
|
if err := h(s, e.Interaction, &d); err != nil {
|
|||
|
fmt.Printf("Error in autocompleteHandlers[%v]: %v\n", d.Name, err)
|
|||
|
}
|
|||
|
}
|
|||
|
case dc.InteractionMessageComponent:
|
|||
|
d := e.MessageComponentData()
|
|||
|
m := NewMessageWriter(s, e.Interaction)
|
|||
|
if h, exists := componentHandlers[d.CustomID]; exists {
|
|||
|
if err := h(s, m, e.Interaction, &d); err != nil {
|
|||
|
if _, ok := err.(UserError); ok {
|
|||
|
if err := m.Message(&MessageData{Content: util.CapitalizeFirst(err.Error())}); err != nil {
|
|||
|
fmt.Printf("Error: %v\n", err)
|
|||
|
}
|
|||
|
} else {
|
|||
|
fmt.Printf("Error in componentHandlers[%v]: %v\n", d.CustomID, err)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
default:
|
|||
|
fmt.Println("Unhandled interaction type:", e.Type)
|
|||
|
}
|
|||
|
})
|
|||
|
|
|||
|
// Open Discord session
|
|||
|
err = dg.Open()
|
|||
|
if err != nil {
|
|||
|
fmt.Println("Error opening Discord session:", err)
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
// Wait until discord session ready
|
|||
|
fmt.Printf("Logged in as %v\n", <-readyCh)
|
|||
|
|
|||
|
// Set up commands
|
|||
|
if registerCommands {
|
|||
|
fmt.Println("Registering commands...")
|
|||
|
for i, v := range commands {
|
|||
|
cmd, err := dg.ApplicationCommandCreate(dg.State.User.ID, "", v)
|
|||
|
if err != nil {
|
|||
|
fmt.Printf("Error adding command %v: %v\n", v.Name, err)
|
|||
|
return
|
|||
|
}
|
|||
|
commands[i] = cmd
|
|||
|
fmt.Printf(" %v/%v done\r", i+1, len(commands))
|
|||
|
}
|
|||
|
fmt.Printf("%v commands registered\n", len(commands))
|
|||
|
}
|
|||
|
|
|||
|
// Exit gracefully when the program is terminated
|
|||
|
fmt.Println("Bot is now running, press Ctrl+C to stop")
|
|||
|
sc := make(chan os.Signal, 1)
|
|||
|
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
|
|||
|
<-sc
|
|||
|
fmt.Println()
|
|||
|
fmt.Println("Received stop signal, shutting down cleanly")
|
|||
|
clients.Range(func(key, value any) bool {
|
|||
|
cl := value.(player.Client)
|
|||
|
close(cl.CmdCh)
|
|||
|
return true
|
|||
|
})
|
|||
|
if registerCommands && unregisterCommands {
|
|||
|
fmt.Println("Unregistering commands...")
|
|||
|
for i, v := range commands {
|
|||
|
err := dg.ApplicationCommandDelete(dg.State.User.ID, "", v.ID)
|
|||
|
if err != nil {
|
|||
|
fmt.Printf("Error deleting command %v: %v\n", v.Name, err)
|
|||
|
return
|
|||
|
}
|
|||
|
fmt.Printf(" %v/%v done\r", i+1, len(commands))
|
|||
|
}
|
|||
|
fmt.Printf("%v commands unregistered\n", len(commands))
|
|||
|
}
|
|||
|
dg.Close()
|
|||
|
}
|