Skip to content

Commit 19245a3

Browse files
Fix Telegram stop debounce bypass
1 parent 8a3de32 commit 19245a3

7 files changed

Lines changed: 129 additions & 2 deletions

File tree

extensions/feishu/src/monitor.reaction.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,7 @@ describe("Feishu inbound debounce regressions", () => {
710710
params.onError?.(new Error("dispatch failed"), [item]);
711711
},
712712
flushKey: async () => {},
713+
cancelKey: () => false,
713714
}),
714715
}),
715716
);

extensions/feishu/src/monitor.test-mocks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export function createFeishuRuntimeMockModule(): {
3434
createInboundDebouncer: () => ({
3535
enqueue: async () => {},
3636
flushKey: async () => {},
37+
cancelKey: () => false,
3738
}),
3839
},
3940
text: {

extensions/feishu/src/test-support/lifecycle-test-support.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ function createImmediateInboundDebounce() {
8888
}
8989
},
9090
flushKey: async () => {},
91+
cancelKey: () => false,
9192
}),
9293
};
9394
}

extensions/msteams/src/monitor-handler.feedback-authz.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ function createRuntimeStub(readAllowFromStore: ReturnType<typeof vi.fn>): Plugin
4747
resolveInboundDebounceMs: () => 0,
4848
createInboundDebouncer: () => ({
4949
enqueue: async () => {},
50+
flushKey: async () => {},
51+
cancelKey: () => false,
5052
}),
5153
},
5254
pairing: {

extensions/msteams/src/monitor-handler.file-consent.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ function createRuntimeStub(stateDir?: string): PluginRuntime {
3939
resolveInboundDebounceMs: () => 0,
4040
createInboundDebouncer: () => ({
4141
enqueue: async () => {},
42+
flushKey: async () => {},
43+
cancelKey: () => false,
4244
}),
4345
},
4446
},

