Files
GIA/core/management/commands/recalculate_contact_availability.py

162 lines
6.5 KiB
Python

from __future__ import annotations
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 = (
"Replay behavioral event ledger rows from persisted message, receipt, "
"and reaction history."
)
def add_arguments(self, parser):
parser.add_argument("--days", type=int, default=90)
parser.add_argument("--limit", type=int, default=20000)
parser.add_argument("--service", default="")
parser.add_argument("--user-id", default="")
parser.add_argument("--dry-run", action="store_true", default=False)
parser.add_argument("--no-reset", action="store_true", default=False)
def _iter_messages(self, *, days: int, limit: int, service: str, user_id: str):
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 90))
limit = max(1, int(options.get("limit") or 20000))
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"))
messages = list(
self._iter_messages(
days=days,
limit=limit,
service=service_filter,
user_id=user_filter,
)
)
indexed = 0
for msg in messages:
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": "recalculate_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:
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": "recalculate_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
reactions = list(
(getattr(msg, "receipt_payload", {}) or {}).get("reactions") or []
)
for reaction in reactions:
item = dict(reaction or {})
if bool(item.get("removed")):
continue
reaction_ts = int(item.get("updated_at") or 0)
if reaction_ts <= 0:
continue
if not dry_run:
append_event_sync(
user=user,
session=session,
ts=reaction_ts,
event_type="presence_available",
direction="system",
actor_identifier=str(item.get("actor") or ""),
origin_transport=service,
origin_message_id=message_id,
origin_chat_id=str(getattr(msg, "source_chat_id", "") or ""),
payload={
"origin": "recalculate_contact_availability",
"message_id": str(msg.id),
"inferred_from": "reaction",
"emoji": str(item.get("emoji") or ""),
"source_service": str(item.get("source_service") or service),
},
)
indexed += 1
self.stdout.write(
self.style.SUCCESS(
"recalculate_contact_availability complete "
f"messages_scanned={len(messages)} indexed={indexed} "
f"dry_run={dry_run} no_reset={bool(options.get('no_reset'))} "
f"days={days} limit={limit}"
)
)