280 lines
6.8 KiB
Go
280 lines
6.8 KiB
Go
|
package antispam
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
func normalizeUser(from string) string {
|
||
|
from = strings.ToLower(from)
|
||
|
|
||
|
if strings.Contains(from, "!") {
|
||
|
split := strings.SplitN(from, "!", 2)
|
||
|
return fmt.Sprintf("*!%s", split[1])
|
||
|
}
|
||
|
|
||
|
return from
|
||
|
}
|
||
|
|
||
|
func normalizeCommand(command string) string {
|
||
|
return strings.ToLower(command)
|
||
|
}
|
||
|
|
||
|
type AntiSpam struct {
|
||
|
userTrackingMap map[string][]time.Time
|
||
|
userTrackingLock sync.RWMutex
|
||
|
|
||
|
UserCooldownDuration time.Duration
|
||
|
|
||
|
UserRateLimitDuration time.Duration
|
||
|
UserRateLimitCount int
|
||
|
|
||
|
commandTrackingMap map[string][]time.Time
|
||
|
commandTrackingLock sync.RWMutex
|
||
|
|
||
|
CommandCooldownDuration time.Duration
|
||
|
|
||
|
CommandRateLimitDuration time.Duration
|
||
|
CommandRateLimitCount int
|
||
|
}
|
||
|
|
||
|
func New() *AntiSpam {
|
||
|
as := new(AntiSpam)
|
||
|
as.init()
|
||
|
return as
|
||
|
}
|
||
|
|
||
|
func (m *AntiSpam) init() {
|
||
|
m.userTrackingMap = map[string][]time.Time{}
|
||
|
m.commandTrackingMap = map[string][]time.Time{}
|
||
|
|
||
|
m.CommandCooldownDuration = 15 * time.Second
|
||
|
m.CommandRateLimitCount = 2
|
||
|
m.CommandRateLimitDuration = 1 * time.Minute
|
||
|
|
||
|
m.UserCooldownDuration = 3 * time.Second
|
||
|
m.UserRateLimitCount = 4
|
||
|
m.UserRateLimitDuration = 1 * time.Minute
|
||
|
}
|
||
|
|
||
|
func (m *AntiSpam) checkUserCooldown(from string) (duration time.Duration) {
|
||
|
from = normalizeUser(from)
|
||
|
|
||
|
m.userTrackingLock.RLock()
|
||
|
defer m.userTrackingLock.RUnlock()
|
||
|
|
||
|
// Are there any times registered at all for this entry?
|
||
|
if timestamps := m.userTrackingMap[from]; timestamps != nil {
|
||
|
|
||
|
var mostRecentTimestamp time.Time
|
||
|
|
||
|
// Figure out most recent time
|
||
|
for _, timestamp := range timestamps {
|
||
|
if mostRecentTimestamp.Before(timestamp) {
|
||
|
mostRecentTimestamp = timestamp
|
||
|
}
|
||
|
}
|
||
|
|
||
|
duration = mostRecentTimestamp.Add(m.UserCooldownDuration).Sub(time.Now())
|
||
|
if duration < 0 {
|
||
|
duration = 0
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (m *AntiSpam) checkCommandCooldown(command string) (duration time.Duration) {
|
||
|
command = normalizeCommand(command)
|
||
|
|
||
|
m.commandTrackingLock.RLock()
|
||
|
defer m.commandTrackingLock.RUnlock()
|
||
|
|
||
|
// Are there any times registered at all for this entry?
|
||
|
if timestamps := m.commandTrackingMap[command]; timestamps != nil {
|
||
|
|
||
|
var mostRecentTimestamp time.Time
|
||
|
|
||
|
// Figure out most recent time
|
||
|
for _, timestamp := range timestamps {
|
||
|
if mostRecentTimestamp.Before(timestamp) {
|
||
|
mostRecentTimestamp = timestamp
|
||
|
}
|
||
|
}
|
||
|
|
||
|
duration = mostRecentTimestamp.Add(m.CommandCooldownDuration).Sub(time.Now())
|
||
|
if duration < 0 {
|
||
|
duration = 0
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (m *AntiSpam) checkUserRateLimit(from string) (duration time.Duration) {
|
||
|
from = normalizeUser(from)
|
||
|
|
||
|
m.userTrackingLock.RLock()
|
||
|
defer m.userTrackingLock.RUnlock()
|
||
|
|
||
|
// Are there any times registered at all for this entry?
|
||
|
if timestamps := m.userTrackingMap[from]; timestamps != nil && len(timestamps) >= m.UserRateLimitCount {
|
||
|
leastRecentTimestamp := timestamps[len(timestamps)-m.UserRateLimitCount]
|
||
|
|
||
|
duration = leastRecentTimestamp.Add(m.UserRateLimitDuration).Sub(time.Now())
|
||
|
if duration < 0 {
|
||
|
duration = 0
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (m *AntiSpam) checkCommandRateLimit(command string) (duration time.Duration) {
|
||
|
command = normalizeCommand(command)
|
||
|
|
||
|
m.commandTrackingLock.RLock()
|
||
|
defer m.commandTrackingLock.RUnlock()
|
||
|
|
||
|
// Are there any times registered at all for this entry?
|
||
|
if timestamps := m.commandTrackingMap[command]; timestamps != nil && len(timestamps) >= m.CommandRateLimitCount {
|
||
|
leastRecentTimestamp := timestamps[len(timestamps)-m.CommandRateLimitCount]
|
||
|
|
||
|
duration = leastRecentTimestamp.Add(m.CommandRateLimitDuration).Sub(time.Now())
|
||
|
if duration < 0 {
|
||
|
duration = 0
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (m *AntiSpam) track(from string, command string) {
|
||
|
currentTime := time.Now()
|
||
|
|
||
|
command = normalizeCommand(command)
|
||
|
from = normalizeUser(from)
|
||
|
|
||
|
// User tracking
|
||
|
go func() {
|
||
|
// Register current time
|
||
|
m.userTrackingLock.Lock()
|
||
|
if m.userTrackingMap[from] == nil {
|
||
|
m.userTrackingMap[from] = []time.Time{}
|
||
|
}
|
||
|
m.userTrackingMap[from] = append(m.userTrackingMap[from], currentTime)
|
||
|
m.userTrackingLock.Unlock()
|
||
|
|
||
|
// Wait for time to run down
|
||
|
var waitDuration time.Duration
|
||
|
if m.UserCooldownDuration > m.UserRateLimitDuration {
|
||
|
waitDuration = m.UserCooldownDuration
|
||
|
} else {
|
||
|
waitDuration = m.UserRateLimitDuration
|
||
|
}
|
||
|
time.Sleep(waitDuration)
|
||
|
|
||
|
// Remove current time
|
||
|
m.userTrackingLock.Lock()
|
||
|
indexToDelete := -1
|
||
|
for i, time := range m.userTrackingMap[from] {
|
||
|
if time == currentTime {
|
||
|
indexToDelete = i
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
if indexToDelete >= 0 {
|
||
|
m.userTrackingMap[from] = append(
|
||
|
m.userTrackingMap[from][:indexToDelete],
|
||
|
m.userTrackingMap[from][indexToDelete+1:]...)
|
||
|
if len(m.userTrackingMap) == 0 {
|
||
|
m.userTrackingMap[from] = nil
|
||
|
}
|
||
|
}
|
||
|
m.userTrackingLock.Unlock()
|
||
|
}()
|
||
|
|
||
|
// Command tracking
|
||
|
go func() {
|
||
|
// Register current time
|
||
|
m.commandTrackingLock.Lock()
|
||
|
if m.commandTrackingMap[command] == nil {
|
||
|
m.commandTrackingMap[command] = []time.Time{}
|
||
|
}
|
||
|
m.commandTrackingMap[command] = append(m.commandTrackingMap[command], currentTime)
|
||
|
m.commandTrackingLock.Unlock()
|
||
|
|
||
|
// Wait for time to run down
|
||
|
var waitDuration time.Duration
|
||
|
if m.CommandCooldownDuration > m.CommandRateLimitDuration {
|
||
|
waitDuration = m.CommandCooldownDuration
|
||
|
} else {
|
||
|
waitDuration = m.CommandRateLimitDuration
|
||
|
}
|
||
|
time.Sleep(waitDuration)
|
||
|
|
||
|
// Remove current time
|
||
|
m.commandTrackingLock.Lock()
|
||
|
indexToDelete := -1
|
||
|
for i, time := range m.commandTrackingMap[command] {
|
||
|
if time == currentTime {
|
||
|
indexToDelete = i
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
if indexToDelete >= 0 {
|
||
|
m.commandTrackingMap[command] = append(
|
||
|
m.commandTrackingMap[command][:indexToDelete],
|
||
|
m.commandTrackingMap[command][indexToDelete+1:]...)
|
||
|
if len(m.commandTrackingMap) == 0 {
|
||
|
m.commandTrackingMap[command] = nil
|
||
|
}
|
||
|
}
|
||
|
m.commandTrackingLock.Unlock()
|
||
|
}()
|
||
|
}
|
||
|
|
||
|
func (m *AntiSpam) Check(from string, command string) (allow bool, waitDuration time.Duration) {
|
||
|
allow = true
|
||
|
|
||
|
// if this user has run a command in the last N seconds (user cooldown)
|
||
|
if currentDuration := m.checkUserCooldown(from); currentDuration > 0 {
|
||
|
allow = false
|
||
|
if currentDuration > waitDuration {
|
||
|
waitDuration = currentDuration
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// if this user has run X commands in the last N seconds (user rate limit)
|
||
|
if currentDuration := m.checkUserRateLimit(from); currentDuration > 0 {
|
||
|
allow = false
|
||
|
if currentDuration > waitDuration {
|
||
|
waitDuration = currentDuration
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// if command has been run just previously (command cooldown)
|
||
|
if currentDuration := m.checkCommandCooldown(command); currentDuration > 0 {
|
||
|
allow = false
|
||
|
if currentDuration > waitDuration {
|
||
|
waitDuration = currentDuration
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// if command has been run X (1?) times in the last N seconds (command rate limit)
|
||
|
if currentDuration := m.checkCommandRateLimit(command); currentDuration > 0 {
|
||
|
allow = false
|
||
|
if currentDuration > waitDuration {
|
||
|
waitDuration = currentDuration
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if allow {
|
||
|
m.track(from, command)
|
||
|
}
|
||
|
|
||
|
return
|
||
|
}
|