@xbibzlibrary/telebibz 0.4.2 → 0.4.4
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 +22 -0
- package/README.id.md +6 -3
- package/README.md +6 -3
- package/README.zh-CN.md +6 -3
- package/dist/src/api/client.d.ts +28 -0
- package/dist/src/api/client.d.ts.map +1 -1
- package/dist/src/api/client.js +28 -0
- package/dist/src/api/client.js.map +1 -1
- package/dist/src/api/transport.d.ts +14 -0
- package/dist/src/api/transport.d.ts.map +1 -1
- package/dist/src/api/transport.js +67 -2
- package/dist/src/api/transport.js.map +1 -1
- package/dist/src/api/types.d.ts +25 -0
- package/dist/src/api/types.d.ts.map +1 -1
- package/dist/src/context/context.d.ts +13 -3
- package/dist/src/context/context.d.ts.map +1 -1
- package/dist/src/context/context.js +15 -0
- package/dist/src/context/context.js.map +1 -1
- package/dist/src/core/bot.d.ts +12 -0
- package/dist/src/core/bot.d.ts.map +1 -1
- package/dist/src/core/bot.js +16 -0
- package/dist/src/core/bot.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/testing.d.ts +6 -0
- package/dist/src/testing.d.ts.map +1 -1
- package/dist/src/testing.js +6 -0
- package/dist/src/testing.js.map +1 -1
- package/dist/src/utils/files.d.ts +45 -0
- package/dist/src/utils/files.d.ts.map +1 -0
- package/dist/src/utils/files.js +53 -0
- package/dist/src/utils/files.js.map +1 -0
- package/dist-cjs/src/api/client.js +28 -0
- package/dist-cjs/src/api/transport.js +67 -2
- package/dist-cjs/src/context/context.js +15 -0
- package/dist-cjs/src/core/bot.js +16 -0
- package/dist-cjs/src/index.js +1 -0
- package/dist-cjs/src/testing.js +6 -0
- package/dist-cjs/src/utils/files.js +58 -0
- package/docs/API.id.md +70 -0
- package/docs/API.md +70 -0
- package/docs/API.zh-CN.md +70 -0
- package/docs/STORAGE.id.md +105 -0
- package/docs/STORAGE.md +105 -0
- package/docs/STORAGE.zh-CN.md +105 -0
- package/examples/files.ts +35 -0
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.FetchTransport = void 0;
|
|
4
4
|
const promises_1 = require("node:fs/promises");
|
|
5
5
|
const node_path_1 = require("node:path");
|
|
6
|
+
const consumers_1 = require("node:stream/consumers");
|
|
6
7
|
const errors_js_1 = require("./errors.js");
|
|
7
8
|
class FetchTransport {
|
|
8
9
|
baseUrl;
|
|
@@ -46,6 +47,37 @@ class FetchTransport {
|
|
|
46
47
|
const retryAfterMs = Math.max(0, (data.parameters?.retry_after ?? 0) * 1000);
|
|
47
48
|
this.floodUntil = Math.max(this.floodUntil, Date.now() + retryAfterMs);
|
|
48
49
|
}
|
|
50
|
+
/** Direct-download URL for a `file_path` from `getFile` (`/bot<token>` → `/file/bot<token>`). */
|
|
51
|
+
fileUrl(filePath) {
|
|
52
|
+
const downloadBase = /\/bot[^/]+$/.test(this.baseUrl) ? this.baseUrl.replace(/\/bot([^/]+)$/, "/file/bot$1") : this.baseUrl;
|
|
53
|
+
return `${downloadBase}/${filePath}`;
|
|
54
|
+
}
|
|
55
|
+
/** Downloads the raw bytes behind a `file_path` from `getFile` (Telegram caps downloads at 20 MB). */
|
|
56
|
+
async download(filePath, signal) {
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const timeout = setTimeout(() => controller.abort(new Error("File download timed out")), Math.max(this.timeoutMs, 120_000));
|
|
59
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
60
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
61
|
+
try {
|
|
62
|
+
const response = await this.fetchImpl(this.fileUrl(filePath), { method: "GET", signal: controller.signal });
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const bodyText = await response.text().catch(() => "");
|
|
65
|
+
throw new errors_js_1.TelegramNetworkError(`Failed to download ${filePath}: HTTP ${response.status} ${response.statusText || "request failed"}`, { method: "getFile", payload: { file_path: filePath }, status: response.status, cause: bodyText.slice(0, 512) });
|
|
66
|
+
}
|
|
67
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error instanceof errors_js_1.TelegramNetworkError)
|
|
71
|
+
throw error;
|
|
72
|
+
if (signal?.aborted)
|
|
73
|
+
throw error;
|
|
74
|
+
throw new errors_js_1.TelegramNetworkError(`Failed to download ${filePath}: ${error instanceof Error ? error.message : String(error)}`, { method: "getFile", payload: { file_path: filePath }, cause: error });
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
clearTimeout(timeout);
|
|
78
|
+
signal?.removeEventListener("abort", onAbort);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
49
81
|
async request({ method, payload = {}, signal, timeoutMs }) {
|
|
50
82
|
const effectiveTimeoutMs = timeoutMs ?? this.timeoutMs;
|
|
51
83
|
const hasUpload = await containsUpload(payload);
|
|
@@ -149,10 +181,20 @@ function isRetryableNetworkError(error) {
|
|
|
149
181
|
async function containsUpload(value) {
|
|
150
182
|
if (value instanceof Uint8Array || value instanceof ArrayBuffer || value instanceof Blob)
|
|
151
183
|
return true;
|
|
184
|
+
if (value instanceof ReadableStream)
|
|
185
|
+
return true;
|
|
152
186
|
if (typeof value === "string")
|
|
153
187
|
return false;
|
|
154
|
-
if (value && typeof value === "object" && "source" in value)
|
|
155
|
-
|
|
188
|
+
if (value && typeof value === "object" && "source" in value) {
|
|
189
|
+
const source = value.source;
|
|
190
|
+
// A path-like string source means the transport reads the file from disk
|
|
191
|
+
// and must switch to multipart — a plain string source is just a file_id.
|
|
192
|
+
if (typeof source === "string" && ((0, node_path_1.isAbsolute)(source) || source.startsWith("./") || source.startsWith("../")))
|
|
193
|
+
return true;
|
|
194
|
+
return containsUpload(source);
|
|
195
|
+
}
|
|
196
|
+
if (typeof value?.pipe === "function")
|
|
197
|
+
return true;
|
|
156
198
|
if (Array.isArray(value)) {
|
|
157
199
|
for (const item of value)
|
|
158
200
|
if (await containsUpload(item))
|
|
@@ -165,6 +207,13 @@ async function containsUpload(value) {
|
|
|
165
207
|
}
|
|
166
208
|
return false;
|
|
167
209
|
}
|
|
210
|
+
/** Converts a Node.js or web ReadableStream to a Blob by draining it. */
|
|
211
|
+
async function streamToBlob(stream) {
|
|
212
|
+
if (stream instanceof ReadableStream)
|
|
213
|
+
return new Blob([await new Response(stream).arrayBuffer()]);
|
|
214
|
+
const bytes = await (0, consumers_1.buffer)(stream);
|
|
215
|
+
return new Blob([bytes]);
|
|
216
|
+
}
|
|
168
217
|
async function appendFormValue(form, key, value) {
|
|
169
218
|
if (value === undefined || value === null)
|
|
170
219
|
return;
|
|
@@ -180,6 +229,10 @@ async function appendFormValue(form, key, value) {
|
|
|
180
229
|
form.append(key, value);
|
|
181
230
|
return;
|
|
182
231
|
}
|
|
232
|
+
if (value instanceof ReadableStream || typeof value?.pipe === "function") {
|
|
233
|
+
form.append(key, await streamToBlob(value));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
183
236
|
if (value && typeof value === "object" && "source" in value) {
|
|
184
237
|
const upload = value;
|
|
185
238
|
if (typeof upload.source === "string" && ((0, node_path_1.isAbsolute)(upload.source) || upload.source.startsWith("./") || upload.source.startsWith("../"))) {
|
|
@@ -188,6 +241,18 @@ async function appendFormValue(form, key, value) {
|
|
|
188
241
|
form.append(key, new Blob([bytes]), upload.filename ?? (0, node_path_1.basename)(filePath));
|
|
189
242
|
return;
|
|
190
243
|
}
|
|
244
|
+
if (upload.filename !== undefined && upload.source instanceof Uint8Array) {
|
|
245
|
+
form.append(key, new Blob([upload.source]), upload.filename);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (upload.filename !== undefined && upload.source instanceof Blob) {
|
|
249
|
+
form.append(key, upload.source, upload.filename);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (upload.filename !== undefined && upload.source instanceof ArrayBuffer) {
|
|
253
|
+
form.append(key, new Blob([upload.source]), upload.filename);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
191
256
|
return appendFormValue(form, key, upload.source);
|
|
192
257
|
}
|
|
193
258
|
if (typeof value === "string") {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Context = void 0;
|
|
4
|
+
const promises_1 = require("node:fs/promises");
|
|
4
5
|
class Context {
|
|
5
6
|
update;
|
|
6
7
|
api;
|
|
@@ -178,6 +179,20 @@ class Context {
|
|
|
178
179
|
async getUserProfilePhotos(userId = this.from?.id, extra = {}) { if (!userId)
|
|
179
180
|
throw new Error("No user in this update."); return this.api.call("getUserProfilePhotos", { user_id: userId, ...extra }); }
|
|
180
181
|
async getFile(fileId) { return this.api.methods.getFile({ file_id: fileId }); }
|
|
182
|
+
/**
|
|
183
|
+
* Resolves `fileId` with `getFile` and downloads the raw bytes (see
|
|
184
|
+
* `Bot.downloadFile`). Pass `destination` to persist the bytes to a local
|
|
185
|
+
* file path. Prefer this over reading `file_path` manually — it throws
|
|
186
|
+
* precise `TelegramError`s when Telegram returns no path.
|
|
187
|
+
*/
|
|
188
|
+
async downloadFile(fileId, options = {}) {
|
|
189
|
+
const downloaded = await this.api.downloadFile(fileId, options.signal !== undefined ? { signal: options.signal } : {});
|
|
190
|
+
if (options.destination !== undefined) {
|
|
191
|
+
await (0, promises_1.writeFile)(options.destination, downloaded.bytes);
|
|
192
|
+
return { ...downloaded, savedTo: options.destination };
|
|
193
|
+
}
|
|
194
|
+
return downloaded;
|
|
195
|
+
}
|
|
181
196
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
182
197
|
// Admin & moderation (full Telegraf-parity surface; chat defaults to ctx.chat)
|
|
183
198
|
// ─────────────────────────────────────────────────────────────────────────────
|
package/dist-cjs/src/core/bot.js
CHANGED
|
@@ -16,6 +16,7 @@ const broadcast_js_1 = require("../broadcast/broadcast.js");
|
|
|
16
16
|
const concurrency_js_1 = require("../utils/concurrency.js");
|
|
17
17
|
const webhook_reply_js_1 = require("./webhook-reply.js");
|
|
18
18
|
const node_async_hooks_1 = require("node:async_hooks");
|
|
19
|
+
const promises_1 = require("node:fs/promises");
|
|
19
20
|
/** Thrown when a single update exceeds `handlerTimeout`; the handler keeps running in the background. */
|
|
20
21
|
class UpdateTimeoutError extends Error {
|
|
21
22
|
name = "UpdateTimeoutError";
|
|
@@ -264,6 +265,21 @@ class Bot {
|
|
|
264
265
|
async getMe() { const me = await this.api.methods.getMe(); this.me = me; return me; }
|
|
265
266
|
async setCommands(commands, scope, languageCode) { return this.api.call("setMyCommands", { commands, scope, language_code: languageCode }); }
|
|
266
267
|
async deleteCommands(scope, languageCode) { return this.api.call("deleteMyCommands", { scope, language_code: languageCode }); }
|
|
268
|
+
/**
|
|
269
|
+
* Downloads a Telegram file by `file_id`: resolves it with `getFile`, then
|
|
270
|
+
* fetches the raw bytes via the transport's download endpoint. Pass
|
|
271
|
+
* `destination` to also persist the bytes to a local file path. The
|
|
272
|
+
* returned `url` is valid for at least one hour; Telegram caps downloads
|
|
273
|
+
* at 20 MB.
|
|
274
|
+
*/
|
|
275
|
+
async downloadFile(fileId, options = {}) {
|
|
276
|
+
const downloaded = await this.api.downloadFile(fileId, options.signal !== undefined ? { signal: options.signal } : {});
|
|
277
|
+
if (options.destination !== undefined) {
|
|
278
|
+
await (0, promises_1.writeFile)(options.destination, downloaded.bytes);
|
|
279
|
+
return { ...downloaded, savedTo: options.destination };
|
|
280
|
+
}
|
|
281
|
+
return downloaded;
|
|
282
|
+
}
|
|
267
283
|
/**
|
|
268
284
|
* Handles a single update. Updates for different chats run in parallel;
|
|
269
285
|
* updates for the same chat are processed strictly in arrival order so
|
package/dist-cjs/src/index.js
CHANGED
|
@@ -29,6 +29,7 @@ __exportStar(require("./queue/queue.js"), exports);
|
|
|
29
29
|
__exportStar(require("./plugins/plugin.js"), exports);
|
|
30
30
|
__exportStar(require("./webhook/handler.js"), exports);
|
|
31
31
|
__exportStar(require("./utils/text.js"), exports);
|
|
32
|
+
__exportStar(require("./utils/files.js"), exports);
|
|
32
33
|
__exportStar(require("./utils/concurrency.js"), exports);
|
|
33
34
|
__exportStar(require("./broadcast/broadcast.js"), exports);
|
|
34
35
|
__exportStar(require("./state/conversation.js"), exports);
|
package/dist-cjs/src/testing.js
CHANGED
|
@@ -9,9 +9,15 @@ const bot_js_1 = require("./core/bot.js");
|
|
|
9
9
|
const context_js_1 = require("./context/context.js");
|
|
10
10
|
class MockTransport {
|
|
11
11
|
calls = [];
|
|
12
|
+
/** Downloads recorded through `download()` (the `file_path` values, in order). */
|
|
13
|
+
downloads = [];
|
|
14
|
+
/** Bytes returned by `download()`; defaults to the UTF-8 encoding of the file path. */
|
|
15
|
+
downloadBytes;
|
|
12
16
|
responses = new Map();
|
|
13
17
|
respond(method, response) { this.responses.set(method, response); return this; }
|
|
14
18
|
async request(request) { this.calls.push(request); const configured = this.responses.get(request.method); const data = typeof configured === "function" ? configured(request.payload) : configured ?? { ok: true, result: true }; return { status: data.ok ? 200 : (data.error_code ?? 500), headers: new Headers(), data: data }; }
|
|
19
|
+
fileUrl(filePath) { return `mock://files/${filePath}`; }
|
|
20
|
+
async download(filePath) { this.downloads.push(filePath); return this.downloadBytes ?? new TextEncoder().encode(filePath); }
|
|
15
21
|
}
|
|
16
22
|
exports.MockTransport = MockTransport;
|
|
17
23
|
function createMockUpdate(overrides = {}) { return { update_id: 1, message: { message_id: 1, date: Date.now(), chat: { id: 1, type: "private" }, from: { id: 2, is_bot: false, first_name: "Test" }, text: "/start" }, ...overrides }; }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* File-upload validation helpers. Telegram caps uploads at 50 MB for most
|
|
4
|
+
* methods (10 MB for photos) and rejects mismatches between content and
|
|
5
|
+
* declared type; these helpers let applications enforce their own limits
|
|
6
|
+
* before a byte leaves the process.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.UploadValidationError = void 0;
|
|
10
|
+
exports.validateUpload = validateUpload;
|
|
11
|
+
exports.assertValidUpload = assertValidUpload;
|
|
12
|
+
/** Thrown by {@link assertValidUpload} when an upload violates the rules. */
|
|
13
|
+
class UploadValidationError extends Error {
|
|
14
|
+
name = "UploadValidationError";
|
|
15
|
+
issues;
|
|
16
|
+
constructor(issues) {
|
|
17
|
+
super(`Upload rejected: ${issues.map((issue) => issue.message).join("; ")}`);
|
|
18
|
+
this.issues = issues;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.UploadValidationError = UploadValidationError;
|
|
22
|
+
function normalizeExtension(extension) {
|
|
23
|
+
const value = extension.trim().toLowerCase();
|
|
24
|
+
return value.startsWith(".") ? value.slice(1) : value;
|
|
25
|
+
}
|
|
26
|
+
function mimeMatches(pattern, mimeType) {
|
|
27
|
+
const normalizedPattern = pattern.trim().toLowerCase();
|
|
28
|
+
if (normalizedPattern.endsWith("/*"))
|
|
29
|
+
return mimeType.toLowerCase().startsWith(normalizedPattern.slice(0, -1));
|
|
30
|
+
return mimeType.toLowerCase() === normalizedPattern;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Validates an upload against the rules and returns every violation found
|
|
34
|
+
* (an empty array means the upload is acceptable).
|
|
35
|
+
*/
|
|
36
|
+
function validateUpload(upload, rules) {
|
|
37
|
+
const issues = [];
|
|
38
|
+
if (rules.maxBytes !== undefined && upload.sizeBytes !== undefined && upload.sizeBytes > rules.maxBytes) {
|
|
39
|
+
issues.push({ code: "too_large", field: "sizeBytes", message: `size ${upload.sizeBytes} bytes exceeds the ${rules.maxBytes} byte limit`, actual: upload.sizeBytes, limit: rules.maxBytes });
|
|
40
|
+
}
|
|
41
|
+
if (rules.allowedMimeTypes !== undefined && rules.allowedMimeTypes.length > 0 && upload.mimeType !== undefined && !rules.allowedMimeTypes.some((pattern) => mimeMatches(pattern, upload.mimeType))) {
|
|
42
|
+
issues.push({ code: "mime_not_allowed", field: "mimeType", message: `MIME type ${upload.mimeType} is not allowed (allowed: ${rules.allowedMimeTypes.join(", ")})`, actual: upload.mimeType, limit: rules.allowedMimeTypes.join(", ") });
|
|
43
|
+
}
|
|
44
|
+
if (rules.allowedExtensions !== undefined && rules.allowedExtensions.length > 0 && upload.fileName !== undefined) {
|
|
45
|
+
const extension = upload.fileName.includes(".") ? upload.fileName.slice(upload.fileName.lastIndexOf(".") + 1) : "";
|
|
46
|
+
const normalized = normalizeExtension(extension);
|
|
47
|
+
if (!normalized || !rules.allowedExtensions.some((allowed) => normalizeExtension(allowed) === normalized)) {
|
|
48
|
+
issues.push({ code: "extension_not_allowed", field: "fileName", message: `extension .${normalized || "?"} is not allowed (allowed: ${rules.allowedExtensions.join(", ")})`, actual: upload.fileName, limit: rules.allowedExtensions.join(", ") });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return issues;
|
|
52
|
+
}
|
|
53
|
+
/** Like {@link validateUpload} but throws {@link UploadValidationError} when any rule is violated. */
|
|
54
|
+
function assertValidUpload(upload, rules) {
|
|
55
|
+
const issues = validateUpload(upload, rules);
|
|
56
|
+
if (issues.length > 0)
|
|
57
|
+
throw new UploadValidationError(issues);
|
|
58
|
+
}
|
package/docs/API.id.md
CHANGED
|
@@ -274,6 +274,22 @@ deleteCommands(
|
|
|
274
274
|
|
|
275
275
|
Jalan pintas ke `deleteMyCommands`.
|
|
276
276
|
|
|
277
|
+
### `bot.downloadFile(fileId, options?)`
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
downloadFile(
|
|
281
|
+
fileId: string,
|
|
282
|
+
options?: { signal?: AbortSignal; destination?: string },
|
|
283
|
+
): Promise<DownloadedFile>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Me-resolve `fileId` lewat `getFile`, lalu mengunduh byte mentahnya melalui endpoint download transport. Berikan `destination` untuk juga menyimpan byte ke path file lokal (`savedTo` terisi pada hasil). Melempar `TelegramError` (kind `validation`) saat Telegram tidak mengembalikan `file_path` atau transport tidak bisa mengunduh, dan `TelegramNetworkError` saat unduhan gagal. Telegram membatasi unduhan pada 20 MB; `url` hasilnya tetap valid minimal satu jam.
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
const file = await bot.downloadFile(photoFileId, { destination: "downloads/photo.jpg" });
|
|
290
|
+
console.log(file.fileName, file.sizeBytes, file.url, file.savedTo);
|
|
291
|
+
```
|
|
292
|
+
|
|
277
293
|
### `bot.handleUpdate(update)`
|
|
278
294
|
|
|
279
295
|
```ts
|
|
@@ -593,6 +609,37 @@ raw(
|
|
|
593
609
|
|
|
594
610
|
Memanggil method string sembarang di transport. Gunakan ini untuk method Telegram atau parameter baru yang belum masuk `TelegramMethodMap`. Response `ok: false` tetap diubah menjadi `TelegramError`.
|
|
595
611
|
|
|
612
|
+
### `api.downloadFile(fileId, options?)`
|
|
613
|
+
|
|
614
|
+
```ts
|
|
615
|
+
downloadFile(fileId: string, options?: { signal?: AbortSignal }): Promise<DownloadedFile>
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
Inti `bot.downloadFile` di level API client: me-resolve `getFile`, memvalidasi `file_path` tersedia, lalu mengunduh byte melalui transport.
|
|
619
|
+
|
|
620
|
+
### `DownloadedFile`
|
|
621
|
+
|
|
622
|
+
```ts
|
|
623
|
+
interface DownloadedFile {
|
|
624
|
+
file: File; // objek File Telegram dari getFile
|
|
625
|
+
bytes: Uint8Array; // byte mentah file (maks 20 MB sesuai Telegram)
|
|
626
|
+
filePath: string; // file_path yang dipakai untuk unduhan
|
|
627
|
+
url: string; // URL unduhan langsung, valid minimal satu jam
|
|
628
|
+
fileName: string; // segmen path terakhir dari filePath
|
|
629
|
+
sizeBytes: number; // panjang byte
|
|
630
|
+
savedTo?: string; // terisi saat Bot.downloadFile menyimpan file ke disk
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
### `fetchTransport.fileUrl(filePath)` dan `fetchTransport.download(filePath, signal?)`
|
|
635
|
+
|
|
636
|
+
```ts
|
|
637
|
+
fileUrl(filePath: string): string
|
|
638
|
+
download(filePath: string, signal?: AbortSignal): Promise<Uint8Array>
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
`FetchTransport` memetakan base URL `/bot<token>` ke endpoint download `/file/bot<token>`; `download` melakukan GET byte (batas bawah timeout 120 detik untuk file besar) dan melempar `TelegramNetworkError` saat HTTP gagal. Keduanya member opsional pada interface `Transport`, jadi transport kustom boleh menghilangkannya — `downloadFile` lalu gagal dengan error validasi yang jelas, bukan crash.
|
|
642
|
+
|
|
596
643
|
### Parameter dan hasil bertipe yang tersedia
|
|
597
644
|
|
|
598
645
|
Tipe berikut dipetakan khusus pada rilis ini.
|
|
@@ -1498,6 +1545,27 @@ template("Halo {{ user.name }}", { user: { name: "Ayu" } });
|
|
|
1498
1545
|
|
|
1499
1546
|
---
|
|
1500
1547
|
|
|
1548
|
+
### `validateUpload(upload, rules)`
|
|
1549
|
+
|
|
1550
|
+
```ts
|
|
1551
|
+
validateUpload(upload: UploadLike, rules: UploadRules): UploadValidationIssue[]
|
|
1552
|
+
```
|
|
1553
|
+
|
|
1554
|
+
Memvalidasi unggahan sebelum dikirim: `maxBytes` (batas ukuran), `allowedMimeTypes` (persis atau wildcard seperti `image/*`), dan `allowedExtensions` (case-insensitive, dengan atau tanpa titik awal). Mengembalikan semua pelanggaran yang ditemukan — array kosong berarti unggahan diterima.
|
|
1555
|
+
|
|
1556
|
+
### `assertValidUpload(upload, rules)`
|
|
1557
|
+
|
|
1558
|
+
Aturan yang sama, tetapi melempar `UploadValidationError` (dengan seluruh `issues` terlampir) alih-alih mengembalikannya.
|
|
1559
|
+
|
|
1560
|
+
```ts
|
|
1561
|
+
import { assertValidUpload } from "@xbibzlibrary/telebibz";
|
|
1562
|
+
|
|
1563
|
+
assertValidUpload(
|
|
1564
|
+
{ sizeBytes: fileBytes.length, mimeType: "image/png", fileName: "logo.png" },
|
|
1565
|
+
{ maxBytes: 5_000_000, allowedMimeTypes: ["image/png", "image/jpeg"], allowedExtensions: [".png", ".jpg"] },
|
|
1566
|
+
);
|
|
1567
|
+
```
|
|
1568
|
+
|
|
1501
1569
|
## 14. Testing utilities
|
|
1502
1570
|
|
|
1503
1571
|
Import dari `@xbibzlibrary/telebibz/testing` atau root package.
|
|
@@ -1524,6 +1592,8 @@ const transport = new MockTransport()
|
|
|
1524
1592
|
});
|
|
1525
1593
|
```
|
|
1526
1594
|
|
|
1595
|
+
`MockTransport` juga mengimplementasikan member download opsional: `download(filePath)` mencatat path ke `downloads` dan mengembalikan `downloadBytes` (default: encoding UTF-8 dari path), serta `fileUrl(filePath)` mengembalikan `mock://files/<filePath>` — sehingga `bot.downloadFile()` sepenuhnya bisa dites tanpa jaringan.
|
|
1596
|
+
|
|
1527
1597
|
### `createMockUpdate(overrides?)`
|
|
1528
1598
|
|
|
1529
1599
|
```ts
|
package/docs/API.md
CHANGED
|
@@ -291,6 +291,22 @@ deleteCommands(
|
|
|
291
291
|
|
|
292
292
|
Shortcut to `deleteMyCommands`.
|
|
293
293
|
|
|
294
|
+
### `bot.downloadFile(fileId, options?)`
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
downloadFile(
|
|
298
|
+
fileId: string,
|
|
299
|
+
options?: { signal?: AbortSignal; destination?: string },
|
|
300
|
+
): Promise<DownloadedFile>
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Resolves `fileId` through `getFile`, then downloads the raw bytes via the transport's download endpoint. Pass `destination` to also persist the bytes to a local file path (`savedTo` is set on the result). Throws a `TelegramError` (kind `validation`) when Telegram returns no `file_path` or the transport cannot download, and a `TelegramNetworkError` when the download fails. Telegram caps downloads at 20 MB; the returned `url` stays valid for at least one hour.
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
const file = await bot.downloadFile(photoFileId, { destination: "downloads/photo.jpg" });
|
|
307
|
+
console.log(file.fileName, file.sizeBytes, file.url, file.savedTo);
|
|
308
|
+
```
|
|
309
|
+
|
|
294
310
|
### `bot.handleUpdate(update)`
|
|
295
311
|
|
|
296
312
|
```ts
|
|
@@ -610,6 +626,37 @@ raw(
|
|
|
610
626
|
|
|
611
627
|
Calls an arbitrary method string on the transport. Use this for Telegram methods or new parameters not yet included in `TelegramMethodMap`. Responses with `ok: false` are still converted into a `TelegramError`.
|
|
612
628
|
|
|
629
|
+
### `api.downloadFile(fileId, options?)`
|
|
630
|
+
|
|
631
|
+
```ts
|
|
632
|
+
downloadFile(fileId: string, options?: { signal?: AbortSignal }): Promise<DownloadedFile>
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
The API-client core of `bot.downloadFile`: resolves `getFile`, validates that a `file_path` came back, and downloads the bytes through the transport.
|
|
636
|
+
|
|
637
|
+
### `DownloadedFile`
|
|
638
|
+
|
|
639
|
+
```ts
|
|
640
|
+
interface DownloadedFile {
|
|
641
|
+
file: File; // Telegram File object from getFile
|
|
642
|
+
bytes: Uint8Array; // raw file bytes (max 20 MB per Telegram)
|
|
643
|
+
filePath: string; // file_path used for the download
|
|
644
|
+
url: string; // direct download URL, valid for at least one hour
|
|
645
|
+
fileName: string; // last path segment of filePath
|
|
646
|
+
sizeBytes: number; // byte length of bytes
|
|
647
|
+
savedTo?: string; // set when Bot.downloadFile persisted the file to disk
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### `fetchTransport.fileUrl(filePath)` and `fetchTransport.download(filePath, signal?)`
|
|
652
|
+
|
|
653
|
+
```ts
|
|
654
|
+
fileUrl(filePath: string): string
|
|
655
|
+
download(filePath: string, signal?: AbortSignal): Promise<Uint8Array>
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
`FetchTransport` maps its `/bot<token>` base URL to the `/file/bot<token>` download endpoint; `download` GETs the bytes (timeout floor of 120 s for large files) and throws `TelegramNetworkError` on HTTP failure. Both are optional members of the `Transport` interface, so custom transports may omit them — `downloadFile` then fails with a precise validation error instead of crashing.
|
|
659
|
+
|
|
613
660
|
### Available typed parameters and results
|
|
614
661
|
|
|
615
662
|
The following types are specially mapped in this release.
|
|
@@ -1532,6 +1579,27 @@ template("Halo {{ user.name }}", { user: { name: "Ayu" } });
|
|
|
1532
1579
|
|
|
1533
1580
|
---
|
|
1534
1581
|
|
|
1582
|
+
### `validateUpload(upload, rules)`
|
|
1583
|
+
|
|
1584
|
+
```ts
|
|
1585
|
+
validateUpload(upload: UploadLike, rules: UploadRules): UploadValidationIssue[]
|
|
1586
|
+
```
|
|
1587
|
+
|
|
1588
|
+
Validates an upload before it is sent: `maxBytes` (size limit), `allowedMimeTypes` (exact or wildcard like `image/*`), and `allowedExtensions` (case-insensitive, with or without the leading dot). Returns every violation found — an empty array means the upload is acceptable.
|
|
1589
|
+
|
|
1590
|
+
### `assertValidUpload(upload, rules)`
|
|
1591
|
+
|
|
1592
|
+
Same rules, but throws `UploadValidationError` (with all `issues` attached) instead of returning them.
|
|
1593
|
+
|
|
1594
|
+
```ts
|
|
1595
|
+
import { assertValidUpload } from "@xbibzlibrary/telebibz";
|
|
1596
|
+
|
|
1597
|
+
assertValidUpload(
|
|
1598
|
+
{ sizeBytes: fileBytes.length, mimeType: "image/png", fileName: "logo.png" },
|
|
1599
|
+
{ maxBytes: 5_000_000, allowedMimeTypes: ["image/png", "image/jpeg"], allowedExtensions: [".png", ".jpg"] },
|
|
1600
|
+
);
|
|
1601
|
+
```
|
|
1602
|
+
|
|
1535
1603
|
## 14. Testing utilities
|
|
1536
1604
|
|
|
1537
1605
|
Import dari `@xbibzlibrary/telebibz/testing` atau root package.
|
|
@@ -1558,6 +1626,8 @@ const transport = new MockTransport()
|
|
|
1558
1626
|
});
|
|
1559
1627
|
```
|
|
1560
1628
|
|
|
1629
|
+
`MockTransport` also implements the optional download members: `download(filePath)` records the path into `downloads` and returns `downloadBytes` (default: the UTF-8 encoding of the path), and `fileUrl(filePath)` returns `mock://files/<filePath>` — so `bot.downloadFile()` is fully testable without network access.
|
|
1630
|
+
|
|
1561
1631
|
### `createMockUpdate(overrides?)`
|
|
1562
1632
|
|
|
1563
1633
|
```ts
|
package/docs/API.zh-CN.md
CHANGED
|
@@ -274,6 +274,22 @@ deleteCommands(
|
|
|
274
274
|
|
|
275
275
|
相当于 `deleteMyCommands` 的快捷方式。
|
|
276
276
|
|
|
277
|
+
### `bot.downloadFile(fileId, options?)`
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
downloadFile(
|
|
281
|
+
fileId: string,
|
|
282
|
+
options?: { signal?: AbortSignal; destination?: string },
|
|
283
|
+
): Promise<DownloadedFile>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
通过 `getFile` 解析 `fileId`,再经由 transport 的下载端点获取原始字节。传入 `destination` 可同时把字节保存到本地文件路径(结果中的 `savedTo` 会被填充)。当 Telegram 未返回 `file_path` 或 transport 不支持下载时抛出 `TelegramError`(kind 为 `validation`);下载失败时抛出 `TelegramNetworkError`。Telegram 限制单次下载 20 MB;返回的 `url` 至少一小时内有效。
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
const file = await bot.downloadFile(photoFileId, { destination: "downloads/photo.jpg" });
|
|
290
|
+
console.log(file.fileName, file.sizeBytes, file.url, file.savedTo);
|
|
291
|
+
```
|
|
292
|
+
|
|
277
293
|
### `bot.handleUpdate(update)`
|
|
278
294
|
|
|
279
295
|
```ts
|
|
@@ -593,6 +609,37 @@ raw(
|
|
|
593
609
|
|
|
594
610
|
在 transport 上调用任意字符串方法。用于调用尚未包含在 `TelegramMethodMap` 的新的 Telegram 方法或参数。即使响应为 `ok: false`,也会被转换为 `TelegramError`。
|
|
595
611
|
|
|
612
|
+
### `api.downloadFile(fileId, options?)`
|
|
613
|
+
|
|
614
|
+
```ts
|
|
615
|
+
downloadFile(fileId: string, options?: { signal?: AbortSignal }): Promise<DownloadedFile>
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
`bot.downloadFile` 的 API 客户端核心:解析 `getFile`、校验返回了 `file_path`,然后通过 transport 下载字节。
|
|
619
|
+
|
|
620
|
+
### `DownloadedFile`
|
|
621
|
+
|
|
622
|
+
```ts
|
|
623
|
+
interface DownloadedFile {
|
|
624
|
+
file: File; // getFile 返回的 Telegram File 对象
|
|
625
|
+
bytes: Uint8Array; // 原始字节(Telegram 上限 20 MB)
|
|
626
|
+
filePath: string; // 用于下载的 file_path
|
|
627
|
+
url: string; // 直接下载 URL,至少一小时内有效
|
|
628
|
+
fileName: string; // filePath 的最后一段
|
|
629
|
+
sizeBytes: number; // bytes 的字节长度
|
|
630
|
+
savedTo?: string; // 当 Bot.downloadFile 把文件写入磁盘时填充
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
### `fetchTransport.fileUrl(filePath)` 与 `fetchTransport.download(filePath, signal?)`
|
|
635
|
+
|
|
636
|
+
```ts
|
|
637
|
+
fileUrl(filePath: string): string
|
|
638
|
+
download(filePath: string, signal?: AbortSignal): Promise<Uint8Array>
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
`FetchTransport` 把 `/bot<token>` 基础 URL 映射为 `/file/bot<token>` 下载端点;`download` 以 GET 获取字节(大文件的 timeout 下限为 120 秒),HTTP 失败时抛出 `TelegramNetworkError`。两者都是 `Transport` 接口的可选成员,自定义 transport 可以省略 —— 此时 `downloadFile` 会以明确的校验错误失败,而不是崩溃。
|
|
642
|
+
|
|
596
643
|
### 可用的类型化参数和返回值
|
|
597
644
|
|
|
598
645
|
以下类型在此发布版本中已被映射:
|
|
@@ -1492,6 +1539,27 @@ template("你好 {{ user.name }}", { user: { name: "Ayu" } });
|
|
|
1492
1539
|
|
|
1493
1540
|
---
|
|
1494
1541
|
|
|
1542
|
+
### `validateUpload(upload, rules)`
|
|
1543
|
+
|
|
1544
|
+
```ts
|
|
1545
|
+
validateUpload(upload: UploadLike, rules: UploadRules): UploadValidationIssue[]
|
|
1546
|
+
```
|
|
1547
|
+
|
|
1548
|
+
在发送前校验上传:`maxBytes`(大小上限)、`allowedMimeTypes`(精确或通配符如 `image/*`)、`allowedExtensions`(大小写不敏感,带不带前导点均可)。返回找到的全部违规 —— 空数组表示上传可接受。
|
|
1549
|
+
|
|
1550
|
+
### `assertValidUpload(upload, rules)`
|
|
1551
|
+
|
|
1552
|
+
规则相同,但不返回结果而是抛出 `UploadValidationError`(附带全部 `issues`)。
|
|
1553
|
+
|
|
1554
|
+
```ts
|
|
1555
|
+
import { assertValidUpload } from "@xbibzlibrary/telebibz";
|
|
1556
|
+
|
|
1557
|
+
assertValidUpload(
|
|
1558
|
+
{ sizeBytes: fileBytes.length, mimeType: "image/png", fileName: "logo.png" },
|
|
1559
|
+
{ maxBytes: 5_000_000, allowedMimeTypes: ["image/png", "image/jpeg"], allowedExtensions: [".png", ".jpg"] },
|
|
1560
|
+
);
|
|
1561
|
+
```
|
|
1562
|
+
|
|
1495
1563
|
## 14. Testing utilities
|
|
1496
1564
|
|
|
1497
1565
|
Import dari `@xbibzlibrary/telebibz/testing` atau root package.
|
|
@@ -1518,6 +1586,8 @@ const transport = new MockTransport()
|
|
|
1518
1586
|
});
|
|
1519
1587
|
```
|
|
1520
1588
|
|
|
1589
|
+
`MockTransport` 同样实现了可选的下载成员:`download(filePath)` 把路径记录进 `downloads` 并返回 `downloadBytes`(默认为路径的 UTF-8 编码),`fileUrl(filePath)` 返回 `mock://files/<filePath>` —— 因此 `bot.downloadFile()` 无需网络即可完整测试。
|
|
1590
|
+
|
|
1521
1591
|
### `createMockUpdate(overrides?)`
|
|
1522
1592
|
|
|
1523
1593
|
```ts
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Panduan cepat storage (Bahasa Indonesia)
|
|
2
|
+
|
|
3
|
+
telebibz menyediakan interface `Storage<K, V>` generik dengan lima adapter. Core package **tanpa runtime dependency**: adapter Redis, SQL, dan Mongo menerima driver interface kecil yang sudah Anda punya, jadi Anda yang memilih driver dan versinya.
|
|
4
|
+
|
|
5
|
+
Semua adapter memakai kontrak yang sama — `get` / `set` / `delete` / `has` / `clear` / `keys()` / `entries()` — ditambah **`update(key, updater, { ttlMs })`** yang menyalin penulisan per key sehingga update bersamaan ke key yang sama tidak pernah saling menimpa. TTL diatur per penulisan lewat `{ ttlMs }`.
|
|
6
|
+
|
|
7
|
+
## MemoryStorage (default — tanpa konfigurasi)
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Bot } from "@xbibzlibrary/telebibz";
|
|
11
|
+
|
|
12
|
+
const bot = new Bot({ token: process.env.TELEGRAM_BOT_TOKEN! });
|
|
13
|
+
// bot.session secara default adalah MemoryStorage<string, S>.
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## JsonFileStorage (persistensi satu file, tetap tanpa dependency)
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { Bot, JsonFileStorage } from "@xbibzlibrary/telebibz";
|
|
20
|
+
|
|
21
|
+
const bot = new Bot({
|
|
22
|
+
token: process.env.TELEGRAM_BOT_TOKEN!,
|
|
23
|
+
session: new JsonFileStorage("state/sessions.json"),
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## RedisStorage (bawa client Anda sendiri)
|
|
28
|
+
|
|
29
|
+
Adapter ini hanya butuh lima method callback-style yang dimiliki setiap client Redis — `node-redis` langsung cocok:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { Bot, RedisStorage } from "@xbibzlibrary/telebibz";
|
|
33
|
+
import { createClient } from "redis"; // driver dan versi pilihan Anda
|
|
34
|
+
|
|
35
|
+
const redis = createClient({ url: process.env.REDIS_URL });
|
|
36
|
+
await redis.connect();
|
|
37
|
+
|
|
38
|
+
const bot = new Bot({
|
|
39
|
+
token: process.env.TELEGRAM_BOT_TOKEN!,
|
|
40
|
+
session: new RedisStorage(redis, "mybot:"), // prefix untuk key Anda
|
|
41
|
+
});
|
|
42
|
+
// TTL per penulisan: await bot.session.set(key, value, { ttlMs: 24 * 60 * 60 * 1000 });
|
|
43
|
+
// (kedaluwarsa PX Redis diterapkan otomatis.)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## SqlStorage (semua database SQL)
|
|
47
|
+
|
|
48
|
+
Implementasikan driver lima method di atas library SQL Anda; contoh ini memakai `better-sqlite3`:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { Bot, SqlStorage } from "@xbibzlibrary/telebibz";
|
|
52
|
+
import Database from "better-sqlite3";
|
|
53
|
+
|
|
54
|
+
const db = new Database("state/bot.db");
|
|
55
|
+
db.exec("CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT NOT NULL, expires_at INTEGER)");
|
|
56
|
+
|
|
57
|
+
const storage = new SqlStorage({
|
|
58
|
+
async get(key) {
|
|
59
|
+
const row = db.prepare("SELECT value, expires_at FROM kv WHERE key = ?").get(key) as { value: string; expires_at: number | null } | undefined;
|
|
60
|
+
return row === undefined ? undefined : JSON.parse(row.value);
|
|
61
|
+
},
|
|
62
|
+
async set(key, value, expiresAt) {
|
|
63
|
+
db.prepare("INSERT INTO kv (key, value, expires_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at")
|
|
64
|
+
.run(key, JSON.stringify(value), expiresAt ?? null);
|
|
65
|
+
},
|
|
66
|
+
async delete(key) { return db.prepare("DELETE FROM kv WHERE key = ?").run(key).changes > 0; },
|
|
67
|
+
async has(key) { return db.prepare("SELECT 1 FROM kv WHERE key = ?").get(key) !== undefined; },
|
|
68
|
+
async clear() { db.prepare("DELETE FROM kv").run(); },
|
|
69
|
+
async entries() {
|
|
70
|
+
const rows = db.prepare("SELECT key, value, expires_at FROM kv").all() as Array<{ key: string; value: string; expires_at: number | null }>;
|
|
71
|
+
return rows.map((row) => [row.key, JSON.parse(row.value), row.expiresAt ?? undefined] as [string, unknown, number | undefined]);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const bot = new Bot({ token: process.env.TELEGRAM_BOT_TOKEN!, session: storage });
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## MongoStorage (bawa collection Anda sendiri)
|
|
79
|
+
|
|
80
|
+
Adapter ini berbicara langsung dengan bentuk collection MongoDB standar — cukup kirim collection Anda:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { Bot, MongoStorage } from "@xbibzlibrary/telebibz";
|
|
84
|
+
import { MongoClient } from "mongodb";
|
|
85
|
+
|
|
86
|
+
const client = new MongoClient(process.env.MONGODB_URL!);
|
|
87
|
+
await client.connect();
|
|
88
|
+
|
|
89
|
+
const bot = new Bot({
|
|
90
|
+
token: process.env.TELEGRAM_BOT_TOKEN!,
|
|
91
|
+
session: new MongoStorage(client.db("mybot").collection("sessions")),
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Memilih
|
|
96
|
+
|
|
97
|
+
| Adapter | Pakai saat | Persistensi | Dependency tambahan |
|
|
98
|
+
|---|---|---|---|
|
|
99
|
+
| `MemoryStorage` | bot satu proses, test | selama proses hidup | tidak ada |
|
|
100
|
+
| `JsonFileStorage` | bot kecil, deploy sederhana | file di disk | tidak ada |
|
|
101
|
+
| `RedisStorage` | multi-instance, state bersama | Redis | client Redis Anda |
|
|
102
|
+
| `SqlStorage` | aplikasi berbasis SQL | semua database SQL | driver SQL Anda |
|
|
103
|
+
| `MongoStorage` | stack Mongo yang sudah ada | MongoDB | driver Mongo Anda |
|
|
104
|
+
|
|
105
|
+
Signature API lengkap: [API.id.md](API.id.md). English: [STORAGE.md](STORAGE.md) · 简体中文: [STORAGE.zh-CN.md](STORAGE.zh-CN.md).
|