| """Persist platform messages belonging to FCC-managed conversations.""" |
|
|
| from datetime import UTC, datetime |
| from typing import Any |
|
|
|
|
| class ManagedMessageLog: |
| """Track managed inbound and outbound messages in insertion order.""" |
|
|
| def __init__(self, *, cap: int | None = None) -> None: |
| self._items: dict[str, list[dict[str, Any]]] = {} |
| self._ids: dict[str, set[str]] = {} |
| self._cap = cap |
|
|
| @property |
| def cap(self) -> int | None: |
| return self._cap |
|
|
| @classmethod |
| def from_json(cls, raw_log: Any, *, cap: int | None = None) -> ManagedMessageLog: |
| """Load current and legacy message-log entries.""" |
| log = cls(cap=cap) |
| if not isinstance(raw_log, dict): |
| return log |
| for chat_key, items in raw_log.items(): |
| if not isinstance(chat_key, str) or not isinstance(items, list): |
| continue |
| for item in items: |
| if not isinstance(item, dict): |
| continue |
| message_id = item.get("message_id") |
| direction = str(item.get("direction") or "") |
| kind = str(item.get("kind") or "") |
| if message_id is None or direction not in {"in", "out"} or not kind: |
| continue |
| log._append( |
| chat_key, |
| str(message_id), |
| ts=str(item.get("ts") or ""), |
| direction=direction, |
| kind=kind, |
| ) |
| return log |
|
|
| def to_json(self) -> dict[str, list[dict[str, Any]]]: |
| return {chat_key: list(items) for chat_key, items in self._items.items()} |
|
|
| def record( |
| self, |
| *, |
| platform: str, |
| chat_id: str, |
| message_id: str, |
| direction: str, |
| kind: str, |
| ) -> bool: |
| """Record one managed platform message.""" |
| if direction not in {"in", "out"}: |
| raise ValueError("Managed message direction must be 'in' or 'out'") |
| if not kind: |
| raise ValueError("Managed message kind cannot be empty") |
| return self._append( |
| make_chat_key(platform, chat_id), |
| str(message_id), |
| ts=datetime.now(UTC).isoformat(), |
| direction=direction, |
| kind=str(kind), |
| ) |
|
|
| def ids_for_chat(self, platform: str, chat_id: str) -> list[str]: |
| chat_key = make_chat_key(platform, chat_id) |
| return [str(item["message_id"]) for item in self._items.get(chat_key, [])] |
|
|
| def remove_ids(self, platform: str, chat_id: str, message_ids: set[str]) -> bool: |
| chat_key = make_chat_key(platform, chat_id) |
| if not message_ids or chat_key not in self._items: |
| return False |
|
|
| before_count = len(self._items[chat_key]) |
| retained = [ |
| item |
| for item in self._items[chat_key] |
| if str(item["message_id"]) not in message_ids |
| ] |
| if retained: |
| self._items[chat_key] = retained |
| self._ids[chat_key] = {str(item["message_id"]) for item in retained} |
| else: |
| self._items.pop(chat_key) |
| self._ids.pop(chat_key, None) |
| return len(retained) != before_count |
|
|
| def clear_chat(self, platform: str, chat_id: str) -> bool: |
| chat_key = make_chat_key(platform, chat_id) |
| removed = self._items.pop(chat_key, None) is not None |
| self._ids.pop(chat_key, None) |
| return removed |
|
|
| def _append( |
| self, |
| chat_key: str, |
| message_id: str, |
| *, |
| ts: str, |
| direction: str, |
| kind: str, |
| ) -> bool: |
| seen = self._ids.setdefault(chat_key, set()) |
| if message_id in seen: |
| return False |
| self._items.setdefault(chat_key, []).append( |
| { |
| "message_id": message_id, |
| "ts": ts, |
| "direction": direction, |
| "kind": kind, |
| } |
| ) |
| seen.add(message_id) |
| self._trim(chat_key) |
| return True |
|
|
| def _trim(self, chat_key: str) -> None: |
| if self._cap is None or self._cap <= 0: |
| return |
| items = self._items.get(chat_key, []) |
| if len(items) <= self._cap: |
| return |
| retained = items[-self._cap :] |
| self._items[chat_key] = retained |
| self._ids[chat_key] = {str(item["message_id"]) for item in retained} |
|
|
|
|
| def make_chat_key(platform: str, chat_id: str) -> str: |
| return f"{platform}:{chat_id}" |
|
|