package twitch import ( "math/rand" "time" "git.sr.ht/~ashkeel/containers/sync" irc "github.com/gempir/go-twitch-irc/v4" "go.uber.org/zap" "git.sr.ht/~ashkeel/strimertul/database" ) const BotTimersKey = "twitch/bot-modules/timers/config" type BotTimersConfig struct { Timers map[string]BotTimer `json:"timers" desc:"List of timers as a dictionary"` } type BotTimer struct { // Whether the timer is enabled Enabled bool `json:"enabled" desc:"Enable the timer"` // Timer name (must be unique) Name string `json:"name" desc:"Timer name (must be unique)"` // Minimum chat messages in the last 5 minutes for timer to trigger MinimumChatActivity int `json:"minimum_chat_activity" desc:"Minimum chat messages in the last 5 minutes for timer to trigger"` // Minimum amount of time (in seconds) that needs to pass before it triggers again MinimumDelay int `json:"minimum_delay" desc:"Minimum amount of time (in seconds) that needs to pass before it triggers again"` // Messages to write (randomly chosen) Messages []string `json:"messages" desc:"Messages to write (randomly chosen)"` } const AverageMessageWindow = 5 type BotTimerModule struct { Config BotTimersConfig bot *Bot lastTrigger *sync.Map[string, time.Time] messages *sync.Slice[int] cancelTimerSub database.CancelFunc } func SetupTimers(bot *Bot) *BotTimerModule { mod := &BotTimerModule{ bot: bot, lastTrigger: sync.NewMap[string, time.Time](), messages: sync.NewSlice[int](), } // Fill messages with zero values // (This can probably be done faster) for i := 0; i < AverageMessageWindow; i += 1 { mod.messages.Push(0) } // Load config from database err := bot.api.db.GetJSON(BotTimersKey, &mod.Config) if err != nil { bot.logger.Debug("Config load error", zap.Error(err)) mod.Config = BotTimersConfig{ Timers: make(map[string]BotTimer), } // Save empty config err = bot.api.db.PutJSON(BotTimersKey, mod.Config) if err != nil { bot.logger.Warn("Could not save default config for bot timers", zap.Error(err)) } } err, mod.cancelTimerSub = bot.api.db.SubscribeKey(BotTimersKey, func(value string) { err := json.UnmarshalFromString(value, &mod.Config) if err != nil { bot.logger.Debug("Error reloading timer config", zap.Error(err)) } else { bot.logger.Info("Reloaded timer config") } }) if err != nil { bot.logger.Error("Could not set-up timer reload subscription", zap.Error(err)) } bot.logger.Debug("Loaded timers", zap.Int("timers", len(mod.Config.Timers))) // Start goroutine for clearing message counters and running timers go mod.runTimers() return mod } func (m *BotTimerModule) runTimers() { for { // Wait until next tick (remainder until next minute, as close to 0 seconds as possible) currentTime := time.Now() nextTick := currentTime.Round(time.Minute).Add(time.Minute) timeUntilNextTick := nextTick.Sub(currentTime) time.Sleep(timeUntilNextTick) err := m.bot.api.db.PutJSON(ChatActivityKey, m.messages.Get()) if err != nil { m.bot.logger.Warn("Error saving chat activity", zap.Error(err)) } // Calculate activity activity := m.currentChatActivity() // Reset timer index := time.Now().Minute() % AverageMessageWindow messages := m.messages.Get() messages[index] = 0 m.messages.Set(messages) // Run timers for name, timer := range m.Config.Timers { m.ProcessTimer(name, timer, activity) } } } func (m *BotTimerModule) ProcessTimer(name string, timer BotTimer, activity int) { // Must be enabled if !timer.Enabled { return } // Check if enough time has passed lastTriggeredTime, ok := m.lastTrigger.GetKey(name) if !ok { // If it's the first time we're checking it, start the cooldown lastTriggeredTime = time.Now() m.lastTrigger.SetKey(name, lastTriggeredTime) } minDelay := timer.MinimumDelay if minDelay < 60 { minDelay = 60 } now := time.Now() if now.Sub(lastTriggeredTime) < time.Duration(minDelay)*time.Second { return } // Make sure chat activity is high enough if activity < timer.MinimumChatActivity { return } // Pick a random message message := timer.Messages[rand.Intn(len(timer.Messages))] // Write message to chat m.bot.WriteMessage(message) // Update last trigger m.lastTrigger.SetKey(name, now) } func (m *BotTimerModule) Close() { if m.cancelTimerSub != nil { m.cancelTimerSub() } } func (m *BotTimerModule) currentChatActivity() int { total := 0 for _, v := range m.messages.Get() { total += v } return total } func (m *BotTimerModule) OnMessage(message irc.PrivateMessage) { index := message.Time.Minute() % AverageMessageWindow m.messages.SetIndex(index, 1) }