65 lines
1.7 KiB
Go
65 lines
1.7 KiB
Go
package service
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// rateLimits defines minimum seconds between the same action by the same user on the same work.
|
|
var rateLimits = map[string]int64{
|
|
"view": 60,
|
|
"download": 300,
|
|
"like": 3600,
|
|
}
|
|
|
|
// RateLimiter provides per-user, per-action, per-work rate limiting backed by an in-memory store.
|
|
// The store is never persisted; it resets on service restart (same behaviour as the Python version).
|
|
type RateLimiter struct {
|
|
mu sync.Mutex
|
|
// fingerprint → actionType → workID → last unix timestamp
|
|
actions map[string]map[string]map[string]int64
|
|
}
|
|
|
|
// NewRateLimiter allocates a new empty RateLimiter.
|
|
func NewRateLimiter() *RateLimiter {
|
|
return &RateLimiter{
|
|
actions: make(map[string]map[string]map[string]int64),
|
|
}
|
|
}
|
|
|
|
// CanPerform returns true and records the timestamp if the rate limit window has elapsed.
|
|
// Returns false if the action is too frequent.
|
|
func (rl *RateLimiter) CanPerform(fingerprint, actionType, workID string) bool {
|
|
rl.mu.Lock()
|
|
defer rl.mu.Unlock()
|
|
|
|
limit, ok := rateLimits[actionType]
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
now := time.Now().Unix()
|
|
|
|
if rl.actions[fingerprint] == nil {
|
|
rl.actions[fingerprint] = make(map[string]map[string]int64)
|
|
}
|
|
if rl.actions[fingerprint][actionType] == nil {
|
|
rl.actions[fingerprint][actionType] = make(map[string]int64)
|
|
}
|
|
|
|
last := rl.actions[fingerprint][actionType][workID]
|
|
if now-last >= limit {
|
|
rl.actions[fingerprint][actionType][workID] = now
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Fingerprint creates an MD5 hex string from IP and User-Agent for rate-limit keying.
|
|
func Fingerprint(ip, userAgent string) string {
|
|
h := md5.Sum([]byte(ip + ":" + userAgent))
|
|
return fmt.Sprintf("%x", h)
|
|
}
|