新增 iMessage 通道适配器完整实现,包含: 1. 核心适配器与工具工厂导出 2. 运行时存储、反射防护、会话路由等基础组件 3. 消息信封、线程管理、回复上下文格式化 4. Tapback 表情反应处理、自定义异常体系 5. 审批按钮、联系人解析、速率限制功能 6. 文本净化、目标解析、缓存管理模块 7. 配置 schema、多账户支持、安装向导等配置模块 8. 审计日志、媒体AI处理等扩展功能
147 lines
4.8 KiB
Python
147 lines
4.8 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import time
|
|
from dataclasses import dataclass
|
|
|
|
TEXT_TTL_S = 4.0
|
|
TEXT_BACKED_BY_ID_TTL_S = 4.0
|
|
MESSAGE_ID_TTL_S = 60.0
|
|
SELF_CHAT_TTL_S = 10.0
|
|
MAX_SELF_CHAT_ENTRIES = 512
|
|
|
|
|
|
@dataclass
|
|
class _SentEntry:
|
|
message_id: str
|
|
text: str
|
|
chat_guid: str
|
|
sent_at: float
|
|
|
|
|
|
@dataclass
|
|
class _SelfChatEntry:
|
|
text_hash: str
|
|
chat_guid: str
|
|
recorded_at: float
|
|
|
|
|
|
class EchoCache:
|
|
def __init__(
|
|
self,
|
|
text_ttl_s: float = TEXT_TTL_S,
|
|
text_backed_by_id_ttl_s: float = TEXT_BACKED_BY_ID_TTL_S,
|
|
message_id_ttl_s: float = MESSAGE_ID_TTL_S,
|
|
self_chat_ttl_s: float = SELF_CHAT_TTL_S,
|
|
max_self_chat_entries: int = MAX_SELF_CHAT_ENTRIES,
|
|
):
|
|
self._text_ttl = text_ttl_s
|
|
self._text_backed_by_id_ttl = text_backed_by_id_ttl_s
|
|
self._msg_id_ttl = message_id_ttl_s
|
|
self._self_chat_ttl = self_chat_ttl_s
|
|
self._max_self_chat_entries = max_self_chat_entries
|
|
|
|
self._by_message_id: dict[str, _SentEntry] = {}
|
|
self._by_text: dict[str, list[_SentEntry]] = {}
|
|
self._by_text_backed_by_id: dict[str, _SentEntry] = {}
|
|
|
|
self._self_chat_cache: list[_SelfChatEntry] = []
|
|
self._last_self_chat_cleanup = time.monotonic()
|
|
|
|
def record_sent(self, message_id: str, text: str, chat_guid: str) -> None:
|
|
now = time.monotonic()
|
|
entry = _SentEntry(message_id=message_id, text=text, chat_guid=chat_guid, sent_at=now)
|
|
self._by_message_id[message_id] = entry
|
|
|
|
text_key = _text_key(text, chat_guid)
|
|
if text_key not in self._by_text:
|
|
self._by_text[text_key] = []
|
|
self._by_text[text_key].append(entry)
|
|
|
|
if message_id:
|
|
self._by_text_backed_by_id[message_id] = entry
|
|
|
|
self._cleanup()
|
|
|
|
def is_echo(self, message_id: str | None = None, text: str | None = None, chat_guid: str = "") -> bool:
|
|
self._cleanup()
|
|
|
|
if message_id and message_id in self._by_message_id:
|
|
return True
|
|
|
|
if message_id and message_id in self._by_text_backed_by_id:
|
|
return True
|
|
|
|
if text:
|
|
text_key = _text_key(text, chat_guid)
|
|
entries = self._by_text.get(text_key, [])
|
|
if entries:
|
|
return True
|
|
|
|
return False
|
|
|
|
def is_self_chat_echo(
|
|
self,
|
|
sender_handle: str,
|
|
self_handle: str,
|
|
chat_guid: str,
|
|
) -> bool:
|
|
if not self_handle:
|
|
return False
|
|
sender_clean = sender_handle.strip().replace(" ", "").lstrip("+")
|
|
self_clean = self_handle.strip().replace(" ", "").lstrip("+")
|
|
return sender_clean == self_clean
|
|
|
|
def record_self_chat_text(self, text: str, chat_guid: str) -> None:
|
|
now = time.monotonic()
|
|
text_hash = hashlib.sha256(f"{chat_guid}:{text.strip()[:200]}".encode()).hexdigest()
|
|
entry = _SelfChatEntry(text_hash=text_hash, chat_guid=chat_guid, recorded_at=now)
|
|
self._self_chat_cache.append(entry)
|
|
self._cleanup_self_chat()
|
|
if len(self._self_chat_cache) > self._max_self_chat_entries:
|
|
self._self_chat_cache = self._self_chat_cache[-self._max_self_chat_entries :]
|
|
|
|
def is_in_self_chat_cache(self, text: str, chat_guid: str) -> bool:
|
|
self._cleanup_self_chat()
|
|
text_hash = hashlib.sha256(f"{chat_guid}:{text.strip()[:200]}".encode()).hexdigest()
|
|
for entry in self._self_chat_cache:
|
|
if entry.text_hash == text_hash and entry.chat_guid == chat_guid:
|
|
return True
|
|
return False
|
|
|
|
def _cleanup_self_chat(self) -> None:
|
|
now = time.monotonic()
|
|
self._self_chat_cache = [
|
|
entry for entry in self._self_chat_cache if now - entry.recorded_at <= self._self_chat_ttl
|
|
]
|
|
|
|
def _cleanup(self) -> None:
|
|
now = time.monotonic()
|
|
|
|
expired_msg_ids = [mid for mid, entry in self._by_message_id.items() if now - entry.sent_at > self._msg_id_ttl]
|
|
for mid in expired_msg_ids:
|
|
del self._by_message_id[mid]
|
|
|
|
expired_text_backed = [
|
|
mid
|
|
for mid, entry in self._by_text_backed_by_id.items()
|
|
if now - entry.sent_at > self._text_backed_by_id_ttl
|
|
]
|
|
for mid in expired_text_backed:
|
|
del self._by_text_backed_by_id[mid]
|
|
|
|
for text_key in list(self._by_text):
|
|
self._by_text[text_key] = [
|
|
entry for entry in self._by_text[text_key] if now - entry.sent_at <= self._text_ttl
|
|
]
|
|
if not self._by_text[text_key]:
|
|
del self._by_text[text_key]
|
|
|
|
if now - self._last_self_chat_cleanup > self._self_chat_ttl:
|
|
self._cleanup_self_chat()
|
|
self._last_self_chat_cleanup = now
|
|
|
|
|
|
def _text_key(text: str, chat_guid: str) -> str:
|
|
return f"{chat_guid}:{text.strip()[:200]}"
|