Implement reactions and image sync

This commit is contained in:
2026-02-17 21:23:03 +00:00
parent cccdb7b72a
commit fb46274bf3
14 changed files with 2011 additions and 202 deletions

View File

@@ -31,6 +31,23 @@ else:
SIGNAL_URL = f"{SIGNAL_HOST}:{SIGNAL_PORT}"
def _is_internal_compose_blob_url(value: str) -> bool:
raw = str(value or "").strip()
if not raw:
return False
if raw.startswith("/compose/media/blob/"):
return True
parsed = urlparse(raw if "://" in raw else f"https://dummy{raw}")
return str(parsed.path or "").startswith("/compose/media/blob/")
def _is_compose_blob_only_text(text_value: str) -> bool:
lines = [line.strip() for line in str(text_value or "").splitlines() if line.strip()]
if not lines:
return False
return all(_is_internal_compose_blob_url(line) for line in lines)
def _get_nested(payload, path):
current = payload
for key in path:
@@ -129,6 +146,41 @@ def _extract_receipt_timestamps(receipt_payload):
return []
def _extract_signal_reaction(envelope):
paths = [
("dataMessage", "reaction"),
("syncMessage", "sentMessage", "message", "reaction"),
("syncMessage", "sentMessage", "reaction"),
]
node = None
for path in paths:
candidate = _get_nested(envelope, path)
if isinstance(candidate, dict):
node = candidate
break
if not isinstance(node, dict):
return None
emoji = str(node.get("emoji") or "").strip()
target_ts = node.get("targetSentTimestamp")
if target_ts is None:
target_ts = node.get("targetTimestamp")
try:
target_ts = int(target_ts)
except Exception:
target_ts = 0
remove = bool(node.get("remove") or node.get("isRemove"))
if not emoji and not remove:
return None
if target_ts <= 0:
return None
return {
"emoji": emoji,
"target_ts": target_ts,
"remove": remove,
"raw": dict(node),
}
def _typing_started(typing_payload):
action = str(typing_payload.get("action") or "").strip().lower()
if action in {"started", "start", "typing", "composing"}:
@@ -343,6 +395,32 @@ class HandleMessage(Command):
)
return
reaction_payload = _extract_signal_reaction(envelope)
if isinstance(reaction_payload, dict):
log.debug(
"reaction-bridge signal-inbound target_ts=%s emoji=%s remove=%s identifiers=%s",
int(reaction_payload.get("target_ts") or 0),
str(reaction_payload.get("emoji") or "") or "-",
bool(reaction_payload.get("remove")),
len(identifiers),
)
for identifier in identifiers:
try:
await self.ur.xmpp.client.apply_external_reaction(
identifier.user,
identifier,
source_service="signal",
emoji=str(reaction_payload.get("emoji") or ""),
remove=bool(reaction_payload.get("remove")),
upstream_message_id="",
upstream_ts=int(reaction_payload.get("target_ts") or 0),
actor=(source_uuid or source_number or ""),
payload=reaction_payload.get("raw") or {},
)
except Exception as exc:
log.warning("Signal reaction relay to XMPP failed: %s", exc)
return
# Handle attachments across multiple Signal payload variants.
attachment_list = _extract_attachments(raw)
xmpp_attachments = []
@@ -385,8 +463,11 @@ class HandleMessage(Command):
f"/compose/media/blob/?key={quote_plus(str(blob_key))}"
)
if (not text) and compose_media_urls:
text = "\n".join(compose_media_urls)
# Keep relay payload text clean for XMPP. Blob URLs are web/history fallback
# only and should not be injected into XMPP body text.
relay_text = text
if attachment_list and _is_compose_blob_only_text(relay_text):
relay_text = ""
# Forward incoming Signal messages to XMPP and apply mutate rules.
identifier_text_overrides = {}
@@ -407,7 +488,7 @@ class HandleMessage(Command):
uploaded_urls = []
for manip in mutate_manips:
prompt = replies.generate_mutate_reply_prompt(
text,
relay_text,
None,
manip,
None,
@@ -423,8 +504,13 @@ class HandleMessage(Command):
result,
is_outgoing_message,
attachments=xmpp_attachments,
source_ref={
"upstream_message_id": "",
"upstream_author": str(source_uuid or source_number or ""),
"upstream_ts": int(ts or 0),
},
)
resolved_text = text
resolved_text = relay_text
if (not resolved_text) and uploaded_urls:
resolved_text = "\n".join(uploaded_urls)
elif (not resolved_text) and compose_media_urls:
@@ -437,11 +523,16 @@ class HandleMessage(Command):
uploaded_urls = await self.ur.xmpp.client.send_from_external(
user,
identifier,
text,
relay_text,
is_outgoing_message,
attachments=xmpp_attachments,
source_ref={
"upstream_message_id": "",
"upstream_author": str(source_uuid or source_number or ""),
"upstream_ts": int(ts or 0),
},
)
resolved_text = text
resolved_text = relay_text
if (not resolved_text) and uploaded_urls:
resolved_text = "\n".join(uploaded_urls)
elif (not resolved_text) and compose_media_urls:
@@ -463,7 +554,7 @@ class HandleMessage(Command):
session_cache[session_key] = chat_session
sender_key = source_uuid or source_number or identifier_candidates[0]
message_key = (chat_session.id, ts, sender_key)
message_text = identifier_text_overrides.get(session_key, text)
message_text = identifier_text_overrides.get(session_key, relay_text)
if message_key not in stored_messages:
await history.store_message(
session=chat_session,