mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-15 00:56:58 +00:00
feat(telegram): add sendPoll support
This commit is contained in:
committed by
Peter Steinberger
parent
fc5d147d1b
commit
08da8e00bf
@@ -96,6 +96,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
|
||||
reactions: true,
|
||||
threads: true,
|
||||
media: true,
|
||||
polls: true,
|
||||
nativeCommands: true,
|
||||
blockStreaming: true,
|
||||
},
|
||||
@@ -273,6 +274,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
|
||||
chunker: (text, limit) => getTelegramRuntime().channel.text.chunkMarkdownText(text, limit),
|
||||
chunkerMode: "markdown",
|
||||
textChunkLimit: 4000,
|
||||
pollMaxOptions: 10,
|
||||
sendText: async ({ to, text, accountId, deps, replyToId, threadId }) => {
|
||||
const send = deps?.sendTelegram ?? getTelegramRuntime().channel.telegram.sendMessageTelegram;
|
||||
const replyToMessageId = parseReplyToMessageId(replyToId);
|
||||
@@ -298,6 +300,10 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
|
||||
});
|
||||
return { channel: "telegram", ...result };
|
||||
},
|
||||
sendPoll: async ({ to, poll, accountId }) =>
|
||||
await getTelegramRuntime().channel.telegram.sendPollTelegram(to, poll, {
|
||||
accountId: accountId ?? undefined,
|
||||
}),
|
||||
},
|
||||
status: {
|
||||
defaultRuntime: {
|
||||
|
||||
@@ -126,7 +126,7 @@ import {
|
||||
} from "../../telegram/audit.js";
|
||||
import { monitorTelegramProvider } from "../../telegram/monitor.js";
|
||||
import { probeTelegram } from "../../telegram/probe.js";
|
||||
import { sendMessageTelegram } from "../../telegram/send.js";
|
||||
import { sendMessageTelegram, sendPollTelegram } from "../../telegram/send.js";
|
||||
import { resolveTelegramToken } from "../../telegram/token.js";
|
||||
import { textToSpeechTelephony } from "../../tts/tts.js";
|
||||
import { getActiveWebListener } from "../../web/active-listener.js";
|
||||
@@ -363,6 +363,7 @@ export function createPluginRuntime(): PluginRuntime {
|
||||
probeTelegram,
|
||||
resolveTelegramToken,
|
||||
sendMessageTelegram,
|
||||
sendPollTelegram,
|
||||
monitorTelegramProvider,
|
||||
messageActions: telegramMessageActions,
|
||||
},
|
||||
|
||||
@@ -120,6 +120,7 @@ type CollectTelegramUnmentionedGroupIds =
|
||||
type ProbeTelegram = typeof import("../../telegram/probe.js").probeTelegram;
|
||||
type ResolveTelegramToken = typeof import("../../telegram/token.js").resolveTelegramToken;
|
||||
type SendMessageTelegram = typeof import("../../telegram/send.js").sendMessageTelegram;
|
||||
type SendPollTelegram = typeof import("../../telegram/send.js").sendPollTelegram;
|
||||
type MonitorTelegramProvider = typeof import("../../telegram/monitor.js").monitorTelegramProvider;
|
||||
type TelegramMessageActions =
|
||||
typeof import("../../channels/plugins/actions/telegram.js").telegramMessageActions;
|
||||
@@ -301,6 +302,7 @@ export type PluginRuntime = {
|
||||
probeTelegram: ProbeTelegram;
|
||||
resolveTelegramToken: ResolveTelegramToken;
|
||||
sendMessageTelegram: SendMessageTelegram;
|
||||
sendPollTelegram: SendPollTelegram;
|
||||
monitorTelegramProvider: MonitorTelegramProvider;
|
||||
messageActions: TelegramMessageActions;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { createTelegramBot, createTelegramWebhookCallback } from "./bot.js";
|
||||
export { monitorTelegramProvider } from "./monitor.js";
|
||||
export { reactMessageTelegram, sendMessageTelegram } from "./send.js";
|
||||
export { reactMessageTelegram, sendMessageTelegram, sendPollTelegram } from "./send.js";
|
||||
export { startTelegramWebhook } from "./webhook.js";
|
||||
|
||||
@@ -17,6 +17,7 @@ import { redactSensitiveText } from "../logging/redact.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { mediaKindFromMime } from "../media/constants.js";
|
||||
import { isGifMedia } from "../media/mime.js";
|
||||
import { normalizePollInput, type PollInput } from "../polls.js";
|
||||
import { loadWebMedia } from "../web/media.js";
|
||||
import { type ResolvedTelegramAccount, resolveTelegramAccount } from "./accounts.js";
|
||||
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
||||
@@ -923,3 +924,122 @@ export async function sendStickerTelegram(
|
||||
|
||||
return { messageId, chatId: resolvedChatId };
|
||||
}
|
||||
|
||||
type TelegramPollOpts = {
|
||||
token?: string;
|
||||
accountId?: string;
|
||||
verbose?: boolean;
|
||||
api?: Bot["api"];
|
||||
retry?: RetryConfig;
|
||||
/** Message ID to reply to (for threading) */
|
||||
replyToMessageId?: number;
|
||||
/** Forum topic thread ID (for forum supergroups) */
|
||||
messageThreadId?: number;
|
||||
/** Send message silently (no notification). Defaults to false. */
|
||||
silent?: boolean;
|
||||
/** Whether votes are anonymous. Defaults to true (Telegram default). */
|
||||
isAnonymous?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Send a poll to a Telegram chat.
|
||||
* @param to - Chat ID or username (e.g., "123456789" or "@username")
|
||||
* @param poll - Poll input with question, options, maxSelections, and optional durationHours
|
||||
* @param opts - Optional configuration
|
||||
*/
|
||||
export async function sendPollTelegram(
|
||||
to: string,
|
||||
poll: PollInput,
|
||||
opts: TelegramPollOpts = {},
|
||||
): Promise<{ messageId: string; chatId: string; pollId?: string }> {
|
||||
const cfg = loadConfig();
|
||||
const account = resolveTelegramAccount({
|
||||
cfg,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
const token = resolveToken(opts.token, account);
|
||||
const target = parseTelegramTarget(to);
|
||||
const chatId = normalizeChatId(target.chatId);
|
||||
const client = resolveTelegramClientOptions(account);
|
||||
const api = opts.api ?? new Bot(token, client ? { client } : undefined).api;
|
||||
|
||||
// Normalize the poll input (validates question, options, maxSelections)
|
||||
const normalizedPoll = normalizePollInput(poll, { maxOptions: 10 });
|
||||
|
||||
const messageThreadId =
|
||||
opts.messageThreadId != null ? opts.messageThreadId : target.messageThreadId;
|
||||
const threadSpec =
|
||||
messageThreadId != null ? { id: messageThreadId, scope: "forum" as const } : undefined;
|
||||
const threadIdParams = buildTelegramThreadParams(threadSpec);
|
||||
|
||||
// Build poll options as simple strings (Grammy accepts string[] or InputPollOption[])
|
||||
const pollOptions = normalizedPoll.options;
|
||||
|
||||
const request = createTelegramRetryRunner({
|
||||
retry: opts.retry,
|
||||
configRetry: account.config.retry,
|
||||
verbose: opts.verbose,
|
||||
shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }),
|
||||
});
|
||||
const logHttpError = createTelegramHttpLogger(cfg);
|
||||
const requestWithDiag = <T>(fn: () => Promise<T>, label?: string) =>
|
||||
withTelegramApiErrorLogging({
|
||||
operation: label ?? "request",
|
||||
fn: () => request(fn, label),
|
||||
}).catch((err) => {
|
||||
logHttpError(label ?? "request", err);
|
||||
throw err;
|
||||
});
|
||||
|
||||
const wrapChatNotFound = (err: unknown) => {
|
||||
if (!/400: Bad Request: chat not found/i.test(formatErrorMessage(err))) {
|
||||
return err;
|
||||
}
|
||||
return new Error(
|
||||
[
|
||||
`Telegram send failed: chat not found (chat_id=${chatId}).`,
|
||||
"Likely: bot not started in DM, bot removed from group/channel, group migrated (new -100… id), or wrong bot token.",
|
||||
`Input was: ${JSON.stringify(to)}.`,
|
||||
].join(" "),
|
||||
);
|
||||
};
|
||||
|
||||
// Telegram currently supports poll duration between 5 and 600 seconds.
|
||||
// Convert generic durationHours into seconds and clamp to Telegram limits.
|
||||
const openPeriodSeconds =
|
||||
normalizedPoll.durationHours !== undefined
|
||||
? Math.max(5, Math.min(normalizedPoll.durationHours * 3600, 600))
|
||||
: undefined;
|
||||
|
||||
// Build poll parameters following Grammy's api.sendPoll signature
|
||||
// sendPoll(chat_id, question, options, other?, signal?)
|
||||
const pollParams = {
|
||||
allows_multiple_answers: normalizedPoll.maxSelections > 1,
|
||||
is_anonymous: opts.isAnonymous ?? true,
|
||||
...(openPeriodSeconds !== undefined ? { open_period: openPeriodSeconds } : {}),
|
||||
...(threadIdParams ? threadIdParams : {}),
|
||||
...(opts.replyToMessageId != null
|
||||
? { reply_to_message_id: Math.trunc(opts.replyToMessageId) }
|
||||
: {}),
|
||||
...(opts.silent === true ? { disable_notification: true } : {}),
|
||||
};
|
||||
|
||||
const result = await requestWithDiag(
|
||||
() => api.sendPoll(chatId, normalizedPoll.question, pollOptions, pollParams),
|
||||
"poll",
|
||||
).catch((err) => {
|
||||
throw wrapChatNotFound(err);
|
||||
});
|
||||
|
||||
const messageId = String(result?.message_id ?? "unknown");
|
||||
const resolvedChatId = String(result?.chat?.id ?? chatId);
|
||||
const pollId = result?.poll?.id;
|
||||
|
||||
recordChannelActivity({
|
||||
channel: "telegram",
|
||||
accountId: account.accountId,
|
||||
direction: "outbound",
|
||||
});
|
||||
|
||||
return { messageId, chatId: resolvedChatId, pollId };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user