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