Files
openclaw/src/telegram/bot-native-command-menu.ts
SidQin-cyber 1ba525f94d fix(telegram): degrade command sync on BOT_COMMANDS_TOO_MUCH
When Telegram rejects native command registration for excessive commands, progressively retry with fewer commands instead of hard-failing startup.

Made-with: Cursor
(cherry picked from commit a02c40483e)
2026-02-26 13:40:58 +00:00

156 lines
4.8 KiB
TypeScript

import type { Bot } from "grammy";
import {
normalizeTelegramCommandName,
TELEGRAM_COMMAND_NAME_PATTERN,
} from "../config/telegram-custom-commands.js";
import type { RuntimeEnv } from "../runtime.js";
import { withTelegramApiErrorLogging } from "./api-logging.js";
export const TELEGRAM_MAX_COMMANDS = 100;
const TELEGRAM_COMMAND_RETRY_RATIO = 0.8;
export type TelegramMenuCommand = {
command: string;
description: string;
};
type TelegramPluginCommandSpec = {
name: string;
description: string;
};
function isBotCommandsTooMuchError(err: unknown): boolean {
if (!err) {
return false;
}
const pattern = /\bBOT_COMMANDS_TOO_MUCH\b/i;
if (typeof err === "string") {
return pattern.test(err);
}
if (err instanceof Error) {
if (pattern.test(err.message)) {
return true;
}
}
if (typeof err === "object") {
const maybe = err as { description?: unknown; message?: unknown };
if (typeof maybe.description === "string" && pattern.test(maybe.description)) {
return true;
}
if (typeof maybe.message === "string" && pattern.test(maybe.message)) {
return true;
}
}
return false;
}
export function buildPluginTelegramMenuCommands(params: {
specs: TelegramPluginCommandSpec[];
existingCommands: Set<string>;
}): { commands: TelegramMenuCommand[]; issues: string[] } {
const { specs, existingCommands } = params;
const commands: TelegramMenuCommand[] = [];
const issues: string[] = [];
const pluginCommandNames = new Set<string>();
for (const spec of specs) {
const normalized = normalizeTelegramCommandName(spec.name);
if (!normalized || !TELEGRAM_COMMAND_NAME_PATTERN.test(normalized)) {
issues.push(
`Plugin command "/${spec.name}" is invalid for Telegram (use a-z, 0-9, underscore; max 32 chars).`,
);
continue;
}
const description = spec.description.trim();
if (!description) {
issues.push(`Plugin command "/${normalized}" is missing a description.`);
continue;
}
if (existingCommands.has(normalized)) {
if (pluginCommandNames.has(normalized)) {
issues.push(`Plugin command "/${normalized}" is duplicated.`);
} else {
issues.push(`Plugin command "/${normalized}" conflicts with an existing Telegram command.`);
}
continue;
}
pluginCommandNames.add(normalized);
existingCommands.add(normalized);
commands.push({ command: normalized, description });
}
return { commands, issues };
}
export function buildCappedTelegramMenuCommands(params: {
allCommands: TelegramMenuCommand[];
maxCommands?: number;
}): {
commandsToRegister: TelegramMenuCommand[];
totalCommands: number;
maxCommands: number;
overflowCount: number;
} {
const { allCommands } = params;
const maxCommands = params.maxCommands ?? TELEGRAM_MAX_COMMANDS;
const totalCommands = allCommands.length;
const overflowCount = Math.max(0, totalCommands - maxCommands);
const commandsToRegister = allCommands.slice(0, maxCommands);
return { commandsToRegister, totalCommands, maxCommands, overflowCount };
}
export function syncTelegramMenuCommands(params: {
bot: Bot;
runtime: RuntimeEnv;
commandsToRegister: TelegramMenuCommand[];
}): void {
const { bot, runtime, commandsToRegister } = params;
const sync = async () => {
// Keep delete -> set ordering to avoid stale deletions racing after fresh registrations.
if (typeof bot.api.deleteMyCommands === "function") {
await withTelegramApiErrorLogging({
operation: "deleteMyCommands",
runtime,
fn: () => bot.api.deleteMyCommands(),
}).catch(() => {});
}
if (commandsToRegister.length === 0) {
return;
}
let retryCommands = commandsToRegister;
while (retryCommands.length > 0) {
try {
await withTelegramApiErrorLogging({
operation: "setMyCommands",
runtime,
fn: () => bot.api.setMyCommands(retryCommands),
});
return;
} catch (err) {
if (!isBotCommandsTooMuchError(err)) {
throw err;
}
const nextCount = Math.floor(retryCommands.length * TELEGRAM_COMMAND_RETRY_RATIO);
const reducedCount =
nextCount < retryCommands.length ? nextCount : retryCommands.length - 1;
if (reducedCount <= 0) {
runtime.error?.(
"Telegram rejected native command registration (BOT_COMMANDS_TOO_MUCH); leaving menu empty. Reduce commands or disable channels.telegram.commands.native.",
);
return;
}
runtime.log?.(
`Telegram rejected ${retryCommands.length} commands (BOT_COMMANDS_TOO_MUCH); retrying with ${reducedCount}.`,
);
retryCommands = retryCommands.slice(0, reducedCount);
}
}
};
void sync().catch((err) => {
runtime.error?.(`Telegram command sync failed: ${String(err)}`);
});
}