Files
openclaw/extensions/telegram/src/reply-parameters.ts
Rubén Cuevas f9146cabfc fix(telegram): preserve native quote replies
Preserve exact Telegram selected quote text for native quote replies, share Telegram reply parameter construction between bot delivery and direct outbound sends, and retry with legacy replies when Telegram rejects native quote parameters.\n\nThanks @rubencu.
2026-04-26 06:09:43 +01:00

129 lines
4.3 KiB
TypeScript

import type { MessageEntity } from "@grammyjs/types";
import { buildTelegramThreadParams, type TelegramThreadSpec } from "./bot/helpers.js";
import { normalizeTelegramReplyToMessageId } from "./outbound-params.js";
export type TelegramReplyParameters = {
message_id: number;
allow_sending_without_reply: true;
quote?: string;
quote_position?: number;
quote_entities?: MessageEntity[];
};
export type TelegramThreadReplyParams = {
message_thread_id?: number;
reply_parameters?: TelegramReplyParameters;
reply_to_message_id?: number;
allow_sending_without_reply?: true;
};
export function resolveTelegramSendThreadSpec(params: {
targetMessageThreadId?: number;
messageThreadId?: number;
chatType?: "direct" | "group" | "unknown";
}): TelegramThreadSpec | undefined {
const messageThreadId =
params.messageThreadId != null ? params.messageThreadId : params.targetMessageThreadId;
if (messageThreadId == null) {
return undefined;
}
// Telegram supports DM topics; keep direct chat thread IDs and rely on
// thread-not-found retry fallback when a plain DM rejects them.
return {
id: messageThreadId,
scope: params.chatType === "direct" ? "dm" : "forum",
};
}
export function buildTelegramThreadReplyParams(opts?: {
thread?: TelegramThreadSpec | null;
replyToMessageId?: number;
replyQuoteMessageId?: number;
replyQuoteText?: string;
replyQuotePosition?: number;
replyQuoteEntities?: unknown[];
useReplyIdAsQuoteSource?: boolean;
}): TelegramThreadReplyParams {
const params: TelegramThreadReplyParams = {};
const threadParams = buildTelegramThreadParams(opts?.thread);
if (threadParams) {
params.message_thread_id = threadParams.message_thread_id;
}
const replyToMessageId = normalizeTelegramReplyToMessageId(opts?.replyToMessageId);
if (replyToMessageId == null) {
return params;
}
const defaultQuoteMessageId =
opts?.useReplyIdAsQuoteSource === true ? replyToMessageId : undefined;
const replyQuoteMessageId = normalizeTelegramReplyToMessageId(
opts?.replyQuoteMessageId ?? defaultQuoteMessageId,
);
const replyQuoteTextRaw =
replyQuoteMessageId === replyToMessageId ? opts?.replyQuoteText : undefined;
const replyQuoteText = replyQuoteTextRaw?.trim() ? replyQuoteTextRaw : undefined;
if (!replyQuoteText) {
params.reply_to_message_id = replyToMessageId;
params.allow_sending_without_reply = true;
return params;
}
const replyParameters: TelegramReplyParameters = {
message_id: replyToMessageId,
quote: replyQuoteText,
allow_sending_without_reply: true,
};
if (typeof opts?.replyQuotePosition === "number" && Number.isFinite(opts.replyQuotePosition)) {
replyParameters.quote_position = Math.trunc(opts.replyQuotePosition);
}
if (Array.isArray(opts?.replyQuoteEntities) && opts.replyQuoteEntities.length > 0) {
replyParameters.quote_entities = opts.replyQuoteEntities as MessageEntity[];
}
params.reply_parameters = replyParameters;
return params;
}
export function buildTelegramSendParams(opts?: {
replyToMessageId?: number;
replyQuoteMessageId?: number;
replyQuoteText?: string;
replyQuotePosition?: number;
replyQuoteEntities?: unknown[];
thread?: TelegramThreadSpec | null;
silent?: boolean;
useReplyIdAsQuoteSource?: boolean;
}): Record<string, unknown> {
const params: Record<string, unknown> = { ...buildTelegramThreadReplyParams(opts) };
if (opts?.silent === true) {
params.disable_notification = true;
}
return params;
}
export function getTelegramNativeQuoteReplyMessageId(
params: Record<string, unknown> | undefined,
): number | undefined {
const replyParameters = params?.reply_parameters;
if (!replyParameters || typeof replyParameters !== "object") {
return undefined;
}
const messageId = (replyParameters as { message_id?: unknown }).message_id;
return typeof messageId === "number" && Number.isFinite(messageId) ? messageId : undefined;
}
export function removeTelegramNativeQuoteParam(
params: Record<string, unknown> | undefined,
): Record<string, unknown> {
if (!params) {
return {};
}
const replyMessageId = getTelegramNativeQuoteReplyMessageId(params);
const { reply_parameters: _ignored, ...rest } = params;
if (replyMessageId != null) {
rest.reply_to_message_id = replyMessageId;
rest.allow_sending_without_reply = true;
}
return rest;
}