Fix Signal compose live updates and self-chat direction

This commit is contained in:
2026-02-22 11:03:08 +00:00
parent 65cd647f01
commit 0f36b2dde7
4 changed files with 124 additions and 13 deletions

View File

@@ -1,5 +1,6 @@
import asyncio
import json
import re
import time
from urllib.parse import quote_plus, urlparse
@@ -198,10 +199,18 @@ def _identifier_candidates(*values):
seen = set()
for value in values:
cleaned = str(value or "").strip()
if not cleaned or cleaned in seen:
if not cleaned:
continue
seen.add(cleaned)
out.append(cleaned)
candidates = [cleaned]
digits = re.sub(r"[^0-9]", "", cleaned)
# Add basic E.164 variants for phone-shaped values.
if digits and cleaned.count("-") < 4:
candidates.extend([digits, f"+{digits}"])
for candidate in candidates:
if not candidate or candidate in seen:
continue
seen.add(candidate)
out.append(candidate)
return out
@@ -349,12 +358,32 @@ class HandleMessage(Command):
ts = c.message.timestamp
source_value = c.message.source
envelope = raw.get("envelope", {})
destination_number = sent_message.get("destination")
bot_uuid = str(getattr(c.bot, "bot_uuid", "") or "").strip()
bot_phone = str(getattr(c.bot, "phone_number", "") or "").strip()
source_uuid_norm = str(source_uuid or "").strip()
source_number_norm = str(source_number or "").strip()
dest_norm = str(dest or "").strip()
destination_number_norm = str(destination_number or "").strip()
bot_phone_digits = re.sub(r"[^0-9]", "", bot_phone)
source_phone_digits = re.sub(r"[^0-9]", "", source_number_norm)
dest_phone_digits = re.sub(r"[^0-9]", "", destination_number_norm or dest_norm)
# Message originating from us
same_recipient = source_uuid == dest
is_from_bot = source_uuid == c.bot.bot_uuid
is_to_bot = dest == c.bot.bot_uuid or dest is None
is_from_bot = bool(bot_uuid and source_uuid_norm and source_uuid_norm == bot_uuid)
if (not is_from_bot) and bot_phone_digits and source_phone_digits:
is_from_bot = source_phone_digits == bot_phone_digits
# For non-sync incoming events destination is usually absent and points to us.
is_to_bot = bool(bot_uuid and dest_norm and dest_norm == bot_uuid)
if (not is_to_bot) and bot_phone_digits and dest_phone_digits:
is_to_bot = dest_phone_digits == bot_phone_digits
if (not is_to_bot) and (not dest_norm) and (not destination_number_norm):
is_to_bot = True
reply_to_self = same_recipient and is_from_bot # Reply
reply_to_others = is_to_bot and not same_recipient # Reply
@@ -363,7 +392,6 @@ class HandleMessage(Command):
envelope_source_uuid = envelope.get("sourceUuid")
envelope_source_number = envelope.get("sourceNumber")
envelope_source = envelope.get("source")
destination_number = sent_message.get("destination")
primary_identifier = dest if is_from_bot else source_uuid
if dest or destination_number:
@@ -450,6 +478,20 @@ class HandleMessage(Command):
len(identifiers),
)
for identifier in identifiers:
try:
await history.apply_reaction(
identifier.user,
identifier,
target_message_id="",
target_ts=int(reaction_payload.get("target_ts") or 0),
emoji=str(reaction_payload.get("emoji") or ""),
source_service="signal",
actor=(source_uuid or source_number or ""),
remove=bool(reaction_payload.get("remove")),
payload=reaction_payload.get("raw") or {},
)
except Exception as exc:
log.warning("Signal reaction history apply failed: %s", exc)
try:
await self.ur.xmpp.client.apply_external_reaction(
identifier.user,

View File

@@ -22,6 +22,24 @@ def _safe_int(value, default=0):
return default
def _message_fingerprint(message_row):
row = dict(message_row or {})
fields = {
"id": str(row.get("id") or ""),
"ts": int(_safe_int(row.get("ts"), 0)),
"text": str(row.get("text") or ""),
"display_text": str(row.get("display_text") or ""),
"outgoing": bool(row.get("outgoing")),
"read_ts": int(_safe_int(row.get("read_ts"), 0)),
"delivered_ts": int(_safe_int(row.get("delivered_ts"), 0)),
"receipt_payload": row.get("receipt_payload") or {},
"reactions": row.get("reactions") or [],
"source_service": str(row.get("source_service") or ""),
"source_label": str(row.get("source_label") or ""),
}
return json.dumps(fields, sort_keys=True, separators=(",", ":"))
def _load_since(user_id, service, identifier, person_id, after_ts, limit):
person = None
person_identifier = None
@@ -174,7 +192,7 @@ async def compose_ws_application(scope, receive, send):
last_ts = 0
limit = 100
last_typing_key = ""
sent_message_ids = set()
sent_message_state = {}
while True:
event = None
@@ -205,10 +223,11 @@ async def compose_ws_application(scope, receive, send):
messages = []
for msg in raw_messages:
message_id = str((msg or {}).get("id") or "").strip()
if message_id and message_id in sent_message_ids:
continue
if message_id:
sent_message_ids.add(message_id)
fingerprint = _message_fingerprint(msg)
if sent_message_state.get(message_id) == fingerprint:
continue
sent_message_state[message_id] = fingerprint
messages.append(msg)
latest = _safe_int(payload.get("last_ts"), last_ts)
if resolved_person_id <= 0:

View File

@@ -2047,8 +2047,13 @@
const appendBubble = function (msg) {
const messageId = String(msg && msg.id ? msg.id : "").trim();
if (messageId && panelState.seenMessageIds.has(messageId)) {
return;
if (messageId) {
const existingRow = Array.from(thread.querySelectorAll(".compose-row")).find(function (row) {
return String((row.dataset && row.dataset.messageId) || "") === messageId;
});
if (existingRow) {
existingRow.remove();
}
}
const row = document.createElement("div");
const outgoing = !!msg.outgoing;
@@ -2114,6 +2119,22 @@
fallback.textContent = "(no text)";
bubble.appendChild(fallback);
}
if (Array.isArray(msg.reactions) && msg.reactions.length) {
const reactionsWrap = document.createElement("div");
reactionsWrap.className = "compose-reactions";
reactionsWrap.setAttribute("aria-label", "Message reactions");
msg.reactions.forEach(function (reaction) {
const chip = document.createElement("span");
chip.className = "compose-reaction-chip";
chip.textContent = String(reaction && reaction.emoji ? reaction.emoji : "");
chip.title =
String((reaction && reaction.actor) || "Unknown")
+ " via "
+ String((reaction && reaction.source_service) || "unknown").toUpperCase();
reactionsWrap.appendChild(chip);
});
bubble.appendChild(reactionsWrap);
}
const meta = document.createElement("p");
meta.className = "compose-msg-meta";

View File

@@ -129,7 +129,36 @@ def _format_ts_label(ts_value: int) -> str:
def _is_outgoing(msg: Message) -> bool:
return str(msg.custom_author or "").upper() in {"USER", "BOT"}
is_outgoing = str(msg.custom_author or "").upper() in {"USER", "BOT"}
if not is_outgoing:
return False
# Signal self-chat special case:
# platform-originated Signal sync events can carry our own sender id and
# are currently stored as USER; render them as counterpart-side so the
# thread reads naturally when messaging ourselves.
try:
session = getattr(msg, "session", None)
identifier_obj = getattr(session, "identifier", None)
service = str(getattr(identifier_obj, "service", "") or "").strip().lower()
if service != "signal":
return True
sender_uuid = str(getattr(msg, "sender_uuid", "") or "").strip()
identifier_value = str(getattr(identifier_obj, "identifier", "") or "").strip()
if not sender_uuid or not identifier_value:
return True
if sender_uuid.lower() == identifier_value.lower():
return False
sender_digits = re.sub(r"[^0-9]", "", sender_uuid)
identifier_digits = re.sub(r"[^0-9]", "", identifier_value)
if sender_digits and identifier_digits and sender_digits == identifier_digits:
return False
except Exception:
return is_outgoing
return True
def _clean_url(candidate: str) -> str: