mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
Fix Telegram stop debounce bypass
This commit is contained in:
@@ -710,6 +710,7 @@ describe("Feishu inbound debounce regressions", () => {
|
||||
params.onError?.(new Error("dispatch failed"), [item]);
|
||||
},
|
||||
flushKey: async () => {},
|
||||
cancelKey: () => false,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -34,6 +34,7 @@ export function createFeishuRuntimeMockModule(): {
|
||||
createInboundDebouncer: () => ({
|
||||
enqueue: async () => {},
|
||||
flushKey: async () => {},
|
||||
cancelKey: () => false,
|
||||
}),
|
||||
},
|
||||
text: {
|
||||
|
||||
@@ -88,6 +88,7 @@ function createImmediateInboundDebounce() {
|
||||
}
|
||||
},
|
||||
flushKey: async () => {},
|
||||
cancelKey: () => false,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ function createRuntimeStub(readAllowFromStore: ReturnType<typeof vi.fn>): Plugin
|
||||
resolveInboundDebounceMs: () => 0,
|
||||
createInboundDebouncer: () => ({
|
||||
enqueue: async () => {},
|
||||
flushKey: async () => {},
|
||||
cancelKey: () => false,
|
||||
}),
|
||||
},
|
||||
pairing: {
|
||||
|
||||
@@ -39,6 +39,8 @@ function createRuntimeStub(stateDir?: string): PluginRuntime {
|
||||
resolveInboundDebounceMs: () => 0,
|
||||
createInboundDebouncer: () => ({
|
||||
enqueue: async () => {},
|
||||
flushKey: async () => {},
|
||||
cancelKey: () => false,
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1507,6 +1507,7 @@ export const registerTelegramHandlers = ({
|
||||
isForum: boolean;
|
||||
resolvedThreadId?: number;
|
||||
dmThreadId?: number;
|
||||
dmPolicy: DmPolicy;
|
||||
storeAllowFrom: string[];
|
||||
senderId: string;
|
||||
effectiveGroupAllow: NormalizedAllowFrom;
|
||||
@@ -1525,6 +1526,7 @@ export const registerTelegramHandlers = ({
|
||||
isForum,
|
||||
resolvedThreadId,
|
||||
dmThreadId,
|
||||
dmPolicy,
|
||||
storeAllowFrom,
|
||||
senderId,
|
||||
effectiveGroupAllow,
|
||||
@@ -1539,6 +1541,30 @@ export const registerTelegramHandlers = ({
|
||||
const messageText = getTelegramTextParts(msg).text;
|
||||
const botUsername = ctx.me?.username;
|
||||
const isAbortControlMessage = isAbortRequestText(messageText, { botUsername });
|
||||
let abortControlAuthorized: Promise<boolean> | undefined;
|
||||
const isAuthorizedAbortControlMessage = () => {
|
||||
if (!isAbortControlMessage || !senderId) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
abortControlAuthorized ??= resolveTelegramCommandIngressAuthorization({
|
||||
accountId,
|
||||
cfg,
|
||||
dmPolicy,
|
||||
isGroup,
|
||||
chatId,
|
||||
resolvedThreadId,
|
||||
senderId,
|
||||
effectiveDmAllow,
|
||||
effectiveGroupAllow,
|
||||
ownerAccess: { ownerList: [], senderIsOwner: false },
|
||||
eventKind: "message",
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: true,
|
||||
modeWhenAccessGroupsOff: "allow",
|
||||
includeDmAllowForGroupCommands: false,
|
||||
}).then((gate) => gate.authorized);
|
||||
return abortControlAuthorized;
|
||||
};
|
||||
|
||||
// Text fragment handling - Telegram splits long pastes into multiple inbound messages (~4096 chars).
|
||||
// We buffer “near-limit” messages and append immediately-following parts.
|
||||
@@ -1607,7 +1633,7 @@ export const registerTelegramHandlers = ({
|
||||
scheduleTextFragmentFlush(entry);
|
||||
return;
|
||||
}
|
||||
} else if (text && isAbortControlMessage) {
|
||||
} else if (text && isAbortControlMessage && (await isAuthorizedAbortControlMessage())) {
|
||||
const senderId = msg.from?.id != null ? String(msg.from.id) : "unknown";
|
||||
const threadId = resolvedThreadId ?? dmThreadId;
|
||||
const key = `text:${chatId}:${threadId ?? "main"}:${senderId}`;
|
||||
@@ -1757,7 +1783,7 @@ export const registerTelegramHandlers = ({
|
||||
debounceLane,
|
||||
})
|
||||
: null;
|
||||
if (isAbortControlMessage && senderId) {
|
||||
if (senderId && (await isAuthorizedAbortControlMessage())) {
|
||||
for (const lane of ["default", "forward"] as const) {
|
||||
inboundDebouncer.cancelKey(
|
||||
buildTelegramInboundDebounceKey({
|
||||
@@ -2682,6 +2708,7 @@ export const registerTelegramHandlers = ({
|
||||
isForum: event.isForum,
|
||||
resolvedThreadId,
|
||||
dmThreadId,
|
||||
dmPolicy,
|
||||
storeAllowFrom,
|
||||
senderId: event.senderId,
|
||||
effectiveGroupAllow,
|
||||
|
||||
@@ -921,6 +921,99 @@ describe("createTelegramBot", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("does not let unauthorized group stop cancel pending same-sender inbound debounce", async () => {
|
||||
const DEBOUNCE_MS = 4321;
|
||||
loadConfig.mockReturnValue({
|
||||
agents: {
|
||||
defaults: {
|
||||
envelopeTimezone: "utc",
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
inbound: {
|
||||
debounceMs: DEBOUNCE_MS,
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
dmPolicy: "pairing",
|
||||
groupPolicy: "open",
|
||||
groups: { "*": { requireMention: false } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
installPerKeySequentializer();
|
||||
|
||||
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
|
||||
const startedBodies: string[] = [];
|
||||
replySpy.mockImplementation(async (ctx: MsgContext, opts?: GetReplyOptions) => {
|
||||
await opts?.onReplyStart?.();
|
||||
const body = ctx.Body ?? "";
|
||||
startedBodies.push(body);
|
||||
return { text: `reply:${body}` };
|
||||
});
|
||||
|
||||
const extractLatestDebounceFlush = () => {
|
||||
const debounceCallIndex = setTimeoutSpy.mock.calls.findLastIndex(
|
||||
(call) => call[1] === DEBOUNCE_MS,
|
||||
);
|
||||
expect(debounceCallIndex).toBeGreaterThanOrEqual(0);
|
||||
clearTimeout(
|
||||
setTimeoutSpy.mock.results[debounceCallIndex]?.value as ReturnType<typeof setTimeout>,
|
||||
);
|
||||
return setTimeoutSpy.mock.calls[debounceCallIndex]?.[0] as (() => Promise<void>) | undefined;
|
||||
};
|
||||
|
||||
try {
|
||||
createTelegramBot({ token: "tok" });
|
||||
const messageHandler = getOnHandler("message") as (
|
||||
ctx: TelegramMiddlewareTestContext,
|
||||
) => Promise<void>;
|
||||
|
||||
await runTelegramMiddlewareChain({
|
||||
ctx: {
|
||||
update: { update_id: 104 },
|
||||
message: {
|
||||
chat: { id: -1007, type: "supergroup", title: "OpenClaw Ops" },
|
||||
text: "first",
|
||||
date: 1736380804,
|
||||
message_id: 104,
|
||||
from: { id: 42, first_name: "Ada" },
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({}),
|
||||
},
|
||||
finalHandler: messageHandler,
|
||||
});
|
||||
|
||||
const flushFirst = extractLatestDebounceFlush();
|
||||
|
||||
await runTelegramMiddlewareChain({
|
||||
ctx: {
|
||||
update: { update_id: 105 },
|
||||
message: {
|
||||
chat: { id: -1007, type: "supergroup", title: "OpenClaw Ops" },
|
||||
text: "stop",
|
||||
date: 1736380805,
|
||||
message_id: 105,
|
||||
from: { id: 42, first_name: "Ada" },
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({}),
|
||||
},
|
||||
finalHandler: messageHandler,
|
||||
});
|
||||
|
||||
await flushFirst?.();
|
||||
await vi.waitFor(() => {
|
||||
expect(startedBodies.some((body) => body.includes("first"))).toBe(true);
|
||||
});
|
||||
} finally {
|
||||
setTimeoutSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("routes callback_query payloads as messages and answers callbacks", async () => {
|
||||
createTelegramBot({ token: "tok" });
|
||||
const callbackHandler = requireValue(
|
||||
|
||||
Reference in New Issue
Block a user