From 0816687c710774c17fdbb4f114f7838e678fcf8b Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Thu, 19 Feb 2026 17:13:34 +0000 Subject: [PATCH] Allow linking chats --- core/clients/signal.py | 45 ++- core/clients/whatsapp.py | 50 ++- .../0026_platformchatlink_is_group.py | 24 ++ core/models.py | 5 +- core/templates/pages/whatsapp-chat-link.html | 2 +- .../partials/ai-workspace-widget.html | 59 ++- core/templates/partials/compose-panel.html | 2 + .../templates/partials/signal-chats-list.html | 89 +++-- .../partials/whatsapp-chats-list.html | 2 +- core/views/compose.py | 23 ++ core/views/signal.py | 27 +- core/views/whatsapp.py | 127 ++++--- core/views/workspace.py | 19 + docker-compose.yml | 353 ------------------ scripts/quadlet/manage.sh | 3 + 15 files changed, 369 insertions(+), 461 deletions(-) create mode 100644 core/migrations/0026_platformchatlink_is_group.py delete mode 100644 docker-compose.yml diff --git a/core/clients/signal.py b/core/clients/signal.py index 2555389..4fa1fee 100644 --- a/core/clients/signal.py +++ b/core/clients/signal.py @@ -11,7 +11,7 @@ from signalbot import Command, Context, SignalBot from core.clients import ClientBase, signalapi from core.messaging import ai, history, media_bridge, natural, replies, utils -from core.models import Chat, Manipulation, PersonIdentifier, QueuedMessage +from core.models import Chat, Manipulation, PersonIdentifier, PlatformChatLink, QueuedMessage from core.util import logs log = logs.get_logger("signalF") @@ -263,6 +263,49 @@ class NewSignalBot(SignalBot): await self._resolve_commands() await self._produce_consume_messages() + async def _upsert_groups(self) -> None: + groups = getattr(self, "groups", None) or [] + if not groups: + self.log.debug("[Signal] _upsert_groups: no groups to persist") + return + + identifiers = await sync_to_async(list)( + PersonIdentifier.objects.filter(service="signal").select_related("user") + ) + seen_user_ids: set = set() + users = [] + for pi in identifiers: + if pi.user_id not in seen_user_ids: + seen_user_ids.add(pi.user_id) + users.append(pi.user) + if not users: + self.log.debug("[Signal] _upsert_groups: no PersonIdentifiers found — skipping") + return + + for user in users: + for group in groups: + group_id = group.get("id") or "" + name = group.get("name") or group_id + if not group_id: + continue + await sync_to_async(PlatformChatLink.objects.update_or_create)( + user=user, + service="signal", + chat_identifier=group_id, + defaults={ + "person": None, + "person_identifier": None, + "is_group": True, + "chat_name": name, + }, + ) + + self.log.info("[Signal] upserted %d groups for %d users", len(groups), len(users)) + + async def _detect_groups(self): + await super()._detect_groups() + await self._upsert_groups() + def start(self): """Start bot without blocking the caller's event loop.""" task = self._event_loop.create_task( diff --git a/core/clients/whatsapp.py b/core/clients/whatsapp.py index 57e3278..e84f665 100644 --- a/core/clients/whatsapp.py +++ b/core/clients/whatsapp.py @@ -15,7 +15,7 @@ from django.core.cache import cache from core.clients import ClientBase, transport from core.messaging import history, media_bridge -from core.models import Message, PersonIdentifier +from core.models import Message, PersonIdentifier, PlatformChatLink class WhatsAppClient(ClientBase): @@ -1514,6 +1514,7 @@ class WhatsAppClient(ClientBase): # Read contact-like rows directly from the session sqlite DB instead. contacts, source, lid_map = await self._sync_contacts_from_sqlite() groups, groups_source = await self._sync_groups_from_client() + await self._upsert_groups(groups) now_ts = int(time.time()) if contacts: @@ -1762,6 +1763,53 @@ class WhatsAppClient(ClientBase): return await asyncio.to_thread(_extract) + async def _upsert_groups(self, groups: list) -> None: + if not groups: + self.log.debug("[WA] _upsert_groups: no groups to persist") + return + + identifiers = await sync_to_async(list)( + PersonIdentifier.objects.filter(service="whatsapp").select_related("user") + ) + seen_user_ids: set = set() + users = [] + for pi in identifiers: + if pi.user_id not in seen_user_ids: + seen_user_ids.add(pi.user_id) + users.append(pi.user) + if not users: + self.log.debug("[WA] _upsert_groups: no PersonIdentifiers found — skipping") + return + + upserted = 0 + for user in users: + for group in groups: + identifier = group.get("identifier") or "" + name = group.get("name") or identifier + jid = group.get("jid") or "" + if "@newsletter" in jid or "@newsletter" in identifier: + continue + await sync_to_async(PlatformChatLink.objects.update_or_create)( + user=user, + service="whatsapp", + chat_identifier=identifier, + defaults={ + "person": None, + "person_identifier": None, + "is_group": True, + "chat_name": name, + "chat_jid": jid, + }, + ) + upserted += 1 + + self.log.info( + "[WA] upserted %d group rows (%d groups × %d users)", + upserted, + len(groups), + len(users), + ) + async def _sync_groups_from_client(self): if self._client is None: return [], "client_missing" diff --git a/core/migrations/0026_platformchatlink_is_group.py b/core/migrations/0026_platformchatlink_is_group.py new file mode 100644 index 0000000..7aa294c --- /dev/null +++ b/core/migrations/0026_platformchatlink_is_group.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.11 on 2026-02-19 14:47 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0025_platformchatlink'), + ] + + operations = [ + migrations.AddField( + model_name='platformchatlink', + name='is_group', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='platformchatlink', + name='person', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.person'), + ), + ] diff --git a/core/models.py b/core/models.py index 4358640..143c90b 100644 --- a/core/models.py +++ b/core/models.py @@ -173,7 +173,7 @@ class PersonIdentifier(models.Model): class PlatformChatLink(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) - person = models.ForeignKey(Person, on_delete=models.CASCADE) + person = models.ForeignKey(Person, on_delete=models.CASCADE, null=True, blank=True) person_identifier = models.ForeignKey( PersonIdentifier, on_delete=models.SET_NULL, @@ -184,6 +184,7 @@ class PlatformChatLink(models.Model): chat_identifier = models.CharField(max_length=255) chat_jid = models.CharField(max_length=255, blank=True, null=True) chat_name = models.CharField(max_length=255, blank=True, null=True) + is_group = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -224,7 +225,7 @@ class PlatformChatLink(models.Model): return super().save(*args, **kwargs) def __str__(self): - return f"{self.person.name} ({self.service}: {self.chat_identifier})" + return f"{self.person.name if self.person_id else self.chat_name or self.chat_identifier} ({self.service}: {self.chat_identifier})" class ChatSession(models.Model): diff --git a/core/templates/pages/whatsapp-chat-link.html b/core/templates/pages/whatsapp-chat-link.html index 4a4f915..1e6a81f 100644 --- a/core/templates/pages/whatsapp-chat-link.html +++ b/core/templates/pages/whatsapp-chat-link.html @@ -76,7 +76,7 @@ {% if existing %}
- Current link: {{ existing.person.name }}{{ existing.chat_identifier }} + Current link: {{ existing.person.name|default:existing.chat_name|default:existing.chat_identifier }}{{ existing.chat_identifier }}
{% endif %} diff --git a/core/templates/partials/ai-workspace-widget.html b/core/templates/partials/ai-workspace-widget.html index 6746c80..fbfb912 100644 --- a/core/templates/partials/ai-workspace-widget.html +++ b/core/templates/partials/ai-workspace-widget.html @@ -21,28 +21,47 @@ {% if contact_rows %}
{% for row in contact_rows %} - + + {% else %} + + {% endif %} {% endfor %}
{% else %} diff --git a/core/templates/partials/compose-panel.html b/core/templates/partials/compose-panel.html index de79fa2..71603f4 100644 --- a/core/templates/partials/compose-panel.html +++ b/core/templates/partials/compose-panel.html @@ -37,6 +37,8 @@

{% if person %} {{ person.name }} + {% elif group_name %} + {{ group_name }} {% else %} {{ identifier }} {% endif %} diff --git a/core/templates/partials/signal-chats-list.html b/core/templates/partials/signal-chats-list.html index 0146e4a..d8e4728 100644 --- a/core/templates/partials/signal-chats-list.html +++ b/core/templates/partials/signal-chats-list.html @@ -18,35 +18,44 @@ {% for item in object_list %} - {{ item.chat.source_number }} + {% if item.chat %}{{ item.chat.source_number }}{% endif %} - - - - - + {% if item.chat %} + + + + + + {% endif %} + + {% if item.chat %}{{ item.chat.account }}{% endif %} + + {% if item.is_group %} + + {% endif %} + {% if item.chat %}{{ item.chat.source_name }}{% else %}{{ item.name }}{% endif %} - {{ item.chat.account }} - {{ item.chat.source_name }} {{ item.person_name|default:"-" }}

- + + {% endif %} {% if type == 'page' %} {% if item.can_compose %} {% endif %} - - + + + {% endif %} {% endif %} - + + {% endif %}