strimertul/twitch/timers/module.go

157 lines
3.6 KiB
Go

package timers
import (
"context"
"encoding/json"
"log/slog"
"math/rand"
"time"
"git.sr.ht/~ashkeel/containers/sync"
"git.sr.ht/~ashkeel/strimertul/database"
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
)
const AverageMessageWindow = 5
type Module struct {
Config Config
lastTrigger *sync.Map[string, time.Time]
messages *sync.Slice[int]
logger *slog.Logger
db database.Database
ctx context.Context
}
func Setup(ctx context.Context, db database.Database, logger *slog.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
if err := db.GetJSON(ConfigKey, &mod.Config); err != nil {
logger.Debug("Config load error", "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", "error", err)
}
}
if err := db.SubscribeKeyContext(ctx, ConfigKey, func(value string) {
if err := json.Unmarshal([]byte(value), &mod.Config); err != nil {
logger.Warn("Error reloading timer config", "error", err)
return
}
logger.Info("Reloaded timer config")
}); err != nil {
logger.Error("Could not set-up timer reload subscription", "error", err)
}
logger.Debug("Loaded timers", slog.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", "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) 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)
}