package alerts import ( "math/rand" "text/template" "github.com/nicklaw5/helix/v2" "go.uber.org/zap" "git.sr.ht/~ashkeel/strimertul/twitch/chat" ) func (m *Module) onEventSubEvent(_ string, value string) { var ev eventSubNotification if err := json.UnmarshalFromString(value, &ev); err != nil { m.logger.Warn("Error parsing webhook payload", zap.Error(err)) return } switch ev.Subscription.Type { case helix.EventSubTypeChannelFollow: // Only process if we care about follows if !m.Config.Follow.Enabled { return } // Parse as a follow event var followEv helix.EventSubChannelFollowEvent if err := json.Unmarshal(ev.Event, &followEv); err != nil { m.logger.Warn("Error parsing follow event", zap.Error(err)) return } // Pick a random message messageID := rand.Intn(len(m.Config.Follow.Messages)) // Pick compiled template or fallback to plain text tpl, ok := m.templates[templateTypeFollow][m.Config.Follow.Messages[messageID]] if !ok { chat.WriteMessage(m.db, m.logger, chat.WriteMessageRequest{ Message: m.Config.Follow.Messages[messageID], Announce: m.Config.Follow.Announce, }) return } m.writeTemplate(tpl, &followEv, m.Config.Follow.Announce) // Compile template and send case helix.EventSubTypeChannelRaid: // Only process if we care about raids if !m.Config.Raid.Enabled { return } // Parse as raid event var raidEv helix.EventSubChannelRaidEvent if err := json.Unmarshal(ev.Event, &raidEv); err != nil { m.logger.Warn("Error parsing raid event", zap.Error(err)) return } // Pick a random message from base set messageID := rand.Intn(len(m.Config.Raid.Messages)) tpl, ok := m.templates[templateTypeRaid][m.Config.Raid.Messages[messageID]] if !ok { // Broken template! chat.WriteMessage(m.db, m.logger, chat.WriteMessageRequest{ Message: m.Config.Raid.Messages[messageID], Announce: m.Config.Raid.Announce, }) return } // If we have variations, get the available variations and pick the one with the highest minimum viewers that are met if len(m.Config.Raid.Variations) > 0 { variation := getBestValidVariation(m.Config.Raid.Variations, func(variation raidVariation) int { if variation.MinViewers != nil && raidEv.Viewers >= *variation.MinViewers { return *variation.MinViewers } return 0 }) tpl = m.replaceWithVariation(tpl, templateTypeRaid, variation.Messages) } // Compile template and send m.writeTemplate(tpl, &raidEv, m.Config.Raid.Announce) case helix.EventSubTypeChannelCheer: // Only process if we care about bits if !m.Config.Cheer.Enabled { return } // Parse as cheer event var cheerEv helix.EventSubChannelCheerEvent if err := json.Unmarshal(ev.Event, &cheerEv); err != nil { m.logger.Warn("Error parsing cheer event", zap.Error(err)) return } // Pick a random message from base set messageID := rand.Intn(len(m.Config.Cheer.Messages)) tpl, ok := m.templates[templateTypeCheer][m.Config.Cheer.Messages[messageID]] if !ok { // Broken template! chat.WriteMessage(m.db, m.logger, chat.WriteMessageRequest{ Message: m.Config.Cheer.Messages[messageID], Announce: m.Config.Cheer.Announce, }) return } // If we have variations, get the available variations and pick the one with the highest minimum amount that is met if len(m.Config.Cheer.Variations) > 0 { variation := getBestValidVariation(m.Config.Cheer.Variations, func(variation cheerVariation) int { if variation.MinAmount != nil && cheerEv.Bits >= *variation.MinAmount { return *variation.MinAmount } return 0 }) tpl = m.replaceWithVariation(tpl, templateTypeCheer, variation.Messages) } // Compile template and send m.writeTemplate(tpl, &cheerEv, m.Config.Cheer.Announce) case helix.EventSubTypeChannelSubscription: // Only process if we care about subscriptions if !m.Config.Subscription.Enabled { return } // Parse as subscription event var subEv helix.EventSubChannelSubscribeEvent if err := json.Unmarshal(ev.Event, &subEv); err != nil { m.logger.Warn("Error parsing new subscription event", zap.Error(err)) return } m.addMixedEvent(subEv) case helix.EventSubTypeChannelSubscriptionMessage: // Only process if we care about subscriptions if !m.Config.Subscription.Enabled { return } // Parse as subscription event var subEv helix.EventSubChannelSubscriptionMessageEvent err := json.Unmarshal(ev.Event, &subEv) if err != nil { m.logger.Warn("Error parsing returning subscription event", zap.Error(err)) return } m.addMixedEvent(subEv) case helix.EventSubTypeChannelSubscriptionGift: // Only process if we care about gifted subs if !m.Config.GiftSub.Enabled { return } // Parse as gift event var giftEv helix.EventSubChannelSubscriptionGiftEvent if err := json.Unmarshal(ev.Event, &giftEv); err != nil { m.logger.Warn("Error parsing subscription gifted event", zap.Error(err)) return } // Pick a random message from base set messageID := rand.Intn(len(m.Config.GiftSub.Messages)) tpl, ok := m.templates[templateTypeGift][m.Config.GiftSub.Messages[messageID]] if !ok { // Broken template! chat.WriteMessage(m.db, m.logger, chat.WriteMessageRequest{ Message: m.Config.GiftSub.Messages[messageID], Announce: m.Config.GiftSub.Announce, }) return } // 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(m.Config.GiftSub.Variations) > 0 { if giftEv.IsAnonymous { variation := getBestValidVariation(m.Config.GiftSub.Variations, func(variation giftSubVariation) int { if variation.IsAnonymous != nil && *variation.IsAnonymous { return 1 } return 0 }) tpl = m.replaceWithVariation(tpl, templateTypeGift, variation.Messages) } else if giftEv.CumulativeTotal > 0 { variation := getBestValidVariation(m.Config.GiftSub.Variations, func(variation giftSubVariation) int { if variation.MinCumulative != nil && *variation.MinCumulative > giftEv.CumulativeTotal { return *variation.MinCumulative } return 0 }) tpl = m.replaceWithVariation(tpl, templateTypeGift, variation.Messages) } } // Compile template and send m.writeTemplate(tpl, &giftEv, m.Config.GiftSub.Announce) } } func (m *Module) replaceWithVariation(tpl *template.Template, templateType templateType, messages []string) *template.Template { if messages != nil { messageID := rand.Intn(len(messages)) // Make sure the template is valid if temp, ok := m.templates[templateType][messages[messageID]]; ok { return temp } } return tpl } // For variations, some variations are better than others, this function returns the best one // by using a provided score function. The score is 0 or less if the variation is not valid, // and 1 or more if it is valid. The variation with the highest score is returned. func getBestValidVariation[T any](variations []T, filterFunc func(T) int) T { var best T var bestScore int for _, variation := range variations { score := filterFunc(variation) if score > bestScore { best = variation bestScore = score } } return best }