Implement reactions and image sync
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user