Fix all integrations

This commit is contained in:
2026-03-08 22:08:55 +00:00
parent bca4d6898f
commit acedc01e83
58 changed files with 4120 additions and 960 deletions

View File

@@ -7,7 +7,7 @@ import time
from datetime import datetime
from datetime import timezone as dt_timezone
from difflib import SequenceMatcher
from urllib.parse import quote_plus, urlencode, urlparse
from urllib.parse import urlencode, urlparse
from asgiref.sync import async_to_sync
from django.conf import settings
@@ -136,6 +136,13 @@ def _identifier_variants(service: str, identifier: str) -> list[str]:
return variants
def _group_channel_identifier(service: str, group_link: PlatformChatLink, bare_id: str) -> str:
service_key = _default_service(service)
if service_key == "whatsapp":
return str(group_link.chat_jid or f"{bare_id}@g.us").strip()
return bare_id
def _safe_limit(raw) -> int:
try:
value = int(raw or 40)
@@ -424,7 +431,7 @@ def _extract_attachment_image_urls(blob) -> list[str]:
direct_urls.append(normalized)
urls.extend(direct_urls)
blob_key = str(blob.get("blob_key") or "").strip()
# Prefer source-hosted URLs (for example share.zm.is) and use blob fallback only
# Prefer source-hosted URLs and use blob fallback only
# when no usable direct URL exists.
if blob_key and image_hint and not direct_urls:
urls.append(f"/compose/media/blob/?key={quote_plus(blob_key)}")
@@ -1783,6 +1790,11 @@ def _context_base(user, service, identifier, person):
service=service,
identifier__in=identifier_variants or [identifier],
).first()
if person_identifier is None and identifier and person is None:
person_identifier = PersonIdentifier.objects.filter(
user=user,
identifier__in=identifier_variants or [identifier],
).first()
if person_identifier:
service = person_identifier.service
@@ -1811,7 +1823,7 @@ def _context_base(user, service, identifier, person):
return {
"person_identifier": None,
"service": service,
"identifier": f"{bare_id}@g.us",
"identifier": _group_channel_identifier(service, group_link, bare_id),
"person": None,
"is_group": True,
"group_name": group_link.chat_name or bare_id,
@@ -2426,6 +2438,63 @@ def _signal_identifier_shape(value: str) -> str:
return "other"
def _preferred_signal_identifier(identifiers: list[str], *, is_group: bool) -> str:
cleaned = []
for value in identifiers:
candidate = str(value or "").strip()
if candidate and candidate not in cleaned:
cleaned.append(candidate)
if not cleaned:
return ""
if is_group:
for candidate in cleaned:
if candidate.startswith("group."):
return candidate
return cleaned[0]
for candidate in cleaned:
if _signal_identifier_shape(candidate) == "phone":
return candidate
for candidate in cleaned:
if _signal_identifier_shape(candidate) == "uuid":
return candidate
return cleaned[0]
def _signal_runtime_alias_map() -> dict[str, set[str]]:
state = transport.get_runtime_state("signal") or {}
alias_map: dict[str, set[str]] = {}
for bucket_name in ("contacts", "groups"):
rows = state.get(bucket_name) or []
if not isinstance(rows, list):
continue
for item in rows:
if not isinstance(item, dict):
continue
identifiers = []
candidates = item.get("identifiers")
if isinstance(candidates, list):
identifiers.extend(candidates)
identifiers.extend(
[
item.get("identifier"),
item.get("number"),
item.get("uuid"),
item.get("id"),
item.get("internal_id"),
]
)
unique = []
for value in identifiers:
candidate = str(value or "").strip()
if candidate and candidate not in unique:
unique.append(candidate)
if len(unique) < 2:
continue
for identifier in unique:
alias_map[identifier] = {value for value in unique if value != identifier}
return alias_map
def _manual_contact_rows(user):
rows = []
seen = set()
@@ -2493,6 +2562,8 @@ def _manual_contact_rows(user):
)
for row in identifiers:
if _default_service(row.service) == "signal":
continue
add_row(
service=row.service,
identifier=row.identifier,
@@ -2500,6 +2571,27 @@ def _manual_contact_rows(user):
source="linked",
)
group_links = (
PlatformChatLink.objects.filter(user=user, is_group=True)
.order_by("service", "chat_name", "chat_identifier")
)
for link in group_links:
if _default_service(link.service) == "signal":
continue
group_identifier = _group_channel_identifier(
str(link.service or "").strip(),
link,
str(link.chat_identifier or "").strip(),
)
if not group_identifier:
continue
add_row(
service=link.service,
identifier=group_identifier,
source=f"{_default_service(link.service)}_group",
detected_name=str(link.chat_name or "").strip(),
)
signal_links = {
str(row.identifier): row
for row in (
@@ -2508,6 +2600,163 @@ def _manual_contact_rows(user):
.order_by("id")
)
}
signal_state = transport.get_runtime_state("signal") or {}
signal_accounts = [
str(value or "").strip()
for value in (signal_state.get("accounts") or [])
if str(value or "").strip()
]
signal_account_set = set(signal_accounts)
signal_entities = {}
signal_alias_index = {}
def _signal_entity_key(identifiers_list: list[str], *, is_group: bool) -> str:
preferred = _preferred_signal_identifier(identifiers_list, is_group=is_group)
if is_group:
return f"group:{preferred}"
for candidate in identifiers_list:
if _signal_identifier_shape(candidate) == "uuid":
return f"contact:uuid:{candidate.lower()}"
for candidate in identifiers_list:
if _signal_identifier_shape(candidate) == "phone":
return f"contact:phone:{candidate}"
return f"contact:other:{preferred.lower()}"
def _resolve_signal_entity_key(candidate: str) -> str:
cleaned = str(candidate or "").strip()
if not cleaned:
return ""
for variant in _identifier_variants("signal", cleaned):
entity_key = signal_alias_index.get(variant)
if entity_key:
return entity_key
return ""
def _register_signal_entity(
*,
identifiers_list,
is_group: bool,
detected_name="",
person=None,
source="signal_runtime",
):
unique_identifiers = []
for value in identifiers_list or []:
cleaned = str(value or "").strip()
if (
not cleaned
or cleaned in unique_identifiers
or cleaned in signal_account_set
):
continue
unique_identifiers.append(cleaned)
if not unique_identifiers:
return None
entity_key = ""
for identifier in unique_identifiers:
entity_key = _resolve_signal_entity_key(identifier)
if entity_key:
break
if not entity_key:
entity_key = _signal_entity_key(unique_identifiers, is_group=is_group)
entity = signal_entities.get(entity_key)
if entity is None:
entity = {
"is_group": bool(is_group),
"identifiers": [],
"detected_name": _clean_detected_name(detected_name or ""),
"person": person,
"sources": set(),
}
signal_entities[entity_key] = entity
for identifier in unique_identifiers:
if identifier not in entity["identifiers"]:
entity["identifiers"].append(identifier)
for variant in _identifier_variants("signal", identifier):
signal_alias_index[variant] = entity_key
cleaned_name = _clean_detected_name(detected_name or "")
if cleaned_name and not entity["detected_name"]:
entity["detected_name"] = cleaned_name
if person is not None and entity.get("person") is None:
entity["person"] = person
entity["sources"].add(str(source or "").strip() or "signal_runtime")
return entity
for row in signal_links.values():
_register_signal_entity(
identifiers_list=[row.identifier],
is_group=False,
detected_name=(str(row.person.name or "").strip() if row.person else ""),
person=row.person,
source="linked",
)
signal_group_links = (
PlatformChatLink.objects.filter(user=user, service="signal", is_group=True)
.order_by("chat_name", "chat_identifier")
)
for link in signal_group_links:
group_identifier = _group_channel_identifier(
"signal",
link,
str(link.chat_identifier or "").strip(),
)
if not group_identifier:
continue
_register_signal_entity(
identifiers_list=[group_identifier],
is_group=True,
detected_name=str(link.chat_name or "").strip(),
source="signal_group",
)
signal_contacts = signal_state.get("contacts") or []
if isinstance(signal_contacts, list):
for item in signal_contacts:
if not isinstance(item, dict):
continue
candidate_identifiers = item.get("identifiers")
if not isinstance(candidate_identifiers, list):
candidate_identifiers = [
item.get("identifier"),
item.get("number"),
item.get("uuid"),
]
linked = None
for candidate in candidate_identifiers:
cleaned = str(candidate or "").strip()
if not cleaned:
continue
linked = signal_links.get(cleaned)
if linked is not None:
break
_register_signal_entity(
identifiers_list=candidate_identifiers,
is_group=False,
detected_name=str(item.get("name") or "").strip(),
person=(linked.person if linked else None),
source="signal_runtime",
)
signal_groups = signal_state.get("groups") or []
if isinstance(signal_groups, list):
for item in signal_groups:
if not isinstance(item, dict):
continue
candidate_identifiers = item.get("identifiers")
if not isinstance(candidate_identifiers, list):
candidate_identifiers = [
item.get("identifier"),
item.get("id"),
item.get("internal_id"),
]
_register_signal_entity(
identifiers_list=candidate_identifiers,
is_group=True,
detected_name=str(item.get("name") or "").strip(),
source="signal_group_raw",
)
signal_chats = Chat.objects.all().order_by("-id")[:500]
for chat in signal_chats:
uuid_candidate = str(chat.source_uuid or "").strip()
@@ -2517,20 +2766,45 @@ def _manual_contact_rows(user):
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) 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 ""
),
)
linked = fallback_linked
if linked is None:
for candidate in (uuid_candidate, number_candidate):
linked = signal_links.get(candidate)
if linked is not None:
break
_register_signal_entity(
identifiers_list=[uuid_candidate, number_candidate],
is_group=False,
detected_name=_clean_detected_name(chat.source_name or chat.account or ""),
person=(linked.person if linked else None),
source="signal_chat",
)
for entity in signal_entities.values():
entity_identifiers = list(entity.get("identifiers") or [])
identifier_value = _preferred_signal_identifier(
entity_identifiers,
is_group=bool(entity.get("is_group")),
)
if not identifier_value:
continue
add_row(
service="signal",
identifier=identifier_value,
person=entity.get("person"),
source=",".join(sorted(entity.get("sources") or {"signal_runtime"})),
account=entity.get("detected_name") or "",
detected_name=entity.get("detected_name") or "",
)
if rows:
rows[-1]["identifier_aliases"] = [
candidate
for candidate in entity_identifiers
if str(candidate or "").strip() and candidate != identifier_value
]
rows[-1]["identifier_search"] = " ".join(
[rows[-1]["identifier"]] + rows[-1]["identifier_aliases"]
).strip()
whatsapp_links = {
str(row.identifier): row
@@ -3225,8 +3499,11 @@ class ComposeContactMatch(LoginRequiredMixin, View):
value = str(identifier or "").strip()
if not value:
return set()
source_shape = _signal_identifier_shape(value)
companions = set()
runtime_aliases = _signal_runtime_alias_map()
for variant in _identifier_variants("signal", value):
companions.update(runtime_aliases.get(variant) or set())
source_shape = _signal_identifier_shape(value)
signal_rows = Chat.objects.filter(source_uuid=value) | Chat.objects.filter(
source_number=value
)