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) }