Fix Signal messages and replies
This commit is contained in:
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user