Fix all integrations
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user