from __future__ import annotations from typing import Iterable from django.core.management.base import BaseCommand from core.events.ledger import append_event_sync from core.models import Message from core.presence.inference import now_ms class Command(BaseCommand): help = ( "Backfill behavioral event ledger rows from historical message and " "read-receipt activity." ) def add_arguments(self, parser): parser.add_argument("--days", type=int, default=30) parser.add_argument("--limit", type=int, default=5000) parser.add_argument("--service", default="") parser.add_argument("--user-id", default="") parser.add_argument("--dry-run", action="store_true", default=False) def _iter_messages( self, *, days: int, limit: int, service: str, user_id: str ) -> Iterable[Message]: cutoff_ts = now_ms() - (max(1, int(days)) * 24 * 60 * 60 * 1000) qs = Message.objects.filter(ts__gte=cutoff_ts).select_related( "user", "session", "session__identifier", "session__identifier__person" ) if service: qs = qs.filter(source_service=str(service).strip().lower()) if user_id: qs = qs.filter(user_id=str(user_id).strip()) return qs.order_by("ts")[: max(1, int(limit))] def handle(self, *args, **options): days = max(1, int(options.get("days") or 30)) limit = max(1, int(options.get("limit") or 5000)) service_filter = str(options.get("service") or "").strip().lower() user_filter = str(options.get("user_id") or "").strip() dry_run = bool(options.get("dry_run")) indexed = 0 scanned = 0 for msg in self._iter_messages( days=days, limit=limit, service=service_filter, user_id=user_filter ): scanned += 1 session = getattr(msg, "session", None) identifier = getattr(session, "identifier", None) person = getattr(identifier, "person", None) user = getattr(msg, "user", None) if not session or not identifier or not person or not user: continue service = ( str(getattr(msg, "source_service", "") or identifier.service or "") .strip() .lower() ) if not service: continue author = str(getattr(msg, "custom_author", "") or "").strip().upper() outgoing = author in {"USER", "BOT"} message_id = str( getattr(msg, "source_message_id", "") or f"django-message-{msg.id}" ).strip() if not dry_run: append_event_sync( user=user, session=session, ts=int(getattr(msg, "ts", 0) or 0), event_type="message_created", direction="out" if outgoing else "in", actor_identifier=str( getattr(msg, "sender_uuid", "") or identifier.identifier or "" ), origin_transport=service, origin_message_id=message_id, origin_chat_id=str(getattr(msg, "source_chat_id", "") or ""), payload={ "origin": "backfill_contact_availability", "message_id": str(msg.id), "text": str(getattr(msg, "text", "") or ""), "outgoing": outgoing, }, ) indexed += 1 read_ts = int(getattr(msg, "read_ts", 0) or 0) if read_ts <= 0: continue if not dry_run: append_event_sync( user=user, session=session, ts=read_ts, event_type="read_receipt", direction="system", actor_identifier=str( getattr(msg, "read_by_identifier", "") or identifier.identifier ), origin_transport=service, origin_message_id=message_id, origin_chat_id=str(getattr(msg, "source_chat_id", "") or ""), payload={ "origin": "backfill_contact_availability", "message_id": str(msg.id), "message_ts": int(getattr(msg, "ts", 0) or 0), "read_by": str( getattr(msg, "read_by_identifier", "") or "" ).strip(), }, ) indexed += 1 self.stdout.write( self.style.SUCCESS( "backfill_contact_availability complete " f"scanned={scanned} indexed={indexed} dry_run={dry_run} " f"days={days} limit={limit}" ) )