2a3c9fb836cabdb0aae1ec343f69ad240b58ec238c05302b7425e223305c9a7e1// Telegram channel type handler.
2// Registered with gateway core during init.
3
4import log from "../../seed/log.js";
5import crypto from "crypto";
6import { getLandUrl } from "../../canopy/identity.js";
7
8// ─────────────────────────────────────────────────────────────────────────
9// VALIDATION
10// ─────────────────────────────────────────────────────────────────────────
11
12function validateConfig(config, direction) {
13 // Telegram always needs botToken + chatId (same bot sends and receives)
14 if (!config.botToken || typeof config.botToken !== "string") {
15 throw new Error("Telegram channel requires a botToken");
16 }
17 if (!config.chatId || typeof config.chatId !== "string") {
18 throw new Error("Telegram channel requires a chatId");
19 }
20}
21
22function buildEncryptedConfig(config, direction) {
23 return {
24 secrets: { botToken: config.botToken },
25 metadata: { chatId: config.chatId },
26 displayIdentifier: config.displayIdentifier || null,
27 };
28}
29
30// ─────────────────────────────────────────────────────────────────────────
31// SENDER
32// ─────────────────────────────────────────────────────────────────────────
33
34async function send(secrets, metadata, notification) {
35 const { botToken } = secrets;
36 const chatId = metadata.chatId;
37
38 const text = `*${notification.title}*\n\n${notification.content}`;
39
40 const res = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
41 method: "POST",
42 headers: { "Content-Type": "application/json" },
43 body: JSON.stringify({
44 chat_id: chatId,
45 text,
46 parse_mode: "Markdown",
47 }),
48 });
49
50 if (!res.ok) {
51 const body = await res.text();
52 throw new Error(`Telegram API error ${res.status}: ${body}`);
53 }
54}
55
56// ─────────────────────────────────────────────────────────────────────────
57// INPUT LIFECYCLE
58// ─────────────────────────────────────────────────────────────────────────
59
60async function registerInput(channel, secrets) {
61 const secretToken = crypto.randomBytes(32).toString("hex");
62 const webhookUrl = `${process.env.BASE_URL || getLandUrl()}/api/v1/gateway/telegram/${channel._id}`;
63
64 const res = await fetch(
65 `https://api.telegram.org/bot${secrets.botToken}/setWebhook`,
66 {
67 method: "POST",
68 headers: { "Content-Type": "application/json" },
69 body: JSON.stringify({
70 url: webhookUrl,
71 secret_token: secretToken,
72 allowed_updates: ["message"],
73 }),
74 },
75 );
76
77 if (!res.ok) {
78 const body = await res.text();
79 throw new Error("Telegram setWebhook failed: " + body);
80 }
81
82 // Store secret token in metadata for webhook verification
83 const { getExtension } = await import("../loader.js");
84 const GatewayChannel = getExtension("gateway")?.exports?.GatewayChannel;
85 await GatewayChannel.findByIdAndUpdate(channel._id, {
86 $set: { "config.metadata.webhookSecret": secretToken },
87 });
88
89 log.verbose("GatewayTelegram", `Telegram webhook registered for channel ${channel._id}`);
90}
91
92async function unregisterInput(channel, secrets) {
93 try {
94 await fetch(
95 `https://api.telegram.org/bot${secrets.botToken}/deleteWebhook`,
96 { method: "POST" },
97 );
98 log.verbose("GatewayTelegram", `Telegram webhook removed for channel ${channel._id}`);
99 } catch (err) {
100 log.error("GatewayTelegram", `Failed to remove Telegram webhook for ${channel._id}:`, err.message);
101 }
102}
103
104// ─────────────────────────────────────────────────────────────────────────
105// REPLY HELPER (used by webhook receiver)
106// ─────────────────────────────────────────────────────────────────────────
107
108export async function sendTelegramReply(channel, chatId, text) {
109 try {
110 const { getExtension } = await import("../loader.js");
111 const getChannelWithSecrets = getExtension("gateway")?.exports?.getChannelWithSecrets;
112 const fullChannel = await getChannelWithSecrets(channel._id);
113 if (!fullChannel?.config?.decryptedSecrets?.botToken) return;
114
115 const botToken = fullChannel.config.decryptedSecrets.botToken;
116
117 // Truncate if too long for Telegram (4096 char limit)
118 if (text.length > 4096) {
119 text = text.slice(0, 4093) + "...";
120 }
121
122 const res = await fetch(
123 `https://api.telegram.org/bot${botToken}/sendMessage`,
124 {
125 method: "POST",
126 headers: { "Content-Type": "application/json" },
127 body: JSON.stringify({
128 chat_id: chatId,
129 text,
130 parse_mode: "Markdown",
131 }),
132 },
133 );
134
135 if (!res.ok) {
136 const body = await res.text();
137 log.error("GatewayTelegram", `Telegram reply failed for channel ${channel._id}: ${body}`);
138 }
139 } catch (err) {
140 log.error("GatewayTelegram", `Telegram reply error for channel ${channel._id}:`, err.message);
141 }
142}
143
144// ─────────────────────────────────────────────────────────────────────────
145// EXPORT HANDLER
146// ─────────────────────────────────────────────────────────────────────────
147
148export default {
149 allowedDirections: ["input", "output", "input-output"],
150 validateConfig,
151 buildEncryptedConfig,
152 send,
153 registerInput,
154 unregisterInput,
155};
1561import log from "../../seed/log.js";
2import handler from "./handler.js";
3import { getExtension } from "../loader.js";
4
5export async function init(core) {
6 const gateway = getExtension("gateway");
7 if (!gateway?.exports?.registerChannelType) {
8 throw new Error("gateway-telegram requires the gateway extension to be loaded first");
9 }
10
11 gateway.exports.registerChannelType("telegram", handler);
12 log.verbose("GatewayTelegram", "Registered telegram channel type");
13
14 const { default: router } = await import("./routes.js");
15
16 return {
17 router,
18 };
19}
201export default {
2 name: "gateway-telegram",
3 version: "1.0.1",
4 builtFor: "treeos-connect",
5 description:
6 "Telegram channel type for the gateway extension. Registers the telegram channel type at boot, " +
7 "enabling trees to communicate through Telegram bots. A channel requires a bot token (from " +
8 "BotFather) and a chat ID (the Telegram group or direct message to connect). The bot token is " +
9 "stored encrypted in the channel's secrets. The chat ID stays in plaintext metadata since it " +
10 "is not sensitive.\n\n" +
11 "Three directions are supported: input (messages from Telegram flow into the tree), output " +
12 "(the tree pushes notifications to Telegram), and input-output (full bidirectional conversation " +
13 "with the tree's AI through Telegram). Output messages are sent as Markdown-formatted text via " +
14 "the Telegram Bot API's sendMessage endpoint, with titles bolded. Messages exceeding Telegram's " +
15 "4096 character limit are automatically truncated.\n\n" +
16 "For input channels, the extension registers a Telegram webhook by calling the setWebhook API " +
17 "with a unique per-channel URL and a cryptographic secret token for request verification. The " +
18 "webhook endpoint at POST /api/v1/gateway/telegram/:channelId responds with 200 immediately to " +
19 "prevent Telegram retries, then processes the message asynchronously. Inbound messages are " +
20 "verified against both the secret token header (X-Telegram-Bot-Api-Secret-Token) and the " +
21 "expected chat ID, rejecting messages from unregistered chats. The sender's username and " +
22 "platform ID are extracted and passed to the gateway core's processGatewayMessage function. " +
23 "When a channel is disabled or deleted, unregisterInput calls deleteWebhook to clean up the " +
24 "Telegram-side registration.",
25
26 needs: {
27 models: ["Node", "User"],
28 extensions: ["gateway"],
29 },
30
31 optional: {},
32
33 provides: {
34 models: {},
35 routes: "./routes.js",
36 tools: false,
37 jobs: false,
38 orchestrator: false,
39 energyActions: {},
40 sessionTypes: {},
41 env: [],
42 cli: [],
43
44 hooks: {
45 fires: [],
46 listens: [],
47 },
48 },
49};
501// Telegram webhook receiver endpoint.
2// No auth middleware. Telegram calls this directly.
3
4import log from "../../seed/log.js";
5import { sendOk } from "../../seed/protocol.js";
6import express from "express";
7import { sendTelegramReply } from "./handler.js";
8
9const router = express.Router();
10
11// POST /api/v1/gateway/telegram/:channelId
12router.post("/gateway/telegram/:channelId", async (req, res) => {
13 // Always respond 200 to Telegram immediately (prevents retries)
14 sendOk(res, { ok: true });
15
16 try {
17 const channelId = req.params.channelId;
18 const update = req.body;
19
20 // Basic structure check
21 if (!update || !update.message || !update.message.text) return;
22
23 // Load channel
24 const { getExtension } = await import("../loader.js");
25 const GatewayChannel = getExtension("gateway")?.exports?.GatewayChannel;
26 const channel = await GatewayChannel.findById(channelId).lean();
27 if (!channel || !channel.enabled) return;
28 if (channel.type !== "telegram") return;
29
30 const hasInput =
31 channel.direction === "input" || channel.direction === "input-output";
32 if (!hasInput) return;
33
34 // Verify secret token header (if configured)
35 const expectedSecret = channel.config?.metadata?.webhookSecret;
36 if (expectedSecret) {
37 const headerSecret = req.headers["x-telegram-bot-api-secret-token"];
38 if (headerSecret !== expectedSecret) {
39 log.error("GatewayTelegram",
40 `Telegram webhook secret mismatch for channel ${channelId}`,
41 );
42 return;
43 }
44 }
45
46 // Verify chatId matches
47 const expectedChatId = channel.config?.metadata?.chatId;
48 const actualChatId = String(update.message.chat.id);
49 if (expectedChatId && actualChatId !== expectedChatId) {
50 log.error("GatewayTelegram",
51 `Telegram chatId mismatch for channel ${channelId}: expected ${expectedChatId}, got ${actualChatId}`,
52 );
53 return;
54 }
55
56 // Extract sender info
57 const from = update.message.from || {};
58 const senderName = from.username || from.first_name || "Unknown";
59 const senderPlatformId = String(from.id || "");
60 const messageText = update.message.text;
61
62 log.verbose("GatewayTelegram",
63 `Telegram message on channel ${channelId} from ${senderName}: "${messageText.slice(0, 80)}"`,
64 );
65
66 // Process the message via gateway core
67 const gateway = getExtension("gateway");
68 if (!gateway?.exports?.processGatewayMessage) {
69 log.error("GatewayTelegram", "Gateway core not loaded");
70 return;
71 }
72
73 const result = await gateway.exports.processGatewayMessage(channelId, {
74 senderName,
75 senderPlatformId,
76 messageText,
77 });
78
79 // Send reply back if input-output and there's a reply
80 if (result.reply && channel.direction === "input-output") {
81 await sendTelegramReply(channel, actualChatId, result.reply);
82 }
83 } catch (err) {
84 log.error("GatewayTelegram",
85 `Telegram webhook error for channel ${req.params.channelId}:`,
86 err.message,
87 );
88 }
89});
90
91export default router;
92
| Version | Published | Downloads |
|---|---|---|
| 1.0.1 | 38d ago | 0 |
| 1.0.0 | 48d ago | 0 |
treeos ext star gateway-telegram
Post comments from the CLI: treeos ext comment gateway-telegram "your comment"
Max 3 comments per extension. One star and one flag per user.
Loading comments...