Merge pull request #729 from clawdbot/fix/telegram-command-mentions

fix: normalize telegram command mentions
This commit is contained in:
Ayaan Zaidi
2026-01-11 21:14:01 +05:30
committed by GitHub
5 changed files with 65 additions and 9 deletions

View File

@@ -71,4 +71,17 @@ describe("control command parsing", () => {
expect(hasControlCommand("prefix /send on")).toBe(false); expect(hasControlCommand("prefix /send on")).toBe(false);
expect(hasControlCommand("/send on")).toBe(true); expect(hasControlCommand("/send on")).toBe(true);
}); });
it("ignores telegram commands addressed to other bots", () => {
expect(
hasControlCommand("/help@otherbot", undefined, {
botUsername: "clawdbot",
}),
).toBe(false);
expect(
hasControlCommand("/help@clawdbot", undefined, {
botUsername: "clawdbot",
}),
).toBe(true);
});
}); });

View File

@@ -1,5 +1,6 @@
import type { ClawdbotConfig } from "../config/types.js"; import type { ClawdbotConfig } from "../config/types.js";
import { import {
type CommandNormalizeOptions,
listChatCommands, listChatCommands,
listChatCommandsForConfig, listChatCommandsForConfig,
normalizeCommandBody, normalizeCommandBody,
@@ -8,11 +9,12 @@ import {
export function hasControlCommand( export function hasControlCommand(
text?: string, text?: string,
cfg?: ClawdbotConfig, cfg?: ClawdbotConfig,
options?: CommandNormalizeOptions,
): boolean { ): boolean {
if (!text) return false; if (!text) return false;
const trimmed = text.trim(); const trimmed = text.trim();
if (!trimmed) return false; if (!trimmed) return false;
const normalizedBody = normalizeCommandBody(trimmed); const normalizedBody = normalizeCommandBody(trimmed, options);
if (!normalizedBody) return false; if (!normalizedBody) return false;
const lowered = normalizedBody.toLowerCase(); const lowered = normalizedBody.toLowerCase();
const commands = cfg ? listChatCommandsForConfig(cfg) : listChatCommands(); const commands = cfg ? listChatCommandsForConfig(cfg) : listChatCommands();

View File

@@ -7,6 +7,7 @@ import {
listChatCommandsForConfig, listChatCommandsForConfig,
listNativeCommandSpecs, listNativeCommandSpecs,
listNativeCommandSpecsForConfig, listNativeCommandSpecsForConfig,
normalizeCommandBody,
shouldHandleTextCommands, shouldHandleTextCommands,
} from "./commands-registry.js"; } from "./commands-registry.js";
@@ -92,4 +93,26 @@ describe("commands registry", () => {
}), }),
).toBe(true); ).toBe(true);
}); });
it("normalizes telegram-style command mentions for the current bot", () => {
expect(
normalizeCommandBody("/help@clawdbot", { botUsername: "clawdbot" }),
).toBe("/help");
expect(
normalizeCommandBody("/help@clawdbot args", {
botUsername: "clawdbot",
}),
).toBe("/help args");
expect(
normalizeCommandBody("/help@clawdbot: args", {
botUsername: "clawdbot",
}),
).toBe("/help args");
});
it("keeps telegram-style command mentions for other bots", () => {
expect(
normalizeCommandBody("/help@otherbot", { botUsername: "clawdbot" }),
).toBe("/help@otherbot");
});
}); });

View File

