diff --git a/app/urls.py b/app/urls.py index 8073e5c..9e551d1 100644 --- a/app/urls.py +++ b/app/urls.py @@ -89,11 +89,21 @@ urlpatterns = [ signal.SignalContactsList.as_view(), name="signal_contacts", ), + path( + "services/whatsapp//contacts//", + whatsapp.WhatsAppContactsList.as_view(), + name="whatsapp_contacts", + ), path( "services/signal//chats//", signal.SignalChatsList.as_view(), name="signal_chats", ), + path( + "services/whatsapp//chats//", + whatsapp.WhatsAppChatsList.as_view(), + name="whatsapp_chats", + ), path( "services/signal//messages///", signal.SignalMessagesList.as_view(), @@ -179,6 +189,11 @@ urlpatterns = [ compose.ComposeContactsDropdown.as_view(), name="compose_contacts_dropdown", ), + path( + "compose/contacts/match/", + compose.ComposeContactMatch.as_view(), + name="compose_contact_match", + ), # AIs path( "ai/workspace/", diff --git a/core/clients/whatsapp.py b/core/clients/whatsapp.py index aaee62b..9e35cb4 100644 --- a/core/clients/whatsapp.py +++ b/core/clients/whatsapp.py @@ -262,8 +262,13 @@ class WhatsAppClient(ClientBase): ) if connected_value: self._connected = True + account = await self._resolve_account_identifier() + account_list = [account] if account else list(self._accounts or []) + if not account_list: + account_list = [self.client_name] self._publish_state( connected=True, + accounts=account_list, pair_status="connected", last_event="connected_probe", connected_at=now_ts, @@ -442,6 +447,12 @@ class WhatsAppClient(ClientBase): async def on_connected(client, event: connected_ev): self._connected = True account = await self._resolve_account_identifier() + if account: + self._remember_contact( + account, + jid=account, + name="Linked Account", + ) self._publish_state( connected=True, warning="", @@ -502,6 +513,12 @@ class WhatsAppClient(ClientBase): status_text = str(status_raw or "").strip().lower() if status_text in {"2", "success"}: account = await self._resolve_account_identifier() + if account: + self._remember_contact( + account, + jid=account, + name="Linked Account", + ) self._connected = True self._publish_state( connected=True, @@ -640,6 +657,35 @@ class WhatsAppClient(ClientBase): out.add(f"+{digits}") return out + def _remember_contact(self, identifier, *, jid="", name="", chat=""): + cleaned = str(identifier or "").strip() + if not cleaned: + return + state = transport.get_runtime_state(self.service) + existing = state.get("contacts") or [] + rows = [item for item in existing if isinstance(item, dict)] + merged = [] + seen = set() + now_ts = int(time.time()) + row = { + "identifier": cleaned, + "jid": str(jid or "").strip(), + "name": str(name or "").strip(), + "chat": str(chat or "").strip(), + "seen_at": now_ts, + } + merged.append(row) + seen.add(cleaned) + for item in rows: + candidate = str(item.get("identifier") or item.get("jid") or "").strip() + if not candidate or candidate in seen: + continue + seen.add(candidate) + merged.append(item) + if len(merged) >= 500: + break + self._publish_state(contacts=merged, last_contact_seen_at=now_ts) + def _jid_to_identifier(self, value): raw = str(value or "").strip() if not raw: @@ -761,6 +807,11 @@ class WhatsAppClient(ClientBase): or self._pluck(event, "timestamp") ) ts = self._normalize_timestamp(raw_ts) + self._remember_contact( + sender or chat, + jid=sender, + chat=chat, + ) identifier_values = self._normalize_identifier_candidates(sender, chat) if not identifier_values: @@ -853,6 +904,11 @@ class WhatsAppClient(ClientBase): or int(time.time() * 1000) ) receipt_type = str(self._pluck(event, "Type") or "").strip() + self._remember_contact( + sender or chat, + jid=sender, + chat=chat, + ) for candidate in self._normalize_identifier_candidates(sender, chat): await self.ur.message_read( @@ -885,6 +941,11 @@ class WhatsAppClient(ClientBase): state = self._pluck(event, "State") or self._pluck(event, "state") state_text = str(state or "").strip().lower() is_typing = state_text in {"1", "composing", "chat_presence_composing"} + self._remember_contact( + sender or chat, + jid=sender, + chat=chat, + ) for candidate in self._normalize_identifier_candidates(sender, chat): if is_typing: @@ -908,6 +969,7 @@ class WhatsAppClient(ClientBase): is_unavailable = bool( self._pluck(event, "Unavailable") or self._pluck(event, "unavailable") ) + self._remember_contact(sender, jid=sender) for candidate in self._normalize_identifier_candidates(sender): if is_unavailable: diff --git a/core/templates/pages/compose-contact-match.html b/core/templates/pages/compose-contact-match.html new file mode 100644 index 0000000..6c0ded7 --- /dev/null +++ b/core/templates/pages/compose-contact-match.html @@ -0,0 +1,129 @@ +{% extends "index.html" %} + +{% block content %} +
+
+
+
+
+

Contact Match

+

+ Manually link Signal, WhatsApp, Instagram, and XMPP identifiers to people. +

+
+
+ +
+ + {% if notice_message %} +
+ {{ notice_message }} +
+ {% endif %} + +
+
+
+

Create Or Link Identifier

+
+ {% csrf_token %} +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+
+

Discovered Contacts

+ {% if candidates %} +
+ + + + + + + + + + + + {% for row in candidates %} + + + + + + + + {% endfor %} + +
ContactServiceIdentifierStatus
{{ row.person_name }} + + {{ row.service|title }} + {{ row.identifier }} + {% if row.linked_person %} + linked + {% else %} + unlinked + {% endif %} + + + + Message + +
+
+ {% else %} +

No contacts discovered yet.

+ {% endif %} +
+
+
+
+
+ +{% endblock %} diff --git a/core/templates/partials/compose-workspace-contacts-widget.html b/core/templates/partials/compose-workspace-contacts-widget.html index 4ce7a7d..9812854 100644 --- a/core/templates/partials/compose-workspace-contacts-widget.html +++ b/core/templates/partials/compose-workspace-contacts-widget.html @@ -64,7 +64,7 @@ - + + {% if not row.linked_person %} + + + Match + + {% endif %} {% endfor %} {% else %} diff --git a/core/templates/partials/nav-contacts-dropdown.html b/core/templates/partials/nav-contacts-dropdown.html index 680d0b6..42eafd4 100644 --- a/core/templates/partials/nav-contacts-dropdown.html +++ b/core/templates/partials/nav-contacts-dropdown.html @@ -1,15 +1,23 @@ {% if items %} {% for item in items %} - + {{ item.person_name }} · {{ item.service|title }} + {% if not item.linked_person %} + · unlinked + {% endif %} {% endfor %} {% else %} No contacts found. {% endif %} + + + + Match Contacts + {% if is_preview %} {% if show_contact_actions %} {% if type == 'page' %} - - {% endif %} + + {% endif %} +