mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
Compute plugin callback authorization dynamically [AI] (#78866)
* fix: compute plugin callback command authorization * addressing codex review * addressing ci * addressing ci * docs: add changelog entry for PR merge
This commit is contained in:
committed by
GitHub
parent
be33b68fd4
commit
c65f3bc70e
@@ -147,6 +147,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Compute plugin callback authorization dynamically [AI]. (#78866) Thanks @pgondhi987.
|
||||
- fix(active-memory): require admin scope for global toggles [AI]. (#78863) Thanks @pgondhi987.
|
||||
- Honor owner enforcement for native commands [AI]. (#78864) Thanks @pgondhi987.
|
||||
- Config/BlueBubbles: remove the duplicate core-owned BlueBubbles config schema while preserving plugin-owned `dmPolicy` allowFrom validation for channel and account configs. Fixes #69238. Thanks @omarshahine.
|
||||
|
||||
@@ -2,6 +2,10 @@ import type { SlackActionMiddlewareArgs } from "@slack/bolt";
|
||||
import type { Block, KnownBlock } from "@slack/web-api";
|
||||
import { resolveApprovalOverGateway } from "openclaw/plugin-sdk/approval-gateway-runtime";
|
||||
import { parseExecApprovalCommandText } from "openclaw/plugin-sdk/approval-reply-runtime";
|
||||
import {
|
||||
resolveCommandAuthorization,
|
||||
resolveCommandAuthorizedFromAuthorizers,
|
||||
} from "openclaw/plugin-sdk/command-auth-native";
|
||||
import { enqueueSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import {
|
||||
@@ -13,7 +17,9 @@ import {
|
||||
SLACK_REPLY_BUTTON_ACTION_ID,
|
||||
SLACK_REPLY_SELECT_ACTION_ID,
|
||||
} from "../../reply-action-ids.js";
|
||||
import { authorizeSlackSystemEventSender } from "../auth.js";
|
||||
import { resolveSlackAllowListMatch, resolveSlackUserAllowed } from "../allow-list.js";
|
||||
import { authorizeSlackSystemEventSender, resolveSlackEffectiveAllowFrom } from "../auth.js";
|
||||
import { resolveSlackChannelConfig } from "../channel-config.js";
|
||||
import type { SlackMonitorContext } from "../context.js";
|
||||
import {
|
||||
buildPluginBindingResolvedText,
|
||||
@@ -673,6 +679,79 @@ async function dispatchSlackPluginInteraction(params: {
|
||||
return pluginResult.matched && pluginResult.handled;
|
||||
}
|
||||
|
||||
async function resolveSlackBlockActionCommandAuthorized(params: {
|
||||
ctx: SlackMonitorContext;
|
||||
parsed: ParsedSlackBlockAction;
|
||||
auth: { channelType?: "im" | "mpim" | "channel" | "group"; channelName?: string };
|
||||
}): Promise<boolean> {
|
||||
const commandsAllowFrom = params.ctx.cfg.commands?.allowFrom;
|
||||
const commandsAllowFromConfigured =
|
||||
commandsAllowFrom != null &&
|
||||
typeof commandsAllowFrom === "object" &&
|
||||
(Array.isArray(commandsAllowFrom.slack) || Array.isArray(commandsAllowFrom["*"]));
|
||||
if (commandsAllowFromConfigured) {
|
||||
return resolveCommandAuthorization({
|
||||
ctx: {
|
||||
Provider: "slack",
|
||||
Surface: "slack",
|
||||
OriginatingChannel: "slack",
|
||||
AccountId: params.ctx.accountId,
|
||||
ChatType: params.auth.channelType === "im" ? "direct" : "group",
|
||||
From: params.parsed.channelId ? `slack:${params.parsed.channelId}` : "slack",
|
||||
SenderId: params.parsed.userId,
|
||||
},
|
||||
cfg: params.ctx.cfg,
|
||||
commandAuthorized: false,
|
||||
}).isAuthorizedSender;
|
||||
}
|
||||
|
||||
const isDirectMessage = params.auth.channelType === "im";
|
||||
const isRoom = params.auth.channelType === "channel" || params.auth.channelType === "group";
|
||||
const { allowFromLower } = await resolveSlackEffectiveAllowFrom(params.ctx, {
|
||||
includePairingStore: isDirectMessage,
|
||||
});
|
||||
const sender = await params.ctx.resolveUserName(params.parsed.userId).catch(() => undefined);
|
||||
const senderName = sender?.name;
|
||||
const ownerAllowed = resolveSlackAllowListMatch({
|
||||
allowList: allowFromLower,
|
||||
id: params.parsed.userId,
|
||||
name: senderName,
|
||||
allowNameMatching: params.ctx.allowNameMatching,
|
||||
}).allowed;
|
||||
|
||||
let channelUsersAllowlistConfigured = false;
|
||||
let channelUserAllowed = false;
|
||||
if (isRoom && params.parsed.channelId) {
|
||||
const channelConfig = resolveSlackChannelConfig({
|
||||
channelId: params.parsed.channelId,
|
||||
channelName: params.auth.channelName,
|
||||
channels: params.ctx.channelsConfig,
|
||||
channelKeys: params.ctx.channelsConfigKeys,
|
||||
defaultRequireMention: params.ctx.defaultRequireMention,
|
||||
allowNameMatching: params.ctx.allowNameMatching,
|
||||
});
|
||||
channelUsersAllowlistConfigured =
|
||||
Array.isArray(channelConfig?.users) && channelConfig.users.length > 0;
|
||||
channelUserAllowed = channelUsersAllowlistConfigured
|
||||
? resolveSlackUserAllowed({
|
||||
allowList: channelConfig?.users,
|
||||
userId: params.parsed.userId,
|
||||
userName: senderName,
|
||||
allowNameMatching: params.ctx.allowNameMatching,
|
||||
})
|
||||
: false;
|
||||
}
|
||||
|
||||
return resolveCommandAuthorizedFromAuthorizers({
|
||||
useAccessGroups: params.ctx.useAccessGroups,
|
||||
authorizers: [
|
||||
{ configured: allowFromLower.length > 0, allowed: ownerAllowed },
|
||||
{ configured: channelUsersAllowlistConfigured, allowed: channelUserAllowed },
|
||||
],
|
||||
modeWhenAccessGroupsOff: "configured",
|
||||
});
|
||||
}
|
||||
|
||||
function enqueueSlackBlockActionEvent(params: {
|
||||
ctx: SlackMonitorContext;
|
||||
parsed: ParsedSlackBlockAction;
|
||||
@@ -844,12 +923,17 @@ async function handleSlackBlockAction(params: {
|
||||
return;
|
||||
}
|
||||
} else if (pluginInteractionData) {
|
||||
const isAuthorizedSender = await resolveSlackBlockActionCommandAuthorized({
|
||||
ctx: params.ctx,
|
||||
parsed,
|
||||
auth,
|
||||
});
|
||||
const handled = await dispatchSlackPluginInteraction({
|
||||
ctx: params.ctx,
|
||||
parsed,
|
||||
pluginInteractionData,
|
||||
auth: {
|
||||
isAuthorizedSender: true,
|
||||
isAuthorizedSender,
|
||||
},
|
||||
respond,
|
||||
});
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const enqueueSystemEventMock = vi.hoisted(() => vi.fn());
|
||||
type DispatchPluginInteractiveHandlerResult = {
|
||||
matched: boolean;
|
||||
handled: boolean;
|
||||
duplicate: boolean;
|
||||
};
|
||||
const dispatchPluginInteractiveHandlerMock = vi.hoisted(() =>
|
||||
vi.fn(async () => ({
|
||||
vi.fn<(arg: unknown) => Promise<DispatchPluginInteractiveHandlerResult>>(async () => ({
|
||||
matched: false,
|
||||
handled: false,
|
||||
duplicate: false,
|
||||
@@ -171,6 +176,7 @@ function createContext(overrides?: {
|
||||
dmPolicy?: "open" | "allowlist" | "pairing" | "disabled";
|
||||
allowFrom?: string[];
|
||||
allowNameMatching?: boolean;
|
||||
useAccessGroups?: boolean;
|
||||
channelsConfig?: Record<string, { users?: string[] }>;
|
||||
cfg?: Record<string, unknown>;
|
||||
shouldDropMismatchedSlackEvent?: (body: unknown) => boolean;
|
||||
@@ -249,6 +255,7 @@ function createContext(overrides?: {
|
||||
dmPolicy: overrides?.dmPolicy ?? ("open" as const),
|
||||
allowFrom: overrides?.allowFrom ?? ["*"],
|
||||
allowNameMatching: overrides?.allowNameMatching ?? false,
|
||||
useAccessGroups: overrides?.useAccessGroups ?? true,
|
||||
channelsConfig: overrides?.channelsConfig ?? {},
|
||||
channelsConfigKeys: Object.keys(overrides?.channelsConfig ?? {}),
|
||||
defaultRequireMention: true,
|
||||
@@ -459,6 +466,9 @@ describe("registerSlackInteractionEvents", () => {
|
||||
conversationId: "C1",
|
||||
interactionId: "U123:C1:100.200:123.trigger:codex:approve:thread-1",
|
||||
threadId: "100.100",
|
||||
auth: expect.objectContaining({
|
||||
isAuthorizedSender: true,
|
||||
}),
|
||||
interaction: expect.objectContaining({
|
||||
actionId: "codex",
|
||||
value: "approve:thread-1",
|
||||
@@ -472,6 +482,150 @@ describe("registerSlackInteractionEvents", () => {
|
||||
expect(app.client.chat.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes false command auth to Slack plugin interactions for non-allowlisted senders", async () => {
|
||||
dispatchPluginInteractiveHandlerMock.mockResolvedValueOnce({
|
||||
matched: true,
|
||||
handled: true,
|
||||
duplicate: false,
|
||||
});
|
||||
const { ctx, getHandler } = createContext({
|
||||
cfg: {
|
||||
commands: {
|
||||
allowFrom: {
|
||||
slack: ["U_OWNER"],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
registerSlackInteractionEvents({ ctx: ctx as never });
|
||||
|
||||
const handler = getHandler();
|
||||
expect(handler).toBeTruthy();
|
||||
|
||||
const ack = vi.fn().mockResolvedValue(undefined);
|
||||
await handler!({
|
||||
ack,
|
||||
body: {
|
||||
user: { id: "U_ALLOWED" },
|
||||
channel: { id: "C1" },
|
||||
container: { channel_id: "C1", message_ts: "100.200", thread_ts: "100.100" },
|
||||
message: {
|
||||
ts: "100.200",
|
||||
text: "fallback",
|
||||
blocks: [
|
||||
{
|
||||
type: "actions",
|
||||
block_id: "codex_actions",
|
||||
elements: [{ type: "button", action_id: "codex" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
action: {
|
||||
type: "button",
|
||||
action_id: "codex",
|
||||
block_id: "codex_actions",
|
||||
value: "approve:thread-1",
|
||||
},
|
||||
});
|
||||
|
||||
const dispatchCall = dispatchPluginInteractiveHandlerMock.mock.calls[0]?.[0] as
|
||||
| {
|
||||
invoke?: (params: {
|
||||
registration: { handler: (ctx: unknown) => unknown };
|
||||
namespace: string;
|
||||
payload: string;
|
||||
}) => Promise<unknown>;
|
||||
}
|
||||
| undefined;
|
||||
const registrationHandler = vi.fn();
|
||||
await dispatchCall?.invoke?.({
|
||||
registration: { handler: registrationHandler },
|
||||
namespace: "codex",
|
||||
payload: "approve:thread-1",
|
||||
});
|
||||
|
||||
expect(registrationHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
auth: expect.objectContaining({
|
||||
isAuthorizedSender: false,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes true command auth to Slack plugin interactions for allowlisted senders", async () => {
|
||||
dispatchPluginInteractiveHandlerMock.mockResolvedValueOnce({
|
||||
matched: true,
|
||||
handled: true,
|
||||
duplicate: false,
|
||||
});
|
||||
const { ctx, getHandler } = createContext({
|
||||
cfg: {
|
||||
commands: {
|
||||
allowFrom: {
|
||||
slack: ["U_OWNER"],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
registerSlackInteractionEvents({ ctx: ctx as never });
|
||||
|
||||
const handler = getHandler();
|
||||
expect(handler).toBeTruthy();
|
||||
|
||||
const ack = vi.fn().mockResolvedValue(undefined);
|
||||
await handler!({
|
||||
ack,
|
||||
body: {
|
||||
user: { id: "U_OWNER" },
|
||||
channel: { id: "C1" },
|
||||
container: { channel_id: "C1", message_ts: "100.200", thread_ts: "100.100" },
|
||||
message: {
|
||||
ts: "100.200",
|
||||
text: "fallback",
|
||||
blocks: [
|
||||
{
|
||||
type: "actions",
|
||||
block_id: "codex_actions",
|
||||
elements: [{ type: "button", action_id: "codex" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
action: {
|
||||
type: "button",
|
||||
action_id: "codex",
|
||||
block_id: "codex_actions",
|
||||
value: "approve:thread-1",
|
||||
},
|
||||
});
|
||||
|
||||
const dispatchCall = dispatchPluginInteractiveHandlerMock.mock.calls[0]?.[0] as
|
||||
| {
|
||||
invoke?: (params: {
|
||||
registration: { handler: (ctx: unknown) => unknown };
|
||||
namespace: string;
|
||||
payload: string;
|
||||
}) => Promise<unknown>;
|
||||
}
|
||||
| undefined;
|
||||
const registrationHandler = vi.fn();
|
||||
await dispatchCall?.invoke?.({
|
||||
registration: { handler: registrationHandler },
|
||||
namespace: "codex",
|
||||
payload: "approve:thread-1",
|
||||
});
|
||||
|
||||
expect(registrationHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
auth: expect.objectContaining({
|
||||
isAuthorizedSender: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("treats Slack reply buttons as plain interaction events instead of plugin dispatch", async () => {
|
||||
const { ctx, app, getHandler } = createContext();
|
||||
registerSlackInteractionEvents({ ctx: ctx as never });
|
||||
|
||||
@@ -820,14 +820,14 @@ export const registerTelegramHandlers = ({
|
||||
return { allowed: true };
|
||||
};
|
||||
|
||||
const isTelegramModelCallbackAuthorized = (params: {
|
||||
const isTelegramModelCallbackAuthorized = async (params: {
|
||||
chatId: number;
|
||||
isGroup: boolean;
|
||||
senderId: string;
|
||||
senderUsername: string;
|
||||
context: TelegramEventAuthorizationContext;
|
||||
cfg: OpenClawConfig;
|
||||
}): boolean => {
|
||||
}): Promise<boolean> => {
|
||||
const { chatId, isGroup, senderId, senderUsername, context, cfg } = params;
|
||||
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
||||
const dmAllowFrom = context.groupAllowOverride ?? allowFrom;
|
||||
@@ -855,8 +855,14 @@ export const registerTelegramHandlers = ({
|
||||
}).isAuthorizedSender;
|
||||
}
|
||||
|
||||
const dmAllow = normalizeDmAllowFromWithStore({
|
||||
const expandedDmAllowFrom = await expandTelegramAllowFromWithAccessGroups({
|
||||
cfg,
|
||||
allowFrom: dmAllowFrom,
|
||||
accountId,
|
||||
senderId,
|
||||
});
|
||||
const dmAllow = normalizeDmAllowFromWithStore({
|
||||
allowFrom: expandedDmAllowFrom,
|
||||
storeAllowFrom: isGroup ? [] : context.storeAllowFrom,
|
||||
dmPolicy: context.dmPolicy,
|
||||
});
|
||||
@@ -1410,6 +1416,7 @@ export const registerTelegramHandlers = ({
|
||||
await replyToCallbackChat(buildPluginBindingResolvedText(resolved));
|
||||
return;
|
||||
}
|
||||
const runtimeCfg = telegramDeps.getRuntimeConfig();
|
||||
const pluginCallback = await dispatchTelegramPluginInteractiveHandler({
|
||||
data,
|
||||
callbackId: callback.id,
|
||||
@@ -1424,7 +1431,14 @@ export const registerTelegramHandlers = ({
|
||||
isGroup,
|
||||
isForum,
|
||||
auth: {
|
||||
isAuthorizedSender: true,
|
||||
isAuthorizedSender: await isTelegramModelCallbackAuthorized({
|
||||
chatId,
|
||||
isGroup,
|
||||
senderId,
|
||||
senderUsername,
|
||||
context: eventAuthContext,
|
||||
cfg: runtimeCfg,
|
||||
}),
|
||||
},
|
||||
callbackMessage: {
|
||||
messageId: callbackMessage.message_id,
|
||||
@@ -1460,7 +1474,6 @@ export const registerTelegramHandlers = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeCfg = telegramDeps.getRuntimeConfig();
|
||||
if (approvalCallback) {
|
||||
const isPluginApproval = approvalCallback.approvalId.startsWith("plugin:");
|
||||
const pluginApprovalAuthorizedSender = isTelegramExecApprovalApprover({
|
||||
@@ -1564,14 +1577,14 @@ export const registerTelegramHandlers = ({
|
||||
const modelCallback = parseModelCallbackData(data);
|
||||
if (modelCallback) {
|
||||
if (
|
||||
!isTelegramModelCallbackAuthorized({
|
||||
!(await isTelegramModelCallbackAuthorized({
|
||||
chatId,
|
||||
isGroup,
|
||||
senderId,
|
||||
senderUsername,
|
||||
context: eventAuthContext,
|
||||
cfg: runtimeCfg,
|
||||
})
|
||||
}))
|
||||
) {
|
||||
logVerbose(
|
||||
`Blocked telegram model callback from ${senderId || "unknown"} (not authorized for /models)`,
|
||||
|
||||
@@ -2185,6 +2185,171 @@ describe("createTelegramBot", () => {
|
||||
expect(replySpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes false command auth to Telegram plugin callbacks for non-allowlisted group senders", async () => {
|
||||
onSpy.mockClear();
|
||||
let observedAuth: TelegramInteractiveHandlerContext["auth"] | undefined;
|
||||
const handler = vi.fn(async ({ auth }: TelegramInteractiveHandlerContext) => {
|
||||
observedAuth = auth;
|
||||
return { handled: true };
|
||||
});
|
||||
registerPluginInteractiveHandler("codex-plugin", {
|
||||
channel: "telegram",
|
||||
namespace: "codexapp",
|
||||
handler: handler as never,
|
||||
});
|
||||
|
||||
const config = {
|
||||
commands: {
|
||||
allowFrom: {
|
||||
telegram: ["111111111"],
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
dmPolicy: "open",
|
||||
capabilities: { inlineButtons: "group" },
|
||||
groupPolicy: "open",
|
||||
groups: { "*": { requireMention: false } },
|
||||
},
|
||||
},
|
||||
} satisfies NonNullable<Parameters<typeof createTelegramBot>[0]["config"]>;
|
||||
loadConfig.mockReturnValue(config);
|
||||
|
||||
createTelegramBot({ token: "tok", config });
|
||||
const callbackHandler = getOnHandler("callback_query") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
await callbackHandler({
|
||||
callbackQuery: {
|
||||
id: "cbq-plugin-auth-false",
|
||||
data: "codexapp:resume:thread-1",
|
||||
from: { id: 999999999, first_name: "Mallory", username: "mallory" },
|
||||
message: {
|
||||
chat: { id: -100999, type: "supergroup", title: "Test Group" },
|
||||
date: 1736380800,
|
||||
message_id: 22,
|
||||
text: "Select a thread",
|
||||
},
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||
});
|
||||
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
expect(observedAuth?.isAuthorizedSender).toBe(false);
|
||||
});
|
||||
|
||||
it("passes true command auth to Telegram plugin callbacks for allowlisted group senders", async () => {
|
||||
onSpy.mockClear();
|
||||
let observedAuth: TelegramInteractiveHandlerContext["auth"] | undefined;
|
||||
const handler = vi.fn(async ({ auth }: TelegramInteractiveHandlerContext) => {
|
||||
observedAuth = auth;
|
||||
return { handled: true };
|
||||
});
|
||||
registerPluginInteractiveHandler("codex-plugin", {
|
||||
channel: "telegram",
|
||||
namespace: "codexapp",
|
||||
handler: handler as never,
|
||||
});
|
||||
|
||||
const config = {
|
||||
commands: {
|
||||
allowFrom: {
|
||||
telegram: ["111111111"],
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
dmPolicy: "open",
|
||||
capabilities: { inlineButtons: "group" },
|
||||
groupPolicy: "open",
|
||||
groups: { "*": { requireMention: false } },
|
||||
},
|
||||
},
|
||||
} satisfies NonNullable<Parameters<typeof createTelegramBot>[0]["config"]>;
|
||||
loadConfig.mockReturnValue(config);
|
||||
|
||||
createTelegramBot({ token: "tok", config });
|
||||
const callbackHandler = getOnHandler("callback_query") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
await callbackHandler({
|
||||
callbackQuery: {
|
||||
id: "cbq-plugin-auth-true",
|
||||
data: "codexapp:resume:thread-1",
|
||||
from: { id: 111111111, first_name: "Ada", username: "ada" },
|
||||
message: {
|
||||
chat: { id: -100999, type: "supergroup", title: "Test Group" },
|
||||
date: 1736380800,
|
||||
message_id: 23,
|
||||
text: "Select a thread",
|
||||
},
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||
});
|
||||
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
expect(observedAuth?.isAuthorizedSender).toBe(true);
|
||||
});
|
||||
|
||||
it("passes true command auth to Telegram plugin callbacks for access-group DM senders", async () => {
|
||||
onSpy.mockClear();
|
||||
let observedAuth: TelegramInteractiveHandlerContext["auth"] | undefined;
|
||||
const handler = vi.fn(async ({ auth }: TelegramInteractiveHandlerContext) => {
|
||||
observedAuth = auth;
|
||||
return { handled: true };
|
||||
});
|
||||
registerPluginInteractiveHandler("codex-plugin", {
|
||||
channel: "telegram",
|
||||
namespace: "codexapp",
|
||||
handler: handler as never,
|
||||
});
|
||||
|
||||
const config = {
|
||||
accessGroups: {
|
||||
operators: {
|
||||
type: "message.senders",
|
||||
members: { telegram: ["123456789"] },
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["accessGroup:operators"],
|
||||
capabilities: { inlineButtons: "dm" },
|
||||
},
|
||||
},
|
||||
} satisfies NonNullable<Parameters<typeof createTelegramBot>[0]["config"]>;
|
||||
loadConfig.mockReturnValue(config);
|
||||
|
||||
createTelegramBot({ token: "tok", config });
|
||||
const callbackHandler = getOnHandler("callback_query") as (
|
||||
ctx: Record<string, unknown>,
|
||||
) => Promise<void>;
|
||||
|
||||
await callbackHandler({
|
||||
callbackQuery: {
|
||||
id: "cbq-plugin-access-group-auth",
|
||||
data: "codexapp:resume:thread-1",
|
||||
from: { id: 123456789, first_name: "Ada", username: "ada" },
|
||||
message: {
|
||||
chat: { id: 123456789, type: "private" },
|
||||
date: 1736380800,
|
||||
message_id: 24,
|
||||
text: "Select a thread",
|
||||
},
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||
});
|
||||
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
expect(observedAuth?.isAuthorizedSender).toBe(true);
|
||||
});
|
||||
|
||||
it("routes Telegram #General callback payloads as topic 1 when Telegram omits topic metadata", async () => {
|
||||
onSpy.mockClear();
|
||||
getChatSpy.mockResolvedValue({ id: -100123456789, type: "supergroup", is_forum: true });
|
||||
|
||||
Reference in New Issue
Block a user