ForcePilot/backend/package/yuxi/channels/adapters/whatsapp/credential_queue.py

121 lines
4.0 KiB
Python
Raw Normal View History

from __future__ import annotations
import json
import threading
import time
from pathlib import Path
from typing import Any
from yuxi.utils.logging_config import logger
class CredentialQueue:
def __init__(self, auth_dir: Path, debounce_ms: int = 500):
self._auth_dir = auth_dir
self._lock = threading.Lock()
self._write_count = 0
self._debounce_ms = debounce_ms
self._pending_creds: dict[str, Any] | None = None
self._last_write_time = 0.0
self._debounce_timer: threading.Timer | None = None
def write_auth(self, creds: dict[str, Any]) -> int:
now = time.monotonic()
with self._lock:
self._write_count += 1
count = self._write_count
if now - self._last_write_time < self._debounce_ms / 1000.0:
self._pending_creds = creds
self._schedule_debounced_write()
return count
self._last_write_time = now
self._pending_creds = None
return self._do_write(creds, count)
def _schedule_debounced_write(self) -> None:
if self._debounce_timer is not None:
self._debounce_timer.cancel()
self._debounce_timer = threading.Timer(
self._debounce_ms / 1000.0,
self._flush_debounced,
)
self._debounce_timer.daemon = True
self._debounce_timer.start()
def _flush_debounced(self) -> None:
with self._lock:
creds = self._pending_creds
self._pending_creds = None
self._debounce_timer = None
if creds is None:
return
count = self._write_count
self._do_write(creds, count)
def _do_write(self, creds: dict[str, Any], count: int) -> int:
path = self._auth_dir / "creds.json"
backup = self._auth_dir / "creds.json.bak"
tmp_path = self._auth_dir / "creds.json.tmp"
try:
data = json.dumps(creds, indent=2, ensure_ascii=False)
tmp_path.write_text(data, encoding="utf-8")
if path.exists():
try:
path.replace(backup)
except OSError:
pass
tmp_path.replace(path)
logger.debug(f"CredentialQueue: wrote creds (#{count}) to {path}")
except (OSError, TypeError) as e:
logger.error(f"CredentialQueue: write failed (#{count}): {e}")
raise
return count
def read_auth(self) -> dict[str, Any] | None:
path = self._auth_dir / "creds.json"
backup = self._auth_dir / "creds.json.bak"
try:
if not path.exists():
return None
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as e:
logger.error(f"CredentialQueue: read failed: {e}")
if backup.exists():
try:
data = json.loads(backup.read_text(encoding="utf-8"))
backup.replace(path)
logger.info("CredentialQueue: restored creds from backup")
return data
except (OSError, json.JSONDecodeError) as e2:
logger.error(f"CredentialQueue: backup recovery failed: {e2}")
return None
def clear_auth(self) -> bool:
with self._lock:
self._pending_creds = None
if self._debounce_timer is not None:
self._debounce_timer.cancel()
self._debounce_timer = None
path = self._auth_dir / "creds.json"
backup = self._auth_dir / "creds.json.bak"
try:
if path.exists():
if backup.exists():
backup.unlink()
path.rename(backup)
logger.info(f"CredentialQueue: cleared creds, backup at {backup}")
return True
except OSError as e:
logger.error(f"CredentialQueue: clear failed: {e}")
return False
@property
def write_count(self) -> int:
return self._write_count