You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

fakelag.go 2.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. // Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
  2. // released under the MIT license
  3. package irc
  4. import (
  5. "time"
  6. )
  7. // fakelag is a system for artificially delaying commands when a user issues
  8. // them too rapidly
  9. type FakelagState uint
  10. const (
  11. // initially, the client is "bursting" and can send n commands without
  12. // encountering fakelag
  13. FakelagBursting FakelagState = iota
  14. // after that, they're "throttled" and we sleep in between commands until
  15. // they're spaced sufficiently far apart
  16. FakelagThrottled
  17. )
  18. // this is intentionally not threadsafe, because it should only be touched
  19. // from the loop that accepts the client's input and runs commands
  20. type Fakelag struct {
  21. config FakelagConfig
  22. suspended bool
  23. nowFunc func() time.Time
  24. sleepFunc func(time.Duration)
  25. state FakelagState
  26. burstCount uint // number of messages sent in the current burst
  27. lastTouch time.Time
  28. }
  29. func (fl *Fakelag) Initialize(config FakelagConfig) {
  30. fl.config = config
  31. fl.nowFunc = time.Now
  32. fl.sleepFunc = time.Sleep
  33. fl.state = FakelagBursting
  34. }
  35. // Idempotently turn off fakelag if it's enabled
  36. func (fl *Fakelag) Suspend() {
  37. if fl.config.Enabled {
  38. fl.suspended = true
  39. fl.config.Enabled = false
  40. }
  41. }
  42. // Idempotently turn fakelag back on if it was previously Suspend'ed
  43. func (fl *Fakelag) Unsuspend() {
  44. if fl.suspended {
  45. fl.config.Enabled = true
  46. fl.suspended = false
  47. }
  48. }
  49. // register a new command, sleep if necessary to delay it
  50. func (fl *Fakelag) Touch() {
  51. if !fl.config.Enabled {
  52. return
  53. }
  54. now := fl.nowFunc()
  55. // XXX if lastTouch.IsZero(), treat it as "very far in the past", which is fine
  56. elapsed := now.Sub(fl.lastTouch)
  57. fl.lastTouch = now
  58. if fl.state == FakelagBursting {
  59. // determine if the previous burst is over
  60. if elapsed > fl.config.Cooldown {
  61. fl.burstCount = 0
  62. }
  63. fl.burstCount++
  64. if fl.burstCount > fl.config.BurstLimit {
  65. // reset burst window for next time
  66. fl.burstCount = 0
  67. // transition to throttling
  68. fl.state = FakelagThrottled
  69. // continue to throttling logic
  70. } else {
  71. return
  72. }
  73. }
  74. if fl.state == FakelagThrottled {
  75. if elapsed > fl.config.Cooldown {
  76. // let them burst again
  77. fl.state = FakelagBursting
  78. fl.burstCount = 1
  79. return
  80. }
  81. var sleepDuration time.Duration
  82. if fl.config.MessagesPerWindow > 0 {
  83. // space them out by at least window/messagesperwindow
  84. sleepDuration = time.Duration((int64(fl.config.Window) / int64(fl.config.MessagesPerWindow)) - int64(elapsed))
  85. } else {
  86. // only burst messages are allowed: sleep until cooldown expires,
  87. // then count this as a burst message
  88. sleepDuration = time.Duration(int64(fl.config.Cooldown) - int64(elapsed))
  89. fl.state = FakelagBursting
  90. fl.burstCount = 1
  91. }
  92. if sleepDuration > 0 {
  93. fl.sleepFunc(sleepDuration)
  94. // the touch time should take into account the time we slept
  95. fl.lastTouch = fl.nowFunc()
  96. }
  97. }
  98. }