Merge branch 'voice-activity-events' into 'main'
Implement UserActive events See merge request whom/bingobot!12
This commit is contained in:
commit
056505d538
5 changed files with 205 additions and 16 deletions
15
config.yaml
15
config.yaml
|
|
@ -4,3 +4,18 @@ log_max_size_mb: 500
|
||||||
log_max_backups: 3
|
log_max_backups: 3
|
||||||
log_max_age_days: 365
|
log_max_age_days: 365
|
||||||
log_compression: false
|
log_compression: false
|
||||||
|
|
||||||
|
# how long (in seconds) a user needs to be in vc
|
||||||
|
# in order to generate a UserActive event
|
||||||
|
voice_activity_threshold_seconds: 600
|
||||||
|
|
||||||
|
# how long (in milliseconds) a voice activity timer sleeps at a time between
|
||||||
|
# context cancellation checks.
|
||||||
|
|
||||||
|
# a higher value means the function sleeps longer which could be
|
||||||
|
# useful for some reason in the future
|
||||||
|
|
||||||
|
# a higher value also means that the timer could take longer to cancel.
|
||||||
|
|
||||||
|
# current recommended value is 1000ms.
|
||||||
|
voice_activity_timer_sleep_interval_millis: 1000
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,25 @@ type AppConfig struct {
|
||||||
LogMaxAgeDays int `yaml:"log_max_age_days"`
|
LogMaxAgeDays int `yaml:"log_max_age_days"`
|
||||||
LogCompression bool `yaml:"log_compression"`
|
LogCompression bool `yaml:"log_compression"`
|
||||||
LogAddSource bool `yaml:"log_add_source"`
|
LogAddSource bool `yaml:"log_add_source"`
|
||||||
|
|
||||||
|
/*
|
||||||
|
how long (in seconds) a user needs to be in vc in order to generate a
|
||||||
|
UserActive event
|
||||||
|
*/
|
||||||
|
VoiceActivityThresholdSeconds int `yaml:"voice_activity_threshold_seconds"`
|
||||||
|
|
||||||
|
/*
|
||||||
|
how long (in milliseconds) a voice activity timer sleeps at a time between
|
||||||
|
context cancellation checks.
|
||||||
|
|
||||||
|
a higher value means the function sleeps longer which could be
|
||||||
|
useful for some reason in the future
|
||||||
|
|
||||||
|
a higher value also means that the timer could take longer to cancel.
|
||||||
|
|
||||||
|
current recommended value is 1000ms.
|
||||||
|
*/
|
||||||
|
VoiceActivityTimerSleepIntervalMillis int `yaml:"voice_activity_timer_sleep_interval_millis"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var config *AppConfig
|
var config *AppConfig
|
||||||
|
|
@ -66,4 +85,6 @@ func setDefaults() {
|
||||||
viper.SetDefault("LogMaxAgeDays", 365)
|
viper.SetDefault("LogMaxAgeDays", 365)
|
||||||
viper.SetDefault("LogCompression", false)
|
viper.SetDefault("LogCompression", false)
|
||||||
viper.SetDefault("LogAddSource", true)
|
viper.SetDefault("LogAddSource", true)
|
||||||
|
viper.SetDefault("VoiceActivityThresholdSeconds", 600)
|
||||||
|
viper.SetDefault("VoiceActivityTimerSleepIntervalMillis", 1000)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
151
internal/discord/activity.go
Normal file
151
internal/discord/activity.go
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
func addHandlers() {
|
func addHandlers() {
|
||||||
session.s.AddHandler(handleConnect)
|
session.s.AddHandler(handleConnect)
|
||||||
session.s.AddHandler(handleDisconnect)
|
session.s.AddHandler(handleDisconnect)
|
||||||
|
session.s.AddHandler(handleVoiceStateUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleConnect(s *discordgo.Session, e *discordgo.Connect) {
|
func handleConnect(s *discordgo.Session, e *discordgo.Connect) {
|
||||||
|
|
@ -19,3 +20,17 @@ func handleDisconnect(s *discordgo.Session, e *discordgo.Disconnect) {
|
||||||
session.connected = false
|
session.connected = false
|
||||||
logging.Info("discord session disconnected")
|
logging.Info("discord session disconnected")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleVoiceStateUpdate(_ *discordgo.Session, e *discordgo.VoiceStateUpdate) {
|
||||||
|
if e.ChannelID == "" {
|
||||||
|
// user disconnected
|
||||||
|
logging.Info("user left channel", "uid", e.UserID)
|
||||||
|
stopActivityTimer(e.UserID)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// user connected
|
||||||
|
logging.Info("user joined channel", "uid", e.UserID, "channel", e.ChannelID)
|
||||||
|
startActivityTimer(e.UserID)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitlab.com/whom/bingobot/internal/discord"
|
|
||||||
"gitlab.com/whom/bingobot/internal/logging"
|
"gitlab.com/whom/bingobot/internal/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -359,20 +358,8 @@ func (ue UserEvent) Data() map[string]string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ue UserEvent) Validate() error {
|
func (ue UserEvent) Validate() error {
|
||||||
if discord.Connected() {
|
// empty for now, we may do some validation later.
|
||||||
_, err := discord.User(ue.uid)
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
// I would love to know how to actually fail here
|
|
||||||
// and still have unit testable code.
|
|
||||||
|
|
||||||
logging.Error(
|
|
||||||
"can't validate UserEvent: nil discord session",
|
|
||||||
"event",
|
|
||||||
fmt.Sprintf("%+v", ue),
|
|
||||||
)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChallengeEvent struct {
|
type ChallengeEvent struct {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue