2016-06-21 02:16:05 +02:00
|
|
|
/*
|
|
|
|
* MumbleDJ
|
|
|
|
* By Matthieu Grieger
|
|
|
|
* bot/mumbledj.go
|
|
|
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
|
|
|
*/
|
|
|
|
|
|
|
|
package bot
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/tls"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
|
|
"github.com/layeh/gumble/gumble"
|
|
|
|
"github.com/layeh/gumble/gumbleffmpeg"
|
|
|
|
"github.com/layeh/gumble/gumbleutil"
|
|
|
|
"github.com/matthieugrieger/mumbledj/interfaces"
|
|
|
|
"github.com/spf13/viper"
|
|
|
|
)
|
|
|
|
|
|
|
|
// MumbleDJ is a struct that keeps track of all aspects of the bot's state.
|
|
|
|
type MumbleDJ struct {
|
|
|
|
AvailableServices []interfaces.Service
|
|
|
|
Client *gumble.Client
|
|
|
|
GumbleConfig *gumble.Config
|
|
|
|
TLSConfig *tls.Config
|
|
|
|
AudioStream *gumbleffmpeg.Stream
|
|
|
|
Queue interfaces.Queue
|
|
|
|
Cache *Cache
|
|
|
|
Skips interfaces.SkipTracker
|
|
|
|
Commands []interfaces.Command
|
|
|
|
Version string
|
|
|
|
Volume float32
|
|
|
|
YouTubeDL *YouTubeDL
|
|
|
|
KeepAlive chan bool
|
|
|
|
}
|
|
|
|
|
|
|
|
// DJ is a struct that keeps track of all aspects of MumbleDJ's environment.
|
|
|
|
var DJ *MumbleDJ
|
|
|
|
|
|
|
|
// NewMumbleDJ initializes and returns a MumbleDJ type.
|
|
|
|
func NewMumbleDJ() *MumbleDJ {
|
|
|
|
SetDefaultConfig()
|
|
|
|
|
|
|
|
return &MumbleDJ{
|
|
|
|
AvailableServices: make([]interfaces.Service, 0),
|
|
|
|
TLSConfig: new(tls.Config),
|
|
|
|
Queue: NewQueue(),
|
|
|
|
Cache: NewCache(),
|
|
|
|
Skips: NewSkipTracker(),
|
|
|
|
Commands: make([]interfaces.Command, 0),
|
|
|
|
YouTubeDL: new(YouTubeDL),
|
|
|
|
KeepAlive: make(chan bool),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// OnConnect event. First moves MumbleDJ into the default channel if one exists.
|
|
|
|
// The configuration is loaded and the audio stream is initialized.
|
|
|
|
func (dj *MumbleDJ) OnConnect(e *gumble.ConnectEvent) {
|
|
|
|
dj.AudioStream = nil
|
|
|
|
logrus.WithFields(logrus.Fields{
|
|
|
|
"volume": fmt.Sprintf("%.2f", viper.GetFloat64("volume.default")),
|
|
|
|
}).Infoln("Setting default volume...")
|
|
|
|
dj.Volume = float32(viper.GetFloat64("volume.default"))
|
|
|
|
|
|
|
|
if viper.GetBool("cache.enabled") {
|
|
|
|
logrus.Infoln("Caching enabled.")
|
|
|
|
dj.Cache.UpdateStatistics()
|
|
|
|
go dj.Cache.CleanPeriodically()
|
|
|
|
} else {
|
|
|
|
logrus.Infoln("Caching disabled.")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// OnDisconnect event. Terminates MumbleDJ process or retries connection if
|
|
|
|
// automatic connection retries are enabled.
|
|
|
|
func (dj *MumbleDJ) OnDisconnect(e *gumble.DisconnectEvent) {
|
|
|
|
if viper.GetBool("connection.retry_enabled") &&
|
|
|
|
(e.Type == gumble.DisconnectError || e.Type == gumble.DisconnectKicked) {
|
|
|
|
logrus.WithFields(logrus.Fields{
|
|
|
|
"interval_secs": fmt.Sprintf("%d", viper.GetInt("connection.retry_interval")),
|
|
|
|
"attempts": fmt.Sprintf("%d", viper.GetInt("connection.retry_attempts")),
|
|
|
|
}).Warnln("Disconnected from server. Retrying connection...")
|
|
|
|
|
|
|
|
success := false
|
|
|
|
for retries := 0; retries < viper.GetInt("connection.retry_attempts"); retries++ {
|
|
|
|
logrus.Infoln("Retrying connection...")
|
|
|
|
if client, err := gumble.DialWithDialer(new(net.Dialer), viper.GetString("connection.address")+":"+viper.GetString("connection.port"), dj.GumbleConfig, dj.TLSConfig); err == nil {
|
|
|
|
dj.Client = client
|
|
|
|
logrus.Infoln("Successfully reconnected to the server!")
|
|
|
|
success = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
time.Sleep(time.Duration(viper.GetInt("connection.retry_interval")) * time.Second)
|
|
|
|
}
|
|
|
|
if !success {
|
|
|
|
dj.KeepAlive <- true
|
|
|
|
logrus.Fatalln("Could not reconnect to server. Exiting...")
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
dj.KeepAlive <- true
|
|
|
|
logrus.Fatalln("Disconnected from server. No reconnect attempts will be made.")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// OnTextMessage event. Checks for command prefix and passes it to the Commander
|
|
|
|
// if it exists. Ignores the incoming message otherwise.
|
|
|
|
func (dj *MumbleDJ) OnTextMessage(e *gumble.TextMessageEvent) {
|
|
|
|
plainMessage := gumbleutil.PlainText(&e.TextMessage)
|
|
|
|
if len(plainMessage) != 0 {
|
|
|
|
if plainMessage[0] == viper.GetString("commands.prefix")[0] &&
|
|
|
|
plainMessage != viper.GetString("commands.prefix") {
|
|
|
|
go func() {
|
|
|
|
message, isPrivateMessage, err := dj.FindAndExecuteCommand(e.Sender, plainMessage[1:])
|
|
|
|
if err != nil {
|
|
|
|
logrus.WithFields(logrus.Fields{
|
|
|
|
"user": e.Sender.Name,
|
|
|
|
"message": err.Error(),
|
|
|
|
}).Warnln("Sending an error message...")
|
2016-06-26 08:23:33 +02:00
|
|
|
dj.SendPrivateMessage(e.Sender, fmt.Sprintf("<b>Error:</b> %s", err.Error()))
|
2016-06-21 02:16:05 +02:00
|
|
|
} else {
|
|
|
|
if isPrivateMessage {
|
|
|
|
logrus.WithFields(logrus.Fields{
|
|
|
|
"user": e.Sender.Name,
|
|
|
|
"message": message,
|
|
|
|
}).Infoln("Sending a private message...")
|
|
|
|
dj.SendPrivateMessage(e.Sender, message)
|
|
|
|
} else {
|
|
|
|
logrus.WithFields(logrus.Fields{
|
|
|
|
"channel": dj.Client.Self.Channel.Name,
|
|
|
|
"message": message,
|
|
|
|
}).Infoln("Sending a message to channel...")
|
|
|
|
dj.Client.Self.Channel.Send(message, false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// OnUserChange event. Checks UserChange type and adjusts skip trackers to
|
|
|
|
// reflect the current status of the users on the server.
|
|
|
|
func (dj *MumbleDJ) OnUserChange(e *gumble.UserChangeEvent) {
|
|
|
|
if e.Type.Has(gumble.UserChangeDisconnected) || e.Type.Has(gumble.UserChangeChannel) {
|
|
|
|
logrus.WithFields(logrus.Fields{
|
|
|
|
"user": e.User.Name,
|
|
|
|
}).Infoln("A user has disconnected or changed channels, updating skip trackers...")
|
|
|
|
dj.Skips.RemoveTrackSkip(e.User)
|
|
|
|
dj.Skips.RemovePlaylistSkip(e.User)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SendPrivateMessage sends a private message to the specified user. This method
|
|
|
|
// verifies that the targeted user is still present in the server before attempting
|
|
|
|
// to send the message.
|
|
|
|
func (dj *MumbleDJ) SendPrivateMessage(user *gumble.User, message string) {
|
|
|
|
dj.Client.Do(func() {
|
|
|
|
if targetUser := dj.Client.Self.Channel.Users.Find(user.Name); targetUser != nil {
|
|
|
|
targetUser.Send(message)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// IsAdmin checks whether a particular Mumble user is a MumbleDJ admin.
|
|
|
|
// Returns true if the user is an admin, and false otherwise.
|
|
|
|
func (dj *MumbleDJ) IsAdmin(user *gumble.User) bool {
|
|
|
|
for _, admin := range viper.GetStringSlice("admins.names") {
|
|
|
|
if user.Name == admin {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Connect starts the process for connecting to a Mumble server.
|
|
|
|
func (dj *MumbleDJ) Connect() error {
|
|
|
|
// Perform startup checks before connecting.
|
|
|
|
logrus.Infoln("Performing startup checks...")
|
|
|
|
PerformStartupChecks()
|
|
|
|
|
|
|
|
// Create Gumble config.
|
|
|
|
dj.GumbleConfig = gumble.NewConfig()
|
|
|
|
dj.GumbleConfig.Username = viper.GetString("connection.username")
|
|
|
|
dj.GumbleConfig.Password = viper.GetString("connection.password")
|
|
|
|
dj.GumbleConfig.Tokens = strings.Split(viper.GetString("connection.access_tokens"), ",")
|
|
|
|
|
|
|
|
// Initialize key pair if needed.
|
|
|
|
if viper.GetBool("connection.insecure") {
|
|
|
|
dj.TLSConfig.InsecureSkipVerify = true
|
|
|
|
}
|
|
|
|
if viper.GetString("connection.cert") != "" {
|
|
|
|
if viper.GetString("connection.key") != "" {
|
|
|
|
viper.Set("connection.key", viper.GetString("connection.cert"))
|
|
|
|
}
|
|
|
|
|
|
|
|
if certificate, err := tls.LoadX509KeyPair(viper.GetString("connection.cert"), viper.GetString("connection.key")); err == nil {
|
|
|
|
dj.TLSConfig.Certificates = append(dj.TLSConfig.Certificates, certificate)
|
|
|
|
} else {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
dj.GumbleConfig.Attach(gumbleutil.Listener{
|
|
|
|
Connect: dj.OnConnect,
|
|
|
|
Disconnect: dj.OnDisconnect,
|
|
|
|
TextMessage: dj.OnTextMessage,
|
|
|
|
UserChange: dj.OnUserChange,
|
|
|
|
})
|
|
|
|
dj.GumbleConfig.Attach(gumbleutil.AutoBitrate)
|
|
|
|
|
|
|
|
var connErr error
|
|
|
|
|
|
|
|
logrus.WithFields(logrus.Fields{
|
|
|
|
"address": viper.GetString("connection.address"),
|
|
|
|
"port": viper.GetString("connection.port"),
|
|
|
|
}).Infoln("Attempting connection to server...")
|
|
|
|
if dj.Client, connErr = gumble.DialWithDialer(new(net.Dialer), viper.GetString("connection.address")+":"+viper.GetString("connection.port"), dj.GumbleConfig, dj.TLSConfig); connErr != nil {
|
|
|
|
return connErr
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// FindAndExecuteCommand attempts to find a reference to a command in an
|
|
|
|
// incoming message. If found, the command is executed and the resulting
|
|
|
|
// message/error is returned.
|
|
|
|
func (dj *MumbleDJ) FindAndExecuteCommand(user *gumble.User, message string) (string, bool, error) {
|
|
|
|
command, err := dj.findCommand(message)
|
|
|
|
if err != nil {
|
|
|
|
return "", true, errors.New("No command was found in this message")
|
|
|
|
}
|
|
|
|
return dj.executeCommand(user, message, command)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetService loops through the available services and determines if a URL
|
|
|
|
// matches a particular service. If a match is found, the service object is
|
|
|
|
// returned.
|
|
|
|
func (dj *MumbleDJ) GetService(url string) (interfaces.Service, error) {
|
|
|
|
for _, service := range dj.AvailableServices {
|
|
|
|
if service.CheckURL(url) {
|
|
|
|
return service, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil, errors.New("The provided URL does not match an enabled service")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (dj *MumbleDJ) findCommand(message string) (interfaces.Command, error) {
|
|
|
|
var possibleCommand string
|
|
|
|
if strings.Contains(message, " ") {
|
|
|
|
possibleCommand = strings.ToLower(message[:strings.Index(message, " ")])
|
|
|
|
} else {
|
|
|
|
possibleCommand = strings.ToLower(message)
|
|
|
|
}
|
|
|
|
for _, command := range dj.Commands {
|
|
|
|
for _, alias := range command.Aliases() {
|
|
|
|
if possibleCommand == alias {
|
|
|
|
return command, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil, errors.New("No command was found in this message")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (dj *MumbleDJ) executeCommand(user *gumble.User, message string, command interfaces.Command) (string, bool, error) {
|
|
|
|
canExecute := false
|
2016-06-25 19:43:33 +02:00
|
|
|
if viper.GetBool("admins.enabled") && command.IsAdminCommand() {
|
2016-06-21 02:16:05 +02:00
|
|
|
canExecute = dj.IsAdmin(user)
|
|
|
|
} else {
|
|
|
|
canExecute = true
|
|
|
|
}
|
|
|
|
|
|
|
|
if canExecute {
|
|
|
|
return command.Execute(user, strings.Split(message, " ")[1:]...)
|
|
|
|
}
|
|
|
|
return "", true, errors.New("You do not have permission to execute this command")
|
|
|
|
}
|