Plugin sms: lettura SMS/iMessage da Messaggi, invio con scelta del servizio, fonte per il monitor

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Fable 5.1 committed 2026-09-23 17:45:14 +02:00
1 parent 6aac16b679
commit 6fad5e1f4a
7 files changed
+241 -9

No files matched your search

+26 -1
View File
@@ -101,6 +101,30 @@ class Monitor:
self.state["wa_ts"] = max(r[1] for r in rows)
return [{"fonte": "WhatsApp", "chi": chat if sender == chat else f"{sender} (gruppo {chat})", "testo": text[:300], "ref": f"wa:{rid}"} for rid, ts, chat, sender, text in rows]
def _collect_sms(self, baseline: bool) -> list[dict]:
try:
import importlib
from avatar.plugins import registry
registry.load()
sms = importlib.import_module("avatar_plugins.sms")
except Exception:
return []
if not sms.DB.exists():
return []
last = self.state.get("sms_rowid")
try:
rows = sms._query("AND m.is_from_me = 0 AND m.ROWID > ?", (last if last is not None else 10**12,), 40) if not (last is None or baseline) else []
if last is None or baseline:
top = sms._query("", (), 1)
self.state["sms_rowid"] = top[0]["id"] if top else 0
return []
except Exception as err:
print(f"[monitor] messaggi: {err}")
return []
if rows:
self.state["sms_rowid"] = max(r["id"] for r in rows)
return [{"fonte": "Messaggi", "chi": r["chi"], "testo": r["testo"][:300], "ref": f"sms:{r['id']}"} for r in rows]
def _collect_telegram(self, baseline: bool) -> list[dict]:
try:
from avatar import telegram_client as tg
@@ -176,7 +200,8 @@ class Monitor:
def scan(self, baseline: bool = False) -> list[dict]:
with _lock:
first = self.state.get("mail_rowid") is None and self.state.get("wa_ts") is None and not self.state.get("tg")
items = self._collect_mail(baseline or first) + self._collect_whatsapp(baseline or first) + self._collect_telegram(baseline or first)
items = (self._collect_mail(baseline or first) + self._collect_whatsapp(baseline or first)
+ self._collect_sms(baseline or first) + self._collect_telegram(baseline or first))
seen = set(self.state.get("seen", []))
items = [it for it in items if it["ref"] not in seen]
self.state.setdefault("seen", []).extend(it["ref"] for it in items)
+60
View File
@@ -0,0 +1,60 @@
"""Rubrica: numeri di telefono ed email → nomi, dai Contatti di macOS (cache in memoria)."""
from __future__ import annotations
import re
import threading
import time
_map: dict[str, str] = {}
_loaded_at = 0.0
_lock = threading.Lock()
def normalize(number: str) -> str:
digits = re.sub(r"\D", "", number or "")
if digits.startswith("00"):
digits = digits[2:]
if len(digits) == 10 and digits.startswith("3"):
digits = "39" + digits
return digits
def _load() -> None:
global _loaded_at
import Contacts
store = Contacts.CNContactStore.alloc().init()
keys = [Contacts.CNContactGivenNameKey, Contacts.CNContactFamilyNameKey, Contacts.CNContactOrganizationNameKey,
Contacts.CNContactPhoneNumbersKey, Contacts.CNContactEmailAddressesKey]
req = Contacts.CNContactFetchRequest.alloc().initWithKeysToFetch_(keys)
new: dict[str, str] = {}
def visit(c, stop):
name = f"{c.givenName()} {c.familyName()}".strip() or c.organizationName()
if not name:
return
for ph in c.phoneNumbers():
d = normalize(ph.value().stringValue())
if len(d) >= 8:
new[d] = name
for e in c.emailAddresses():
new[str(e.value()).lower()] = name
store.enumerateContactsWithFetchRequest_error_usingBlock_(req, None, visit)
_map.clear()
_map.update(new)
_loaded_at = time.time()
def name_for(handle: str) -> str:
"""Nome del contatto per un numero o un'email; altrimenti il valore originale."""
with _lock:
if time.time() - _loaded_at > 3600:
try:
_load()
except Exception:
pass
h = (handle or "").strip()
if "@" in h:
return _map.get(h.lower(), h)
d = normalize(h)
return _map.get(d) or _map.get(d[-10:]) or h