fix: signal own reactions match uuid + phone (#632) (thanks @neist)

Co-authored-by: neist <1029724+neist@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-01-10 02:24:57 +01:00
parent 172fc777ed
commit f634db5c17
3 changed files with 77 additions and 16 deletions

View File

@@ -27,6 +27,7 @@
- Providers: add Microsoft Teams provider with polling, attachments, and CLI send support. (#404) — thanks @onutc - 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: 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 - 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 - 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: 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 - Discord: fix forum thread starters and cache channel lookups for thread context. (#585) — thanks @thewilloftheshadow

View File

@@ -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 () => { it("processes messages when reaction metadata is present", async () => {
const abortController = new AbortController(); const abortController = new AbortController();
replyMock.mockResolvedValue({ text: "pong" }); replyMock.mockResolvedValue({ text: "pong" });

View File

@@ -124,36 +124,42 @@ type SignalReactionTarget = {
display: string; display: string;
}; };
function resolveSignalReactionTarget( function resolveSignalReactionTargets(
reaction: SignalReactionMessage, reaction: SignalReactionMessage,
): SignalReactionTarget | null { ): SignalReactionTarget[] {
const targets: SignalReactionTarget[] = [];
const uuid = reaction.targetAuthorUuid?.trim(); const uuid = reaction.targetAuthorUuid?.trim();
if (uuid) { if (uuid) {
return { kind: "uuid", id: uuid, display: `uuid:${uuid}` }; targets.push({ kind: "uuid", id: uuid, display: `uuid:${uuid}` });
} }
const author = reaction.targetAuthor?.trim(); const author = reaction.targetAuthor?.trim();
if (!author) return null; if (author) {
const normalized = normalizeE164(author); const normalized = normalizeE164(author);
return { kind: "phone", id: normalized, display: normalized }; targets.push({ kind: "phone", id: normalized, display: normalized });
}
return targets;
} }
function shouldEmitSignalReactionNotification(params: { function shouldEmitSignalReactionNotification(params: {
mode?: SignalReactionNotificationMode; mode?: SignalReactionNotificationMode;
account?: string | null; account?: string | null;
target?: SignalReactionTarget | null; targets?: SignalReactionTarget[];
sender?: ReturnType<typeof resolveSignalSender> | null; sender?: ReturnType<typeof resolveSignalSender> | null;
allowlist?: string[]; allowlist?: string[];
}) { }) {
const { mode, account, target, sender, allowlist } = params; const { mode, account, targets, sender, allowlist } = params;
const effectiveMode = mode ?? "own"; const effectiveMode = mode ?? "own";
if (effectiveMode === "off") return false; if (effectiveMode === "off") return false;
if (effectiveMode === "own") { if (effectiveMode === "own") {
const accountId = account?.trim(); const accountId = account?.trim();
if (!accountId || !target) return false; if (!accountId || !targets || targets.length === 0) return false;
if (target.kind === "uuid") { const normalizedAccount = normalizeE164(accountId);
return accountId === target.id || accountId === `uuid:${target.id}`; return targets.some((target) => {
} if (target.kind === "uuid") {
return normalizeE164(accountId) === target.id; return accountId === target.id || accountId === `uuid:${target.id}`;
}
return normalizedAccount === target.id;
});
} }
if (effectiveMode === "allowlist") { if (effectiveMode === "allowlist") {
if (!sender || !allowlist || allowlist.length === 0) return false; if (!sender || !allowlist || allowlist.length === 0) return false;
@@ -401,11 +407,11 @@ export async function monitorSignalProvider(
const senderDisplay = formatSignalSenderDisplay(sender); const senderDisplay = formatSignalSenderDisplay(sender);
const senderName = envelope.sourceName ?? senderDisplay; const senderName = envelope.sourceName ?? senderDisplay;
logVerbose(`signal reaction: ${emojiLabel} from ${senderName}`); logVerbose(`signal reaction: ${emojiLabel} from ${senderName}`);
const target = resolveSignalReactionTarget(reaction); const targets = resolveSignalReactionTargets(reaction);
const shouldNotify = shouldEmitSignalReactionNotification({ const shouldNotify = shouldEmitSignalReactionNotification({
mode: reactionMode, mode: reactionMode,
account, account,
target, targets,
sender, sender,
allowlist: reactionAllowlist, allowlist: reactionAllowlist,
}); });
@@ -433,7 +439,7 @@ export async function monitorSignalProvider(
emojiLabel, emojiLabel,
actorLabel: senderName, actorLabel: senderName,
messageId, messageId,
targetLabel: target?.display, targetLabel: targets[0]?.display,
groupLabel, groupLabel,
}); });
const senderId = formatSignalSenderId(sender); const senderId = formatSignalSenderId(sender);