fix: telegram sendPayload and plugin auth (#1917) (thanks @JoshuaLelon)
This commit is contained in:
@@ -29,6 +29,7 @@ Status: unreleased.
|
|||||||
- Browser: fall back to URL matching for extension relay target resolution. (#1999) Thanks @jonit-dev.
|
- Browser: fall back to URL matching for extension relay target resolution. (#1999) Thanks @jonit-dev.
|
||||||
- Update: ignore dist/control-ui for dirty checks and restore after ui builds. (#1976) Thanks @Glucksberg.
|
- Update: ignore dist/control-ui for dirty checks and restore after ui builds. (#1976) Thanks @Glucksberg.
|
||||||
- Telegram: allow caption param for media sends. (#1888) Thanks @mguellsegarra.
|
- Telegram: allow caption param for media sends. (#1888) Thanks @mguellsegarra.
|
||||||
|
- Telegram: support plugin sendPayload channelData (media/buttons) and validate plugin commands. (#1917) Thanks @JoshuaLelon.
|
||||||
- Telegram: avoid block replies when streaming is disabled. (#1885) Thanks @ivancasco.
|
- Telegram: avoid block replies when streaming is disabled. (#1885) Thanks @ivancasco.
|
||||||
- Auth: show copyable Google auth URL after ASCII prompt. (#1787) Thanks @robbyczgw-cla.
|
- Auth: show copyable Google auth URL after ASCII prompt. (#1787) Thanks @robbyczgw-cla.
|
||||||
- Routing: precompile session key regexes. (#1697) Thanks @Ray0907.
|
- Routing: precompile session key regexes. (#1697) Thanks @Ray0907.
|
||||||
|
|||||||
81
src/channels/plugins/outbound/telegram.test.ts
Normal file
81
src/channels/plugins/outbound/telegram.test.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { ClawdbotConfig } from "../../../config/config.js";
|
||||||
|
import { telegramOutbound } from "./telegram.js";
|
||||||
|
|
||||||
|
describe("telegramOutbound.sendPayload", () => {
|
||||||
|
it("sends text payload with buttons", async () => {
|
||||||
|
const sendTelegram = vi.fn(async () => ({ messageId: "m1", chatId: "c1" }));
|
||||||
|
|
||||||
|
const result = await telegramOutbound.sendPayload?.({
|
||||||
|
cfg: {} as ClawdbotConfig,
|
||||||
|
to: "telegram:123",
|
||||||
|
text: "ignored",
|
||||||
|
payload: {
|
||||||
|
text: "Hello",
|
||||||
|
channelData: {
|
||||||
|
telegram: {
|
||||||
|
buttons: [[{ text: "Option", callback_data: "/option" }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deps: { sendTelegram },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendTelegram).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sendTelegram).toHaveBeenCalledWith(
|
||||||
|
"telegram:123",
|
||||||
|
"Hello",
|
||||||
|
expect.objectContaining({
|
||||||
|
buttons: [[{ text: "Option", callback_data: "/option" }]],
|
||||||
|
textMode: "html",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(result).toEqual({ channel: "telegram", messageId: "m1", chatId: "c1" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends media payloads and attaches buttons only to first", async () => {
|
||||||
|
const sendTelegram = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({ messageId: "m1", chatId: "c1" })
|
||||||
|
.mockResolvedValueOnce({ messageId: "m2", chatId: "c1" });
|
||||||
|
|
||||||
|
const result = await telegramOutbound.sendPayload?.({
|
||||||
|
cfg: {} as ClawdbotConfig,
|
||||||
|
to: "telegram:123",
|
||||||
|
text: "ignored",
|
||||||
|
payload: {
|
||||||
|
text: "Caption",
|
||||||
|
mediaUrls: ["https://example.com/a.png", "https://example.com/b.png"],
|
||||||
|
channelData: {
|
||||||
|
telegram: {
|
||||||
|
buttons: [[{ text: "Go", callback_data: "/go" }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deps: { sendTelegram },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendTelegram).toHaveBeenCalledTimes(2);
|
||||||
|
expect(sendTelegram).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
"telegram:123",
|
||||||
|
"Caption",
|
||||||
|
expect.objectContaining({
|
||||||
|
mediaUrl: "https://example.com/a.png",
|
||||||
|
buttons: [[{ text: "Go", callback_data: "/go" }]],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const secondOpts = sendTelegram.mock.calls[1]?.[2] as { buttons?: unknown } | undefined;
|
||||||
|
expect(sendTelegram).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
"telegram:123",
|
||||||
|
"",
|
||||||
|
expect.objectContaining({
|
||||||
|
mediaUrl: "https://example.com/b.png",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(secondOpts?.buttons).toBeUndefined();
|
||||||
|
expect(result).toEqual({ channel: "telegram", messageId: "m2", chatId: "c1" });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -18,6 +18,7 @@ function parseThreadId(threadId?: string | number | null) {
|
|||||||
const parsed = Number.parseInt(trimmed, 10);
|
const parsed = Number.parseInt(trimmed, 10);
|
||||||
return Number.isFinite(parsed) ? parsed : undefined;
|
return Number.isFinite(parsed) ? parsed : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const telegramOutbound: ChannelOutboundAdapter = {
|
export const telegramOutbound: ChannelOutboundAdapter = {
|
||||||
deliveryMode: "direct",
|
deliveryMode: "direct",
|
||||||
chunker: markdownToTelegramHtmlChunks,
|
chunker: markdownToTelegramHtmlChunks,
|
||||||
@@ -54,20 +55,42 @@ export const telegramOutbound: ChannelOutboundAdapter = {
|
|||||||
const send = deps?.sendTelegram ?? sendMessageTelegram;
|
const send = deps?.sendTelegram ?? sendMessageTelegram;
|
||||||
const replyToMessageId = parseReplyToMessageId(replyToId);
|
const replyToMessageId = parseReplyToMessageId(replyToId);
|
||||||
const messageThreadId = parseThreadId(threadId);
|
const messageThreadId = parseThreadId(threadId);
|
||||||
|
|
||||||
// Extract Telegram-specific data from channelData
|
|
||||||
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 }>> }
|
||||||
| undefined;
|
| undefined;
|
||||||
|
const text = payload.text ?? "";
|
||||||
const result = await send(to, payload.text ?? "", {
|
const mediaUrls = payload.mediaUrls?.length
|
||||||
|
? payload.mediaUrls
|
||||||
|
: payload.mediaUrl
|
||||||
|
? [payload.mediaUrl]
|
||||||
|
: [];
|
||||||
|
const baseOpts = {
|
||||||
verbose: false,
|
verbose: false,
|
||||||
textMode: "html",
|
textMode: "html" as const,
|
||||||
messageThreadId,
|
messageThreadId,
|
||||||
replyToMessageId,
|
replyToMessageId,
|
||||||
accountId: accountId ?? undefined,
|
accountId: accountId ?? undefined,
|
||||||
buttons: telegramData?.buttons,
|
};
|
||||||
});
|
|
||||||
return { channel: "telegram", ...result };
|
if (mediaUrls.length === 0) {
|
||||||
|
const result = await send(to, text, {
|
||||||
|
...baseOpts,
|
||||||
|
buttons: telegramData?.buttons,
|
||||||
|
});
|
||||||
|
return { channel: "telegram", ...result };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Telegram allows reply_markup on media; attach buttons only to first send.
|
||||||
|
let finalResult: Awaited<ReturnType<typeof send>> | undefined;
|
||||||
|
for (let i = 0; i < mediaUrls.length; i += 1) {
|
||||||
|
const mediaUrl = mediaUrls[i];
|
||||||
|
const isFirst = i === 0;
|
||||||
|
finalResult = await send(to, isFirst ? text : "", {
|
||||||
|
...baseOpts,
|
||||||
|
mediaUrl,
|
||||||
|
...(isFirst ? { buttons: telegramData?.buttons } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { channel: "telegram", ...(finalResult ?? { messageId: "unknown", chatId: to }) };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
106
src/telegram/bot-native-commands.plugin-auth.test.ts
Normal file
106
src/telegram/bot-native-commands.plugin-auth.test.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { ChannelGroupPolicy } from "../config/group-policy.js";
|
||||||
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
|
import type { TelegramAccountConfig } from "../config/types.js";
|
||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import { registerTelegramNativeCommands } from "./bot-native-commands.js";
|
||||||
|
|
||||||
|
const getPluginCommandSpecs = vi.hoisted(() => vi.fn());
|
||||||
|
const matchPluginCommand = vi.hoisted(() => vi.fn());
|
||||||
|
const executePluginCommand = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
|
vi.mock("../plugins/commands.js", () => ({
|
||||||
|
getPluginCommandSpecs,
|
||||||
|
matchPluginCommand,
|
||||||
|
executePluginCommand,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const deliverReplies = vi.hoisted(() => vi.fn(async () => {}));
|
||||||
|
vi.mock("./bot/delivery.js", () => ({ deliverReplies }));
|
||||||
|
|
||||||
|
vi.mock("./pairing-store.js", () => ({
|
||||||
|
readTelegramAllowFromStore: vi.fn(async () => []),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("registerTelegramNativeCommands (plugin auth)", () => {
|
||||||
|
it("allows requireAuth:false plugin command even when sender is unauthorized", async () => {
|
||||||
|
const command = {
|
||||||
|
name: "plugin",
|
||||||
|
description: "Plugin command",
|
||||||
|
requireAuth: false,
|
||||||
|
handler: vi.fn(),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
getPluginCommandSpecs.mockReturnValue([{ name: "plugin", description: "Plugin command" }]);
|
||||||
|
matchPluginCommand.mockReturnValue({ command, args: undefined });
|
||||||
|
executePluginCommand.mockResolvedValue({ text: "ok" });
|
||||||
|
|
||||||
|
const handlers: Record<string, (ctx: unknown) => Promise<void>> = {};
|
||||||
|
const bot = {
|
||||||
|
api: {
|
||||||
|
setMyCommands: vi.fn().mockResolvedValue(undefined),
|
||||||
|
sendMessage: vi.fn(),
|
||||||
|
},
|
||||||
|
command: (name: string, handler: (ctx: unknown) => Promise<void>) => {
|
||||||
|
handlers[name] = handler;
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const cfg = {} as ClawdbotConfig;
|
||||||
|
const telegramCfg = {} as TelegramAccountConfig;
|
||||||
|
const resolveGroupPolicy = () =>
|
||||||
|
({
|
||||||
|
allowlistEnabled: false,
|
||||||
|
allowed: true,
|
||||||
|
}) as ChannelGroupPolicy;
|
||||||
|
|
||||||
|
registerTelegramNativeCommands({
|
||||||
|
bot: bot as unknown as Parameters<typeof registerTelegramNativeCommands>[0]["bot"],
|
||||||
|
cfg,
|
||||||
|
runtime: {} as RuntimeEnv,
|
||||||
|
accountId: "default",
|
||||||
|
telegramCfg,
|
||||||
|
allowFrom: ["999"],
|
||||||
|
groupAllowFrom: [],
|
||||||
|
replyToMode: "off",
|
||||||
|
textLimit: 4000,
|
||||||
|
useAccessGroups: false,
|
||||||
|
nativeEnabled: false,
|
||||||
|
nativeSkillsEnabled: false,
|
||||||
|
nativeDisabledExplicit: false,
|
||||||
|
resolveGroupPolicy,
|
||||||
|
resolveTelegramGroupConfig: () => ({
|
||||||
|
groupConfig: undefined,
|
||||||
|
topicConfig: undefined,
|
||||||
|
}),
|
||||||
|
shouldSkipUpdate: () => false,
|
||||||
|
opts: { token: "token" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
message: {
|
||||||
|
chat: { id: 123, type: "private" },
|
||||||
|
from: { id: 111, username: "nope" },
|
||||||
|
message_id: 10,
|
||||||
|
date: 123456,
|
||||||
|
},
|
||||||
|
match: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
await handlers.plugin?.(ctx);
|
||||||
|
|
||||||
|
expect(matchPluginCommand).toHaveBeenCalled();
|
||||||
|
expect(executePluginCommand).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
isAuthorizedSender: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(deliverReplies).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
replies: [{ text: "ok" }],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(bot.api.sendMessage).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -17,13 +17,17 @@ import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/pr
|
|||||||
import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
|
import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
|
||||||
import { danger, logVerbose } from "../globals.js";
|
import { danger, logVerbose } from "../globals.js";
|
||||||
import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
|
import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
|
||||||
|
import {
|
||||||
|
normalizeTelegramCommandName,
|
||||||
|
TELEGRAM_COMMAND_NAME_PATTERN,
|
||||||
|
} from "../config/telegram-custom-commands.js";
|
||||||
import { resolveAgentRoute } from "../routing/resolve-route.js";
|
import { resolveAgentRoute } from "../routing/resolve-route.js";
|
||||||
import { resolveThreadSessionKeys } from "../routing/session-key.js";
|
import { resolveThreadSessionKeys } from "../routing/session-key.js";
|
||||||
import { resolveCommandAuthorizedFromAuthorizers } from "../channels/command-gating.js";
|
import { resolveCommandAuthorizedFromAuthorizers } from "../channels/command-gating.js";
|
||||||
import {
|
import {
|
||||||
|
executePluginCommand,
|
||||||
getPluginCommandSpecs,
|
getPluginCommandSpecs,
|
||||||
matchPluginCommand,
|
matchPluginCommand,
|
||||||
executePluginCommand,
|
|
||||||
} from "../plugins/commands.js";
|
} from "../plugins/commands.js";
|
||||||
import type { ChannelGroupPolicy } from "../config/group-policy.js";
|
import type { ChannelGroupPolicy } from "../config/group-policy.js";
|
||||||
import type {
|
import type {
|
||||||
@@ -47,6 +51,18 @@ import { readTelegramAllowFromStore } from "./pairing-store.js";
|
|||||||
|
|
||||||
type TelegramNativeCommandContext = Context & { match?: string };
|
type TelegramNativeCommandContext = Context & { match?: string };
|
||||||
|
|
||||||
|
type TelegramCommandAuthResult = {
|
||||||
|
chatId: number;
|
||||||
|
isGroup: boolean;
|
||||||
|
isForum: boolean;
|
||||||
|
resolvedThreadId?: number;
|
||||||
|
senderId: string;
|
||||||
|
senderUsername: string;
|
||||||
|
groupConfig?: TelegramGroupConfig;
|
||||||
|
topicConfig?: TelegramTopicConfig;
|
||||||
|
commandAuthorized: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type RegisterTelegramNativeCommandsParams = {
|
type RegisterTelegramNativeCommandsParams = {
|
||||||
bot: Bot;
|
bot: Bot;
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
@@ -70,6 +86,134 @@ type RegisterTelegramNativeCommandsParams = {
|
|||||||
opts: { token: string };
|
opts: { token: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function resolveTelegramCommandAuth(params: {
|
||||||
|
msg: NonNullable<TelegramNativeCommandContext["message"]>;
|
||||||
|
bot: Bot;
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
telegramCfg: TelegramAccountConfig;
|
||||||
|
allowFrom?: Array<string | number>;
|
||||||
|
groupAllowFrom?: Array<string | number>;
|
||||||
|
useAccessGroups: boolean;
|
||||||
|
resolveGroupPolicy: (chatId: string | number) => ChannelGroupPolicy;
|
||||||
|
resolveTelegramGroupConfig: (
|
||||||
|
chatId: string | number,
|
||||||
|
messageThreadId?: number,
|
||||||
|
) => { groupConfig?: TelegramGroupConfig; topicConfig?: TelegramTopicConfig };
|
||||||
|
requireAuth: boolean;
|
||||||
|
}): Promise<TelegramCommandAuthResult | null> {
|
||||||
|
const {
|
||||||
|
msg,
|
||||||
|
bot,
|
||||||
|
cfg,
|
||||||
|
telegramCfg,
|
||||||
|
allowFrom,
|
||||||
|
groupAllowFrom,
|
||||||
|
useAccessGroups,
|
||||||
|
resolveGroupPolicy,
|
||||||
|
resolveTelegramGroupConfig,
|
||||||
|
requireAuth,
|
||||||
|
} = params;
|
||||||
|
const chatId = msg.chat.id;
|
||||||
|
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
|
||||||
|
const messageThreadId = (msg as { message_thread_id?: number }).message_thread_id;
|
||||||
|
const isForum = (msg.chat as { is_forum?: boolean }).is_forum === true;
|
||||||
|
const resolvedThreadId = resolveTelegramForumThreadId({
|
||||||
|
isForum,
|
||||||
|
messageThreadId,
|
||||||
|
});
|
||||||
|
const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
|
||||||
|
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId);
|
||||||
|
const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
|
||||||
|
const effectiveGroupAllow = normalizeAllowFromWithStore({
|
||||||
|
allowFrom: groupAllowOverride ?? groupAllowFrom,
|
||||||
|
storeAllowFrom,
|
||||||
|
});
|
||||||
|
const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined";
|
||||||
|
const senderIdRaw = msg.from?.id;
|
||||||
|
const senderId = senderIdRaw ? String(senderIdRaw) : "";
|
||||||
|
const senderUsername = msg.from?.username ?? "";
|
||||||
|
|
||||||
|
if (isGroup && groupConfig?.enabled === false) {
|
||||||
|
await bot.api.sendMessage(chatId, "This group is disabled.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (isGroup && topicConfig?.enabled === false) {
|
||||||
|
await bot.api.sendMessage(chatId, "This topic is disabled.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (requireAuth && isGroup && hasGroupAllowOverride) {
|
||||||
|
if (
|
||||||
|
senderIdRaw == null ||
|
||||||
|
!isSenderAllowed({
|
||||||
|
allow: effectiveGroupAllow,
|
||||||
|
senderId: String(senderIdRaw),
|
||||||
|
senderUsername,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
await bot.api.sendMessage(chatId, "You are not authorized to use this command.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGroup && useAccessGroups) {
|
||||||
|
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
||||||
|
const groupPolicy = telegramCfg.groupPolicy ?? defaultGroupPolicy ?? "open";
|
||||||
|
if (groupPolicy === "disabled") {
|
||||||
|
await bot.api.sendMessage(chatId, "Telegram group commands are disabled.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (groupPolicy === "allowlist" && requireAuth) {
|
||||||
|
if (
|
||||||
|
senderIdRaw == null ||
|
||||||
|
!isSenderAllowed({
|
||||||
|
allow: effectiveGroupAllow,
|
||||||
|
senderId: String(senderIdRaw),
|
||||||
|
senderUsername,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
await bot.api.sendMessage(chatId, "You are not authorized to use this command.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const groupAllowlist = resolveGroupPolicy(chatId);
|
||||||
|
if (groupAllowlist.allowlistEnabled && !groupAllowlist.allowed) {
|
||||||
|
await bot.api.sendMessage(chatId, "This group is not allowed.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dmAllow = normalizeAllowFromWithStore({
|
||||||
|
allowFrom: allowFrom,
|
||||||
|
storeAllowFrom,
|
||||||
|
});
|
||||||
|
const senderAllowed = isSenderAllowed({
|
||||||
|
allow: dmAllow,
|
||||||
|
senderId,
|
||||||
|
senderUsername,
|
||||||
|
});
|
||||||
|
const commandAuthorized = resolveCommandAuthorizedFromAuthorizers({
|
||||||
|
useAccessGroups,
|
||||||
|
authorizers: [{ configured: dmAllow.hasEntries, allowed: senderAllowed }],
|
||||||
|
modeWhenAccessGroupsOff: "configured",
|
||||||
|
});
|
||||||
|
if (requireAuth && !commandAuthorized) {
|
||||||
|
await bot.api.sendMessage(chatId, "You are not authorized to use this command.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
chatId,
|
||||||
|
isGroup,
|
||||||
|
isForum,
|
||||||
|
resolvedThreadId,
|
||||||
|
senderId,
|
||||||
|
senderUsername,
|
||||||
|
groupConfig,
|
||||||
|
topicConfig,
|
||||||
|
commandAuthorized,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const registerTelegramNativeCommands = ({
|
export const registerTelegramNativeCommands = ({
|
||||||
bot,
|
bot,
|
||||||
cfg,
|
cfg,
|
||||||
@@ -109,15 +253,49 @@ export const registerTelegramNativeCommands = ({
|
|||||||
}
|
}
|
||||||
const customCommands = customResolution.commands;
|
const customCommands = customResolution.commands;
|
||||||
const pluginCommandSpecs = getPluginCommandSpecs();
|
const pluginCommandSpecs = getPluginCommandSpecs();
|
||||||
|
const pluginCommands: Array<{ command: string; description: string }> = [];
|
||||||
|
const existingCommands = new Set(
|
||||||
|
[
|
||||||
|
...nativeCommands.map((command) => command.name),
|
||||||
|
...customCommands.map((command) => command.command),
|
||||||
|
].map((command) => command.toLowerCase()),
|
||||||
|
);
|
||||||
|
const pluginCommandNames = new Set<string>();
|
||||||
|
for (const spec of pluginCommandSpecs) {
|
||||||
|
const normalized = normalizeTelegramCommandName(spec.name);
|
||||||
|
if (!normalized || !TELEGRAM_COMMAND_NAME_PATTERN.test(normalized)) {
|
||||||
|
runtime.error?.(
|
||||||
|
danger(
|
||||||
|
`Plugin command "/${spec.name}" is invalid for Telegram (use a-z, 0-9, underscore; max 32 chars).`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const description = spec.description.trim();
|
||||||
|
if (!description) {
|
||||||
|
runtime.error?.(danger(`Plugin command "/${normalized}" is missing a description.`));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (existingCommands.has(normalized)) {
|
||||||
|
runtime.error?.(
|
||||||
|
danger(`Plugin command "/${normalized}" conflicts with an existing Telegram command.`),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (pluginCommandNames.has(normalized)) {
|
||||||
|
runtime.error?.(danger(`Plugin command "/${normalized}" is duplicated.`));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pluginCommandNames.add(normalized);
|
||||||
|
existingCommands.add(normalized);
|
||||||
|
pluginCommands.push({ command: normalized, description });
|
||||||
|
}
|
||||||
const allCommands: Array<{ command: string; description: string }> = [
|
const allCommands: Array<{ command: string; description: string }> = [
|
||||||
...nativeCommands.map((command) => ({
|
...nativeCommands.map((command) => ({
|
||||||
command: command.name,
|
command: command.name,
|
||||||
description: command.description,
|
description: command.description,
|
||||||
})),
|
})),
|
||||||
...pluginCommandSpecs.map((spec) => ({
|
...pluginCommands,
|
||||||
command: spec.name,
|
|
||||||
description: spec.description,
|
|
||||||
})),
|
|
||||||
...customCommands,
|
...customCommands,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -134,99 +312,30 @@ export const registerTelegramNativeCommands = ({
|
|||||||
const msg = ctx.message;
|
const msg = ctx.message;
|
||||||
if (!msg) return;
|
if (!msg) return;
|
||||||
if (shouldSkipUpdate(ctx)) return;
|
if (shouldSkipUpdate(ctx)) return;
|
||||||
const chatId = msg.chat.id;
|
const auth = await resolveTelegramCommandAuth({
|
||||||
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
|
msg,
|
||||||
const messageThreadId = (msg as { message_thread_id?: number }).message_thread_id;
|
bot,
|
||||||
const isForum = (msg.chat as { is_forum?: boolean }).is_forum === true;
|
cfg,
|
||||||
const resolvedThreadId = resolveTelegramForumThreadId({
|
telegramCfg,
|
||||||
|
allowFrom,
|
||||||
|
groupAllowFrom,
|
||||||
|
useAccessGroups,
|
||||||
|
resolveGroupPolicy,
|
||||||
|
resolveTelegramGroupConfig,
|
||||||
|
requireAuth: true,
|
||||||
|
});
|
||||||
|
if (!auth) return;
|
||||||
|
const {
|
||||||
|
chatId,
|
||||||
|
isGroup,
|
||||||
isForum,
|
isForum,
|
||||||
messageThreadId,
|
resolvedThreadId,
|
||||||
});
|
|
||||||
const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
|
|
||||||
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId);
|
|
||||||
const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
|
|
||||||
const effectiveGroupAllow = normalizeAllowFromWithStore({
|
|
||||||
allowFrom: groupAllowOverride ?? groupAllowFrom,
|
|
||||||
storeAllowFrom,
|
|
||||||
});
|
|
||||||
const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined";
|
|
||||||
|
|
||||||
if (isGroup && groupConfig?.enabled === false) {
|
|
||||||
await bot.api.sendMessage(chatId, "This group is disabled.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isGroup && topicConfig?.enabled === false) {
|
|
||||||
await bot.api.sendMessage(chatId, "This topic is disabled.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isGroup && hasGroupAllowOverride) {
|
|
||||||
const senderId = msg.from?.id;
|
|
||||||
const senderUsername = msg.from?.username ?? "";
|
|
||||||
if (
|
|
||||||
senderId == null ||
|
|
||||||
!isSenderAllowed({
|
|
||||||
allow: effectiveGroupAllow,
|
|
||||||
senderId: String(senderId),
|
|
||||||
senderUsername,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
await bot.api.sendMessage(chatId, "You are not authorized to use this command.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGroup && useAccessGroups) {
|
|
||||||
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
|
||||||
const groupPolicy = telegramCfg.groupPolicy ?? defaultGroupPolicy ?? "open";
|
|
||||||
if (groupPolicy === "disabled") {
|
|
||||||
await bot.api.sendMessage(chatId, "Telegram group commands are disabled.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (groupPolicy === "allowlist") {
|
|
||||||
const senderId = msg.from?.id;
|
|
||||||
if (senderId == null) {
|
|
||||||
await bot.api.sendMessage(chatId, "You are not authorized to use this command.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const senderUsername = msg.from?.username ?? "";
|
|
||||||
if (
|
|
||||||
!isSenderAllowed({
|
|
||||||
allow: effectiveGroupAllow,
|
|
||||||
senderId: String(senderId),
|
|
||||||
senderUsername,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
await bot.api.sendMessage(chatId, "You are not authorized to use this command.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const groupAllowlist = resolveGroupPolicy(chatId);
|
|
||||||
if (groupAllowlist.allowlistEnabled && !groupAllowlist.allowed) {
|
|
||||||
await bot.api.sendMessage(chatId, "This group is not allowed.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const senderId = msg.from?.id ? String(msg.from.id) : "";
|
|
||||||
const senderUsername = msg.from?.username ?? "";
|
|
||||||
const dmAllow = normalizeAllowFromWithStore({
|
|
||||||
allowFrom: allowFrom,
|
|
||||||
storeAllowFrom,
|
|
||||||
});
|
|
||||||
const senderAllowed = isSenderAllowed({
|
|
||||||
allow: dmAllow,
|
|
||||||
senderId,
|
senderId,
|
||||||
senderUsername,
|
senderUsername,
|
||||||
});
|
groupConfig,
|
||||||
const commandAuthorized = resolveCommandAuthorizedFromAuthorizers({
|
topicConfig,
|
||||||
useAccessGroups,
|
commandAuthorized,
|
||||||
authorizers: [{ configured: dmAllow.hasEntries, allowed: senderAllowed }],
|
} = auth;
|
||||||
modeWhenAccessGroupsOff: "configured",
|
|
||||||
});
|
|
||||||
if (!commandAuthorized) {
|
|
||||||
await bot.api.sendMessage(chatId, "You are not authorized to use this command.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const commandDefinition = findCommandByNativeName(command.name, "telegram");
|
const commandDefinition = findCommandByNativeName(command.name, "telegram");
|
||||||
const rawText = ctx.match?.trim() ?? "";
|
const rawText = ctx.match?.trim() ?? "";
|
||||||
@@ -373,114 +482,33 @@ export const registerTelegramNativeCommands = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register handlers for plugin commands
|
for (const pluginCommand of pluginCommands) {
|
||||||
for (const pluginSpec of pluginCommandSpecs) {
|
bot.command(pluginCommand.command, async (ctx: TelegramNativeCommandContext) => {
|
||||||
bot.command(pluginSpec.name, async (ctx: TelegramNativeCommandContext) => {
|
|
||||||
const msg = ctx.message;
|
const msg = ctx.message;
|
||||||
if (!msg) return;
|
if (!msg) return;
|
||||||
if (shouldSkipUpdate(ctx)) return;
|
if (shouldSkipUpdate(ctx)) return;
|
||||||
const chatId = msg.chat.id;
|
const chatId = msg.chat.id;
|
||||||
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
|
|
||||||
const messageThreadId = (msg as { message_thread_id?: number }).message_thread_id;
|
|
||||||
const isForum = (msg.chat as { is_forum?: boolean }).is_forum === true;
|
|
||||||
const resolvedThreadId = resolveTelegramForumThreadId({
|
|
||||||
isForum,
|
|
||||||
messageThreadId,
|
|
||||||
});
|
|
||||||
const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
|
|
||||||
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId);
|
|
||||||
const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
|
|
||||||
const effectiveGroupAllow = normalizeAllowFromWithStore({
|
|
||||||
allowFrom: groupAllowOverride ?? groupAllowFrom,
|
|
||||||
storeAllowFrom,
|
|
||||||
});
|
|
||||||
const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined";
|
|
||||||
|
|
||||||
if (isGroup && groupConfig?.enabled === false) {
|
|
||||||
await bot.api.sendMessage(chatId, "This group is disabled.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isGroup && topicConfig?.enabled === false) {
|
|
||||||
await bot.api.sendMessage(chatId, "This topic is disabled.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isGroup && hasGroupAllowOverride) {
|
|
||||||
const senderId = msg.from?.id;
|
|
||||||
const senderUsername = msg.from?.username ?? "";
|
|
||||||
if (
|
|
||||||
senderId == null ||
|
|
||||||
!isSenderAllowed({
|
|
||||||
allow: effectiveGroupAllow,
|
|
||||||
senderId: String(senderId),
|
|
||||||
senderUsername,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
await bot.api.sendMessage(chatId, "You are not authorized to use this command.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGroup && useAccessGroups) {
|
|
||||||
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
|
||||||
const groupPolicy = telegramCfg.groupPolicy ?? defaultGroupPolicy ?? "open";
|
|
||||||
if (groupPolicy === "disabled") {
|
|
||||||
await bot.api.sendMessage(chatId, "Telegram group commands are disabled.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (groupPolicy === "allowlist") {
|
|
||||||
const senderId = msg.from?.id;
|
|
||||||
if (senderId == null) {
|
|
||||||
await bot.api.sendMessage(chatId, "You are not authorized to use this command.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const senderUsername = msg.from?.username ?? "";
|
|
||||||
if (
|
|
||||||
!isSenderAllowed({
|
|
||||||
allow: effectiveGroupAllow,
|
|
||||||
senderId: String(senderId),
|
|
||||||
senderUsername,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
await bot.api.sendMessage(chatId, "You are not authorized to use this command.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const groupAllowlist = resolveGroupPolicy(chatId);
|
|
||||||
if (groupAllowlist.allowlistEnabled && !groupAllowlist.allowed) {
|
|
||||||
await bot.api.sendMessage(chatId, "This group is not allowed.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const senderId = msg.from?.id ? String(msg.from.id) : "";
|
|
||||||
const senderUsername = msg.from?.username ?? "";
|
|
||||||
const dmAllow = normalizeAllowFromWithStore({
|
|
||||||
allowFrom: allowFrom,
|
|
||||||
storeAllowFrom,
|
|
||||||
});
|
|
||||||
const senderAllowed = isSenderAllowed({
|
|
||||||
allow: dmAllow,
|
|
||||||
senderId,
|
|
||||||
senderUsername,
|
|
||||||
});
|
|
||||||
const commandAuthorized = resolveCommandAuthorizedFromAuthorizers({
|
|
||||||
useAccessGroups,
|
|
||||||
authorizers: [{ configured: dmAllow.hasEntries, allowed: senderAllowed }],
|
|
||||||
modeWhenAccessGroupsOff: "configured",
|
|
||||||
});
|
|
||||||
if (!commandAuthorized) {
|
|
||||||
await bot.api.sendMessage(chatId, "You are not authorized to use this command.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match and execute plugin command
|
|
||||||
const rawText = ctx.match?.trim() ?? "";
|
const rawText = ctx.match?.trim() ?? "";
|
||||||
const commandBody = `/${pluginSpec.name}${rawText ? ` ${rawText}` : ""}`;
|
const commandBody = `/${pluginCommand.command}${rawText ? ` ${rawText}` : ""}`;
|
||||||
const match = matchPluginCommand(commandBody);
|
const match = matchPluginCommand(commandBody);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
await bot.api.sendMessage(chatId, "Command not found.");
|
await bot.api.sendMessage(chatId, "Command not found.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const auth = await resolveTelegramCommandAuth({
|
||||||
|
msg,
|
||||||
|
bot,
|
||||||
|
cfg,
|
||||||
|
telegramCfg,
|
||||||
|
allowFrom,
|
||||||
|
groupAllowFrom,
|
||||||
|
useAccessGroups,
|
||||||
|
resolveGroupPolicy,
|
||||||
|
resolveTelegramGroupConfig,
|
||||||
|
requireAuth: match.command.requireAuth !== false,
|
||||||
|
});
|
||||||
|
if (!auth) return;
|
||||||
|
const { resolvedThreadId, senderId, commandAuthorized } = auth;
|
||||||
|
|
||||||
const result = await executePluginCommand({
|
const result = await executePluginCommand({
|
||||||
command: match.command,
|
command: match.command,
|
||||||
@@ -491,8 +519,6 @@ export const registerTelegramNativeCommands = ({
|
|||||||
commandBody,
|
commandBody,
|
||||||
config: cfg,
|
config: cfg,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Deliver the result
|
|
||||||
const tableMode = resolveMarkdownTableMode({
|
const tableMode = resolveMarkdownTableMode({
|
||||||
cfg,
|
cfg,
|
||||||
channel: "telegram",
|
channel: "telegram",
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import { isGifMedia } from "../../media/mime.js";
|
|||||||
import { saveMediaBuffer } from "../../media/store.js";
|
import { saveMediaBuffer } from "../../media/store.js";
|
||||||
import type { RuntimeEnv } from "../../runtime.js";
|
import type { RuntimeEnv } from "../../runtime.js";
|
||||||
import { loadWebMedia } from "../../web/media.js";
|
import { loadWebMedia } from "../../web/media.js";
|
||||||
import { resolveTelegramVoiceSend } from "../voice.js";
|
|
||||||
import { buildInlineKeyboard } from "../send.js";
|
import { buildInlineKeyboard } from "../send.js";
|
||||||
|
import { resolveTelegramVoiceSend } from "../voice.js";
|
||||||
import { buildTelegramThreadParams, resolveTelegramReplyId } from "./helpers.js";
|
import { buildTelegramThreadParams, resolveTelegramReplyId } from "./helpers.js";
|
||||||
import type { TelegramContext } from "./types.js";
|
import type { TelegramContext } from "./types.js";
|
||||||
|
|
||||||
@@ -81,18 +81,16 @@ export async function deliverReplies(params: {
|
|||||||
: reply.mediaUrl
|
: reply.mediaUrl
|
||||||
? [reply.mediaUrl]
|
? [reply.mediaUrl]
|
||||||
: [];
|
: [];
|
||||||
|
const telegramData = reply.channelData?.telegram as
|
||||||
|
| { buttons?: Array<Array<{ text: string; callback_data: string }>> }
|
||||||
|
| undefined;
|
||||||
|
const replyMarkup = buildInlineKeyboard(telegramData?.buttons);
|
||||||
if (mediaList.length === 0) {
|
if (mediaList.length === 0) {
|
||||||
// Extract Telegram buttons from channelData
|
|
||||||
const telegramData = reply.channelData?.telegram as
|
|
||||||
| { buttons?: Array<Array<{ text: string; callback_data: string }>> }
|
|
||||||
| undefined;
|
|
||||||
const replyMarkup = buildInlineKeyboard(telegramData?.buttons);
|
|
||||||
|
|
||||||
const chunks = chunkText(reply.text || "");
|
const chunks = chunkText(reply.text || "");
|
||||||
for (let i = 0; i < chunks.length; i++) {
|
for (let i = 0; i < chunks.length; i += 1) {
|
||||||
const chunk = chunks[i];
|
const chunk = chunks[i];
|
||||||
if (!chunk) continue;
|
if (!chunk) continue;
|
||||||
// Only attach buttons to the first chunk
|
// Only attach buttons to the first chunk.
|
||||||
const shouldAttachButtons = i === 0 && replyMarkup;
|
const shouldAttachButtons = i === 0 && replyMarkup;
|
||||||
await sendTelegramText(bot, chatId, chunk.html, runtime, {
|
await sendTelegramText(bot, chatId, chunk.html, runtime, {
|
||||||
replyToMessageId:
|
replyToMessageId:
|
||||||
@@ -137,10 +135,12 @@ export async function deliverReplies(params: {
|
|||||||
first = false;
|
first = false;
|
||||||
const replyToMessageId =
|
const replyToMessageId =
|
||||||
replyToId && (replyToMode === "all" || !hasReplied) ? replyToId : undefined;
|
replyToId && (replyToMode === "all" || !hasReplied) ? replyToId : undefined;
|
||||||
|
const shouldAttachButtonsToMedia = isFirstMedia && replyMarkup && !followUpText;
|
||||||
const mediaParams: Record<string, unknown> = {
|
const mediaParams: Record<string, unknown> = {
|
||||||
caption: htmlCaption,
|
caption: htmlCaption,
|
||||||
reply_to_message_id: replyToMessageId,
|
reply_to_message_id: replyToMessageId,
|
||||||
...(htmlCaption ? { parse_mode: "HTML" } : {}),
|
...(htmlCaption ? { parse_mode: "HTML" } : {}),
|
||||||
|
...(shouldAttachButtonsToMedia ? { reply_markup: replyMarkup } : {}),
|
||||||
};
|
};
|
||||||
if (threadParams) {
|
if (threadParams) {
|
||||||
mediaParams.message_thread_id = threadParams.message_thread_id;
|
mediaParams.message_thread_id = threadParams.message_thread_id;
|
||||||
@@ -195,6 +195,7 @@ export async function deliverReplies(params: {
|
|||||||
hasReplied,
|
hasReplied,
|
||||||
messageThreadId,
|
messageThreadId,
|
||||||
linkPreview,
|
linkPreview,
|
||||||
|
replyMarkup,
|
||||||
});
|
});
|
||||||
// Skip this media item; continue with next.
|
// Skip this media item; continue with next.
|
||||||
continue;
|
continue;
|
||||||
@@ -219,7 +220,8 @@ export async function deliverReplies(params: {
|
|||||||
// 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 && isFirstMedia) {
|
||||||
const chunks = chunkText(pendingFollowUpText);
|
const chunks = chunkText(pendingFollowUpText);
|
||||||
for (const chunk of chunks) {
|
for (let i = 0; i < chunks.length; i += 1) {
|
||||||
|
const chunk = chunks[i];
|
||||||
const replyToMessageIdFollowup =
|
const replyToMessageIdFollowup =
|
||||||
replyToId && (replyToMode === "all" || !hasReplied) ? replyToId : undefined;
|
replyToId && (replyToMode === "all" || !hasReplied) ? replyToId : undefined;
|
||||||
await sendTelegramText(bot, chatId, chunk.html, runtime, {
|
await sendTelegramText(bot, chatId, chunk.html, runtime, {
|
||||||
@@ -228,6 +230,7 @@ export async function deliverReplies(params: {
|
|||||||
textMode: "html",
|
textMode: "html",
|
||||||
plainText: chunk.text,
|
plainText: chunk.text,
|
||||||
linkPreview,
|
linkPreview,
|
||||||
|
replyMarkup: i === 0 ? replyMarkup : undefined,
|
||||||
});
|
});
|
||||||
if (replyToId && !hasReplied) {
|
if (replyToId && !hasReplied) {
|
||||||
hasReplied = true;
|
hasReplied = true;
|
||||||
@@ -289,10 +292,12 @@ async function sendTelegramVoiceFallbackText(opts: {
|
|||||||
hasReplied: boolean;
|
hasReplied: boolean;
|
||||||
messageThreadId?: number;
|
messageThreadId?: number;
|
||||||
linkPreview?: boolean;
|
linkPreview?: boolean;
|
||||||
|
replyMarkup?: ReturnType<typeof buildInlineKeyboard>;
|
||||||
}): Promise<boolean> {
|
}): Promise<boolean> {
|
||||||
const chunks = opts.chunkText(opts.text);
|
const chunks = opts.chunkText(opts.text);
|
||||||
let hasReplied = opts.hasReplied;
|
let hasReplied = opts.hasReplied;
|
||||||
for (const chunk of chunks) {
|
for (let i = 0; i < chunks.length; i += 1) {
|
||||||
|
const chunk = chunks[i];
|
||||||
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,
|
||||||
@@ -300,6 +305,7 @@ async function sendTelegramVoiceFallbackText(opts: {
|
|||||||
textMode: "html",
|
textMode: "html",
|
||||||
plainText: chunk.text,
|
plainText: chunk.text,
|
||||||
linkPreview: opts.linkPreview,
|
linkPreview: opts.linkPreview,
|
||||||
|
replyMarkup: i === 0 ? opts.replyMarkup : undefined,
|
||||||
});
|
});
|
||||||
if (opts.replyToId && !hasReplied) {
|
if (opts.replyToId && !hasReplied) {
|
||||||
hasReplied = true;
|
hasReplied = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user