Increase platform abstraction cohesion
This commit is contained in:
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user