@mastra/telegram 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/LICENSE.md +30 -0
- package/dist/index.cjs +19508 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +421 -0
- package/dist/index.js +680 -0
- package/dist/index.js.map +1 -0
- package/package.json +12 -12
package/dist/index.js
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
// src/telegram-provider.ts
|
|
2
|
+
import { randomUUID, timingSafeEqual } from "crypto";
|
|
3
|
+
import { AgentChannels, resolveWaitUntil } from "@mastra/core/channels";
|
|
4
|
+
import { InMemoryChannelsStorage } from "@mastra/core/storage";
|
|
5
|
+
import { createTelegramAdapter } from "@chat-adapter/telegram";
|
|
6
|
+
|
|
7
|
+
// src/telegram-client.ts
|
|
8
|
+
import { randomBytes } from "crypto";
|
|
9
|
+
|
|
10
|
+
// src/types.ts
|
|
11
|
+
var TELEGRAM_API_BASE_URL = "https://api.telegram.org";
|
|
12
|
+
var DEFAULT_ALLOWED_UPDATES = [
|
|
13
|
+
"message",
|
|
14
|
+
"edited_message",
|
|
15
|
+
"channel_post",
|
|
16
|
+
"edited_channel_post",
|
|
17
|
+
"callback_query",
|
|
18
|
+
"message_reaction"
|
|
19
|
+
];
|
|
20
|
+
var BOTFATHER_DEEP_LINK = "https://t.me/botfather";
|
|
21
|
+
|
|
22
|
+
// src/telegram-client.ts
|
|
23
|
+
async function botApiRequest(botToken, method, apiBaseUrl, payload) {
|
|
24
|
+
const init = payload === void 0 ? void 0 : { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) };
|
|
25
|
+
let response;
|
|
26
|
+
try {
|
|
27
|
+
response = await fetch(`${apiBaseUrl}/bot${botToken}/${method}`, {
|
|
28
|
+
...init,
|
|
29
|
+
signal: AbortSignal.timeout(1e4)
|
|
30
|
+
});
|
|
31
|
+
} catch (cause) {
|
|
32
|
+
throw Object.assign(new Error(`Telegram ${method} request failed`, { cause }), {
|
|
33
|
+
isTransportError: true
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const body = await response.json().catch(() => null);
|
|
37
|
+
if (!response.ok || !body?.ok) {
|
|
38
|
+
const detail = body?.description ?? `HTTP ${response.status}`;
|
|
39
|
+
throw new Error(`Telegram ${method} failed: ${detail}`);
|
|
40
|
+
}
|
|
41
|
+
return body.result;
|
|
42
|
+
}
|
|
43
|
+
async function getMe(botToken, apiBaseUrl = TELEGRAM_API_BASE_URL) {
|
|
44
|
+
let result;
|
|
45
|
+
try {
|
|
46
|
+
result = await botApiRequest(botToken, "getMe", apiBaseUrl);
|
|
47
|
+
} catch (cause) {
|
|
48
|
+
if (cause instanceof Error && cause.isTransportError) {
|
|
49
|
+
throw cause;
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`Telegram rejected the bot token: ${cause instanceof Error ? cause.message : String(cause)}`, {
|
|
52
|
+
cause
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (!result?.is_bot) {
|
|
56
|
+
throw new Error("Telegram getMe returned a non-bot user; expected a BotFather token");
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
async function setWebhook(botToken, options, apiBaseUrl = TELEGRAM_API_BASE_URL) {
|
|
61
|
+
await botApiRequest(botToken, "setWebhook", apiBaseUrl, {
|
|
62
|
+
url: options.url,
|
|
63
|
+
secret_token: options.secretToken,
|
|
64
|
+
allowed_updates: options.allowedUpdates,
|
|
65
|
+
drop_pending_updates: options.dropPendingUpdates
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async function deleteWebhook(botToken, dropPendingUpdates = false, apiBaseUrl = TELEGRAM_API_BASE_URL) {
|
|
69
|
+
await botApiRequest(botToken, "deleteWebhook", apiBaseUrl, {
|
|
70
|
+
drop_pending_updates: dropPendingUpdates
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async function setMyCommands(botToken, options, apiBaseUrl = TELEGRAM_API_BASE_URL) {
|
|
74
|
+
await botApiRequest(botToken, "setMyCommands", apiBaseUrl, {
|
|
75
|
+
commands: options.commands,
|
|
76
|
+
scope: options.scope,
|
|
77
|
+
language_code: options.languageCode
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function generateSecretToken() {
|
|
81
|
+
return randomBytes(32).toString("base64url");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/commands.ts
|
|
85
|
+
var DEFAULT_COMMANDS = [
|
|
86
|
+
{ command: "start", description: "Start a conversation" },
|
|
87
|
+
{ command: "help", description: "Show what this bot can do" },
|
|
88
|
+
{ command: "settings", description: "Manage your preferences" }
|
|
89
|
+
];
|
|
90
|
+
function normalizeCommands(raw) {
|
|
91
|
+
if (!raw) return [];
|
|
92
|
+
const seen = /* @__PURE__ */ new Set();
|
|
93
|
+
const commands = [];
|
|
94
|
+
for (const item of raw) {
|
|
95
|
+
const input = typeof item === "string" ? { command: item } : item;
|
|
96
|
+
const command = input.command.replace(/^\//, "").toLowerCase().replace(/[^a-z0-9_]/g, "").slice(0, 32);
|
|
97
|
+
if (!command || seen.has(command)) continue;
|
|
98
|
+
seen.add(command);
|
|
99
|
+
const description = (input.description?.trim() || `Run /${command}`).slice(0, 256);
|
|
100
|
+
commands.push({ command, description });
|
|
101
|
+
}
|
|
102
|
+
return commands;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/crypto.ts
|
|
106
|
+
import { createCipheriv, createDecipheriv, hkdfSync, randomBytes as randomBytes2 } from "crypto";
|
|
107
|
+
var ALGO_PREFIX = "aes-256-gcm-hkdf";
|
|
108
|
+
var HKDF_INFO = "mastra-telegram-encryption";
|
|
109
|
+
function deriveKey(passphrase, salt) {
|
|
110
|
+
return Buffer.from(hkdfSync("sha256", passphrase, salt, HKDF_INFO, 32));
|
|
111
|
+
}
|
|
112
|
+
function isEncrypted(value) {
|
|
113
|
+
return value.startsWith(`${ALGO_PREFIX}:`);
|
|
114
|
+
}
|
|
115
|
+
function encrypt(plaintext, passphrase) {
|
|
116
|
+
const salt = randomBytes2(16);
|
|
117
|
+
const iv = randomBytes2(12);
|
|
118
|
+
const cipher = createCipheriv("aes-256-gcm", deriveKey(passphrase, salt), iv);
|
|
119
|
+
const enc = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
120
|
+
const tag = cipher.getAuthTag();
|
|
121
|
+
return `${ALGO_PREFIX}:${salt.toString("base64")}:${iv.toString("base64")}:${tag.toString("base64")}:${enc.toString("base64")}`;
|
|
122
|
+
}
|
|
123
|
+
function decrypt(value, passphrase) {
|
|
124
|
+
if (!isEncrypted(value)) return value;
|
|
125
|
+
const [, saltB64, ivB64, tagB64, ctB64] = value.split(":");
|
|
126
|
+
if (!saltB64 || !ivB64 || !tagB64 || ctB64 === void 0) {
|
|
127
|
+
throw new Error("Invalid ciphertext payload");
|
|
128
|
+
}
|
|
129
|
+
const decipher = createDecipheriv(
|
|
130
|
+
"aes-256-gcm",
|
|
131
|
+
deriveKey(passphrase, Buffer.from(saltB64, "base64")),
|
|
132
|
+
Buffer.from(ivB64, "base64")
|
|
133
|
+
);
|
|
134
|
+
decipher.setAuthTag(Buffer.from(tagB64, "base64"));
|
|
135
|
+
return Buffer.concat([decipher.update(Buffer.from(ctB64, "base64")), decipher.final()]).toString("utf8");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/install-store.ts
|
|
139
|
+
var PLATFORM = "telegram";
|
|
140
|
+
var TelegramInstallStore = class {
|
|
141
|
+
constructor(storage, encryptionKey) {
|
|
142
|
+
this.storage = storage;
|
|
143
|
+
this.encryptionKey = encryptionKey;
|
|
144
|
+
}
|
|
145
|
+
storage;
|
|
146
|
+
encryptionKey;
|
|
147
|
+
/** The active or pending installation for an agent, if any. */
|
|
148
|
+
async getByAgent(agentId) {
|
|
149
|
+
const record = await this.storage.getInstallationByAgent(PLATFORM, agentId);
|
|
150
|
+
return record ? this.#fromRecord(record) : null;
|
|
151
|
+
}
|
|
152
|
+
/** Look up an installation by the routing id in its webhook path (M1 dispatch). */
|
|
153
|
+
async getByWebhookId(webhookId) {
|
|
154
|
+
const record = await this.storage.getInstallationByWebhookId(webhookId);
|
|
155
|
+
return record && record.platform === PLATFORM ? this.#fromRecord(record) : null;
|
|
156
|
+
}
|
|
157
|
+
/** Insert or replace an installation. */
|
|
158
|
+
async save(installation) {
|
|
159
|
+
await this.storage.saveInstallation(this.#toRecord(installation));
|
|
160
|
+
}
|
|
161
|
+
/** All Telegram installations (active and pending). */
|
|
162
|
+
async list() {
|
|
163
|
+
const records = await this.storage.listInstallations(PLATFORM);
|
|
164
|
+
return records.map((r) => this.#fromRecord(r));
|
|
165
|
+
}
|
|
166
|
+
/** Remove an agent's installation, if present. */
|
|
167
|
+
async deleteByAgent(agentId) {
|
|
168
|
+
const record = await this.storage.getInstallationByAgent(PLATFORM, agentId);
|
|
169
|
+
if (record) await this.storage.deleteInstallation(record.id);
|
|
170
|
+
}
|
|
171
|
+
#enc(value) {
|
|
172
|
+
return value && this.encryptionKey ? encrypt(value, this.encryptionKey) : value;
|
|
173
|
+
}
|
|
174
|
+
#dec(value) {
|
|
175
|
+
if (!value) return value;
|
|
176
|
+
if (!this.encryptionKey) {
|
|
177
|
+
if (isEncrypted(value)) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
"Telegram installation secrets are encrypted at rest, but no encryption key is configured. Set `encryptionKey` on TelegramProvider or MASTRA_ENCRYPTION_KEY."
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
return decrypt(value, this.encryptionKey);
|
|
185
|
+
}
|
|
186
|
+
#toRecord(install) {
|
|
187
|
+
const data = {
|
|
188
|
+
botToken: this.#enc(install.botToken),
|
|
189
|
+
secretToken: this.#enc(install.secretToken),
|
|
190
|
+
username: install.username,
|
|
191
|
+
webhookUrl: install.webhookUrl,
|
|
192
|
+
commands: install.commands
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
id: install.id,
|
|
196
|
+
platform: PLATFORM,
|
|
197
|
+
agentId: install.agentId,
|
|
198
|
+
status: install.status,
|
|
199
|
+
webhookId: install.webhookId,
|
|
200
|
+
data,
|
|
201
|
+
createdAt: install.installedAt,
|
|
202
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
#fromRecord(record) {
|
|
206
|
+
const data = record.data ?? {};
|
|
207
|
+
return {
|
|
208
|
+
id: record.id,
|
|
209
|
+
agentId: record.agentId,
|
|
210
|
+
webhookId: record.webhookId ?? "",
|
|
211
|
+
status: record.status === "active" ? "active" : "pending",
|
|
212
|
+
botToken: this.#dec(data.botToken),
|
|
213
|
+
secretToken: this.#dec(data.secretToken),
|
|
214
|
+
username: data.username,
|
|
215
|
+
webhookUrl: data.webhookUrl,
|
|
216
|
+
commands: data.commands,
|
|
217
|
+
installedAt: record.createdAt
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
function toInstallationInfo(install) {
|
|
222
|
+
return {
|
|
223
|
+
id: install.id,
|
|
224
|
+
platform: PLATFORM,
|
|
225
|
+
agentId: install.agentId,
|
|
226
|
+
status: install.status,
|
|
227
|
+
displayName: install.username,
|
|
228
|
+
installedAt: install.installedAt
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/telegram-provider.ts
|
|
233
|
+
function resolveTelegramAdapterConfig(config) {
|
|
234
|
+
return {
|
|
235
|
+
streaming: config.streaming ?? true,
|
|
236
|
+
typingStatus: config.typingStatus ?? true
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
var SECRET_HEADER = "x-telegram-bot-api-secret-token";
|
|
240
|
+
var TelegramProvider = class {
|
|
241
|
+
id = PLATFORM;
|
|
242
|
+
#config;
|
|
243
|
+
#mastra;
|
|
244
|
+
#store;
|
|
245
|
+
/** Live adapters, keyed by installation id. */
|
|
246
|
+
#adapters = /* @__PURE__ */ new Map();
|
|
247
|
+
/** Cached sync view of whether any active bot is registered (for {@link getInfo}). */
|
|
248
|
+
#configured = false;
|
|
249
|
+
#initPromise = null;
|
|
250
|
+
constructor(config = {}) {
|
|
251
|
+
this.#config = config;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Called by Mastra when this channel is registered.
|
|
255
|
+
* @internal
|
|
256
|
+
*/
|
|
257
|
+
__attach(mastra) {
|
|
258
|
+
if (this.#mastra && this.#mastra !== mastra) {
|
|
259
|
+
this.#initPromise = null;
|
|
260
|
+
this.#store = void 0;
|
|
261
|
+
this.#adapters.clear();
|
|
262
|
+
this.#configured = false;
|
|
263
|
+
}
|
|
264
|
+
this.#mastra = mastra;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Per-bot webhook route. A single POST endpoint keyed by an opaque
|
|
268
|
+
* `webhookId`; the per-bot secret is verified from the request header, never
|
|
269
|
+
* carried in the URL. Auto-initializes on first hit (mirrors `@mastra/slack`).
|
|
270
|
+
*/
|
|
271
|
+
getRoutes() {
|
|
272
|
+
const self = this;
|
|
273
|
+
const withInit = (handler) => {
|
|
274
|
+
return async ({ mastra }) => {
|
|
275
|
+
self.#mastra = mastra;
|
|
276
|
+
await self.#autoInitialize();
|
|
277
|
+
return handler.bind(self);
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
return [
|
|
281
|
+
{
|
|
282
|
+
path: `/${PLATFORM}/events/:webhookId`,
|
|
283
|
+
method: "POST",
|
|
284
|
+
requiresAuth: false,
|
|
285
|
+
createHandler: withInit(this.#handleWebhook)
|
|
286
|
+
}
|
|
287
|
+
];
|
|
288
|
+
}
|
|
289
|
+
/** Discovery metadata for the editor UI. */
|
|
290
|
+
getInfo() {
|
|
291
|
+
return {
|
|
292
|
+
id: this.id,
|
|
293
|
+
name: "Telegram",
|
|
294
|
+
isConfigured: this.#configured,
|
|
295
|
+
connectOptionsSchema: {
|
|
296
|
+
type: "object",
|
|
297
|
+
properties: {
|
|
298
|
+
botToken: {
|
|
299
|
+
type: "string",
|
|
300
|
+
description: "BotFather bot token. Omit to receive a BotFather deep link instead."
|
|
301
|
+
},
|
|
302
|
+
name: {
|
|
303
|
+
type: "string",
|
|
304
|
+
description: "Display name for the bot (defaults to the bot's @username)."
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Restore installations from storage: rebuild an adapter per active bot and
|
|
312
|
+
* inject `AgentChannels` so the agent can receive events immediately.
|
|
313
|
+
* Idempotent. Does not re-register webhooks (they persist server-side across
|
|
314
|
+
* restarts); reconnect an agent if its `baseUrl` changed.
|
|
315
|
+
*/
|
|
316
|
+
async initialize() {
|
|
317
|
+
if (this.#initPromise) return this.#initPromise;
|
|
318
|
+
this.#initPromise = this.#doInitialize();
|
|
319
|
+
try {
|
|
320
|
+
await this.#initPromise;
|
|
321
|
+
} catch (err) {
|
|
322
|
+
this.#initPromise = null;
|
|
323
|
+
throw err;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async #doInitialize() {
|
|
327
|
+
const store = await this.#getStore();
|
|
328
|
+
const active = (await store.list()).filter((i) => i.status === "active");
|
|
329
|
+
this.#configured = active.length > 0;
|
|
330
|
+
for (const installation of active) {
|
|
331
|
+
try {
|
|
332
|
+
await this.#activateInstallation(installation);
|
|
333
|
+
} catch (err) {
|
|
334
|
+
console.error(`[Telegram] Failed to restore installation "${installation.id}":`, err);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Update runtime provider settings. Telegram has no global auth credential to
|
|
340
|
+
* clear (per-bot tokens are managed via {@link connect}/{@link disconnect}),
|
|
341
|
+
* so `null` is a no-op; an object merges `apiBaseUrl`/`baseUrl` overrides.
|
|
342
|
+
*/
|
|
343
|
+
async configure(credentials) {
|
|
344
|
+
if (credentials === null) return;
|
|
345
|
+
const apiBaseUrlChanged = credentials.apiBaseUrl !== void 0 && credentials.apiBaseUrl !== this.#config.apiBaseUrl;
|
|
346
|
+
this.#config = { ...this.#config, ...credentials };
|
|
347
|
+
if (!apiBaseUrlChanged) return;
|
|
348
|
+
const wasInitialized = this.#initPromise !== null;
|
|
349
|
+
for (const adapter of this.#adapters.values()) {
|
|
350
|
+
try {
|
|
351
|
+
await adapter.stopPolling();
|
|
352
|
+
} catch (err) {
|
|
353
|
+
console.warn("[Telegram] Failed to stop polling while reconfiguring:", err);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
this.#adapters.clear();
|
|
357
|
+
this.#initPromise = null;
|
|
358
|
+
if (wasInitialized) await this.initialize();
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Connect an agent to a Telegram bot.
|
|
362
|
+
*
|
|
363
|
+
* - With `options.botToken`: validate via `getMe`, mint a per-bot webhook
|
|
364
|
+
* secret, persist the installation, register the transport (webhook or
|
|
365
|
+
* polling), and return `{ type: 'immediate' }`.
|
|
366
|
+
* - Without a token: persist a pending installation and return
|
|
367
|
+
* `{ type: 'deep_link' }` pointing at BotFather.
|
|
368
|
+
*/
|
|
369
|
+
async connect(agentId, options = {}) {
|
|
370
|
+
const store = await this.#getStore();
|
|
371
|
+
const existing = await store.getByAgent(agentId);
|
|
372
|
+
if (existing?.status === "active") {
|
|
373
|
+
throw new Error(`Agent "${agentId}" is already connected to Telegram. Disconnect first to reconnect.`);
|
|
374
|
+
}
|
|
375
|
+
if (!options.botToken) {
|
|
376
|
+
const installationId2 = existing?.id ?? randomUUID();
|
|
377
|
+
await store.save({
|
|
378
|
+
id: installationId2,
|
|
379
|
+
agentId,
|
|
380
|
+
webhookId: existing?.webhookId ?? randomUUID(),
|
|
381
|
+
status: "pending",
|
|
382
|
+
installedAt: existing?.installedAt ?? /* @__PURE__ */ new Date()
|
|
383
|
+
});
|
|
384
|
+
return { type: "deep_link", url: BOTFATHER_DEEP_LINK, installationId: installationId2 };
|
|
385
|
+
}
|
|
386
|
+
const me = await getMe(options.botToken, this.#apiBaseUrl());
|
|
387
|
+
const installationId = existing?.id ?? randomUUID();
|
|
388
|
+
const webhookId = existing?.webhookId ?? randomUUID();
|
|
389
|
+
const baseUrl = this.#getBaseUrl();
|
|
390
|
+
const mode = this.#resolveMode(baseUrl);
|
|
391
|
+
if (mode === "webhook" && !baseUrl) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
'TelegramProvider needs a baseUrl to register a webhook. Set `baseUrl`, configure the Mastra server, or use `mode: "polling"`.'
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
const webhookUrl = mode === "webhook" ? `${baseUrl}/${PLATFORM}/events/${webhookId}` : void 0;
|
|
397
|
+
const commands = normalizeCommands(options.commands ?? this.#config.commands ?? DEFAULT_COMMANDS);
|
|
398
|
+
const installation = {
|
|
399
|
+
id: installationId,
|
|
400
|
+
agentId,
|
|
401
|
+
webhookId,
|
|
402
|
+
status: "active",
|
|
403
|
+
botToken: options.botToken,
|
|
404
|
+
secretToken: generateSecretToken(),
|
|
405
|
+
username: options.name ?? me.username ?? me.first_name,
|
|
406
|
+
webhookUrl,
|
|
407
|
+
commands: commands.length ? commands : void 0,
|
|
408
|
+
installedAt: existing?.installedAt ?? /* @__PURE__ */ new Date()
|
|
409
|
+
};
|
|
410
|
+
await this.#registerTransport(installation, mode);
|
|
411
|
+
await this.#registerCommands(installation);
|
|
412
|
+
await store.save(installation);
|
|
413
|
+
await this.#activateInstallation(installation);
|
|
414
|
+
this.#configured = true;
|
|
415
|
+
await this.#config.onInstall?.(installation);
|
|
416
|
+
return { type: "immediate", installationId };
|
|
417
|
+
}
|
|
418
|
+
/** Disconnect an agent from Telegram, removing its webhook and installation. */
|
|
419
|
+
async disconnect(agentId) {
|
|
420
|
+
const store = await this.#getStore();
|
|
421
|
+
const existing = await store.getByAgent(agentId);
|
|
422
|
+
if (!existing) {
|
|
423
|
+
throw new Error(`No Telegram installation found for agent "${agentId}"`);
|
|
424
|
+
}
|
|
425
|
+
const adapter = this.#adapters.get(existing.id);
|
|
426
|
+
if (adapter) {
|
|
427
|
+
try {
|
|
428
|
+
await adapter.stopPolling();
|
|
429
|
+
} catch (err) {
|
|
430
|
+
console.warn(`[Telegram] Failed to stop polling for agent "${agentId}":`, err);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (existing.botToken) {
|
|
434
|
+
try {
|
|
435
|
+
await deleteWebhook(existing.botToken, true, this.#apiBaseUrl());
|
|
436
|
+
} catch (err) {
|
|
437
|
+
console.warn(`[Telegram] Failed to delete webhook for agent "${agentId}":`, err);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
this.#adapters.delete(existing.id);
|
|
441
|
+
await store.deleteByAgent(agentId);
|
|
442
|
+
this.#configured = (await store.list()).some((i) => i.status === "active");
|
|
443
|
+
}
|
|
444
|
+
/** List installations (public info only — no tokens or secrets). */
|
|
445
|
+
async listInstallations() {
|
|
446
|
+
const store = await this.#getStore();
|
|
447
|
+
const installations = await store.list();
|
|
448
|
+
return installations.map(toInstallationInfo);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Get the full installation for an agent (includes the bot token / secret).
|
|
452
|
+
* Returns `null` if the agent has no Telegram installation. Mirrors
|
|
453
|
+
* `SlackProvider.getInstallation`.
|
|
454
|
+
*/
|
|
455
|
+
async getInstallation(agentId) {
|
|
456
|
+
const store = await this.#getStore();
|
|
457
|
+
return await store.getByAgent(agentId) ?? null;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Whether at least one bot is actively registered. Mirrors
|
|
461
|
+
* `SlackProvider.isConfigured` (Telegram has no global credential to check —
|
|
462
|
+
* "configured" means an active installation exists).
|
|
463
|
+
*/
|
|
464
|
+
isConfigured() {
|
|
465
|
+
return this.#configured;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Get the live `TelegramAdapter` for an installation id, if one is active.
|
|
469
|
+
* Used for message formatting/posting. Mirrors `SlackProvider.getAdapter`.
|
|
470
|
+
*/
|
|
471
|
+
getAdapter(installationId) {
|
|
472
|
+
return this.#adapters.get(installationId);
|
|
473
|
+
}
|
|
474
|
+
// ===========================================================================
|
|
475
|
+
// Webhook handling
|
|
476
|
+
// ===========================================================================
|
|
477
|
+
async #handleWebhook(c) {
|
|
478
|
+
const webhookId = c.req.param("webhookId");
|
|
479
|
+
if (!webhookId) return c.json({ ok: false, error: "Missing webhookId" }, 400);
|
|
480
|
+
const store = await this.#getStore();
|
|
481
|
+
const installation = await store.getByWebhookId(webhookId);
|
|
482
|
+
if (!installation || installation.status !== "active") {
|
|
483
|
+
return c.json({ ok: false, error: "Unknown webhook" }, 404);
|
|
484
|
+
}
|
|
485
|
+
const provided = c.req.header(SECRET_HEADER);
|
|
486
|
+
if (!secretMatches(provided, installation.secretToken)) {
|
|
487
|
+
return c.json({ ok: false, error: "Invalid secret token" }, 401);
|
|
488
|
+
}
|
|
489
|
+
const agent = this.#resolveAgent(installation.agentId);
|
|
490
|
+
if (!agent || !this.#mastra) {
|
|
491
|
+
return c.json({ ok: true });
|
|
492
|
+
}
|
|
493
|
+
const adapter = this.#getOrCreateAdapter(installation);
|
|
494
|
+
let channels = agent.getChannels();
|
|
495
|
+
if (!channels || channels.adapters[PLATFORM] !== adapter) {
|
|
496
|
+
channels = this.#createAgentChannels(agent, adapter);
|
|
497
|
+
await channels.initialize(this.#mastra);
|
|
498
|
+
}
|
|
499
|
+
const waitUntil = this.#config.waitUntil ?? resolveWaitUntil(c);
|
|
500
|
+
try {
|
|
501
|
+
return await channels.handleWebhookEvent(PLATFORM, c.req.raw, waitUntil ? { waitUntil } : void 0);
|
|
502
|
+
} catch (err) {
|
|
503
|
+
console.error("[Telegram] Error delegating to AgentChannels:", err);
|
|
504
|
+
return c.json({ ok: true });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
// ===========================================================================
|
|
508
|
+
// Internals
|
|
509
|
+
// ===========================================================================
|
|
510
|
+
#apiBaseUrl() {
|
|
511
|
+
return this.#config.apiBaseUrl ?? TELEGRAM_API_BASE_URL;
|
|
512
|
+
}
|
|
513
|
+
#resolveMode(baseUrl) {
|
|
514
|
+
const mode = this.#config.mode ?? "auto";
|
|
515
|
+
if (mode === "auto") return baseUrl ? "webhook" : "polling";
|
|
516
|
+
return mode;
|
|
517
|
+
}
|
|
518
|
+
/** Register (or clear) the receive transport for a bot, enforcing the exclusion. */
|
|
519
|
+
async #registerTransport(installation, mode) {
|
|
520
|
+
if (!installation.botToken) return;
|
|
521
|
+
if (mode === "webhook" && installation.webhookUrl && installation.secretToken) {
|
|
522
|
+
await setWebhook(
|
|
523
|
+
installation.botToken,
|
|
524
|
+
{
|
|
525
|
+
url: installation.webhookUrl,
|
|
526
|
+
secretToken: installation.secretToken,
|
|
527
|
+
allowedUpdates: this.#config.allowedUpdates ?? [...DEFAULT_ALLOWED_UPDATES],
|
|
528
|
+
dropPendingUpdates: true
|
|
529
|
+
},
|
|
530
|
+
this.#apiBaseUrl()
|
|
531
|
+
);
|
|
532
|
+
} else {
|
|
533
|
+
await deleteWebhook(installation.botToken, true, this.#apiBaseUrl());
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/** Publish the bot's command list (best-effort — a failure won't block connect). */
|
|
537
|
+
async #registerCommands(installation) {
|
|
538
|
+
if (!installation.botToken || !installation.commands?.length) return;
|
|
539
|
+
try {
|
|
540
|
+
await setMyCommands(
|
|
541
|
+
installation.botToken,
|
|
542
|
+
{ commands: installation.commands, scope: this.#config.commandScope },
|
|
543
|
+
this.#apiBaseUrl()
|
|
544
|
+
);
|
|
545
|
+
} catch (err) {
|
|
546
|
+
console.warn(`[Telegram] Failed to register commands for agent "${installation.agentId}":`, err);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
#getOrCreateAdapter(installation) {
|
|
550
|
+
const existing = this.#adapters.get(installation.id);
|
|
551
|
+
if (existing) return existing;
|
|
552
|
+
const adapter = createTelegramAdapter({
|
|
553
|
+
botToken: installation.botToken,
|
|
554
|
+
secretToken: installation.secretToken,
|
|
555
|
+
userName: installation.username,
|
|
556
|
+
apiBaseUrl: this.#apiBaseUrl(),
|
|
557
|
+
mode: installation.webhookUrl ? "webhook" : this.#config.mode ?? "auto",
|
|
558
|
+
...this.#config.logger !== void 0 ? { logger: this.#config.logger } : {},
|
|
559
|
+
...this.#config.longPolling !== void 0 ? { longPolling: this.#config.longPolling } : {}
|
|
560
|
+
});
|
|
561
|
+
this.#adapters.set(installation.id, adapter);
|
|
562
|
+
return adapter;
|
|
563
|
+
}
|
|
564
|
+
/** Rebuild the adapter and inject AgentChannels for an active installation. */
|
|
565
|
+
async #activateInstallation(installation) {
|
|
566
|
+
const agent = this.#resolveAgent(installation.agentId);
|
|
567
|
+
const adapter = this.#getOrCreateAdapter(installation);
|
|
568
|
+
if (agent && this.#mastra) {
|
|
569
|
+
const channels = this.#createAgentChannels(agent, adapter);
|
|
570
|
+
await channels.initialize(this.#mastra);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Create AgentChannels for an agent with the Telegram adapter, preserving any
|
|
575
|
+
* adapters/config the agent author already configured (mirrors `@mastra/slack`).
|
|
576
|
+
*/
|
|
577
|
+
#createAgentChannels(agent, adapter) {
|
|
578
|
+
const existing = agent.getChannels();
|
|
579
|
+
const existingConfig = existing?.channelConfig;
|
|
580
|
+
const cfg = this.#config;
|
|
581
|
+
const entry = {
|
|
582
|
+
adapter,
|
|
583
|
+
...resolveTelegramAdapterConfig(cfg),
|
|
584
|
+
// Telegram has no Block Kit; default tool rendering to plain text so
|
|
585
|
+
// 'cards'/'grouped'/'timeline' don't degrade to fallback text unexpectedly.
|
|
586
|
+
toolDisplay: cfg.toolDisplay ?? "text",
|
|
587
|
+
...cfg.cors !== void 0 ? { cors: cfg.cors } : {},
|
|
588
|
+
...cfg.formatError !== void 0 ? { formatError: cfg.formatError } : {}
|
|
589
|
+
};
|
|
590
|
+
const channels = new AgentChannels({
|
|
591
|
+
...existingConfig,
|
|
592
|
+
adapters: { ...existingConfig?.adapters, [PLATFORM]: entry },
|
|
593
|
+
userName: agent.name,
|
|
594
|
+
handlers: cfg.handlers ?? existingConfig?.handlers,
|
|
595
|
+
inlineMedia: cfg.inlineMedia ?? existingConfig?.inlineMedia,
|
|
596
|
+
inlineLinks: cfg.inlineLinks ?? existingConfig?.inlineLinks,
|
|
597
|
+
state: cfg.state ?? existingConfig?.state,
|
|
598
|
+
threadContext: cfg.threadContext ?? existingConfig?.threadContext,
|
|
599
|
+
chatOptions: cfg.chatOptions ?? existingConfig?.chatOptions,
|
|
600
|
+
tools: cfg.tools ?? existingConfig?.tools,
|
|
601
|
+
resolveResourceId: cfg.resolveResourceId ?? existingConfig?.resolveResourceId,
|
|
602
|
+
waitUntil: cfg.waitUntil ?? existingConfig?.waitUntil,
|
|
603
|
+
resolveWaitUntil: cfg.resolveWaitUntil ?? existingConfig?.resolveWaitUntil
|
|
604
|
+
});
|
|
605
|
+
agent.setChannels(channels);
|
|
606
|
+
return channels;
|
|
607
|
+
}
|
|
608
|
+
async #autoInitialize() {
|
|
609
|
+
if (!this.#mastra) return;
|
|
610
|
+
await this.initialize();
|
|
611
|
+
}
|
|
612
|
+
#resolveAgent(agentId) {
|
|
613
|
+
try {
|
|
614
|
+
return this.#mastra?.getAgentById(agentId);
|
|
615
|
+
} catch {
|
|
616
|
+
return void 0;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
async #getStore() {
|
|
620
|
+
if (this.#store) return this.#store;
|
|
621
|
+
const encryptionKey = this.#config.encryptionKey ?? process.env.MASTRA_ENCRYPTION_KEY;
|
|
622
|
+
this.#store = new TelegramInstallStore(await this.#resolveStorage(), encryptionKey);
|
|
623
|
+
return this.#store;
|
|
624
|
+
}
|
|
625
|
+
async #resolveStorage() {
|
|
626
|
+
if (this.#config.storage) return this.#config.storage;
|
|
627
|
+
const mastraStore = this.#mastra?.getStorage();
|
|
628
|
+
if (mastraStore) {
|
|
629
|
+
try {
|
|
630
|
+
await mastraStore.init();
|
|
631
|
+
const channels = await mastraStore.getStore("channels");
|
|
632
|
+
if (channels) return channels;
|
|
633
|
+
} catch {
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return new InMemoryChannelsStorage();
|
|
637
|
+
}
|
|
638
|
+
#getBaseUrl() {
|
|
639
|
+
if (this.#config.baseUrl) return stripTrailingSlash(this.#config.baseUrl);
|
|
640
|
+
const server = this.#mastra?.getServer();
|
|
641
|
+
if (!server) return void 0;
|
|
642
|
+
const protocol = server.studioProtocol ?? "http";
|
|
643
|
+
const host = server.studioHost ?? server.host ?? "localhost";
|
|
644
|
+
const port = server.studioPort ?? server.port ?? (Number(process.env.PORT) || 4111);
|
|
645
|
+
const includePort = !(protocol === "https" && port === 443 || protocol === "http" && port === 80);
|
|
646
|
+
return includePort ? `${protocol}://${host}:${port}` : `${protocol}://${host}`;
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
function secretMatches(provided, expected) {
|
|
650
|
+
if (!provided || !expected) return false;
|
|
651
|
+
const a = Buffer.from(provided);
|
|
652
|
+
const b = Buffer.from(expected);
|
|
653
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
654
|
+
}
|
|
655
|
+
function stripTrailingSlash(url) {
|
|
656
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/index.ts
|
|
660
|
+
import { createTelegramAdapter as createTelegramAdapter2, TelegramAdapter } from "@chat-adapter/telegram";
|
|
661
|
+
export {
|
|
662
|
+
BOTFATHER_DEEP_LINK,
|
|
663
|
+
DEFAULT_ALLOWED_UPDATES,
|
|
664
|
+
DEFAULT_COMMANDS,
|
|
665
|
+
PLATFORM,
|
|
666
|
+
TELEGRAM_API_BASE_URL,
|
|
667
|
+
TelegramAdapter,
|
|
668
|
+
TelegramInstallStore,
|
|
669
|
+
TelegramProvider,
|
|
670
|
+
createTelegramAdapter2 as createTelegramAdapter,
|
|
671
|
+
deleteWebhook,
|
|
672
|
+
generateSecretToken,
|
|
673
|
+
getMe,
|
|
674
|
+
normalizeCommands,
|
|
675
|
+
resolveTelegramAdapterConfig,
|
|
676
|
+
setMyCommands,
|
|
677
|
+
setWebhook,
|
|
678
|
+
toInstallationInfo
|
|
679
|
+
};
|
|
680
|
+
//# sourceMappingURL=index.js.map
|