Continue implementing WhatsApp

This commit is contained in:
2026-02-16 00:39:16 +00:00
parent b1a53034d5
commit 3d32834ccf
12 changed files with 796 additions and 120 deletions

View File

@@ -4,6 +4,7 @@ import hashlib
import json
import re
import time
from difflib import SequenceMatcher
from datetime import datetime, timezone as dt_timezone
from urllib.parse import quote_plus, urlencode, urlparse
@@ -1357,7 +1358,29 @@ def _manual_contact_rows(user):
.order_by("person__name", "service", "identifier")
)
def add_row(*, service, identifier, person=None, source="linked", account=""):
def _normalize_contact_key(value: str) -> str:
raw = str(value or "").strip().lower()
if "@" in raw:
raw = raw.split("@", 1)[0]
return raw
def _clean_detected_name(value: str) -> str:
text = str(value or "").strip()
if not text:
return ""
if text in {"~", "-", "_"}:
return ""
return text
def add_row(
*,
service,
identifier,
person=None,
source="linked",
account="",
detected_name="",
):
service_key = _default_service(service)
identifier_value = str(identifier or "").strip()
if not identifier_value:
@@ -1367,12 +1390,14 @@ def _manual_contact_rows(user):
return
seen.add(key)
urls = _compose_urls(service_key, identifier_value, person.id if person else None)
person_name = person.name if person else ""
if not person_name:
person_name = str(account or identifier_value)
linked_person_name = person.name if person else ""
detected = _clean_detected_name(detected_name or account or "")
person_name = linked_person_name or detected or identifier_value
rows.append(
{
"person_name": person_name,
"linked_person_name": linked_person_name,
"detected_name": detected,
"service": service_key,
"service_icon_class": _service_icon_class(service_key),
"identifier": identifier_value,
@@ -1405,19 +1430,24 @@ def _manual_contact_rows(user):
}
signal_chats = Chat.objects.all().order_by("-id")[:500]
for chat in signal_chats:
for candidate in (
str(chat.source_uuid or "").strip(),
str(chat.source_number or "").strip(),
):
uuid_candidate = str(chat.source_uuid or "").strip()
number_candidate = str(chat.source_number or "").strip()
fallback_linked = None
if uuid_candidate:
fallback_linked = signal_links.get(uuid_candidate)
if fallback_linked is None and number_candidate:
fallback_linked = signal_links.get(number_candidate)
for candidate in (uuid_candidate, number_candidate):
if not candidate:
continue
linked = signal_links.get(candidate)
linked = signal_links.get(candidate) or fallback_linked
add_row(
service="signal",
identifier=candidate,
person=(linked.person if linked else None),
source="signal_chat",
account=str(chat.account or ""),
detected_name=_clean_detected_name(chat.source_name or chat.account or ""),
)
whatsapp_links = {
@@ -1429,6 +1459,12 @@ def _manual_contact_rows(user):
)
}
wa_contacts = transport.get_runtime_state("whatsapp").get("contacts") or []
wa_accounts = transport.get_runtime_state("whatsapp").get("accounts") or []
wa_account_keys = {
_normalize_contact_key(value)
for value in wa_accounts
if str(value or "").strip()
}
if isinstance(wa_contacts, list):
for item in wa_contacts:
if not isinstance(item, dict):
@@ -1436,19 +1472,69 @@ def _manual_contact_rows(user):
candidate = str(item.get("identifier") or item.get("jid") or "").strip()
if not candidate:
continue
if _normalize_contact_key(candidate) in wa_account_keys:
continue
detected_name = _clean_detected_name(item.get("name") or item.get("chat") or "")
if detected_name.lower() == "linked account":
continue
linked = whatsapp_links.get(candidate)
if linked is None and "@" in candidate:
linked = whatsapp_links.get(candidate.split("@", 1)[0])
add_row(
service="whatsapp",
identifier=candidate,
person=(linked.person if linked else None),
source="whatsapp_runtime",
account=str(item.get("name") or item.get("chat") or ""),
account=detected_name,
detected_name=detected_name,
)
rows.sort(key=lambda row: (row["person_name"].lower(), row["service"], row["identifier"]))
return rows
def _name_for_match(value: str) -> str:
lowered = re.sub(r"[^a-z0-9]+", " ", str(value or "").strip().lower())
return re.sub(r"\s+", " ", lowered).strip()
def _suggest_people_for_candidate(candidate: dict, people: list[Person]) -> list[dict]:
if not people:
return []
base_name = str(candidate.get("detected_name") or "").strip()
if not base_name:
return []
base_norm = _name_for_match(base_name)
if not base_norm:
return []
scored = []
base_tokens = {token for token in base_norm.split(" ") if token}
for person in people:
person_norm = _name_for_match(person.name)
if not person_norm:
continue
ratio = SequenceMatcher(None, base_norm, person_norm).ratio()
person_tokens = {token for token in person_norm.split(" ") if token}
overlap = 0.0
if base_tokens and person_tokens:
overlap = len(base_tokens & person_tokens) / max(
len(base_tokens), len(person_tokens)
)
score = max(ratio, overlap)
if score < 0.62:
continue
scored.append(
{
"person": person,
"score": score,
}
)
scored.sort(key=lambda item: item["score"], reverse=True)
return scored[:3]
def _load_messages(user, person_identifier, limit):
if person_identifier is None:
return {"session": None, "messages": []}
@@ -1646,12 +1732,18 @@ class ComposeContactMatch(LoginRequiredMixin, View):
]
def _context(self, request, notice="", level="info"):
people = (
people_qs = (
Person.objects.filter(user=request.user)
.prefetch_related("personidentifier_set")
.order_by("name")
)
people = list(people_qs)
candidates = _manual_contact_rows(request.user)
for row in candidates:
row["suggestions"] = []
if row.get("linked_person"):
continue
row["suggestions"] = _suggest_people_for_candidate(row, people)
return {
"people": people,
"candidates": candidates,