Fully implement WhatsApp, Signal and XMPP multiplexing

This commit is contained in:
2026-02-16 19:19:32 +00:00
parent 3f82c27ab9
commit 658ab10647
9 changed files with 659 additions and 111 deletions

View File

@@ -6,9 +6,13 @@ from urllib.parse import parse_qs
from asgiref.sync import sync_to_async
from django.core import signing
from core.models import ChatSession, Message, PersonIdentifier, WorkspaceConversation
from core.models import Message, Person, PersonIdentifier, WorkspaceConversation
from core.realtime.typing_state import get_person_typing_state
from core.views.compose import COMPOSE_WS_TOKEN_SALT, _serialize_messages_with_artifacts
from core.views.compose import (
COMPOSE_WS_TOKEN_SALT,
ComposeHistorySync,
_serialize_messages_with_artifacts,
)
def _safe_int(value, default=0):
@@ -19,77 +23,108 @@ def _safe_int(value, default=0):
def _load_since(user_id, service, identifier, person_id, after_ts, limit):
person = None
person_identifier = None
if person_id:
resolved_person_id = _safe_int(person_id)
if resolved_person_id > 0:
person = Person.objects.filter(id=resolved_person_id, user_id=user_id).first()
if person is not None:
person_identifier = (
PersonIdentifier.objects.filter(
user_id=user_id,
person_id=person_id,
person_id=person.id,
service=service,
).first()
or PersonIdentifier.objects.filter(
user_id=user_id,
person_id=person_id,
person_id=person.id,
).first()
)
if person_identifier is None and identifier:
elif identifier:
person_identifier = PersonIdentifier.objects.filter(
user_id=user_id,
service=service,
identifier=identifier,
).first()
if person_identifier is None:
return {"messages": [], "last_ts": after_ts, "person_id": 0}
session = ChatSession.objects.filter(
user_id=user_id,
identifier=person_identifier,
).first()
if session is None:
session_ids = ComposeHistorySync._session_ids_for_scope(
user=user_id,
person=person,
service=service,
person_identifier=person_identifier,
explicit_identifier=identifier,
)
if not session_ids:
return {
"messages": [],
"last_ts": after_ts,
"person_id": int(person_identifier.person_id),
"last_ts": int(after_ts or 0),
"person_id": int(person.id) if person is not None else 0,
}
qs = Message.objects.filter(user_id=user_id, session=session).order_by("ts")
base_queryset = Message.objects.filter(
user_id=user_id,
session_id__in=session_ids,
)
qs = base_queryset.order_by("ts")
seed_previous = None
if after_ts > 0:
seed_previous = (
Message.objects.filter(
user_id=user_id,
session=session,
ts__lte=after_ts,
)
.order_by("-ts")
.first()
base_queryset.filter(ts__lte=after_ts).order_by("-ts").first()
)
qs = qs.filter(ts__gt=after_ts)
# Use a small rolling window to capture late/out-of-order timestamps.
# Frontend dedupes by message id, so repeated rows are ignored.
window_start = max(0, int(after_ts) - 5 * 60 * 1000)
qs = qs.filter(ts__gte=window_start)
rows = list(qs[: max(10, min(limit, 200))])
rows_desc = list(
qs.select_related(
"session",
"session__identifier",
"session__identifier__person",
)
.order_by("-ts")[: max(10, min(limit, 200))]
)
rows_desc.reverse()
rows = rows_desc
newest = (
Message.objects.filter(user_id=user_id, session=session)
Message.objects.filter(
user_id=user_id,
session_id__in=session_ids,
)
.order_by("-ts")
.values_list("ts", flat=True)
.first()
)
conversation = (
WorkspaceConversation.objects.filter(
user_id=user_id,
participants__id=person_identifier.person_id,
)
.order_by("-last_event_ts", "-created_at")
.first()
effective_person_id = (
int(person.id)
if person is not None
else (int(person_identifier.person_id) if person_identifier is not None else 0)
)
counterpart_identifiers = {
str(value or "").strip()
for value in PersonIdentifier.objects.filter(
user_id=user_id,
person_id=person_identifier.person_id,
).values_list("identifier", flat=True)
if str(value or "").strip()
}
conversation = None
counterpart_identifiers = set()
if effective_person_id > 0:
conversation = (
WorkspaceConversation.objects.filter(
user_id=user_id,
participants__id=effective_person_id,
)
.order_by("-last_event_ts", "-created_at")
.first()
)
counterpart_identifiers = {
str(value or "").strip()
for value in PersonIdentifier.objects.filter(
user_id=user_id,
person_id=effective_person_id,
).values_list("identifier", flat=True)
if str(value or "").strip()
}
return {
"messages": _serialize_messages_with_artifacts(
rows,
@@ -98,7 +133,7 @@ def _load_since(user_id, service, identifier, person_id, after_ts, limit):
seed_previous=seed_previous,
),
"last_ts": int(newest or after_ts or 0),
"person_id": int(person_identifier.person_id),
"person_id": int(effective_person_id),
}
@@ -133,6 +168,7 @@ async def compose_ws_application(scope, receive, send):
last_ts = 0
limit = 100
last_typing_key = ""
sent_message_ids = set()
while True:
event = None
@@ -159,7 +195,15 @@ async def compose_ws_application(scope, receive, send):
after_ts=last_ts,
limit=limit,
)
messages = payload.get("messages") or []
raw_messages = payload.get("messages") or []
messages = []
for msg in raw_messages:
message_id = str((msg or {}).get("id") or "").strip()
if message_id and message_id in sent_message_ids:
continue
if message_id:
sent_message_ids.add(message_id)
messages.append(msg)
latest = _safe_int(payload.get("last_ts"), last_ts)
if resolved_person_id <= 0:
resolved_person_id = _safe_int(payload.get("person_id"), 0)