package timers import ( "context" "math/rand" "time" "git.sr.ht/~ashkeel/containers/sync" jsoniter "github.com/json-iterator/go" "go.uber.org/zap" "git.sr.ht/~ashkeel/strimertul/database" "git.sr.ht/~ashkeel/strimertul/twitch/chat" ) var json = jsoniter.ConfigFastest const AverageMessageWindow = 5 type Module struct { Config Config lastTrigger *sync.Map[string, time.Time] messages *sync.Slice[int] logger *zap.Logger db database.Database ctx context.Context cancelTimerSub database.CancelFunc } func Setup(ctx context.Context, db database.Database, logger *zap.Logger) *Module { mod := &Module{ lastTrigger: sync.NewMap[string, time.Time](), messages: sync.NewSlice[int](), db: db, ctx: ctx, logger: logger, } // Fill messages with zero values // (This can probably be done faster) for i := 0; i < AverageMessageWindow; i++ { mod.messages.Push(0) } // Load config from database err := db.GetJSON(ConfigKey, &mod.Config) if err != nil { logger.Debug("Config load error", zap.Error(err)) mod.Config = Config{ Timers: make(map[string]ChatTimer), } // Save empty config err = db.PutJSON(ConfigKey, mod.Config) if err != nil { logger.Warn("Could not save default config for bot timers", zap.Error(err)) } } mod.cancelTimerSub, err = db.SubscribeKey(ConfigKey, func(value string) { err := json.UnmarshalFromString(value, &mod.Config) if err != nil { logger.Debug("Error reloading timer config", zap.Error(err)) } else { logger.Info("Reloaded timer config") } }) if err != nil { logger.Error("Could not set-up timer reload subscription", zap.Error(err)) } 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 *Module) 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.db.PutJSON(chat.ActivityKey, m.messages.Get()) if err != nil { m.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 *Module) ProcessTimer(name string, timer ChatTimer, 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 chat.WriteMessage(m.db, m.logger, chat.WriteMessageRequest{ Message: message, Announce: timer.Announce, }) // Update last trigger m.lastTrigger.SetKey(name, now) } func (m *Module) Close() { if m.cancelTimerSub != nil { m.cancelTimerSub() } } func (m *Module) currentChatActivity() int { total := 0 for _, v := range m.messages.Get() { total += v } return total } func (m *Module) OnMessage() { index := time.Now().Minute() % AverageMessageWindow m.messages.SetIndex(index, 1) }