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) }