feat: add reply tags and replyToMode

This commit is contained in:
Peter Steinberger
2026-01-02 23:18:41 +01:00
parent a9ff03acaf
commit 2c92ccd66e
19 changed files with 353 additions and 27 deletions

View File

@@ -9,6 +9,7 @@ import { chunkText } from "../auto-reply/chunk.js";
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
import { getReplyFromConfig } from "../auto-reply/reply.js";
import type { ReplyPayload } from "../auto-reply/types.js";
import type { ReplyToMode } from "../config/config.js";
import { loadConfig } from "../config/config.js";
import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
import { danger, isVerbose, logVerbose } from "../globals.js";
@@ -39,6 +40,7 @@ export type TelegramBotOptions = {
requireMention?: boolean;
allowFrom?: Array<string | number>;
mediaMaxMb?: number;
replyToMode?: ReplyToMode;
proxyFetch?: typeof fetch;
};
@@ -59,6 +61,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
const cfg = loadConfig();
const allowFrom = opts.allowFrom ?? cfg.telegram?.allowFrom;
const replyToMode = opts.replyToMode ?? cfg.telegram?.replyToMode ?? "off";
const mediaMaxBytes =
(opts.mediaMaxMb ?? cfg.telegram?.mediaMaxMb ?? 5) * 1024 * 1024;
const logger = getChildLogger({ module: "telegram-auto-reply" });
@@ -137,7 +140,9 @@ export function createTelegramBot(opts: TelegramBotOptions) {
).trim();
if (!rawBody) return;
const replySuffix = replyTarget
? `\n\n[Replying to ${replyTarget.sender}]\n${replyTarget.body}\n[/Replying]`
? `\n\n[Replying to ${replyTarget.sender}${
replyTarget.id ? ` id:${replyTarget.id}` : ""
}]\n${replyTarget.body}\n[/Replying]`
: "";
const body = formatAgentEnvelope({
surface: "Telegram",
@@ -211,6 +216,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
token: opts.token,
runtime,
bot,
replyToMode,
});
} catch (err) {
runtime.error?.(danger(`handler failed: ${String(err)}`));
@@ -233,13 +239,17 @@ async function deliverReplies(params: {
token: string;
runtime: RuntimeEnv;
bot: Bot;
replyToMode: ReplyToMode;
}) {
const { replies, chatId, runtime, bot } = params;
const { replies, chatId, runtime, bot, replyToMode } = params;
let hasReplied = false;
for (const reply of replies) {
if (!reply?.text && !reply?.mediaUrl && !(reply?.mediaUrls?.length ?? 0)) {
runtime.error?.(danger("reply missing text/media"));
continue;
}
const replyToId =
replyToMode === "off" ? undefined : resolveTelegramReplyId(reply.replyToId);
const mediaList = reply.mediaUrls?.length
? reply.mediaUrls
: reply.mediaUrl
@@ -247,7 +257,15 @@ async function deliverReplies(params: {
: [];
if (mediaList.length === 0) {
for (const chunk of chunkText(reply.text || "", 4000)) {
await sendTelegramText(bot, chatId, chunk, runtime);
await sendTelegramText(bot, chatId, chunk, runtime, {
replyToMessageId:
replyToId && (replyToMode === "all" || !hasReplied)
? replyToId
: undefined,
});
if (replyToId && !hasReplied) {
hasReplied = true;
}
}
continue;
}
@@ -259,14 +277,33 @@ async function deliverReplies(params: {
const file = new InputFile(media.buffer, media.fileName ?? "file");
const caption = first ? (reply.text ?? undefined) : undefined;
first = false;
const replyToMessageId =
replyToId && (replyToMode === "all" || !hasReplied)
? replyToId
: undefined;
if (kind === "image") {
await bot.api.sendPhoto(chatId, file, { caption });
await bot.api.sendPhoto(chatId, file, {
caption,
reply_to_message_id: replyToMessageId,
});
} else if (kind === "video") {
await bot.api.sendVideo(chatId, file, { caption });
await bot.api.sendVideo(chatId, file, {
caption,
reply_to_message_id: replyToMessageId,
});
} else if (kind === "audio") {
await bot.api.sendAudio(chatId, file, { caption });
await bot.api.sendAudio(chatId, file, {
caption,
reply_to_message_id: replyToMessageId,
});
} else {
await bot.api.sendDocument(chatId, file, { caption });
await bot.api.sendDocument(chatId, file, {
caption,
reply_to_message_id: replyToMessageId,
});
}
if (replyToId && !hasReplied) {
hasReplied = true;
}
}
}
@@ -315,6 +352,13 @@ function hasBotMention(msg: TelegramMessage, botUsername: string) {
return false;
}
function resolveTelegramReplyId(raw?: string): number | undefined {
if (!raw) return undefined;
const parsed = Number(raw);
if (!Number.isFinite(parsed)) return undefined;
return parsed;
}
async function resolveMedia(
ctx: TelegramContext,
maxBytes: number,
@@ -363,10 +407,12 @@ async function sendTelegramText(
chatId: string,
text: string,
runtime: RuntimeEnv,
opts?: { replyToMessageId?: number },
): Promise<number | undefined> {
try {
const res = await bot.api.sendMessage(chatId, text, {
parse_mode: "Markdown",
reply_to_message_id: opts?.replyToMessageId,
});
return res.message_id;
} catch (err) {
@@ -375,7 +421,9 @@ async function sendTelegramText(
runtime.log?.(
`telegram markdown parse failed; retrying without formatting: ${errText}`,
);
const res = await bot.api.sendMessage(chatId, text, {});
const res = await bot.api.sendMessage(chatId, text, {
reply_to_message_id: opts?.replyToMessageId,
});
return res.message_id;
}
throw err;