Merge branch 'main' into 'piper/remove-log-source-info'

# Conflicts:
#   config.yaml
#   internal/config/config.go
This commit is contained in:
piper pentagram 2024-11-14 19:11:58 +00:00
commit 1cdaa8fc7e
8 changed files with 239 additions and 19 deletions

View file

@ -2,6 +2,7 @@ image: golang:latest
stages:
- build
- test
compile:
stage: build
@ -11,3 +12,13 @@ compile:
paths:
- bingobot
- start.sh
tests-state-pkg:
stage: test
script:
- go test ./internal/state
tests-config-pkg:
stage: test
script:
- go test ./internal/config

View file

@ -4,3 +4,18 @@ log_max_size_mb: 500
log_max_backups: 3
log_max_age_days: 365
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

View file

@ -13,14 +13,46 @@ type AppConfig struct {
LogMaxBackups int `yaml:"log_max_backups"`
LogMaxAgeDays int `yaml:"log_max_age_days"`
LogCompression bool `yaml:"log_compression"`
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
func init() {
setDefaults()
viper.Unmarshal(&config)
}
func Get() *AppConfig {
return config
}
func GetDefaultConfig() *AppConfig {
var config *AppConfig
setDefaults()
viper.Unmarshal(&config)
return config
}
func Init() error {
setDefaults()
@ -52,4 +84,7 @@ func setDefaults() {
viper.SetDefault("LogMaxBackups", 3)
viper.SetDefault("LogMaxAgeDays", 365)
viper.SetDefault("LogCompression", false)
viper.SetDefault("LogAddSource", true)
viper.SetDefault("VoiceActivityThresholdSeconds", 600)
viper.SetDefault("VoiceActivityTimerSleepIntervalMillis", 1000)
}

View file

@ -25,12 +25,9 @@ log_compression: false
func TestDefaultConfigs(t *testing.T) {
k := "testdefaultkey"
v := "testdefaultval"
viper.SetDefault(k, v)
_, err := Parse()
if err != nil {
if err := Init(); err != nil {
t.Error(err)
}

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

View file

@ -8,6 +8,7 @@ import (
func addHandlers() {
session.s.AddHandler(handleConnect)
session.s.AddHandler(handleDisconnect)
session.s.AddHandler(handleVoiceStateUpdate)
}
func handleConnect(s *discordgo.Session, e *discordgo.Connect) {
@ -19,3 +20,17 @@ func handleDisconnect(s *discordgo.Session, e *discordgo.Disconnect) {
session.connected = false
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)
}

View file

@ -13,7 +13,6 @@ import (
"sync"
"time"
"gitlab.com/whom/bingobot/internal/discord"
"gitlab.com/whom/bingobot/internal/logging"
)
@ -359,20 +358,8 @@ func (ue UserEvent) Data() map[string]string {
}
func (ue UserEvent) Validate() error {
if discord.Connected() {
_, 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),
)
// empty for now, we may do some validation later.
return nil
}
}
type ChallengeEvent struct {

View file

@ -4,6 +4,8 @@ import (
"fmt"
"testing"
"time"
"gitlab.com/whom/bingobot/internal/logging"
)
/* WARNING:
@ -12,8 +14,15 @@ import (
*/
const TestTok = "TEST_NAME"
var loggingInitialized = false
func SetupTest(t *testing.T) {
// have to set up logger
if !loggingInitialized {
logging.Init()
loggingInitialized = true
}
old, _ := time.Parse(
time.RFC3339,
VeryOldVote,