@xbibzlibrary/telebibz 0.1.19 → 0.3.1
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 +42 -8
- package/CONTRIBUTING.md +2 -2
- package/README.id.md +44 -5
- package/README.md +45 -6
- package/README.zh-CN.md +44 -5
- package/RELEASE_AUTOMATION.md +17 -5
- package/bin/telebibz.mjs +1 -1
- package/dist/src/api/client.d.ts +3 -1
- package/dist/src/api/client.d.ts.map +1 -1
- package/dist/src/api/client.js +3 -1
- package/dist/src/api/client.js.map +1 -1
- package/dist/src/api/transport.d.ts +18 -1
- package/dist/src/api/transport.d.ts.map +1 -1
- package/dist/src/api/transport.js +26 -2
- package/dist/src/api/transport.js.map +1 -1
- package/dist/src/branding/terminal.d.ts +62 -0
- package/dist/src/branding/terminal.d.ts.map +1 -1
- package/dist/src/branding/terminal.js +258 -0
- package/dist/src/branding/terminal.js.map +1 -1
- package/dist/src/broadcast/broadcast.d.ts +50 -0
- package/dist/src/broadcast/broadcast.d.ts.map +1 -0
- package/dist/src/broadcast/broadcast.js +56 -0
- package/dist/src/broadcast/broadcast.js.map +1 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +7 -3
- package/dist/src/cli.js.map +1 -1
- package/dist/src/context/context.d.ts +24 -1
- package/dist/src/context/context.d.ts.map +1 -1
- package/dist/src/context/context.js +102 -8
- package/dist/src/context/context.js.map +1 -1
- package/dist/src/core/bot.d.ts +61 -2
- package/dist/src/core/bot.d.ts.map +1 -1
- package/dist/src/core/bot.js +180 -31
- package/dist/src/core/bot.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/keyboard/index.d.ts +12 -0
- package/dist/src/keyboard/index.d.ts.map +1 -1
- package/dist/src/keyboard/index.js +13 -1
- package/dist/src/keyboard/index.js.map +1 -1
- package/dist/src/observability/logger.d.ts +28 -1
- package/dist/src/observability/logger.d.ts.map +1 -1
- package/dist/src/observability/logger.js +110 -0
- package/dist/src/observability/logger.js.map +1 -1
- package/dist/src/plugins/plugin.d.ts +2 -0
- package/dist/src/plugins/plugin.d.ts.map +1 -1
- package/dist/src/plugins/plugin.js +11 -4
- package/dist/src/plugins/plugin.js.map +1 -1
- package/dist/src/router/router.d.ts +19 -0
- package/dist/src/router/router.d.ts.map +1 -1
- package/dist/src/router/router.js +125 -23
- package/dist/src/router/router.js.map +1 -1
- package/dist/src/state/forms.d.ts +0 -1
- package/dist/src/state/forms.d.ts.map +1 -1
- package/dist/src/state/forms.js +27 -24
- package/dist/src/state/forms.js.map +1 -1
- package/dist/src/storage/storage.d.ts +10 -0
- package/dist/src/storage/storage.d.ts.map +1 -1
- package/dist/src/storage/storage.js +20 -2
- package/dist/src/storage/storage.js.map +1 -1
- package/dist/src/utils/concurrency.d.ts +25 -0
- package/dist/src/utils/concurrency.d.ts.map +1 -0
- package/dist/src/utils/concurrency.js +52 -0
- package/dist/src/utils/concurrency.js.map +1 -0
- package/dist/src/utils/text.d.ts +22 -0
- package/dist/src/utils/text.d.ts.map +1 -1
- package/dist/src/utils/text.js +0 -0
- package/dist/src/utils/text.js.map +1 -1
- package/dist/src/webhook/handler.d.ts +3 -0
- package/dist/src/webhook/handler.d.ts.map +1 -1
- package/dist/src/webhook/handler.js +85 -0
- package/dist/src/webhook/handler.js.map +1 -1
- package/dist-cjs/src/api/client.js +3 -1
- package/dist-cjs/src/api/transport.js +26 -2
- package/dist-cjs/src/branding/terminal.js +264 -1
- package/dist-cjs/src/broadcast/broadcast.js +58 -0
- package/dist-cjs/src/cli.js +7 -3
- package/dist-cjs/src/context/context.js +102 -8
- package/dist-cjs/src/core/bot.js +178 -29
- package/dist-cjs/src/index.js +2 -0
- package/dist-cjs/src/keyboard/index.js +13 -1
- package/dist-cjs/src/observability/logger.js +113 -1
- package/dist-cjs/src/plugins/plugin.js +11 -4
- package/dist-cjs/src/router/router.js +126 -24
- package/dist-cjs/src/state/forms.js +27 -24
- package/dist-cjs/src/storage/storage.js +20 -2
- package/dist-cjs/src/utils/concurrency.js +57 -0
- package/dist-cjs/src/utils/text.js +0 -0
- package/dist-cjs/src/webhook/handler.js +86 -0
- package/docs/API.id.md +120 -4
- package/docs/API.md +122 -4
- package/docs/API.zh-CN.md +119 -3
- package/package.json +3 -3
|
@@ -1,12 +1,57 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Router = void 0;
|
|
3
|
+
exports.Router = exports.UPDATE_FILTER_TYPES = void 0;
|
|
4
4
|
const compose_js_1 = require("../middleware/compose.js");
|
|
5
5
|
function testRegExp(expression, value) {
|
|
6
6
|
expression.lastIndex = 0;
|
|
7
7
|
return expression.test(value);
|
|
8
8
|
}
|
|
9
|
+
/** Update types accepted by `on()` filters, mirroring the Telegram `Update` object. */
|
|
10
|
+
exports.UPDATE_FILTER_TYPES = [
|
|
11
|
+
"message",
|
|
12
|
+
"edited_message",
|
|
13
|
+
"channel_post",
|
|
14
|
+
"edited_channel_post",
|
|
15
|
+
"business_connection",
|
|
16
|
+
"business_message",
|
|
17
|
+
"edited_business_message",
|
|
18
|
+
"deleted_business_messages",
|
|
19
|
+
"guest_message",
|
|
20
|
+
"message_reaction",
|
|
21
|
+
"message_reaction_count",
|
|
22
|
+
"inline_query",
|
|
23
|
+
"chosen_inline_result",
|
|
24
|
+
"callback_query",
|
|
25
|
+
"shipping_query",
|
|
26
|
+
"pre_checkout_query",
|
|
27
|
+
"purchased_paid_media",
|
|
28
|
+
"poll",
|
|
29
|
+
"poll_answer",
|
|
30
|
+
"my_chat_member",
|
|
31
|
+
"chat_member",
|
|
32
|
+
"chat_join_request",
|
|
33
|
+
"chat_boost",
|
|
34
|
+
"removed_chat_boost",
|
|
35
|
+
];
|
|
36
|
+
const updateFilterTypes = new Set(exports.UPDATE_FILTER_TYPES);
|
|
37
|
+
function assertValidFilter(filter) {
|
|
38
|
+
const type = filter.split(":", 1)[0] ?? "";
|
|
39
|
+
if (!updateFilterTypes.has(type))
|
|
40
|
+
throw new TypeError(`Unknown update type in filter "${filter}". Expected one of: ${exports.UPDATE_FILTER_TYPES.join(", ")}.`);
|
|
41
|
+
}
|
|
42
|
+
function matchesFilter(ctx, filter) {
|
|
43
|
+
const separator = filter.indexOf(":");
|
|
44
|
+
const type = separator < 0 ? filter : filter.slice(0, separator);
|
|
45
|
+
const field = separator < 0 ? undefined : filter.slice(separator + 1);
|
|
46
|
+
const payload = ctx.update[type];
|
|
47
|
+
if (payload === undefined || payload === null)
|
|
48
|
+
return false;
|
|
49
|
+
if (field === undefined || field === "")
|
|
50
|
+
return true;
|
|
51
|
+
return typeof payload === "object" && payload[field] !== undefined;
|
|
52
|
+
}
|
|
9
53
|
class Router {
|
|
54
|
+
middlewares = [];
|
|
10
55
|
routes = [];
|
|
11
56
|
sequence = 0;
|
|
12
57
|
matchMode;
|
|
@@ -16,7 +61,7 @@ class Router {
|
|
|
16
61
|
use(...middleware) {
|
|
17
62
|
if (middleware.length === 0)
|
|
18
63
|
return this;
|
|
19
|
-
this.
|
|
64
|
+
this.middlewares.push(...middleware);
|
|
20
65
|
return this;
|
|
21
66
|
}
|
|
22
67
|
route(matcher, ...middleware) {
|
|
@@ -27,29 +72,61 @@ class Router {
|
|
|
27
72
|
}
|
|
28
73
|
command(name, ...middleware) {
|
|
29
74
|
return this.route(async (ctx) => {
|
|
30
|
-
const text = ctx.message?.text;
|
|
75
|
+
const text = ctx.message?.text ?? ctx.message?.caption;
|
|
31
76
|
if (!text?.startsWith("/"))
|
|
32
77
|
return false;
|
|
33
|
-
const
|
|
34
|
-
|
|
78
|
+
const parts = text.slice(1).split(/\s+/);
|
|
79
|
+
const commandWithBot = parts[0] ?? "";
|
|
80
|
+
const [command, botUsername] = commandWithBot.split("@");
|
|
81
|
+
if (botUsername && ctx.me?.username && botUsername.toLowerCase() !== ctx.me.username.toLowerCase()) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const matched = typeof name === "string"
|
|
35
85
|
? command === name.replace(/^\//, "")
|
|
36
|
-
: testRegExp(name, command);
|
|
86
|
+
: testRegExp(name, command ?? "");
|
|
87
|
+
if (matched) {
|
|
88
|
+
ctx.args = parts.slice(1);
|
|
89
|
+
}
|
|
90
|
+
return matched;
|
|
37
91
|
}, ...middleware);
|
|
38
92
|
}
|
|
39
93
|
text(value, ...middleware) {
|
|
40
|
-
return this.route((ctx) =>
|
|
94
|
+
return this.route((ctx) => {
|
|
95
|
+
const text = ctx.message?.text ?? ctx.message?.caption;
|
|
96
|
+
return text === value;
|
|
97
|
+
}, ...middleware);
|
|
41
98
|
}
|
|
42
99
|
regex(expression, ...middleware) {
|
|
43
|
-
return this.route((ctx) =>
|
|
100
|
+
return this.route((ctx) => {
|
|
101
|
+
const text = ctx.message?.text ?? ctx.message?.caption ?? "";
|
|
102
|
+
const match = text.match(expression);
|
|
103
|
+
if (match) {
|
|
104
|
+
ctx.match = match;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}, ...middleware);
|
|
44
109
|
}
|
|
45
110
|
callback(pattern, ...middleware) {
|
|
46
111
|
return this.route((ctx) => {
|
|
47
112
|
const data = ctx.callbackQuery?.data ?? "";
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
113
|
+
if (typeof pattern === "string") {
|
|
114
|
+
if (pattern.endsWith("*")) {
|
|
115
|
+
const prefix = pattern.slice(0, -1);
|
|
116
|
+
if (data.startsWith(prefix)) {
|
|
117
|
+
ctx.params = { ...ctx.params, wildcard: data.slice(prefix.length) };
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
return data === pattern;
|
|
123
|
+
}
|
|
124
|
+
const match = data.match(pattern);
|
|
125
|
+
if (match) {
|
|
126
|
+
ctx.match = match;
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
53
130
|
}, ...middleware);
|
|
54
131
|
}
|
|
55
132
|
chat(chatId, ...middleware) {
|
|
@@ -58,6 +135,19 @@ class Router {
|
|
|
58
135
|
return id !== undefined && (id === chatId || String(id) === String(chatId));
|
|
59
136
|
}, ...middleware);
|
|
60
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Registers handlers for update types, with optional payload narrowing:
|
|
140
|
+
* `on("message")`, `on("message:photo")`, `on("callback_query:data")`,
|
|
141
|
+
* or an array such as `["message:text", "callback_query:data"]`.
|
|
142
|
+
*/
|
|
143
|
+
on(filter, ...middleware) {
|
|
144
|
+
const filters = (Array.isArray(filter) ? filter : [filter]).map((value) => String(value));
|
|
145
|
+
if (filters.length === 0)
|
|
146
|
+
throw new Error("At least one update filter is required.");
|
|
147
|
+
for (const value of filters)
|
|
148
|
+
assertValidFilter(value);
|
|
149
|
+
return this.route((ctx) => filters.some((value) => matchesFilter(ctx, value)), ...middleware);
|
|
150
|
+
}
|
|
61
151
|
predicate(matcher, ...middleware) {
|
|
62
152
|
return this.route(matcher, ...middleware);
|
|
63
153
|
}
|
|
@@ -67,18 +157,30 @@ class Router {
|
|
|
67
157
|
});
|
|
68
158
|
}
|
|
69
159
|
async handle(ctx, terminal) {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
160
|
+
const routeDispatcher = async (currentCtx, next) => {
|
|
161
|
+
const ordered = [...this.routes].sort((left, right) => left.priority - right.priority);
|
|
162
|
+
let matched = false;
|
|
163
|
+
for (const route of ordered) {
|
|
164
|
+
if (!(await route.matcher(currentCtx)))
|
|
165
|
+
continue;
|
|
166
|
+
matched = true;
|
|
167
|
+
await (0, compose_js_1.compose)(route.middleware)(currentCtx);
|
|
168
|
+
if (this.matchMode === "first")
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
if (!matched) {
|
|
172
|
+
if (terminal)
|
|
173
|
+
await terminal();
|
|
174
|
+
else
|
|
175
|
+
await next();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
if (this.middlewares.length === 0) {
|
|
179
|
+
await routeDispatcher(ctx, async () => { });
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
await (0, compose_js_1.compose)([...this.middlewares, routeDispatcher])(ctx);
|
|
79
183
|
}
|
|
80
|
-
if (!matched)
|
|
81
|
-
await terminal?.();
|
|
82
184
|
}
|
|
83
185
|
}
|
|
84
186
|
exports.Router = Router;
|
|
@@ -3,32 +3,35 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.validators = exports.Form = void 0;
|
|
4
4
|
class Form {
|
|
5
5
|
fields = new Map();
|
|
6
|
-
values = {};
|
|
7
6
|
field(definition) { this.fields.set(definition.name, definition); return this; }
|
|
8
|
-
async parse(input) {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
async parse(input) {
|
|
8
|
+
const issues = [];
|
|
9
|
+
const values = {};
|
|
10
|
+
for (const [name, field] of this.fields) {
|
|
11
|
+
const raw = input[name];
|
|
12
|
+
if ((raw === undefined || raw === null || raw === "") && field.required) {
|
|
13
|
+
issues.push({ path: name, message: "Field is required", code: "required" });
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (raw === undefined || raw === null || raw === "")
|
|
17
|
+
continue;
|
|
18
|
+
try {
|
|
19
|
+
let value = field.parse(raw);
|
|
20
|
+
if (field.transform)
|
|
21
|
+
value = await field.transform(value);
|
|
22
|
+
const error = await field.validate?.(value);
|
|
23
|
+
if (error)
|
|
24
|
+
issues.push({ path: name, message: error, code: "invalid" });
|
|
25
|
+
else
|
|
26
|
+
values[name] = value;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
issues.push({ path: name, message: error instanceof Error ? error.message : "Invalid value", code: "parse" });
|
|
30
|
+
}
|
|
13
31
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
let value = field.parse(raw);
|
|
18
|
-
if (field.transform)
|
|
19
|
-
value = await field.transform(value);
|
|
20
|
-
const error = await field.validate?.(value);
|
|
21
|
-
if (error)
|
|
22
|
-
issues.push({ path: name, message: error, code: "invalid" });
|
|
23
|
-
else
|
|
24
|
-
this.values[name] = value;
|
|
25
|
-
}
|
|
26
|
-
catch (error) {
|
|
27
|
-
issues.push({ path: name, message: error instanceof Error ? error.message : "Invalid value", code: "parse" });
|
|
28
|
-
}
|
|
29
|
-
} return issues.length ? { success: false, issues } : { success: true, data: this.values }; }
|
|
30
|
-
reset() { for (const key of Object.keys(this.values))
|
|
31
|
-
delete this.values[key]; }
|
|
32
|
+
return issues.length ? { success: false, issues } : { success: true, data: values };
|
|
33
|
+
}
|
|
34
|
+
reset() { }
|
|
32
35
|
}
|
|
33
36
|
exports.Form = Form;
|
|
34
37
|
exports.validators = { string: (value) => { if (typeof value !== "string")
|
|
@@ -23,6 +23,20 @@ class MemoryStorage {
|
|
|
23
23
|
}
|
|
24
24
|
return entry.value;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Reads the raw entry including expiry metadata. Intended for storage adapters
|
|
28
|
+
* (such as `JsonFileStorage`) that need to persist TTL information across restarts.
|
|
29
|
+
*/
|
|
30
|
+
async readEntry(key) {
|
|
31
|
+
const entry = this.valuesMap.get(key);
|
|
32
|
+
if (!entry)
|
|
33
|
+
return undefined;
|
|
34
|
+
if (entry.expiresAt !== undefined && entry.expiresAt <= Date.now()) {
|
|
35
|
+
this.valuesMap.delete(key);
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
return entry.expiresAt === undefined ? { value: entry.value } : { value: entry.value, expiresAt: entry.expiresAt };
|
|
39
|
+
}
|
|
26
40
|
async set(key, value, options = {}) {
|
|
27
41
|
const expiresAt = expiration(options.ttlMs);
|
|
28
42
|
this.valuesMap.set(key, expiresAt === undefined ? { value } : { value, expiresAt });
|
|
@@ -84,8 +98,12 @@ class JsonFileStorage {
|
|
|
84
98
|
await this.ready;
|
|
85
99
|
this.writeChain = this.writeChain.then(async () => {
|
|
86
100
|
const output = {};
|
|
87
|
-
for await (const
|
|
88
|
-
|
|
101
|
+
for await (const key of this.memory.keys()) {
|
|
102
|
+
const entry = await this.memory.readEntry(key);
|
|
103
|
+
if (entry === undefined)
|
|
104
|
+
continue;
|
|
105
|
+
output[key] = entry.expiresAt === undefined ? { value: entry.value } : { value: entry.value, expiresAt: entry.expiresAt };
|
|
106
|
+
}
|
|
89
107
|
await (0, promises_1.mkdir)((0, node_path_1.dirname)(this.filePath), { recursive: true });
|
|
90
108
|
const temporary = `${this.filePath}.${process.pid}.tmp`;
|
|
91
109
|
await (0, promises_1.writeFile)(temporary, JSON.stringify(output, null, 2), "utf8");
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal concurrency primitives with zero dependencies.
|
|
4
|
+
*
|
|
5
|
+
* These helpers never add proactive delays: they only cap how many async tasks
|
|
6
|
+
* run at the same time. `Infinity` means fully parallel execution.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.Limiter = void 0;
|
|
10
|
+
exports.validateConcurrency = validateConcurrency;
|
|
11
|
+
exports.mapWithConcurrency = mapWithConcurrency;
|
|
12
|
+
function validateConcurrency(value, label = "concurrency") {
|
|
13
|
+
if (value === Infinity)
|
|
14
|
+
return;
|
|
15
|
+
if (!Number.isInteger(value) || value < 1)
|
|
16
|
+
throw new RangeError(`${label} must be a positive integer or Infinity`);
|
|
17
|
+
}
|
|
18
|
+
/** Promise semaphore: runs tasks immediately while a slot is free, queues the rest in FIFO order. */
|
|
19
|
+
class Limiter {
|
|
20
|
+
limit;
|
|
21
|
+
active = 0;
|
|
22
|
+
waiters = [];
|
|
23
|
+
constructor(limit) {
|
|
24
|
+
this.limit = limit;
|
|
25
|
+
validateConcurrency(limit);
|
|
26
|
+
}
|
|
27
|
+
/** Number of tasks currently running. */
|
|
28
|
+
get activeCount() { return this.active; }
|
|
29
|
+
/** Number of tasks waiting for a free slot. */
|
|
30
|
+
get queuedCount() { return this.waiters.length; }
|
|
31
|
+
async run(task) {
|
|
32
|
+
if (this.active >= this.limit)
|
|
33
|
+
await new Promise((resolve) => { this.waiters.push(resolve); });
|
|
34
|
+
this.active += 1;
|
|
35
|
+
try {
|
|
36
|
+
return await task();
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
this.active -= 1;
|
|
40
|
+
this.waiters.shift()?.();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.Limiter = Limiter;
|
|
45
|
+
/**
|
|
46
|
+
* Maps items through an async worker with a concurrency cap.
|
|
47
|
+
* Results keep the input order; `Infinity` runs everything in parallel.
|
|
48
|
+
*/
|
|
49
|
+
async function mapWithConcurrency(items, limit, worker) {
|
|
50
|
+
validateConcurrency(limit);
|
|
51
|
+
const results = new Array(items.length);
|
|
52
|
+
const limiter = new Limiter(limit);
|
|
53
|
+
await Promise.all(items.map((item, index) => limiter.run(async () => {
|
|
54
|
+
results[index] = await worker(item, index);
|
|
55
|
+
})));
|
|
56
|
+
return results;
|
|
57
|
+
}
|
|
Binary file
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createWebhookHandler = createWebhookHandler;
|
|
4
|
+
exports.webhookCallback = webhookCallback;
|
|
4
5
|
const node_crypto_1 = require("node:crypto");
|
|
5
6
|
function createWebhookHandler(bot, options = {}) {
|
|
6
7
|
const maxBodyBytes = options.maxBodyBytes ?? 1_048_576;
|
|
@@ -31,6 +32,91 @@ function createWebhookHandler(bot, options = {}) {
|
|
|
31
32
|
}
|
|
32
33
|
};
|
|
33
34
|
}
|
|
35
|
+
function webhookCallback(bot, _framework = "express", options = {}) {
|
|
36
|
+
const maxBodyBytes = options.maxBodyBytes ?? 1_048_576;
|
|
37
|
+
return async (req, res) => {
|
|
38
|
+
const rawReq = req;
|
|
39
|
+
const rawRes = res;
|
|
40
|
+
const sendResponse = (status, message) => {
|
|
41
|
+
if (rawRes && typeof rawRes.writeHead === "function" && typeof rawRes.end === "function") {
|
|
42
|
+
rawRes.writeHead(status, { "Content-Type": "text/plain" });
|
|
43
|
+
rawRes.end(message);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const expressLike = rawRes;
|
|
47
|
+
if (typeof expressLike?.status === "function" && typeof expressLike.send === "function") {
|
|
48
|
+
expressLike.status(status).send(message);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const koaLike = rawRes;
|
|
52
|
+
if (koaLike !== undefined) {
|
|
53
|
+
koaLike.status = status;
|
|
54
|
+
koaLike.body = message;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
if (rawReq.method !== "POST") {
|
|
58
|
+
sendResponse(405, "Method Not Allowed");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (options.secretToken) {
|
|
62
|
+
if (!secureEqual(readHeader(rawReq, "x-telegram-bot-api-secret-token"), options.secretToken)) {
|
|
63
|
+
sendResponse(401, "Unauthorized");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
let update;
|
|
69
|
+
if (typeof rawReq.arrayBuffer === "function") {
|
|
70
|
+
// Web-standard Request objects (e.g. Deno, Bun, WinterCG runtimes, or
|
|
71
|
+
// a converted fetch Request) expose the body through arrayBuffer().
|
|
72
|
+
// Checked before `body` because a Request's `body` is a ReadableStream.
|
|
73
|
+
const raw = await rawReq.arrayBuffer();
|
|
74
|
+
if (raw.byteLength > maxBodyBytes) {
|
|
75
|
+
sendResponse(413, "Payload Too Large");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
update = JSON.parse(new TextDecoder().decode(raw));
|
|
79
|
+
}
|
|
80
|
+
else if (rawReq.body && typeof rawReq.body === "object") {
|
|
81
|
+
update = rawReq.body;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const chunks = [];
|
|
85
|
+
let totalBytes = 0;
|
|
86
|
+
for await (const chunk of rawReq) {
|
|
87
|
+
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
88
|
+
totalBytes += buffer.length;
|
|
89
|
+
if (totalBytes > maxBodyBytes) {
|
|
90
|
+
sendResponse(413, "Payload Too Large");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
chunks.push(buffer);
|
|
94
|
+
}
|
|
95
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
96
|
+
update = JSON.parse(raw);
|
|
97
|
+
}
|
|
98
|
+
if (!Number.isInteger(update?.update_id)) {
|
|
99
|
+
sendResponse(400, "Bad Request");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
await bot.handleUpdate(update);
|
|
103
|
+
sendResponse(200, "OK");
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
await options.onError?.(error);
|
|
107
|
+
sendResponse(500, "Internal Server Error");
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function readHeader(req, name) {
|
|
112
|
+
const headers = req.headers;
|
|
113
|
+
if (!headers || typeof headers !== "object")
|
|
114
|
+
return "";
|
|
115
|
+
if (typeof headers.get === "function")
|
|
116
|
+
return headers.get(name) ?? "";
|
|
117
|
+
const value = headers[name];
|
|
118
|
+
return Array.isArray(value) ? value[0] ?? "" : value ?? "";
|
|
119
|
+
}
|
|
34
120
|
function secureEqual(left, right) {
|
|
35
121
|
const leftBuffer = Buffer.from(left, "utf8");
|
|
36
122
|
const rightBuffer = Buffer.from(right, "utf8");
|
package/docs/API.id.md
CHANGED
|
@@ -68,11 +68,13 @@ type BotStatus =
|
|
|
68
68
|
| `transportOptions` | `Omit<FetchTransportOptions, "baseUrl">` | `{}` | Timeout, retry, backoff, jitter, headers, dan fetch implementation. |
|
|
69
69
|
| `session` | `Storage<string, S>` | storage baru | Penyimpanan session berdasarkan kunci chat/user; dapat memakai adapter persistent. |
|
|
70
70
|
| `services` | `Record<string, unknown>` | `{}` | Dependency/service yang tersedia melalui `ctx.services`. |
|
|
71
|
+
| `branding` | `boolean` | `true` | Pengalaman startup terminal: efek ketik, glass progress bar, banner rainbow animasi `Tele Bibz`, dan baris update yang mudah dibaca. Hanya dirender pada TTY interaktif. |
|
|
71
72
|
| `polling.timeout` | `number` | `30` | Long-poll timeout dalam detik untuk `getUpdates`. |
|
|
72
73
|
| `polling.limit` | `number` | `100` | Jumlah maksimum update per request polling. |
|
|
73
74
|
| `polling.allowedUpdates` | `string[]` | `[]` | Filter update Telegram. |
|
|
74
75
|
| `polling.retryDelayMs` | `number` | `500` | Delay awal ketika polling gagal. |
|
|
75
76
|
| `polling.maxRetryDelayMs` | `number` | `30000` | Batas maksimum delay reconnect. |
|
|
77
|
+
| `updates.concurrency` | `number` | `Infinity` | Batas jumlah update yang diproses bersamaan. Update selalu berjalan paralel antar chat dan tetap berurutan di dalam satu chat, sehingga burst 1000+ pesan tertangani sekaligus. |
|
|
76
78
|
|
|
77
79
|
### Konstruktor `Bot`
|
|
78
80
|
|
|
@@ -140,6 +142,30 @@ onRegex(expression: RegExp, handler: Middleware<Context<S>>): this
|
|
|
140
142
|
|
|
141
143
|
Menangani message text menggunakan `RegExp`. Parameter route tidak diekstrak otomatis ke `ctx.params`; gunakan predicate atau middleware custom jika memerlukan ekstraksi.
|
|
142
144
|
|
|
145
|
+
### `bot.on(filter, handler)`
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
on(filter: UpdateFilter | UpdateFilter[], handler: Middleware<Context<S>>): this
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Mendaftarkan handler untuk tipe update, opsional dipersempit dengan field payload. Contoh: `"message"`, `"message:text"`, `"message:photo"`, `"edited_message"`, `"channel_post"`, `"callback_query"`, `"callback_query:data"`, `"inline_query"`, `"chat_member"`, `"message_reaction"`, atau array seperti `["message:text", "callback_query:data"]`. Tipe update tidak valid melempar `TypeError` saat registrasi.
|
|
152
|
+
|
|
153
|
+
### `bot.hears(trigger, handler)`
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
hears(trigger: string | RegExp, handler: Middleware<Context<S>>): this
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Menangani message text yang sama persis (string) atau yang cocok dengan `RegExp`.
|
|
160
|
+
|
|
161
|
+
### `bot.catch(handler)`
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
catch(handler: (error: unknown, ctx: Context<S>) => void | Promise<void>): this
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Mendaftarkan error boundary untuk handler update. Jika dipasang, kegagalan handler dicatat, dipancarkan sebagai `update:error`/`bot:error`, dan diteruskan ke handler ini alih-alih menolak `handleUpdate()` — webhook menjawab `200` dan polling berlanjut. Tanpa boundary, error dilempar ulang.
|
|
168
|
+
|
|
143
169
|
### `bot.usePlugin(plugin)`
|
|
144
170
|
|
|
145
171
|
```ts
|
|
@@ -176,7 +202,7 @@ launch(options?: {
|
|
|
176
202
|
}): Promise<void>
|
|
177
203
|
```
|
|
178
204
|
|
|
179
|
-
Menjalankan bot dalam mode polling. Saat mulai, lifecycle berpindah melalui `starting` lalu `running`, kemudian loop `getUpdates()` memproses setiap update secara
|
|
205
|
+
Menjalankan bot dalam mode polling. Saat mulai, lifecycle berpindah melalui `starting` lalu `running`, kemudian loop `getUpdates()` memproses setiap batch update secara konkuren: update dari chat berbeda berjalan paralel, sedangkan update dari chat yang sama menjaga urutan kedatangannya. Kegagalan polling memancarkan `polling:reconnect` dan menggunakan backoff eksponensial.
|
|
180
206
|
|
|
181
207
|
Mode selain `"polling"` melempar error dan menyarankan penggunaan `createWebhookHandler()` untuk webhook.
|
|
182
208
|
|
|
@@ -253,8 +279,54 @@ handleUpdate(update: Update): Promise<void>
|
|
|
253
279
|
|
|
254
280
|
Memproses satu update secara manual. Method menentukan kunci session dari `chat.id` dan `from.id`, membuat `Context`, memancarkan event `update` dan `message`, menjalankan middleware lalu router, dan menyimpan session setelah pipeline selesai.
|
|
255
281
|
|
|
282
|
+
Update dari chat berbeda diproses paralel; update dari chat yang sama diserialisasi sesuai urutan kedatangan, sehingga session, wizard, dan conversation tidak pernah saling tumpang tindih dan penulisan session tidak pernah hilang. Burst update konkuren hanya memicu satu inisialisasi `getMe`.
|
|
283
|
+
|
|
256
284
|
Error pipeline mengubah status bot menjadi `error`, memancarkan `bot:error`, lalu dilempar kembali.
|
|
257
285
|
|
|
286
|
+
### `bot.handleUpdates(updates)`
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
handleUpdates(updates: readonly Update[]): Promise<void>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Menangani satu batch update sekaligus: setiap chat dalam batch langsung diproses — paralel antar chat, berurutan per chat — sehingga burst 1000 pesan tidak pernah terhambat oleh satu handler yang lambat. Kegagalan handler individual dicatat ke log, dipancarkan sebagai `update:error`, dan diteruskan ke error boundary `catch()`; kegagalan tersebut tidak pernah menolak promise ini. Loop polling memakai method ini untuk setiap batch `getUpdates`.
|
|
293
|
+
|
|
294
|
+
### `bot.broadcast(chatIds, send, options?)`
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
broadcast(
|
|
298
|
+
chatIds: readonly ChatId[],
|
|
299
|
+
send: (chatId: ChatId) => Promise<unknown>,
|
|
300
|
+
options?: BroadcastOptions,
|
|
301
|
+
): Promise<BroadcastReport>
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Mengirim ke banyak chat secara paralel — dibuat untuk broadcast ke 1000+ user. Tidak ada cooldown proaktif: semua chat langsung dicoba sekaligus (sampai `options.concurrency`, default `Infinity`). Ketika Telegram menjawab 429, pengiriman otomatis diulang setelah tepat delay `retry_after` yang diperintahkan Telegram (maksimal `options.maxAttempts`, default `10`), sehingga burst tetap terkirim lengkap, bukan gagal. Error yang tidak bisa di-retry (misalnya chat yang tidak bisa dihubungi bot) dicatat per chat pada laporan yang dikembalikan.
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
const report = await bot.broadcast(
|
|
308
|
+
subscriberIds,
|
|
309
|
+
(chatId) => bot.api.methods.sendMessage({ chat_id: chatId, text: "Newsletter #42" }),
|
|
310
|
+
{ onProgress: (progress) => console.log(`${progress.delivered}/${progress.total} terkirim`) },
|
|
311
|
+
);
|
|
312
|
+
console.log(`Terkirim ${report.delivered} dari ${report.total} dalam ${report.durationMs}ms`);
|
|
313
|
+
for (const failure of report.failures) console.warn(`Gagal: ${failure.chatId} — ${failure.error}`);
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
#### `BroadcastOptions` dan `BroadcastReport`
|
|
317
|
+
|
|
318
|
+
| Properti | Tipe | Default | Deskripsi |
|
|
319
|
+
|---|---|---:|---|
|
|
320
|
+
| `BroadcastOptions.concurrency` | `number` | `Infinity` | Berapa chat dikirimi pesan secara bersamaan. |
|
|
321
|
+
| `BroadcastOptions.maxAttempts` | `number` | `10` | Percobaan per chat ketika Telegram menjawab 429. |
|
|
322
|
+
| `BroadcastOptions.onProgress` | `(progress: BroadcastProgress) => void` | — | Dipanggil setelah setiap chat selesai. |
|
|
323
|
+
| `BroadcastOptions.signal` | `AbortSignal` | — | Membatalkan pengiriman tertunda; pesan yang sudah terkirim tetap terkirim. |
|
|
324
|
+
| `BroadcastReport.total` | `number` | — | Jumlah chat dalam sesi broadcast. |
|
|
325
|
+
| `BroadcastReport.delivered` | `number` | — | Chat yang menerima pesan. |
|
|
326
|
+
| `BroadcastReport.failed` | `number` | — | Chat yang tidak menerima. |
|
|
327
|
+
| `BroadcastReport.durationMs` | `number` | — | Durasi total sesi broadcast. |
|
|
328
|
+
| `BroadcastReport.failures` | `BroadcastFailure[]` | — | Catatan per chat `{ chatId, attempts, error, errorKind }`. |
|
|
329
|
+
|
|
258
330
|
### Contoh bot minimal
|
|
259
331
|
|
|
260
332
|
```ts
|
|
@@ -391,6 +463,7 @@ interface Transport {
|
|
|
391
463
|
| `backoffMs` | `250` | Delay exponential awal. |
|
|
392
464
|
| `maxBackoffMs` | `8000` | Batas delay transport. |
|
|
393
465
|
| `jitter` | `0.2` | Variasi acak ±20% dari exponential delay. |
|
|
466
|
+
| `floodGate` | `true` | Ketika Telegram menjawab 429, permintaan BARU ditunda sampai jendela `retry_after` yang diperintahkan Telegram berlalu. Bukan cooldown proaktif — penundaan satu-satunya hanyalah yang diminta Telegram sendiri. |
|
|
394
467
|
| `headers` | `{}` | Header tambahan. |
|
|
395
468
|
|
|
396
469
|
### `new FetchTransport(options?)`
|
|
@@ -633,6 +706,22 @@ new Context<S>(options: ContextOptions<S>): Context<S>
|
|
|
633
706
|
| `send` | `send(text, extra?): Promise<Message>` | Mengirim message ke chat update tanpa reply reference. |
|
|
634
707
|
| `edit` | `edit(text, extra?): Promise<Message \| true>` | Mengedit message update menggunakan `editMessageText`. |
|
|
635
708
|
| `delete` | `delete(): Promise<true>` | Menghapus message update. |
|
|
709
|
+
| `replyWithHTML` | `replyWithHTML(text, extra?): Promise<Message>` | Membalas dengan `parse_mode: "HTML"`. |
|
|
710
|
+
| `replyWithMarkdown` | `replyWithMarkdown(text, extra?): Promise<Message>` | Membalas dengan `parse_mode: "MarkdownV2"`. |
|
|
711
|
+
| `replyWithPhoto` | `replyWithPhoto(photo, extra?): Promise<Message>` | Mengirim `sendPhoto` dengan quote-reply otomatis. |
|
|
712
|
+
| `replyWithDocument` | `replyWithDocument(document, extra?): Promise<Message>` | Mengirim `sendDocument` dengan quote-reply otomatis. |
|
|
713
|
+
| `replyWithAudio` | `replyWithAudio(audio, extra?): Promise<Message>` | Mengirim `sendAudio` dengan quote-reply otomatis. |
|
|
714
|
+
| `replyWithVideo` | `replyWithVideo(video, extra?): Promise<Message>` | Mengirim `sendVideo` dengan quote-reply otomatis. |
|
|
715
|
+
| `replyWithVoice` | `replyWithVoice(voice, extra?): Promise<Message>` | Mengirim `sendVoice` dengan quote-reply otomatis. |
|
|
716
|
+
| `replyWithAnimation` | `replyWithAnimation(animation, extra?): Promise<Message>` | Mengirim `sendAnimation` dengan quote-reply otomatis. |
|
|
717
|
+
| `replyWithVideoNote` | `replyWithVideoNote(videoNote, extra?): Promise<Message>` | Mengirim `sendVideoNote` dengan quote-reply otomatis. |
|
|
718
|
+
| `replyWithSticker` | `replyWithSticker(sticker, extra?): Promise<Message>` | Mengirim `sendSticker` dengan quote-reply otomatis. |
|
|
719
|
+
| `replyWithMediaGroup` | `replyWithMediaGroup(media, extra?): Promise<Message[]>` | Mengirim album via `sendMediaGroup` dengan quote-reply otomatis. |
|
|
720
|
+
| `replyWithLocation` | `replyWithLocation(latitude, longitude, extra?): Promise<Message>` | Mengirim `sendLocation` dengan quote-reply otomatis. |
|
|
721
|
+
| `replyWithVenue` | `replyWithVenue(latitude, longitude, title, address, extra?): Promise<Message>` | Mengirim `sendVenue` dengan quote-reply otomatis. |
|
|
722
|
+
| `replyWithContact` | `replyWithContact(phoneNumber, firstName, extra?): Promise<Message>` | Mengirim `sendContact` dengan quote-reply otomatis. |
|
|
723
|
+
| `replyWithPoll` | `replyWithPoll(question, options, extra?): Promise<Message>` | Mengirim `sendPoll` dengan quote-reply otomatis. |
|
|
724
|
+
| `replyWithDice` | `replyWithDice(emoji?, extra?): Promise<Message>` | Mengirim `sendDice` dengan quote-reply otomatis. |
|
|
636
725
|
| `copy` | `copy(fromChatId, messageId, extra?): Promise<unknown>` | Memanggil `copyMessage` ke chat context. |
|
|
637
726
|
| `forward` | `forward(fromChatId, messageId, extra?): Promise<Message>` | Memanggil `forwardMessage` ke chat context. |
|
|
638
727
|
| `pin` | `pin(messageId?, extra?): Promise<true>` | Memanggil `pinChatMessage`, default message id dari context. |
|
|
@@ -645,7 +734,7 @@ new Context<S>(options: ContextOptions<S>): Context<S>
|
|
|
645
734
|
| `getFile` | `getFile(fileId): Promise<unknown>` | Mengambil file berdasarkan id. |
|
|
646
735
|
| `withReplyMarkup` | `withReplyMarkup(markup): this` | Menyimpan markup di `ctx.state.reply_markup` dan mengembalikan context. Metode ini tidak otomatis mengirim message. |
|
|
647
736
|
|
|
648
|
-
`reply`, `send`, `getChat`, dan beberapa helper lain melempar error ketika update tidak memiliki chat yang diperlukan. `edit` dan `delete` membutuhkan chat serta message.
|
|
737
|
+
Semua pengirim `replyWith*` menerima parameter native Telegram sebagai `extra` dan otomatis me-quote message yang masuk. `reply_parameters` pada `extra` digabung dengan `message_id` otomatis, bukan menggantikannya. `reply`, `send`, `getChat`, dan beberapa helper lain melempar error ketika update tidak memiliki chat yang diperlukan. `edit` dan `delete` membutuhkan chat serta message.
|
|
649
738
|
|
|
650
739
|
---
|
|
651
740
|
|
|
@@ -956,6 +1045,20 @@ new Scheduler(): Scheduler
|
|
|
956
1045
|
|
|
957
1046
|
Format cron penuh tidak didukung oleh built-in scheduler. Ekspresi selain `*/N` melempar `Error`.
|
|
958
1047
|
|
|
1048
|
+
### `Limiter` dan `mapWithConcurrency`
|
|
1049
|
+
|
|
1050
|
+
```ts
|
|
1051
|
+
new Limiter(limit: number): Limiter
|
|
1052
|
+
|
|
1053
|
+
mapWithConcurrency<T, R>(
|
|
1054
|
+
items: readonly T[],
|
|
1055
|
+
limit: number,
|
|
1056
|
+
worker: (item: T, index: number) => Promise<R>,
|
|
1057
|
+
): Promise<R[]>
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
`Limiter` adalah semaphore berbasis promise: task langsung berjalan selama ada slot kosong dan mengantre FIFO setelahnya. `limit` menerima bilangan bulat positif atau `Infinity` (sepenuhnya paralel — default library). `mapWithConcurrency` memetakan item melalui async worker dengan batas yang sama sambil menjaga urutan hasil. Primitif ini tidak menambahkan delay apa pun — hanya membatasi jumlah task yang berjalan bersamaan. `Limiter` mengekspos `activeCount` dan `queuedCount` untuk observability.
|
|
1061
|
+
|
|
959
1062
|
## 9. Plugin dan services
|
|
960
1063
|
|
|
961
1064
|
### `Plugin<Context>`
|
|
@@ -1263,7 +1366,20 @@ new Menu(id: string): Menu
|
|
|
1263
1366
|
|
|
1264
1367
|
## 12. Logging Terminal
|
|
1265
1368
|
|
|
1266
|
-
|
|
1369
|
+
Saat stdout adalah TTY interaktif, setiap `bot.start()` / `bot.launch()` memainkan urutan startup: efek ketik `Installing Dependencies......`, glass progress bar dengan kilau menyapu, dan banner ASCII rainbow animasi `Tele Bibz` (font figlet `Speed`) yang terus mengalir sampai bot terhubung, lalu diam dengan `✓ Connected as @<username>`.
|
|
1370
|
+
|
|
1371
|
+
Setiap update yang ditangani bot dicatat dalam baris yang mudah dibaca:
|
|
1372
|
+
|
|
1373
|
+
```text
|
|
1374
|
+
[ => ] Message From 123456789 John Doe 29/08/2026 15:04:05
|
|
1375
|
+
↳ Text: /start
|
|
1376
|
+
[ => ] Callback From 123456789 John Doe 29/08/2026 15:04:07
|
|
1377
|
+
↳ Data: menu:open
|
|
1378
|
+
```
|
|
1379
|
+
|
|
1380
|
+
Teks pesan/command dibatasi 50 karakter; data tombol callback ditampilkan penuh. Error dicetak merah lengkap dengan stack. Nonaktifkan dengan `branding: false` pada `Bot`, atau set `logger.format: "json"` untuk log terstruktur — pada mode itu update masuk dikeluarkan sebagai entry `update.received`. Stdout non-interaktif (pipe, Docker, CI) otomatis fallback ke teks polos tanpa animasi.
|
|
1381
|
+
|
|
1382
|
+
Helper branding tambahan yang diekspor untuk aplikasi: `runStartupSequence()`, `startTeleBibzBanner()`, `printTeleBibzBanner()`, `paintRainbow()`, dan `printStatusLine()`.
|
|
1267
1383
|
|
|
1268
1384
|
## 13. Utilitas Teks
|
|
1269
1385
|
|
|
@@ -1691,7 +1807,7 @@ Package memvendorkan declaration Telegram berlisensi MIT dan mengeksposnya sebag
|
|
|
1691
1807
|
|
|
1692
1808
|
## 19. Kompatibilitas dan batasan yang perlu diketahui
|
|
1693
1809
|
|
|
1694
|
-
Perpustakaan menargetkan Node.js `>=
|
|
1810
|
+
Perpustakaan menargetkan Node.js `>=22`, menggunakan ESM sebagai module utama, serta menyediakan build CommonJS. Webhook membutuhkan runtime yang menyediakan Web `Request`, `Response`, `Headers`, `FormData`, `Blob`, dan `AbortController`; Node.js modern menyediakannya secara native.
|
|
1695
1811
|
|
|
1696
1812
|
Daftar method yang dihasilkan API dan peta method API bukanlah hal yang sama. `TelegramMethodName` mencakup 184 nama runtime, tetapi `TelegramMethodMap` hanya memiliki parameter/hasil yang bertipe khusus untuk subset yang tercantum pada bagian API client. Untuk method lain, gunakan `api.raw()` atau tambahkan deklarasi tipe di sisi aplikasi.
|
|
1697
1813
|
|