74 lines
2.1 KiB
Python
74 lines
2.1 KiB
Python
"""Outbound send guard: per-UTC-day counter + structured logging.
|
|
|
|
The voip.ms API caps sending at 100 SMS/MMS per day. This guard refuses
|
|
sends beyond a configurable limit (default 100) before they hit the network,
|
|
and logs every outbound send.
|
|
|
|
Note: this counter is per-process. If you run multiple instances behind a
|
|
load balancer, replace it with a shared store (Redis, etc.).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
|
|
from .exceptions import VoipMsRateLimitError
|
|
|
|
logger = logging.getLogger("voipms.sms.guard")
|
|
|
|
|
|
class DailySendCounter:
|
|
"""Counts outbound sends per UTC day, refusing sends past the limit."""
|
|
|
|
def __init__(self, limit: int) -> None:
|
|
self._limit = limit
|
|
self._date: str | None = None
|
|
self._count = 0
|
|
|
|
@staticmethod
|
|
def _today() -> str:
|
|
return datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
|
|
@property
|
|
def used_today(self) -> int:
|
|
if self._date != self._today():
|
|
return 0
|
|
return self._count
|
|
|
|
@property
|
|
def limit(self) -> int:
|
|
return self._limit
|
|
|
|
def check_and_increment(self) -> int:
|
|
"""Raise VoipMsRateLimitError if the daily cap is hit; otherwise increment.
|
|
|
|
Returns the count after this send.
|
|
"""
|
|
today = self._today()
|
|
if self._date != today:
|
|
self._date = today
|
|
self._count = 0
|
|
|
|
if self._count >= self._limit:
|
|
raise VoipMsRateLimitError(
|
|
f"daily send limit ({self._limit}) reached",
|
|
limit=self._limit,
|
|
used_today=self._count,
|
|
)
|
|
|
|
self._count += 1
|
|
return self._count
|
|
|
|
def log_send(self, *, kind: str, did: str, dst: str, chars: int) -> None:
|
|
remaining = self._limit - self._count
|
|
logger.info(
|
|
"voipms_send kind=%s did=%s dst=%s chars=%d used_today=%d limit=%d remaining=%d",
|
|
kind,
|
|
did,
|
|
dst,
|
|
chars,
|
|
self._count,
|
|
self._limit,
|
|
remaining,
|
|
) |