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()