EXTENSION for treeos-connect
gateway-telegram
Telegram channel type for the gateway extension. Registers the telegram channel type at boot, enabling trees to communicate through Telegram bots. A channel requires a bot token (from BotFather) and a chat ID (the Telegram group or direct message to connect). The bot token is stored encrypted in the channel's secrets. The chat ID stays in plaintext metadata since it is not sensitive. Three directions are supported: input (messages from Telegram flow into the tree), output (the tree pushes notifications to Telegram), and input-output (full bidirectional conversation with the tree's AI through Telegram). Output messages are sent as Markdown-formatted text via the Telegram Bot API's sendMessage endpoint, with titles bolded. Messages exceeding Telegram's 4096 character limit are automatically truncated. For input channels, the extension registers a Telegram webhook by calling the setWebhook API with a unique per-channel URL and a cryptographic secret token for request verification. The webhook endpoint at POST /api/v1/gateway/telegram/:channelId responds with 200 immediately to prevent Telegram retries, then processes the message asynchronously. Inbound messages are verified against both the secret token header (X-Telegram-Bot-Api-Secret-Token) and the expected chat ID, rejecting messages from unregistered chats. The sender's username and platform ID are extracted and passed to the gateway core's processGatewayMessage function. When a channel is disabled or deleted, unregisterInput calls deleteWebhook to clean up the Telegram-side registration.
v1.0.1 by TreeOS Site 0 downloads 4 files 318 lines 10.6 KB published 38d ago
treeos ext install gateway-telegram
View changelog

Manifest

Provides

  • routes

Requires

  • models: Node, User
  • extensions: gateway
SHA256: 2a3c9fb836cabdb0aae1ec343f69ad240b58ec238c05302b7425e223305c9a7e

Dependents

1 package depend on this

PackageTypeRelationship
treeos-connect v1.0.3bundleincludes

Source Code

1// 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};
156
1import 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}
20
1export 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};
50
1// 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

Versions

Version Published Downloads
1.0.1 38d ago 0
1.0.0 48d ago 0
0 stars
0 flags
React from the CLI: treeos ext star gateway-telegram

Comments

Loading comments...

Post comments from the CLI: treeos ext comment gateway-telegram "your comment"
Max 3 comments per extension. One star and one flag per user.