Spaces:
Running
Running
| from __future__ import annotations | |
| import time | |
| from dataclasses import dataclass, field | |
| from typing import Any | |
| from starlette.responses import JSONResponse | |
| from app.core.config import Settings | |
| def rate_limit_key(user_id: str | None, ip: str) -> str: return user_id or ip | |
| class Bucket: tokens: float; updated_at: float = field(default_factory=time.time) | |
| class InMemoryTokenBucket: | |
| def __init__(self, requests_per_minute: int, burst: int) -> None: | |
| self.rate_per_second=max(requests_per_minute,1)/60.0; self.capacity=max(burst,1); self._buckets: dict[str, Bucket]={} | |
| def allow(self, key: str, cost: int=1) -> bool: | |
| now=time.time(); bucket=self._buckets.get(key) | |
| if bucket is None: bucket=Bucket(float(self.capacity), now); self._buckets[key]=bucket | |
| bucket.tokens=min(float(self.capacity), bucket.tokens + max(now-bucket.updated_at,0.0)*self.rate_per_second); bucket.updated_at=now | |
| if bucket.tokens>=cost: bucket.tokens-=cost; return True | |
| return False | |
| class RateLimitMiddleware: | |
| def __init__(self, app: Any, settings: Settings) -> None: | |
| self.app=app; self.settings=settings; self.bucket=InMemoryTokenBucket(settings.rate_limit_requests_per_minute, settings.rate_limit_burst) | |
| async def __call__(self, scope: dict[str, Any], receive: Any, send: Any) -> None: | |
| if not self.settings.rate_limit_enabled or scope.get('type')!='http': await self.app(scope, receive, send); return | |
| client=scope.get('client') or ('unknown',0); ip=client[0] if isinstance(client, tuple) else 'unknown'; headers=dict(scope.get('headers', [])); user=headers.get(b'x-matrix-user-id', b'').decode('latin-1') or None | |
| if not self.bucket.allow(rate_limit_key(user, str(ip))): | |
| response=JSONResponse(status_code=429, content={'detail':'Rate limit exceeded','code':'rate_limit_exceeded'}, headers={'Retry-After':'60'}); await response(scope, receive, send); return | |
| await self.app(scope, receive, send) | |