feat(telegram): add linkPreview config option
Add channels.telegram.linkPreview config to control whether link previews are shown in outbound messages. When set to false, uses Telegram's link_preview_options.is_disabled to suppress URL previews. - Add linkPreview to TelegramAccountConfig type - Add Zod schema validation for linkPreview - Pass link_preview_options to sendMessage in send.ts and bot/delivery.ts - Propagate linkPreview config through deliverReplies callers - Add tests for link preview behavior Fixes #1675 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
committed by
Peter Steinberger
parent
43a6c5b77f
commit
92ab3f22dc
@@ -118,6 +118,8 @@ export type TelegramAccountConfig = {
|
|||||||
reactionLevel?: "off" | "ack" | "minimal" | "extensive";
|
reactionLevel?: "off" | "ack" | "minimal" | "extensive";
|
||||||
/** Heartbeat visibility settings for this channel. */
|
/** Heartbeat visibility settings for this channel. */
|
||||||
heartbeat?: ChannelHeartbeatVisibilityConfig;
|
heartbeat?: ChannelHeartbeatVisibilityConfig;
|
||||||
|
/** Controls whether link previews are shown in outbound messages. Default: true. */
|
||||||
|
linkPreview?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TelegramTopicConfig = {
|
export type TelegramTopicConfig = {
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ export const TelegramAccountSchemaBase = z
|
|||||||
reactionNotifications: z.enum(["off", "own", "all"]).optional(),
|
reactionNotifications: z.enum(["off", "own", "all"]).optional(),
|
||||||
reactionLevel: z.enum(["off", "ack", "minimal", "extensive"]).optional(),
|
reactionLevel: z.enum(["off", "ack", "minimal", "extensive"]).optional(),
|
||||||
heartbeat: ChannelHeartbeatVisibilitySchema,
|
heartbeat: ChannelHeartbeatVisibilitySchema,
|
||||||
|
linkPreview: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ export const dispatchTelegramMessage = async ({
|
|||||||
tableMode,
|
tableMode,
|
||||||
chunkMode,
|
chunkMode,
|
||||||
onVoiceRecording: sendRecordVoice,
|
onVoiceRecording: sendRecordVoice,
|
||||||
|
linkPreview: telegramCfg.linkPreview,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (err, info) => {
|
onError: (err, info) => {
|
||||||
|
|||||||
@@ -348,6 +348,7 @@ export const registerTelegramNativeCommands = ({
|
|||||||
messageThreadId: resolvedThreadId,
|
messageThreadId: resolvedThreadId,
|
||||||
tableMode,
|
tableMode,
|
||||||
chunkMode,
|
chunkMode,
|
||||||
|
linkPreview: telegramCfg.linkPreview,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (err, info) => {
|
onError: (err, info) => {
|
||||||
|
|||||||
@@ -108,4 +108,60 @@ describe("deliverReplies", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("includes link_preview_options when linkPreview is false", async () => {
|
||||||
|
const runtime = { error: vi.fn(), log: vi.fn() };
|
||||||
|
const sendMessage = vi.fn().mockResolvedValue({
|
||||||
|
message_id: 3,
|
||||||
|
chat: { id: "123" },
|
||||||
|
});
|
||||||
|
const bot = { api: { sendMessage } } as unknown as Bot;
|
||||||
|
|
||||||
|
await deliverReplies({
|
||||||
|
replies: [{ text: "Check https://example.com" }],
|
||||||
|
chatId: "123",
|
||||||
|
token: "tok",
|
||||||
|
runtime,
|
||||||
|
bot,
|
||||||
|
replyToMode: "off",
|
||||||
|
textLimit: 4000,
|
||||||
|
linkPreview: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendMessage).toHaveBeenCalledWith(
|
||||||
|
"123",
|
||||||
|
expect.any(String),
|
||||||
|
expect.objectContaining({
|
||||||
|
link_preview_options: { is_disabled: true },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not include link_preview_options when linkPreview is true", async () => {
|
||||||
|
const runtime = { error: vi.fn(), log: vi.fn() };
|
||||||
|
const sendMessage = vi.fn().mockResolvedValue({
|
||||||
|
message_id: 4,
|
||||||
|
chat: { id: "123" },
|
||||||
|
});
|
||||||
|
const bot = { api: { sendMessage } } as unknown as Bot;
|
||||||
|
|
||||||
|
await deliverReplies({
|
||||||
|
replies: [{ text: "Check https://example.com" }],
|
||||||
|
chatId: "123",
|
||||||
|
token: "tok",
|
||||||
|
runtime,
|
||||||
|
bot,
|
||||||
|
replyToMode: "off",
|
||||||
|
textLimit: 4000,
|
||||||
|
linkPreview: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendMessage).toHaveBeenCalledWith(
|
||||||
|
"123",
|
||||||
|
expect.any(String),
|
||||||
|
expect.not.objectContaining({
|
||||||
|
link_preview_options: expect.anything(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,8 +36,11 @@ export async function deliverReplies(params: {
|
|||||||
chunkMode?: ChunkMode;
|
chunkMode?: ChunkMode;
|
||||||
/** Callback invoked before sending a voice message to switch typing indicator. */
|
/** Callback invoked before sending a voice message to switch typing indicator. */
|
||||||
onVoiceRecording?: () => Promise<void> | void;
|
onVoiceRecording?: () => Promise<void> | void;
|
||||||
|
/** Controls whether link previews are shown. Default: true (previews enabled). */
|
||||||
|
linkPreview?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { replies, chatId, runtime, bot, replyToMode, textLimit, messageThreadId } = params;
|
const { replies, chatId, runtime, bot, replyToMode, textLimit, messageThreadId, linkPreview } =
|
||||||
|
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;
|
||||||
@@ -85,6 +88,7 @@ export async function deliverReplies(params: {
|
|||||||
messageThreadId,
|
messageThreadId,
|
||||||
textMode: "html",
|
textMode: "html",
|
||||||
plainText: chunk.text,
|
plainText: chunk.text,
|
||||||
|
linkPreview,
|
||||||
});
|
});
|
||||||
if (replyToId && !hasReplied) {
|
if (replyToId && !hasReplied) {
|
||||||
hasReplied = true;
|
hasReplied = true;
|
||||||
@@ -180,6 +184,7 @@ export async function deliverReplies(params: {
|
|||||||
messageThreadId,
|
messageThreadId,
|
||||||
textMode: "html",
|
textMode: "html",
|
||||||
plainText: chunk.text,
|
plainText: chunk.text,
|
||||||
|
linkPreview,
|
||||||
});
|
});
|
||||||
if (replyToId && !hasReplied) {
|
if (replyToId && !hasReplied) {
|
||||||
hasReplied = true;
|
hasReplied = true;
|
||||||
@@ -248,17 +253,22 @@ async function sendTelegramText(
|
|||||||
messageThreadId?: number;
|
messageThreadId?: number;
|
||||||
textMode?: "markdown" | "html";
|
textMode?: "markdown" | "html";
|
||||||
plainText?: string;
|
plainText?: string;
|
||||||
|
linkPreview?: boolean;
|
||||||
},
|
},
|
||||||
): Promise<number | undefined> {
|
): Promise<number | undefined> {
|
||||||
const baseParams = buildTelegramSendParams({
|
const baseParams = buildTelegramSendParams({
|
||||||
replyToMessageId: opts?.replyToMessageId,
|
replyToMessageId: opts?.replyToMessageId,
|
||||||
messageThreadId: opts?.messageThreadId,
|
messageThreadId: opts?.messageThreadId,
|
||||||
});
|
});
|
||||||
|
// Add link_preview_options when link preview is disabled.
|
||||||
|
const linkPreviewEnabled = opts?.linkPreview ?? true;
|
||||||
|
const linkPreviewOptions = linkPreviewEnabled ? undefined : { is_disabled: true };
|
||||||
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 {
|
||||||
const res = await bot.api.sendMessage(chatId, htmlText, {
|
const res = await bot.api.sendMessage(chatId, htmlText, {
|
||||||
parse_mode: "HTML",
|
parse_mode: "HTML",
|
||||||
|
...(linkPreviewOptions ? { link_preview_options: linkPreviewOptions } : {}),
|
||||||
...baseParams,
|
...baseParams,
|
||||||
});
|
});
|
||||||
return res.message_id;
|
return res.message_id;
|
||||||
@@ -268,6 +278,7 @@ async function sendTelegramText(
|
|||||||
runtime.log?.(`telegram HTML parse failed; retrying without formatting: ${errText}`);
|
runtime.log?.(`telegram HTML parse failed; retrying without formatting: ${errText}`);
|
||||||
const fallbackText = opts?.plainText ?? text;
|
const fallbackText = opts?.plainText ?? text;
|
||||||
const res = await bot.api.sendMessage(chatId, fallbackText, {
|
const res = await bot.api.sendMessage(chatId, fallbackText, {
|
||||||
|
...(linkPreviewOptions ? { link_preview_options: linkPreviewOptions } : {}),
|
||||||
...baseParams,
|
...baseParams,
|
||||||
});
|
});
|
||||||
return res.message_id;
|
return res.message_id;
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ type TelegramSendOpts = {
|
|||||||
messageThreadId?: number;
|
messageThreadId?: number;
|
||||||
/** Inline keyboard buttons (reply markup). */
|
/** Inline keyboard buttons (reply markup). */
|
||||||
buttons?: Array<Array<{ text: string; callback_data: string }>>;
|
buttons?: Array<Array<{ text: string; callback_data: string }>>;
|
||||||
|
/** Controls whether link previews are shown. Default: true (previews enabled). */
|
||||||
|
linkPreview?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TelegramSendResult = {
|
type TelegramSendResult = {
|
||||||
@@ -198,20 +200,21 @@ export async function sendMessageTelegram(
|
|||||||
});
|
});
|
||||||
const renderHtmlText = (value: string) => renderTelegramHtmlText(value, { textMode, tableMode });
|
const renderHtmlText = (value: string) => renderTelegramHtmlText(value, { textMode, tableMode });
|
||||||
|
|
||||||
|
// Resolve link preview setting: explicit opt > config > default (enabled).
|
||||||
|
const linkPreviewEnabled = opts.linkPreview ?? account.config.linkPreview ?? true;
|
||||||
|
const linkPreviewOptions = linkPreviewEnabled ? undefined : { is_disabled: true };
|
||||||
|
|
||||||
const sendTelegramText = async (
|
const sendTelegramText = async (
|
||||||
rawText: string,
|
rawText: string,
|
||||||
params?: Record<string, unknown>,
|
params?: Record<string, unknown>,
|
||||||
fallbackText?: string,
|
fallbackText?: string,
|
||||||
) => {
|
) => {
|
||||||
const htmlText = renderHtmlText(rawText);
|
const htmlText = renderHtmlText(rawText);
|
||||||
const sendParams = params
|
const sendParams = {
|
||||||
? {
|
parse_mode: "HTML" as const,
|
||||||
parse_mode: "HTML" as const,
|
...(linkPreviewOptions ? { link_preview_options: linkPreviewOptions } : {}),
|
||||||
...params,
|
...params,
|
||||||
}
|
};
|
||||||
: {
|
|
||||||
parse_mode: "HTML" as const,
|
|
||||||
};
|
|
||||||
const res = await request(() => api.sendMessage(chatId, htmlText, sendParams), "message").catch(
|
const res = await request(() => api.sendMessage(chatId, htmlText, sendParams), "message").catch(
|
||||||
async (err) => {
|
async (err) => {
|
||||||
// Telegram rejects malformed HTML (e.g., unsupported tags or entities).
|
// Telegram rejects malformed HTML (e.g., unsupported tags or entities).
|
||||||
|
|||||||
Reference in New Issue
Block a user