forked from go-chat-bot/bot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
cmd.go
347 lines (309 loc) · 10.6 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
package bot
import (
"errors"
"fmt"
"sync"
)
// 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 {
CronSpec string // CronSpec that schedules some function
Channels []string // A list of channels to notify
CmdFunc func(channel string) (string, error) // func 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
}
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)
var (
commands = make(map[string]*customCommand)
passiveCommands = make(map[string]*customCommand)
filterCommands = make(map[string]*customCommand)
periodicCommands = make(map[string]PeriodicConfig)
)
// 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,
}
}
// 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 no-arg function which gets triggered periodically
func RegisterPeriodicCommand(command string, config PeriodicConfig) {
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)
}
}