@capybara-hq/plugin-telegram 0.1.0-rc.6 → 0.1.0-rc.7
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/.capybara-plugin/plugin.json +2 -2
- package/README.md +20 -8
- package/dist/index.mjs +139 -26
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"name": "telegram",
|
|
4
|
-
"version": "0.1.0-rc.
|
|
4
|
+
"version": "0.1.0-rc.7",
|
|
5
5
|
"publisher": "Capybara",
|
|
6
6
|
"description": {
|
|
7
7
|
"en": "Telegram (channel@1 bot over the Bot API: long polling or webhook, inline keyboard cards)",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"entry": "./dist/index.mjs",
|
|
18
18
|
"manifest": {
|
|
19
19
|
"name": "telegram",
|
|
20
|
-
"version": "0.1.0-rc.
|
|
20
|
+
"version": "0.1.0-rc.7",
|
|
21
21
|
"sdk": "^0.1.0-rc.4",
|
|
22
22
|
"provides": [
|
|
23
23
|
"channel@1"
|
package/README.md
CHANGED
|
@@ -14,18 +14,21 @@ global (one person has the same id for every bot), so one person is one identity
|
|
|
14
14
|
|
|
15
15
|
The Telegram docs cited here are the [Bot API reference](https://core.telegram.org/bots/api) (Bot API 10.3), consulted on 2026-11-08.
|
|
16
16
|
|
|
17
|
-
Verified live on 2026-11-08 against a real bot: verify, send, edit, a 5000-character split send and a card with an inline callback keyboard, in a private chat and in a basic group. Not covered live: a person clicking a card button, webhook mode and forum supergroups.
|
|
17
|
+
Verified live on 2026-11-08 against a real bot: verify, send, edit, a 5000-character split send and a card with an inline callback keyboard, in a private chat and in a basic group. Verified live on 2026-11-10: a reply with a table sent as a rich message, and a plain draft edited into one and edited again, in a private chat (live test `rich table`; in Node, a slow IPv4 connect can lose to an unreachable IPv6 address: run it with `NODE_OPTIONS=--network-family-autoselection-attempt-timeout=3000` if every call fails with a network error). Not covered live: a person clicking a card button, webhook mode and forum supergroups.
|
|
18
18
|
|
|
19
19
|
## Create the bot
|
|
20
20
|
|
|
21
21
|
1. In Telegram, open [@BotFather](https://t.me/BotFather) and send `/newbot`. Pick a display name and a username
|
|
22
22
|
(it must end in `bot`). BotFather replies with the bot token, such as `123456:ABC-DEF…`
|
|
23
23
|
([Bot API: authorizing your bot](https://core.telegram.org/bots/api#authorizing-your-bot)). Treat it like a password.
|
|
24
|
-
2. **Privacy mode** (`/setprivacy`):
|
|
25
|
-
|
|
26
|
-
(
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
2. **Privacy mode** (`/setprivacy`): for groups, turn it **off** (`Disable`), or make the bot an admin of
|
|
25
|
+
each group it joins. With privacy mode on (the default), Telegram delivers group commands addressed to the
|
|
26
|
+
bot (`/cmd@botname`) and replies to the bot's messages, but **not** a plain `@botname` mention, although
|
|
27
|
+
BotFather's description says otherwise (verified live 2026-11-10;
|
|
28
|
+
[Bot API: privacy mode](https://core.telegram.org/bots/features#privacy-mode)). With it off the bot receives
|
|
29
|
+
every group message; the plugin still forwards only mentions of the bot and replies to it, everything else
|
|
30
|
+
is dropped. Telegram applies a privacy change to a group only after the bot is removed from the group and
|
|
31
|
+
added again. `verify` logs a warning while privacy mode is on. Private chats work either way.
|
|
29
32
|
3. **Groups** (`/setjoingroups`): enable it if people should add the bot to groups, disable it for a private-only bot.
|
|
30
33
|
4. **Topics in private chats**: leave the bot's topic mode off. It is not supported (see [Behavior](#behavior)); `verify` warns when it is on.
|
|
31
34
|
5. In the Capybara org settings, enable the `telegram` plugin, then **create a new channel connection** under Channels:
|
|
@@ -85,13 +88,22 @@ The deployment-level config lives under the kernel config `extensionConfig.teleg
|
|
|
85
88
|
- **Format**: the model reply's Markdown is converted to Telegram HTML (`parse_mode: "HTML"`); everything else is escaped.
|
|
86
89
|
Link previews are off. Links are limited to http(s): `tg://` and other schemes stay plain text, so model text cannot produce `tg://user?id=…` mentions.
|
|
87
90
|
A raw `@username` in model text would notify people, so the converter puts a U+2060 WORD JOINER after the `@` outside code and links, and Telegram no longer parses it as a mention.
|
|
88
|
-
- **
|
|
91
|
+
- **Tables**: a reply with a Markdown (GFM) table is sent as a [rich message](https://core.telegram.org/bots/api/rich-messages)
|
|
92
|
+
(Bot API 10.1: [`sendRichMessage`](https://core.telegram.org/bots/api#sendrichmessage), and `editMessageText` with `rich_message` while it streams),
|
|
93
|
+
so the table renders natively (bordered, striped, column alignment from the delimiter row). The rest of the reply goes through the same converter
|
|
94
|
+
as plain replies (escaping, http(s)-only links, the mention guard), as rich HTML with `<br>` line breaks (rich HTML collapses bare newlines).
|
|
95
|
+
A rich message holds up to 32768 characters, so such a reply is never split. A table wider than 20 columns becomes the list described below;
|
|
96
|
+
a reply over 500 blocks (table rows count) or 32768 characters, or one with cards, is sent as plain HTML.
|
|
97
|
+
When Telegram refuses the rich message for its content (any 400, or 404 from a Bot API server without the method), the same reply is sent or edited
|
|
98
|
+
as plain HTML, where each table is a list (one item per row, led by its first cell); flood waits, 5xx and a blocked bot are not retried as plain HTML.
|
|
99
|
+
Telegram turns a text message into a rich one and back on edit, so streaming keeps the send-then-edit flow (no delete and resend, no extra notification).
|
|
100
|
+
- **Limit and splitting**: one plain message holds at most 4096 characters after entity parsing
|
|
89
101
|
([`sendMessage`, `text`](https://core.telegram.org/bots/api#sendmessage)); longer text is split into several messages and the last id is returned.
|
|
90
102
|
An edit that would need several messages reports `too_large` and the kernel sends a new message instead.
|
|
91
103
|
- **Streaming edits**: replies are sent and then edited ([`editMessageText`](https://core.telegram.org/bots/api#editmessagetext)); `minEditIntervalMs = 3000`.
|
|
92
104
|
Telegram's limit for a group is about 20 messages per minute ([Broadcasting to users, FAQ](https://core.telegram.org/bots/faq))
|
|
93
105
|
and edits count; a 429 is honored through `retry_after`. "message is not modified" on an edit counts as success.
|
|
94
|
-
[`sendMessageDraft`](https://core.telegram.org/bots/api#sendmessagedraft)
|
|
106
|
+
[`sendMessageDraft`](https://core.telegram.org/bots/api#sendmessagedraft) and `sendRichMessageDraft` are not used: it exists only in private chats, is a 30-second preview that must be followed by a real
|
|
95
107
|
`sendMessage`, and does not fit the kernel's send-then-edit model.
|
|
96
108
|
- **Idempotency**: the Bot API has no idempotent send, so `idempotencyKey` is ignored. A send whose response is lost and is retried can show the message twice; the alternative (never retrying) would lose messages.
|
|
97
109
|
- **Permission manifest**: `network: ["api.telegram.org"]`, shown only and not enforced.
|
package/dist/index.mjs
CHANGED
|
@@ -291,9 +291,14 @@ function splitRow(line) {
|
|
|
291
291
|
}
|
|
292
292
|
var candidate = (line) => !/^\s{4,}/.test(line) && !/^\s*>/.test(line);
|
|
293
293
|
function delimiterRow(line, width) {
|
|
294
|
-
if (line === void 0 || !candidate(line)) return
|
|
294
|
+
if (line === void 0 || !candidate(line)) return void 0;
|
|
295
295
|
const cells = splitRow(line);
|
|
296
|
-
|
|
296
|
+
if (cells === void 0 || cells.length !== width || !cells.every((c) => DELIM_CELL.test(c))) {
|
|
297
|
+
return void 0;
|
|
298
|
+
}
|
|
299
|
+
return cells.map(
|
|
300
|
+
(c) => c.startsWith(":") ? c.endsWith(":") ? "center" : "left" : c.endsWith(":") ? "right" : void 0
|
|
301
|
+
);
|
|
297
302
|
}
|
|
298
303
|
var fit = (cells, width) => Array.from({ length: width }, (_, i) => cells[i] ?? "");
|
|
299
304
|
function segmentTables(md) {
|
|
@@ -313,7 +318,8 @@ function segmentTables(md) {
|
|
|
313
318
|
continue;
|
|
314
319
|
}
|
|
315
320
|
const header = !inFence && candidate(line) ? splitRow(line) : void 0;
|
|
316
|
-
|
|
321
|
+
const align = header && delimiterRow(lines[i + 1], header.length);
|
|
322
|
+
if (!header || !align) {
|
|
317
323
|
text.push(line);
|
|
318
324
|
continue;
|
|
319
325
|
}
|
|
@@ -327,7 +333,7 @@ function segmentTables(md) {
|
|
|
327
333
|
rows.push(fit(cells, header.length));
|
|
328
334
|
}
|
|
329
335
|
flushText();
|
|
330
|
-
out.push({ kind: "table", table: { header, rows } });
|
|
336
|
+
out.push({ kind: "table", table: { header, rows, align } });
|
|
331
337
|
i = j - 1;
|
|
332
338
|
}
|
|
333
339
|
flushText();
|
|
@@ -337,7 +343,7 @@ var BR = /<br\s*\/?>/gi;
|
|
|
337
343
|
var CJK_END = /[⺀-鿿가-豈--]$/u;
|
|
338
344
|
var INDENT = " ";
|
|
339
345
|
var pair = (h, v) => h === "" ? v : `${h}${CJK_END.test(h) ? ":" : ": "}${v}`;
|
|
340
|
-
var lead = (
|
|
346
|
+
var lead = (cell2) => /`|\*\*|__/.test(cell2) ? cell2 : `**${cell2}**`;
|
|
341
347
|
function tableToListMarkdown(t) {
|
|
342
348
|
const clean = (s) => s.replace(BR, " / ").trim();
|
|
343
349
|
const header = t.header.map(clean);
|
|
@@ -442,8 +448,8 @@ function codeBlock(lang, lines) {
|
|
|
442
448
|
const l = lang.replace(/[^A-Za-z0-9_+#.-]/g, "");
|
|
443
449
|
return l === "" ? `<pre>${body}</pre>` : `<pre><code class="language-${l}">${body}</code></pre>`;
|
|
444
450
|
}
|
|
445
|
-
|
|
446
|
-
|
|
451
|
+
var normaliseMarkdown = (md) => md.replace(/\r\n?/g, "\n").replaceAll("\0", "");
|
|
452
|
+
function convertLines(lines) {
|
|
447
453
|
const out = [];
|
|
448
454
|
let fence;
|
|
449
455
|
let quote;
|
|
@@ -476,7 +482,10 @@ function markdownToTelegramHtml(md) {
|
|
|
476
482
|
}
|
|
477
483
|
if (fence) out.push(codeBlock(fence.lang, fence.lines));
|
|
478
484
|
closeQuote();
|
|
479
|
-
return
|
|
485
|
+
return out.join("\n");
|
|
486
|
+
}
|
|
487
|
+
function markdownToTelegramHtml(md) {
|
|
488
|
+
return neutraliseHtmlMentions(convertLines(tablesToLists(normaliseMarkdown(md)).split("\n")));
|
|
480
489
|
}
|
|
481
490
|
function tokenise(html) {
|
|
482
491
|
const tokens = [];
|
|
@@ -1047,6 +1056,50 @@ async function writeOffset(kv, key, offset, log) {
|
|
|
1047
1056
|
}
|
|
1048
1057
|
}
|
|
1049
1058
|
|
|
1059
|
+
// src/rich.ts
|
|
1060
|
+
var RICH_LIMITS = { text: 32768, blocks: 500, columns: 20 };
|
|
1061
|
+
function cell(tag, text, align) {
|
|
1062
|
+
const body = text.split(BR).map((part) => inline(part.trim())).join("<br>");
|
|
1063
|
+
return `<${tag}${align ? ` align="${align}"` : ""}>${body}</${tag}>`;
|
|
1064
|
+
}
|
|
1065
|
+
function tableToRichHtml(t) {
|
|
1066
|
+
const row = (tag, cells) => `<tr>${cells.map((c, i) => cell(tag, c, t.align[i])).join("")}</tr>`;
|
|
1067
|
+
return `<table bordered striped>${row("th", t.header)}${t.rows.map((r) => row("td", r)).join("")}</table>`;
|
|
1068
|
+
}
|
|
1069
|
+
function textToRichHtml(html) {
|
|
1070
|
+
return html.split(/(<pre[^>]*>[\s\S]*?<\/pre>)/).map(
|
|
1071
|
+
(part, i) => i % 2 === 1 ? part : part.replace(/\n*(<\/?blockquote>)\n*/g, "$1").replace(/^\n+|\n+$/g, "").replace(/\n/g, "<br>")
|
|
1072
|
+
).join("");
|
|
1073
|
+
}
|
|
1074
|
+
function markdownToRichHtml(md) {
|
|
1075
|
+
const segments = segmentTables(normaliseMarkdown(md));
|
|
1076
|
+
const native = (t) => t.header.length <= RICH_LIMITS.columns;
|
|
1077
|
+
if (!segments.some((s) => s.kind === "table" && native(s.table))) return void 0;
|
|
1078
|
+
const parts = [];
|
|
1079
|
+
let lines = [];
|
|
1080
|
+
let blocks = 0;
|
|
1081
|
+
const flush = () => {
|
|
1082
|
+
const html2 = textToRichHtml(convertLines(lines));
|
|
1083
|
+
lines = [];
|
|
1084
|
+
if (html2 === "") return;
|
|
1085
|
+
parts.push(html2);
|
|
1086
|
+
blocks += 1 + (html2.match(/<pre|<blockquote/g)?.length ?? 0);
|
|
1087
|
+
};
|
|
1088
|
+
for (const s of segments) {
|
|
1089
|
+
if (s.kind === "text") lines.push(...s.lines);
|
|
1090
|
+
else if (!native(s.table)) lines.push(...tableToListMarkdown(s.table));
|
|
1091
|
+
else {
|
|
1092
|
+
flush();
|
|
1093
|
+
parts.push(tableToRichHtml(s.table));
|
|
1094
|
+
blocks += 1 + s.table.rows.length + 1;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
flush();
|
|
1098
|
+
const html = neutraliseHtmlMentions(parts.join(""));
|
|
1099
|
+
if (blocks > RICH_LIMITS.blocks || visibleLength(html) > RICH_LIMITS.text) return void 0;
|
|
1100
|
+
return html;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1050
1103
|
// src/webhook.ts
|
|
1051
1104
|
import { timingSafeEqual } from "node:crypto";
|
|
1052
1105
|
var SECRET_HEADER = "x-telegram-bot-api-secret-token";
|
|
@@ -1127,6 +1180,13 @@ var tooLarge = () => new ChannelSendError2("message too long for a single Telegr
|
|
|
1127
1180
|
reason: "too_large"
|
|
1128
1181
|
});
|
|
1129
1182
|
var urlButtonRejected = (e) => e instanceof TelegramApiError && (e.errorCode ?? e.status) === 400 && /button URL|Wrong HTTP URL|BUTTON_URL_INVALID/i.test(e.description ?? "");
|
|
1183
|
+
var richRefused = (e) => {
|
|
1184
|
+
if (!(e instanceof TelegramApiError)) return false;
|
|
1185
|
+
const mapped = toChannelSendError(e);
|
|
1186
|
+
return !mapped.retryable && (mapped.reason === "rejected" || mapped.reason === "too_large");
|
|
1187
|
+
};
|
|
1188
|
+
var notModified = (e) => e instanceof TelegramApiError && /message is not modified/i.test(e.description ?? "");
|
|
1189
|
+
var richOf = (message) => (message.cards ?? []).length === 0 && message.text.trim() !== "" ? markdownToRichHtml(message.text) : void 0;
|
|
1130
1190
|
function createTelegramChannel(o) {
|
|
1131
1191
|
const apiFor = (config, signal) => {
|
|
1132
1192
|
const c = TelegramConnection.parse(config);
|
|
@@ -1167,6 +1227,33 @@ function createTelegramChannel(o) {
|
|
|
1167
1227
|
return attempt(post.withoutUrlButtons);
|
|
1168
1228
|
}
|
|
1169
1229
|
}
|
|
1230
|
+
const warnRichFallback = (e, method) => o.log.warn("telegram: Telegram refused a rich message; the reply is sent as plain HTML", {
|
|
1231
|
+
method,
|
|
1232
|
+
...e instanceof TelegramApiError && { error: e.description ?? e.message }
|
|
1233
|
+
});
|
|
1234
|
+
async function sendRich(api, target, html, extra) {
|
|
1235
|
+
let r;
|
|
1236
|
+
try {
|
|
1237
|
+
r = await api.call("sendRichMessage", {
|
|
1238
|
+
chat_id: target.chatId,
|
|
1239
|
+
rich_message: { html },
|
|
1240
|
+
...extra
|
|
1241
|
+
});
|
|
1242
|
+
} catch (e) {
|
|
1243
|
+
if (richRefused(e)) {
|
|
1244
|
+
warnRichFallback(e, "sendRichMessage");
|
|
1245
|
+
return void 0;
|
|
1246
|
+
}
|
|
1247
|
+
throw toChannelSendError(e);
|
|
1248
|
+
}
|
|
1249
|
+
if (typeof r?.message_id !== "number") {
|
|
1250
|
+
throw new ChannelSendError2("Telegram returned no message id", {
|
|
1251
|
+
retryable: false,
|
|
1252
|
+
reason: "rejected"
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
return r.message_id;
|
|
1256
|
+
}
|
|
1170
1257
|
async function sendOne(api, target, post, extra) {
|
|
1171
1258
|
let r;
|
|
1172
1259
|
try {
|
|
@@ -1201,9 +1288,9 @@ function createTelegramChannel(o) {
|
|
|
1201
1288
|
if (typeof me?.id !== "number" || !me.username) {
|
|
1202
1289
|
throw new Error("Telegram getMe returned no bot user");
|
|
1203
1290
|
}
|
|
1204
|
-
if (me.can_read_all_group_messages) {
|
|
1291
|
+
if (!me.can_read_all_group_messages) {
|
|
1205
1292
|
o.log.warn(
|
|
1206
|
-
"telegram: the bot
|
|
1293
|
+
"telegram: privacy mode is on, so in groups the bot gets only commands and replies to its messages, not plain @mentions; turn privacy mode off in BotFather (then remove and re-add the bot to each group) or make the bot a group admin",
|
|
1207
1294
|
{ botId: me.id }
|
|
1208
1295
|
);
|
|
1209
1296
|
}
|
|
@@ -1285,8 +1372,11 @@ function createTelegramChannel(o) {
|
|
|
1285
1372
|
},
|
|
1286
1373
|
async send({ config, target, message, locale, signal }) {
|
|
1287
1374
|
const { call, botId } = apiFor(config, signal);
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1375
|
+
const rich = richOf(message);
|
|
1376
|
+
const posts = rich === void 0 ? await render(message, locale, botId) : [];
|
|
1377
|
+
if (rich === void 0 && posts.length === 0) {
|
|
1378
|
+
throw new ChannelSendError2("nothing to send", { retryable: false });
|
|
1379
|
+
}
|
|
1290
1380
|
const store = threadStore(o.kv, botId);
|
|
1291
1381
|
const inThread = target.chatType === "group" && target.threadId !== void 0;
|
|
1292
1382
|
const extra = {};
|
|
@@ -1298,25 +1388,50 @@ function createTelegramChannel(o) {
|
|
|
1298
1388
|
const topic = await store.topicOf(target.threadId);
|
|
1299
1389
|
if (topic !== void 0) extra.message_thread_id = topic;
|
|
1300
1390
|
}
|
|
1391
|
+
const record = async (sent) => {
|
|
1392
|
+
if (!inThread || target.threadId === void 0) return;
|
|
1393
|
+
try {
|
|
1394
|
+
await store.recordSent(target.chatId, sent, target.threadId);
|
|
1395
|
+
} catch {
|
|
1396
|
+
o.log.warn("telegram: could not record a sent message's thread", {
|
|
1397
|
+
chatId: target.chatId,
|
|
1398
|
+
messageId: sent
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
if (rich !== void 0) {
|
|
1403
|
+
const sent = await sendRich({ call }, target, rich, extra);
|
|
1404
|
+
if (sent !== void 0) {
|
|
1405
|
+
await record(sent);
|
|
1406
|
+
return { messageId: `${target.chatId}:${sent}` };
|
|
1407
|
+
}
|
|
1408
|
+
posts.push(...await render(message, locale, botId));
|
|
1409
|
+
}
|
|
1301
1410
|
let last = 0;
|
|
1302
1411
|
for (const post of posts) {
|
|
1303
1412
|
last = await sendOne({ call }, target, post, extra);
|
|
1304
|
-
|
|
1305
|
-
try {
|
|
1306
|
-
await store.recordSent(target.chatId, last, target.threadId);
|
|
1307
|
-
} catch {
|
|
1308
|
-
o.log.warn("telegram: could not record a sent message's thread", {
|
|
1309
|
-
chatId: target.chatId,
|
|
1310
|
-
messageId: last
|
|
1311
|
-
});
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1413
|
+
await record(last);
|
|
1314
1414
|
}
|
|
1315
1415
|
return { messageId: `${target.chatId}:${last}` };
|
|
1316
1416
|
},
|
|
1317
1417
|
async update({ config, messageId, message, locale, signal }) {
|
|
1318
1418
|
const { call, botId } = apiFor(config, signal);
|
|
1319
1419
|
const { chatId, messageId: id } = parseMessageId(messageId);
|
|
1420
|
+
const rich = richOf(message);
|
|
1421
|
+
if (rich !== void 0) {
|
|
1422
|
+
try {
|
|
1423
|
+
await call("editMessageText", {
|
|
1424
|
+
chat_id: chatId,
|
|
1425
|
+
message_id: id,
|
|
1426
|
+
rich_message: { html: rich }
|
|
1427
|
+
});
|
|
1428
|
+
return;
|
|
1429
|
+
} catch (e) {
|
|
1430
|
+
if (notModified(e)) return;
|
|
1431
|
+
if (!richRefused(e)) throw toChannelSendError(e);
|
|
1432
|
+
warnRichFallback(e, "editMessageText");
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1320
1435
|
const posts = await render(message, locale, botId);
|
|
1321
1436
|
if (posts.length > 1) throw tooLarge();
|
|
1322
1437
|
const post = posts[0];
|
|
@@ -1334,9 +1449,7 @@ function createTelegramChannel(o) {
|
|
|
1334
1449
|
})
|
|
1335
1450
|
);
|
|
1336
1451
|
} catch (e) {
|
|
1337
|
-
if (
|
|
1338
|
-
return;
|
|
1339
|
-
}
|
|
1452
|
+
if (notModified(e)) return;
|
|
1340
1453
|
throw toChannelSendError(e);
|
|
1341
1454
|
}
|
|
1342
1455
|
}
|
|
@@ -1346,7 +1459,7 @@ function createTelegramChannel(o) {
|
|
|
1346
1459
|
// src/index.ts
|
|
1347
1460
|
var index_default = defineExtension({
|
|
1348
1461
|
name: "telegram",
|
|
1349
|
-
version: "0.1.0-rc.
|
|
1462
|
+
version: "0.1.0-rc.7",
|
|
1350
1463
|
sdk: "^0.1.0-rc.4",
|
|
1351
1464
|
provides: [CHANNEL_EXTENSION_POINT],
|
|
1352
1465
|
permissions: { network: ["api.telegram.org"] },
|
package/package.json
CHANGED