mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-04 13:51:30 +00:00
refactor(telegram): centralize approval callback shaping
This commit is contained in:
@@ -1,12 +1,7 @@
|
||||
import type { ExecApprovalReplyDecision } from "openclaw/plugin-sdk/approval-runtime";
|
||||
import { sanitizeTelegramCallbackData } from "./approval-callback-data.js";
|
||||
import type { TelegramInlineButtons } from "./button-types.js";
|
||||
|
||||
const MAX_CALLBACK_DATA_BYTES = 64;
|
||||
|
||||
function fitsCallbackData(value: string): boolean {
|
||||
return Buffer.byteLength(value, "utf8") <= MAX_CALLBACK_DATA_BYTES;
|
||||
}
|
||||
|
||||
export function buildTelegramExecApprovalButtons(
|
||||
approvalId: string,
|
||||
): TelegramInlineButtons | undefined {
|
||||
@@ -21,23 +16,21 @@ function buildTelegramExecApprovalButtonsForDecisions(
|
||||
approvalId: string,
|
||||
allowedDecisions: readonly ExecApprovalReplyDecision[],
|
||||
): TelegramInlineButtons | undefined {
|
||||
const allowOnce = `/approve ${approvalId} allow-once`;
|
||||
if (!allowedDecisions.includes("allow-once") || !fitsCallbackData(allowOnce)) {
|
||||
const allowOnce = sanitizeTelegramCallbackData(`/approve ${approvalId} allow-once`);
|
||||
if (!allowedDecisions.includes("allow-once") || !allowOnce) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const primaryRow: Array<{ text: string; callback_data: string }> = [
|
||||
{ text: "Allow Once", callback_data: allowOnce },
|
||||
];
|
||||
// Use a shorter decision alias so full plugin:<uuid> IDs still fit Telegram's
|
||||
// 64-byte callback_data limit for the "Allow Always" action.
|
||||
const allowAlways = `/approve ${approvalId} always`;
|
||||
if (allowedDecisions.includes("allow-always") && fitsCallbackData(allowAlways)) {
|
||||
const allowAlways = sanitizeTelegramCallbackData(`/approve ${approvalId} allow-always`);
|
||||
if (allowedDecisions.includes("allow-always") && allowAlways) {
|
||||
primaryRow.push({ text: "Allow Always", callback_data: allowAlways });
|
||||
}
|
||||
const rows: Array<Array<{ text: string; callback_data: string }>> = [primaryRow];
|
||||
const deny = `/approve ${approvalId} deny`;
|
||||
if (allowedDecisions.includes("deny") && fitsCallbackData(deny)) {
|
||||
const deny = sanitizeTelegramCallbackData(`/approve ${approvalId} deny`);
|
||||
if (allowedDecisions.includes("deny") && deny) {
|
||||
rows.push([{ text: "Deny", callback_data: deny }]);
|
||||
}
|
||||
return rows;
|
||||
|
||||
33
extensions/telegram/src/approval-callback-data.test.ts
Normal file
33
extensions/telegram/src/approval-callback-data.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
fitsTelegramCallbackData,
|
||||
rewriteTelegramApprovalDecisionAlias,
|
||||
sanitizeTelegramCallbackData,
|
||||
} from "./approval-callback-data.js";
|
||||
|
||||
describe("approval callback data", () => {
|
||||
it("enforces Telegram callback byte boundaries", () => {
|
||||
expect(fitsTelegramCallbackData("x".repeat(63))).toBe(true);
|
||||
expect(fitsTelegramCallbackData("x".repeat(64))).toBe(true);
|
||||
expect(fitsTelegramCallbackData("x".repeat(65))).toBe(false);
|
||||
});
|
||||
|
||||
it("rewrites /approve allow-always callbacks to always", () => {
|
||||
const approvalId = `plugin:${"a".repeat(36)}`;
|
||||
expect(rewriteTelegramApprovalDecisionAlias(`/approve ${approvalId} allow-always`)).toBe(
|
||||
`/approve ${approvalId} always`,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps rewritten allow-always callbacks when canonical form would overflow", () => {
|
||||
const approvalId = `plugin:${"a".repeat(36)}`;
|
||||
expect(sanitizeTelegramCallbackData(`/approve ${approvalId} allow-always`)).toBe(
|
||||
`/approve ${approvalId} always`,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps 64-byte callbacks and drops 65-byte callbacks through sanitize", () => {
|
||||
expect(sanitizeTelegramCallbackData("x".repeat(64))).toBe("x".repeat(64));
|
||||
expect(sanitizeTelegramCallbackData("x".repeat(65))).toBeUndefined();
|
||||
});
|
||||
});
|
||||
23
extensions/telegram/src/approval-callback-data.ts
Normal file
23
extensions/telegram/src/approval-callback-data.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
const TELEGRAM_CALLBACK_DATA_MAX_BYTES = 64;
|
||||
|
||||
const TELEGRAM_APPROVE_ALLOW_ALWAYS_PATTERN =
|
||||
/^\/approve(?:@[^\s]+)?\s+[A-Za-z0-9][A-Za-z0-9._:-]*\s+allow-always$/i;
|
||||
|
||||
export function fitsTelegramCallbackData(value: string): boolean {
|
||||
return Buffer.byteLength(value, "utf8") <= TELEGRAM_CALLBACK_DATA_MAX_BYTES;
|
||||
}
|
||||
|
||||
export function rewriteTelegramApprovalDecisionAlias(value: string): string {
|
||||
if (!value.endsWith(" allow-always")) {
|
||||
return value;
|
||||
}
|
||||
if (!TELEGRAM_APPROVE_ALLOW_ALWAYS_PATTERN.test(value)) {
|
||||
return value;
|
||||
}
|
||||
return value.slice(0, -"allow-always".length) + "always";
|
||||
}
|
||||
|
||||
export function sanitizeTelegramCallbackData(value: string): string | undefined {
|
||||
const rewritten = rewriteTelegramApprovalDecisionAlias(value);
|
||||
return fitsTelegramCallbackData(rewritten) ? rewritten : undefined;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type InteractiveReply,
|
||||
type InteractiveReplyButton,
|
||||
} from "openclaw/plugin-sdk/interactive-runtime";
|
||||
import { sanitizeTelegramCallbackData } from "./approval-callback-data.js";
|
||||
|
||||
export type TelegramButtonStyle = "danger" | "success" | "primary";
|
||||
|
||||
@@ -16,11 +17,6 @@ export type TelegramInlineButton = {
|
||||
export type TelegramInlineButtons = ReadonlyArray<ReadonlyArray<TelegramInlineButton>>;
|
||||
|
||||
const TELEGRAM_INTERACTIVE_ROW_SIZE = 3;
|
||||
const MAX_CALLBACK_DATA_BYTES = 64;
|
||||
|
||||
function fitsTelegramCallbackData(value: string): boolean {
|
||||
return Buffer.byteLength(value, "utf8") <= MAX_CALLBACK_DATA_BYTES;
|
||||
}
|
||||
|
||||
function toTelegramButtonStyle(
|
||||
style?: InteractiveReplyButton["style"],
|
||||
@@ -28,36 +24,24 @@ function toTelegramButtonStyle(
|
||||
return style === "danger" || style === "success" || style === "primary" ? style : undefined;
|
||||
}
|
||||
|
||||
function rewriteTelegramApprovalAlias(value: string): string {
|
||||
if (!value.endsWith(" allow-always")) {
|
||||
return value;
|
||||
}
|
||||
const approvePrefixMatch = value.match(
|
||||
/^\/approve(?:@[^\s]+)?\s+[A-Za-z0-9][A-Za-z0-9._:-]*\s+allow-always$/i,
|
||||
);
|
||||
if (!approvePrefixMatch) {
|
||||
return value;
|
||||
}
|
||||
return value.slice(0, -"allow-always".length) + "always";
|
||||
}
|
||||
|
||||
function chunkInteractiveButtons(
|
||||
buttons: readonly InteractiveReplyButton[],
|
||||
rows: TelegramInlineButton[][],
|
||||
) {
|
||||
for (let i = 0; i < buttons.length; i += TELEGRAM_INTERACTIVE_ROW_SIZE) {
|
||||
const row = buttons
|
||||
.slice(i, i + TELEGRAM_INTERACTIVE_ROW_SIZE)
|
||||
.map((button) => ({
|
||||
...button,
|
||||
value: rewriteTelegramApprovalAlias(button.value),
|
||||
}))
|
||||
.filter((button) => fitsTelegramCallbackData(button.value))
|
||||
.map((button) => ({
|
||||
text: button.label,
|
||||
callback_data: button.value,
|
||||
style: toTelegramButtonStyle(button.style),
|
||||
}));
|
||||
const row = buttons.slice(i, i + TELEGRAM_INTERACTIVE_ROW_SIZE).flatMap((button) => {
|
||||
const callbackData = sanitizeTelegramCallbackData(button.value);
|
||||
if (!callbackData) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
text: button.label,
|
||||
callback_data: callbackData,
|
||||
style: toTelegramButtonStyle(button.style),
|
||||
},
|
||||
];
|
||||
});
|
||||
if (row.length > 0) {
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ import {
|
||||
} from "./accounts.js";
|
||||
import { resolveTelegramAutoThreadId } from "./action-threading.js";
|
||||
import { lookupTelegramChatId } from "./api-fetch.js";
|
||||
import { buildTelegramExecApprovalButtons } from "./approval-buttons.js";
|
||||
import { telegramApprovalCapability } from "./approval-native.js";
|
||||
import * as auditModule from "./audit.js";
|
||||
import { buildTelegramGroupPeerId } from "./bot/helpers.js";
|
||||
|
||||
Reference in New Issue
Block a user