mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-26 09:05:14 +00:00
150 lines
4.7 KiB
TypeScript
150 lines
4.7 KiB
TypeScript
import type { Message, UserFromGetMe } from "grammy/types";
|
||
import { parseExecApprovalCommandText } from "openclaw/plugin-sdk/approval-reply-runtime";
|
||
import {
|
||
listChatCommands,
|
||
maybeResolveTextAlias,
|
||
normalizeCommandBody,
|
||
} from "openclaw/plugin-sdk/command-auth-native";
|
||
import {
|
||
isAbortRequestText,
|
||
isBtwRequestText,
|
||
} from "openclaw/plugin-sdk/command-primitives-runtime";
|
||
import { resolveTelegramForumThreadId } from "./bot/helpers.js";
|
||
|
||
const TELEGRAM_READ_ONLY_STATUS_COMMAND_KEYS = new Set([
|
||
"commands",
|
||
"context",
|
||
"help",
|
||
"status",
|
||
"tasks",
|
||
"tools",
|
||
"whoami",
|
||
]);
|
||
|
||
type TelegramSequentialKeyContext = {
|
||
chat?: { id?: number };
|
||
me?: UserFromGetMe;
|
||
message?: Message;
|
||
channelPost?: Message;
|
||
editedMessage?: Message;
|
||
editedChannelPost?: Message;
|
||
update?: {
|
||
message?: Message;
|
||
edited_message?: Message;
|
||
channel_post?: Message;
|
||
edited_channel_post?: Message;
|
||
callback_query?: { message?: Message; data?: string };
|
||
message_reaction?: { chat?: { id?: number } };
|
||
};
|
||
};
|
||
|
||
export function isTelegramReadOnlyControlLaneText(params: {
|
||
rawText?: string;
|
||
botUsername?: string;
|
||
}): boolean {
|
||
// Only read-only status commands should bypass the per-topic lane.
|
||
// Diagnostics and export commands materialize state and should not interleave with an active turn.
|
||
const normalizedBody = normalizeCommandBody(
|
||
params.rawText?.trim() ?? "",
|
||
params.botUsername ? { botUsername: params.botUsername } : undefined,
|
||
);
|
||
const alias = maybeResolveTextAlias(normalizedBody);
|
||
if (!alias) {
|
||
return false;
|
||
}
|
||
const command = listChatCommands().find((entry) =>
|
||
entry.textAliases.some((candidate) => candidate.trim().toLowerCase() === alias),
|
||
);
|
||
return command?.category === "status" && TELEGRAM_READ_ONLY_STATUS_COMMAND_KEYS.has(command.key);
|
||
}
|
||
|
||
function isTelegramTargetedStopCommand(rawText?: string, botUsername?: string): boolean {
|
||
const trimmed = rawText?.trim();
|
||
if (!trimmed) {
|
||
return false;
|
||
}
|
||
// Isolated ingress may not have getMe() metadata yet. A targeted Telegram
|
||
// /stop@bot command still needs the control lane so it can cancel a busy turn.
|
||
const match = trimmed.match(/^\/stop@([A-Za-z0-9_]+)(?:$|\s|[.!?…,,。;;::'"’”)\]}])/iu);
|
||
if (!match) {
|
||
return false;
|
||
}
|
||
const normalizedBotUsername = botUsername?.trim().toLowerCase();
|
||
if (!normalizedBotUsername) {
|
||
return true;
|
||
}
|
||
return match[1]?.toLowerCase() === normalizedBotUsername;
|
||
}
|
||
|
||
export function isTelegramControlLaneText(params: {
|
||
rawText?: string;
|
||
botUsername?: string;
|
||
}): boolean {
|
||
if (
|
||
isAbortRequestText(
|
||
params.rawText,
|
||
params.botUsername ? { botUsername: params.botUsername } : undefined,
|
||
)
|
||
) {
|
||
return true;
|
||
}
|
||
if (isTelegramTargetedStopCommand(params.rawText, params.botUsername)) {
|
||
return true;
|
||
}
|
||
return isTelegramReadOnlyControlLaneText(params);
|
||
}
|
||
|
||
export function getTelegramSequentialKey(ctx: TelegramSequentialKeyContext): string {
|
||
const reaction = ctx.update?.message_reaction;
|
||
if (reaction?.chat?.id) {
|
||
return `telegram:${reaction.chat.id}`;
|
||
}
|
||
const msg =
|
||
ctx.message ??
|
||
ctx.channelPost ??
|
||
ctx.editedMessage ??
|
||
ctx.editedChannelPost ??
|
||
ctx.update?.message ??
|
||
ctx.update?.edited_message ??
|
||
ctx.update?.channel_post ??
|
||
ctx.update?.edited_channel_post ??
|
||
ctx.update?.callback_query?.message;
|
||
const chatId = msg?.chat?.id ?? ctx.chat?.id;
|
||
const rawText = msg?.text ?? msg?.caption;
|
||
const botUsername = ctx.me?.username;
|
||
if (isTelegramControlLaneText({ rawText, botUsername })) {
|
||
if (typeof chatId === "number") {
|
||
return `telegram:${chatId}:control`;
|
||
}
|
||
return "telegram:control";
|
||
}
|
||
if (isBtwRequestText(rawText, botUsername ? { botUsername } : undefined)) {
|
||
const messageId = msg?.message_id;
|
||
if (typeof chatId === "number" && typeof messageId === "number") {
|
||
return `telegram:${chatId}:btw:${messageId}`;
|
||
}
|
||
if (typeof chatId === "number") {
|
||
return `telegram:${chatId}:btw`;
|
||
}
|
||
return "telegram:btw";
|
||
}
|
||
const callbackData = ctx.update?.callback_query?.data;
|
||
if (callbackData && parseExecApprovalCommandText(callbackData) !== null) {
|
||
if (typeof chatId === "number") {
|
||
return `telegram:${chatId}:approval`;
|
||
}
|
||
return "telegram:approval";
|
||
}
|
||
const isGroup = msg?.chat?.type === "group" || msg?.chat?.type === "supergroup";
|
||
const messageThreadId = msg?.message_thread_id;
|
||
const isForum =
|
||
msg?.chat?.is_forum ?? (msg?.chat?.type === "supergroup" && msg?.is_topic_message === true);
|
||
const threadId = isGroup
|
||
? resolveTelegramForumThreadId({ isForum, messageThreadId })
|
||
: messageThreadId;
|
||
if (typeof chatId === "number") {
|
||
return threadId != null ? `telegram:${chatId}:topic:${threadId}` : `telegram:${chatId}`;
|
||
}
|
||
return "telegram:unknown";
|
||
}
|