Skip to content
This repository has been archived by the owner on Oct 25, 2024. It is now read-only.

Commit

Permalink
introduce rate limiting of chat
Browse files Browse the repository at this point in the history
  • Loading branch information
maxsupermanhd committed Sep 19, 2024
1 parent 7be24ff commit 34e3c01
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 0 deletions.
9 changes: 9 additions & 0 deletions connfilter.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,15 @@ where g.game_time < 60000 and g.time_started + $1::interval > now() and (i.pkey
}
}

// stage 8 chat rate limit mute
if account == nil {
rlcDuration, rlcExceeded := ratelimitChatCheck(inst, ip)
if rlcExceeded {
jd.AllowChat = false
jd.Messages = append(jd.Messages, "You were limited to quickchat due to spamming for "+rlcDuration.String())
}
}

inst.logger.Printf("connfilter resolved key %v nljoin %v (acc %v) nlplay %v (action %v) nlchat %v (allowed %v)",
pubkeyB64,
allowNonLinkedJoin, account,
Expand Down
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ func main() {

go routineDiscordErrorReporter()

ratelimitChatPenalties = ratelimitChatLoadPenalties()

recoverInstances()

signals := make(chan os.Signal, 1)
Expand Down
8 changes: 8 additions & 0 deletions messageProcessor.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,14 @@ func messageHandlerProcessChat(inst *instance, msg string) bool {
if msgtype == "WZCHATCMD" && (strings.HasPrefix(string(msgcontent), "/votekick")) {
instWriteFmt(inst, `chat direct %s %s`, msgb64pubkey, "If you would like to become a part of Autohoster moderation team, feel free to contact us: https://wz2100-autohost.net/about#contact")
}
if msgtype == "WZCHATLOB" {
ratelimitChatHandleMessage(inst, msgip)
rlcDuration, rlcExceeded := ratelimitChatCheck(inst, msgip)
if rlcExceeded {
instWriteFmt(inst, `set chat quickchat %s`, msgb64pubkey)
instWriteFmt(inst, `chat direct %s %s`, msgb64pubkey, "You were limited to quickchat due to spamming for "+rlcDuration.String())
}
}
return false
}

Expand Down
138 changes: 138 additions & 0 deletions ratelimit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package main

import (
"container/list"
"encoding/json"
"io/fs"
"log"
"maps"
"os"
"sync"
"time"
)

var (
ratelimitChatLock = sync.Mutex{}
ratelimitLastCleanup = time.Now()
ratelimitChatData = map[string]*list.List{}
ratelimitChatPenalties = map[string]time.Time{}
)

func _ratelimitChatSavePenalties() {
b, err := json.MarshalIndent(ratelimitChatPenalties, "", "\t")
if err != nil {
log.Println("Failed to save chat rate limit penalties: ", err)
return
}
perm := fs.FileMode(cfg.GetDInt(644, "filePerms"))
os.WriteFile(cfg.GetDString("ratelimitChatPenalties.json", "ratelimitPenaltiesFilename"), b, perm)
}

func ratelimitChatLoadPenalties() map[string]time.Time {
b, err := os.ReadFile(cfg.GetDString("ratelimitChatPenalties.json", "ratelimitPenaltiesFilename"))
if err != nil {
return map[string]time.Time{}
}
ret := map[string]time.Time{}
if json.Unmarshal(b, &ret) != nil {
return map[string]time.Time{}
}
return ret
}

func ratelimitChatHandleMessage(inst *instance, ip string) (time.Duration, bool) {
ca := tryCfgGetD(tryGetIntGen("ratelimitChatAmount"), 0, inst.cfgs...)
if ca <= 0 {
return 0, false
}
ct := tryCfgGetD(tryGetIntGen("ratelimitChatDuration"), 0, inst.cfgs...)
if ct <= 0 {
return 0, false
}
ratelimitChatLock.Lock()
_ratelimitCleanup()
rld, rlt := _ratelimitChatAddMessage(ip, ca, time.Duration(ct)*time.Second)
ratelimitChatLock.Unlock()
return rld, rlt
}

func _ratelimitChatAddMessage(ip string, ca int, ct time.Duration) (time.Duration, bool) {
l, ok := ratelimitChatData[ip]
if !ok {
l := list.New()
l.PushFront(time.Now())
ratelimitChatData[ip] = l
return 0, false
}
l.PushFront(time.Now())
if _ratelimitCheckList(l, ca, ct) {
lastp, ok := ratelimitChatPenalties[ip]
dur := 5 * time.Minute
if ok && time.Since(lastp) < 30*time.Minute {
dur = 45 * time.Minute
}
due := time.Now().Add(dur)
ratelimitChatPenalties[ip] = due
_ratelimitChatSavePenalties()
return dur, true
}
return 0, false
}

func _ratelimitCheckList(l *list.List, ca int, ct time.Duration) bool {
hits := 0
for e := l.Front(); e != nil; e = e.Next() {
t := e.Value.(time.Time)
if time.Since(t) < time.Second*time.Duration(ct) {
hits++
}
}
return hits >= ca
}

func ratelimitChatCheck(_ *instance, ip string) (time.Duration, bool) {
ratelimitChatLock.Lock()
rld, rlt := _ratelimitChatCheckPenalties(ip)
ratelimitChatLock.Unlock()
return rld, rlt
}

func _ratelimitChatCheckPenalties(ip string) (time.Duration, bool) {
_ratelimitCleanup()
p, ok := ratelimitChatPenalties[ip]
if !ok {
return 0, false
}
if p.After(time.Now()) {
return time.Until(p), true
} else {
return 0, false
}
}

func _ratelimitCleanup() {
if time.Since(ratelimitLastCleanup) < 5*time.Minute {
return
}
maps.DeleteFunc(ratelimitChatPenalties, func(k string, v time.Time) bool {
return time.Since(v) > time.Hour
})
maps.DeleteFunc(ratelimitChatData, func(k string, v *list.List) bool {
return _ratelimitPruneList(v, time.Hour)
})
}

func _ratelimitPruneList(l *list.List, ct time.Duration) bool {
toRemove := []*list.Element{}
for e := l.Front(); e != nil; e = e.Next() {
t := e.Value.(time.Time)
if time.Since(t) < ct {
el := e
toRemove = append(toRemove, el)
}
}
for _, v := range toRemove {
l.Remove(v)
}
return l.Len() == 0
}

0 comments on commit 34e3c01

Please sign in to comment.