Increase platform abstraction cohesion

This commit is contained in:
2026-03-06 17:47:58 +00:00
parent 438e561da0
commit 8c091b1e6d
55 changed files with 6555 additions and 440 deletions

View File

@@ -52,7 +52,7 @@ from core.models import (
WorkspaceConversation,
)
from core.presence import get_settings as get_availability_settings
from core.presence import spans_for_range
from core.presence import latest_state_for_people, spans_for_range
from core.realtime.typing_state import get_person_typing_state
from core.transports.capabilities import supports, unsupported_reason
from core.translation.engine import process_inbound_translation
@@ -190,6 +190,92 @@ def _serialize_availability_spans(spans):
return rows
def _availability_summary_for_person(*, user, person: Person, service: str) -> dict:
person_key = str(person.id)
selected_service = str(service or "").strip().lower()
state_map = latest_state_for_people(
user=user,
person_ids=[person_key],
service=selected_service,
)
row = state_map.get(person_key)
is_cross_service = False
if row is None and selected_service:
state_map = latest_state_for_people(
user=user,
person_ids=[person_key],
service="",
)
row = state_map.get(person_key)
is_cross_service = row is not None
if row is None:
return {}
ts_value = int(row.get("ts") or 0)
state_value = str(row.get("state") or "unknown").strip().lower() or "unknown"
return {
"state": state_value,
"state_label": state_value.title(),
"service": str(row.get("service") or selected_service or "").strip().lower(),
"confidence": float(row.get("confidence") or 0.0),
"source_kind": str(row.get("source_kind") or "").strip(),
"ts": ts_value,
"ts_label": _format_ts_label(ts_value) if ts_value > 0 else "",
"is_cross_service": bool(is_cross_service),
}
def _compose_availability_payload(
*,
user,
person: Person | None,
service: str,
range_start: int,
range_end: int,
) -> tuple[bool, list[dict], dict]:
settings_row = get_availability_settings(user)
if (
person is None
or not settings_row.enabled
or not settings_row.show_in_chat
):
return False, [], {}
service_key = str(service or "").strip().lower()
rows = _serialize_availability_spans(
spans_for_range(
user=user,
person=person,
start_ts=int(range_start or 0),
end_ts=int(range_end or 0),
service=service_key,
limit=200,
)
)
used_cross_service = False
if not rows and service_key:
rows = _serialize_availability_spans(
spans_for_range(
user=user,
person=person,
start_ts=int(range_start or 0),
end_ts=int(range_end or 0),
service="",
limit=200,
)
)
used_cross_service = bool(rows)
summary = _availability_summary_for_person(
user=user,
person=person,
service=service_key,
)
if used_cross_service and summary:
summary["is_cross_service"] = True
return True, rows, summary
def _is_outgoing(msg: Message) -> bool:
is_outgoing = str(msg.custom_author or "").upper() in {"USER", "BOT"}
if not is_outgoing:
@@ -507,6 +593,66 @@ def _serialize_message(msg: Message) -> dict:
)
# Receipt payload and metadata
receipt_payload = msg.receipt_payload or {}
deleted_payload = dict((receipt_payload or {}).get("deleted") or {})
is_deleted = bool(
(receipt_payload or {}).get("is_deleted")
or deleted_payload
or (receipt_payload or {}).get("delete_events")
)
deleted_ts = 0
for candidate in (
deleted_payload.get("deleted_ts"),
deleted_payload.get("updated_at"),
deleted_payload.get("ts"),
):
try:
deleted_ts = int(candidate or 0)
except Exception:
deleted_ts = 0
if deleted_ts > 0:
break
deleted_display = _format_ts_label(deleted_ts) if deleted_ts > 0 else ""
deleted_actor = str(deleted_payload.get("actor") or "").strip()
deleted_source_service = str(deleted_payload.get("source_service") or "").strip()
edit_history_rows = []
for row in list((receipt_payload or {}).get("edit_history") or []):
item = dict(row or {})
edited_ts = 0
for candidate in (
item.get("edited_ts"),
item.get("updated_at"),
item.get("ts"),
):
try:
edited_ts = int(candidate or 0)
except Exception:
edited_ts = 0
if edited_ts > 0:
break
previous_text = str(item.get("previous_text") or "")
new_text = str(item.get("new_text") or "")
edit_history_rows.append(
{
"edited_ts": edited_ts,
"edited_display": _format_ts_label(edited_ts) if edited_ts > 0 else "",
"source_service": str(item.get("source_service") or "").strip().lower(),
"actor": str(item.get("actor") or "").strip(),
"previous_text": previous_text,
"new_text": new_text,
}
)
edit_history_rows.sort(key=lambda row: int(row.get("edited_ts") or 0))
edit_count = len(edit_history_rows)
last_edit_ts = int(edit_history_rows[-1].get("edited_ts") or 0) if edit_count else 0
last_edit_display = _format_ts_label(last_edit_ts) if last_edit_ts > 0 else ""
if is_deleted:
display_text = "(message deleted)"
image_urls = []
image_url = ""
hide_text = False
read_source_service = str(msg.read_source_service or "").strip()
read_by_identifier = str(msg.read_by_identifier or "").strip()
reaction_rows = []
@@ -570,6 +716,17 @@ def _serialize_message(msg: Message) -> dict:
"receipt_payload": receipt_payload,
"read_source_service": read_source_service,
"read_by_identifier": read_by_identifier,
"is_deleted": is_deleted,
"deleted_ts": deleted_ts,
"deleted_display": deleted_display,
"deleted_actor": deleted_actor,
"deleted_source_service": deleted_source_service,
"edit_history": edit_history_rows,
"edit_history_json": json.dumps(edit_history_rows),
"edit_count": edit_count,
"is_edited": bool(edit_count),
"last_edit_ts": last_edit_ts,
"last_edit_display": last_edit_display,
"reactions": reaction_rows,
"source_message_id": str(getattr(msg, "source_message_id", "") or ""),
"reply_to_id": str(getattr(msg, "reply_to_id", "") or ""),
@@ -2694,35 +2851,27 @@ def _panel_context(
counterpart_identifiers=counterpart_identifiers,
conversation=conversation,
)
availability_slices = []
availability_enabled = False
availability_settings = get_availability_settings(request.user)
if (
base["person"] is not None
and availability_settings.enabled
and availability_settings.show_in_chat
):
range_start = (
int(session_bundle["messages"][0].ts or 0) if session_bundle["messages"] else 0
)
range_end = (
int(session_bundle["messages"][-1].ts or 0) if session_bundle["messages"] else 0
)
if range_start <= 0 or range_end <= 0:
now_ts = int(time.time() * 1000)
range_start = now_ts - (24 * 60 * 60 * 1000)
range_end = now_ts
availability_enabled = True
availability_slices = _serialize_availability_spans(
spans_for_range(
user=request.user,
person=base["person"],
start_ts=range_start,
end_ts=range_end,
service=base["service"],
limit=200,
)
)
range_start = (
int(session_bundle["messages"][0].ts or 0) if session_bundle["messages"] else 0
)
range_end = (
int(session_bundle["messages"][-1].ts or 0) if session_bundle["messages"] else 0
)
if range_start <= 0 or range_end <= 0:
now_ts = int(time.time() * 1000)
range_start = now_ts - (24 * 60 * 60 * 1000)
range_end = now_ts
(
availability_enabled,
availability_slices,
availability_summary,
) = _compose_availability_payload(
user=request.user,
person=base["person"],
service=base["service"],
range_start=range_start,
range_end=range_end,
)
glance_items = _build_glance_items(
serialized_messages,
person_id=(base["person"].id if base["person"] else None),
@@ -2923,9 +3072,15 @@ def _panel_context(
"manual_icon_class": "fa-solid fa-paper-plane",
"panel_id": f"compose-panel-{unique}",
"typing_state_json": json.dumps(typing_state),
"capability_send": supports(base["service"], "send"),
"capability_send_reason": unsupported_reason(base["service"], "send"),
"capability_reactions": supports(base["service"], "reactions"),
"capability_reactions_reason": unsupported_reason(base["service"], "reactions"),
"availability_enabled": availability_enabled,
"availability_slices": availability_slices,
"availability_slices_json": json.dumps(availability_slices),
"availability_summary": availability_summary,
"availability_summary_json": json.dumps(availability_summary),
"command_options": command_options,
"bp_binding_summary": bp_binding_summary,
"platform_options": platform_options,
@@ -3383,31 +3538,23 @@ class ComposeThread(LoginRequiredMixin, View):
counterpart_identifiers = _counterpart_identifiers_for_person(
request.user, base["person"]
)
availability_slices = []
availability_settings = get_availability_settings(request.user)
if (
base["person"] is not None
and availability_settings.enabled
and availability_settings.show_in_chat
):
range_start = (
int(messages[0].ts or 0) if messages else max(0, int(after_ts or 0))
)
range_end = int(latest_ts or 0)
if range_start <= 0 or range_end <= 0:
now_ts = int(time.time() * 1000)
range_start = now_ts - (24 * 60 * 60 * 1000)
range_end = now_ts
availability_slices = _serialize_availability_spans(
spans_for_range(
user=request.user,
person=base["person"],
start_ts=range_start,
end_ts=range_end,
service=base["service"],
limit=200,
)
)
range_start = int(messages[0].ts or 0) if messages else max(0, int(after_ts or 0))
range_end = int(latest_ts or 0)
if range_start <= 0 or range_end <= 0:
now_ts = int(time.time() * 1000)
range_start = now_ts - (24 * 60 * 60 * 1000)
range_end = now_ts
(
_availability_enabled,
availability_slices,
availability_summary,
) = _compose_availability_payload(
user=request.user,
person=base["person"],
service=base["service"],
range_start=range_start,
range_end=range_end,
)
payload = {
"messages": _serialize_messages_with_artifacts(
messages,
@@ -3417,6 +3564,7 @@ class ComposeThread(LoginRequiredMixin, View):
),
"last_ts": latest_ts,
"availability_slices": availability_slices,
"availability_summary": availability_summary,
"typing": get_person_typing_state(
user_id=request.user.id,
person_id=base["person"].id if base["person"] else None,
@@ -4459,6 +4607,18 @@ class ComposeSend(LoginRequiredMixin, View):
log_prefix = (
f"[ComposeSend] service={base['service']} identifier={base['identifier']}"
)
if bool(getattr(settings, "CAPABILITY_ENFORCEMENT_ENABLED", True)) and not supports(
str(base["service"] or "").strip().lower(),
"send",
):
reason = unsupported_reason(str(base["service"] or "").strip().lower(), "send")
return self._response(
request,
ok=False,
message=f"Send not supported: {reason}",
level="warning",
panel_id=panel_id,
)
logger.debug(f"{log_prefix} text_len={len(text)} attempting send")
# If runtime is out-of-process, enqueue command and return immediately (non-blocking).