forked from go-chat-bot/bot
-
Notifications
You must be signed in to change notification settings - Fork 1
/
cmd.go
453 lines (400 loc) · 14.2 KB
/
cmd.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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
package bot
import (
"errors"
"fmt"
"sync"
)
var (
// ErrProtocolServerMismatch server && proto must match
ErrProtocolServerMismatch = errors.New("the specified protocol and server do not correspond to this bot instance")
errNoChannelSpecified = errors.New("no channel was specified for this message")
)
// Cmd holds the parsed user's input for easier handling of commands
type Cmd struct {
Raw string // Raw is full string passed to the command
Channel string // Channel where the command was called
ChannelData *ChannelData // More info about the channel, including network
User *User // User who sent the message
Message string // Full string without the prefix
MessageData *Message // Message with extra flags
Command string // Command is the first argument passed to the bot
RawArgs string // Raw arguments after the command
Args []string // Arguments as array
}
// ChannelData holds the improved channel info, which includes protocol and server
type ChannelData struct {
Protocol string // What protocol the message was sent on (irc, slack, telegram)
Server string // The server hostname the message was sent on
Channel string // The channel name the message appeared in
HumanName string // The human readable name of the channel.
IsPrivate bool // Whether the channel is a group or private chat
}
// URI gives back an URI-fied string containing protocol, server and channel.
func (c *ChannelData) URI() string {
return fmt.Sprintf("%s://%s/%s", c.Protocol, c.Server, c.Channel)
}
// Message holds the message info - for IRC and Slack networks, this can include whether the message was an action.
type Message struct {
Text string // The actual content of this Message
IsAction bool // True if this was a '/me does something' message
}
// FilterCmd holds information about what is output being filtered - message and
// channel where it is being sent
type FilterCmd struct {
Target string // Channel or user the message is being sent to
Message string // Message text being sent
User *User // User who triggered original message
}
// PassiveCmd holds the information which will be passed to passive commands when receiving a message
type PassiveCmd struct {
Raw string // Raw message sent to the channel
MessageData *Message // Message with extra
Channel string // Channel which the message was sent to
ChannelData *ChannelData // Channel and network info
User *User // User who sent this message
}
// PeriodicConfig holds a cron specification for periodically notifying the configured channels
type PeriodicConfig struct {
Version int
CronSpec string // CronSpec that schedules some function
Channels []string // A list of channels to notify, ignored for V2
CmdFunc func(channel string) (string, error) // func to be executed at the period specified on CronSpec
CmdFuncV2 func() ([]CmdResult, error) // func v2 to be executed at the period specified on CronSpec
}
// User holds user id, nick and real name
type User struct {
ID string
Nick string
RealName string
IsBot bool
}
// MessageStream allows event information to be transmitted to an arbitrary channel
// https://github.com/go-chat-bot/bot/issues/97
type MessageStream struct {
Data chan MessageStreamMessage
// Done is almost never called, usually the bot should just leave the chan open
Done chan bool
}
// MessageStreamMessage the actual Message passed back to MessageStream in a chan
type MessageStreamMessage struct {
Message string
ChannelData *ChannelData
}
type customCommand struct {
Version int
Cmd string
CmdFuncV1 activeCmdFuncV1
CmdFuncV2 activeCmdFuncV2
CmdFuncV3 activeCmdFuncV3
PassiveFuncV1 passiveCmdFuncV1
PassiveFuncV2 passiveCmdFuncV2
FilterFuncV1 filterCmdFuncV1
Description string
ExampleArgs string
}
// CmdResult is the result message of V2 commands
type CmdResult struct {
Channel string // The channel where the bot should send the message
Message string // The message to be sent
}
// CmdResultV3 is the result message of V3 commands
type CmdResultV3 struct {
Channel string
Message chan string
Done chan bool
}
const (
v1 = iota
v2
v3
pv1
pv2
fv1
)
const (
commandNotAvailable = "Command %v not available."
noCommandsAvailable = "No commands available."
errorExecutingCommand = "Error executing %s: %s"
)
type passiveCmdFuncV1 func(cmd *PassiveCmd) (string, error)
type passiveCmdFuncV2 func(cmd *PassiveCmd) (CmdResultV3, error)
type activeCmdFuncV1 func(cmd *Cmd) (string, error)
type activeCmdFuncV2 func(cmd *Cmd) (CmdResult, error)
type activeCmdFuncV3 func(cmd *Cmd) (CmdResultV3, error)
type filterCmdFuncV1 func(cmd *FilterCmd) (string, error)
type messageStreamFunc func(ms *MessageStream) error
type messageStreamSyncMap struct {
sync.RWMutex
messageStreams map[messageStreamKey]*MessageStream
}
type messageStreamKey struct {
StreamName string
Server string
Protocol string
}
// messageStreamConfig holds the registered function for the streamname
type messageStreamConfig struct {
version int
streamName string
msgFunc messageStreamFunc
}
var (
commands = make(map[string]*customCommand)
passiveCommands = make(map[string]*customCommand)
filterCommands = make(map[string]*customCommand)
periodicCommands = make(map[string]PeriodicConfig)
messageStreamConfigs []*messageStreamConfig
msMap = &messageStreamSyncMap{
messageStreams: make(map[messageStreamKey]*MessageStream),
}
)
// RegisterCommand adds a new command to the bot.
// The command(s) should be registered in the Init() func of your package
// command: String which the user will use to execute the command, example: reverse
// decription: Description of the command to use in !help, example: Reverses a string
// exampleArgs: Example args to be displayed in !help <command>, example: string to be reversed
// cmdFunc: Function which will be executed. It will received a parsed command as a Cmd value
func RegisterCommand(command, description, exampleArgs string, cmdFunc activeCmdFuncV1) {
commands[command] = &customCommand{
Version: v1,
Cmd: command,
CmdFuncV1: cmdFunc,
Description: description,
ExampleArgs: exampleArgs,
}
}
// RegisterCommandV2 adds a new command to the bot.
// It is the same as RegisterCommand but the command can specify the channel to reply to
func RegisterCommandV2(command, description, exampleArgs string, cmdFunc activeCmdFuncV2) {
commands[command] = &customCommand{
Version: v2,
Cmd: command,
CmdFuncV2: cmdFunc,
Description: description,
ExampleArgs: exampleArgs,
}
}
// RegisterCommandV3 adds a new command to the bot.
// It is the same as RegisterCommand but the command return a chan
func RegisterCommandV3(command, description, exampleArgs string, cmdFunc activeCmdFuncV3) {
commands[command] = &customCommand{
Version: v3,
Cmd: command,
CmdFuncV3: cmdFunc,
Description: description,
ExampleArgs: exampleArgs,
}
}
// RegisterMessageStream adds a new message stream to the bot.
// The command should be registered in the Init() func of your package
// MessageStreams send messages to a channel
// streamName: String used to identify the command, for internal use only (ex: webhook)
// messageStreamFunc: Function which will be executed. It will received a MessageStream with a chan to push
func RegisterMessageStream(streamName string, msgFunc messageStreamFunc) {
messageStreamConfigs = append(messageStreamConfigs, &messageStreamConfig{
version: v1,
streamName: streamName,
msgFunc: msgFunc,
})
}
// RegisterPassiveCommand adds a new passive command to the bot.
// The command should be registered in the Init() func of your package
// Passive commands receives all the text posted to a channel without any parsing
// command: String used to identify the command, for internal use only (ex: logs)
// cmdFunc: Function which will be executed. It will received the raw message, channel and nick
func RegisterPassiveCommand(command string, cmdFunc passiveCmdFuncV1) {
passiveCommands[command] = &customCommand{
Version: pv1,
Cmd: command,
PassiveFuncV1: cmdFunc,
}
}
// RegisterPassiveCommandV2 adds a new passive command to the bot.
// The command should be registered in the Init() func of your package
// Passive commands receives all the text posted to a channel without any parsing
// command: String used to identify the command, for internal use only (ex: logs)
// cmdFunc: Function which will be executed. It will received the raw message, channel and nick
func RegisterPassiveCommandV2(command string, cmdFunc passiveCmdFuncV2) {
passiveCommands[command] = &customCommand{
Version: pv2,
Cmd: command,
PassiveFuncV2: cmdFunc,
}
}
// RegisterFilterCommand adds a command that is run every time bot is about to
// send a message. The comand should be registered in the Init() func of your
// package.
// Filter commands receive message and its destination and should return
// modified version. Returning empty string prevents message being sent
// completely
// command: String used to identify the command, for internal use only (ex: silence)
// cmdFunc: Function which will be executed. It will receive the message, target
// channel and nick who triggered original message
func RegisterFilterCommand(command string, cmdFunc filterCmdFuncV1) {
filterCommands[command] = &customCommand{
Version: fv1,
Cmd: command,
FilterFuncV1: cmdFunc,
}
}
// RegisterPeriodicCommand adds a command that is run periodically.
// The command should be registered in the Init() func of your package
// config: PeriodicConfig which specify CronSpec and a channel list
// cmdFunc: A function with single string argument (channel) which gets triggered periodically
func RegisterPeriodicCommand(command string, config PeriodicConfig) {
config.Version = v1
periodicCommands[command] = config
}
// RegisterPeriodicCommandV2 adds a command that is run periodically.
// The command should be registered in the Init() func of your package
// config: PeriodicConfig which specifies CronSpec
// cmdFuncV2: A no-arg function which gets triggered periodically
// It should return slice of CmdResults (channel and message to send to it)
func RegisterPeriodicCommandV2(command string, config PeriodicConfig) {
config.Version = v2
periodicCommands[command] = config
}
// Disable allows disabling commands that were registered.
// It is useful when running multiple bot instances to disabled some plugins like url which
// is already present on some protocols.
func (b *Bot) Disable(cmds []string) {
b.disabledCmds = append(b.disabledCmds, cmds...)
}
func (b *Bot) executePassiveCommands(cmd *PassiveCmd) {
var wg sync.WaitGroup
for k, v := range passiveCommands {
if b.isDisabled(k) {
continue
}
wg.Add(1)
go func(cmdFunc *customCommand) {
defer wg.Done()
switch cmdFunc.Version {
case pv1:
result, err := cmdFunc.PassiveFuncV1(cmd)
if err != nil {
b.errored(fmt.Sprintf("Error executing %s", cmdFunc.Cmd), err)
} else {
b.SendMessage(cmd.Channel, result, cmd.User)
}
case pv2:
result, err := cmdFunc.PassiveFuncV2(cmd)
if err != nil {
b.errored(fmt.Sprintf("Error executing %s", cmdFunc.Cmd), err)
return
}
for {
select {
case message := <-result.Message:
if message != "" {
b.SendMessage(result.Channel, message, cmd.User)
}
case <-result.Done:
return
}
}
default:
}
}(v)
}
wg.Wait()
}
func (b *Bot) executeFilterCommands(cmd *FilterCmd) string {
for k, filter := range filterCommands {
switch filter.Version {
case fv1:
filtered, err := filter.FilterFuncV1(cmd)
if err != nil {
b.errored(fmt.Sprintf("Error executing filter %s", k), err)
continue
}
cmd.Message = filtered
}
}
return cmd.Message
}
func (b *Bot) isDisabled(cmd string) bool {
for _, c := range b.disabledCmds {
if c == cmd {
return true
}
}
return false
}
func (b *Bot) handleCmd(c *Cmd) {
cmd := commands[c.Command]
if cmd == nil {
b.errored(fmt.Sprintf("Command not found %v", c.Command), errors.New("Command not found"))
return
}
switch cmd.Version {
case v1:
message, err := cmd.CmdFuncV1(c)
b.checkCmdError(err, c)
if message != "" {
b.SendMessage(c.Channel, message, c.User)
}
case v2:
result, err := cmd.CmdFuncV2(c)
b.checkCmdError(err, c)
if result.Channel == "" {
result.Channel = c.Channel
}
if result.Message != "" {
b.SendMessage(result.Channel, result.Message, c.User)
}
case v3:
result, err := cmd.CmdFuncV3(c)
b.checkCmdError(err, c)
if result.Channel == "" {
result.Channel = c.Channel
}
for {
select {
case message := <-result.Message:
if message != "" {
b.SendMessage(result.Channel, message, c.User)
}
case <-result.Done:
return
}
}
}
}
func (b *Bot) checkCmdError(err error, c *Cmd) {
if err != nil {
errorMsg := fmt.Sprintf(errorExecutingCommand, c.Command, err.Error())
b.errored(errorMsg, err)
b.SendMessage(c.Channel, errorMsg, c.User)
}
}
// handleMessageStream
// if there are two bots (telegram, irc) and three messsages(a, b, c) then there will be six entries in messageStreams[key]
// when a message is sent into a chan it has a good chance of arriving at the wrong bot instance
// for every message we check to see if it matched this b.Protocol and b.Server
// if it doesn't we lookup the entry in messageStreams[key] send it to *that* Data chan
func (b *Bot) handleMessageStream(streamName string, ms *MessageStream) {
for {
select {
case d := <-ms.Data:
if d.ChannelData.Protocol != b.Protocol || d.ChannelData.Server != b.Server {
// then lookup who it *should* be sent to and send it back into *that* chan
key := messageStreamKey{Protocol: d.ChannelData.Protocol, Server: d.ChannelData.Server, StreamName: streamName}
msMap.RLock()
msMap.messageStreams[key].Data <- d
msMap.RUnlock()
continue
}
// this message is meant for us!
if d.ChannelData.Channel == "" {
b.errored("handleMessageStream: "+d.Message, errNoChannelSpecified)
continue
}
if d.Message != "" {
b.SendMessage(d.ChannelData.Channel, d.Message, nil)
}
case <-ms.Done:
return
}
}
}