from __future__ import annotations from django.db.models import Count, Max, Q from core.models import ConversationEvent, Person, User def _kind_from_event_type(event_type: str) -> str: normalized = str(event_type or "").strip().lower() return { "message_created": "message_sent", "delivery_receipt": "message_delivered", "read_receipt": "message_read", "typing_started": "composing_started", "typing_stopped": "composing_stopped", "composing_abandoned": "composing_abandoned", "presence_available": "presence_available", "presence_unavailable": "presence_unavailable", }.get(normalized, normalized) def get_shadow_behavioral_availability_stats(*, user: User) -> list[dict]: person_map = { str(row["id"]): str(row["name"] or "") for row in Person.objects.filter(user=user).values("id", "name") } rows = ( ConversationEvent.objects.filter( user=user, session__identifier__person__isnull=False, ) .values("session__identifier__person_id", "origin_transport") .annotate( total_events=Count("id"), presence_events=Count( "id", filter=Q(event_type__in=["presence_available", "presence_unavailable"]), ), read_events=Count("id", filter=Q(event_type="read_receipt")), typing_events=Count( "id", filter=Q( event_type__in=["typing_started", "typing_stopped"] ), ), message_events=Count("id", filter=Q(event_type="message_created")), abandoned_events=Count("id", filter=Q(event_type="composing_abandoned")), last_event_ts=Max("ts"), ) .order_by("-total_events", "session__identifier__person_id", "origin_transport") ) output = [] for row in rows: person_id = str(row.get("session__identifier__person_id") or "").strip() output.append( { "person_id": person_id, "person_name": person_map.get(person_id, person_id or "-"), "service": str(row.get("origin_transport") or "").strip().lower(), "total_events": int(row.get("total_events") or 0), "presence_events": int(row.get("presence_events") or 0), "read_events": int(row.get("read_events") or 0), "typing_events": int(row.get("typing_events") or 0), "message_events": int(row.get("message_events") or 0), "abandoned_events": int(row.get("abandoned_events") or 0), "last_event_ts": int(row.get("last_event_ts") or 0), } ) return output def get_shadow_behavioral_latest_states( *, user: User, person_ids: list[str], transport: str = "" ) -> list[dict]: queryset = ConversationEvent.objects.filter( user=user, session__identifier__person_id__in=[str(value) for value in person_ids], event_type__in=[ "message_created", "delivery_receipt", "read_receipt", "typing_started", "typing_stopped", "composing_abandoned", "presence_available", "presence_unavailable", ], ).select_related("session__identifier") if transport: queryset = queryset.filter(origin_transport=str(transport).strip().lower()) rows = [] seen = set() for row in queryset.order_by( "session__identifier__person_id", "-ts", "-created_at" )[:500]: person_id = str(getattr(row.session.identifier, "person_id", "") or "").strip() if not person_id or person_id in seen: continue seen.add(person_id) rows.append( { "person_id": person_id, "transport": str(row.origin_transport or "").strip().lower(), "kind": _kind_from_event_type(row.event_type), "ts": int(row.ts or 0), } ) return rows def get_shadow_behavioral_events_for_range( *, user: User, person_id: str, start_ts: int, end_ts: int, transport: str = "", ) -> list[dict]: queryset = ConversationEvent.objects.filter( user=user, session__identifier__person_id=str(person_id or "").strip(), ts__gte=int(start_ts), ts__lte=int(end_ts), event_type__in=[ "message_created", "delivery_receipt", "read_receipt", "typing_started", "typing_stopped", "composing_abandoned", "presence_available", "presence_unavailable", ], ).order_by("ts", "created_at") if transport: queryset = queryset.filter(origin_transport=str(transport).strip().lower()) return [ { "person_id": str(person_id or "").strip(), "session_id": str(row.session_id or ""), "transport": str(row.origin_transport or "").strip().lower(), "kind": _kind_from_event_type(row.event_type), "direction": str(row.direction or "").strip().lower(), "ts": int(row.ts or 0), "payload": dict(row.payload or {}), } for row in queryset[:1000] ]