Files
2026-09-06 15:15:21 +02:00

63 lines
2.1 KiB
Go

// Package ratelimit enforces per-key and per-end-user request ceilings.
//
// Redis is authoritative for the live window; api.usage_daily holds completed
// days. A Redis flush therefore loses at most the window in progress, which for
// abuse detection is an acceptable trade.
package ratelimit
import (
"context"
"time"
)
// Result is what a limiter says about one request.
type Result struct {
Allowed bool
Limit int
Remaining int
RetryAfter time.Duration
}
// Limiter is deliberately narrow so the middleware can be tested without Redis,
// and so a self-hoster running a single instance could drop in an in-process
// implementation instead of standing up Redis at all.
type Limiter interface {
// Allow records one request against key and reports whether it may
// proceed. Implementations must be atomic: a check-then-increment race
// under load is exactly when the limit matters most.
Allow(ctx context.Context, key string, limit int, window time.Duration) (Result, error)
}
// Quota is the ceiling for one tier, from api.keys.quota_tier.
type Quota struct {
// Requests allowed per window for the key as a whole.
KeyLimit int
// Requests allowed per window for a single end user within that key. One
// Discord user should not be able to consume a whole self-hosted
// instance's allowance, and a key whose traffic is 90% one user is the
// enumeration signal worth alerting on.
UserLimit int
Window time.Duration
}
// Tiers maps api.keys.quota_tier to its ceiling. Values are a starting point,
// not a considered policy: watch real usage before hardening them.
var Tiers = map[string]Quota{
"default": {KeyLimit: 600, UserLimit: 120, Window: time.Minute},
"verified": {KeyLimit: 3000, UserLimit: 600, Window: time.Minute},
"trusted": {KeyLimit: 12000, UserLimit: 2400, Window: time.Minute},
"internal": {KeyLimit: 0, UserLimit: 0, Window: time.Minute}, // 0 = unlimited
}
// QuotaFor falls back to the tightest tier for an unknown name, so a typo in
// the database cannot accidentally grant unlimited access.
func QuotaFor(tier string) Quota {
if q, ok := Tiers[tier]; ok {
return q
}
return Tiers["default"]
}