refactor(telegram): extract native command menu helpers

This commit is contained in:
Peter Steinberger
2026-02-14 02:02:44 +01:00
parent 2e84ae7019
commit cc2249a431
4 changed files with 207 additions and 68 deletions

View File

@@ -26,10 +26,6 @@ import { resolveCommandAuthorizedFromAuthorizers } from "../channels/command-gat
import { createReplyPrefixOptions } from "../channels/reply-prefix.js";
import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
import { resolveTelegramCustomCommands } from "../config/telegram-custom-commands.js";
import {
normalizeTelegramCommandName,
TELEGRAM_COMMAND_NAME_PATTERN,
} from "../config/telegram-custom-commands.js";
import { danger, logVerbose } from "../globals.js";
import { getChildLogger } from "../logging.js";
import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
@@ -42,6 +38,11 @@ import { resolveAgentRoute } from "../routing/resolve-route.js";
import { resolveThreadSessionKeys } from "../routing/session-key.js";
import { withTelegramApiErrorLogging } from "./api-logging.js";
import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js";
import {
buildCappedTelegramMenuCommands,
buildPluginTelegramMenuCommands,
syncTelegramMenuCommands,
} from "./bot-native-command-menu.js";
import { TelegramUpdateKeyContext } from "./bot-updates.js";
import { TelegramBotOptions } from "./bot.js";
import { deliverReplies } from "./bot/delivery.js";
@@ -321,86 +322,41 @@ export const registerTelegramNativeCommands = ({
}
const customCommands = customResolution.commands;
const pluginCommandSpecs = getPluginCommandSpecs();
const pluginCommands: Array<{ command: string; description: string }> = [];
const existingCommands = new Set(
[
...nativeCommands.map((command) => command.name),
...customCommands.map((command) => command.command),
].map((command) => command.toLowerCase()),
);
const pluginCommandNames = new Set<string>();
for (const spec of pluginCommandSpecs) {
const normalized = normalizeTelegramCommandName(spec.name);
if (!normalized || !TELEGRAM_COMMAND_NAME_PATTERN.test(normalized)) {
runtime.error?.(
danger(
`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) {
runtime.error?.(danger(`Plugin command "/${normalized}" is missing a description.`));
continue;
}
if (existingCommands.has(normalized)) {
runtime.error?.(
danger(`Plugin command "/${normalized}" conflicts with an existing Telegram command.`),
);
continue;
}
if (pluginCommandNames.has(normalized)) {
runtime.error?.(danger(`Plugin command "/${normalized}" is duplicated.`));
continue;
}
pluginCommandNames.add(normalized);
existingCommands.add(normalized);
pluginCommands.push({ command: normalized, description });
const pluginCatalog = buildPluginTelegramMenuCommands({
specs: pluginCommandSpecs,
existingCommands,
});
for (const issue of pluginCatalog.issues) {
runtime.error?.(danger(issue));
}
const allCommandsFull: Array<{ command: string; description: string }> = [
...nativeCommands.map((command) => ({
command: command.name,
description: command.description,
})),
...pluginCommands,
...pluginCatalog.commands,
...customCommands,
];
const TELEGRAM_MAX_COMMANDS = 100;
if (allCommandsFull.length > TELEGRAM_MAX_COMMANDS) {
const { commandsToRegister, totalCommands, maxCommands, overflowCount } =
buildCappedTelegramMenuCommands({
allCommands: allCommandsFull,
});
if (overflowCount > 0) {
runtime.log?.(
`Telegram limits bots to ${TELEGRAM_MAX_COMMANDS} commands. ` +
`${allCommandsFull.length} configured; registering first ${TELEGRAM_MAX_COMMANDS}. ` +
`Use channels.telegram.commands.native: false to disable, or reduce skill/custom commands.`,
`Telegram limits bots to ${maxCommands} commands. ` +
`${totalCommands} configured; registering first ${maxCommands}. ` +
`Use channels.telegram.commands.native: false to disable, or reduce plugin/skill/custom commands.`,
);
}
// Telegram only limits the setMyCommands payload (menu entries).
const commandsToRegister = allCommandsFull.slice(0, TELEGRAM_MAX_COMMANDS);
// Clear stale commands before registering new ones to prevent
// leftover commands from deleted skills persisting across restarts (#5717).
// Chain delete → set so a late-resolving delete cannot wipe newly registered commands.
const registerCommands = () => {
if (commandsToRegister.length > 0) {
withTelegramApiErrorLogging({
operation: "setMyCommands",
runtime,
fn: () => bot.api.setMyCommands(commandsToRegister),
}).catch(() => {});
}
};
if (typeof bot.api.deleteMyCommands === "function") {
withTelegramApiErrorLogging({
operation: "deleteMyCommands",
runtime,
fn: () => bot.api.deleteMyCommands(),
})
.catch(() => {})
.then(registerCommands)
.catch(() => {});
} else {
registerCommands();
}
// Keep hidden commands callable by registering handlers for the full catalog.
syncTelegramMenuCommands({ bot, runtime, commandsToRegister });
if (commandsToRegister.length > 0) {
if (typeof (bot as unknown as { command?: unknown }).command !== "function") {
@@ -643,7 +599,7 @@ export const registerTelegramNativeCommands = ({
});
}
for (const pluginCommand of pluginCommands) {
for (const pluginCommand of pluginCatalog.commands) {
bot.command(pluginCommand.command, async (ctx: TelegramNativeCommandContext) => {
const msg = ctx.message;
if (!msg) {