360 lines
7.9 KiB
Go
360 lines
7.9 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"sync"
|
|
"syscall"
|
|
|
|
"dcbot/dca0"
|
|
"dcbot/util"
|
|
|
|
"github.com/bwmarrin/discordgo"
|
|
)
|
|
|
|
////////////////////////////////
|
|
// Helper functions.
|
|
////////////////////////////////
|
|
// The first return value contains the voice channel ID, if it was found. If it
|
|
// was not found, it is set to "".
|
|
// The second return value indicates whether the voice channel was found.
|
|
func GetUserVoiceChannel(g *discordgo.Guild, userID string) (string, bool) {
|
|
for _, vs := range g.VoiceStates {
|
|
if vs.UserID == userID {
|
|
return vs.ChannelID, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
////////////////////////////////
|
|
// Structs.
|
|
////////////////////////////////
|
|
type Playback struct {
|
|
Track
|
|
CmdCh chan dca0.Command
|
|
RespCh chan dca0.Response
|
|
Paused bool
|
|
Loop bool // Whether playback is looping right now.
|
|
}
|
|
|
|
type Track struct {
|
|
Title string // Title, if any.
|
|
Url string // Short URL, for example from YouTube.
|
|
MediaUrl string // Long URL of the associated media file.
|
|
}
|
|
|
|
// All methods of Client are thread safe, however manual locking is required
|
|
// when accessing any fields.
|
|
type Client struct {
|
|
sync.RWMutex
|
|
|
|
// The discordgo session.
|
|
s *discordgo.Session
|
|
|
|
// TextChannelID and VoiceChannelID indicate the current channels through
|
|
// which the bot should send text / audio. They may be set to "".
|
|
TextChannelID string
|
|
VoiceChannelID string
|
|
|
|
// Current audio playback.
|
|
Playback *Playback
|
|
// Queue.
|
|
Queue []*Track
|
|
}
|
|
|
|
func NewClient(s *discordgo.Session) *Client {
|
|
return &Client{
|
|
s: s,
|
|
}
|
|
}
|
|
|
|
func (c *Client) Messagef(format string, a ...interface{}) {
|
|
c.RLock()
|
|
if c.TextChannelID == "" {
|
|
fmt.Printf(format+"\n", a...)
|
|
} else {
|
|
c.s.ChannelMessageSend(c.TextChannelID, fmt.Sprintf(format, a...))
|
|
}
|
|
c.RUnlock()
|
|
}
|
|
|
|
// Updates the text channel and voice channel IDs. May set them to "" if there
|
|
// are none associated with the message.
|
|
func (c *Client) UpdateChannels(g *discordgo.Guild, m *discordgo.Message) {
|
|
c.Lock()
|
|
c.TextChannelID = m.ChannelID
|
|
|
|
vc, _ := GetUserVoiceChannel(g, m.Author.ID)
|
|
c.VoiceChannelID = vc
|
|
c.Unlock()
|
|
}
|
|
|
|
func (c *Client) GetTextChannelID() string {
|
|
c.RLock()
|
|
ret := c.TextChannelID
|
|
c.RUnlock()
|
|
return ret
|
|
}
|
|
|
|
func (c *Client) GetVoiceChannelID() string {
|
|
c.RLock()
|
|
ret := c.TextChannelID
|
|
c.RUnlock()
|
|
return ret
|
|
}
|
|
|
|
// Returns a COPY of c.Playback. Modifications to the returned Playback struct
|
|
// are NOT preserved, as it is a copy.
|
|
func (c *Client) GetPlaybackInfo() (p Playback, ok bool) {
|
|
c.RLock()
|
|
ret := c.Playback
|
|
c.RUnlock()
|
|
if ret == nil {
|
|
return Playback{}, false
|
|
} else {
|
|
return *ret, true
|
|
}
|
|
}
|
|
|
|
func (c *Client) QueueLen() int {
|
|
c.RLock()
|
|
l := len(c.Queue)
|
|
c.RUnlock()
|
|
return l
|
|
}
|
|
|
|
// Similarly to GetPlaybackInfo, this function returns a COPY. Any modifications
|
|
// are NOT preserved.
|
|
// ok field returns false if the index is out of bounds.
|
|
func (c *Client) QueueAt(i int) (t Track, ok bool) {
|
|
l := c.QueueLen()
|
|
if i >= l {
|
|
return Track{}, false
|
|
}
|
|
c.RLock()
|
|
ret := c.Queue[i]
|
|
c.RUnlock()
|
|
return *ret, true
|
|
}
|
|
|
|
func (c *Client) QueuePushBack(t *Track) {
|
|
c.Lock()
|
|
c.Queue = append(c.Queue, t)
|
|
c.Unlock()
|
|
}
|
|
|
|
func (c *Client) QueuePushFront(t *Track) {
|
|
c.Lock()
|
|
c.Queue = append([]*Track{t}, c.Queue...)
|
|
c.Unlock()
|
|
}
|
|
|
|
func (c *Client) QueuePopFront() (t Track, ok bool) {
|
|
t, ok = c.QueueAt(0)
|
|
if ok {
|
|
c.Lock()
|
|
c.Queue = c.Queue[1:]
|
|
c.Unlock()
|
|
}
|
|
return t, ok
|
|
}
|
|
|
|
// Deletes a single item at any position.
|
|
// Returns false if i was out of bounds.
|
|
func (c *Client) QueueDelete(i int) bool {
|
|
if i >= c.QueueLen() {
|
|
return false
|
|
}
|
|
c.Lock()
|
|
c.Queue = append(c.Queue[:i], c.Queue[i+1:]...)
|
|
c.Unlock()
|
|
return true
|
|
}
|
|
|
|
// Swaps two items in the queue.
|
|
// Returns false if a or b is out of bounds.
|
|
func (c *Client) QueueSwap(a, b int) bool {
|
|
if a == b {
|
|
return true
|
|
}
|
|
l := c.QueueLen()
|
|
if a >= l || b >= l {
|
|
return false
|
|
}
|
|
c.Lock()
|
|
c.Queue[a], c.Queue[b] = c.Queue[b], c.Queue[a]
|
|
c.Unlock()
|
|
return true
|
|
}
|
|
|
|
func (c *Client) QueueClear() {
|
|
c.Lock()
|
|
c.Queue = nil
|
|
c.Unlock()
|
|
}
|
|
|
|
func (c *Client) QueueFront() (t Track, ok bool) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
if len(c.Queue) == 0 {
|
|
return Track{}, false
|
|
}
|
|
ret := *c.Queue[0]
|
|
return ret, true
|
|
}
|
|
|
|
////////////////////////////////
|
|
// Global variables.
|
|
////////////////////////////////
|
|
var clients map[string]*Client // Guild ID to client
|
|
var mClients sync.Mutex
|
|
|
|
var cfg Config
|
|
|
|
////////////////////////////////
|
|
// Main program.
|
|
////////////////////////////////
|
|
func main() {
|
|
if err := ReadConfig(&cfg); err != nil {
|
|
fmt.Println(err)
|
|
if err := WriteDefaultConfig(); err != nil {
|
|
fmt.Println("Failed to create the default configuration file:", err)
|
|
return
|
|
}
|
|
fmt.Println("Wrote the default configuration to " + configFile + ".")
|
|
fmt.Println("You will have to manually configure the token by editing " + configFile + ".")
|
|
return
|
|
}
|
|
|
|
if cfg.Token == tokenDefaultString {
|
|
fmt.Println("Please set your bot token in " + configFile + " first.")
|
|
return
|
|
}
|
|
|
|
// Check if all binary dependencies are installed correctly.
|
|
const notInstalledErrMsg = "Unable to find %s in the specified path '%s', please make sure it's installed correctly.\nYou can manually set its path by editing %s\n"
|
|
if !util.CheckInstalled(cfg.YtdlPath, "--version") {
|
|
fmt.Printf(notInstalledErrMsg, "youtube-dl", cfg.YtdlPath, configFile)
|
|
return
|
|
}
|
|
if !util.CheckInstalled(cfg.FfmpegPath, "-version") {
|
|
fmt.Printf(notInstalledErrMsg, "ffmpeg", cfg.FfmpegPath, configFile)
|
|
return
|
|
}
|
|
|
|
// Initialize client map.
|
|
clients = make(map[string]*Client)
|
|
|
|
// Initialize bot.
|
|
dg, err := discordgo.New("Bot " + cfg.Token)
|
|
if err != nil {
|
|
fmt.Println("Error creating Discord session:", err)
|
|
return
|
|
}
|
|
|
|
dg.AddHandler(ready)
|
|
dg.AddHandler(banAdd)
|
|
dg.AddHandler(messageCreate)
|
|
|
|
// What information we need about guilds.
|
|
dg.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildMessages | discordgo.IntentsGuildVoiceStates | discordgo.IntentsGuildBans
|
|
|
|
// Open the websocket and begin listening.
|
|
err = dg.Open()
|
|
if err != nil {
|
|
fmt.Println("Error opening Discord session:", err)
|
|
return
|
|
}
|
|
|
|
// Wait here until Ctrl+c or other term signal is received.
|
|
fmt.Println("Bot is now running. Press Ctrl+c to exit.")
|
|
sc := make(chan os.Signal, 1)
|
|
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
|
|
<-sc
|
|
|
|
fmt.Println("\nSignal received, closing Discord session.")
|
|
|
|
// Cleanly close down the Discord session.
|
|
dg.Close()
|
|
}
|
|
|
|
func ready(s *discordgo.Session, event *discordgo.Ready) {
|
|
u := s.State.User
|
|
fmt.Println("Logged in as", u.Username+"#"+u.Discriminator+".")
|
|
s.UpdateListeningStatus(cfg.Prefix + "help")
|
|
}
|
|
|
|
func banAdd(s *discordgo.Session, event *discordgo.GuildBanAdd) {
|
|
fmt.Println(event)
|
|
}
|
|
|
|
func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
|
|
// Ignore messages from the bot itself.
|
|
if m.Author.ID == s.State.User.ID {
|
|
return
|
|
}
|
|
|
|
var g *discordgo.Guild
|
|
g, err := s.State.Guild(m.GuildID)
|
|
if err != nil {
|
|
// Could not find guild.
|
|
s.ChannelMessageSend(m.ChannelID, "This bot only works in guilds (servers).")
|
|
return
|
|
}
|
|
|
|
var c *Client
|
|
mClients.Lock()
|
|
{
|
|
var ok bool
|
|
if c, ok = clients[m.GuildID]; !ok {
|
|
c = NewClient(s)
|
|
clients[m.GuildID] = c
|
|
}
|
|
}
|
|
mClients.Unlock()
|
|
// Update the text and voice channels associated with the client.
|
|
c.UpdateChannels(g, m.Message)
|
|
|
|
args, ok := CmdGetArgs(m.Content)
|
|
if !ok {
|
|
// Not a command.
|
|
return
|
|
}
|
|
|
|
if len(args) == 0 {
|
|
c.Messagef("No command specified. Type `%shelp` for help.", cfg.Prefix)
|
|
return
|
|
}
|
|
|
|
switch args[0] {
|
|
case "help":
|
|
commandHelp(c)
|
|
case "play":
|
|
commandPlay(s, g, c, args[1:])
|
|
case "seek":
|
|
commandSeek(c, args[1:])
|
|
case "pos":
|
|
commandPos(c)
|
|
case "loop":
|
|
commandLoop(c)
|
|
case "add":
|
|
commandAdd(c, args[1:], false)
|
|
case "queue":
|
|
commandQueue(c)
|
|
case "pause":
|
|
commandPause(c)
|
|
case "stop":
|
|
commandStop(c)
|
|
case "skip":
|
|
commandSkip(c)
|
|
case "delete":
|
|
commandDelete(c, args[1:])
|
|
case "swap":
|
|
commandSwap(c, args[1:])
|
|
case "shuffle":
|
|
commandShuffle(c)
|
|
}
|
|
}
|