fix: wire telegram quote support (#2900)
Co-authored-by: aduk059 <aduk059@users.noreply.github.com>
This commit is contained in:
parent
ca431942d7
commit
1f73dac923
@ -60,6 +60,9 @@ function buildSendSchema(options: { includeButtons: boolean; includeCards: boole
|
|||||||
threadId: Type.Optional(Type.String()),
|
threadId: Type.Optional(Type.String()),
|
||||||
asVoice: Type.Optional(Type.Boolean()),
|
asVoice: Type.Optional(Type.Boolean()),
|
||||||
silent: Type.Optional(Type.Boolean()),
|
silent: Type.Optional(Type.Boolean()),
|
||||||
|
quoteText: Type.Optional(
|
||||||
|
Type.String({ description: "Quote text for Telegram reply_parameters" }),
|
||||||
|
),
|
||||||
bestEffort: Type.Optional(Type.Boolean()),
|
bestEffort: Type.Optional(Type.Boolean()),
|
||||||
gifPlayback: Type.Optional(Type.Boolean()),
|
gifPlayback: Type.Optional(Type.Boolean()),
|
||||||
buttons: Type.Optional(
|
buttons: Type.Optional(
|
||||||
|
|||||||
@ -261,6 +261,31 @@ describe("handleTelegramAction", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("passes quoteText when provided", async () => {
|
||||||
|
const cfg = {
|
||||||
|
channels: { telegram: { botToken: "tok" } },
|
||||||
|
} as MoltbotConfig;
|
||||||
|
await handleTelegramAction(
|
||||||
|
{
|
||||||
|
action: "sendMessage",
|
||||||
|
to: "123456",
|
||||||
|
content: "Replying now",
|
||||||
|
replyToMessageId: 144,
|
||||||
|
quoteText: "The text you want to quote",
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
expect(sendMessageTelegram).toHaveBeenCalledWith(
|
||||||
|
"123456",
|
||||||
|
"Replying now",
|
||||||
|
expect.objectContaining({
|
||||||
|
token: "tok",
|
||||||
|
replyToMessageId: 144,
|
||||||
|
quoteText: "The text you want to quote",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("allows media-only messages without content", async () => {
|
it("allows media-only messages without content", async () => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
channels: { telegram: { botToken: "tok" } },
|
channels: { telegram: { botToken: "tok" } },
|
||||||
|
|||||||
@ -165,6 +165,7 @@ export async function handleTelegramAction(
|
|||||||
const messageThreadId = readNumberParam(params, "messageThreadId", {
|
const messageThreadId = readNumberParam(params, "messageThreadId", {
|
||||||
integer: true,
|
integer: true,
|
||||||
});
|
});
|
||||||
|
const quoteText = readStringParam(params, "quoteText");
|
||||||
const token = resolveTelegramToken(cfg, { accountId }).token;
|
const token = resolveTelegramToken(cfg, { accountId }).token;
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -178,6 +179,7 @@ export async function handleTelegramAction(
|
|||||||
buttons,
|
buttons,
|
||||||
replyToMessageId: replyToMessageId ?? undefined,
|
replyToMessageId: replyToMessageId ?? undefined,
|
||||||
messageThreadId: messageThreadId ?? undefined,
|
messageThreadId: messageThreadId ?? undefined,
|
||||||
|
quoteText: quoteText ?? undefined,
|
||||||
asVoice: typeof params.asVoice === "boolean" ? params.asVoice : undefined,
|
asVoice: typeof params.asVoice === "boolean" ? params.asVoice : undefined,
|
||||||
silent: typeof params.silent === "boolean" ? params.silent : undefined,
|
silent: typeof params.silent === "boolean" ? params.silent : undefined,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -49,6 +49,7 @@ export type MsgContext = {
|
|||||||
ReplyToIdFull?: string;
|
ReplyToIdFull?: string;
|
||||||
ReplyToBody?: string;
|
ReplyToBody?: string;
|
||||||
ReplyToSender?: string;
|
ReplyToSender?: string;
|
||||||
|
ReplyToIsQuote?: boolean;
|
||||||
ForwardedFrom?: string;
|
ForwardedFrom?: string;
|
||||||
ForwardedFromType?: string;
|
ForwardedFromType?: string;
|
||||||
ForwardedFromId?: string;
|
ForwardedFromId?: string;
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
b6d3dea7c656c8a480059c32e954c4d39053ff79c4e9c69b38f4c04e3f0280d4
|
bd5789522e6bde45274e15fdd45b10c9a41da378b190d6f42cef5ef2a69d72a7
|
||||||
|
|||||||
@ -23,6 +23,7 @@ function readTelegramSendParams(params: Record<string, unknown>) {
|
|||||||
const buttons = params.buttons;
|
const buttons = params.buttons;
|
||||||
const asVoice = typeof params.asVoice === "boolean" ? params.asVoice : undefined;
|
const asVoice = typeof params.asVoice === "boolean" ? params.asVoice : undefined;
|
||||||
const silent = typeof params.silent === "boolean" ? params.silent : undefined;
|
const silent = typeof params.silent === "boolean" ? params.silent : undefined;
|
||||||
|
const quoteText = readStringParam(params, "quoteText");
|
||||||
return {
|
return {
|
||||||
to,
|
to,
|
||||||
content,
|
content,
|
||||||
@ -32,6 +33,7 @@ function readTelegramSendParams(params: Record<string, unknown>) {
|
|||||||
buttons,
|
buttons,
|
||||||
asVoice,
|
asVoice,
|
||||||
silent,
|
silent,
|
||||||
|
quoteText: quoteText ?? undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -56,8 +56,10 @@ export const telegramOutbound: ChannelOutboundAdapter = {
|
|||||||
const replyToMessageId = parseReplyToMessageId(replyToId);
|
const replyToMessageId = parseReplyToMessageId(replyToId);
|
||||||
const messageThreadId = parseThreadId(threadId);
|
const messageThreadId = parseThreadId(threadId);
|
||||||
const telegramData = payload.channelData?.telegram as
|
const telegramData = payload.channelData?.telegram as
|
||||||
| { buttons?: Array<Array<{ text: string; callback_data: string }>> }
|
| { buttons?: Array<Array<{ text: string; callback_data: string }>>; quoteText?: string }
|
||||||
| undefined;
|
| undefined;
|
||||||
|
const quoteText =
|
||||||
|
typeof telegramData?.quoteText === "string" ? telegramData.quoteText : undefined;
|
||||||
const text = payload.text ?? "";
|
const text = payload.text ?? "";
|
||||||
const mediaUrls = payload.mediaUrls?.length
|
const mediaUrls = payload.mediaUrls?.length
|
||||||
? payload.mediaUrls
|
? payload.mediaUrls
|
||||||
@ -69,6 +71,7 @@ export const telegramOutbound: ChannelOutboundAdapter = {
|
|||||||
textMode: "html" as const,
|
textMode: "html" as const,
|
||||||
messageThreadId,
|
messageThreadId,
|
||||||
replyToMessageId,
|
replyToMessageId,
|
||||||
|
quoteText,
|
||||||
accountId: accountId ?? undefined,
|
accountId: accountId ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -480,13 +480,13 @@ export const buildTelegramMessageContext = async ({
|
|||||||
const replyTarget = describeReplyTarget(msg);
|
const replyTarget = describeReplyTarget(msg);
|
||||||
const forwardOrigin = normalizeForwardedContext(msg);
|
const forwardOrigin = normalizeForwardedContext(msg);
|
||||||
const replySuffix = replyTarget
|
const replySuffix = replyTarget
|
||||||
? (replyTarget as any).isQuote
|
? replyTarget.kind === "quote"
|
||||||
? `\n\n[Quoting ${replyTarget.sender}${
|
? `\n\n[Quoting ${replyTarget.sender}${
|
||||||
replyTarget.id ? ` id:${replyTarget.id}` : ""
|
replyTarget.id ? ` id:${replyTarget.id}` : ""
|
||||||
}]\n"${replyTarget.body}"\n[/Quoting]`
|
}]\n"${replyTarget.body}"\n[/Quoting]`
|
||||||
: `\n\n[Replying to ${replyTarget.sender}${
|
: `\n\n[Replying to ${replyTarget.sender}${
|
||||||
replyTarget.id ? ` id:${replyTarget.id}` : ""
|
replyTarget.id ? ` id:${replyTarget.id}` : ""
|
||||||
}]\n${replyTarget.body}\n[/Replying]`
|
}]\n${replyTarget.body}\n[/Replying]`
|
||||||
: "";
|
: "";
|
||||||
const forwardPrefix = forwardOrigin
|
const forwardPrefix = forwardOrigin
|
||||||
? `[Forwarded from ${forwardOrigin.from}${
|
? `[Forwarded from ${forwardOrigin.from}${
|
||||||
@ -569,6 +569,7 @@ export const buildTelegramMessageContext = async ({
|
|||||||
ReplyToId: replyTarget?.id,
|
ReplyToId: replyTarget?.id,
|
||||||
ReplyToBody: replyTarget?.body,
|
ReplyToBody: replyTarget?.body,
|
||||||
ReplyToSender: replyTarget?.sender,
|
ReplyToSender: replyTarget?.sender,
|
||||||
|
ReplyToIsQuote: replyTarget?.kind === "quote" ? true : undefined,
|
||||||
ForwardedFrom: forwardOrigin?.from,
|
ForwardedFrom: forwardOrigin?.from,
|
||||||
ForwardedFromType: forwardOrigin?.fromType,
|
ForwardedFromType: forwardOrigin?.fromType,
|
||||||
ForwardedFromId: forwardOrigin?.fromId,
|
ForwardedFromId: forwardOrigin?.fromId,
|
||||||
|
|||||||
@ -210,6 +210,10 @@ export const dispatchTelegramMessage = async ({
|
|||||||
draftStream?.stop();
|
draftStream?.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const replyQuoteText =
|
||||||
|
ctxPayload.ReplyToIsQuote && ctxPayload.ReplyToBody
|
||||||
|
? ctxPayload.ReplyToBody.trim() || undefined
|
||||||
|
: undefined;
|
||||||
await deliverReplies({
|
await deliverReplies({
|
||||||
replies: [payload],
|
replies: [payload],
|
||||||
chatId: String(chatId),
|
chatId: String(chatId),
|
||||||
@ -223,6 +227,7 @@ export const dispatchTelegramMessage = async ({
|
|||||||
chunkMode,
|
chunkMode,
|
||||||
onVoiceRecording: sendRecordVoice,
|
onVoiceRecording: sendRecordVoice,
|
||||||
linkPreview: telegramCfg.linkPreview,
|
linkPreview: telegramCfg.linkPreview,
|
||||||
|
replyQuoteText,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (err, info) => {
|
onError: (err, info) => {
|
||||||
|
|||||||
@ -894,6 +894,73 @@ describe("createTelegramBot", () => {
|
|||||||
expect(payload.ReplyToSender).toBe("Ada");
|
expect(payload.ReplyToSender).toBe("Ada");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses quote text when a Telegram partial reply is received", async () => {
|
||||||
|
onSpy.mockReset();
|
||||||
|
sendMessageSpy.mockReset();
|
||||||
|
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||||
|
replySpy.mockReset();
|
||||||
|
|
||||||
|
createTelegramBot({ token: "tok" });
|
||||||
|
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||||
|
|
||||||
|
await handler({
|
||||||
|
message: {
|
||||||
|
chat: { id: 7, type: "private" },
|
||||||
|
text: "Sure, see below",
|
||||||
|
date: 1736380800,
|
||||||
|
reply_to_message: {
|
||||||
|
message_id: 9001,
|
||||||
|
text: "Can you summarize this?",
|
||||||
|
from: { first_name: "Ada" },
|
||||||
|
},
|
||||||
|
quote: {
|
||||||
|
text: "summarize this",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
me: { username: "moltbot_bot" },
|
||||||
|
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||||
|
const payload = replySpy.mock.calls[0][0];
|
||||||
|
expect(payload.Body).toContain("[Quoting Ada id:9001]");
|
||||||
|
expect(payload.Body).toContain('"summarize this"');
|
||||||
|
expect(payload.ReplyToId).toBe("9001");
|
||||||
|
expect(payload.ReplyToBody).toBe("summarize this");
|
||||||
|
expect(payload.ReplyToSender).toBe("Ada");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles quote-only replies without reply metadata", async () => {
|
||||||
|
onSpy.mockReset();
|
||||||
|
sendMessageSpy.mockReset();
|
||||||
|
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
|
||||||
|
replySpy.mockReset();
|
||||||
|
|
||||||
|
createTelegramBot({ token: "tok" });
|
||||||
|
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||||
|
|
||||||
|
await handler({
|
||||||
|
message: {
|
||||||
|
chat: { id: 7, type: "private" },
|
||||||
|
text: "Sure, see below",
|
||||||
|
date: 1736380800,
|
||||||
|
quote: {
|
||||||
|
text: "summarize this",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
me: { username: "moltbot_bot" },
|
||||||
|
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||||
|
const payload = replySpy.mock.calls[0][0];
|
||||||
|
expect(payload.Body).toContain("[Quoting unknown sender]");
|
||||||
|
expect(payload.Body).toContain('"summarize this"');
|
||||||
|
expect(payload.ReplyToId).toBeUndefined();
|
||||||
|
expect(payload.ReplyToBody).toBe("summarize this");
|
||||||
|
expect(payload.ReplyToSender).toBe("unknown sender");
|
||||||
|
});
|
||||||
|
|
||||||
it("sends replies without native reply threading", async () => {
|
it("sends replies without native reply threading", async () => {
|
||||||
onSpy.mockReset();
|
onSpy.mockReset();
|
||||||
sendMessageSpy.mockReset();
|
sendMessageSpy.mockReset();
|
||||||
|
|||||||
@ -168,6 +168,37 @@ describe("deliverReplies", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses reply_parameters when quote text is provided", async () => {
|
||||||
|
const runtime = { error: vi.fn(), log: vi.fn() };
|
||||||
|
const sendMessage = vi.fn().mockResolvedValue({
|
||||||
|
message_id: 10,
|
||||||
|
chat: { id: "123" },
|
||||||
|
});
|
||||||
|
const bot = { api: { sendMessage } } as unknown as Bot;
|
||||||
|
|
||||||
|
await deliverReplies({
|
||||||
|
replies: [{ text: "Hello there", replyToId: "500" }],
|
||||||
|
chatId: "123",
|
||||||
|
token: "tok",
|
||||||
|
runtime,
|
||||||
|
bot,
|
||||||
|
replyToMode: "all",
|
||||||
|
textLimit: 4000,
|
||||||
|
replyQuoteText: "quoted text",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendMessage).toHaveBeenCalledWith(
|
||||||
|
"123",
|
||||||
|
expect.any(String),
|
||||||
|
expect.objectContaining({
|
||||||
|
reply_parameters: {
|
||||||
|
message_id: 500,
|
||||||
|
quote: "quoted text",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("falls back to text when sendVoice fails with VOICE_MESSAGES_FORBIDDEN", async () => {
|
it("falls back to text when sendVoice fails with VOICE_MESSAGES_FORBIDDEN", async () => {
|
||||||
const runtime = { error: vi.fn(), log: vi.fn() };
|
const runtime = { error: vi.fn(), log: vi.fn() };
|
||||||
const sendVoice = vi
|
const sendVoice = vi
|
||||||
|
|||||||
@ -42,9 +42,20 @@ export async function deliverReplies(params: {
|
|||||||
onVoiceRecording?: () => Promise<void> | void;
|
onVoiceRecording?: () => Promise<void> | void;
|
||||||
/** Controls whether link previews are shown. Default: true (previews enabled). */
|
/** Controls whether link previews are shown. Default: true (previews enabled). */
|
||||||
linkPreview?: boolean;
|
linkPreview?: boolean;
|
||||||
|
/** Optional quote text for Telegram reply_parameters. */
|
||||||
|
replyQuoteText?: string;
|
||||||
}) {
|
}) {
|
||||||
const { replies, chatId, runtime, bot, replyToMode, textLimit, messageThreadId, linkPreview } =
|
const {
|
||||||
params;
|
replies,
|
||||||
|
chatId,
|
||||||
|
runtime,
|
||||||
|
bot,
|
||||||
|
replyToMode,
|
||||||
|
textLimit,
|
||||||
|
messageThreadId,
|
||||||
|
linkPreview,
|
||||||
|
replyQuoteText,
|
||||||
|
} = params;
|
||||||
const chunkMode = params.chunkMode ?? "length";
|
const chunkMode = params.chunkMode ?? "length";
|
||||||
const threadParams = buildTelegramThreadParams(messageThreadId);
|
const threadParams = buildTelegramThreadParams(messageThreadId);
|
||||||
let hasReplied = false;
|
let hasReplied = false;
|
||||||
@ -97,6 +108,7 @@ export async function deliverReplies(params: {
|
|||||||
await sendTelegramText(bot, chatId, chunk.html, runtime, {
|
await sendTelegramText(bot, chatId, chunk.html, runtime, {
|
||||||
replyToMessageId:
|
replyToMessageId:
|
||||||
replyToId && (replyToMode === "all" || !hasReplied) ? replyToId : undefined,
|
replyToId && (replyToMode === "all" || !hasReplied) ? replyToId : undefined,
|
||||||
|
replyQuoteText,
|
||||||
messageThreadId,
|
messageThreadId,
|
||||||
textMode: "html",
|
textMode: "html",
|
||||||
plainText: chunk.text,
|
plainText: chunk.text,
|
||||||
@ -140,13 +152,14 @@ export async function deliverReplies(params: {
|
|||||||
const shouldAttachButtonsToMedia = isFirstMedia && replyMarkup && !followUpText;
|
const shouldAttachButtonsToMedia = isFirstMedia && replyMarkup && !followUpText;
|
||||||
const mediaParams: Record<string, unknown> = {
|
const mediaParams: Record<string, unknown> = {
|
||||||
caption: htmlCaption,
|
caption: htmlCaption,
|
||||||
reply_to_message_id: replyToMessageId,
|
|
||||||
...(htmlCaption ? { parse_mode: "HTML" } : {}),
|
...(htmlCaption ? { parse_mode: "HTML" } : {}),
|
||||||
...(shouldAttachButtonsToMedia ? { reply_markup: replyMarkup } : {}),
|
...(shouldAttachButtonsToMedia ? { reply_markup: replyMarkup } : {}),
|
||||||
|
...buildTelegramSendParams({
|
||||||
|
replyToMessageId,
|
||||||
|
messageThreadId,
|
||||||
|
replyQuoteText,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
if (threadParams) {
|
|
||||||
mediaParams.message_thread_id = threadParams.message_thread_id;
|
|
||||||
}
|
|
||||||
if (isGif) {
|
if (isGif) {
|
||||||
await withTelegramApiErrorLogging({
|
await withTelegramApiErrorLogging({
|
||||||
operation: "sendAnimation",
|
operation: "sendAnimation",
|
||||||
@ -207,6 +220,7 @@ export async function deliverReplies(params: {
|
|||||||
messageThreadId,
|
messageThreadId,
|
||||||
linkPreview,
|
linkPreview,
|
||||||
replyMarkup,
|
replyMarkup,
|
||||||
|
replyQuoteText,
|
||||||
});
|
});
|
||||||
// Skip this media item; continue with next.
|
// Skip this media item; continue with next.
|
||||||
continue;
|
continue;
|
||||||
@ -391,6 +405,7 @@ async function sendTelegramVoiceFallbackText(opts: {
|
|||||||
messageThreadId?: number;
|
messageThreadId?: number;
|
||||||
linkPreview?: boolean;
|
linkPreview?: boolean;
|
||||||
replyMarkup?: ReturnType<typeof buildInlineKeyboard>;
|
replyMarkup?: ReturnType<typeof buildInlineKeyboard>;
|
||||||
|
replyQuoteText?: string;
|
||||||
}): Promise<boolean> {
|
}): Promise<boolean> {
|
||||||
const chunks = opts.chunkText(opts.text);
|
const chunks = opts.chunkText(opts.text);
|
||||||
let hasReplied = opts.hasReplied;
|
let hasReplied = opts.hasReplied;
|
||||||
@ -399,6 +414,7 @@ async function sendTelegramVoiceFallbackText(opts: {
|
|||||||
await sendTelegramText(opts.bot, opts.chatId, chunk.html, opts.runtime, {
|
await sendTelegramText(opts.bot, opts.chatId, chunk.html, opts.runtime, {
|
||||||
replyToMessageId:
|
replyToMessageId:
|
||||||
opts.replyToId && (opts.replyToMode === "all" || !hasReplied) ? opts.replyToId : undefined,
|
opts.replyToId && (opts.replyToMode === "all" || !hasReplied) ? opts.replyToId : undefined,
|
||||||
|
replyQuoteText: opts.replyQuoteText,
|
||||||
messageThreadId: opts.messageThreadId,
|
messageThreadId: opts.messageThreadId,
|
||||||
textMode: "html",
|
textMode: "html",
|
||||||
plainText: chunk.text,
|
plainText: chunk.text,
|
||||||
@ -415,11 +431,20 @@ async function sendTelegramVoiceFallbackText(opts: {
|
|||||||
function buildTelegramSendParams(opts?: {
|
function buildTelegramSendParams(opts?: {
|
||||||
replyToMessageId?: number;
|
replyToMessageId?: number;
|
||||||
messageThreadId?: number;
|
messageThreadId?: number;
|
||||||
|
replyQuoteText?: string;
|
||||||
}): Record<string, unknown> {
|
}): Record<string, unknown> {
|
||||||
const threadParams = buildTelegramThreadParams(opts?.messageThreadId);
|
const threadParams = buildTelegramThreadParams(opts?.messageThreadId);
|
||||||
const params: Record<string, unknown> = {};
|
const params: Record<string, unknown> = {};
|
||||||
|
const quoteText = opts?.replyQuoteText?.trim();
|
||||||
if (opts?.replyToMessageId) {
|
if (opts?.replyToMessageId) {
|
||||||
params.reply_to_message_id = opts.replyToMessageId;
|
if (quoteText) {
|
||||||
|
params.reply_parameters = {
|
||||||
|
message_id: Math.trunc(opts.replyToMessageId),
|
||||||
|
quote: quoteText,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
params.reply_to_message_id = opts.replyToMessageId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (threadParams) {
|
if (threadParams) {
|
||||||
params.message_thread_id = threadParams.message_thread_id;
|
params.message_thread_id = threadParams.message_thread_id;
|
||||||
@ -434,6 +459,7 @@ async function sendTelegramText(
|
|||||||
runtime: RuntimeEnv,
|
runtime: RuntimeEnv,
|
||||||
opts?: {
|
opts?: {
|
||||||
replyToMessageId?: number;
|
replyToMessageId?: number;
|
||||||
|
replyQuoteText?: string;
|
||||||
messageThreadId?: number;
|
messageThreadId?: number;
|
||||||
textMode?: "markdown" | "html";
|
textMode?: "markdown" | "html";
|
||||||
plainText?: string;
|
plainText?: string;
|
||||||
@ -443,6 +469,7 @@ async function sendTelegramText(
|
|||||||
): Promise<number | undefined> {
|
): Promise<number | undefined> {
|
||||||
const baseParams = buildTelegramSendParams({
|
const baseParams = buildTelegramSendParams({
|
||||||
replyToMessageId: opts?.replyToMessageId,
|
replyToMessageId: opts?.replyToMessageId,
|
||||||
|
replyQuoteText: opts?.replyQuoteText,
|
||||||
messageThreadId: opts?.messageThreadId,
|
messageThreadId: opts?.messageThreadId,
|
||||||
});
|
});
|
||||||
// Add link_preview_options when link preview is disabled.
|
// Add link_preview_options when link preview is disabled.
|
||||||
|
|||||||
@ -150,24 +150,29 @@ export function resolveTelegramReplyId(raw?: string): number | undefined {
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function describeReplyTarget(msg: TelegramMessage) {
|
export type TelegramReplyTarget = {
|
||||||
|
id?: string;
|
||||||
|
sender: string;
|
||||||
|
body: string;
|
||||||
|
kind: "reply" | "quote";
|
||||||
|
};
|
||||||
|
|
||||||
|
export function describeReplyTarget(msg: TelegramMessage): TelegramReplyTarget | null {
|
||||||
const reply = msg.reply_to_message;
|
const reply = msg.reply_to_message;
|
||||||
if (!reply) return null;
|
const quote = msg.quote;
|
||||||
|
|
||||||
// Check for quote (partial message reply)
|
|
||||||
const quote = (msg as any).quote;
|
|
||||||
let body = "";
|
let body = "";
|
||||||
let isQuote = false;
|
let kind: TelegramReplyTarget["kind"] = "reply";
|
||||||
|
|
||||||
if (quote && quote.text) {
|
if (quote?.text) {
|
||||||
// Use quoted text
|
|
||||||
body = quote.text.trim();
|
body = quote.text.trim();
|
||||||
isQuote = true;
|
if (body) {
|
||||||
} else {
|
kind = "quote";
|
||||||
// Regular reply - use full message
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body && reply) {
|
||||||
const replyBody = (reply.text ?? reply.caption ?? "").trim();
|
const replyBody = (reply.text ?? reply.caption ?? "").trim();
|
||||||
body = replyBody;
|
body = replyBody;
|
||||||
|
|
||||||
if (!body) {
|
if (!body) {
|
||||||
if (reply.photo) body = "<media:image>";
|
if (reply.photo) body = "<media:image>";
|
||||||
else if (reply.video) body = "<media:video>";
|
else if (reply.video) body = "<media:video>";
|
||||||
@ -179,16 +184,15 @@ export function describeReplyTarget(msg: TelegramMessage) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!body) return null;
|
if (!body) return null;
|
||||||
const sender = buildSenderName(reply);
|
const sender = reply ? buildSenderName(reply) : undefined;
|
||||||
const senderLabel = sender ? `${sender}` : "unknown sender";
|
const senderLabel = sender ? `${sender}` : "unknown sender";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: reply.message_id ? String(reply.message_id) : undefined,
|
id: reply?.message_id ? String(reply.message_id) : undefined,
|
||||||
sender: senderLabel,
|
sender: senderLabel,
|
||||||
body,
|
body,
|
||||||
isQuote // Add flag to distinguish quote from regular reply
|
kind,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,12 @@
|
|||||||
import type { Message } from "@grammyjs/types";
|
import type { Message } from "@grammyjs/types";
|
||||||
|
|
||||||
export type TelegramMessage = Message;
|
export type TelegramQuote = {
|
||||||
|
text?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TelegramMessage = Message & {
|
||||||
|
quote?: TelegramQuote;
|
||||||
|
};
|
||||||
|
|
||||||
export type TelegramStreamMode = "off" | "partial" | "block";
|
export type TelegramStreamMode = "off" | "partial" | "block";
|
||||||
|
|
||||||
|
|||||||
@ -46,6 +46,8 @@ type TelegramSendOpts = {
|
|||||||
silent?: boolean;
|
silent?: boolean;
|
||||||
/** Message ID to reply to (for threading) */
|
/** Message ID to reply to (for threading) */
|
||||||
replyToMessageId?: number;
|
replyToMessageId?: number;
|
||||||
|
/** Quote text for Telegram reply_parameters. */
|
||||||
|
quoteText?: string;
|
||||||
/** Forum topic thread ID (for forum supergroups) */
|
/** Forum topic thread ID (for forum supergroups) */
|
||||||
messageThreadId?: number;
|
messageThreadId?: number;
|
||||||
/** Inline keyboard buttons (reply markup). */
|
/** Inline keyboard buttons (reply markup). */
|
||||||
@ -198,9 +200,17 @@ export async function sendMessageTelegram(
|
|||||||
const messageThreadId =
|
const messageThreadId =
|
||||||
opts.messageThreadId != null ? opts.messageThreadId : target.messageThreadId;
|
opts.messageThreadId != null ? opts.messageThreadId : target.messageThreadId;
|
||||||
const threadIdParams = buildTelegramThreadParams(messageThreadId);
|
const threadIdParams = buildTelegramThreadParams(messageThreadId);
|
||||||
const threadParams: Record<string, number> = threadIdParams ? { ...threadIdParams } : {};
|
const threadParams: Record<string, unknown> = threadIdParams ? { ...threadIdParams } : {};
|
||||||
|
const quoteText = opts.quoteText?.trim();
|
||||||
if (opts.replyToMessageId != null) {
|
if (opts.replyToMessageId != null) {
|
||||||
threadParams.reply_to_message_id = Math.trunc(opts.replyToMessageId);
|
if (quoteText) {
|
||||||
|
threadParams.reply_parameters = {
|
||||||
|
message_id: Math.trunc(opts.replyToMessageId),
|
||||||
|
quote: quoteText,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
threadParams.reply_to_message_id = Math.trunc(opts.replyToMessageId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const hasThreadParams = Object.keys(threadParams).length > 0;
|
const hasThreadParams = Object.keys(threadParams).length > 0;
|
||||||
const request = createTelegramRetryRunner({
|
const request = createTelegramRetryRunner({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user