mirror of https://github.com/openclaw/openclaw.git
fix(telegram): accept approval callbacks from forwarding target recipients
When approvals.exec.targets routes to a Telegram DM, the recipient receives inline approval buttons but may not have explicit channels.telegram.execApprovals configured. This adds a fallback isTelegramExecApprovalTargetRecipient check so those DM recipients can act on the buttons they were sent. Includes accountId scoping for multi-bot deployments and 9 new tests.
This commit is contained in:
parent
3a43401924
commit
aee7992629
|
|
@ -80,6 +80,7 @@ import { enforceTelegramDmAccess } from "./dm-access.js";
|
|||
import {
|
||||
isTelegramExecApprovalApprover,
|
||||
isTelegramExecApprovalClientEnabled,
|
||||
isTelegramExecApprovalTargetRecipient,
|
||||
shouldEnableTelegramExecApprovalButtons,
|
||||
} from "./exec-approvals.js";
|
||||
import {
|
||||
|
|
@ -1289,9 +1290,12 @@ export const registerTelegramHandlers = ({
|
|||
|
||||
const runtimeCfg = telegramDeps.loadConfig();
|
||||
if (isApprovalCallback) {
|
||||
const isExplicitApprover =
|
||||
isTelegramExecApprovalClientEnabled({ cfg: runtimeCfg, accountId }) &&
|
||||
isTelegramExecApprovalApprover({ cfg: runtimeCfg, accountId, senderId });
|
||||
if (
|
||||
!isTelegramExecApprovalClientEnabled({ cfg: runtimeCfg, accountId }) ||
|
||||
!isTelegramExecApprovalApprover({ cfg: runtimeCfg, accountId, senderId })
|
||||
!isExplicitApprover &&
|
||||
!isTelegramExecApprovalTargetRecipient({ cfg: runtimeCfg, senderId, accountId })
|
||||
) {
|
||||
logVerbose(
|
||||
`Blocked telegram exec approval callback from ${senderId || "unknown"} (not an approver)`,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { OpenClawConfig } from "../../../src/config/config.js";
|
|||
import {
|
||||
isTelegramExecApprovalApprover,
|
||||
isTelegramExecApprovalClientEnabled,
|
||||
isTelegramExecApprovalTargetRecipient,
|
||||
resolveTelegramExecApprovalTarget,
|
||||
shouldEnableTelegramExecApprovalButtons,
|
||||
shouldInjectTelegramExecApprovalButtons,
|
||||
|
|
@ -89,4 +90,79 @@ describe("telegram exec approvals", () => {
|
|||
|
||||
expect(shouldEnableTelegramExecApprovalButtons({ cfg, to: "123" })).toBe(false);
|
||||
});
|
||||
|
||||
describe("isTelegramExecApprovalTargetRecipient", () => {
|
||||
function buildTargetConfig(
|
||||
targets: Array<{ channel: string; to: string; accountId?: string }>,
|
||||
): OpenClawConfig {
|
||||
return {
|
||||
channels: { telegram: { botToken: "tok" } },
|
||||
approvals: { exec: { enabled: true, mode: "targets", targets } },
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
it("accepts sender who is a DM target", () => {
|
||||
const cfg = buildTargetConfig([{ channel: "telegram", to: "12345" }]);
|
||||
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345" })).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects sender not in any target", () => {
|
||||
const cfg = buildTargetConfig([{ channel: "telegram", to: "12345" }]);
|
||||
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "99999" })).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects group targets", () => {
|
||||
const cfg = buildTargetConfig([{ channel: "telegram", to: "-100123456" }]);
|
||||
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "123456" })).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores non-telegram targets", () => {
|
||||
const cfg = buildTargetConfig([{ channel: "discord", to: "12345" }]);
|
||||
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345" })).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when no targets configured", () => {
|
||||
const cfg = buildConfig();
|
||||
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345" })).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when senderId is empty or null", () => {
|
||||
const cfg = buildTargetConfig([{ channel: "telegram", to: "12345" }]);
|
||||
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "" })).toBe(false);
|
||||
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: null })).toBe(false);
|
||||
expect(isTelegramExecApprovalTargetRecipient({ cfg })).toBe(false);
|
||||
});
|
||||
|
||||
it("matches across multiple targets", () => {
|
||||
const cfg = buildTargetConfig([
|
||||
{ channel: "slack", to: "U12345" },
|
||||
{ channel: "telegram", to: "67890" },
|
||||
{ channel: "telegram", to: "11111" },
|
||||
]);
|
||||
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "67890" })).toBe(true);
|
||||
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "11111" })).toBe(true);
|
||||
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "U12345" })).toBe(false);
|
||||
});
|
||||
|
||||
it("scopes by accountId in multi-bot deployments", () => {
|
||||
const cfg = buildTargetConfig([
|
||||
{ channel: "telegram", to: "12345", accountId: "account-a" },
|
||||
{ channel: "telegram", to: "67890", accountId: "account-b" },
|
||||
]);
|
||||
expect(
|
||||
isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345", accountId: "account-a" }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345", accountId: "account-b" }),
|
||||
).toBe(false);
|
||||
expect(isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345" })).toBe(true);
|
||||
});
|
||||
|
||||
it("allows unscoped targets regardless of callback accountId", () => {
|
||||
const cfg = buildTargetConfig([{ channel: "telegram", to: "12345" }]);
|
||||
expect(
|
||||
isTelegramExecApprovalTargetRecipient({ cfg, senderId: "12345", accountId: "any-account" }),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { TelegramExecApprovalConfig } from "openclaw/plugin-sdk/config-runt
|
|||
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { resolveTelegramAccount } from "./accounts.js";
|
||||
import { resolveTelegramInlineButtonsConfigScope } from "./inline-buttons.js";
|
||||
import { resolveTelegramTargetChatType } from "./targets.js";
|
||||
import { isNumericTelegramChatId, resolveTelegramTargetChatType } from "./targets.js";
|
||||
|
||||
function normalizeApproverId(value: string | number): string {
|
||||
return String(value).trim();
|
||||
|
|
@ -47,6 +47,37 @@ export function isTelegramExecApprovalApprover(params: {
|
|||
return approvers.includes(senderId);
|
||||
}
|
||||
|
||||
/** Check if sender is an implicit approver via exec approval forwarding targets. */
|
||||
export function isTelegramExecApprovalTargetRecipient(params: {
|
||||
cfg: OpenClawConfig;
|
||||
senderId?: string | null;
|
||||
accountId?: string | null;
|
||||
}): boolean {
|
||||
const senderId = params.senderId?.trim();
|
||||
if (!senderId) {
|
||||
return false;
|
||||
}
|
||||
const targets = params.cfg.approvals?.exec?.targets;
|
||||
if (!targets || !Array.isArray(targets)) {
|
||||
return false;
|
||||
}
|
||||
const accountId = params.accountId?.trim() || undefined;
|
||||
return targets.some((target) => {
|
||||
const channel = target.channel?.trim().toLowerCase();
|
||||
if (channel !== "telegram") {
|
||||
return false;
|
||||
}
|
||||
if (accountId && target.accountId && target.accountId !== accountId) {
|
||||
return false;
|
||||
}
|
||||
const to = target.to?.trim();
|
||||
if (!to || !isNumericTelegramChatId(to) || to.startsWith("-")) {
|
||||
return false;
|
||||
}
|
||||
return to === senderId;
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveTelegramExecApprovalTarget(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
import {
|
||||
isTelegramExecApprovalApprover,
|
||||
isTelegramExecApprovalClientEnabled,
|
||||
isTelegramExecApprovalTargetRecipient,
|
||||
} from "../../plugin-sdk/telegram-runtime.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
|
||||
import { requireGatewayClientScopeForInternalChannel } from "./command-gates.js";
|
||||
|
|
@ -140,16 +141,13 @@ export const handleApproveCommand: CommandHandler = async (params, allowTextComm
|
|||
senderId: params.command.senderId,
|
||||
};
|
||||
|
||||
const isImplicitTargetApprover = isTelegramExecApprovalTargetRecipient(telegramApproverContext);
|
||||
|
||||
if (!isPluginId) {
|
||||
if (
|
||||
!isTelegramExecApprovalClientEnabled({ cfg: params.cfg, accountId: params.ctx.AccountId })
|
||||
) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: "❌ Telegram exec approvals are not enabled for this bot account." },
|
||||
};
|
||||
}
|
||||
if (!isTelegramExecApprovalApprover(telegramApproverContext)) {
|
||||
const isExplicitApprover =
|
||||
isTelegramExecApprovalClientEnabled({ cfg: params.cfg, accountId: params.ctx.AccountId }) &&
|
||||
isTelegramExecApprovalApprover(telegramApproverContext);
|
||||
if (!isExplicitApprover && !isImplicitTargetApprover) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: "❌ You are not authorized to approve exec requests on Telegram." },
|
||||
|
|
@ -160,7 +158,7 @@ export const handleApproveCommand: CommandHandler = async (params, allowTextComm
|
|||
// Keep plugin-ID routing independent from exec approval client enablement so
|
||||
// forwarded plugin approvals remain resolvable, but still require explicit
|
||||
// Telegram approver membership for security parity.
|
||||
if (isPluginId && !isTelegramExecApprovalApprover(telegramApproverContext)) {
|
||||
if (isPluginId && !isTelegramExecApprovalApprover(telegramApproverContext) && !isImplicitTargetApprover) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: "❌ You are not authorized to approve plugin requests on Telegram." },
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export {
|
|||
inspectTelegramAccount,
|
||||
isTelegramExecApprovalApprover,
|
||||
isTelegramExecApprovalClientEnabled,
|
||||
isTelegramExecApprovalTargetRecipient,
|
||||
listTelegramAccountIds,
|
||||
listTelegramDirectoryGroupsFromConfig,
|
||||
listTelegramDirectoryPeersFromConfig,
|
||||
|
|
|
|||
Loading…
Reference in New Issue