Fix Signal emoji sync
This commit is contained in:
@@ -1039,6 +1039,23 @@ class SignalClient(ClientBase):
|
|||||||
)
|
)
|
||||||
if result is False or result is None:
|
if result is False or result is None:
|
||||||
raise RuntimeError("signal_send_failed")
|
raise RuntimeError("signal_send_failed")
|
||||||
|
legacy_message_id = str(metadata.get("legacy_message_id") or "").strip()
|
||||||
|
if legacy_message_id and isinstance(result, int):
|
||||||
|
message_row = await sync_to_async(
|
||||||
|
lambda: Message.objects.filter(id=legacy_message_id).first()
|
||||||
|
)()
|
||||||
|
if message_row is not None:
|
||||||
|
update_fields = []
|
||||||
|
if int(message_row.delivered_ts or 0) <= 0:
|
||||||
|
message_row.delivered_ts = int(result)
|
||||||
|
update_fields.append("delivered_ts")
|
||||||
|
if str(message_row.source_message_id or "").strip() != str(result):
|
||||||
|
message_row.source_message_id = str(result)
|
||||||
|
update_fields.append("source_message_id")
|
||||||
|
if update_fields:
|
||||||
|
await sync_to_async(message_row.save)(
|
||||||
|
update_fields=update_fields
|
||||||
|
)
|
||||||
transport.set_runtime_command_result(
|
transport.set_runtime_command_result(
|
||||||
self.service,
|
self.service,
|
||||||
command_id,
|
command_id,
|
||||||
@@ -1275,6 +1292,45 @@ class SignalClient(ClientBase):
|
|||||||
destination_uuid,
|
destination_uuid,
|
||||||
destination_number,
|
destination_number,
|
||||||
)
|
)
|
||||||
|
reaction_payload = _extract_signal_reaction(envelope)
|
||||||
|
if identifiers and isinstance(reaction_payload, dict):
|
||||||
|
source_uuid = str(
|
||||||
|
envelope.get("sourceUuid") or envelope.get("source") or ""
|
||||||
|
).strip()
|
||||||
|
source_number = str(envelope.get("sourceNumber") or "").strip()
|
||||||
|
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:
|
||||||
|
self.log.warning(
|
||||||
|
"signal raw sync reaction history apply failed: %s", exc
|
||||||
|
)
|
||||||
|
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:
|
||||||
|
self.log.warning(
|
||||||
|
"signal raw sync reaction relay to XMPP failed: %s", exc
|
||||||
|
)
|
||||||
if identifiers and text:
|
if identifiers and text:
|
||||||
ts_raw = (
|
ts_raw = (
|
||||||
sync_sent_message.get("timestamp")
|
sync_sent_message.get("timestamp")
|
||||||
@@ -1358,6 +1414,21 @@ class SignalClient(ClientBase):
|
|||||||
return
|
return
|
||||||
|
|
||||||
identifiers = await self._resolve_signal_identifiers(source_uuid, source_number)
|
identifiers = await self._resolve_signal_identifiers(source_uuid, source_number)
|
||||||
|
reaction_payload = _extract_signal_reaction(envelope)
|
||||||
|
if (not identifiers) and isinstance(reaction_payload, dict):
|
||||||
|
# Sync reactions from our own linked device can arrive with source=our
|
||||||
|
# account and destination=<contact>. Resolve by destination as fallback.
|
||||||
|
destination_uuid = str(
|
||||||
|
envelope.get("destinationServiceId")
|
||||||
|
or envelope.get("destinationUuid")
|
||||||
|
or ""
|
||||||
|
).strip()
|
||||||
|
destination_number = str(envelope.get("destinationNumber") or "").strip()
|
||||||
|
if destination_uuid or destination_number:
|
||||||
|
identifiers = await self._resolve_signal_identifiers(
|
||||||
|
destination_uuid,
|
||||||
|
destination_number,
|
||||||
|
)
|
||||||
if not identifiers:
|
if not identifiers:
|
||||||
identifiers = await self._auto_link_single_user_signal_identifier(
|
identifiers = await self._auto_link_single_user_signal_identifier(
|
||||||
source_uuid, source_number
|
source_uuid, source_number
|
||||||
@@ -1371,7 +1442,6 @@ class SignalClient(ClientBase):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
reaction_payload = _extract_signal_reaction(envelope)
|
|
||||||
if isinstance(reaction_payload, dict):
|
if isinstance(reaction_payload, dict):
|
||||||
for identifier in identifiers:
|
for identifier in identifiers:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -221,3 +221,101 @@ class SignalInboundReplyLinkTests(TransactionTestCase):
|
|||||||
any(str(row.get("emoji") or "") == "❤️" for row in reactions),
|
any(str(row.get("emoji") or "") == "❤️" for row in reactions),
|
||||||
"Expected Signal heart reaction to be applied to anchor receipt payload.",
|
"Expected Signal heart reaction to be applied to anchor receipt payload.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_process_raw_inbound_event_applies_sync_reaction_using_destination_fallback(self):
|
||||||
|
fake_ur = Mock()
|
||||||
|
fake_ur.message_received = AsyncMock(return_value=None)
|
||||||
|
fake_ur.xmpp = Mock()
|
||||||
|
fake_ur.xmpp.client = Mock()
|
||||||
|
fake_ur.xmpp.client.apply_external_reaction = AsyncMock(return_value=None)
|
||||||
|
client = SignalClient.__new__(SignalClient)
|
||||||
|
client.service = "signal"
|
||||||
|
client.ur = fake_ur
|
||||||
|
client.log = Mock()
|
||||||
|
client.client = Mock()
|
||||||
|
# Emulate sync event from our own linked device.
|
||||||
|
client.client.bot_uuid = "f5f53a90-8b8c-4f0c-9520-8f13aab0b219"
|
||||||
|
client.client.phone_number = "+15550009999"
|
||||||
|
client._resolve_signal_identifiers = AsyncMock(return_value=[self.identifier])
|
||||||
|
client._auto_link_single_user_signal_identifier = AsyncMock(return_value=[])
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"envelope": {
|
||||||
|
"sourceNumber": "+15550009999",
|
||||||
|
"sourceUuid": "f5f53a90-8b8c-4f0c-9520-8f13aab0b219",
|
||||||
|
"destinationNumber": "+15550002000",
|
||||||
|
"timestamp": 1772545465000,
|
||||||
|
"syncMessage": {
|
||||||
|
"sentMessage": {
|
||||||
|
"destinationNumber": "+15550002000",
|
||||||
|
"message": {
|
||||||
|
"reaction": {
|
||||||
|
"emoji": "🔥",
|
||||||
|
"targetSentTimestamp": 1772545458187,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async_to_sync(client._process_raw_inbound_event)(json.dumps(payload))
|
||||||
|
|
||||||
|
self.anchor.refresh_from_db()
|
||||||
|
reactions = list((self.anchor.receipt_payload or {}).get("reactions") or [])
|
||||||
|
self.assertTrue(
|
||||||
|
any(str(row.get("emoji") or "") == "🔥" for row in reactions),
|
||||||
|
"Expected sync reaction to be applied via destination-number fallback resolution.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SignalRuntimeCommandWritebackTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
username="signal-runtime-writeback-user",
|
||||||
|
email="signal-runtime-writeback@example.com",
|
||||||
|
password="x",
|
||||||
|
)
|
||||||
|
self.person = Person.objects.create(user=self.user, name="Signal Runtime")
|
||||||
|
self.identifier = PersonIdentifier.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
person=self.person,
|
||||||
|
service="signal",
|
||||||
|
identifier="+15550003000",
|
||||||
|
)
|
||||||
|
self.session = ChatSession.objects.create(user=self.user, identifier=self.identifier)
|
||||||
|
self.message = Message.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
session=self.session,
|
||||||
|
sender_uuid="",
|
||||||
|
custom_author="USER",
|
||||||
|
text="queued signal send",
|
||||||
|
ts=1772545467000,
|
||||||
|
delivered_ts=None,
|
||||||
|
source_service="web",
|
||||||
|
source_message_id="1772545467000",
|
||||||
|
source_chat_id="+15550003000",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_execute_runtime_send_updates_legacy_message_ids(self):
|
||||||
|
client = SignalClient.__new__(SignalClient)
|
||||||
|
client.service = "signal"
|
||||||
|
client.log = Mock()
|
||||||
|
command = {
|
||||||
|
"id": "cmd-1",
|
||||||
|
"action": "send_message_raw",
|
||||||
|
"payload": {
|
||||||
|
"recipient": "+15550003000",
|
||||||
|
"text": "queued signal send",
|
||||||
|
"attachments": [],
|
||||||
|
"metadata": {"legacy_message_id": str(self.message.id)},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
with patch(
|
||||||
|
"core.clients.signal.signalapi.send_message_raw",
|
||||||
|
new=AsyncMock(return_value=1772545467999),
|
||||||
|
):
|
||||||
|
async_to_sync(client._execute_runtime_command)(command)
|
||||||
|
|
||||||
|
self.message.refresh_from_db()
|
||||||
|
self.assertEqual(1772545467999, int(self.message.delivered_ts or 0))
|
||||||
|
self.assertEqual("1772545467999", str(self.message.source_message_id or ""))
|
||||||
|
|||||||
Reference in New Issue
Block a user