Merge branch 'main' into 'piper/remove-log-source-info'
# Conflicts: # config.yaml # internal/config/config.go
This commit is contained in:
commit
1cdaa8fc7e
8 changed files with 239 additions and 19 deletions
|
|
@ -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
|
||||
|
|
|
|||
15
config.yaml
15
config.yaml
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
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() {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
// empty for now, we may do some validation later.
|
||||
return nil
|
||||
}
|
||||
|
||||
type ChallengeEvent struct {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue