{{ existing.chat_identifier }}
+ Current link: {{ existing.person.name|default:existing.chat_name|default:existing.chat_identifier }} ← {{ existing.chat_identifier }}
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 %}
{{ existing.chat_identifier }}
+ Current link: {{ existing.person.name|default:existing.chat_name|default:existing.chat_identifier }} ← {{ existing.chat_identifier }}
{% 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 %}