From f634db5c17678f8f8a58dca96b88e6550d283450 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 10 Jan 2026 02:24:57 +0100 Subject: [PATCH] fix: signal own reactions match uuid + phone (#632) (thanks @neist) Co-authored-by: neist <1029724+neist@users.noreply.github.com> --- CHANGELOG.md | 1 + src/signal/monitor.tool-result.test.ts | 54 ++++++++++++++++++++++++++ src/signal/monitor.ts | 38 ++++++++++-------- 3 files changed, 77 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9b120b0c..46b5eb9f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - Providers: add Microsoft Teams provider with polling, attachments, and CLI send support. (#404) — thanks @onutc - Slack: honor reply tags + replyToMode while keeping threaded replies in-thread. (#574) — thanks @bolismauro - Slack: configurable reply threading (`slack.replyToMode`) + proper mrkdwn formatting for outbound messages. (#464) — thanks @austinm911 +- Signal: match own-mode reactions when target includes uuid + phone. (#632) — thanks @neist - Providers: remove ack reactions after reply on Discord/Slack/Telegram. (#633) — thanks @levifig - Discord: avoid category parent overrides for channel allowlists and refactor thread context helpers. (#588) — thanks @steipete - Discord: fix forum thread starters and cache channel lookups for thread context. (#585) — thanks @thewilloftheshadow diff --git a/src/signal/monitor.tool-result.test.ts b/src/signal/monitor.tool-result.test.ts index 0ba6eb130..b9a6a24bf 100644 --- a/src/signal/monitor.tool-result.test.ts +++ b/src/signal/monitor.tool-result.test.ts @@ -249,6 +249,60 @@ describe("monitorSignalProvider tool results", () => { ); }); + it("notifies on own reactions when target includes uuid + phone", async () => { + config = { + ...config, + signal: { + autoStart: false, + dmPolicy: "open", + allowFrom: ["*"], + account: "+15550002222", + reactionNotifications: "own", + }, + }; + const abortController = new AbortController(); + + streamMock.mockImplementation(async ({ onEvent }) => { + const payload = { + envelope: { + sourceNumber: "+15550001111", + sourceName: "Ada", + timestamp: 1, + reactionMessage: { + emoji: "✅", + targetAuthor: "+15550002222", + targetAuthorUuid: "123e4567-e89b-12d3-a456-426614174000", + targetSentTimestamp: 2, + }, + }, + }; + 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(); + + const route = resolveAgentRoute({ + cfg: config as ClawdbotConfig, + provider: "signal", + accountId: "default", + peer: { kind: "dm", id: normalizeE164("+15550001111") }, + }); + const events = peekSystemEvents(route.sessionKey); + expect(events.some((text) => text.includes("Signal reaction added"))).toBe( + true, + ); + }); + it("processes messages when reaction metadata is present", async () => { const abortController = new AbortController(); replyMock.mockResolvedValue({ text: "pong" }); diff --git a/src/signal/monitor.ts b/src/signal/monitor.ts index a5fab8022..30e34964a 100644 --- a/src/signal/monitor.ts +++ b/src/signal/monitor.ts @@ -124,36 +124,42 @@ type SignalReactionTarget = { display: string; }; -function resolveSignalReactionTarget( +function resolveSignalReactionTargets( reaction: SignalReactionMessage, -): SignalReactionTarget | null { +): SignalReactionTarget[] { + const targets: SignalReactionTarget[] = []; const uuid = reaction.targetAuthorUuid?.trim(); if (uuid) { - return { kind: "uuid", id: uuid, display: `uuid:${uuid}` }; + targets.push({ kind: "uuid", id: uuid, display: `uuid:${uuid}` }); } const author = reaction.targetAuthor?.trim(); - if (!author) return null; - const normalized = normalizeE164(author); - return { kind: "phone", id: normalized, display: normalized }; + if (author) { + const normalized = normalizeE164(author); + targets.push({ kind: "phone", id: normalized, display: normalized }); + } + return targets; } function shouldEmitSignalReactionNotification(params: { mode?: SignalReactionNotificationMode; account?: string | null; - target?: SignalReactionTarget | null; + targets?: SignalReactionTarget[]; sender?: ReturnType | null; allowlist?: string[]; }) { - const { mode, account, target, sender, allowlist } = params; + const { mode, account, targets, sender, allowlist } = params; const effectiveMode = mode ?? "own"; if (effectiveMode === "off") return false; if (effectiveMode === "own") { const accountId = account?.trim(); - if (!accountId || !target) return false; - if (target.kind === "uuid") { - return accountId === target.id || accountId === `uuid:${target.id}`; - } - return normalizeE164(accountId) === target.id; + if (!accountId || !targets || targets.length === 0) return false; + const normalizedAccount = normalizeE164(accountId); + return targets.some((target) => { + if (target.kind === "uuid") { + return accountId === target.id || accountId === `uuid:${target.id}`; + } + return normalizedAccount === target.id; + }); } if (effectiveMode === "allowlist") { if (!sender || !allowlist || allowlist.length === 0) return false; @@ -401,11 +407,11 @@ export async function monitorSignalProvider( const senderDisplay = formatSignalSenderDisplay(sender); const senderName = envelope.sourceName ?? senderDisplay; logVerbose(`signal reaction: ${emojiLabel} from ${senderName}`); - const target = resolveSignalReactionTarget(reaction); + const targets = resolveSignalReactionTargets(reaction); const shouldNotify = shouldEmitSignalReactionNotification({ mode: reactionMode, account, - target, + targets, sender, allowlist: reactionAllowlist, }); @@ -433,7 +439,7 @@ export async function monitorSignalProvider( emojiLabel, actorLabel: senderName, messageId, - targetLabel: target?.display, + targetLabel: targets[0]?.display, groupLabel, }); const senderId = formatSignalSenderId(sender);