agents
Version:
A home for your AI agents
427 lines (426 loc) • 17.4 kB
JavaScript
import { t as isChannelMessageSurface } from "../surface-bZZJqBka.js";
import { a as isRecord, i as encodeUtf8, n as defaultText, o as renderInput, r as emptyIngressResponse, s as uncertain } from "../internal-CYlgHl1l.js";
import { i as createPacer, r as consumeChunks, t as matchesPath } from "../ingress-BfetZbMO.js";
//#region src/channels/adapters/telegram.ts
const TELEGRAM_MESSAGE_LIMIT = 4096;
const DEFAULT_STREAM_INTERVAL_MS = 500;
const DEFAULT_TELEGRAM_WEBHOOK_PATH = "/webhooks/telegram";
const APPROVAL_INSTRUCTIONS = "Reply YES to approve or NO to reject.";
const INTERACTION_FOOTER = /\n\nReply YES to approve or NO to reject\.\n\n\[channel-interaction:v1:([A-Za-z0-9_-]+)\]$/;
function channelIdentity(chatId) {
return `telegram:chat:${chatId}`;
}
function userIdentity(userId) {
return `telegram:user:${userId}`;
}
function messageIdentity(chatId, messageId) {
return `${channelIdentity(chatId)}:message:${messageId}`;
}
function updateIdentity(chatId, updateId) {
return `${channelIdentity(chatId)}:update:${updateId}`;
}
function encodeInteractionId(interactionId) {
const bytes = encodeUtf8(interactionId);
let binary = "";
for (const byte of bytes) binary += String.fromCharCode(byte);
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function decodeInteractionId(value) {
try {
const base64 = value.replace(/-/g, "+").replace(/_/g, "/");
const padding = "=".repeat((4 - base64.length % 4) % 4);
const binary = atob(base64 + padding);
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
return new TextDecoder("utf-8", { fatal: true }).decode(bytes) || void 0;
} catch {
return;
}
}
function approvalText(interactionId, request) {
return [
...request.title ? [request.title] : [],
request.summary,
`Input:\n${renderInput(request.input)}`,
APPROVAL_INSTRUCTIONS,
`[channel-interaction:v1:${encodeInteractionId(interactionId)}]`
].join("\n\n");
}
function asApiResponse(value) {
return value !== null && typeof value === "object" ? value : void 0;
}
function classifyResponse(response, payload) {
if (payload.ok === true) {
const result = asApiResponse(payload.result);
if (typeof result?.message_id === "number") return {
status: "delivered",
reference: String(result.message_id)
};
return uncertain("TELEGRAM_DELIVERY_ERROR", "Telegram returned an invalid delivery response");
}
if (payload.ok === false) {
const errorCode = typeof payload.error_code === "number" ? payload.error_code : response.status;
const message = typeof payload.description === "string" ? payload.description : "Telegram rejected the message";
const error = {
code: `TELEGRAM_API_ERROR_${errorCode}`,
message
};
if (errorCode >= 500) return {
status: "uncertain",
error
};
return {
status: "failed",
retryable: errorCode === 429,
error
};
}
return uncertain("TELEGRAM_DELIVERY_ERROR", "Telegram returned an invalid delivery response");
}
function asTelegramMessage(value) {
return value !== null && typeof value === "object" ? value : void 0;
}
function telegramActor(message) {
const sender = message.from;
if (sender) {
const fullName = [sender.first_name, sender.last_name].filter(Boolean).join(" ");
return {
id: userIdentity(sender.id),
identity: { subject: `user:${sender.id}` },
...sender.username && { username: sender.username },
...fullName && { fullName },
isBot: sender.is_bot
};
}
const senderChat = message.sender_chat;
if (!senderChat) return void 0;
const fullName = senderChat.title ?? [senderChat.first_name, senderChat.last_name].filter(Boolean).join(" ");
return {
id: channelIdentity(senderChat.id),
identity: { subject: `chat:${senderChat.id}` },
...senderChat.username && { username: senderChat.username },
...fullName && { fullName },
isBot: "unknown"
};
}
function chatLabel(chat) {
return `Telegram · ${(chat.title ?? (chat.username ? `@${chat.username}` : void 0) ?? [chat.first_name, chat.last_name].filter(Boolean).join(" ") ?? String(chat.id)) || chat.id}`;
}
function eventContext(message, eventId, botUserId) {
const actor = telegramActor(message);
const channelId = channelIdentity(message.chat.id);
return {
eventId,
thread: {
id: typeof message.message_thread_id === "number" ? `${channelId}:topic:${message.message_thread_id}` : channelId,
isDirectMessage: message.chat.type === "private" ? true : message.chat.type === "group" || message.chat.type === "supergroup" || message.chat.type === "channel" ? false : "unknown"
},
replySurface: {
version: 1,
address: {
chatId: String(message.chat.id),
...botUserId && { botUserId },
...typeof message.message_thread_id === "number" && { messageThreadId: message.message_thread_id },
replyToMessageId: message.message_id
},
label: chatLabel(message.chat)
},
...actor && { actor }
};
}
function telegramTimestamp(value) {
if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
const date = /* @__PURE__ */ new Date(value * 1e3);
return Number.isFinite(date.getTime()) ? date.toISOString() : void 0;
}
function normalizedMessage(message, eventId, edited, botUserId) {
const reply = asTelegramMessage(message.reply_to_message);
const sentAt = telegramTimestamp(message.date);
const editedAt = telegramTimestamp(message.edit_date);
return {
type: "message",
...eventContext(message, eventId, botUserId),
message: {
id: messageIdentity(message.chat.id, message.message_id),
text: message.text,
...typeof reply?.message_id === "number" && { reply: {
id: messageIdentity(message.chat.id, reply.message_id),
...typeof reply.text === "string" && { text: reply.text }
} },
...(sentAt || edited || editedAt) && { metadata: {
...sentAt && { sentAt },
...edited && { edited: true },
...editedAt && { editedAt }
} }
}
};
}
function approvalResponse(message, eventId, botUserId) {
if (botUserId === void 0) return void 0;
if (message.text !== "YES" && message.text !== "NO") return void 0;
const reply = asTelegramMessage(message.reply_to_message);
if (typeof reply?.message_id !== "number") return void 0;
const author = reply.from;
if (author?.is_bot !== true || author.id !== botUserId) return void 0;
const encodedInteractionId = typeof reply.text === "string" ? reply.text.match(INTERACTION_FOOTER)?.[1] : void 0;
const interactionId = encodedInteractionId ? decodeInteractionId(encodedInteractionId) : void 0;
if (!interactionId) return void 0;
return {
type: "approval-response",
...eventContext(message, eventId, botUserId),
interactionId,
decision: message.text === "YES" ? "approve" : "reject",
reference: messageIdentity(message.chat.id, message.message_id)
};
}
function inboundEvent(update, botUserId) {
const edited = update.edited_message !== void 0;
const message = asTelegramMessage(edited ? update.edited_message : update.message);
if (typeof update.update_id !== "number" || typeof message?.message_id !== "number" || typeof message.text !== "string" || typeof message.chat?.id !== "number") return;
const eventId = updateIdentity(message.chat.id, update.update_id);
return approvalResponse(message, eventId, botUserId) ?? normalizedMessage(message, eventId, edited, botUserId);
}
/** Recover this bot's numeric user id from its BotFather token prefix. */
function telegramBotUserId(botToken) {
const prefix = botToken.match(/^(\d+):/)?.[1];
if (!prefix) return void 0;
const botUserId = Number(prefix);
return Number.isSafeInteger(botUserId) && botUserId > 0 ? botUserId : void 0;
}
/** Create dependency-free Telegram webhook ingress for a destination chat. */
function telegramWebhook(options) {
if (!options.secretToken.trim()) throw new Error("secretToken is required to create Telegram webhook ingress");
if (options.botUserId !== void 0 && (!Number.isSafeInteger(options.botUserId) || options.botUserId <= 0)) throw new Error("botUserId must be a positive Telegram user id");
const botUserId = options.botUserId;
const path = options.path ?? DEFAULT_TELEGRAM_WEBHOOK_PATH;
return { async receive(request) {
if (!matchesPath(request, path)) return null;
if (request.method !== "POST") return emptyIngressResponse(405);
if (request.headers.get("x-telegram-bot-api-secret-token") !== options.secretToken) return emptyIngressResponse(401);
let update;
try {
update = await request.json();
} catch {
return emptyIngressResponse(400);
}
if (update === null || typeof update !== "object") return emptyIngressResponse(400);
const raw = update;
const event = inboundEvent(raw, botUserId);
return {
events: event ? [{
event,
raw
}] : [],
response: new Response(null, { status: 200 })
};
} };
}
function telegramSurface(surface) {
if (!isChannelMessageSurface(surface) || surface.version !== 1 || !isRecord(surface.address) || typeof surface.address.chatId !== "string" || surface.address.chatId.length === 0 || surface.address.botUserId !== void 0 && (!Number.isSafeInteger(surface.address.botUserId) || Number(surface.address.botUserId) <= 0)) return;
if (surface.address.messageThreadId !== void 0 && !Number.isSafeInteger(surface.address.messageThreadId) || surface.address.replyToMessageId !== void 0 && !Number.isSafeInteger(surface.address.replyToMessageId)) return;
return surface;
}
/**
* Whether a chat can show an animated draft.
*
* `sendMessageDraft` is private chats only, and Telegram gives private chats
* positive ids. Anywhere else the answer is simply sent once at the end.
*/
function supportsDraft(chatId) {
const numeric = Number(chatId);
return Number.isSafeInteger(numeric) && numeric > 0;
}
/** A non-zero draft identifier; reusing one animates between snapshots. */
function newDraftId() {
const [random] = crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(1));
return (random ?? 1) % 2147483647 + 1;
}
function splitText(text, limit) {
const pieces = [];
let piece = "";
let length = 0;
for (const character of text) {
if (length === limit) {
pieces.push(piece);
piece = "";
length = 0;
}
piece += character;
length += 1;
}
if (piece.length > 0) pieces.push(piece);
return pieces;
}
function textLength(text) {
return [...text].length;
}
/** Create a configured Telegram Bot API Channel. */
function telegram(options) {
if (!options.botToken.trim()) throw new Error("botToken is required to create a Telegram channel");
const fetch = options.fetch ?? globalThis.fetch;
const apiBaseUrl = (options.apiBaseUrl ?? "https://api.telegram.org").replace(/\/$/, "");
const maxLength = options.maxLength ?? TELEGRAM_MESSAGE_LIMIT;
if (!Number.isInteger(maxLength) || maxLength < 1 || maxLength > TELEGRAM_MESSAGE_LIMIT) throw new Error(`maxLength must be an integer between 1 and ${TELEGRAM_MESSAGE_LIMIT}`);
const toText = options.toText ?? defaultText;
const streamIntervalMs = options.streamIntervalMs ?? DEFAULT_STREAM_INTERVAL_MS;
if (!Number.isInteger(streamIntervalMs) || streamIntervalMs < 0) throw new Error("streamIntervalMs must be a non-negative integer");
const botUserId = telegramBotUserId(options.botToken);
const ingress = options.webhook ? telegramWebhook({
...options.webhook,
botUserId
}) : void 0;
async function send(destinationValue, text, parseMode) {
const destination = telegramSurface(destinationValue);
if (!destination || botUserId !== void 0 && destination.address.botUserId !== void 0 && destination.address.botUserId !== botUserId) return {
status: "failed",
retryable: false,
error: {
code: "TELEGRAM_SURFACE_INVALID",
message: `Telegram cannot parse the address for Channel "${destinationValue.channelKey}"`
}
};
if (textLength(text) > maxLength) return {
status: "failed",
retryable: false,
error: {
code: "TELEGRAM_MESSAGE_TOO_LONG",
message: `Telegram message exceeds the configured ${maxLength}-character limit`
}
};
let response;
try {
response = await fetch(`${apiBaseUrl}/bot${options.botToken}/sendMessage`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
chat_id: destination.address.chatId,
text,
...destination.address.messageThreadId !== void 0 && { message_thread_id: destination.address.messageThreadId },
...destination.address.replyToMessageId !== void 0 && { reply_parameters: { message_id: destination.address.replyToMessageId } },
...parseMode && { parse_mode: parseMode }
})
});
} catch {
return uncertain("TELEGRAM_DELIVERY_ERROR", "Telegram delivery failed with an unknown outcome");
}
let payload;
try {
payload = await response.json();
} catch {
return uncertain("TELEGRAM_DELIVERY_ERROR", "Telegram returned an invalid delivery response");
}
const apiResponse = asApiResponse(payload);
return apiResponse ? classifyResponse(response, apiResponse) : uncertain("TELEGRAM_DELIVERY_ERROR", "Telegram returned an invalid delivery response");
}
/**
* Show one ephemeral preview. Failures are swallowed on purpose: a draft is
* a 30-second animation, and losing one must never cost the real message.
*
* The configured parse mode is deliberately not applied. A partial answer
* routinely holds unbalanced markup, which Telegram rejects outright.
*/
async function sendDraft(destination, draftId, text) {
try {
return asApiResponse(await (await fetch(`${apiBaseUrl}/bot${options.botToken}/sendMessageDraft`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
chat_id: Number(destination.address.chatId),
draft_id: draftId,
text,
...destination.address.messageThreadId !== void 0 && { message_thread_id: destination.address.messageThreadId }
})
})).json())?.ok === true;
} catch {
return false;
}
}
/** Persist the answer, splitting it when it outgrows one Telegram message. */
async function sendComplete(destination, text) {
const [head, ...tail] = splitText(text, maxLength);
const parseMode = tail.length === 0 ? options.parseMode : void 0;
const first = await send(destination, head, parseMode);
if (first.status !== "delivered") return first;
for (const piece of tail) if ((await send(destination, piece, parseMode)).status !== "delivered") return uncertain("TELEGRAM_STREAM_PARTIAL", "Telegram accepted only part of a split answer", first.reference);
return first;
}
async function streamMessage(destinationValue, chunks, streamOptions) {
const destination = telegramSurface(destinationValue);
if (!destination || botUserId !== void 0 && destination.address.botUserId !== void 0 && destination.address.botUserId !== botUserId) {
await chunks.cancel().catch(() => {});
return {
status: "failed",
retryable: false,
error: {
code: "TELEGRAM_SURFACE_INVALID",
message: `Telegram cannot parse the address for Channel "${destinationValue.channelKey}"`
}
};
}
const draftId = supportsDraft(destination.address.chatId) ? newDraftId() : void 0;
const prefix = streamOptions.title ? `${streamOptions.title}\n\n` : "";
const shouldPreview = createPacer(streamIntervalMs);
let answer = "";
let draftsStopped = draftId === void 0;
return consumeChunks(chunks, {
async onChunk(chunk) {
if (chunk.type !== "text" || chunk.text.length === 0) return;
answer += chunk.text;
if (draftsStopped || !shouldPreview()) return;
if (!await sendDraft(destination, draftId, splitText(`${prefix}${answer}`, maxLength)[0] ?? "")) draftsStopped = true;
},
async onFinish(outcome) {
if (answer.length === 0) return {
status: "failed",
retryable: false,
error: outcome.interrupted ? {
code: "TELEGRAM_STREAM_INTERRUPTED",
message: "The answer ended before producing any text to send"
} : {
code: "TELEGRAM_STREAM_EMPTY",
message: "The stream carried no text to send"
}
};
const result = await sendComplete(destination, `${prefix}${answer}`);
if (!outcome.interrupted || result.status !== "delivered") return result;
return uncertain("TELEGRAM_STREAM_INTERRUPTED", "An incomplete answer was sent because the stream ended early", result.reference);
}
});
}
return {
...options.route && { route: options.route },
...ingress && { ingress },
contactSurface(identity) {
if ((identity.scope ?? "default") !== "default") return null;
const match = identity.subject.match(/^(user|chat):(-?\d+)$/);
if (!match) return null;
return {
version: 1,
address: {
chatId: match[2],
...botUserId && { botUserId }
},
label: `Telegram · ${match[1]} ${match[2]}`
};
},
deliver(destination, message) {
return send(destination, toText(message), options.parseMode);
},
stream(destination, chunks, streamOptions) {
return streamMessage(destination, chunks, streamOptions);
},
requestApproval(destination, { interactionId, request }) {
if (interactionId.length === 0) return Promise.resolve({
status: "failed",
retryable: false,
error: {
code: "TELEGRAM_INTERACTION_ID_REQUIRED",
message: "Telegram approval requests require a non-empty interaction id"
}
});
return send(destination, approvalText(interactionId, request));
}
};
}
//#endregion
export { telegram, telegramWebhook };
//# sourceMappingURL=telegram.js.map