Fix Signal compose live updates and self-chat direction
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user