Merge pull request #616 from neist/feat/signal-reactions

feat(signal): add reaction message support
This commit is contained in:
Peter Steinberger 2026-01-09 22:40:37 +00:00 committed by GitHub
commit f4b9a00d37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 60 additions and 0 deletions

View File

@ -102,6 +102,7 @@
- Pairing: replies now include sender ids for Discord/Slack/Signal/iMessage/WhatsApp; pairing list labels them explicitly.
- Messages: default inbound/outbound prefixes from the routed agents `identity.name` when set. (#578) — thanks @p6l-richard
- Signal: accept UUID-only senders for pairing/allowlists/routing when sourceNumber is missing. (#523) — thanks @neist
- Signal: ignore reaction-only messages so they don't surface as unknown media. (#616) — thanks @neist
- Agent system prompt: avoid automatic self-updates unless explicitly requested.
- Onboarding: tighten QuickStart hint copy for configuring later.
- Onboarding: set Gemini 3 Pro as the default model for Gemini API key auth. (#489) — thanks @jonasjancarik

View File

@ -153,6 +153,42 @@ describe("monitorSignalProvider tool results", () => {
);
});
it("ignores reaction-only messages", async () => {
const abortController = new AbortController();
streamMock.mockImplementation(async ({ onEvent }) => {
const payload = {
envelope: {
sourceNumber: "+15550001111",
sourceName: "Ada",
timestamp: 1,
reactionMessage: {
emoji: "👍",
targetAuthor: "+15550002222",
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();
expect(replyMock).not.toHaveBeenCalled();
expect(sendMock).not.toHaveBeenCalled();
expect(updateLastRouteMock).not.toHaveBeenCalled();
});
it("does not resend pairing code when a request is already pending", async () => {
config = {
...config,

View File

@ -41,6 +41,15 @@ type SignalEnvelope = {
dataMessage?: SignalDataMessage | null;
editMessage?: { dataMessage?: SignalDataMessage | null } | null;
syncMessage?: unknown;
reactionMessage?: SignalReactionMessage | null;
};
type SignalReactionMessage = {
emoji?: string | null;
targetAuthor?: string | null;
targetAuthorUuid?: string | null;
targetSentTimestamp?: number | null;
isRemove?: boolean | null;
};
type SignalDataMessage = {
@ -305,8 +314,22 @@ export async function monitorSignalProvider(
const envelope = payload?.envelope;
if (!envelope) return;
if (envelope.syncMessage) return;
const dataMessage =
envelope.dataMessage ?? envelope.editMessage?.dataMessage;
if (envelope.reactionMessage && !dataMessage) {
const reaction = envelope.reactionMessage;
if (reaction.isRemove) return; // Ignore reaction removals
const emoji = reaction.emoji ?? "unknown";
const sender = resolveSignalSender(envelope);
if (!sender) return;
const senderDisplay = formatSignalSenderDisplay(sender);
const senderName = envelope.sourceName ?? senderDisplay;
logVerbose(`signal reaction: ${emoji} from ${senderName}`);
// Skip processing reactions as messages for now - just log them
// Future: could dispatch as a notification or store for context
return;
}
if (!dataMessage) return;
const sender = resolveSignalSender(envelope);