From 4606093f161a2c8ba6e76fc46f7fc87c95d64be2 Mon Sep 17 00:00:00 2001 From: Roj Date: Sun, 12 May 2024 18:42:56 +0300 Subject: [PATCH 1/5] Add support for downloading files --- MTKruto | 2 +- allowed_methods.ts | 1 + client_manager.ts | 12 +++++ download_manager.ts | 111 ++++++++++++++++++++++++++++++++++++++++++++ main.ts | 30 +++++++++++- worker.ts | 49 +++++++++++++++---- 6 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 download_manager.ts diff --git a/MTKruto b/MTKruto index be7485d..c47bc53 160000 --- a/MTKruto +++ b/MTKruto @@ -1 +1 @@ -Subproject commit be7485df397152b6cd6d67e0db84fe9a2d339185 +Subproject commit c47bc533aa96b1325bfee487e04e5e19751945de diff --git a/allowed_methods.ts b/allowed_methods.ts index 3e209c8..8adc698 100644 --- a/allowed_methods.ts +++ b/allowed_methods.ts @@ -107,6 +107,7 @@ export const ALLOWED_METHODS = [ "removeStoryFromHighlights", "blockUser", "unblockUser", + "download", "downloadLiveStreamChunk", "getLiveStreamChannels", "getVideoChat", diff --git a/client_manager.ts b/client_manager.ts index a8bbc89..09926de 100644 --- a/client_manager.ts +++ b/client_manager.ts @@ -36,6 +36,8 @@ import { import { StorageDenoKV } from "mtkruto/storage/1_storage_deno_kv.ts"; import { transportProviderTcp } from "mtkruto/transport/3_transport_provider_tcp.ts"; +import { DownloadManager } from "./download_manager.ts"; + export interface ClientStats { connected: boolean; me: User; @@ -87,6 +89,16 @@ export class ClientManager { }; } + #downloadManagers = new Map(); + async download(id: string, fileId: string) { + const client = await this.getClient(id); + let downloadManager = this.#downloadManagers.get(client); + if (!downloadManager) { + downloadManager = new DownloadManager(client); + } + return downloadManager.download(fileId); + } + async getClient(id: string) { { const client = this.#clients.get(id); diff --git a/download_manager.ts b/download_manager.ts new file mode 100644 index 0000000..52aa245 --- /dev/null +++ b/download_manager.ts @@ -0,0 +1,111 @@ +import * as path from "std/path/mod.ts"; +import { exists, existsSync } from "std/fs/mod.ts"; + +import { Client } from "mtkruto/mod.ts"; +import { Queue } from "mtkruto/1_utilities.ts"; + +export class DownloadManager { + #client: Client; + static DOWNLOADS_PATH = path.join(Deno.cwd(), ".downloads"); + + constructor(client: Client) { + this.#client = client; + if (!existsSync(DownloadManager.DOWNLOADS_PATH)) { + Deno.mkdirSync(DownloadManager.DOWNLOADS_PATH); + } + } + + async *download(fileId: string) { + const dir = path.join(DownloadManager.DOWNLOADS_PATH, fileId); + if (!await exists(dir)) { + await Deno.mkdir(dir); + } + let n = 0; + let offset = 0; + const haveAllParts = await exists(path.join(dir, "_all")); + let partsAvailable = 0; + for await (const entry of Deno.readDir(dir)) { + if (entry.name.startsWith("_") || !entry.isFile) { + continue; + } + if (entry.name == partsAvailable + "") { + ++partsAvailable; + const { size } = await Deno.stat(path.join(dir, entry.name)); + offset += size; + } + } + let download: Download | undefined; + if (!haveAllParts) { + download = this.#startDownload(fileId, partsAvailable, offset); + } + for (let i = 0; i < partsAvailable; ++i) { + const part = await Deno.readFile(path.join(dir, i + "")); + offset += part.byteLength; + ++n; + yield part; + } + if (download) { + while (true) { + if (download.partsAvailable > n) { + if (await exists(path.join(dir, n + ""))) { + const part = await Deno.readFile(path.join(dir, n + "")); + ++n; + yield part; + } + } else if (download.haveAllParts) { + break; + } + await new Promise((r) => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + download.addEventListener("partAvailable", () => { + clearTimeout(timeout); + r(); + }, { once: true, signal: controller.signal }); + }); + } + } + } + + #downloadQueue = new Queue("downloads"); + #downloads = new Map(); + #startDownload(fileId: string, partsAvailable: number, offset: number) { + let download = this.#downloads.get(fileId); + if (!download) { + download = new Download(this.#client, fileId, partsAvailable, offset); + this.#downloads.set(fileId, download); + this.#downloadQueue.add(() => + download!.start().finally(() => this.#downloads.delete(fileId)) + ); + } + return download; + } +} + +class Download extends EventTarget { + haveAllParts = false; + + constructor( + private client: Client, + private fileId: string, + public partsAvailable: number, + private offset: number, + ) { + super(); + } + + async start() { + const dir = path.join(DownloadManager.DOWNLOADS_PATH, this.fileId); + if (!await exists(dir)) { + await Deno.mkdir(dir); + } + for await ( + const chunk of this.client.download(this.fileId, { offset: this.offset }) + ) { + await Deno.writeFile(path.join(dir, "" + this.partsAvailable), chunk); + ++this.partsAvailable; + this.dispatchEvent(new Event("partAvailable")); + } + await Deno.writeFile(path.join(dir, "_all"), new Uint8Array()); + } +} diff --git a/main.ts b/main.ts index 4f46996..e2563ed 100644 --- a/main.ts +++ b/main.ts @@ -155,8 +155,36 @@ async function handleMethod( const result = await workers.call(worker, "serve", id, method, params); if (result === "DROP") { return drop(); - } else { + } else if (Array.isArray(result)) { return Response.json(...result); + } else { + const firstChunk = await workers.call(worker, "next", result.streamId); + if (firstChunk == null) { + return badRequest("Invalid stream ID"); + } + return new Response( + new ReadableStream({ + start(controller) { + controller.enqueue(firstChunk.value); + if (firstChunk.done) { + controller.close(); + } + }, + async pull(controller) { + const chunk = await workers.call(worker, "next", result.streamId); + if (chunk == null) { + controller.close(); + } else { + if (chunk.value) { + controller.enqueue(chunk.value); + } + if (chunk.done) { + controller.close(); + } + } + }, + }), + ); } } diff --git a/worker.ts b/worker.ts index 5c98aca..928899b 100644 --- a/worker.ts +++ b/worker.ts @@ -28,10 +28,9 @@ import { InputError } from "mtkruto/0_errors.ts"; import { setLogVerbosity } from "mtkruto/1_utilities.ts"; import { functions, setLoggingProvider, types } from "mtkruto/mod.ts"; -import { serialize } from "./tl_json.ts"; -import { deserialize } from "./tl_json.ts"; import { transform } from "./transform.ts"; import { fileLogger } from "./file_logger.ts"; +import { deserialize, serialize } from "./tl_json.ts"; import { isFunctionDisallowed } from "./disallowed_functions.ts"; import { ClientManager, ClientStats } from "./client_manager.ts"; import { ALLOWED_METHODS, AllowedMethod } from "./allowed_methods.ts"; @@ -82,6 +81,7 @@ const handlers = { init, clientCount, serve, + next, stats, getUpdates, invoke, @@ -152,23 +152,56 @@ async function serve( id: string, method: AllowedMethod, args: any[], -): Promise<"DROP" | Parameters> { +): Promise< + "DROP" | Parameters | { streamId: string } +> { if (!id.trim() || !method.trim()) { return "DROP"; } if (!(ALLOWED_METHODS.includes(method))) { return "DROP"; } - const client = await clientManager.getClient(id); - // deno-lint-ignore ban-ts-comment - // @ts-ignore - const result = transform(await client[method](...args)); + let result; + if (method == "download") { + result = await clientManager.download(id, args[0]); + } else { + const client = await clientManager.getClient(id); + // deno-lint-ignore ban-ts-comment + // @ts-ignore + result = transform(await client[method](...args)); + } if (result !== undefined) { - return [result]; + if ( + typeof result === "object" && result != null && + Symbol.asyncIterator in result + ) { + return { streamId: getStreamId(result) }; + } else { + return [result]; + } } else { return [null]; } } +const streams = new Map>(); +function getStreamId(iterable: AsyncIterable) { + const id = crypto.randomUUID(); + streams.set(id, iterable[Symbol.asyncIterator]()); + return id; +} + +async function next(streamId: string) { + const result = await streams.get(streamId)?.next(); + + if (result === undefined) { + return null; + } else { + if (result.done) { + streams.delete(streamId); + } + return result; + } +} async function invoke( id: string, From 75ec341907476f5afffc6f43c29de0b2ebee29ae Mon Sep 17 00:00:00 2001 From: Roj Date: Tue, 14 May 2024 16:18:18 +0300 Subject: [PATCH 2/5] Add missing license header --- download_manager.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/download_manager.ts b/download_manager.ts index 52aa245..cf85346 100644 --- a/download_manager.ts +++ b/download_manager.ts @@ -1,3 +1,23 @@ +/** + * MTKruto Server + * Copyright (C) 2024 Roj + * + * This file is part of MTKruto Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import * as path from "std/path/mod.ts"; import { exists, existsSync } from "std/fs/mod.ts"; From 5020c234cdb34e08a97414b5f5e5e5feda12c7fc Mon Sep 17 00:00:00 2001 From: Roj Date: Tue, 14 May 2024 16:37:05 +0300 Subject: [PATCH 3/5] Fix various issues with getUpdates --- client_manager.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/client_manager.ts b/client_manager.ts index 09926de..1edce1d 100644 --- a/client_manager.ts +++ b/client_manager.ts @@ -238,17 +238,12 @@ export class ClientManager { if (this.#webhooks.has(client)) { throw new InputError("getUpdates is not allowed when a webhook is set."); } - - if (this.#polls.has(client)) { + { const controller = this.#getUpdatesControllers.get(client); if (controller) { controller.abort(); } this.#getUpdatesControllers.delete(client); - // just in case - this.#polls.delete(client); - this.#updateResolvers.get(client)?.(); - this.#updateResolvers.delete(client); } this.#polls.add(client); let controller: AbortController | null = null; @@ -288,7 +283,6 @@ export class ClientManager { return []; } } finally { - this.#updateResolvers.delete(client); this.#polls.delete(client); this.#lastGetUpdates.set(client, new Date()); if (timeout != null) { From 4d72dac6776f78c2f81b406f7a9e3775f0aca89c Mon Sep 17 00:00:00 2001 From: Roj Date: Tue, 14 May 2024 16:54:52 +0300 Subject: [PATCH 4/5] Update MTKruto --- MTKruto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MTKruto b/MTKruto index c47bc53..51f5f05 160000 --- a/MTKruto +++ b/MTKruto @@ -1 +1 @@ -Subproject commit c47bc533aa96b1325bfee487e04e5e19751945de +Subproject commit 51f5f0582c417ace4c5f0988d083db325581e5b4 From bfe4738f6bf1fb2c7e75e372b10f9fbefeb68d53 Mon Sep 17 00:00:00 2001 From: Roj Date: Sat, 18 May 2024 14:17:14 +0300 Subject: [PATCH 5/5] Use latest MTKruto --- MTKruto | 2 +- client_manager.ts | 2 +- disallowed_functions.ts | 5 +- tests/tl_json/deserialize_test.ts | 138 ---------------------------- tests/tl_json/serialize_test.ts | 114 ------------------------ tests/transform_test.ts | 8 +- tl_json.ts | 143 ------------------------------ transform.ts | 18 +++- worker.ts | 19 ++-- 9 files changed, 37 insertions(+), 412 deletions(-) delete mode 100644 tests/tl_json/deserialize_test.ts delete mode 100644 tests/tl_json/serialize_test.ts delete mode 100644 tl_json.ts diff --git a/MTKruto b/MTKruto index 51f5f05..14f6ef4 160000 --- a/MTKruto +++ b/MTKruto @@ -1 +1 @@ -Subproject commit 51f5f0582c417ace4c5f0988d083db325581e5b4 +Subproject commit 14f6ef49659e1995082ab565c6897d43275b8737 diff --git a/client_manager.ts b/client_manager.ts index 1edce1d..1f5ef8e 100644 --- a/client_manager.ts +++ b/client_manager.ts @@ -23,11 +23,11 @@ import * as path from "std/path/mod.ts"; import { existsSync } from "std/fs/exists.ts"; import { unreachable } from "std/assert/unreachable.ts"; +import { InputError } from "mtkruto/0_errors.ts"; import { Mutex, Queue } from "mtkruto/1_utilities.ts"; import { Client, errors, - InputError, InvokeErrorHandler, NetworkStatistics, Update, diff --git a/disallowed_functions.ts b/disallowed_functions.ts index bb82308..3ad9da0 100644 --- a/disallowed_functions.ts +++ b/disallowed_functions.ts @@ -18,7 +18,6 @@ * along with this program. If not, see . */ -import { functions, name } from "mtkruto/mod.ts"; import { isMtprotoFunction } from "mtkruto/client/0_utilities.ts"; const DISALLOWED_FUNCTIONS = [ @@ -66,11 +65,11 @@ const DISALLOWED_FUNCTIONS = [ ]; export function isFunctionDisallowed(function_: any) { - if (function_ instanceof functions.ping) { + if (function_._ == "ping") { return false; } - if (DISALLOWED_FUNCTIONS.includes(function_[name])) { + if (DISALLOWED_FUNCTIONS.includes(function_._)) { return true; } diff --git a/tests/tl_json/deserialize_test.ts b/tests/tl_json/deserialize_test.ts deleted file mode 100644 index 788d082..0000000 --- a/tests/tl_json/deserialize_test.ts +++ /dev/null @@ -1,138 +0,0 @@ -/** - * MTKruto Server - * Copyright (C) 2024 Roj - * - * This file is part of MTKruto Server. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { assertEquals, assertThrows } from "std/assert/mod.ts"; - -import { functions, types } from "mtkruto/mod.ts"; - -import { deserialize } from "../../tl_json.ts"; - -Deno.test("errors on null, symbol, undefined", () => { - assertThrows(() => deserialize(null)); - assertThrows(() => deserialize(Symbol.for("err"))); - assertThrows(() => deserialize(undefined)); -}); - -Deno.test("returns strings as-is", () => { - for (let i = 0; i < 100; ++i) { - const id = crypto.randomUUID(); - assertEquals(deserialize(id), id); - } -}); - -Deno.test("returns true as-is", () => { - assertEquals(deserialize(true), true); -}); - -Deno.test("returns false as undefined", () => { - assertEquals(deserialize(false), undefined); -}); - -Deno.test("errors on objects with no _ field", () => { - assertThrows(() => deserialize({})); - assertThrows(() => deserialize({ a: 1 })); -}); - -Deno.test("errors on BigInt objects with no value field", () => { - assertThrows(() => deserialize({ _: "bigint" })); -}); - -Deno.test("errors on BigInt objects with non-string value field", () => { - assertThrows(() => deserialize({ _: "bigint", value: 1 })); -}); -Deno.test("errors on BigInt objects with empty value field", () => { - assertThrows(() => deserialize({ _: "bigint", value: "" })); -}); - -Deno.test("properly deserializes BigInt objects", () => { - for (let i = 0; i < 1000; ++i) { - const bi = BigInt(Math.ceil(Math.random() * 1000)); - assertEquals(deserialize({ _: "bigint", value: String(bi) }), bi); - } -}); -Deno.test("errors on bytes objects with no value field", () => { - assertThrows(() => deserialize({ _: "bytes" })); -}); - -Deno.test("errors on bytes objects with non-string value field", () => { - assertThrows(() => deserialize({ _: "bytes", value: 1 })); -}); -Deno.test("errors on bytes objects with empty value field", () => { - assertThrows(() => deserialize({ _: "bytes", value: "" })); -}); - -Deno.test("properly deserializes bytes objects", () => { - const enc = new TextEncoder(); - for (let i = 0; i < 1000; ++i) { - const bytes = Math.ceil(Math.random() * 1000) + ""; - assertEquals( - deserialize({ _: "bytes", value: btoa(bytes) }), - enc.encode(bytes), - ); - } -}); - -Deno.test("properly deserializes functions", () => { - for (let i = 0; i < 1000; ++i) { - const pingId = BigInt(1 + Math.ceil(Math.random() * 1000)); - assertEquals( - deserialize({ _: "ping", ping_id: { _: "bigint", value: pingId + "" } }), - new functions.ping({ ping_id: pingId }), - ); - } - - const actual = { - _: "messages.sendMedia", - random_id: { - _: "bigint", - value: "123", - }, - peer: { - _: "inputPeerSelf", - }, - media: { - _: "inputMediaPhoto", - id: { - _: "inputPhoto", - id: { _: "bigint", value: "123" }, - access_hash: { _: "bigint", value: "123" }, - file_reference: { _: "bytes", value: btoa("R") }, - }, - spoiler: true, - }, - message: "Test", - silent: false, - }; - const expected = new functions.messages.sendMedia({ - random_id: 123n, - peer: new types.InputPeerSelf(), - media: new types.InputMediaPhoto({ - id: new types.InputPhoto({ - id: 123n, - access_hash: 123n, - file_reference: new Uint8Array([0x52]), - }), - spoiler: true, - }), - message: "Test", - silent: undefined, - }); - assertEquals(deserialize(actual), expected); -}); diff --git a/tests/tl_json/serialize_test.ts b/tests/tl_json/serialize_test.ts deleted file mode 100644 index 20bc064..0000000 --- a/tests/tl_json/serialize_test.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * MTKruto Server - * Copyright (C) 2024 Roj - * - * This file is part of MTKruto Server. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { assertEquals } from "std/assert/mod.ts"; -import { encodeBase64 } from "std/encoding/base64.ts"; - -import { functions, types } from "mtkruto/mod.ts"; - -import { serialize } from "../../tl_json.ts"; - -Deno.test("string", () => { - assertEquals(serialize("string"), "string"); -}); - -Deno.test("number", () => { - assertEquals(serialize("number"), "number"); -}); - -Deno.test("bigint", () => { - assertEquals(serialize(123n), { _: "bigint", value: "123" }); -}); - -Deno.test("boolean", () => { - assertEquals(serialize(true), true); - assertEquals(serialize(false), false); -}); - -Deno.test("undefined", () => { - assertEquals(serialize(undefined), false); -}); - -Deno.test("bytes", () => { - const enc = new TextEncoder(); - const bytes = enc.encode("Hello, world!"); - assertEquals(serialize(bytes), { _: "bytes", value: encodeBase64(bytes) }); -}); - -Deno.test("object", () => { - const actual = new functions.messages.sendMedia({ - random_id: 123n, - peer: new types.InputPeerSelf(), - media: new types.InputMediaPhoto({ - id: new types.InputPhoto({ - id: 123n, - access_hash: 123n, - file_reference: new Uint8Array([0x52]), - }), - spoiler: true, - }), - message: "Test", - silent: undefined, - }); - const expected = { - _: "messages.sendMedia", - background: false, - clear_draft: false, - entities: false, - invert_media: false, - media: { - _: "inputMediaPhoto", - id: { - _: "inputPhoto", - access_hash: { - _: "bigint", - value: "123", - }, - file_reference: { - _: "bytes", - value: "Ug==", - }, - id: { - _: "bigint", - value: "123", - }, - }, - spoiler: true, - ttl_seconds: false, - }, - message: "Test", - noforwards: false, - peer: { - _: "inputPeerSelf", - }, - quick_reply_shortcut: false, - random_id: { - _: "bigint", - value: "123", - }, - reply_markup: false, - reply_to: false, - schedule_date: false, - send_as: false, - silent: false, - update_stickersets_order: false, - }; - assertEquals(serialize(actual), expected); -}); diff --git a/tests/transform_test.ts b/tests/transform_test.ts index aa7bb66..b07812e 100644 --- a/tests/transform_test.ts +++ b/tests/transform_test.ts @@ -24,6 +24,12 @@ import { transform } from "../transform.ts"; Deno.test("transform", () => { const date = new Date(); - const a = { _: { _: { _: date, a: [date, date] } } }; + const bigint = 123123n; + const buffer = crypto.getRandomValues(new Uint8Array(1024)); + const a = { + _: { + _: { _: date, a: [buffer, date, date], bigint, c: bigint, x: buffer }, + }, + }; assertEquals(transform(transform(a)), a); }); diff --git a/tl_json.ts b/tl_json.ts deleted file mode 100644 index d86a7ac..0000000 --- a/tl_json.ts +++ /dev/null @@ -1,143 +0,0 @@ -/** - * MTKruto Server - * Copyright (C) 2024 Roj - * - * This file is part of MTKruto Server. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import { decodeBase64, encodeBase64 } from "std/encoding/base64.ts"; - -import { functions, InputError, name, types } from "mtkruto/mod.ts"; - -function collectObjects(types: any, map: Record) { - for (const type of Object.values(types)) { - const name_: string | undefined = (type as any)[name]; - if (name_) { - map[name_] = type; - } - if ((type as any).constructor == Object) { - collectObjects(type, map); - } - } -} - -export const typeNameMap = {} as Record; -export const functionNameMap = {} as Record; -collectObjects(types, typeNameMap); -collectObjects(functions, functionNameMap); - -export function deserialize(value: any): any { - if (typeof value === "string") { - return value; - } else if (typeof value === "number") { - return value; - } else if (typeof value === "boolean") { - return value ? true : undefined; - } else if (typeof value === "object") { - if (!("_" in value)) { - throw new InputError("Expected object to contain the field _"); - } - if (typeof value._ !== "string") { - throw new InputError("Expected the _ field to be string"); - } - if (value._ == "bigint") { - if (!("value" in value)) { - throw new InputError( - "Expected BigInt object to include the field value", - ); - } - if (typeof value.value !== "string") { - throw new InputError("Expected BigInt value to be string"); - } - const v = value.value.trim(); - if (!v.trim()) { - throw new InputError("Expected BigInt value to be non-empty"); - } - try { - return BigInt(v); - } catch { - throw new InputError(`Invalid BigInt: ${v}`); - } - } - if (value._ == "bytes") { - if (!("value" in value)) { - throw new InputError( - "Expected bytes object to include the field value", - ); - } - if (typeof value.value !== "string") { - throw new InputError("Expected bytes value to be string"); - } - const v = value.value.trim(); - if (!v.trim()) { - throw new InputError("Expected bytes value to be non-empty"); - } - try { - return decodeBase64(v); - } catch { - throw new InputError("Expected bytes value to be vaild Base64"); - } - } - - if (!(value._ in typeNameMap) && !(value._ in functionNameMap)) { - throw new InputError(`Invalid type: ${value._}`); - } - - // @ts-ignore: no idea why it errors, but trust me it works - const params = Object.entries(value) - .filter(([k]) => k != "_") - .map(([k, v]) => [k, deserialize(v)]); - // @ts-ignore: too complex to represent, I feel you - const constructor = value._ in typeNameMap - ? typeNameMap[value._] - : functionNameMap[value._]; - return new constructor(Object.fromEntries(params)); - } else { - throw new InputError(`Unexpected value: ${typeof value}`); - } -} - -export function serialize(value: any): any { - if (typeof value === "string") { - return value; - } else if (typeof value === "number") { - return value; - } else if (typeof value === "boolean") { - return value; - } else if (value === undefined) { - return false; - } else if (typeof value === "bigint") { - return { - _: "bigint", - value: value + "", - }; - } else if (value instanceof Uint8Array) { - return { - _: "bytes", - value: encodeBase64(value), - }; - } - - // @ts-ignore: please - const params = Object.entries(value) - .filter(([k]) => k !== "__R") - .map(([k, v]) => [k, serialize(v)]); - - return { - _: value.constructor[name], - ...Object.fromEntries(params), - }; -} diff --git a/transform.ts b/transform.ts index 2ffd3fc..e2b95de 100644 --- a/transform.ts +++ b/transform.ts @@ -18,6 +18,8 @@ * along with this program. If not, see . */ +import { decodeBase64, encodeBase64 } from "std/encoding/base64.ts"; + /** * Utility function to transform dates in objects into a JSON-(de)serializable format and vice-verca. */ @@ -27,14 +29,28 @@ export function transform(a: any) { if (a[key] != null && typeof a[key] === "object") { if (a[key] instanceof Date) { a[key] = { _: "date", value: a[key].toJSON() }; + } else if (a[key] instanceof Uint8Array) { + a[key] = { _: "bytes", value: encodeBase64(a[key]) }; } else if ( - "_" in a[key] && a[key] == "date" && "value" in a[key] && + "_" in a[key] && a[key]._ == "date" && "value" in a[key] && typeof a[key].value === "string" ) { a[key] = new Date(a[key].value); + } else if ( + "_" in a[key] && a[key]._ == "bigint" && "value" in a[key] && + typeof a[key].value === "string" + ) { + a[key] = BigInt(a[key].value); + } else if ( + "_" in a[key] && a[key]._ == "bytes" && "value" in a[key] && + typeof a[key].value === "string" + ) { + a[key] = decodeBase64(a[key].value); } else { transform(a[key]); } + } else if (typeof a[key] === "bigint") { + a[key] = { _: "bigint", value: String(a[key]) }; } } } diff --git a/worker.ts b/worker.ts index 928899b..39e9e68 100644 --- a/worker.ts +++ b/worker.ts @@ -26,11 +26,10 @@ import { existsSync } from "std/fs/mod.ts"; import { InputError } from "mtkruto/0_errors.ts"; import { setLogVerbosity } from "mtkruto/1_utilities.ts"; -import { functions, setLoggingProvider, types } from "mtkruto/mod.ts"; +import { errors, isValidType, setLoggingProvider } from "mtkruto/mod.ts"; import { transform } from "./transform.ts"; import { fileLogger } from "./file_logger.ts"; -import { deserialize, serialize } from "./tl_json.ts"; import { isFunctionDisallowed } from "./disallowed_functions.ts"; import { ClientManager, ClientStats } from "./client_manager.ts"; import { ALLOWED_METHODS, AllowedMethod } from "./allowed_methods.ts"; @@ -60,9 +59,9 @@ addEventListener("message", async (e) => { status: 400, headers: { "x-error-type": "input" }, }]; - } else if (err instanceof types.Rpc_error) { - result = [err.error_message, { - status: err.error_code, + } else if (err instanceof errors.TelegramError) { + result = [err.errorMessage, { + status: err.errorCode, headers: { "x-error-type": "rpc" }, }]; } else { @@ -207,15 +206,15 @@ async function invoke( id: string, function_: any, ): Promise> { - const function__ = deserialize(function_); - if (!(function__ instanceof functions.Function)) { - throw new InputError("Expected a function"); + function_ = transform(function_); + if (!isValidType(function_)) { + throw new InputError("Invalid function"); } - if (isFunctionDisallowed(function__)) { + if (isFunctionDisallowed(function_)) { throw new InputError("Unallowed function"); } const client = await clientManager.getClient(id); - const result = serialize(await client.invoke(function__)); + const result = transform(await client.invoke(function_)); if (result !== undefined) { return [result]; } else {