refactor: share telegram caption splitting
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { type Bot, InputFile } from "grammy";
|
import { type Bot, InputFile } from "grammy";
|
||||||
import { markdownToTelegramChunks, markdownToTelegramHtml } from "../format.js";
|
import { markdownToTelegramChunks, markdownToTelegramHtml } from "../format.js";
|
||||||
|
import { splitTelegramCaption } from "../caption.js";
|
||||||
import type { ReplyPayload } from "../../auto-reply/types.js";
|
import type { ReplyPayload } from "../../auto-reply/types.js";
|
||||||
import type { ReplyToMode } from "../../config/config.js";
|
import type { ReplyToMode } from "../../config/config.js";
|
||||||
import { danger, logVerbose } from "../../globals.js";
|
import { danger, logVerbose } from "../../globals.js";
|
||||||
@@ -16,10 +17,6 @@ import type { TelegramContext } from "./types.js";
|
|||||||
|
|
||||||
const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
|
const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
|
||||||
|
|
||||||
// Telegram limits media captions to 1024 characters.
|
|
||||||
// Text beyond this must be sent as a separate follow-up message.
|
|
||||||
const TELEGRAM_MAX_CAPTION_LENGTH = 1024;
|
|
||||||
|
|
||||||
export async function deliverReplies(params: {
|
export async function deliverReplies(params: {
|
||||||
replies: ReplyPayload[];
|
replies: ReplyPayload[];
|
||||||
chatId: string;
|
chatId: string;
|
||||||
@@ -73,6 +70,7 @@ export async function deliverReplies(params: {
|
|||||||
// (when caption exceeds Telegram's 1024-char limit)
|
// (when caption exceeds Telegram's 1024-char limit)
|
||||||
let pendingFollowUpText: string | undefined;
|
let pendingFollowUpText: string | undefined;
|
||||||
for (const mediaUrl of mediaList) {
|
for (const mediaUrl of mediaList) {
|
||||||
|
const isFirstMedia = first;
|
||||||
const media = await loadWebMedia(mediaUrl);
|
const media = await loadWebMedia(mediaUrl);
|
||||||
const kind = mediaKindFromMime(media.contentType ?? undefined);
|
const kind = mediaKindFromMime(media.contentType ?? undefined);
|
||||||
const isGif = isGifMedia({
|
const isGif = isGifMedia({
|
||||||
@@ -82,11 +80,11 @@ export async function deliverReplies(params: {
|
|||||||
const fileName = media.fileName ?? (isGif ? "animation.gif" : "file");
|
const fileName = media.fileName ?? (isGif ? "animation.gif" : "file");
|
||||||
const file = new InputFile(media.buffer, fileName);
|
const file = new InputFile(media.buffer, fileName);
|
||||||
// Caption only on first item; if text exceeds limit, defer to follow-up message.
|
// Caption only on first item; if text exceeds limit, defer to follow-up message.
|
||||||
const rawCaption = first ? (reply.text ?? undefined) : undefined;
|
const { caption, followUpText } = splitTelegramCaption(
|
||||||
const captionTooLong = rawCaption != null && rawCaption.length > TELEGRAM_MAX_CAPTION_LENGTH;
|
isFirstMedia ? (reply.text ?? undefined) : undefined,
|
||||||
const caption = captionTooLong ? undefined : rawCaption;
|
);
|
||||||
if (captionTooLong && rawCaption) {
|
if (followUpText) {
|
||||||
pendingFollowUpText = rawCaption;
|
pendingFollowUpText = followUpText;
|
||||||
}
|
}
|
||||||
first = false;
|
first = false;
|
||||||
const replyToMessageId =
|
const replyToMessageId =
|
||||||
@@ -138,22 +136,26 @@ export async function deliverReplies(params: {
|
|||||||
if (replyToId && !hasReplied) {
|
if (replyToId && !hasReplied) {
|
||||||
hasReplied = true;
|
hasReplied = true;
|
||||||
}
|
}
|
||||||
}
|
// Send deferred follow-up text right after the first media item.
|
||||||
// Send deferred follow-up text when caption was too long for media.
|
// Chunk it in case it's extremely long (same logic as text-only replies).
|
||||||
// Chunk it in case it's extremely long (same logic as text-only replies).
|
if (pendingFollowUpText && isFirstMedia) {
|
||||||
if (pendingFollowUpText) {
|
const chunks = markdownToTelegramChunks(pendingFollowUpText, textLimit);
|
||||||
const chunks = markdownToTelegramChunks(pendingFollowUpText, textLimit);
|
for (const chunk of chunks) {
|
||||||
for (const chunk of chunks) {
|
const replyToMessageIdFollowup =
|
||||||
await sendTelegramText(bot, chatId, chunk.html, runtime, {
|
replyToId && (replyToMode === "all" || !hasReplied) ? replyToId : undefined;
|
||||||
replyToMessageId:
|
await bot.api.sendMessage(
|
||||||
replyToId && (replyToMode === "all" || !hasReplied) ? replyToId : undefined,
|
chatId,
|
||||||
messageThreadId,
|
chunk.text,
|
||||||
textMode: "html",
|
buildTelegramSendParams({
|
||||||
plainText: chunk.text,
|
replyToMessageId: replyToMessageIdFollowup,
|
||||||
});
|
messageThreadId,
|
||||||
if (replyToId && !hasReplied) {
|
}),
|
||||||
hasReplied = true;
|
);
|
||||||
|
if (replyToId && !hasReplied) {
|
||||||
|
hasReplied = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
pendingFollowUpText = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,6 +193,21 @@ export async function resolveMedia(
|
|||||||
return { path: saved.path, contentType: saved.contentType, placeholder };
|
return { path: saved.path, contentType: saved.contentType, placeholder };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildTelegramSendParams(opts?: {
|
||||||
|
replyToMessageId?: number;
|
||||||
|
messageThreadId?: number;
|
||||||
|
}): Record<string, unknown> {
|
||||||
|
const threadParams = buildTelegramThreadParams(opts?.messageThreadId);
|
||||||
|
const params: Record<string, unknown> = {};
|
||||||
|
if (opts?.replyToMessageId) {
|
||||||
|
params.reply_to_message_id = opts.replyToMessageId;
|
||||||
|
}
|
||||||
|
if (threadParams) {
|
||||||
|
params.message_thread_id = threadParams.message_thread_id;
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
async function sendTelegramText(
|
async function sendTelegramText(
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
chatId: string,
|
chatId: string,
|
||||||
@@ -203,13 +220,10 @@ async function sendTelegramText(
|
|||||||
plainText?: string;
|
plainText?: string;
|
||||||
},
|
},
|
||||||
): Promise<number | undefined> {
|
): Promise<number | undefined> {
|
||||||
const threadParams = buildTelegramThreadParams(opts?.messageThreadId);
|
const baseParams = buildTelegramSendParams({
|
||||||
const baseParams: Record<string, unknown> = {
|
replyToMessageId: opts?.replyToMessageId,
|
||||||
reply_to_message_id: opts?.replyToMessageId,
|
messageThreadId: opts?.messageThreadId,
|
||||||
};
|
});
|
||||||
if (threadParams) {
|
|
||||||
baseParams.message_thread_id = threadParams.message_thread_id;
|
|
||||||
}
|
|
||||||
const textMode = opts?.textMode ?? "markdown";
|
const textMode = opts?.textMode ?? "markdown";
|
||||||
const htmlText = textMode === "html" ? text : markdownToTelegramHtml(text);
|
const htmlText = textMode === "html" ? text : markdownToTelegramHtml(text);
|
||||||
try {
|
try {
|
||||||
|
|||||||
15
src/telegram/caption.ts
Normal file
15
src/telegram/caption.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export const TELEGRAM_MAX_CAPTION_LENGTH = 1024;
|
||||||
|
|
||||||
|
export function splitTelegramCaption(text?: string): {
|
||||||
|
caption?: string;
|
||||||
|
followUpText?: string;
|
||||||
|
} {
|
||||||
|
const trimmed = text?.trim() ?? "";
|
||||||
|
if (!trimmed) {
|
||||||
|
return { caption: undefined, followUpText: undefined };
|
||||||
|
}
|
||||||
|
if (trimmed.length > TELEGRAM_MAX_CAPTION_LENGTH) {
|
||||||
|
return { caption: undefined, followUpText: trimmed };
|
||||||
|
}
|
||||||
|
return { caption: trimmed, followUpText: undefined };
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import { loadWebMedia } from "../web/media.js";
|
|||||||
import { resolveTelegramAccount } from "./accounts.js";
|
import { resolveTelegramAccount } from "./accounts.js";
|
||||||
import { resolveTelegramFetch } from "./fetch.js";
|
import { resolveTelegramFetch } from "./fetch.js";
|
||||||
import { markdownToTelegramHtml } from "./format.js";
|
import { markdownToTelegramHtml } from "./format.js";
|
||||||
|
import { splitTelegramCaption } from "./caption.js";
|
||||||
import { recordSentMessage } from "./sent-message-cache.js";
|
import { recordSentMessage } from "./sent-message-cache.js";
|
||||||
import { parseTelegramTarget, stripTelegramInternalPrefixes } from "./targets.js";
|
import { parseTelegramTarget, stripTelegramInternalPrefixes } from "./targets.js";
|
||||||
import { resolveTelegramVoiceSend } from "./voice.js";
|
import { resolveTelegramVoiceSend } from "./voice.js";
|
||||||
@@ -58,10 +59,6 @@ type TelegramReactionOpts = {
|
|||||||
|
|
||||||
const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
|
const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
|
||||||
|
|
||||||
// Telegram limits media captions to 1024 characters.
|
|
||||||
// Text beyond this must be sent as a separate follow-up message.
|
|
||||||
const TELEGRAM_MAX_CAPTION_LENGTH = 1024;
|
|
||||||
|
|
||||||
function resolveToken(explicit: string | undefined, params: { accountId: string; token: string }) {
|
function resolveToken(explicit: string | undefined, params: { accountId: string; token: string }) {
|
||||||
if (explicit?.trim()) return explicit.trim();
|
if (explicit?.trim()) return explicit.trim();
|
||||||
if (!params.token) {
|
if (!params.token) {
|
||||||
@@ -201,11 +198,10 @@ export async function sendMessageTelegram(
|
|||||||
});
|
});
|
||||||
const fileName = media.fileName ?? (isGif ? "animation.gif" : inferFilename(kind)) ?? "file";
|
const fileName = media.fileName ?? (isGif ? "animation.gif" : inferFilename(kind)) ?? "file";
|
||||||
const file = new InputFile(media.buffer, fileName);
|
const file = new InputFile(media.buffer, fileName);
|
||||||
const trimmedText = text?.trim() || "";
|
const { caption, followUpText } = splitTelegramCaption(text);
|
||||||
// If text exceeds Telegram's caption limit, send media without caption
|
// If text exceeds Telegram's caption limit, send media without caption
|
||||||
// then send text as a separate follow-up message.
|
// then send text as a separate follow-up message.
|
||||||
const needsSeparateText = trimmedText.length > TELEGRAM_MAX_CAPTION_LENGTH;
|
const needsSeparateText = Boolean(followUpText);
|
||||||
const caption = needsSeparateText ? undefined : trimmedText || undefined;
|
|
||||||
// When splitting, put reply_markup only on the follow-up text (the "main" content),
|
// When splitting, put reply_markup only on the follow-up text (the "main" content),
|
||||||
// not on the media message.
|
// not on the media message.
|
||||||
const mediaParams = hasThreadParams
|
const mediaParams = hasThreadParams
|
||||||
@@ -283,7 +279,7 @@ export async function sendMessageTelegram(
|
|||||||
|
|
||||||
// If text was too long for a caption, send it as a separate follow-up message.
|
// If text was too long for a caption, send it as a separate follow-up message.
|
||||||
// Use plain text to match caption behavior (captions don't use HTML conversion).
|
// Use plain text to match caption behavior (captions don't use HTML conversion).
|
||||||
if (needsSeparateText && trimmedText) {
|
if (needsSeparateText && followUpText) {
|
||||||
const textParams =
|
const textParams =
|
||||||
hasThreadParams || replyMarkup
|
hasThreadParams || replyMarkup
|
||||||
? {
|
? {
|
||||||
@@ -294,8 +290,8 @@ export async function sendMessageTelegram(
|
|||||||
const textRes = await request(
|
const textRes = await request(
|
||||||
() =>
|
() =>
|
||||||
textParams
|
textParams
|
||||||
? api.sendMessage(chatId, trimmedText, textParams)
|
? api.sendMessage(chatId, followUpText, textParams)
|
||||||
: api.sendMessage(chatId, trimmedText),
|
: api.sendMessage(chatId, followUpText),
|
||||||
"message",
|
"message",
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
throw wrapChatNotFound(err);
|
throw wrapChatNotFound(err);
|
||||||
|
|||||||
Reference in New Issue
Block a user