@xbibzlibrary/telebibz 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/APPROVAL_FEATURE.md +73 -0
  2. package/CHANGELOG.md +21 -0
  3. package/LICENSE +21 -0
  4. package/README.md +171 -0
  5. package/SECURITY.md +15 -0
  6. package/bin/telebibz.mjs +3 -0
  7. package/dist/generated/api.d.ts +13 -0
  8. package/dist/generated/api.d.ts.map +1 -0
  9. package/dist/generated/api.js +192 -0
  10. package/dist/generated/api.js.map +1 -0
  11. package/dist/src/api/client.d.ts +32 -0
  12. package/dist/src/api/client.d.ts.map +1 -0
  13. package/dist/src/api/client.js +50 -0
  14. package/dist/src/api/client.js.map +1 -0
  15. package/dist/src/api/errors.d.ts +45 -0
  16. package/dist/src/api/errors.d.ts.map +1 -0
  17. package/dist/src/api/errors.js +65 -0
  18. package/dist/src/api/errors.js.map +1 -0
  19. package/dist/src/api/index.d.ts +5 -0
  20. package/dist/src/api/index.d.ts.map +1 -0
  21. package/dist/src/api/index.js +5 -0
  22. package/dist/src/api/index.js.map +1 -0
  23. package/dist/src/api/transport.d.ts +37 -0
  24. package/dist/src/api/transport.d.ts.map +1 -0
  25. package/dist/src/api/transport.js +134 -0
  26. package/dist/src/api/transport.js.map +1 -0
  27. package/dist/src/api/types.d.ts +441 -0
  28. package/dist/src/api/types.d.ts.map +1 -0
  29. package/dist/src/api/types.js +2 -0
  30. package/dist/src/api/types.js.map +1 -0
  31. package/dist/src/approval/approval.d.ts +63 -0
  32. package/dist/src/approval/approval.d.ts.map +1 -0
  33. package/dist/src/approval/approval.js +90 -0
  34. package/dist/src/approval/approval.js.map +1 -0
  35. package/dist/src/cache/cache.d.ts +33 -0
  36. package/dist/src/cache/cache.d.ts.map +1 -0
  37. package/dist/src/cache/cache.js +37 -0
  38. package/dist/src/cache/cache.js.map +1 -0
  39. package/dist/src/cli.d.ts +2 -0
  40. package/dist/src/cli.d.ts.map +1 -0
  41. package/dist/src/cli.js +55 -0
  42. package/dist/src/cli.js.map +1 -0
  43. package/dist/src/context/context.d.ts +46 -0
  44. package/dist/src/context/context.d.ts.map +1 -0
  45. package/dist/src/context/context.js +63 -0
  46. package/dist/src/context/context.js.map +1 -0
  47. package/dist/src/core/bot.d.ts +76 -0
  48. package/dist/src/core/bot.d.ts.map +1 -0
  49. package/dist/src/core/bot.js +160 -0
  50. package/dist/src/core/bot.js.map +1 -0
  51. package/dist/src/core/events.d.ts +71 -0
  52. package/dist/src/core/events.d.ts.map +1 -0
  53. package/dist/src/core/events.js +24 -0
  54. package/dist/src/core/events.js.map +1 -0
  55. package/dist/src/index.d.ts +18 -0
  56. package/dist/src/index.d.ts.map +1 -0
  57. package/dist/src/index.js +18 -0
  58. package/dist/src/index.js.map +1 -0
  59. package/dist/src/keyboard/index.d.ts +34 -0
  60. package/dist/src/keyboard/index.d.ts.map +1 -0
  61. package/dist/src/keyboard/index.js +43 -0
  62. package/dist/src/keyboard/index.js.map +1 -0
  63. package/dist/src/middleware/compose.d.ts +5 -0
  64. package/dist/src/middleware/compose.d.ts.map +1 -0
  65. package/dist/src/middleware/compose.js +17 -0
  66. package/dist/src/middleware/compose.js.map +1 -0
  67. package/dist/src/plugins/plugin.d.ts +36 -0
  68. package/dist/src/plugins/plugin.d.ts.map +1 -0
  69. package/dist/src/plugins/plugin.js +31 -0
  70. package/dist/src/plugins/plugin.js.map +1 -0
  71. package/dist/src/queue/queue.d.ts +52 -0
  72. package/dist/src/queue/queue.d.ts.map +1 -0
  73. package/dist/src/queue/queue.js +82 -0
  74. package/dist/src/queue/queue.js.map +1 -0
  75. package/dist/src/router/router.d.ts +35 -0
  76. package/dist/src/router/router.d.ts.map +1 -0
  77. package/dist/src/router/router.js +23 -0
  78. package/dist/src/router/router.js.map +1 -0
  79. package/dist/src/state/conversation.d.ts +40 -0
  80. package/dist/src/state/conversation.d.ts.map +1 -0
  81. package/dist/src/state/conversation.js +38 -0
  82. package/dist/src/state/conversation.js.map +1 -0
  83. package/dist/src/state/forms.d.ts +35 -0
  84. package/dist/src/state/forms.d.ts.map +1 -0
  85. package/dist/src/state/forms.js +41 -0
  86. package/dist/src/state/forms.js.map +1 -0
  87. package/dist/src/state/menu.d.ts +30 -0
  88. package/dist/src/state/menu.d.ts.map +1 -0
  89. package/dist/src/state/menu.js +28 -0
  90. package/dist/src/state/menu.js.map +1 -0
  91. package/dist/src/storage/storage.d.ts +33 -0
  92. package/dist/src/storage/storage.d.ts.map +1 -0
  93. package/dist/src/storage/storage.js +51 -0
  94. package/dist/src/storage/storage.js.map +1 -0
  95. package/dist/src/testing.d.ts +17 -0
  96. package/dist/src/testing.d.ts.map +1 -0
  97. package/dist/src/testing.js +12 -0
  98. package/dist/src/testing.js.map +1 -0
  99. package/dist/src/utils/text.d.ts +17 -0
  100. package/dist/src/utils/text.d.ts.map +1 -0
  101. package/dist/src/utils/text.js +0 -0
  102. package/dist/src/utils/text.js.map +1 -0
  103. package/dist/src/webhook/handler.d.ts +8 -0
  104. package/dist/src/webhook/handler.d.ts.map +1 -0
  105. package/dist/src/webhook/handler.js +27 -0
  106. package/dist/src/webhook/handler.js.map +1 -0
  107. package/dist-cjs/generated/api.js +194 -0
  108. package/dist-cjs/package.json +3 -0
  109. package/dist-cjs/src/api/client.js +53 -0
  110. package/dist-cjs/src/api/errors.js +74 -0
  111. package/dist-cjs/src/api/index.js +20 -0
  112. package/dist-cjs/src/api/transport.js +137 -0
  113. package/dist-cjs/src/api/types.js +2 -0
  114. package/dist-cjs/src/approval/approval.js +94 -0
  115. package/dist-cjs/src/cache/cache.js +41 -0
  116. package/dist-cjs/src/cli.js +57 -0
  117. package/dist-cjs/src/context/context.js +66 -0
  118. package/dist-cjs/src/core/bot.js +163 -0
  119. package/dist-cjs/src/core/events.js +27 -0
  120. package/dist-cjs/src/index.js +33 -0
  121. package/dist-cjs/src/keyboard/index.js +49 -0
  122. package/dist-cjs/src/middleware/compose.js +20 -0
  123. package/dist-cjs/src/plugins/plugin.js +35 -0
  124. package/dist-cjs/src/queue/queue.js +86 -0
  125. package/dist-cjs/src/router/router.js +26 -0
  126. package/dist-cjs/src/state/conversation.js +43 -0
  127. package/dist-cjs/src/state/forms.js +44 -0
  128. package/dist-cjs/src/state/menu.js +33 -0
  129. package/dist-cjs/src/storage/storage.js +54 -0
  130. package/dist-cjs/src/testing.js +18 -0
  131. package/dist-cjs/src/utils/text.js +0 -0
  132. package/dist-cjs/src/webhook/handler.js +29 -0
  133. package/docs/README.md +21 -0
  134. package/package.json +78 -0
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TelegramNetworkError = exports.TelegramValidationError = exports.TelegramAuthError = exports.TelegramRateLimitError = exports.TelegramError = void 0;
4
+ exports.classifyTelegramError = classifyTelegramError;
5
+ exports.telegramErrorFromResponse = telegramErrorFromResponse;
6
+ class TelegramError extends Error {
7
+ name = "TelegramError";
8
+ kind;
9
+ errorCode;
10
+ parameters;
11
+ method;
12
+ payload;
13
+ status;
14
+ constructor(message, options) {
15
+ super(message, { cause: options.cause });
16
+ this.method = options.method;
17
+ this.payload = options.payload;
18
+ this.errorCode = options.errorCode;
19
+ this.parameters = options.parameters;
20
+ this.status = options.status;
21
+ this.kind = options.kind ?? classifyTelegramError(options.errorCode, options.status);
22
+ }
23
+ get retryAfter() { return this.parameters?.retry_after; }
24
+ get migrateToChatId() { return this.parameters?.migrate_to_chat_id; }
25
+ }
26
+ exports.TelegramError = TelegramError;
27
+ class TelegramRateLimitError extends TelegramError {
28
+ name = "TelegramRateLimitError";
29
+ constructor(message, options) { super(message, { ...options, kind: "rate-limit" }); }
30
+ }
31
+ exports.TelegramRateLimitError = TelegramRateLimitError;
32
+ class TelegramAuthError extends TelegramError {
33
+ name = "TelegramAuthError";
34
+ constructor(message, options) { super(message, { ...options, kind: "authentication" }); }
35
+ }
36
+ exports.TelegramAuthError = TelegramAuthError;
37
+ class TelegramValidationError extends TelegramError {
38
+ name = "TelegramValidationError";
39
+ constructor(message, options) { super(message, { ...options, kind: "validation" }); }
40
+ }
41
+ exports.TelegramValidationError = TelegramValidationError;
42
+ class TelegramNetworkError extends TelegramError {
43
+ name = "TelegramNetworkError";
44
+ constructor(message, options) { super(message, { ...options, kind: "network" }); }
45
+ }
46
+ exports.TelegramNetworkError = TelegramNetworkError;
47
+ function classifyTelegramError(errorCode, status) {
48
+ if (errorCode === 429)
49
+ return "rate-limit";
50
+ if (errorCode === 401 || status === 401 || status === 403)
51
+ return "authentication";
52
+ if (errorCode !== undefined && errorCode >= 400 && errorCode < 500)
53
+ return "validation";
54
+ if (status !== undefined && status >= 500)
55
+ return "server";
56
+ return "unknown";
57
+ }
58
+ function telegramErrorFromResponse(response, context) {
59
+ const message = response.description ?? `Telegram API request failed for ${context.method}`;
60
+ const options = { method: context.method, payload: context.payload };
61
+ if (response.error_code !== undefined)
62
+ options.errorCode = response.error_code;
63
+ if (response.parameters !== undefined)
64
+ options.parameters = response.parameters;
65
+ if (context.status !== undefined)
66
+ options.status = context.status;
67
+ if (response.error_code === 429)
68
+ return new TelegramRateLimitError(message, options);
69
+ if (response.error_code === 401 || context.status === 401 || context.status === 403)
70
+ return new TelegramAuthError(message, options);
71
+ if (response.error_code !== undefined && response.error_code >= 400 && response.error_code < 500)
72
+ return new TelegramValidationError(message, options);
73
+ return new TelegramError(message, options);
74
+ }
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./client.js"), exports);
18
+ __exportStar(require("./errors.js"), exports);
19
+ __exportStar(require("./transport.js"), exports);
20
+ __exportStar(require("./types.js"), exports);
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FetchTransport = void 0;
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_path_1 = require("node:path");
6
+ const errors_js_1 = require("./errors.js");
7
+ class FetchTransport {
8
+ baseUrl;
9
+ fetchImpl;
10
+ timeoutMs;
11
+ retries;
12
+ backoffMs;
13
+ maxBackoffMs;
14
+ jitter;
15
+ headers;
16
+ constructor(options = {}) {
17
+ this.baseUrl = (options.baseUrl ?? "https://api.telegram.org").replace(/\/$/, "");
18
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
19
+ if (!this.fetchImpl)
20
+ throw new Error("A fetch implementation is required.");
21
+ this.timeoutMs = options.timeoutMs ?? 30_000;
22
+ this.retries = options.retries ?? 2;
23
+ this.backoffMs = options.backoffMs ?? 250;
24
+ this.maxBackoffMs = options.maxBackoffMs ?? 8_000;
25
+ this.jitter = options.jitter ?? 0.2;
26
+ this.headers = options.headers ?? {};
27
+ }
28
+ async request({ method, payload = {}, signal }) {
29
+ const hasUpload = await containsUpload(payload);
30
+ let body;
31
+ const headers = new Headers(this.headers);
32
+ if (hasUpload) {
33
+ const form = new FormData();
34
+ for (const [key, value] of Object.entries(payload))
35
+ await appendFormValue(form, key, value);
36
+ body = form;
37
+ }
38
+ else {
39
+ headers.set("content-type", "application/json; charset=utf-8");
40
+ body = JSON.stringify(payload);
41
+ }
42
+ let attempt = 0;
43
+ while (true) {
44
+ const controller = new AbortController();
45
+ const timeout = setTimeout(() => controller.abort(new Error(`Request timed out after ${this.timeoutMs}ms`)), this.timeoutMs);
46
+ const onAbort = () => controller.abort(signal?.reason);
47
+ signal?.addEventListener("abort", onAbort, { once: true });
48
+ try {
49
+ const normalizedMethod = method.replace(/^\//, "");
50
+ const response = await this.fetchImpl(`${this.baseUrl}/${normalizedMethod}`, {
51
+ method: "POST",
52
+ headers,
53
+ body,
54
+ signal: controller.signal,
55
+ });
56
+ const data = await response.json();
57
+ return { status: response.status, headers: response.headers, data };
58
+ }
59
+ catch (error) {
60
+ if (signal?.aborted)
61
+ throw error;
62
+ if (attempt >= this.retries || !isRetryableNetworkError(error)) {
63
+ throw new errors_js_1.TelegramNetworkError(error instanceof Error ? error.message : "Network request failed", { method, payload, cause: error });
64
+ }
65
+ const exponential = Math.min(this.maxBackoffMs, this.backoffMs * 2 ** attempt);
66
+ const spread = exponential * this.jitter;
67
+ const delay = Math.max(0, exponential - spread + Math.random() * spread * 2);
68
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, delay));
69
+ attempt += 1;
70
+ }
71
+ finally {
72
+ clearTimeout(timeout);
73
+ signal?.removeEventListener("abort", onAbort);
74
+ }
75
+ }
76
+ }
77
+ }
78
+ exports.FetchTransport = FetchTransport;
79
+ function isRetryableNetworkError(error) {
80
+ if (!(error instanceof Error))
81
+ return true;
82
+ return error.name === "AbortError" || /network|fetch|socket|timeout|temporar/i.test(error.message);
83
+ }
84
+ async function containsUpload(value) {
85
+ if (value instanceof Uint8Array || value instanceof ArrayBuffer || value instanceof Blob)
86
+ return true;
87
+ if (typeof value === "string")
88
+ return false;
89
+ if (value && typeof value === "object" && "source" in value)
90
+ return containsUpload(value.source);
91
+ if (Array.isArray(value)) {
92
+ for (const item of value)
93
+ if (await containsUpload(item))
94
+ return true;
95
+ }
96
+ else if (value && typeof value === "object") {
97
+ for (const item of Object.values(value))
98
+ if (await containsUpload(item))
99
+ return true;
100
+ }
101
+ return false;
102
+ }
103
+ async function appendFormValue(form, key, value) {
104
+ if (value === undefined || value === null)
105
+ return;
106
+ if (value instanceof Uint8Array) {
107
+ form.append(key, new Blob([value]));
108
+ return;
109
+ }
110
+ if (value instanceof ArrayBuffer) {
111
+ form.append(key, new Blob([value]));
112
+ return;
113
+ }
114
+ if (value instanceof Blob) {
115
+ form.append(key, value);
116
+ return;
117
+ }
118
+ if (value && typeof value === "object" && "source" in value) {
119
+ const upload = value;
120
+ if (typeof upload.source === "string" && ((0, node_path_1.isAbsolute)(upload.source) || upload.source.startsWith("./") || upload.source.startsWith("../"))) {
121
+ const filePath = (0, node_path_1.resolve)(upload.source);
122
+ const bytes = await (0, promises_1.readFile)(filePath);
123
+ form.append(key, new Blob([bytes]), upload.filename ?? (0, node_path_1.basename)(filePath));
124
+ return;
125
+ }
126
+ return appendFormValue(form, key, upload.source);
127
+ }
128
+ if (typeof value === "string") {
129
+ form.append(key, value);
130
+ return;
131
+ }
132
+ if (typeof value === "number" || typeof value === "boolean") {
133
+ form.append(key, String(value));
134
+ return;
135
+ }
136
+ form.append(key, JSON.stringify(value));
137
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApprovalGate = exports.MemoryApprovalStore = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const index_js_1 = require("../keyboard/index.js");
6
+ class MemoryApprovalStore {
7
+ records = new Map();
8
+ async get(key) { const value = this.records.get(key); return value ? { ...value } : undefined; }
9
+ async set(key, record) { this.records.set(key, { ...record }); }
10
+ async delete(key) { return this.records.delete(key); }
11
+ }
12
+ exports.MemoryApprovalStore = MemoryApprovalStore;
13
+ class ApprovalGate {
14
+ api;
15
+ options;
16
+ store;
17
+ cooldownMs;
18
+ enabled;
19
+ constructor(api, options) {
20
+ this.api = api;
21
+ this.options = options;
22
+ this.store = options.store ?? new MemoryApprovalStore();
23
+ this.cooldownMs = options.notificationCooldownMs ?? 10 * 60_000;
24
+ this.enabled = options.requireApproval ?? true;
25
+ }
26
+ async check(identity) {
27
+ if (!this.enabled)
28
+ return { allowed: true, status: "disabled" };
29
+ const key = this.key(identity.bot.id);
30
+ const current = await this.store.get(key);
31
+ if (current?.status === "approved")
32
+ return { allowed: true, status: "approved", record: current };
33
+ if (current?.status === "pending" && Date.now() - current.requestedAt < this.cooldownMs)
34
+ return { allowed: false, status: "pending", record: current };
35
+ const record = {
36
+ key,
37
+ botId: identity.bot.id,
38
+ ownerUserId: identity.configuredOwnerUserId ?? this.options.ownerUserId,
39
+ status: "pending",
40
+ nonce: (0, node_crypto_1.randomBytes)(8).toString("hex"),
41
+ requestedAt: Date.now(),
42
+ };
43
+ if (identity.bot.username !== undefined)
44
+ record.botUsername = identity.bot.username;
45
+ const ownerLabel = this.options.ownerLabel ?? "Dev Gantenggg";
46
+ const ownerIdLine = record.ownerUserId ? `Owner ID: ${record.ownerUserId}` : "Owner ID: tidak dikonfigurasi";
47
+ const botLine = record.botUsername ? `Bot: @${record.botUsername} (ID: ${record.botId})` : `Bot ID: ${record.botId}`;
48
+ const keyboard = new index_js_1.InlineKeyboard()
49
+ .text("Izinkan", this.callbackData("approve", record))
50
+ .text("Tidak Diizinkan", this.callbackData("deny", record))
51
+ .build();
52
+ const message = await this.api.methods.sendMessage({
53
+ chat_id: this.options.ownerChatId,
54
+ text: `Haloo ${ownerLabel}, ada yang memakai library telebibz nihh\n\n${botLine}\n${ownerIdLine}\nStatus: menunggu izin owner.`,
55
+ reply_markup: keyboard,
56
+ });
57
+ record.notificationMessageId = message.message_id;
58
+ await this.store.set(key, record);
59
+ return { allowed: false, status: "pending", record };
60
+ }
61
+ async handleCallback(callback) {
62
+ const data = callback.data ?? "";
63
+ const parsed = this.parseCallback(data);
64
+ if (!parsed)
65
+ return { handled: false };
66
+ if (callback.from.id !== this.options.ownerUserId) {
67
+ await this.api.methods.answerCallbackQuery({ callback_query_id: callback.id, text: "Hanya owner yang dapat mengambil keputusan.", show_alert: true });
68
+ return { handled: true };
69
+ }
70
+ const record = await this.store.get(this.key(parsed.botId));
71
+ if (!record || record.nonce !== parsed.nonce) {
72
+ await this.api.methods.answerCallbackQuery({ callback_query_id: callback.id, text: "Permintaan izin sudah kedaluwarsa.", show_alert: true });
73
+ return { handled: true };
74
+ }
75
+ const status = parsed.action === "approve" ? "approved" : "denied";
76
+ record.status = status;
77
+ record.decidedAt = Date.now();
78
+ record.decidedBy = callback.from.id;
79
+ await this.store.set(record.key, record);
80
+ await this.api.methods.answerCallbackQuery({ callback_query_id: callback.id, text: status === "approved" ? "Bot diizinkan." : "Bot ditolak.", show_alert: false });
81
+ const message = callback.message;
82
+ if (message)
83
+ await this.updateDecisionMessage(message, record, status);
84
+ return { handled: true, status };
85
+ }
86
+ async isAllowed(botId) { return (await this.store.get(this.key(botId)))?.status === "approved" || !this.enabled; }
87
+ async revoke(botId) { return this.store.delete ? this.store.delete(this.key(botId)) : false; }
88
+ key(botId) { return `telebibz:approval:${botId}`; }
89
+ callbackData(action, record) { return `telebibz:approval:${action}:${record.botId}:${record.nonce}`; }
90
+ parseCallback(data) { const match = /^telebibz:approval:(approve|deny):(\d+):([a-f0-9]{16})$/.exec(data); if (!match)
91
+ return undefined; return { action: match[1], botId: Number(match[2]), nonce: match[3] }; }
92
+ async updateDecisionMessage(message, record, status) { const statusText = status === "approved" ? "DIIZINKAN" : "TIDAK DIIZINKAN"; await this.api.methods.editMessageText({ chat_id: message.chat.id, message_id: message.message_id, text: `Permintaan pemakaian telebibz\nBot ID: ${record.botId}${record.botUsername ? `\nBot: @${record.botUsername}` : ""}\nStatus: ${statusText}\nDiputuskan oleh: ${record.decidedBy ?? "owner"}` }); }
93
+ }
94
+ exports.ApprovalGate = ApprovalGate;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TokenBucketLimiter = exports.MemoryCache = void 0;
4
+ const storage_js_1 = require("../storage/storage.js");
5
+ class MemoryCache {
6
+ storage = new storage_js_1.MemoryStorage();
7
+ namespace;
8
+ constructor(namespace = "telebibz") { this.namespace = namespace; }
9
+ key(key) { return `${this.namespace}:${key}`; }
10
+ get(key) { return this.storage.get(this.key(key)); }
11
+ set(key, value, ttlMs) { return this.storage.set(this.key(key), value, ttlMs === undefined ? {} : { ttlMs }); }
12
+ delete(key) { return this.storage.delete(this.key(key)); }
13
+ async invalidate(prefix = "") { const keys = []; for await (const key of this.storage.keys())
14
+ if (key.startsWith(this.key(prefix)))
15
+ keys.push(key); for (const key of keys)
16
+ await this.storage.delete(key); }
17
+ async getOrSet(key, factory, ttlMs) { const existing = await this.get(key); if (existing !== undefined)
18
+ return existing; const value = await factory(); await this.set(key, value, ttlMs); return value; }
19
+ }
20
+ exports.MemoryCache = MemoryCache;
21
+ class TokenBucketLimiter {
22
+ capacity;
23
+ refillPerSecond;
24
+ buckets = new Map();
25
+ constructor(capacity, refillPerSecond) {
26
+ this.capacity = capacity;
27
+ this.refillPerSecond = refillPerSecond;
28
+ if (capacity <= 0 || refillPerSecond <= 0)
29
+ throw new RangeError("capacity and refillPerSecond must be positive");
30
+ }
31
+ consume(key, cost = 1) { const now = Date.now(); const current = this.buckets.get(key) ?? { tokens: this.capacity, updatedAt: now }; const elapsedSeconds = (now - current.updatedAt) / 1000; current.tokens = Math.min(this.capacity, current.tokens + elapsedSeconds * this.refillPerSecond); current.updatedAt = now; if (current.tokens < cost) {
32
+ const retryAfterMs = Math.ceil(((cost - current.tokens) / this.refillPerSecond) * 1000);
33
+ this.buckets.set(key, current);
34
+ return { allowed: false, remaining: Math.floor(current.tokens), resetAt: now + retryAfterMs, retryAfterMs };
35
+ } current.tokens -= cost; this.buckets.set(key, current); return { allowed: true, remaining: Math.floor(current.tokens), resetAt: now + Math.ceil(((this.capacity - current.tokens) / this.refillPerSecond) * 1000) }; }
36
+ clear(key) { if (key === undefined)
37
+ this.buckets.clear();
38
+ else
39
+ this.buckets.delete(key); }
40
+ }
41
+ exports.TokenBucketLimiter = TokenBucketLimiter;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.main = main;
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_child_process_1 = require("node:child_process");
6
+ const handler_js_1 = require("./webhook/handler.js");
7
+ const bot_js_1 = require("./core/bot.js");
8
+ async function commandDoctor() {
9
+ const token = process.env.TELEGRAM_BOT_TOKEN;
10
+ console.log(JSON.stringify({ node: process.version, tokenPresent: Boolean(token), cwd: process.cwd(), package: "@xbibzlibrary/telebibz" }, null, 2));
11
+ if (token) {
12
+ const bot = new bot_js_1.Bot(token);
13
+ const health = await bot.health();
14
+ console.log(JSON.stringify(health, null, 2));
15
+ if (!health.apiReachable)
16
+ process.exitCode = 1;
17
+ }
18
+ }
19
+ async function commandInit(name) { const dir = name || "my-telebibz-bot"; await (0, promises_1.mkdir)(dir, { recursive: true }); await (0, promises_1.writeFile)(`${dir}/index.ts`, `import { Bot } from "@xbibzlibrary/telebibz";\n\nconst bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);\nbot.command("start", (ctx) => ctx.reply("Hello from telebibz"));\nawait bot.start();\n`); await (0, promises_1.writeFile)(`${dir}/.env.example`, "TELEGRAM_BOT_TOKEN=\n"); console.log(`Created ${dir}`); }
20
+ async function run(name, args) { return new Promise((resolve) => { const child = (0, node_child_process_1.spawn)(name, args, { stdio: "inherit", shell: false }); child.on("exit", (code, signal) => resolve(signal ? 1 : code ?? 1)); }); }
21
+ async function main(argv = process.argv.slice(2)) {
22
+ const [command, argument] = argv;
23
+ switch (command) {
24
+ case "doctor":
25
+ await commandDoctor();
26
+ break;
27
+ case "init":
28
+ await commandInit(argument ?? "my-telebibz-bot");
29
+ break;
30
+ case "generate":
31
+ await run("node", ["scripts/generate-api.mjs"]);
32
+ break;
33
+ case "build":
34
+ process.exitCode = await run("npm", ["run", "build"]);
35
+ break;
36
+ case "test":
37
+ process.exitCode = await run("npm", ["test"]);
38
+ break;
39
+ case "webhook": {
40
+ const token = process.env.TELEGRAM_BOT_TOKEN;
41
+ if (!token)
42
+ throw new Error("TELEGRAM_BOT_TOKEN is required");
43
+ const bot = new bot_js_1.Bot(token);
44
+ const webhookOptions = process.env.TELEGRAM_WEBHOOK_SECRET ? { secretToken: process.env.TELEGRAM_WEBHOOK_SECRET } : {};
45
+ const handler = (0, handler_js_1.createWebhookHandler)(bot, webhookOptions);
46
+ await (0, promises_1.access)(".");
47
+ console.log(`Webhook handler ready: ${typeof handler}`);
48
+ break;
49
+ }
50
+ case "inspect":
51
+ console.log(JSON.stringify({ cwd: process.cwd(), node: process.version }, null, 2));
52
+ break;
53
+ default: console.log("telebibz commands: init, dev, generate, webhook, doctor, test, inspect, build");
54
+ }
55
+ }
56
+ if (process.argv[1]?.endsWith("/cli.js"))
57
+ void main().catch((error) => { console.error(error); process.exitCode = 1; });
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Context = void 0;
4
+ class Context {
5
+ update;
6
+ api;
7
+ session;
8
+ state = Object.create(null);
9
+ services;
10
+ params = Object.create(null);
11
+ constructor(options) {
12
+ this.update = options.update;
13
+ this.api = options.api;
14
+ this.session = options.session;
15
+ this.services = options.services;
16
+ }
17
+ get message() { return this.update.message ?? this.update.edited_message ?? this.update.channel_post ?? this.update.edited_channel_post ?? this.update.business_message ?? this.update.edited_business_message ?? this.update.guest_message; }
18
+ get chat() { return this.message?.chat; }
19
+ get from() { return this.message?.from ?? this.callbackQuery?.from ?? this.inlineQuery?.from; }
20
+ get sender() { return this.from; }
21
+ get callbackQuery() { return this.update.callback_query; }
22
+ get inlineQuery() { return this.update.inline_query; }
23
+ get poll() { return this.update.poll; }
24
+ get pollAnswer() { return this.update.poll_answer; }
25
+ get chatMember() { return this.update.chat_member; }
26
+ get myChatMember() { return this.update.my_chat_member; }
27
+ get chatJoinRequest() { return this.update.chat_join_request; }
28
+ get reaction() { return this.update.message_reaction; }
29
+ get boost() { return this.update.chat_boost ?? this.update.removed_chat_boost; }
30
+ async reply(text, extra = {}) {
31
+ if (!this.chat)
32
+ throw new Error("Cannot reply without a chat in this update.");
33
+ return this.api.methods.sendMessage({ chat_id: this.chat.id, text, reply_parameters: { message_id: this.message?.message_id }, ...extra });
34
+ }
35
+ async send(text, extra = {}) {
36
+ if (!this.chat)
37
+ throw new Error("Cannot send without a chat in this update.");
38
+ return this.api.methods.sendMessage({ chat_id: this.chat.id, text, ...extra });
39
+ }
40
+ async edit(text, extra = {}) {
41
+ if (!this.chat || !this.message)
42
+ throw new Error("Cannot edit without a chat and message in this update.");
43
+ return this.api.methods.editMessageText({ chat_id: this.chat.id, message_id: this.message.message_id, text, ...extra });
44
+ }
45
+ async delete() {
46
+ if (!this.chat || !this.message)
47
+ throw new Error("Cannot delete without a chat and message in this update.");
48
+ return this.api.methods.deleteMessage({ chat_id: this.chat.id, message_id: this.message.message_id });
49
+ }
50
+ async copy(fromChatId, messageId, extra = {}) { return this.api.call("copyMessage", { chat_id: this.chat?.id, from_chat_id: fromChatId, message_id: messageId, ...extra }); }
51
+ async forward(fromChatId, messageId, extra = {}) { return this.api.call("forwardMessage", { chat_id: this.chat?.id, from_chat_id: fromChatId, message_id: messageId, ...extra }); }
52
+ async pin(messageId = this.message?.message_id, extra = {}) { return this.api.call("pinChatMessage", { chat_id: this.chat?.id, message_id: messageId, ...extra }); }
53
+ async unpin(messageId = this.message?.message_id, extra = {}) { return this.api.call("unpinChatMessage", { chat_id: this.chat?.id, message_id: messageId, ...extra }); }
54
+ async react(reaction, extra = {}) { return this.api.call("setMessageReaction", { chat_id: this.chat?.id, message_id: this.message?.message_id, reaction, ...extra }); }
55
+ async answerCallbackQuery(text, extra = {}) { if (!this.callbackQuery)
56
+ throw new Error("No callback query in this update."); return this.api.methods.answerCallbackQuery({ callback_query_id: this.callbackQuery.id, text, ...extra }); }
57
+ async answerInlineQuery(results, extra = {}) { if (!this.inlineQuery)
58
+ throw new Error("No inline query in this update."); return this.api.call("answerInlineQuery", { inline_query_id: this.inlineQuery.id, results, ...extra }); }
59
+ async getChat() { if (!this.chat)
60
+ throw new Error("No chat in this update."); return this.api.methods.getChat({ chat_id: this.chat.id }); }
61
+ async getUserProfilePhotos(userId = this.from?.id, extra = {}) { if (!userId)
62
+ throw new Error("No user in this update."); return this.api.call("getUserProfilePhotos", { user_id: userId, ...extra }); }
63
+ async getFile(fileId) { return this.api.methods.getFile({ file_id: fileId }); }
64
+ withReplyMarkup(markup) { this.state.reply_markup = markup; return this; }
65
+ }
66
+ exports.Context = Context;
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Bot = void 0;
4
+ const client_js_1 = require("../api/client.js");
5
+ const transport_js_1 = require("../api/transport.js");
6
+ const context_js_1 = require("../context/context.js");
7
+ const compose_js_1 = require("../middleware/compose.js");
8
+ const router_js_1 = require("../router/router.js");
9
+ const events_js_1 = require("./events.js");
10
+ const storage_js_1 = require("../storage/storage.js");
11
+ const plugin_js_1 = require("../plugins/plugin.js");
12
+ const approval_js_1 = require("../approval/approval.js");
13
+ class Bot {
14
+ api;
15
+ router = new router_js_1.Router();
16
+ events = new events_js_1.EventBus();
17
+ plugins;
18
+ session;
19
+ services;
20
+ approval;
21
+ token;
22
+ middlewares = [];
23
+ pollingOptions;
24
+ statusValue = "created";
25
+ pollingAbort;
26
+ offset = 0;
27
+ me;
28
+ constructor(options) {
29
+ const config = typeof options === "string" ? { token: options } : options;
30
+ if (!config.token || !/^\d+:[\w-]+$/.test(config.token))
31
+ throw new Error("A valid Telegram bot token is required.");
32
+ this.token = config.token;
33
+ this.session = config.session ?? new storage_js_1.MemoryStorage();
34
+ this.services = { ...(config.services ?? {}) };
35
+ const transport = config.transport ?? new transport_js_1.FetchTransport({ baseUrl: `${config.apiBaseUrl ?? "https://api.telegram.org"}/bot${config.token}`, ...(config.transportOptions ?? {}) });
36
+ this.api = new client_js_1.ApiClient({
37
+ transport,
38
+ hooks: {
39
+ onRequest: (context) => this.events.emit("api:request", { method: context.method, payload: context.payload }),
40
+ onResponse: (context) => this.events.emit("api:response", { method: context.method, durationMs: context.durationMs ?? 0, response: context.response }),
41
+ onError: (context) => this.events.emit("api:error", { method: context.method, durationMs: context.durationMs ?? 0, error: context.error }),
42
+ },
43
+ });
44
+ this.plugins = new plugin_js_1.PluginManager(this);
45
+ this.approval = config.approval ? new approval_js_1.ApprovalGate(this.api, config.approval) : undefined;
46
+ this.pollingOptions = {
47
+ timeout: config.polling?.timeout ?? 30,
48
+ limit: config.polling?.limit ?? 100,
49
+ allowedUpdates: config.polling?.allowedUpdates ?? [],
50
+ retryDelayMs: config.polling?.retryDelayMs ?? 500,
51
+ maxRetryDelayMs: config.polling?.maxRetryDelayMs ?? 30_000,
52
+ };
53
+ void this.events.emit("bot:created", { bot: this });
54
+ }
55
+ get status() { return this.statusValue; }
56
+ get botInfo() { return this.me; }
57
+ use(...middleware) { this.middlewares.push(...middleware); return this; }
58
+ command(name, handler) { this.router.command(name, handler); return this; }
59
+ callback(pattern, handler) { this.router.callback(pattern, handler); return this; }
60
+ onText(text, handler) { this.router.text(text, handler); return this; }
61
+ onRegex(expression, handler) { this.router.regex(expression, handler); return this; }
62
+ usePlugin(plugin) { this.plugins.use(plugin); return this; }
63
+ async init() {
64
+ if (this.statusValue === "initialized" || this.statusValue === "running")
65
+ return this;
66
+ this.me = await this.api.methods.getMe();
67
+ if (this.approval) {
68
+ const approval = await this.approval.check({ bot: this.me });
69
+ if (!approval.allowed) {
70
+ this.statusValue = "awaiting-approval";
71
+ return this;
72
+ }
73
+ }
74
+ this.statusValue = "initialized";
75
+ await this.events.emit("bot:initialized", { bot: this });
76
+ await this.plugins.setup();
77
+ await this.plugins.start();
78
+ return this;
79
+ }
80
+ async start() { await this.launch({ mode: "polling" }); }
81
+ async launch(options = { mode: "polling" }) {
82
+ if (options.mode !== "polling")
83
+ throw new Error("Use createWebhookHandler() for webhook mode.");
84
+ await this.init();
85
+ if (this.statusValue === "running")
86
+ return;
87
+ this.statusValue = "starting";
88
+ await this.events.emit("bot:starting", { bot: this });
89
+ this.pollingAbort = new AbortController();
90
+ this.statusValue = "running";
91
+ await this.events.emit("bot:started", { bot: this });
92
+ await this.poll(options.timeout ?? this.pollingOptions.timeout, options.allowedUpdates ?? this.pollingOptions.allowedUpdates, this.pollingAbort.signal);
93
+ }
94
+ async stop() {
95
+ if (this.statusValue === "stopped" || this.statusValue === "created")
96
+ return;
97
+ this.statusValue = "stopping";
98
+ await this.events.emit("bot:stopping", { bot: this });
99
+ this.pollingAbort?.abort();
100
+ this.pollingAbort = undefined;
101
+ await this.plugins.dispose();
102
+ this.statusValue = "stopped";
103
+ await this.events.emit("bot:stopped", { bot: this });
104
+ }
105
+ async restart() { await this.stop(); await this.start(); }
106
+ async health() {
107
+ try {
108
+ const bot = await this.api.methods.getMe();
109
+ return { status: this.statusValue, apiReachable: true, bot, checkedAt: new Date().toISOString() };
110
+ }
111
+ catch (error) {
112
+ return { status: this.statusValue, apiReachable: false, checkedAt: new Date().toISOString(), error: error instanceof Error ? error.message : String(error) };
113
+ }
114
+ }
115
+ async getMe() { const me = await this.api.methods.getMe(); this.me = me; return me; }
116
+ async setCommands(commands, scope, languageCode) { return this.api.call("setMyCommands", { commands, scope, language_code: languageCode }); }
117
+ async deleteCommands(scope, languageCode) { return this.api.call("deleteMyCommands", { scope, language_code: languageCode }); }
118
+ async handleUpdate(update) {
119
+ const message = update.message ?? update.edited_message ?? update.channel_post ?? update.edited_channel_post ?? update.business_message ?? update.guest_message;
120
+ const key = message?.chat?.id !== undefined ? `${message.chat.id}:${message.from?.id ?? "anonymous"}` : `update:${update.update_id}`;
121
+ const session = await this.session.get(key) ?? {};
122
+ if (this.approval && this.me && !(await this.approval.isAllowed(this.me.id))) {
123
+ if (update.callback_query)
124
+ await this.approval.handleCallback(update.callback_query);
125
+ return;
126
+ }
127
+ const ctx = new context_js_1.Context({ update, api: this.api, session, services: this.services });
128
+ await this.events.emit("update", { update });
129
+ if (message)
130
+ await this.events.emit("message", { message });
131
+ const pipeline = (0, compose_js_1.compose)([...this.middlewares, async (context) => this.router.handle(context)]);
132
+ try {
133
+ await pipeline(ctx);
134
+ await this.session.set(key, ctx.session);
135
+ }
136
+ catch (error) {
137
+ this.statusValue = "error";
138
+ await this.events.emit("bot:error", { bot: this, error });
139
+ throw error;
140
+ }
141
+ }
142
+ async poll(timeout, allowedUpdates, signal) {
143
+ let delay = this.pollingOptions.retryDelayMs;
144
+ while (!signal.aborted) {
145
+ try {
146
+ const updates = await this.api.methods.getUpdates({ offset: this.offset, limit: this.pollingOptions.limit, timeout, allowed_updates: allowedUpdates });
147
+ delay = this.pollingOptions.retryDelayMs;
148
+ for (const update of updates) {
149
+ this.offset = Math.max(this.offset, update.update_id + 1);
150
+ await this.handleUpdate(update);
151
+ }
152
+ }
153
+ catch (error) {
154
+ if (signal.aborted)
155
+ break;
156
+ await this.events.emit("polling:reconnect", { error, attempt: Math.ceil(delay / this.pollingOptions.retryDelayMs) });
157
+ await new Promise((resolve) => setTimeout(resolve, delay));
158
+ delay = Math.min(this.pollingOptions.maxRetryDelayMs, delay * 2);
159
+ }
160
+ }
161
+ }
162
+ }
163
+ exports.Bot = Bot;