-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathplugin.go
More file actions
296 lines (264 loc) · 8.92 KB
/
Copy pathplugin.go
File metadata and controls
296 lines (264 loc) · 8.92 KB
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
package sdbot
import (
"fmt"
"regexp"
"strings"
"time"
)
// Plugin is a command that triggers on some regexp and performs a task
// based on the message that triggered it as defined by its EventHandler.
// It can trigger on several different formats (consider prefix "." and
// command "cmd"). Note that cooldowns will not affect command syntax, but
// will ignore the commands hould it have been sent less than Cooldown time
// from the last instance.
//
// NewPlugin: ".cmd"
// NewPluginWithArgs:
// 1 arg: ".cmd arg0"
// 2 args: ".cmd arg0, arg1" (space is optional)
// n args: ".cmd arg0,arg1,arg2,arg3, ...,arg n" (space is optional)
//
// It is possible to define a Command with a regexp string. For example,
// a command of "y|n" will trigger on either y or n.
//
// Note that if a command is given as something like s(ay|peak) then the
// parsed args will be pushed back in the slice, and args[0] will contain
// the string of either "ay" or "peak", depending on which triggered the
// message.
//
// Each fired event is run in its own separate goroutine, so for anything
// that must be run sequentially (ie. cannot read and write to the same file
// at once) use Bot.Synchronize.
type Plugin struct {
Bot *Bot
Name string
Prefix *regexp.Regexp
Suffix *regexp.Regexp
Command string
NumArgs int
Cooldown time.Duration
LastUsed time.Time
EventHandler EventHandler
kill chan struct{}
}
// TimedPlugin structs will fire an event on a regular schedule defined by the
// time.Duration provided to the time.Ticker. Each event is run in its own
// goroutine, so every event will fire regardless of whether or not the last
// event ticked had completed. For anything that must be run sequentially
// (ie. cannot read and write to the same file at once) use Bot.Synchronize.
//
// Because each event fires in its own goroutine, you should take care to not
// have each event take longer than the duration of the ticker, or else you
// will be spawning goroutines faster than you can finish them, which is a
// recipe for disaster.
type TimedPlugin struct {
Bot *Bot
Name string
Ticker *time.Ticker
Period time.Duration
TimedEventHandler TimedEventHandler
kill chan struct{}
}
// DefaultEventHandler is the default event handler that you can make use of
// if you do not want to encapsulate your plugin with any custom behaviour.
// Refer to the example plugins to see how you can make use of this.
type DefaultEventHandler struct {
Plugin *Plugin
}
// DefaultTimedEventHandler is the default event handler for a TimedPlugin.
// Refer to the example plugins to see how you can make use of this.
type DefaultTimedEventHandler struct {
TimedPlugin *TimedPlugin
}
// NewPlugin (and its variants) define convenient ways to make new Plugin
// structs. You must add an event handler after creation. If you want
// to add custom prefixes or suffixes, you can do it with the Plugin.SetPrefix
// and Plugin.SetSuffix methods. Otherwise the bot will load prefixes and
// suffixes from your Config.
//
// NewPlugin in particular creates a Plugin that will trigger on a command.
func NewPlugin(cmd string) *Plugin {
return &Plugin{
Command: cmd,
}
}
// NewPluginWithoutCommand creates a new Plugin that will trigger on every
// chat and private event.
func NewPluginWithoutCommand() *Plugin {
return &Plugin{}
}
// NewPluginWithArgs creates a new Plugin that will trigger on a command, and
// will parse comma-separated arguments provided along with the command. It
// will not trigger unless an adequate amount of commas are present.
func NewPluginWithArgs(cmd string, numArgs int) *Plugin {
return &Plugin{
Command: cmd,
NumArgs: numArgs,
}
}
// NewPluginWithCooldown creates a new Plugin that will trigger on a command,
// but will not trigger if the last time it was used was not at least the
// provided time.Duration ago.
func NewPluginWithCooldown(cmd string, cooldown time.Duration) *Plugin {
return &Plugin{
Command: cmd,
Cooldown: cooldown,
}
}
// NewPluginWithArgsAndCooldown creates a new Plugin with arguments and a
// cooldown as described by both NewPluginWithArgs and NewPluginWithCooldown.
func NewPluginWithArgsAndCooldown(cmd string, numArgs int, cooldown time.Duration) *Plugin {
return &Plugin{
Command: cmd,
NumArgs: numArgs,
Cooldown: cooldown,
}
}
// NewTimedPlugin creates a new TimedPlugin that fires events on its
// TimedEventHandler every given period of time.Duration.
func NewTimedPlugin(period time.Duration) *TimedPlugin {
return &TimedPlugin{
Period: period,
}
}
// SetEventHandler sets the EventHandler of the Plugin.
// Allows you to use a custom EventHandler with any fields you want.
// The EventHandler of every Plugin MUST be set after the creation of a Plugin.
func (p *Plugin) SetEventHandler(eh EventHandler) {
p.EventHandler = eh
}
// SetEventHandler sets the TimedEventHandler of the TimedPlugin.
// The TimedEventHandler of every TimedPlugin MUST be set after its creation.
func (tp *TimedPlugin) SetEventHandler(teh TimedEventHandler) {
tp.TimedEventHandler = teh
}
// SetPrefix overrides the Plugin's default Prefix as read by the Config.
func (p *Plugin) SetPrefix(prefixes []string) {
if len(prefixes) == 0 {
return
}
regStr := "^(" + strings.Join(prefixes, "|") + ")"
reg, err := regexp.Compile(regStr)
CheckErr(err)
p.Prefix = reg
}
// SetSuffix overrides the Plugin's default Suffix as read by the Config.
func (p *Plugin) SetSuffix(suffixes []string) {
if len(suffixes) == 0 {
return
}
regStr := "(" + strings.Join(suffixes, "|") + ")$"
reg, err := regexp.Compile(regStr)
CheckErr(err)
p.Suffix = reg
}
// Formats the prefixes and suffixes into the regexp that will be used to match
// messages.
func (p *Plugin) formatPrefixAndSuffix() {
ps := p.Prefix.String()
ss := p.Suffix.String()
var flags string
var args string
if p.Bot.Config.CaseInsensitive {
flags = "(?i)"
}
if p.NumArgs > 0 {
if p.NumArgs == 1 {
args = " +(.+)"
} else {
args = " +([^,]+)"
}
for i := 0; i < p.NumArgs-1; i++ {
if i == p.NumArgs-2 {
args = strings.Join([]string{args, ", +(.+)"}, "")
} else {
args = strings.Join([]string{args, ", +([^,]+)"}, "")
}
}
} else {
p.Prefix = regexp.MustCompile(fmt.Sprintf("^(%s%s%s$)", flags, ps[1:], p.Command))
p.Suffix = regexp.MustCompile(fmt.Sprintf("(%s%s)$", flags, ss[:len(ss)-1]))
return
}
p.Prefix = regexp.MustCompile(fmt.Sprintf("^(%s%s%s%s)", flags, ps[1:], p.Command, args))
p.Suffix = regexp.MustCompile(fmt.Sprintf("(%s%s)$", flags, ss[:len(ss)-1]))
}
// Find out if the message is a match for this plugin.
func (p *Plugin) match(m *Message) bool {
return p.Prefix.MatchString(m.Message) && p.Suffix.MatchString(m.Message)
}
// Parse the message. Returns the arguments provided to the message.
func (p *Plugin) parse(m *Message) []string {
submatches := p.Prefix.FindStringSubmatch(m.Message)
switch p.NumArgs {
case 0:
return []string{}
default:
return submatches[3:]
}
}
// Starts a loop in its own goroutine listening for events.
func (p *Plugin) listen() {
go func() {
for {
select {
case m := <-p.Bot.pluginChatChannelsRead(p.Name):
if !p.Bot.Config.IgnoreChatMessages && p.match(m) {
args := p.parse(m)
Debugf("[on plugin] Starting chat event handler goroutine for plugin `%s` with args `%+v`", p.Name, args)
if m.Time.Sub(p.LastUsed) > p.Cooldown {
p.LastUsed = m.Time
go p.EventHandler.HandleEvent(m, args)
}
}
case m := <-p.Bot.pluginPrivateChannelsRead(p.Name):
if !p.Bot.Config.IgnorePrivateMessages && p.match(m) {
args := p.parse(m)
Debugf("[on plugin] Starting private event handler goroutine for plugin `%s` with args `%+v`", p.Name, args)
if m.Time.Sub(p.LastUsed) > p.Cooldown {
p.LastUsed = m.Time
go p.EventHandler.HandleEvent(m, args)
}
}
case <-p.kill:
return
}
}
}()
}
// Request the termination of the Plugin.Listen loop.
func (p *Plugin) stopListening() {
p.kill <- struct{}{}
}
// Starts a loop listening on the time.Ticker.
func (tp *TimedPlugin) start() {
tp.Ticker = time.NewTicker(tp.Period)
go func() {
for {
select {
case <-tp.Ticker.C:
go tp.TimedEventHandler.HandleEvent()
case <-tp.kill:
return
}
}
}()
}
// Request the termination of the TimedPlugin.Start loop.
func (tp *TimedPlugin) stop() {
tp.Ticker.Stop()
tp.kill <- struct{}{}
}
// EventHandler defines the behaviour and action of any event on a Plugin. Use
// the DefaultEventHandler unless you want to add custom behaviour. For
// example, you could keep track of variables that are known globally to the
// plugin. (Every Plugin event goes through the same handler)
type EventHandler interface {
HandleEvent(*Message, []string)
}
// TimedEventHandler defines the behaviour and action of any event on a
// TimedPlugin. Use the DefaultTimedEventHandler unless you want to add custom
// behaviour.
type TimedEventHandler interface {
HandleEvent()
}