Implement Manticore fully and re-theme
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import re
|
||||
import time
|
||||
|
||||
from asgiref.sync import sync_to_async
|
||||
from django.conf import settings
|
||||
@@ -12,7 +13,8 @@ from core.clients.whatsapp import WhatsAppClient
|
||||
from core.clients.xmpp import XMPPClient
|
||||
from core.commands.base import CommandContext
|
||||
from core.commands.engine import process_inbound_message
|
||||
from core.events import event_ledger_status
|
||||
from core.events import append_event, event_ledger_enabled, event_ledger_status
|
||||
from core.events.behavior import ComposingTracker
|
||||
from core.messaging import history
|
||||
from core.models import PersonIdentifier
|
||||
from core.observability.tracing import ensure_trace_id
|
||||
@@ -32,7 +34,13 @@ class UnifiedRouter(object):
|
||||
self.typing_auto_stop_seconds = int(
|
||||
getattr(settings, "XMPP_TYPING_AUTO_STOP_SECONDS", 3)
|
||||
)
|
||||
self.composing_abandoned_window_seconds = int(
|
||||
getattr(settings, "COMPOSING_ABANDONED_WINDOW_SECONDS", 300)
|
||||
)
|
||||
self._typing_stop_tasks = {}
|
||||
self._composing_tracker = ComposingTracker(
|
||||
window_ms=self.composing_abandoned_window_seconds * 1000
|
||||
)
|
||||
|
||||
self.log = logs.get_logger("router")
|
||||
self.log.info("Initialised Unified Router Interface.")
|
||||
@@ -85,6 +93,55 @@ class UnifiedRouter(object):
|
||||
|
||||
self._typing_stop_tasks[key] = self.loop.create_task(_timer())
|
||||
|
||||
def _behavior_direction(self, protocol: str) -> str:
|
||||
return "out" if str(protocol or "").strip().lower() == "xmpp" else "in"
|
||||
|
||||
def _event_ts_from_kwargs(self, kwargs: dict) -> int | None:
|
||||
payload = dict(kwargs.get("payload") or {})
|
||||
for candidate in (
|
||||
kwargs.get("ts"),
|
||||
payload.get("ts"),
|
||||
payload.get("timestamp"),
|
||||
payload.get("messageTimestamp"),
|
||||
payload.get("message_ts"),
|
||||
):
|
||||
try:
|
||||
parsed = int(candidate)
|
||||
except Exception:
|
||||
continue
|
||||
if parsed > 0:
|
||||
return parsed
|
||||
return int(time.time() * 1000)
|
||||
|
||||
async def _append_identifier_event(
|
||||
self,
|
||||
*,
|
||||
identifier_row,
|
||||
event_type: str,
|
||||
protocol: str,
|
||||
direction: str,
|
||||
ts: int | None = None,
|
||||
payload: dict | None = None,
|
||||
raw_payload: dict | None = None,
|
||||
actor_identifier: str = "",
|
||||
):
|
||||
if not event_ledger_enabled():
|
||||
return None
|
||||
session = await history.get_chat_session(identifier_row.user, identifier_row)
|
||||
await append_event(
|
||||
user=identifier_row.user,
|
||||
session=session,
|
||||
ts=ts,
|
||||
event_type=event_type,
|
||||
direction=direction,
|
||||
actor_identifier=str(actor_identifier or identifier_row.identifier or ""),
|
||||
origin_transport=str(protocol or "").strip().lower(),
|
||||
origin_chat_id=str(identifier_row.identifier or ""),
|
||||
payload=dict(payload or {}),
|
||||
raw_payload=dict(raw_payload or {}),
|
||||
)
|
||||
return session
|
||||
|
||||
def _start(self):
|
||||
self.log.info("Starting unified router clients")
|
||||
self.xmpp.start()
|
||||
@@ -117,6 +174,9 @@ class UnifiedRouter(object):
|
||||
message_text = str(kwargs.get("text") or "").strip()
|
||||
if local_message is None:
|
||||
return
|
||||
self._composing_tracker.observe_message(
|
||||
str(getattr(local_message, "session_id", "") or "")
|
||||
)
|
||||
identifiers = await self._resolve_identifier_objects(protocol, identifier)
|
||||
if identifiers:
|
||||
outgoing = str(
|
||||
@@ -239,6 +299,10 @@ class UnifiedRouter(object):
|
||||
timestamps = kwargs.get("message_timestamps") or []
|
||||
read_ts = kwargs.get("read_ts")
|
||||
payload = kwargs.get("payload") or {}
|
||||
payload_type = str((payload or {}).get("type") or "").strip().lower()
|
||||
receipt_event_type = (
|
||||
"delivery_receipt" if payload_type == "delivered" else "read_receipt"
|
||||
)
|
||||
trace_id = (
|
||||
ensure_trace_id(payload=payload)
|
||||
if bool(getattr(settings, "TRACE_PROPAGATION_ENABLED", True))
|
||||
@@ -257,6 +321,7 @@ class UnifiedRouter(object):
|
||||
read_by_identifier=read_by or row.identifier,
|
||||
payload=payload,
|
||||
trace_id=trace_id,
|
||||
receipt_event_type=receipt_event_type,
|
||||
)
|
||||
record_native_signal(
|
||||
AvailabilitySignal(
|
||||
@@ -264,12 +329,13 @@ class UnifiedRouter(object):
|
||||
person=row.person,
|
||||
person_identifier=row,
|
||||
service=str(protocol or "").strip().lower(),
|
||||
source_kind="read_receipt",
|
||||
source_kind=receipt_event_type,
|
||||
availability_state="available",
|
||||
confidence=0.95,
|
||||
ts=int(read_ts or 0),
|
||||
payload={
|
||||
"origin": "router.message_read",
|
||||
"receipt_event_type": receipt_event_type,
|
||||
"message_timestamps": [
|
||||
int(v) for v in list(timestamps or []) if str(v).isdigit()
|
||||
],
|
||||
@@ -309,11 +375,41 @@ class UnifiedRouter(object):
|
||||
payload=payload,
|
||||
)
|
||||
)
|
||||
state_event = None
|
||||
if state == "available":
|
||||
state_event = "presence_available"
|
||||
elif state == "unavailable":
|
||||
state_event = "presence_unavailable"
|
||||
if state_event:
|
||||
try:
|
||||
await self._append_identifier_event(
|
||||
identifier_row=row,
|
||||
event_type=state_event,
|
||||
protocol=protocol,
|
||||
direction="system",
|
||||
ts=(ts or None),
|
||||
payload={
|
||||
"state": state,
|
||||
"confidence": confidence,
|
||||
**payload,
|
||||
},
|
||||
raw_payload=payload,
|
||||
actor_identifier=str(row.identifier or ""),
|
||||
)
|
||||
except Exception as exc:
|
||||
self.log.warning(
|
||||
"Failed to append presence event for %s: %s",
|
||||
row.identifier,
|
||||
exc,
|
||||
)
|
||||
await self._refresh_workspace_metrics_for_identifiers(identifiers)
|
||||
|
||||
async def started_typing(self, protocol, *args, **kwargs):
|
||||
self.log.info(f"Started typing ({protocol}) {args} {kwargs}")
|
||||
identifier = kwargs.get("identifier")
|
||||
payload = dict(kwargs.get("payload") or {})
|
||||
event_ts = self._event_ts_from_kwargs(kwargs)
|
||||
direction = self._behavior_direction(protocol)
|
||||
identifiers = await self._resolve_identifier_objects(protocol, identifier)
|
||||
for src in identifiers:
|
||||
record_native_signal(
|
||||
@@ -329,6 +425,30 @@ class UnifiedRouter(object):
|
||||
payload={"origin": "router.started_typing"},
|
||||
)
|
||||
)
|
||||
try:
|
||||
session = await history.get_chat_session(src.user, src)
|
||||
state = self._composing_tracker.observe_started(
|
||||
str(session.id),
|
||||
int(event_ts or 0),
|
||||
)
|
||||
await append_event(
|
||||
user=src.user,
|
||||
session=session,
|
||||
ts=event_ts,
|
||||
event_type="typing_started",
|
||||
direction=direction,
|
||||
actor_identifier=str(src.identifier or ""),
|
||||
origin_transport=str(protocol or "").strip().lower(),
|
||||
origin_chat_id=str(src.identifier or ""),
|
||||
payload=dict(payload or {}, revision=int(state.revision or 1)),
|
||||
raw_payload=dict(payload or {}),
|
||||
)
|
||||
except Exception as exc:
|
||||
self.log.warning(
|
||||
"Failed to append typing-start event for %s: %s",
|
||||
src.identifier,
|
||||
exc,
|
||||
)
|
||||
if protocol != "xmpp":
|
||||
set_person_typing_state(
|
||||
user_id=src.user_id,
|
||||
@@ -362,6 +482,9 @@ class UnifiedRouter(object):
|
||||
async def stopped_typing(self, protocol, *args, **kwargs):
|
||||
self.log.info(f"Stopped typing ({protocol}) {args} {kwargs}")
|
||||
identifier = kwargs.get("identifier")
|
||||
payload = dict(kwargs.get("payload") or {})
|
||||
event_ts = self._event_ts_from_kwargs(kwargs)
|
||||
direction = self._behavior_direction(protocol)
|
||||
identifiers = await self._resolve_identifier_objects(protocol, identifier)
|
||||
for src in identifiers:
|
||||
record_native_signal(
|
||||
@@ -377,6 +500,52 @@ class UnifiedRouter(object):
|
||||
payload={"origin": "router.stopped_typing"},
|
||||
)
|
||||
)
|
||||
try:
|
||||
session = await history.get_chat_session(src.user, src)
|
||||
await append_event(
|
||||
user=src.user,
|
||||
session=session,
|
||||
ts=event_ts,
|
||||
event_type="typing_stopped",
|
||||
direction=direction,
|
||||
actor_identifier=str(src.identifier or ""),
|
||||
origin_transport=str(protocol or "").strip().lower(),
|
||||
origin_chat_id=str(src.identifier or ""),
|
||||
payload=dict(payload or {}),
|
||||
raw_payload=dict(payload or {}),
|
||||
)
|
||||
if session is not None:
|
||||
abandoned = self._composing_tracker.observe_stopped(
|
||||
str(session.id),
|
||||
int(event_ts or 0),
|
||||
)
|
||||
if abandoned is not None:
|
||||
await append_event(
|
||||
user=src.user,
|
||||
session=session,
|
||||
ts=int(abandoned.get("stopped_ts") or event_ts or 0),
|
||||
event_type="composing_abandoned",
|
||||
direction=direction,
|
||||
actor_identifier=str(src.identifier or ""),
|
||||
origin_transport=str(protocol or "").strip().lower(),
|
||||
origin_chat_id=str(src.identifier or ""),
|
||||
payload={
|
||||
**dict(payload or {}),
|
||||
"abandoned": True,
|
||||
"duration_ms": int(
|
||||
abandoned.get("duration_ms") or 0
|
||||
),
|
||||
"revision": int(abandoned.get("revision") or 1),
|
||||
"started_ts": int(abandoned.get("started_ts") or 0),
|
||||
},
|
||||
raw_payload=dict(payload or {}),
|
||||
)
|
||||
except Exception as exc:
|
||||
self.log.warning(
|
||||
"Failed to append typing-stop event for %s: %s",
|
||||
src.identifier,
|
||||
exc,
|
||||
)
|
||||
if protocol != "xmpp":
|
||||
set_person_typing_state(
|
||||
user_id=src.user_id,
|
||||
|
||||
Reference in New Issue
Block a user