实现了完整的Nextcloud Talk聊天适配器,包含以下核心功能: 1. 基础客户端通信与认证 2. 会话路由与聊天类型解析 3. 消息分块与格式转换 4. 用户配对与权限校验 5. 轮询式消息监听 6. 配置验证与诊断工具 7. 安全策略与去重缓存 8. 指标统计与状态快照 新增配套的配对用户缓存文件与模块导出入口。
128 lines
4.2 KiB
Python
128 lines
4.2 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import time
|
|
from collections import OrderedDict
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_DEDUP_TTL = 86400
|
|
_MAX_ENTRIES = 10000
|
|
_PERSIST_PATH = os.getenv(
|
|
"NEXTCLOUDTALK_DEDUP_FILE",
|
|
os.path.join(os.path.dirname(__file__), ".dedup_cache.json"),
|
|
)
|
|
|
|
|
|
class NextcloudTalkDedupGuard:
|
|
def __init__(self, ttl: int = _DEDUP_TTL, max_entries: int = _MAX_ENTRIES):
|
|
self._ttl = ttl
|
|
self._max_entries = max_entries
|
|
self._pending: dict[str, float] = {}
|
|
self._committed: OrderedDict[str, float] = OrderedDict()
|
|
self._invalid: set[str] = set()
|
|
self._loaded = False
|
|
|
|
def _load_persisted(self) -> None:
|
|
if self._loaded:
|
|
return
|
|
self._loaded = True
|
|
try:
|
|
if os.path.exists(_PERSIST_PATH):
|
|
with open(_PERSIST_PATH, encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
now = time.time()
|
|
count = 0
|
|
for key, ts in data.items():
|
|
if now - ts < self._ttl:
|
|
self._committed[key] = ts
|
|
count += 1
|
|
if count:
|
|
logger.debug(f"[NextcloudTalk] Dedup: loaded {count} entries from persist")
|
|
except Exception as e:
|
|
logger.warning(f"[NextcloudTalk] Dedup: failed to load persist: {e}")
|
|
|
|
def _save_persisted(self) -> None:
|
|
try:
|
|
data = dict(self._committed)
|
|
with open(_PERSIST_PATH, "w", encoding="utf-8") as f:
|
|
json.dump(data, f)
|
|
except Exception as e:
|
|
logger.warning(f"[NextcloudTalk] Dedup: failed to persist: {e}")
|
|
|
|
def _make_key(self, token: str, message_id: str) -> str:
|
|
return f"{token}:{message_id}"
|
|
|
|
def _gc(self) -> None:
|
|
now = time.time()
|
|
stale = [k for k, ts in self._committed.items() if now - ts > self._ttl]
|
|
for k in stale:
|
|
del self._committed[k]
|
|
while len(self._committed) > self._max_entries:
|
|
self._committed.popitem(last=False)
|
|
|
|
def claim(self, token: str, message_id: str) -> bool:
|
|
if not token or not message_id:
|
|
return True
|
|
self._load_persisted()
|
|
key = self._make_key(token, message_id)
|
|
if key in self._committed:
|
|
logger.debug(f"[NextcloudTalk] Dedup: duplicate claim rejected for {key}")
|
|
return False
|
|
if key in self._pending:
|
|
logger.debug(f"[NextcloudTalk] Dedup: already pending {key}")
|
|
return False
|
|
self._pending[key] = time.time()
|
|
return True
|
|
|
|
def commit(self, token: str, message_id: str) -> None:
|
|
if not token or not message_id:
|
|
return
|
|
key = self._make_key(token, message_id)
|
|
self._pending.pop(key, None)
|
|
self._committed[key] = time.time()
|
|
self._gc()
|
|
|
|
def release(self, token: str, message_id: str) -> None:
|
|
if not token or not message_id:
|
|
return
|
|
key = self._make_key(token, message_id)
|
|
self._pending.pop(key, None)
|
|
|
|
def mark_invalid(self, token: str, message_id: str) -> None:
|
|
if not token or not message_id:
|
|
return
|
|
key = self._make_key(token, message_id)
|
|
self._pending.pop(key, None)
|
|
self._invalid.add(key)
|
|
|
|
def is_invalid(self, token: str, message_id: str) -> bool:
|
|
if not token or not message_id:
|
|
return False
|
|
key = self._make_key(token, message_id)
|
|
return key in self._invalid
|
|
|
|
def is_duplicate(self, token: str, message_id: str) -> bool:
|
|
if not token or not message_id:
|
|
return False
|
|
self._load_persisted()
|
|
key = self._make_key(token, message_id)
|
|
if key in self._committed:
|
|
return True
|
|
self._gc()
|
|
return key in self._committed
|
|
|
|
def stats(self) -> dict[str, int]:
|
|
self._gc()
|
|
return {
|
|
"committed": len(self._committed),
|
|
"pending": len(self._pending),
|
|
"invalid": len(self._invalid),
|
|
}
|
|
|
|
def flush(self) -> None:
|
|
self._gc()
|
|
self._save_persisted()
|