Merge pull request #637 from neist/main

fix(signal): handle reactions in dataMessage.reaction format
This commit is contained in:
Peter Steinberger
2026-01-10 18:14:30 +00:00
committed by GitHub
3 changed files with 66 additions and 3 deletions

View File

@@ -35,6 +35,7 @@
- iOS/Android: enable stricter concurrency/lint checks; fix Swift 6 strict concurrency issues + Android lint errors (ExifInterface, obsolete SDK check). (#662) — thanks @KristijanJovanovski.
- iOS/macOS: share `AsyncTimeout`, require explicit `bridgeStableID` on connect, and harden tool display defaults (avoids missing-resource label fallbacks).
- Telegram: serialize media-group processing to avoid missed albums under load.
- Signal: handle `dataMessage.reaction` events (signal-cli SSE) to avoid broken attachment errors. (#637) — thanks @neist.
- Docs: showcase entries for ParentPay, R2 Upload, iOS TestFlight, and Oura Health. (#650) — thanks @henrino3.
## 2026.1.9

View File

@@ -197,6 +197,45 @@ describe("monitorSignalProvider tool results", () => {
expect(updateLastRouteMock).not.toHaveBeenCalled();
});
it("ignores reaction-only messages when reactions live in dataMessage", async () => {
const abortController = new AbortController();
streamMock.mockImplementation(async ({ onEvent }) => {
const payload = {
envelope: {
sourceNumber: "+15550001111",
sourceName: "Ada",
timestamp: 1,
dataMessage: {
reaction: {
emoji: "👍",
targetAuthor: "+15550002222",
targetSentTimestamp: 2,
},
attachments: [{}],
},
},
};
await onEvent({
event: "receive",
data: JSON.stringify(payload),
});
abortController.abort();
});
await monitorSignalProvider({
autoStart: false,
baseUrl: "http://127.0.0.1:8080",
abortSignal: abortController.signal,
});
await flush();
expect(replyMock).not.toHaveBeenCalled();
expect(sendMock).not.toHaveBeenCalled();
expect(updateLastRouteMock).not.toHaveBeenCalled();
});
it("enqueues system events for reaction notifications", async () => {
config = {
...config,

View File

@@ -70,6 +70,7 @@ type SignalDataMessage = {
groupName?: string | null;
} | null;
quote?: { text?: string | null } | null;
reaction?: SignalReactionMessage | null;
};
type SignalAttachment = {
@@ -143,6 +144,20 @@ function resolveSignalReactionTargets(
return targets;
}
function isSignalReactionMessage(
reaction: SignalReactionMessage | null | undefined,
): reaction is SignalReactionMessage {
if (!reaction) return false;
const emoji = reaction.emoji?.trim();
const timestamp = reaction.targetSentTimestamp;
const hasTarget = Boolean(
reaction.targetAuthor?.trim() || reaction.targetAuthorUuid?.trim(),
);
return Boolean(
emoji && typeof timestamp === "number" && timestamp > 0 && hasTarget,
);
}
function shouldEmitSignalReactionNotification(params: {
mode?: SignalReactionNotificationMode;
account?: string | null;
@@ -403,8 +418,17 @@ export async function monitorSignalProvider(
}
const dataMessage =
envelope.dataMessage ?? envelope.editMessage?.dataMessage;
if (envelope.reactionMessage && !dataMessage) {
const reaction = envelope.reactionMessage;
const reaction = isSignalReactionMessage(envelope.reactionMessage)
? envelope.reactionMessage
: isSignalReactionMessage(dataMessage?.reaction)
? dataMessage?.reaction
: null;
const messageText = (dataMessage?.message ?? "").trim();
const quoteText = dataMessage?.quote?.text?.trim() ?? "";
const hasBodyContent =
Boolean(messageText || quoteText) ||
Boolean(!reaction && dataMessage?.attachments?.length);
if (reaction && !hasBodyContent) {
if (reaction.isRemove) return; // Ignore reaction removals
const emojiLabel = reaction.emoji?.trim() || "emoji";
const senderDisplay = formatSignalSenderDisplay(sender);
@@ -550,7 +574,6 @@ export async function monitorSignalProvider(
? isSignalSenderAllowed(sender, effectiveGroupAllow)
: true
: dmAllowed;
const messageText = (dataMessage.message ?? "").trim();
let mediaPath: string | undefined;
let mediaType: string | undefined;