forked from matthieugrieger/mumbledj
/
main.go
249 lines (216 loc) · 7.57 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
/*
* MumbleDJ
* By Matthieu Grieger
* main.go
* Copyright (c) 2014, 2015 Matthieu Grieger (MIT License)
*/
package main
import (
"crypto/tls"
"flag"
"fmt"
"os"
"os/user"
"reflect"
"strings"
"time"
"github.com/layeh/gopus"
"github.com/layeh/gumble/gumble"
"github.com/layeh/gumble/gumble_ffmpeg"
"github.com/layeh/gumble/gumbleutil"
)
// mumbledj is a struct that keeps track of all aspects of the bot's current
// state.
type mumbledj struct {
config gumble.Config
client *gumble.Client
keepAlive chan bool
defaultChannel []string
conf DjConfig
queue *SongQueue
audioStream *gumble_ffmpeg.Stream
homeDir string
playlistSkips map[string][]string
cache *SongCache
}
// OnConnect event. First moves MumbleDJ into the default channel specified
// via commandline args, and moves to root channel if the channel does not exist. The current
// user's homedir path is stored, configuration is loaded, and the audio stream is set up.
func (dj *mumbledj) OnConnect(e *gumble.ConnectEvent) {
if dj.client.Channels.Find(dj.defaultChannel...) != nil {
dj.client.Self.Move(dj.client.Channels.Find(dj.defaultChannel...))
} else {
fmt.Println("Channel doesn't exist or one was not provided, staying in root channel...")
}
dj.audioStream = gumble_ffmpeg.New(dj.client)
dj.audioStream.Volume = dj.conf.Volume.DefaultVolume
dj.client.AudioEncoder.SetApplication(gopus.Audio)
dj.client.Self.SetComment(dj.conf.General.DefaultComment)
if dj.conf.Cache.Enabled {
dj.cache.Update()
go dj.cache.ClearExpired()
}
}
// OnDisconnect event. Terminates MumbleDJ thread.
func (dj *mumbledj) OnDisconnect(e *gumble.DisconnectEvent) {
if e.Type == gumble.DisconnectError || e.Type == gumble.DisconnectKicked {
fmt.Println("Disconnected from server... Will retry connection in 30 second intervals for 15 minutes.")
reconnectSuccess := false
for retries := 0; retries <= 30; retries++ {
fmt.Println("Retrying connection...")
if err := dj.client.Connect(); err == nil {
fmt.Println("Successfully reconnected to the server!")
reconnectSuccess = true
break
}
time.Sleep(30 * time.Second)
}
if !reconnectSuccess {
fmt.Println("Could not reconnect to server. Exiting...")
dj.keepAlive <- true
os.Exit(1)
}
} else {
dj.keepAlive <- true
}
}
// OnTextMessage event. Checks for command prefix, and calls parseCommand 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] == dj.conf.General.CommandPrefix[0] && plainMessage != dj.conf.General.CommandPrefix {
parseCommand(e.Sender, e.Sender.Name, plainMessage[1:])
}
}
}
// OnUserChange event. Checks UserChange type, and adjusts items such as skiplists to reflect
// the current status of the users on the server.
func (dj *mumbledj) OnUserChange(e *gumble.UserChangeEvent) {
if e.Type.Has(gumble.UserChangeDisconnected) {
if dj.audioStream.IsPlaying() {
if !isNil(dj.queue.CurrentSong().Playlist()) {
dj.queue.CurrentSong().Playlist().RemoveSkip(e.User.Name)
}
dj.queue.CurrentSong().RemoveSkip(e.User.Name)
}
}
}
// HasPermission checks if username has the permissions to execute a command. Permissions are specified in
// mumbledj.gcfg.
func (dj *mumbledj) HasPermission(username string, command bool) bool {
if dj.conf.Permissions.AdminsEnabled && command {
for _, adminName := range dj.conf.Permissions.Admins {
if username == adminName {
return true
}
}
return false
}
return true
}
// SendPrivateMessage sends a private message to a user. Essentially just checks if a user is still in the server
// before sending them the message.
func (dj *mumbledj) SendPrivateMessage(user *gumble.User, message string) {
if targetUser := dj.client.Self.Channel.Users.Find(user.Name); targetUser != nil {
targetUser.Send(message)
}
}
// CheckAPIKeys enables the services with API keys in the environment varaibles
func CheckAPIKeys() {
anyDisabled := false
// Checks YouTube API key
if os.Getenv("YOUTUBE_API_KEY") == "" {
anyDisabled = true
fmt.Printf("The youtube service has been disabled as you do not have a YouTube API key defined in your environment variables.\n")
} else {
services = append(services, YouTube{})
}
// Checks Soundcloud API key
if os.Getenv("SOUNDCLOUD_API_KEY") == "" {
anyDisabled = true
fmt.Printf("The soundcloud service has been disabled as you do not have a Soundcloud API key defined in your environment variables.\n")
} else {
services = append(services, SoundCloud{})
}
// Checks to see if any service was disabled
if anyDisabled {
fmt.Printf("Please see the following link for info on how to enable missing services: https://github.com/matthieugrieger/mumbledj\n")
}
// Exits application if no services are enabled
if services == nil {
fmt.Printf("No services are enabled, and thus closing\n")
os.Exit(1)
}
}
// isNil checks to see if an object is nil
func isNil(a interface{}) bool {
defer func() { recover() }()
return a == nil || reflect.ValueOf(a).IsNil()
}
// dj variable declaration. This is done outside of main() to allow global use.
var dj = mumbledj{
keepAlive: make(chan bool),
queue: NewSongQueue(),
playlistSkips: make(map[string][]string),
cache: NewSongCache(),
}
// main primarily performs startup tasks. Grabs and parses commandline
// args, sets up the gumble client and its listeners, and then connects to the server.
func main() {
CheckAPIKeys()
if currentUser, err := user.Current(); err == nil {
dj.homeDir = currentUser.HomeDir
}
if err := loadConfiguration(); err == nil {
fmt.Println("Configuration successfully loaded!")
} else {
panic(err)
}
var address, port, username, password, channel, pemCert, pemKey, accesstokens string
var insecure bool
flag.StringVar(&address, "server", "localhost", "address for Mumble server")
flag.StringVar(&port, "port", "64738", "port for Mumble server")
flag.StringVar(&username, "username", "MumbleDJ", "username of MumbleDJ on server")
flag.StringVar(&password, "password", "", "password for Mumble server (if needed)")
flag.StringVar(&channel, "channel", "root", "default channel for MumbleDJ")
flag.StringVar(&pemCert, "cert", "", "path to user PEM certificate for MumbleDJ")
flag.StringVar(&pemKey, "key", "", "path to user PEM key for MumbleDJ")
flag.StringVar(&accesstokens, "accesstokens", "", "list of access tokens for channel auth")
flag.BoolVar(&insecure, "insecure", false, "skip certificate checking")
flag.Parse()
dj.config = gumble.Config{
Username: username,
Password: password,
Address: address + ":" + port,
Tokens: strings.Split(accesstokens, " "),
}
dj.client = gumble.NewClient(&dj.config)
dj.config.TLSConfig.InsecureSkipVerify = true
if !insecure {
gumbleutil.CertificateLockFile(dj.client, fmt.Sprintf("%s/.mumbledj/cert.lock", dj.homeDir))
}
if pemCert != "" {
if pemKey == "" {
pemKey = pemCert
}
if certificate, err := tls.LoadX509KeyPair(pemCert, pemKey); err != nil {
panic(err)
} else {
dj.config.TLSConfig.Certificates = append(dj.config.TLSConfig.Certificates, certificate)
}
}
dj.defaultChannel = strings.Split(channel, "/")
dj.client.Attach(gumbleutil.Listener{
Connect: dj.OnConnect,
Disconnect: dj.OnDisconnect,
TextMessage: dj.OnTextMessage,
UserChange: dj.OnUserChange,
})
dj.client.Attach(gumbleutil.AutoBitrate)
if err := dj.client.Connect(); err != nil {
fmt.Printf("Could not connect to Mumble server at %s:%s.\n", address, port)
os.Exit(1)
}
<-dj.keepAlive
}