extensions/telegram/src/bot-handlers.runtime.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,6 +1507,7 @@ export const registerTelegramHandlers = ({
15071507
isForum: boolean;
15081508
resolvedThreadId?: number;
15091509
dmThreadId?: number;
1510+
dmPolicy: DmPolicy;
15101511
storeAllowFrom: string[];
15111512
senderId: string;
15121513
effectiveGroupAllow: NormalizedAllowFrom;
@@ -1525,6 +1526,7 @@ export const registerTelegramHandlers = ({
15251526
isForum,
15261527
resolvedThreadId,
15271528
dmThreadId,
1529+
dmPolicy,
15281530
storeAllowFrom,
15291531
senderId,
15301532
effectiveGroupAllow,
@@ -1539,6 +1541,30 @@ export const registerTelegramHandlers = ({
15391541
const messageText = getTelegramTextParts(msg).text;
15401542
const botUsername = ctx.me?.username;
15411543
const isAbortControlMessage = isAbortRequestText(messageText, { botUsername });
1544+
let abortControlAuthorized: Promise<boolean> | undefined;
1545+
const isAuthorizedAbortControlMessage = () => {
1546+
if (!isAbortControlMessage || !senderId) {
1547+
return Promise.resolve(false);
1548+
}
1549+
abortControlAuthorized ??= resolveTelegramCommandIngressAuthorization({
1550+
accountId,
1551+
cfg,
1552+
dmPolicy,
1553+
isGroup,
1554+
chatId,
1555+
resolvedThreadId,
1556+
senderId,
1557+
effectiveDmAllow,
1558+
effectiveGroupAllow,
1559+
ownerAccess: { ownerList: [], senderIsOwner: false },
1560+
eventKind: "message",
1561+
allowTextCommands: true,
1562+
hasControlCommand: true,
1563+
modeWhenAccessGroupsOff: "allow",
1564+
includeDmAllowForGroupCommands: false,
1565+
}).then((gate) => gate.authorized);
1566+
return abortControlAuthorized;
1567+
};
15421568

15431569
// Text fragment handling - Telegram splits long pastes into multiple inbound messages (~4096 chars).
15441570
// We buffer “near-limit” messages and append immediately-following parts.
@@ -1607,7 +1633,7 @@ export const registerTelegramHandlers = ({
16071633
scheduleTextFragmentFlush(entry);
16081634
return;
16091635
}
1610-
} else if (text && isAbortControlMessage) {
1636+
} else if (text && isAbortControlMessage && (await isAuthorizedAbortControlMessage())) {
16111637
const senderId = msg.from?.id != null ? String(msg.from.id) : "unknown";
16121638
const threadId = resolvedThreadId ?? dmThreadId;
16131639
const key = `text:${chatId}:${threadId ?? "main"}:${senderId}`;
@@ -1757,7 +1783,7 @@ export const registerTelegramHandlers = ({
17571783
debounceLane,
17581784
})
17591785
: null;
1760-
if (isAbortControlMessage && senderId) {
1786+
if (senderId && (await isAuthorizedAbortControlMessage())) {
17611787
for (const lane of ["default", "forward"] as const) {
17621788
inboundDebouncer.cancelKey(
17631789
buildTelegramInboundDebounceKey({
@@ -2682,6 +2708,7 @@ export const registerTelegramHandlers = ({
26822708
isForum: event.isForum,
26832709
resolvedThreadId,
26842710
dmThreadId,
2711+
dmPolicy,
26852712
storeAllowFrom,
26862713
senderId: event.senderId,
26872714
effectiveGroupAllow,

extensions/telegram/src/bot.create-telegram-bot.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,99 @@ describe("createTelegramBot", () => {
921921
}
922922
});
923923

924+
it("does not let unauthorized group stop cancel pending same-sender inbound debounce", async () => {
925+
const DEBOUNCE_MS = 4321;
926+
loadConfig.mockReturnValue({
927+
agents: {
928+
defaults: {
929+
envelopeTimezone: "utc",
930+
},
931+
},
932+
messages: {
933+
inbound: {
934+
debounceMs: DEBOUNCE_MS,
935+
},
936+
},
937+
channels: {
938+
telegram: {
939+
dmPolicy: "pairing",
940+
groupPolicy: "open",
941+
groups: { "*": { requireMention: false } },
942+
},
943+
},
944+
});
945+
946+
installPerKeySequentializer();
947+
948+
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
949+
const startedBodies: string[] = [];
950+
replySpy.mockImplementation(async (ctx: MsgContext, opts?: GetReplyOptions) => {
951+
await opts?.onReplyStart?.();
952+
const body = ctx.Body ?? "";
953+
startedBodies.push(body);
954+
return { text: `reply:${body}` };
955+
});
956+
957+
const extractLatestDebounceFlush = () => {
958+
const debounceCallIndex = setTimeoutSpy.mock.calls.findLastIndex(
959+
(call) => call[1] === DEBOUNCE_MS,
960+
);
961+
expect(debounceCallIndex).toBeGreaterThanOrEqual(0);
962+
clearTimeout(
963+
setTimeoutSpy.mock.results[debounceCallIndex]?.value as ReturnType<typeof setTimeout>,
964+
);
965+
return setTimeoutSpy.mock.calls[debounceCallIndex]?.[0] as (() => Promise<void>) | undefined;
966+
};
967+
968+
try {
969+
createTelegramBot({ token: "tok" });
970+
const messageHandler = getOnHandler("message") as (
971+
ctx: TelegramMiddlewareTestContext,
972+
) => Promise<void>;
973+
974+
await runTelegramMiddlewareChain({
975+
ctx: {
976+
update: { update_id: 104 },
977+
message: {
978+
chat: { id: -1007, type: "supergroup", title: "OpenClaw Ops" },
979+
text: "first",
980+
date: 1736380804,
981+
message_id: 104,
982+
from: { id: 42, first_name: "Ada" },
983+
},
984+
me: { username: "openclaw_bot" },
985+
getFile: async () => ({}),
986+
},
987+
finalHandler: messageHandler,
988+
});
989+
990+
const flushFirst = extractLatestDebounceFlush();
991+
992+
await runTelegramMiddlewareChain({
993+
ctx: {
994+
update: { update_id: 105 },
995+
message: {
996+
chat: { id: -1007, type: "supergroup", title: "OpenClaw Ops" },
997+
text: "stop",
998+
date: 1736380805,
999+
message_id: 105,
1000+
from: { id: 42, first_name: "Ada" },
1001+
},
1002+
me: { username: "openclaw_bot" },
1003+
getFile: async () => ({}),
1004+
},
1005+
finalHandler: messageHandler,
1006+
});
1007+
1008+
await flushFirst?.();
1009+
await vi.waitFor(() => {
1010+
expect(startedBodies.some((body) => body.includes("first"))).toBe(true);
1011+
});
1012+
} finally {
1013+
setTimeoutSpy.mockRestore();
1014+
}
1015+
});
1016+
9241017
it("routes callback_query payloads as messages and answers callbacks", async () => {
9251018
createTelegramBot({ token: "tok" });
9261019
const callbackHandler = requireValue(

0 commit comments

Comments
 (0)