Fix Signal messages and replies

This commit is contained in:
2026-03-03 15:51:58 +00:00
parent 56c620473f
commit d6bd56dace
31 changed files with 3317 additions and 668 deletions

View File

@@ -28,6 +28,7 @@ from django.views import View
from core.clients import transport
from core.commands.base import CommandContext
from core.commands.engine import process_inbound_message
from core.commands.policies import ensure_variant_policies_for_profile
from core.messaging import ai as ai_runner
from core.messaging import media_bridge
from core.messaging.utils import messages_to_string
@@ -1610,6 +1611,29 @@ def _latest_whatsapp_bridge_ref(message: Message | None) -> dict:
return best
def _latest_signal_bridge_ref(message: Message | None) -> dict:
if message is None:
return {}
payload = dict(getattr(message, "receipt_payload", {}) or {})
refs = dict(payload.get("bridge_refs") or {})
rows = list(refs.get("signal") or [])
best = {}
best_updated = -1
for row in rows:
if not isinstance(row, dict):
continue
has_upstream = str(row.get("upstream_message_id") or "").strip() or int(
row.get("upstream_ts") or 0
)
if not has_upstream:
continue
updated_at = int(row.get("updated_at") or 0)
if updated_at >= best_updated:
best = dict(row)
best_updated = updated_at
return best
def _build_whatsapp_reply_metadata(reply_to: Message | None, channel_identifier: str) -> dict:
if reply_to is None:
return {}
@@ -1640,6 +1664,58 @@ def _build_whatsapp_reply_metadata(reply_to: Message | None, channel_identifier:
}
def _build_signal_reply_metadata(reply_to: Message | None, channel_identifier: str) -> dict:
if reply_to is None:
return {}
quote_timestamp = 0
source_message_id = str(getattr(reply_to, "source_message_id", "") or "").strip()
if source_message_id.isdigit():
quote_timestamp = int(source_message_id)
if not quote_timestamp:
bridge_ref = _latest_signal_bridge_ref(reply_to)
upstream_id = str(bridge_ref.get("upstream_message_id") or "").strip()
if upstream_id.isdigit():
quote_timestamp = int(upstream_id)
if not quote_timestamp:
quote_timestamp = int(bridge_ref.get("upstream_ts") or 0)
if not quote_timestamp:
quote_timestamp = int(getattr(reply_to, "ts", 0) or 0)
if quote_timestamp <= 0:
return {}
quote_author = ""
sender_uuid = str(getattr(reply_to, "sender_uuid", "") or "").strip()
if sender_uuid:
quote_author = sender_uuid
# Signal quote payloads work best with phone-style identifiers.
# Inbound rows may store sender UUID; prefer known chat/number in that case.
source_chat_id = str(getattr(reply_to, "source_chat_id", "") or "").strip()
if quote_author and SIGNAL_UUID_PATTERN.match(quote_author):
if source_chat_id:
quote_author = source_chat_id
if (
str(getattr(reply_to, "custom_author", "") or "").strip().upper() in {"USER", "BOT"}
or not quote_author
):
quote_author = str(getattr(settings, "SIGNAL_NUMBER", "") or "").strip() or quote_author
if not quote_author:
quote_author = source_chat_id
if not quote_author:
quote_author = str(channel_identifier or "").strip()
if not quote_author:
return {}
quote_text = str(getattr(reply_to, "text", "") or "").strip()
payload = {
"quote_timestamp": int(quote_timestamp),
"quote_author": quote_author,
}
if quote_text:
payload["quote_text"] = quote_text[:512]
return payload
def _canonical_command_channel_identifier(service: str, identifier: str) -> str:
value = str(identifier or "").strip()
if not value:
@@ -1689,6 +1765,7 @@ def _ensure_bp_profile_and_actions(user) -> CommandProfile:
if (not created) and (not row.enabled):
row.enabled = True
row.save(update_fields=["enabled", "updated_at"])
ensure_variant_policies_for_profile(profile)
return profile
@@ -1779,46 +1856,102 @@ def _command_options_for_channel(user, service: str, identifier: str) -> list[di
channel_identifier__in=list(variants),
enabled=True,
).exists()
options.append(
{
"slug": slug,
"name": str(profile.name or slug).strip() or slug,
"trigger_token": str(profile.trigger_token or "").strip(),
"enabled_here": bool(enabled_here),
"profile_enabled": bool(profile.enabled),
if slug == "bp":
policies = ensure_variant_policies_for_profile(profile) if profile.id else {}
label_by_key = {
"bp": "bp",
"bp_set": "bp set",
"bp_set_range": "bp set range",
}
)
task_announce_enabled = False
if variants:
source = (
ChatTaskSource.objects.filter(
user=user,
service=service_key,
channel_identifier__in=list(variants),
enabled=True,
options.extend(
[
{
"slug": "bp",
"toggle_slug": "bp",
"name": "bp",
"trigger_token": "#bp#",
"enabled_here": bool(enabled_here),
"profile_enabled": bool(profile.enabled),
"mode_label": str(
(policies.get("bp").generation_mode if policies.get("bp") else "ai")
).upper(),
},
{
"slug": "bp_set",
"toggle_slug": "bp",
"name": "bp set",
"trigger_token": "#bp set#",
"enabled_here": bool(enabled_here),
"profile_enabled": bool(profile.enabled),
"mode_label": str(
(policies.get("bp_set").generation_mode if policies.get("bp_set") else "verbatim")
).upper(),
},
{
"slug": "bp_set_range",
"toggle_slug": "bp",
"name": "bp set range",
"trigger_token": "#bp set range#",
"enabled_here": bool(enabled_here),
"profile_enabled": bool(profile.enabled),
"mode_label": str(
(
policies.get("bp_set_range").generation_mode
if policies.get("bp_set_range")
else "verbatim"
)
).upper(),
},
]
)
for row in options:
if row.get("slug") in label_by_key:
row["enabled_label"] = "Enabled" if row.get("enabled_here") else "Disabled"
else:
options.append(
{
"slug": slug,
"toggle_slug": slug,
"name": str(profile.name or slug).strip() or slug,
"trigger_token": str(profile.trigger_token or "").strip(),
"enabled_here": bool(enabled_here),
"profile_enabled": bool(profile.enabled),
"mode_label": "",
"enabled_label": "Enabled" if enabled_here else "Disabled",
}
)
.order_by("-updated_at")
.first()
)
settings_row = dict(getattr(source, "settings", {}) or {}) if source else {}
task_announce_enabled = str(settings_row.get("announce_task_id", "")).strip().lower() in {
"1",
"true",
"yes",
"on",
}
options.append(
{
"slug": "task_announce",
"name": "Announce Task IDs",
"trigger_token": "",
"enabled_here": bool(task_announce_enabled),
"profile_enabled": True,
}
)
return options
def _bp_binding_summary_for_channel(user, service: str, identifier: str) -> dict:
service_key = _default_service(service)
variants = _command_channel_identifier_variants(service_key, identifier)
if not variants:
return {"ingress_count": 0, "egress_count": 0}
profile = (
CommandProfile.objects.filter(user=user, slug="bp")
.order_by("id")
.first()
)
if profile is None:
return {"ingress_count": 0, "egress_count": 0}
ingress_count = CommandChannelBinding.objects.filter(
profile=profile,
direction="ingress",
service=service_key,
channel_identifier__in=list(variants),
enabled=True,
).count()
egress_count = CommandChannelBinding.objects.filter(
profile=profile,
direction="egress",
service=service_key,
channel_identifier__in=list(variants),
enabled=True,
).count()
return {"ingress_count": int(ingress_count), "egress_count": int(egress_count)}
def _toggle_task_announce_for_channel(
*,
user,
@@ -2450,6 +2583,11 @@ def _panel_context(
base["service"],
base["identifier"],
)
bp_binding_summary = _bp_binding_summary_for_channel(
request.user,
base["service"],
base["identifier"],
)
recent_contacts = _recent_manual_contacts(
request.user,
current_service=base["service"],
@@ -2457,6 +2595,27 @@ def _panel_context(
current_person=base["person"],
limit=12,
)
signal_ingest_warning = ""
if base["service"] == "signal":
signal_state = transport.get_runtime_state("signal") or {}
error_type = str(signal_state.get("last_inbound_exception_type") or "").strip()
error_message = str(
signal_state.get("last_inbound_exception_message") or ""
).strip()
try:
error_ts = int(signal_state.get("last_inbound_exception_ts") or 0)
except Exception:
error_ts = 0
try:
ok_ts = int(signal_state.get("last_inbound_ok_ts") or 0)
except Exception:
ok_ts = 0
if (error_type or error_message) and error_ts >= ok_ts:
signal_ingest_warning = (
"Signal inbound decrypt/metadata error detected"
+ (f" ({error_type})" if error_type else "")
+ (f": {error_message[:220]}" if error_message else "")
)
return {
"service": base["service"],
@@ -2485,6 +2644,9 @@ def _panel_context(
"compose_quick_insights_url": reverse("compose_quick_insights"),
"compose_history_sync_url": reverse("compose_history_sync"),
"compose_toggle_command_url": reverse("compose_toggle_command"),
"command_routing_scoped_url": (
f"{reverse('command_routing')}?{urlencode({'service': base['service'], 'identifier': base['identifier'] or ''})}"
),
"compose_answer_suggestion_send_url": reverse("compose_answer_suggestion_send"),
"compose_ws_url": ws_url,
"tasks_hub_url": reverse("tasks_hub"),
@@ -2515,8 +2677,10 @@ def _panel_context(
"panel_id": f"compose-panel-{unique}",
"typing_state_json": json.dumps(typing_state),
"command_options": command_options,
"bp_binding_summary": bp_binding_summary,
"platform_options": platform_options,
"recent_contacts": recent_contacts,
"signal_ingest_warning": signal_ingest_warning,
"is_group": base.get("is_group", False),
"group_name": base.get("group_name", ""),
}
@@ -3366,6 +3530,8 @@ class ComposeToggleCommand(LoginRequiredMixin, View):
status=400,
)
slug = str(request.POST.get("slug") or "bp").strip().lower() or "bp"
if slug in {"bp_set", "bp_set_range"}:
slug = "bp"
enabled = str(request.POST.get("enabled") or "1").strip().lower() in {
"1",
"true",
@@ -3406,6 +3572,9 @@ class ComposeToggleCommand(LoginRequiredMixin, View):
if enabled
else f"{slug} disabled for this chat."
)
scoped_settings_url = (
f"{reverse('command_routing')}?{urlencode({'service': service, 'identifier': channel_identifier})}"
)
return JsonResponse(
{
"ok": True,
@@ -3416,7 +3585,7 @@ class ComposeToggleCommand(LoginRequiredMixin, View):
"settings_url": (
f"{reverse('tasks_settings')}?{urlencode({'service': service, 'identifier': channel_identifier})}"
if slug == "task_announce"
else reverse("command_routing")
else scoped_settings_url
),
}
)
@@ -3442,7 +3611,7 @@ class ComposeBindBP(ComposeToggleCommand):
"message": "bp enabled for this chat.",
"slug": "bp",
"enabled": True,
"settings_url": reverse("command_routing"),
"settings_url": f"{reverse('command_routing')}?{urlencode({'service': service, 'identifier': str(identifier or '')})}",
}
)
@@ -4026,6 +4195,10 @@ class ComposeSend(LoginRequiredMixin, View):
outbound_reply_metadata = _build_whatsapp_reply_metadata(
reply_to, str(base["identifier"] or "")
)
elif base["service"] == "signal":
outbound_reply_metadata = _build_signal_reply_metadata(
reply_to, str(base["identifier"] or "")
)
if base["service"] == "whatsapp":
runtime_state = transport.get_runtime_state("whatsapp")
last_seen = int(runtime_state.get("runtime_seen_at") or 0)
@@ -4102,6 +4275,10 @@ class ComposeSend(LoginRequiredMixin, View):
outbound_reply_metadata = _build_whatsapp_reply_metadata(
reply_to, str(base["identifier"] or "")
)
elif base["service"] == "signal":
outbound_reply_metadata = _build_signal_reply_metadata(
reply_to, str(base["identifier"] or "")
)
ts = async_to_sync(transport.send_message_raw)(
base["service"],
base["identifier"],