该提交新增了完整的BlueBubbles渠道插件,支持通过BlueBubbles Server集成iMessage功能,包含以下核心能力: 1. 支持私聊和群聊会话管理,自动区分会话类型 2. 完整的消息收发支持,包括文本、图片、语音、文件、视频消息 3. 支持消息反应、已读回执、消息编辑与撤回 4. 内置去重、防抖处理机制 5. 支持Webhook和WebSocket两种事件接收方式 6. 完善的权限与安全校验机制 7. 历史消息同步与抓包功能 8. TTS语音合成与发送支持 9. 群组管理能力,包括重命名、修改头像、增减成员等
149 lines
4.5 KiB
Python
149 lines
4.5 KiB
Python
import logging
|
|
import platform
|
|
import re
|
|
import sqlite3
|
|
import time
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_contact_cache: dict[str, tuple[str, float]] = {}
|
|
_MAX_CACHE_ENTRIES = 2048
|
|
_POSITIVE_TTL = 3600
|
|
_NEGATIVE_TTL = 300
|
|
|
|
|
|
def normalize_phone_lookup_key(value: str) -> str | None:
|
|
digits = re.sub(r"\D", "", value)
|
|
if len(digits) == 11 and digits.startswith("1"):
|
|
digits = digits[1:]
|
|
return digits if len(digits) >= 7 else None
|
|
|
|
|
|
def is_darwin() -> bool:
|
|
return platform.system().lower() == "darwin"
|
|
|
|
|
|
async def query_address_book(participants: list[dict]) -> list[dict]:
|
|
if not is_darwin() or not participants:
|
|
return participants
|
|
|
|
sources_dir = Path.home() / "Library/Application Support/AddressBook/Sources"
|
|
if not sources_dir.exists():
|
|
return participants
|
|
|
|
db_files = list(sources_dir.glob("*/AddressBook-v22.abcddb"))
|
|
if not db_files:
|
|
return participants
|
|
|
|
numbers_to_lookup = []
|
|
for p in participants:
|
|
if not p.get("name") and p.get("phone"):
|
|
key = normalize_phone_lookup_key(p["phone"])
|
|
if key:
|
|
numbers_to_lookup.append(key)
|
|
|
|
if not numbers_to_lookup:
|
|
return participants
|
|
|
|
results: dict[str, str] = {}
|
|
for db_file in db_files:
|
|
try:
|
|
conn = sqlite3.connect(str(db_file))
|
|
cursor = conn.cursor()
|
|
placeholders = ",".join(["?" for _ in numbers_to_lookup])
|
|
cursor.execute(
|
|
f"""
|
|
SELECT ZABCDPHONENUMBER.ZFULLNUMBER,
|
|
ZABCDRECORD.ZFIRSTNAME,
|
|
ZABCDRECORD.ZLASTNAME,
|
|
ZABCDRECORD.ZORGANIZATION
|
|
FROM ZABCDPHONENUMBER
|
|
JOIN ZABCDRECORD ON ZABCDPHONENUMBER.ZOWNER = ZABCDRECORD.Z_PK
|
|
WHERE ZABCDPHONENUMBER.ZFULLNUMBER IN ({placeholders})
|
|
""",
|
|
numbers_to_lookup,
|
|
)
|
|
for row in cursor.fetchall():
|
|
full_number, first, last, org = row
|
|
digits = normalize_phone_lookup_key(full_number)
|
|
if digits and digits not in results:
|
|
name = org or f"{first or ''} {last or ''}".strip()
|
|
if name:
|
|
results[digits] = name
|
|
conn.close()
|
|
except Exception:
|
|
continue
|
|
|
|
for p in participants:
|
|
if not p.get("name") and p.get("phone"):
|
|
key = normalize_phone_lookup_key(p["phone"])
|
|
if key and key in results:
|
|
p["name"] = results[key]
|
|
|
|
return participants
|
|
|
|
|
|
def lookup_contact_name(phone: str) -> str | None:
|
|
now = time.time()
|
|
lookup_key = normalize_phone_lookup_key(phone)
|
|
if not lookup_key:
|
|
return None
|
|
|
|
cached = _contact_cache.get(lookup_key)
|
|
if cached:
|
|
name, ts = cached
|
|
if now - ts < _POSITIVE_TTL and name:
|
|
return name
|
|
if now - ts < _NEGATIVE_TTL and not name:
|
|
return None
|
|
|
|
if not is_darwin():
|
|
_contact_cache[lookup_key] = ("", now)
|
|
return None
|
|
|
|
sources_dir = Path.home() / "Library/Application Support/AddressBook/Sources"
|
|
if not sources_dir.exists():
|
|
_contact_cache[lookup_key] = ("", now)
|
|
return None
|
|
|
|
for db_file in sources_dir.glob("*/AddressBook-v22.abcddb"):
|
|
try:
|
|
conn = sqlite3.connect(str(db_file))
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"""
|
|
SELECT ZABCDRECORD.ZFIRSTNAME, ZABCDRECORD.ZLASTNAME,
|
|
ZABCDRECORD.ZORGANIZATION
|
|
FROM ZABCDPHONENUMBER
|
|
JOIN ZABCDRECORD ON ZABCDPHONENUMBER.ZOWNER = ZABCDRECORD.Z_PK
|
|
WHERE ZABCDPHONENUMBER.ZFULLNUMBER = ?
|
|
""",
|
|
(phone,),
|
|
)
|
|
row = cursor.fetchone()
|
|
conn.close()
|
|
if row:
|
|
first, last, org = row
|
|
name = org or f"{first or ''} {last or ''}".strip()
|
|
if name:
|
|
_cache_and_trim(lookup_key, name)
|
|
return name
|
|
except Exception:
|
|
continue
|
|
|
|
_cache_and_trim(lookup_key, "")
|
|
return None
|
|
|
|
|
|
def _cache_and_trim(key: str, value: str):
|
|
now = time.time()
|
|
if len(_contact_cache) >= _MAX_CACHE_ENTRIES:
|
|
oldest = min(_contact_cache.items(), key=lambda x: x[1][1])
|
|
del _contact_cache[oldest[0]]
|
|
_contact_cache[key] = (value, now)
|
|
|
|
|
|
def clear_contact_cache():
|
|
_contact_cache.clear()
|