fix(signal): ignore system messages (expiration timer, group permission changes)
signal-cli sends dataMessage envelopes for system events (disappearing message timer changes, group permission updates) with no user text. These fell through to the agent, which responded with a confused 'I got a media message' reply. Add isExpirationUpdate and groupV2Change fields to SignalDataMessage type, and early-return on these system-only events before dispatch. Fixes #27615, Fixes #30981
This commit is contained in:
parent
e5282e6bda
commit
2685b11e95
@ -565,6 +565,21 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
||||
const groupName = dataMessage.groupInfo?.groupName ?? undefined;
|
||||
const isGroup = Boolean(groupId);
|
||||
|
||||
// Skip signal-cli system messages that have no user-visible content
|
||||
const isTimerUpdate =
|
||||
!messageText &&
|
||||
!quoteText &&
|
||||
!dataMessage.attachments?.length &&
|
||||
(dataMessage.isExpirationUpdate === true ||
|
||||
(typeof dataMessage.expiresInSeconds === "number" && dataMessage.expiresInSeconds > 0));
|
||||
const isGroupV2Change = Boolean(dataMessage.groupV2Change);
|
||||
if (isTimerUpdate || isGroupV2Change) {
|
||||
logVerbose(
|
||||
`signal: skipping system message (isTimerUpdate=${isTimerUpdate}, isGroupV2Change=${isGroupV2Change})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isGroup) {
|
||||
const allowedDirectMessage = await handleSignalDirectMessageAccess({
|
||||
dmPolicy: deps.dmPolicy,
|
||||
|
||||
@ -39,6 +39,9 @@ export type SignalDataMessage = {
|
||||
} | null;
|
||||
quote?: { text?: string | null } | null;
|
||||
reaction?: SignalReactionMessage | null;
|
||||
expiresInSeconds?: number | null;
|
||||
groupV2Change?: Record<string, unknown> | null;
|
||||
isExpirationUpdate?: boolean | null;
|
||||
};
|
||||
|
||||
export type SignalReactionMessage = {
|
||||
|
||||
128
src/signal/monitor/event-handler.system-messages.test.ts
Normal file
128
src/signal/monitor/event-handler.system-messages.test.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createSignalEventHandler } from "./event-handler.js";
|
||||
import {
|
||||
createBaseSignalEventHandlerDeps,
|
||||
createSignalReceiveEvent,
|
||||
} from "./event-handler.test-harness.js";
|
||||
|
||||
const { sendTypingMock, sendReadReceiptMock, dispatchInboundMessageMock } = vi.hoisted(() => ({
|
||||
sendTypingMock: vi.fn(),
|
||||
sendReadReceiptMock: vi.fn(),
|
||||
dispatchInboundMessageMock: vi.fn(async () => ({
|
||||
queuedFinal: false,
|
||||
counts: { tool: 0, block: 0, final: 0 },
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("../send.js", () => ({
|
||||
sendMessageSignal: vi.fn(),
|
||||
sendTypingSignal: sendTypingMock,
|
||||
sendReadReceiptSignal: sendReadReceiptMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../auto-reply/dispatch.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../auto-reply/dispatch.js")>();
|
||||
return {
|
||||
...actual,
|
||||
dispatchInboundMessage: dispatchInboundMessageMock,
|
||||
dispatchInboundMessageWithDispatcher: dispatchInboundMessageMock,
|
||||
dispatchInboundMessageWithBufferedDispatcher: dispatchInboundMessageMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../pairing/pairing-store.js", () => ({
|
||||
readChannelAllowFromStore: vi.fn().mockResolvedValue([]),
|
||||
upsertChannelPairingRequest: vi.fn(),
|
||||
}));
|
||||
|
||||
function makeDeps() {
|
||||
return createBaseSignalEventHandlerDeps({
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
cfg: { messages: { inbound: { debounceMs: 0 } } } as any,
|
||||
historyLimit: 0,
|
||||
});
|
||||
}
|
||||
|
||||
describe("signal system message filtering", () => {
|
||||
beforeEach(() => {
|
||||
sendTypingMock.mockReset().mockResolvedValue(true);
|
||||
sendReadReceiptMock.mockReset().mockResolvedValue(true);
|
||||
dispatchInboundMessageMock.mockClear();
|
||||
});
|
||||
|
||||
it("filters expiration timer update (expiresInSeconds, no text)", async () => {
|
||||
const handler = createSignalEventHandler(makeDeps());
|
||||
await handler(
|
||||
createSignalReceiveEvent({
|
||||
dataMessage: {
|
||||
message: null,
|
||||
attachments: [],
|
||||
expiresInSeconds: 604800,
|
||||
groupInfo: { groupId: "g1", groupName: "Test Group" },
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(dispatchInboundMessageMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("filters expiration timer update with isExpirationUpdate flag", async () => {
|
||||
const handler = createSignalEventHandler(makeDeps());
|
||||
await handler(
|
||||
createSignalReceiveEvent({
|
||||
dataMessage: {
|
||||
message: null,
|
||||
attachments: [],
|
||||
isExpirationUpdate: true,
|
||||
expiresInSeconds: 604800,
|
||||
groupInfo: { groupId: "g1", groupName: "Test Group" },
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(dispatchInboundMessageMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("filters groupV2Change messages", async () => {
|
||||
const handler = createSignalEventHandler(makeDeps());
|
||||
await handler(
|
||||
createSignalReceiveEvent({
|
||||
dataMessage: {
|
||||
message: null,
|
||||
attachments: [],
|
||||
groupV2Change: { editor: "+15550001111", changes: [] },
|
||||
groupInfo: { groupId: "g1", groupName: "Test Group" },
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(dispatchInboundMessageMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does NOT filter normal message with expiresInSeconds=0", async () => {
|
||||
const handler = createSignalEventHandler(makeDeps());
|
||||
await handler(
|
||||
createSignalReceiveEvent({
|
||||
dataMessage: {
|
||||
message: "hello",
|
||||
attachments: [],
|
||||
expiresInSeconds: 0,
|
||||
groupInfo: { groupId: "g1", groupName: "Test Group" },
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(dispatchInboundMessageMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does NOT filter message with text even if expiresInSeconds > 0", async () => {
|
||||
const handler = createSignalEventHandler(makeDeps());
|
||||
await handler(
|
||||
createSignalReceiveEvent({
|
||||
dataMessage: {
|
||||
message: "hello with timer",
|
||||
attachments: [],
|
||||
expiresInSeconds: 604800,
|
||||
groupInfo: { groupId: "g1", groupName: "Test Group" },
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(dispatchInboundMessageMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user