mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Implement compliance key lifecycle on main service, add message retention and rate limits
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"""
|
||||
Single MESSAGE_RETENTION_DAYS policy (required env, no in-code default).
|
||||
|
||||
- Positive float: age-based cleanup after that many days (same cutoff for compliance MEK,
|
||||
soft-deleted DM keys, and edit-history rows). Value may be an arithmetic expression.
|
||||
- 0: retain forever (no time-based cleanup; compliance MEK is still stored when a public key is configured).
|
||||
- -1: do not store compliance-wrapped MEK; no time-based cleanup (same as 0 for expiry).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import math
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
MESSAGE_RETENTION_DAYS = "MESSAGE_RETENTION_DAYS"
|
||||
|
||||
_state: MessageRetentionState | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MessageRetentionState:
|
||||
"""Parsed MESSAGE_RETENTION_DAYS (days, after evaluating optional expression)."""
|
||||
|
||||
days: float
|
||||
|
||||
def never_store_compliance_mek(self) -> bool:
|
||||
return self.days == -1.0
|
||||
|
||||
def cleanup_enabled(self) -> bool:
|
||||
return self.days > 0.0
|
||||
|
||||
def retention_timedelta(self) -> timedelta:
|
||||
return timedelta(days=self.days)
|
||||
|
||||
|
||||
def _eval_numeric(node: ast.AST) -> float:
|
||||
if isinstance(node, ast.Constant):
|
||||
if isinstance(node.value, bool):
|
||||
raise ValueError("MESSAGE_RETENTION_DAYS expression must be numeric")
|
||||
if isinstance(node.value, (int, float)):
|
||||
return float(node.value)
|
||||
raise ValueError("MESSAGE_RETENTION_DAYS expression must be numeric")
|
||||
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
|
||||
return -_eval_numeric(node.operand)
|
||||
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.UAdd):
|
||||
return _eval_numeric(node.operand)
|
||||
if isinstance(node, ast.BinOp):
|
||||
left = _eval_numeric(node.left)
|
||||
right = _eval_numeric(node.right)
|
||||
if isinstance(node.op, ast.Add):
|
||||
return left + right
|
||||
if isinstance(node.op, ast.Sub):
|
||||
return left - right
|
||||
if isinstance(node.op, ast.Mult):
|
||||
return left * right
|
||||
if isinstance(node.op, ast.Div):
|
||||
return left / right
|
||||
if isinstance(node.op, ast.FloorDiv):
|
||||
return left // right
|
||||
if isinstance(node.op, ast.Mod):
|
||||
return left % right
|
||||
if isinstance(node.op, ast.Pow):
|
||||
return left ** right
|
||||
raise ValueError("Unsupported operator in MESSAGE_RETENTION_DAYS")
|
||||
if isinstance(node, ast.Num): # py<3.8 compatibility
|
||||
return float(node.n)
|
||||
raise ValueError("Unsupported syntax in MESSAGE_RETENTION_DAYS (only numbers and + - * / // % **)")
|
||||
|
||||
|
||||
def eval_message_retention_expression(raw: str) -> float:
|
||||
s = raw.strip()
|
||||
if not s:
|
||||
raise ValueError("MESSAGE_RETENTION_DAYS must not be empty")
|
||||
tree = ast.parse(s, mode="eval")
|
||||
if not isinstance(tree, ast.Expression):
|
||||
raise ValueError("Invalid MESSAGE_RETENTION_DAYS expression")
|
||||
value = _eval_numeric(tree.body)
|
||||
if math.isnan(value) or math.isinf(value):
|
||||
raise ValueError("MESSAGE_RETENTION_DAYS must be finite")
|
||||
if value < 0 and value != -1.0:
|
||||
raise ValueError("MESSAGE_RETENTION_DAYS must be >= 0, or exactly -1")
|
||||
return value
|
||||
|
||||
|
||||
def load_message_retention_from_env() -> MessageRetentionState:
|
||||
raw = os.getenv(MESSAGE_RETENTION_DAYS)
|
||||
if raw is None or not str(raw).strip():
|
||||
raise ValueError(
|
||||
"MESSAGE_RETENTION_DAYS environment variable must be set "
|
||||
"(float days; expressions like 1/24/60*5 allowed; 0 = retain forever; -1 = do not store compliance MEK)"
|
||||
)
|
||||
return MessageRetentionState(days=eval_message_retention_expression(str(raw)))
|
||||
|
||||
|
||||
def get_message_retention() -> MessageRetentionState:
|
||||
global _state
|
||||
if _state is None:
|
||||
_state = load_message_retention_from_env()
|
||||
return _state
|
||||
|
||||
|
||||
def reset_message_retention_cache_for_tests() -> None:
|
||||
global _state
|
||||
_state = None
|
||||
Reference in New Issue
Block a user