2024-11-13 16:32:58 -08:00
|
|
|
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()
|
|
|
|
|
|
2025-01-08 15:27:27 -08:00
|
|
|
if _, ok := activityTimers[uid]; ok {
|
2024-11-13 16:32:58 -08:00
|
|
|
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)
|
|
|
|
|
}
|