mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 00:17:27 +00:00
fix: sanitize native command names for Telegram API (#19257)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: b608be3488
Co-authored-by: akramcodez <179671552+akramcodez@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
This commit is contained in:
@@ -62,6 +62,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Telegram: debounce the first draft-stream preview update (30-char threshold) and finalize short responses by editing the stop-time preview message, improving first push notifications and avoiding duplicate final sends. (#18148) Thanks @Marvae.
|
- Telegram: debounce the first draft-stream preview update (30-char threshold) and finalize short responses by editing the stop-time preview message, improving first push notifications and avoiding duplicate final sends. (#18148) Thanks @Marvae.
|
||||||
- Telegram: disable block streaming when `channels.telegram.streamMode` is `off`, preventing newline/content-block replies from splitting into multiple messages. (#17679) Thanks @saivarunk.
|
- Telegram: disable block streaming when `channels.telegram.streamMode` is `off`, preventing newline/content-block replies from splitting into multiple messages. (#17679) Thanks @saivarunk.
|
||||||
- Telegram: keep `streamMode: "partial"` draft previews in a single message across assistant-message/reasoning boundaries, preventing duplicate preview bubbles during partial-mode tool-call turns. (#18956) Thanks @obviyus.
|
- Telegram: keep `streamMode: "partial"` draft previews in a single message across assistant-message/reasoning boundaries, preventing duplicate preview bubbles during partial-mode tool-call turns. (#18956) Thanks @obviyus.
|
||||||
|
- Telegram: normalize native command names for Telegram menu registration (`-` -> `_`) to avoid `BOT_COMMAND_INVALID` command-menu wipeouts, and log failed command syncs instead of silently swallowing them. (#19257) Thanks @akramcodez.
|
||||||
- Telegram: route non-abort slash commands on the normal chat/topic sequential lane while keeping true abort requests (`/stop`, `stop`) on the control lane, preventing command/reply race conditions from control-lane bypass. (#17899) Thanks @obviyus.
|
- Telegram: route non-abort slash commands on the normal chat/topic sequential lane while keeping true abort requests (`/stop`, `stop`) on the control lane, preventing command/reply race conditions from control-lane bypass. (#17899) Thanks @obviyus.
|
||||||
- Telegram: ignore `<media:...>` placeholder lines when extracting `MEDIA:` tool-result paths, preventing false local-file reads and dropped replies. (#18510) Thanks @yinghaosang.
|
- Telegram: ignore `<media:...>` placeholder lines when extracting `MEDIA:` tool-result paths, preventing false local-file reads and dropped replies. (#18510) Thanks @yinghaosang.
|
||||||
- Telegram: skip retries when inbound media `getFile` fails with Telegram's 20MB limit and continue processing message text, avoiding dropped messages for oversized attachments. (#18531) Thanks @brandonwise.
|
- Telegram: skip retries when inbound media `getFile` fails with Telegram's 20MB limit and continue processing message text, avoiding dropped messages for oversized attachments. (#18531) Thanks @brandonwise.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ describe("telegram custom commands schema", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects custom commands with invalid names", () => {
|
it("normalizes hyphens in custom command names", () => {
|
||||||
const res = OpenClawSchema.safeParse({
|
const res = OpenClawSchema.safeParse({
|
||||||
channels: {
|
channels: {
|
||||||
telegram: {
|
telegram: {
|
||||||
@@ -30,17 +30,13 @@ describe("telegram custom commands schema", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.success).toBe(false);
|
expect(res.success).toBe(true);
|
||||||
if (res.success) {
|
if (!res.success) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(
|
expect(res.data.channels?.telegram?.customCommands).toEqual([
|
||||||
res.error.issues.some(
|
{ command: "bad_name", description: "Override status" },
|
||||||
(issue) =>
|
]);
|
||||||
issue.path.join(".") === "channels.telegram.customCommands.0.command" &&
|
|
||||||
issue.message.includes("invalid"),
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function normalizeTelegramCommandName(value: string): string {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
const withoutSlash = trimmed.startsWith("/") ? trimmed.slice(1) : trimmed;
|
const withoutSlash = trimmed.startsWith("/") ? trimmed.slice(1) : trimmed;
|
||||||
return withoutSlash.trim().toLowerCase();
|
return withoutSlash.trim().toLowerCase().replace(/-/g, "_");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeTelegramCommandDescription(value: string): string {
|
export function normalizeTelegramCommandDescription(value: string): string {
|
||||||
|
|||||||
@@ -100,5 +100,7 @@ export function syncTelegramMenuCommands(params: {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
void sync().catch(() => {});
|
void sync().catch((err) => {
|
||||||
|
runtime.error?.(`Telegram command sync failed: ${String(err)}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,6 +149,37 @@ describe("registerTelegramNativeCommands", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("normalizes hyphenated native command names for Telegram registration", async () => {
|
||||||
|
const setMyCommands = vi.fn().mockResolvedValue(undefined);
|
||||||
|
const command = vi.fn();
|
||||||
|
|
||||||
|
registerTelegramNativeCommands({
|
||||||
|
...buildParams({}),
|
||||||
|
bot: {
|
||||||
|
api: {
|
||||||
|
setMyCommands,
|
||||||
|
sendMessage: vi.fn().mockResolvedValue(undefined),
|
||||||
|
},
|
||||||
|
command,
|
||||||
|
} as unknown as Parameters<typeof registerTelegramNativeCommands>[0]["bot"],
|
||||||
|
});
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(setMyCommands).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
const registeredCommands = setMyCommands.mock.calls[0]?.[0] as Array<{
|
||||||
|
command: string;
|
||||||
|
description: string;
|
||||||
|
}>;
|
||||||
|
expect(registeredCommands.some((entry) => entry.command === "export_session")).toBe(true);
|
||||||
|
expect(registeredCommands.some((entry) => entry.command === "export-session")).toBe(false);
|
||||||
|
|
||||||
|
const registeredHandlers = command.mock.calls.map(([name]) => name);
|
||||||
|
expect(registeredHandlers).toContain("export_session");
|
||||||
|
expect(registeredHandlers).not.toContain("export-session");
|
||||||
|
});
|
||||||
|
|
||||||
it("passes agent-scoped media roots for plugin command replies with media", async () => {
|
it("passes agent-scoped media roots for plugin command replies with media", async () => {
|
||||||
const commandHandlers = new Map<string, (ctx: unknown) => Promise<void>>();
|
const commandHandlers = new Map<string, (ctx: unknown) => Promise<void>>();
|
||||||
const sendMessage = vi.fn().mockResolvedValue(undefined);
|
const sendMessage = vi.fn().mockResolvedValue(undefined);
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ import { createReplyPrefixOptions } from "../channels/reply-prefix.js";
|
|||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import type { ChannelGroupPolicy } from "../config/group-policy.js";
|
import type { ChannelGroupPolicy } from "../config/group-policy.js";
|
||||||
import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
|
import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
|
||||||
import { resolveTelegramCustomCommands } from "../config/telegram-custom-commands.js";
|
import {
|
||||||
|
normalizeTelegramCommandName,
|
||||||
|
resolveTelegramCustomCommands,
|
||||||
|
TELEGRAM_COMMAND_NAME_PATTERN,
|
||||||
|
} from "../config/telegram-custom-commands.js";
|
||||||
import type {
|
import type {
|
||||||
ReplyToMode,
|
ReplyToMode,
|
||||||
TelegramAccountConfig,
|
TelegramAccountConfig,
|
||||||
@@ -310,7 +314,7 @@ export const registerTelegramNativeCommands = ({
|
|||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
const reservedCommands = new Set(
|
const reservedCommands = new Set(
|
||||||
listNativeCommandSpecs().map((command) => command.name.toLowerCase()),
|
listNativeCommandSpecs().map((command) => normalizeTelegramCommandName(command.name)),
|
||||||
);
|
);
|
||||||
for (const command of skillCommands) {
|
for (const command of skillCommands) {
|
||||||
reservedCommands.add(command.name.toLowerCase());
|
reservedCommands.add(command.name.toLowerCase());
|
||||||
@@ -326,7 +330,7 @@ export const registerTelegramNativeCommands = ({
|
|||||||
const pluginCommandSpecs = getPluginCommandSpecs();
|
const pluginCommandSpecs = getPluginCommandSpecs();
|
||||||
const existingCommands = new Set(
|
const existingCommands = new Set(
|
||||||
[
|
[
|
||||||
...nativeCommands.map((command) => command.name),
|
...nativeCommands.map((command) => normalizeTelegramCommandName(command.name)),
|
||||||
...customCommands.map((command) => command.command),
|
...customCommands.map((command) => command.command),
|
||||||
].map((command) => command.toLowerCase()),
|
].map((command) => command.toLowerCase()),
|
||||||
);
|
);
|
||||||
@@ -338,10 +342,23 @@ export const registerTelegramNativeCommands = ({
|
|||||||
runtime.error?.(danger(issue));
|
runtime.error?.(danger(issue));
|
||||||
}
|
}
|
||||||
const allCommandsFull: Array<{ command: string; description: string }> = [
|
const allCommandsFull: Array<{ command: string; description: string }> = [
|
||||||
...nativeCommands.map((command) => ({
|
...nativeCommands
|
||||||
command: command.name,
|
.map((command) => {
|
||||||
description: command.description,
|
const normalized = normalizeTelegramCommandName(command.name);
|
||||||
})),
|
if (!TELEGRAM_COMMAND_NAME_PATTERN.test(normalized)) {
|
||||||
|
runtime.error?.(
|
||||||
|
danger(
|
||||||
|
`Native command "${command.name}" is invalid for Telegram (resolved to "${normalized}"). Skipping.`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
command: normalized,
|
||||||
|
description: command.description,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((cmd): cmd is { command: string; description: string } => cmd !== null),
|
||||||
...(nativeEnabled ? pluginCatalog.commands : []),
|
...(nativeEnabled ? pluginCatalog.commands : []),
|
||||||
...customCommands,
|
...customCommands,
|
||||||
];
|
];
|
||||||
@@ -419,7 +436,8 @@ export const registerTelegramNativeCommands = ({
|
|||||||
logVerbose("telegram: bot.command unavailable; skipping native handlers");
|
logVerbose("telegram: bot.command unavailable; skipping native handlers");
|
||||||
} else {
|
} else {
|
||||||
for (const command of nativeCommands) {
|
for (const command of nativeCommands) {
|
||||||
bot.command(command.name, async (ctx: TelegramNativeCommandContext) => {
|
const normalizedCommandName = normalizeTelegramCommandName(command.name);
|
||||||
|
bot.command(normalizedCommandName, async (ctx: TelegramNativeCommandContext) => {
|
||||||
const msg = ctx.message;
|
const msg = ctx.message;
|
||||||
if (!msg) {
|
if (!msg) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
listNativeCommandSpecs,
|
listNativeCommandSpecs,
|
||||||
listNativeCommandSpecsForConfig,
|
listNativeCommandSpecsForConfig,
|
||||||
} from "../auto-reply/commands-registry.js";
|
} from "../auto-reply/commands-registry.js";
|
||||||
|
import { normalizeTelegramCommandName } from "../config/telegram-custom-commands.js";
|
||||||
import {
|
import {
|
||||||
answerCallbackQuerySpy,
|
answerCallbackQuerySpy,
|
||||||
commandSpy,
|
commandSpy,
|
||||||
@@ -72,7 +73,7 @@ describe("createTelegramBot", () => {
|
|||||||
}>;
|
}>;
|
||||||
const skillCommands = resolveSkillCommands(config);
|
const skillCommands = resolveSkillCommands(config);
|
||||||
const native = listNativeCommandSpecsForConfig(config, { skillCommands }).map((command) => ({
|
const native = listNativeCommandSpecsForConfig(config, { skillCommands }).map((command) => ({
|
||||||
command: command.name,
|
command: normalizeTelegramCommandName(command.name),
|
||||||
description: command.description,
|
description: command.description,
|
||||||
}));
|
}));
|
||||||
expect(registered.slice(0, native.length)).toEqual(native);
|
expect(registered.slice(0, native.length)).toEqual(native);
|
||||||
@@ -113,7 +114,7 @@ describe("createTelegramBot", () => {
|
|||||||
}>;
|
}>;
|
||||||
const skillCommands = resolveSkillCommands(config);
|
const skillCommands = resolveSkillCommands(config);
|
||||||
const native = listNativeCommandSpecsForConfig(config, { skillCommands }).map((command) => ({
|
const native = listNativeCommandSpecsForConfig(config, { skillCommands }).map((command) => ({
|
||||||
command: command.name,
|
command: normalizeTelegramCommandName(command.name),
|
||||||
description: command.description,
|
description: command.description,
|
||||||
}));
|
}));
|
||||||
const nativeStatus = native.find((command) => command.command === "status");
|
const nativeStatus = native.find((command) => command.command === "status");
|
||||||
|
|||||||
Reference in New Issue
Block a user