This repository has been archived by the owner on Feb 20, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
modules.alerts.go
475 lines (455 loc) · 15.5 KB
/
modules.alerts.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
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
package twitch
import (
"bytes"
"context"
"encoding/json"
"fmt"
"math/rand"
"sync"
"text/template"
"time"
"github.com/Masterminds/sprig/v3"
jsoniter "github.com/json-iterator/go"
"github.com/nicklaw5/helix/v2"
"github.com/strimertul/strimertul/modules/database"
)
const BotAlertsKey = "twitch/bot-modules/alerts/config"
type eventSubNotification struct {
Subscription helix.EventSubSubscription `json:"subscription"`
Challenge string `json:"challenge"`
Event json.RawMessage `json:"event"`
}
type BotAlertsConfig struct {
Follow struct {
Enabled bool `json:"enabled"`
Messages []string `json:"messages"`
} `json:"follow"`
Subscription struct {
Enabled bool `json:"enabled"`
Messages []string `json:"messages"`
Variations []struct {
MinStreak *int `json:"min_streak,omitempty"`
IsGifted *bool `json:"is_gifted,omitempty"`
Messages []string `json:"messages"`
} `json:"variations"`
} `json:"subscription"`
GiftSub struct {
Enabled bool `json:"enabled"`
Messages []string `json:"messages"`
Variations []struct {
MinCumulative *int `json:"min_cumulative,omitempty"`
IsAnonymous *bool `json:"is_anonymous,omitempty"`
Messages []string `json:"messages"`
} `json:"variations"`
} `json:"gift_sub"`
Raid struct {
Enabled bool `json:"enabled"`
Messages []string `json:"messages"`
Variations []struct {
MinViewers *int `json:"min_viewers,omitempty"`
Messages []string `json:"messages"`
} `json:"variations"`
} `json:"raid"`
Cheer struct {
Enabled bool `json:"enabled"`
Messages []string `json:"messages"`
Variations []struct {
MinAmount *int `json:"min_amount,omitempty"`
Messages []string `json:"messages"`
} `json:"variations"`
} `json:"cheer"`
}
type BotAlertsModule struct {
Config BotAlertsConfig
bot *Bot
mu sync.Mutex
templates map[string]*template.Template
}
func SetupAlerts(bot *Bot) *BotAlertsModule {
mod := &BotAlertsModule{
bot: bot,
mu: sync.Mutex{},
templates: make(map[string]*template.Template),
}
// Load config from database
err := bot.api.db.GetJSON(BotAlertsKey, &mod.Config)
if err != nil {
bot.logger.WithError(err).Debug("config load error")
mod.Config = BotAlertsConfig{}
// Save empty config
bot.api.db.PutJSON(BotAlertsKey, mod.Config)
}
mod.compileTemplates()
go bot.api.db.Subscribe(context.Background(), func(changed []database.ModifiedKV) error {
for _, kv := range changed {
if kv.Key == BotAlertsKey {
err := jsoniter.ConfigFastest.Unmarshal(kv.Data, &mod.Config)
if err != nil {
bot.logger.WithError(err).Debug("error reloading timer config")
} else {
bot.logger.Info("reloaded timer config")
}
mod.compileTemplates()
}
}
return nil
}, BotAlertsKey)
// Subscriptions are handled with a slight delay as info come from different events and must be aggregated
pendingSubs := make(map[string]subMixedEvent)
pendingMux := sync.Mutex{}
processPendingSub := func(user string) {
pendingMux.Lock()
defer pendingMux.Unlock()
sub, ok := pendingSubs[user]
defer delete(pendingSubs, user)
if !ok {
// Somehow it's gone? Return early
return
}
// One last check in case config changed
if !mod.Config.Subscription.Enabled {
return
}
// Assign random message
messageID := rand.Intn(len(mod.Config.Subscription.Messages))
tpl, ok := mod.templates[fmt.Sprintf("sub-%d", messageID)]
// If template is broken, write it as is (soft fail, plus we raise attention I guess?)
if !ok {
mod.bot.WriteMessage(mod.Config.Subscription.Messages[messageID])
return
}
// Check for variations, either by streak or gifted
if sub.IsGift {
for variationIndex, variation := range mod.Config.Subscription.Variations {
if variation.IsGifted != nil && *variation.IsGifted {
// Make sure template is valid
if temp, ok := mod.templates[fmt.Sprintf("sub-v%d-%d", variationIndex, messageID)]; ok {
tpl = temp
break
}
}
}
} else if sub.DurationMonths > 0 {
minMonths := -1
for variationIndex, variation := range mod.Config.Subscription.Variations {
if variation.MinStreak != nil && sub.DurationMonths >= *variation.MinStreak && sub.DurationMonths >= minMonths {
// Make sure template is valid
if temp, ok := mod.templates[fmt.Sprintf("sub-v%d-%d", variationIndex, messageID)]; ok {
tpl = temp
minMonths = *variation.MinStreak
}
}
}
}
writeTemplate(bot, tpl, sub)
}
addPendingSub := func(ev interface{}) {
switch ev.(type) {
case helix.EventSubChannelSubscribeEvent:
sub := ev.(helix.EventSubChannelSubscribeEvent)
pendingMux.Lock()
defer pendingMux.Unlock()
if ev, ok := pendingSubs[sub.UserID]; ok {
// Already pending, add extra data
ev.IsGift = sub.IsGift
pendingSubs[sub.UserID] = ev
return
}
pendingSubs[sub.UserID] = subMixedEvent{
UserID: sub.UserID,
UserLogin: sub.UserLogin,
UserName: sub.UserName,
BroadcasterUserID: sub.BroadcasterUserID,
BroadcasterUserLogin: sub.BroadcasterUserLogin,
BroadcasterUserName: sub.BroadcasterUserName,
Tier: sub.Tier,
IsGift: sub.IsGift,
}
go func() {
// Wait a bit to make sure we aggregate all events
time.Sleep(time.Second * 3)
processPendingSub(sub.UserID)
}()
case helix.EventSubChannelSubscriptionMessageEvent:
sub := ev.(helix.EventSubChannelSubscriptionMessageEvent)
pendingMux.Lock()
defer pendingMux.Unlock()
if ev, ok := pendingSubs[sub.UserID]; ok {
// Already pending, add extra data
ev.StreakMonths = sub.StreakMonths
ev.DurationMonths = sub.DurationMonths
ev.CumulativeMonths = sub.CumulativeMonths
ev.Message = sub.Message
return
}
pendingSubs[sub.UserID] = subMixedEvent{
UserID: sub.UserID,
UserLogin: sub.UserLogin,
UserName: sub.UserName,
BroadcasterUserID: sub.BroadcasterUserID,
BroadcasterUserLogin: sub.BroadcasterUserLogin,
BroadcasterUserName: sub.BroadcasterUserName,
Tier: sub.Tier,
StreakMonths: sub.StreakMonths,
DurationMonths: sub.DurationMonths,
CumulativeMonths: sub.CumulativeMonths,
Message: sub.Message,
}
go func() {
// Wait a bit to make sure we aggregate all events
time.Sleep(time.Second * 3)
processPendingSub(sub.UserID)
}()
}
}
go bot.api.db.Subscribe(context.Background(), func(changed []database.ModifiedKV) error {
for _, kv := range changed {
if kv.Key == "stulbe/ev/webhook" {
var ev eventSubNotification
err := jsoniter.ConfigFastest.Unmarshal(kv.Data, &ev)
if err != nil {
bot.logger.WithError(err).Debug("error parsing webhook payload")
continue
}
switch ev.Subscription.Type {
case helix.EventSubTypeChannelFollow:
// Only process if we care about follows
if !mod.Config.Follow.Enabled {
continue
}
// Parse as follow event
var followEv helix.EventSubChannelFollowEvent
err := jsoniter.ConfigFastest.Unmarshal(ev.Event, &followEv)
if err != nil {
bot.logger.WithError(err).Debug("error parsing follow event")
continue
}
// Pick a random message
messageID := rand.Intn(len(mod.Config.Follow.Messages))
// Pick compiled template or fallback to plain text
if tpl, ok := mod.templates[fmt.Sprintf("follow-%d", messageID)]; ok {
writeTemplate(bot, tpl, &followEv)
} else {
bot.WriteMessage(mod.Config.Follow.Messages[messageID])
}
// Compile template and send
case helix.EventSubTypeChannelRaid:
// Only process if we care about raids
if !mod.Config.Raid.Enabled {
continue
}
// Parse as raid event
var raidEv helix.EventSubChannelRaidEvent
err := jsoniter.ConfigFastest.Unmarshal(ev.Event, &raidEv)
if err != nil {
bot.logger.WithError(err).Debug("error parsing raid event")
continue
}
// Pick a random message from base set
messageID := rand.Intn(len(mod.Config.Raid.Messages))
tpl, ok := mod.templates[fmt.Sprintf("raid-%d", messageID)]
if !ok {
// Broken template!
mod.bot.WriteMessage(mod.Config.Raid.Messages[messageID])
continue
}
// If we have variations, loop through all the available variations and pick the one with the highest minimum viewers that are met
if len(mod.Config.Raid.Variations) > 0 {
minViewers := -1
for variationIndex, variation := range mod.Config.Raid.Variations {
if variation.MinViewers != nil && *variation.MinViewers > minViewers && raidEv.Viewers >= *variation.MinViewers {
// Make sure the template is valid
if varTemplate, ok := mod.templates[fmt.Sprintf("raid-v%d-%d", variationIndex, messageID)]; ok {
tpl = varTemplate
minViewers = *variation.MinViewers
}
}
}
}
// Compile template and send
writeTemplate(bot, tpl, &raidEv)
case helix.EventSubTypeChannelCheer:
// Only process if we care about bits
if !mod.Config.Cheer.Enabled {
continue
}
// Parse as cheer event
var cheerEv helix.EventSubChannelCheerEvent
err := jsoniter.ConfigFastest.Unmarshal(ev.Event, &cheerEv)
if err != nil {
bot.logger.WithError(err).Debug("error parsing cheer event")
continue
}
// Pick a random message from base set
messageID := rand.Intn(len(mod.Config.Cheer.Messages))
tpl, ok := mod.templates[fmt.Sprintf("cheer-%d", messageID)]
if !ok {
// Broken template!
mod.bot.WriteMessage(mod.Config.Raid.Messages[messageID])
continue
}
// If we have variations, loop through all the available variations and pick the one with the highest minimum amount that is met
if len(mod.Config.Cheer.Variations) > 0 {
minAmount := -1
for variationIndex, variation := range mod.Config.Cheer.Variations {
if variation.MinAmount != nil && *variation.MinAmount > minAmount && cheerEv.Bits >= *variation.MinAmount {
// Make sure the template is valid
if varTemplate, ok := mod.templates[fmt.Sprintf("cheer-v%d-%d", variationIndex, messageID)]; ok {
tpl = varTemplate
minAmount = *variation.MinAmount
}
}
}
}
// Compile template and send
writeTemplate(bot, tpl, &cheerEv)
case helix.EventSubTypeChannelSubscription:
// Only process if we care about subscriptions
if !mod.Config.Subscription.Enabled {
continue
}
// Parse as subscription event
var subEv helix.EventSubChannelSubscribeEvent
err := jsoniter.ConfigFastest.Unmarshal(ev.Event, &subEv)
if err != nil {
bot.logger.WithError(err).Debug("error parsing sub event")
continue
}
addPendingSub(subEv)
case helix.EventSubTypeChannelSubscriptionMessage:
// Only process if we care about subscriptions
if !mod.Config.Subscription.Enabled {
continue
}
// Parse as subscription event
var subEv helix.EventSubChannelSubscriptionMessageEvent
err := jsoniter.ConfigFastest.Unmarshal(ev.Event, &subEv)
if err != nil {
bot.logger.WithError(err).Debug("error parsing sub event")
continue
}
addPendingSub(subEv)
case helix.EventSubTypeChannelSubscriptionGift:
// Only process if we care about gifted subs
if !mod.Config.GiftSub.Enabled {
continue
}
// Parse as gift event
var giftEv helix.EventSubChannelSubscriptionGiftEvent
err := jsoniter.ConfigFastest.Unmarshal(ev.Event, &giftEv)
if err != nil {
bot.logger.WithError(err).Debug("error parsing raid event")
continue
}
// Pick a random message from base set
messageID := rand.Intn(len(mod.Config.GiftSub.Messages))
tpl, ok := mod.templates[fmt.Sprintf("gift-%d", messageID)]
if !ok {
// Broken template!
mod.bot.WriteMessage(mod.Config.GiftSub.Messages[messageID])
continue
}
// If we have variations, loop through all the available variations and pick the one with the highest minimum cumulative total that are met
if len(mod.Config.GiftSub.Variations) > 0 {
if giftEv.IsAnonymous {
for variationIndex, variation := range mod.Config.GiftSub.Variations {
if variation.IsAnonymous != nil && *variation.IsAnonymous {
// Make sure template is valid
if temp, ok := mod.templates[fmt.Sprintf("gift-v%d-%d", variationIndex, messageID)]; ok {
tpl = temp
break
}
}
}
} else if giftEv.CumulativeTotal > 0 {
minCumulative := -1
for variationIndex, variation := range mod.Config.GiftSub.Variations {
if variation.MinCumulative != nil && *variation.MinCumulative > minCumulative && giftEv.CumulativeTotal >= *variation.MinCumulative {
// Make sure template is valid
if temp, ok := mod.templates[fmt.Sprintf("gift-v%d-%d", variationIndex, messageID)]; ok {
tpl = temp
minCumulative = *variation.MinCumulative
}
}
}
}
}
// Compile template and send
writeTemplate(bot, tpl, &giftEv)
}
}
}
return nil
}, "stulbe/ev/webhook")
bot.logger.Debug("loaded bot alerts")
return mod
}
func (m *BotAlertsModule) compileTemplates() {
m.templates = make(map[string]*template.Template)
for index, msg := range m.Config.Follow.Messages {
m.addTemplate(fmt.Sprintf("follow-%d", index), msg)
}
for index, msg := range m.Config.Subscription.Messages {
m.addTemplate(fmt.Sprintf("sub-%d", index), msg)
}
for varIndex, variation := range m.Config.Subscription.Variations {
for index, msg := range variation.Messages {
m.addTemplate(fmt.Sprintf("sub-v%d-%d", varIndex, index), msg)
}
}
for index, msg := range m.Config.Raid.Messages {
m.addTemplate(fmt.Sprintf("raid-%d", index), msg)
}
for varIndex, variation := range m.Config.Raid.Variations {
for index, msg := range variation.Messages {
m.addTemplate(fmt.Sprintf("raid-v%d-%d", varIndex, index), msg)
}
}
for index, msg := range m.Config.Cheer.Messages {
m.addTemplate(fmt.Sprintf("cheer-%d", index), msg)
}
for varIndex, variation := range m.Config.Cheer.Variations {
for index, msg := range variation.Messages {
m.addTemplate(fmt.Sprintf("cheer-v%d-%d", varIndex, index), msg)
}
}
for index, msg := range m.Config.GiftSub.Messages {
m.addTemplate(fmt.Sprintf("gift-%d", index), msg)
}
for varIndex, variation := range m.Config.GiftSub.Variations {
for index, msg := range variation.Messages {
m.addTemplate(fmt.Sprintf("gift-v%d-%d", varIndex, index), msg)
}
}
}
func (m *BotAlertsModule) addTemplate(id string, msg string) {
tpl, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(msg)
if err != nil {
m.bot.logger.WithError(err).Error("error compiling template")
return
}
m.templates[id] = tpl
}
func writeTemplate(bot *Bot, tpl *template.Template, data interface{}) {
var buf bytes.Buffer
err := tpl.Execute(&buf, data)
if err != nil {
bot.logger.WithError(err).Error("error executing template for alert")
return
}
bot.WriteMessage(buf.String())
}
type subMixedEvent struct {
UserID string
UserLogin string
UserName string
BroadcasterUserID string
BroadcasterUserLogin string
BroadcasterUserName string
Tier string
IsGift bool
CumulativeMonths int
StreakMonths int
DurationMonths int
Message helix.EventSubMessage
}