bingobot/internal/discord/activity.go

152 lines
3.4 KiB
Go
Raw Normal View History

package discord
import (
"context"
"errors"
"sync"
"time"
"gitlab.com/whom/bingobot/internal/config"
"gitlab.com/whom/bingobot/internal/logging"
"gitlab.com/whom/bingobot/internal/state"
)
/*
Stuff having to do with tracking and responding to user activity will be kept here.
*/
var activityTimers map[string]*UserActivityTimer
var timerMutex sync.RWMutex
var ErrTimerExpired = errors.New("timer expired")
func init() {
activityTimers = map[string]*UserActivityTimer{}
}
type UserActivityTimer struct {
UID string
Cancel context.CancelFunc
ctx context.Context
startTime time.Time
sleepDuration time.Duration
}
func NewActivityTimer(uid string) *UserActivityTimer {
return &UserActivityTimer{
UID: uid,
}
}
/*
Start() initializes the timer and calls run()
*/
func (t *UserActivityTimer) Start(ctx context.Context) {
logging.Info("starting voiceActivityTimer", "uid", t.UID)
t.sleepDuration = time.Millisecond * time.Duration(config.Get().VoiceActivityTimerSleepIntervalMillis)
activityTimerDuration := time.Second * time.Duration(config.Get().VoiceActivityThresholdSeconds)
t.startTime = time.Now()
t.ctx, t.Cancel = context.WithDeadlineCause(
ctx,
t.startTime.Add(activityTimerDuration),
ErrTimerExpired,
)
t.run()
}
/*
shouldStop() returns true for one of two reasons:
1. the context is manually cancelled. this happens
when the bot is shutting down and we must clean up.
2. the context's deadline expires naturally when it has
reached its end time.
both cases manifest as the ctx.Done() channel
being non-empty.
*/
func (t *UserActivityTimer) shouldStop() bool {
select {
case <-t.ctx.Done():
return true
default:
return false
}
}
/*
run() performs the tick loop that provides the timer functionality.
it is called from Start().
*/
func (t *UserActivityTimer) run() {
for !t.shouldStop() {
<-time.Tick(t.sleepDuration)
}
// the timer's context has been cancelled or deadline expired.
logging.Info("voiceActivityTimer stopping", "uid", t.UID, "reason", context.Cause(t.ctx))
if context.Cause(t.ctx) == ErrTimerExpired {
/*
we should start a new timer to replace this one
*/
emitUserActiveEvent(t.UID)
defer startActivityTimer(t.UID)
return
}
stopActivityTimer(t.UID)
}
/*
startActivityTimer() creates and starts a UserActivityTimer for
the specified uid.
if one already exists, it will be cancelled and recreated.
*/
func startActivityTimer(uid string) {
stopActivityTimer(uid) // if a timer already exists, stop it
timerMutex.Lock()
defer timerMutex.Unlock()
activityTimers[uid] = NewActivityTimer(uid)
go activityTimers[uid].Start(context.Background())
}
/*
stopActivityTimer() cancels any running timer for the given uid
and removes it from the activityTimers map.
*/
func stopActivityTimer(uid string) {
timerMutex.Lock()
defer timerMutex.Unlock()
if _, ok := activityTimers[uid]; ok {
activityTimers[uid].Cancel()
delete(activityTimers, uid)
}
}
/*
emitUserActiveEvent() is called when a UserActivityTimer reaches its deadline without being cancelled.
this represents a user having reached the defined VoiceActivityThresholdSeconds
in the config.
*/
func emitUserActiveEvent(uid string) {
err := state.PublishEvent(state.NewUserActiveEvent(uid))
if err != nil {
logging.Error("failed to publish UserActiveEvent", "uid", uid, "err", err)
return
}
logging.Info("published UserActiveEvent", "uid", uid)
}