@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.
Files changed (95) hide show
  1. package/CHANGELOG.md +42 -8
  2. package/CONTRIBUTING.md +2 -2
  3. package/README.id.md +44 -5
  4. package/README.md +45 -6
  5. package/README.zh-CN.md +44 -5
  6. package/RELEASE_AUTOMATION.md +17 -5
  7. package/bin/telebibz.mjs +1 -1
  8. package/dist/src/api/client.d.ts +3 -1
  9. package/dist/src/api/client.d.ts.map +1 -1
  10. package/dist/src/api/client.js +3 -1
  11. package/dist/src/api/client.js.map +1 -1
  12. package/dist/src/api/transport.d.ts +18 -1
  13. package/dist/src/api/transport.d.ts.map +1 -1
  14. package/dist/src/api/transport.js +26 -2
  15. package/dist/src/api/transport.js.map +1 -1
  16. package/dist/src/branding/terminal.d.ts +62 -0
  17. package/dist/src/branding/terminal.d.ts.map +1 -1
  18. package/dist/src/branding/terminal.js +258 -0
  19. package/dist/src/branding/terminal.js.map +1 -1
  20. package/dist/src/broadcast/broadcast.d.ts +50 -0
  21. package/dist/src/broadcast/broadcast.d.ts.map +1 -0
  22. package/dist/src/broadcast/broadcast.js +56 -0
  23. package/dist/src/broadcast/broadcast.js.map +1 -0
  24. package/dist/src/cli.d.ts.map +1 -1
  25. package/dist/src/cli.js +7 -3
  26. package/dist/src/cli.js.map +1 -1
  27. package/dist/src/context/context.d.ts +24 -1
  28. package/dist/src/context/context.d.ts.map +1 -1
  29. package/dist/src/context/context.js +102 -8
  30. package/dist/src/context/context.js.map +1 -1
  31. package/dist/src/core/bot.d.ts +61 -2
  32. package/dist/src/core/bot.d.ts.map +1 -1
  33. package/dist/src/core/bot.js +180 -31
  34. package/dist/src/core/bot.js.map +1 -1
  35. package/dist/src/index.d.ts +2 -0
  36. package/dist/src/index.d.ts.map +1 -1
  37. package/dist/src/index.js +2 -0
  38. package/dist/src/index.js.map +1 -1
  39. package/dist/src/keyboard/index.d.ts +12 -0
  40. package/dist/src/keyboard/index.d.ts.map +1 -1
  41. package/dist/src/keyboard/index.js +13 -1
  42. package/dist/src/keyboard/index.js.map +1 -1
  43. package/dist/src/observability/logger.d.ts +28 -1
  44. package/dist/src/observability/logger.d.ts.map +1 -1
  45. package/dist/src/observability/logger.js +110 -0
  46. package/dist/src/observability/logger.js.map +1 -1
  47. package/dist/src/plugins/plugin.d.ts +2 -0
  48. package/dist/src/plugins/plugin.d.ts.map +1 -1
  49. package/dist/src/plugins/plugin.js +11 -4
  50. package/dist/src/plugins/plugin.js.map +1 -1
  51. package/dist/src/router/router.d.ts +19 -0
  52. package/dist/src/router/router.d.ts.map +1 -1
  53. package/dist/src/router/router.js +125 -23
  54. package/dist/src/router/router.js.map +1 -1
  55. package/dist/src/state/forms.d.ts +0 -1
  56. package/dist/src/state/forms.d.ts.map +1 -1
  57. package/dist/src/state/forms.js +27 -24
  58. package/dist/src/state/forms.js.map +1 -1
  59. package/dist/src/storage/storage.d.ts +10 -0
  60. package/dist/src/storage/storage.d.ts.map +1 -1
  61. package/dist/src/storage/storage.js +20 -2
  62. package/dist/src/storage/storage.js.map +1 -1
  63. package/dist/src/utils/concurrency.d.ts +25 -0
  64. package/dist/src/utils/concurrency.d.ts.map +1 -0
  65. package/dist/src/utils/concurrency.js +52 -0
  66. package/dist/src/utils/concurrency.js.map +1 -0
  67. package/dist/src/utils/text.d.ts +22 -0
  68. package/dist/src/utils/text.d.ts.map +1 -1
  69. package/dist/src/utils/text.js +0 -0
  70. package/dist/src/utils/text.js.map +1 -1
  71. package/dist/src/webhook/handler.d.ts +3 -0
  72. package/dist/src/webhook/handler.d.ts.map +1 -1
  73. package/dist/src/webhook/handler.js +85 -0
  74. package/dist/src/webhook/handler.js.map +1 -1
  75. package/dist-cjs/src/api/client.js +3 -1
  76. package/dist-cjs/src/api/transport.js +26 -2
  77. package/dist-cjs/src/branding/terminal.js +264 -1
  78. package/dist-cjs/src/broadcast/broadcast.js +58 -0
  79. package/dist-cjs/src/cli.js +7 -3
  80. package/dist-cjs/src/context/context.js +102 -8
  81. package/dist-cjs/src/core/bot.js +178 -29
  82. package/dist-cjs/src/index.js +2 -0
  83. package/dist-cjs/src/keyboard/index.js +13 -1
  84. package/dist-cjs/src/observability/logger.js +113 -1
  85. package/dist-cjs/src/plugins/plugin.js +11 -4
  86. package/dist-cjs/src/router/router.js +126 -24
  87. package/dist-cjs/src/state/forms.js +27 -24
  88. package/dist-cjs/src/storage/storage.js +20 -2
  89. package/dist-cjs/src/utils/concurrency.js +57 -0
  90. package/dist-cjs/src/utils/text.js +0 -0
  91. package/dist-cjs/src/webhook/handler.js +86 -0
  92. package/docs/API.id.md +120 -4
  93. package/docs/API.md +122 -4
  94. package/docs/API.zh-CN.md +119 -3
  95. 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.routes.push({ priority: -1_000_000 + this.sequence++, matcher: () => true, middleware });
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 command = text.slice(1).split(/[\s@]/, 1)[0] ?? "";
34
- return typeof name === "string"
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) => ctx.message?.text === value, ...middleware);
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) => testRegExp(expression, ctx.message?.text ?? ""), ...middleware);
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
- return typeof pattern === "string"
49
- ? pattern.endsWith("*")
50
- ? data.startsWith(pattern.slice(0, -1))
51
- : data === pattern
52
- : testRegExp(pattern, data);
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 ordered = [...this.routes].sort((left, right) => left.priority - right.priority);
71
- let matched = false;
72
- for (const route of ordered) {
73
- if (!(await route.matcher(ctx)))
74
- continue;
75
- matched = true;
76
- await (0, compose_js_1.compose)(route.middleware)(ctx);
77
- if (this.matchMode === "first")
78
- break;
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) { const issues = []; for (const [name, field] of this.fields) {
9
- const raw = input[name];
10
- if ((raw === undefined || raw === null || raw === "") && field.required) {
11
- issues.push({ path: name, message: "Field is required", code: "required" });
12
- continue;
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
- if (raw === undefined || raw === null || raw === "")
15
- continue;
16
- try {
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 [key, value] of this.memory.entries())
88
- output[key] = { value };
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 berurutan. Kegagalan polling memancarkan `polling:reconnect` dan menggunakan backoff eksponensial.
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
- This package starts directly after Telegram API connectivity is established. The terminal prints a boxed telebibz attribution, an animated startup status when attached to a TTY, and structured colorful logs for lifecycle, API, polling, webhook, and update events. Set logger format to `json` for machine ingestion.
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 `>=20`, 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.
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