@@ -361,7 +361,14 @@ export function buildCommandText(commandName: string, args?: string): string {
return trimmedArgs ? `/${commandName} ${trimmedArgs}` : `/${commandName}`; return trimmedArgs ? `/${commandName} ${trimmedArgs}` : `/${commandName}`;
} }
export function normalizeCommandBody(raw: string): string { export type CommandNormalizeOptions = {
botUsername?: string;
};
export function normalizeCommandBody(
raw: string,
options?: CommandNormalizeOptions,
): string {
const trimmed = raw.trim(); const trimmed = raw.trim();
if (!trimmed.startsWith("/")) return trimmed; if (!trimmed.startsWith("/")) return trimmed;
@@ -378,17 +385,26 @@ export function normalizeCommandBody(raw: string): string {
})() })()
: singleLine; : singleLine;
const lowered = normalized.toLowerCase(); const normalizedBotUsername = options?.botUsername?.trim().toLowerCase();
const mentionMatch = normalizedBotUsername
? normalized.match(/^\/([^\s@]+)@([^\s]+)(.*)$/)
: null;
const commandBody =
mentionMatch && mentionMatch[2].toLowerCase() === normalizedBotUsername
? `/${mentionMatch[1]}${mentionMatch[3] ?? ""}`
: normalized;
const lowered = commandBody.toLowerCase();
const exact = TEXT_ALIAS_MAP.get(lowered); const exact = TEXT_ALIAS_MAP.get(lowered);
if (exact) return exact.canonical; if (exact) return exact.canonical;
const tokenMatch = normalized.match(/^\/([^\s]+)(?:\s+([\s\S]+))?$/); const tokenMatch = commandBody.match(/^\/([^\s]+)(?:\s+([\s\S]+))?$/);
if (!tokenMatch) return normalized; if (!tokenMatch) return commandBody;
const [, token, rest] = tokenMatch; const [, token, rest] = tokenMatch;
const tokenKey = `/${token.toLowerCase()}`; const tokenKey = `/${token.toLowerCase()}`;
const tokenSpec = TEXT_ALIAS_MAP.get(tokenKey); const tokenSpec = TEXT_ALIAS_MAP.get(tokenKey);
if (!tokenSpec) return normalized; if (!tokenSpec) return commandBody;
if (rest && !tokenSpec.acceptsArgs) return normalized; if (rest && !tokenSpec.acceptsArgs) return commandBody;
const normalizedRest = rest?.trimStart(); const normalizedRest = rest?.trimStart();
return normalizedRest return normalizedRest
? `${tokenSpec.canonical} ${normalizedRest}` ? `${tokenSpec.canonical} ${normalizedRest}`

View File

@@ -17,6 +17,7 @@ import { hasControlCommand } from "../auto-reply/command-detection.js";
import { import {
buildCommandText, buildCommandText,
listNativeCommandSpecsForConfig, listNativeCommandSpecsForConfig,
normalizeCommandBody,
} from "../auto-reply/commands-registry.js"; } from "../auto-reply/commands-registry.js";
import { formatAgentEnvelope } from "../auto-reply/envelope.js"; import { formatAgentEnvelope } from "../auto-reply/envelope.js";
import { import {
@@ -557,7 +558,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
!wasMentioned && !wasMentioned &&
!hasAnyMention && !hasAnyMention &&
commandAuthorized && commandAuthorized &&
hasControlCommand(msg.text ?? msg.caption ?? "", cfg); hasControlCommand(msg.text ?? msg.caption ?? "", cfg, { botUsername });
const effectiveWasMentioned = wasMentioned || shouldBypassMention; const effectiveWasMentioned = wasMentioned || shouldBypassMention;
const canDetectMention = Boolean(botUsername) || mentionRegexes.length > 0; const canDetectMention = Boolean(botUsername) || mentionRegexes.length > 0;
if (isGroup && requireMention && canDetectMention) { if (isGroup && requireMention && canDetectMention) {
@@ -682,10 +683,11 @@ export function createTelegramBot(opts: TelegramBotOptions) {
].filter((entry): entry is string => Boolean(entry)); ].filter((entry): entry is string => Boolean(entry));
const groupSystemPrompt = const groupSystemPrompt =
systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined; systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
const commandBody = normalizeCommandBody(rawBody, { botUsername });
const ctxPayload = { const ctxPayload = {
Body: combinedBody, Body: combinedBody,
RawBody: rawBody, RawBody: rawBody,
CommandBody: rawBody, CommandBody: commandBody,
From: isGroup From: isGroup
? buildTelegramGroupFrom(chatId, messageThreadId) ? buildTelegramGroupFrom(chatId, messageThreadId)
: `telegram:${chatId}`, : `telegram:${chatId}`,