2018-03-22 16:04:21 +01:00
|
|
|
// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
|
|
|
// released under the MIT license
|
|
|
|
|
|
|
|
package irc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
// fakelag is a system for artificially delaying commands when a user issues
|
|
|
|
// them too rapidly
|
|
|
|
|
|
|
|
type FakelagState uint
|
|
|
|
|
|
|
|
const (
|
|
|
|
// initially, the client is "bursting" and can send n commands without
|
|
|
|
// encountering fakelag
|
|
|
|
FakelagBursting FakelagState = iota
|
|
|
|
// after that, they're "throttled" and we sleep in between commands until
|
|
|
|
// they're spaced sufficiently far apart
|
|
|
|
FakelagThrottled
|
|
|
|
)
|
|
|
|
|
|
|
|
// this is intentionally not threadsafe, because it should only be touched
|
|
|
|
// from the loop that accepts the client's input and runs commands
|
|
|
|
type Fakelag struct {
|
2019-03-08 09:12:21 +01:00
|
|
|
config FakelagConfig
|
2020-03-27 15:40:19 +01:00
|
|
|
suspended bool
|
2019-03-08 09:12:21 +01:00
|
|
|
nowFunc func() time.Time
|
|
|
|
sleepFunc func(time.Duration)
|
2018-03-22 16:04:21 +01:00
|
|
|
|
|
|
|
state FakelagState
|
|
|
|
burstCount uint // number of messages sent in the current burst
|
|
|
|
lastTouch time.Time
|
|
|
|
}
|
|
|
|
|
2019-03-08 09:12:21 +01:00
|
|
|
func (fl *Fakelag) Initialize(config FakelagConfig) {
|
|
|
|
fl.config = config
|
|
|
|
fl.nowFunc = time.Now
|
|
|
|
fl.sleepFunc = time.Sleep
|
|
|
|
fl.state = FakelagBursting
|
2018-03-22 16:04:21 +01:00
|
|
|
}
|
|
|
|
|
2020-03-27 15:40:19 +01:00
|
|
|
// Idempotently turn off fakelag if it's enabled
|
|
|
|
func (fl *Fakelag) Suspend() {
|
|
|
|
if fl.config.Enabled {
|
|
|
|
fl.suspended = true
|
|
|
|
fl.config.Enabled = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Idempotently turn fakelag back on if it was previously Suspend'ed
|
|
|
|
func (fl *Fakelag) Unsuspend() {
|
|
|
|
if fl.suspended {
|
|
|
|
fl.config.Enabled = true
|
|
|
|
fl.suspended = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-22 16:04:21 +01:00
|
|
|
// register a new command, sleep if necessary to delay it
|
|
|
|
func (fl *Fakelag) Touch() {
|
2019-03-08 09:12:21 +01:00
|
|
|
if !fl.config.Enabled {
|
2018-03-22 16:04:21 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
now := fl.nowFunc()
|
|
|
|
// XXX if lastTouch.IsZero(), treat it as "very far in the past", which is fine
|
|
|
|
elapsed := now.Sub(fl.lastTouch)
|
|
|
|
fl.lastTouch = now
|
|
|
|
|
|
|
|
if fl.state == FakelagBursting {
|
|
|
|
// determine if the previous burst is over
|
2019-03-08 09:12:21 +01:00
|
|
|
if elapsed > fl.config.Cooldown {
|
2018-03-22 16:04:21 +01:00
|
|
|
fl.burstCount = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
fl.burstCount++
|
2019-03-08 09:12:21 +01:00
|
|
|
if fl.burstCount > fl.config.BurstLimit {
|
2018-03-22 16:04:21 +01:00
|
|
|
// reset burst window for next time
|
|
|
|
fl.burstCount = 0
|
|
|
|
// transition to throttling
|
|
|
|
fl.state = FakelagThrottled
|
|
|
|
// continue to throttling logic
|
|
|
|
} else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if fl.state == FakelagThrottled {
|
2019-03-08 09:12:21 +01:00
|
|
|
if elapsed > fl.config.Cooldown {
|
2018-03-28 19:18:08 +02:00
|
|
|
// let them burst again
|
2018-03-22 16:04:21 +01:00
|
|
|
fl.state = FakelagBursting
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// space them out by at least window/messagesperwindow
|
2019-03-08 09:12:21 +01:00
|
|
|
sleepDuration := time.Duration((int64(fl.config.Window) / int64(fl.config.MessagesPerWindow)) - int64(elapsed))
|
2018-04-16 10:30:01 +02:00
|
|
|
if sleepDuration > 0 {
|
|
|
|
fl.sleepFunc(sleepDuration)
|
|
|
|
// the touch time should take into account the time we slept
|
|
|
|
fl.lastTouch = fl.nowFunc()
|
2018-03-22 16:04:21 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|