From a2739b678c016b4bb9862ce20b25567c715fb80d Mon Sep 17 00:00:00 2001 From: Alina Sireneva Date: Wed, 28 Feb 2024 00:33:23 +0300 Subject: [PATCH] chore!: started extracting platform-specific stuff into separate packages --- .config/eslint.cjs | 4 +- packages/core/package.json | 42 ++---- packages/core/scripts/generate-client.cjs | 37 +++-- packages/core/src/highlevel/client.ts | 35 ++--- packages/core/src/highlevel/index.ts | 1 - packages/core/src/highlevel/methods.ts | 14 +- .../core/src/highlevel/methods/_imports.ts | 2 +- packages/core/src/highlevel/methods/_init.ts | 43 ++---- .../methods/bots/get-callback-answer.ts | 4 +- .../highlevel/methods/files/download-file.ts | 33 +---- .../methods/files/download-file.web.ts | 5 - .../methods/files/download-iterable.ts | 3 +- .../methods/files/normalize-input-media.ts | 3 +- .../highlevel/methods/files/upload-file.ts | 16 --- .../src/highlevel/types/bots/keyboards.ts | 4 +- .../src/highlevel/types/media/document.ts | 5 +- .../src/highlevel/types/media/thumbnail.ts | 9 +- .../src/highlevel/types/peers/chat-photo.ts | 5 +- .../highlevel/types/updates/callback-query.ts | 4 +- .../src/highlevel/utils/convert-file-id.ts | 13 +- .../src/highlevel/utils/file-type.test.ts | 7 +- .../src/highlevel/utils/file-utils.test.ts | 35 ++--- .../core/src/highlevel/utils/file-utils.ts | 14 +- .../core/src/highlevel/utils/inline-utils.ts | 7 +- .../core/src/highlevel/utils/inspectable.ts | 4 +- .../src/highlevel/utils/platform/storage.ts | 7 - .../highlevel/utils/platform/storage.web.ts | 11 -- .../src/highlevel/utils/string-session.ts | 7 +- .../src/highlevel/utils/voice-utils.test.ts | 9 +- .../highlevel/worker/platform/connect.web.ts | 4 +- packages/core/src/network/auth-key.test.ts | 39 +++--- packages/core/src/network/client.ts | 10 +- packages/core/src/network/network-manager.ts | 17 +-- .../core/src/network/session-connection.ts | 2 +- packages/core/src/network/transports/index.ts | 7 - .../network/transports/intermediate.test.ts | 34 ++--- .../src/network/transports/obfuscated.test.ts | 29 ++-- .../src/network/transports/websocket.test.ts | 11 +- packages/core/src/platform.ts | 34 +++++ packages/core/src/storage/index.ts | 3 +- .../storage/{providers => }/memory/driver.ts | 2 +- .../storage/{providers => }/memory/index.ts | 4 +- .../{providers => }/memory/memory.test.ts | 0 .../memory/repository/auth-keys.ts | 2 +- .../{providers => }/memory/repository/kv.ts | 2 +- .../memory/repository/peers.ts | 2 +- .../memory/repository/ref-messages.ts | 2 +- .../src/storage/service/utils.test-utils.ts | 2 +- packages/core/src/storage/storage.ts | 5 +- packages/core/src/utils/bigint-utils.test.ts | 12 +- packages/core/src/utils/binary/asn1-parser.ts | 6 +- packages/core/src/utils/crypto/abstract.ts | 2 - .../src/utils/crypto/factorization.test.ts | 5 +- packages/core/src/utils/crypto/index.ts | 6 +- packages/core/src/utils/crypto/keys.test.ts | 7 +- packages/core/src/utils/crypto/keys.ts | 11 +- .../src/utils/crypto/miller-rabin.test.ts | 5 +- .../core/src/utils/crypto/mtproto.test.ts | 25 ++-- .../core/src/utils/crypto/password.test.ts | 30 ++-- packages/core/src/utils/crypto/password.ts | 6 +- packages/core/src/utils/crypto/utils.test.ts | 47 ++++--- packages/core/src/utils/crypto/wasm.ts | 23 +-- packages/core/src/utils/dcs.ts | 4 - .../utils/{platform => }/error-reporting.ts | 2 +- packages/core/src/utils/index.ts | 7 +- packages/core/src/utils/logger.ts | 35 ++--- packages/core/src/utils/platform/crypto.ts | 4 - .../core/src/utils/platform/crypto.web.ts | 11 -- packages/core/src/utils/platform/transport.ts | 4 - .../core/src/utils/platform/transport.web.ts | 14 -- packages/crypto-node/package.json | 8 +- packages/crypto-node/src/index.ts | 4 +- .../src/callback-data-builder.test.ts | 4 +- packages/dispatcher/src/context/base.ts | 3 +- .../dispatcher/src/context/callback-query.ts | 3 +- .../src/context/chat-join-request.ts | 3 +- .../src/context/chosen-inline-result.ts | 3 +- .../dispatcher/src/context/inline-query.ts | 3 +- packages/dispatcher/src/context/message.ts | 9 +- packages/dispatcher/src/context/parse.ts | 3 +- .../src/context/pre-checkout-query.ts | 3 +- packages/dispatcher/src/dispatcher.ts | 2 +- packages/dispatcher/src/handler.ts | 2 +- packages/file-id/package.json | 3 + packages/file-id/src/parse.test.ts | 10 +- packages/file-id/src/parse.ts | 50 ++++--- packages/file-id/src/serialize-unique.test.ts | 4 +- packages/file-id/src/serialize-unique.ts | 11 +- packages/file-id/src/serialize.ts | 8 +- packages/file-id/src/utils.test.ts | 26 ++-- packages/http-proxy/index.ts | 8 +- packages/http-proxy/package.json | 2 +- packages/mtproxy/fake-tls.ts | 4 +- packages/mtproxy/index.ts | 6 +- packages/mtproxy/package.json | 2 +- packages/node/package.json | 10 +- packages/node/{index.ts => src/client.ts} | 42 ++++-- packages/node/src/index.ts | 8 ++ packages/node/src/methods.ts | 5 + packages/node/src/methods/download-file.ts | 40 ++++++ packages/node/src/methods/upload-file.ts | 41 ++++++ packages/node/src/platform.ts | 78 +++++++++++ packages/node/{ => src}/utils.ts | 0 .../src/utils/crypto.test.ts} | 2 +- .../node.ts => node/src/utils/crypto.ts} | 26 ++-- .../platform => node/src/utils}/exit-hook.ts | 0 .../platform => node/src/utils}/logging.ts | 2 +- packages/node/src/utils/stream-utils.ts | 26 ++++ .../transports => node/src/utils}/tcp.test.ts | 14 +- .../transports => node/src/utils}/tcp.ts | 6 +- packages/node/tsconfig.json | 6 +- packages/socks-proxy/index.ts | 13 +- packages/socks-proxy/package.json | 2 +- packages/sqlite/src/driver.ts | 4 +- packages/sqlite/src/repository/kv.ts | 5 +- packages/test/package.json | 13 ++ packages/test/src/client.test.ts | 4 +- packages/test/src/client.ts | 2 + packages/test/src/crypto.ts | 101 +++++++------- packages/test/src/index.ts | 2 + packages/test/src/platform.test.ts | 60 ++++++++ packages/test/src/platform.ts | 4 + packages/test/src/platform.web.ts | 4 + packages/test/src/transport.test.ts | 2 + .../tl-runtime/src/platform.test-utils.ts | 9 -- packages/tl-runtime/src/platform.ts | 6 + packages/tl-runtime/src/reader.test.ts | 63 ++++----- packages/tl-runtime/src/reader.ts | 13 +- packages/tl-runtime/src/writer.test.ts | 15 +- packages/tl-runtime/src/writer.ts | 25 ++-- packages/tl/package.json | 1 + packages/tl/scripts/gen-rsa-keys.ts | 2 +- packages/wasm/package.json | 4 + packages/wasm/src/index.ts | 12 +- packages/wasm/src/types.ts | 2 +- packages/web/README.md | 23 +++ packages/web/build.config.cjs | 1 + packages/web/package.json | 36 +++++ .../web.test.ts => web/src/crypto.test.ts} | 2 +- .../utils/crypto/web.ts => web/src/crypto.ts} | 21 ++- packages/web/src/encodings/base64.ts | 132 ++++++++++++++++++ packages/web/src/encodings/hex.ts | 78 +++++++++++ packages/web/src/encodings/utf8.ts | 24 ++++ .../exit-hook.web.ts => web/src/exit-hook.ts} | 0 .../providers => web/src}/idb/driver.ts | 4 +- .../providers => web/src}/idb/idb.test.ts | 0 .../providers => web/src}/idb/index.ts | 3 +- .../src}/idb/repository/auth-keys.ts | 3 +- .../src}/idb/repository/kv.ts | 3 +- .../src}/idb/repository/peers.ts | 3 +- .../src}/idb/repository/ref-messages.ts | 3 +- .../providers => web/src}/idb/utils.ts | 0 packages/web/src/index.ts | 2 + .../logging.web.ts => web/src/logging.ts} | 2 +- packages/web/src/platform.ts | 51 +++++++ packages/web/src/wasm.ts | 42 ++++++ packages/web/tsconfig.json | 13 ++ packages/web/typedoc.cjs | 10 ++ packages/web/utils.ts | 1 + pnpm-lock.yaml | 49 +++++-- 160 files changed, 1452 insertions(+), 772 deletions(-) delete mode 100644 packages/core/src/highlevel/methods/files/download-file.web.ts delete mode 100644 packages/core/src/highlevel/utils/platform/storage.ts delete mode 100644 packages/core/src/highlevel/utils/platform/storage.web.ts create mode 100644 packages/core/src/platform.ts rename packages/core/src/storage/{providers => }/memory/driver.ts (87%) rename packages/core/src/storage/{providers => }/memory/index.ts (87%) rename packages/core/src/storage/{providers => }/memory/memory.test.ts (100%) rename packages/core/src/storage/{providers => }/memory/repository/auth-keys.ts (96%) rename packages/core/src/storage/{providers => }/memory/repository/kv.ts (89%) rename packages/core/src/storage/{providers => }/memory/repository/peers.ts (95%) rename packages/core/src/storage/{providers => }/memory/repository/ref-messages.ts (92%) rename packages/core/src/utils/{platform => }/error-reporting.ts (95%) delete mode 100644 packages/core/src/utils/platform/crypto.ts delete mode 100644 packages/core/src/utils/platform/crypto.web.ts delete mode 100644 packages/core/src/utils/platform/transport.ts delete mode 100644 packages/core/src/utils/platform/transport.web.ts rename packages/node/{index.ts => src/client.ts} (66%) create mode 100644 packages/node/src/index.ts create mode 100644 packages/node/src/methods.ts create mode 100644 packages/node/src/methods/download-file.ts create mode 100644 packages/node/src/methods/upload-file.ts create mode 100644 packages/node/src/platform.ts rename packages/node/{ => src}/utils.ts (100%) rename packages/{core/src/utils/crypto/node.test.ts => node/src/utils/crypto.test.ts} (83%) rename packages/{core/src/utils/crypto/node.ts => node/src/utils/crypto.ts} (83%) rename packages/{core/src/utils/platform => node/src/utils}/exit-hook.ts (100%) rename packages/{core/src/utils/platform => node/src/utils}/logging.ts (95%) create mode 100644 packages/node/src/utils/stream-utils.ts rename packages/{core/src/network/transports => node/src/utils}/tcp.test.ts (92%) rename packages/{core/src/network/transports => node/src/utils}/tcp.ts (93%) create mode 100644 packages/test/src/platform.test.ts create mode 100644 packages/test/src/platform.ts create mode 100644 packages/test/src/platform.web.ts delete mode 100644 packages/tl-runtime/src/platform.test-utils.ts create mode 100644 packages/web/README.md create mode 100644 packages/web/build.config.cjs create mode 100644 packages/web/package.json rename packages/{core/src/utils/crypto/web.test.ts => web/src/crypto.test.ts} (90%) rename packages/{core/src/utils/crypto/web.ts => web/src/crypto.ts} (75%) create mode 100644 packages/web/src/encodings/base64.ts create mode 100644 packages/web/src/encodings/hex.ts create mode 100644 packages/web/src/encodings/utf8.ts rename packages/{core/src/utils/platform/exit-hook.web.ts => web/src/exit-hook.ts} (100%) rename packages/{core/src/storage/providers => web/src}/idb/driver.ts (97%) rename packages/{core/src/storage/providers => web/src}/idb/idb.test.ts (100%) rename packages/{core/src/storage/providers => web/src}/idb/index.ts (94%) rename packages/{core/src/storage/providers => web/src}/idb/repository/auth-keys.ts (97%) rename packages/{core/src/storage/providers => web/src}/idb/repository/kv.ts (94%) rename packages/{core/src/storage/providers => web/src}/idb/repository/peers.ts (94%) rename packages/{core/src/storage/providers => web/src}/idb/repository/ref-messages.ts (95%) rename packages/{core/src/storage/providers => web/src}/idb/utils.ts (100%) create mode 100644 packages/web/src/index.ts rename packages/{core/src/utils/platform/logging.web.ts => web/src/logging.ts} (95%) create mode 100644 packages/web/src/platform.ts create mode 100644 packages/web/src/wasm.ts create mode 100644 packages/web/tsconfig.json create mode 100644 packages/web/typedoc.cjs create mode 100644 packages/web/utils.ts diff --git a/.config/eslint.cjs b/.config/eslint.cjs index 8f16012a..48b52cb1 100644 --- a/.config/eslint.cjs +++ b/.config/eslint.cjs @@ -254,7 +254,7 @@ module.exports = { }, }, { - files: ['**/scripts/**', '*.test.ts', 'packages/create-*/**', '**/build.config.cjs'], + files: ['**/scripts/**', '*.test.ts', 'packages/create-*/**', '**/build.config.cjs', 'packages/node/**'], rules: { 'no-console': 'off', 'no-restricted-imports': [ @@ -273,7 +273,7 @@ module.exports = { }, }, { - files: ['e2e/**'], + files: ['e2e/**', 'packages/node/**'], rules: { 'no-restricted-globals': 'off', }, diff --git a/packages/core/package.json b/packages/core/package.json index 32849987..294c5a03 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -12,18 +12,12 @@ "gen-client": "node ./scripts/generate-client.cjs", "gen-updates": "node ./scripts/generate-updates.cjs" }, - "browser": { - "./src/utils/platform/crypto.js": "./src/utils/platform/crypto.web.js", - "./src/utils/platform/transport.js": "./src/utils/platform/transport.web.js", - "./src/utils/platform/logging.js": "./src/utils/platform/logging.web.js", - "./src/utils/platform/random.js": "./src/utils/platform/random.web.js", - "./src/utils/platform/exit-hook.js": "./src/utils/platform/exit-hook.web.js", - "./src/highlevel/worker/platform/connect.js": "./src/highlevel/worker/platform/connect.web.js", - "./src/highlevel/worker/platform/register.js": "./src/highlevel/worker/platform/register.web.js", - "./src/highlevel/methods/files/_platform.js": "./src/highlevel/methods/files/_platform.web.js", - "./src/highlevel/methods/files/download-file.js": "./src/highlevel/methods/files/download-file.web.js", - "./src/highlevel/utils/platform/storage.js": "./src/highlevel/utils/platform/storage.web.js", - "./src/storage/json-file.js": false + "exports": { + ".": "./src/index.ts", + "./utils.js": "./src/utils/index.ts", + "./client.js": "./src/highlevel/client.ts", + "./methods.js": "./src/highlevel/methods.ts", + "./platform.js": "./src/platform.ts" }, "distOnlyFields": { "exports": { @@ -35,25 +29,17 @@ "import": "./esm/utils/index.js", "require": "./cjs/utils/index.js" }, - "./utils/crypto/*": { - "import": "./esm/utils/crypto/*", - "require": "./cjs/utils/crypto/*" - }, - "./network/transports/*": { - "import": "./esm/network/transports/*", - "require": "./cjs/network/transports/*" - }, - "./storage/*": { - "import": "./esm/storage/*", - "require": "./cjs/storage/*" - }, - "./highlevel/*": { - "import": "./esm/highlevel/*", - "require": "./cjs/highlevel/*" - }, "./methods.js": { "import": "./esm/highlevel/methods.js", "require": "./cjs/highlevel/methods.js" + }, + "./platform.js": { + "import": "./esm/platform.js", + "require": "./cjs/platform.js" + }, + "./client.js": { + "import": "./esm/highlevel/client.js", + "require": "./cjs/highlevel/client.js" } } }, diff --git a/packages/core/scripts/generate-client.cjs b/packages/core/scripts/generate-client.cjs index 3772b8f0..cd8f20c2 100644 --- a/packages/core/scripts/generate-client.cjs +++ b/packages/core/scripts/generate-client.cjs @@ -273,6 +273,7 @@ async function addSingleMethod(state, fileName) { } const isExported = (stmt.modifiers || []).find((mod) => mod.kind === ts.SyntaxKind.ExportKeyword) + const isDeclare = (stmt.modifiers || []).find((mod) => mod.kind === ts.SyntaxKind.DeclareKeyword) const isInitialize = checkForFlag(stmt, '@initialize') const isManualImpl = checkForFlag(stmt, '@manual-impl') const isInitializeSuper = isInitialize === 'super' @@ -327,7 +328,7 @@ async function addSingleMethod(state, fileName) { }) } - if (!isExported) continue + if (!isExported && !isDeclare) continue const firstArg = stmt.parameters[0] @@ -344,7 +345,7 @@ async function addSingleMethod(state, fileName) { state.methods.used[name] = relPath } - if (isExported) { + if (isExported || isDeclare) { const isPrivate = checkForFlag(stmt, '@internal') const isManual = checkForFlag(stmt, '@manual') const isNoemit = checkForFlag(stmt, '@noemit') @@ -358,6 +359,7 @@ async function addSingleMethod(state, fileName) { isPrivate, isManual, isNoemit, + isDeclare, shouldEmit, func: stmt, comment: getLeadingComments(stmt), @@ -369,12 +371,14 @@ async function addSingleMethod(state, fileName) { hasOverloads: hasOverloads[name] && !isOverload, }) - if (!(module in state.imports)) { - state.imports[module] = new Set() - } + if (!isDeclare) { + if (!(module in state.imports)) { + state.imports[module] = new Set() + } - if (!isManual || isManual.split('=')[1] !== 'noemit') { - state.imports[module].add(name) + if (!isManual || isManual.split('=')[1] !== 'noemit') { + state.imports[module].add(name) + } } } } @@ -399,6 +403,9 @@ async function addSingleMethod(state, fileName) { } state.imports[module].add(stmt.name.escapedText) + + state.exported[module] = state.exported[module] || new Set() + state.exported[module].add(stmt.name.escapedText) continue } @@ -429,6 +436,9 @@ async function addSingleMethod(state, fileName) { } state.imports[module].add(stmt.name.escapedText) + + state.exported[module] = state.exported[module] || new Set() + state.exported[module].add(stmt.name.escapedText) } else if (isCopy) { state.copy.push({ from: relPath, code: stmt.getFullText().trim() }) } else if (isTypeExported) { @@ -442,6 +452,7 @@ async function main() { const output = fs.createWriteStream(targetFile) const state = { imports: {}, + exported: {}, fields: [], init: [], methods: { @@ -527,6 +538,7 @@ on(name: string, handler: (...args: any[]) => void): this\n`) available, rawApiMethods, dependencies, + isDeclare, }) => { if (!available && !overload) { // no @available directive @@ -659,7 +671,7 @@ on(name: string, handler: (...args: any[]) => void): this\n`) output.write(`${name}${generics}(${parameters})${returnType}\n`) } - if (!overload && !isManual) { + if (!overload && !isManual && !isDeclare) { if (hasOverloads) { classProtoDecls.push('// @ts-expect-error this kinda breaks typings for overloads, idc') } @@ -737,9 +749,14 @@ on(name: string, handler: (...args: any[]) => void): this\n`) const outputMethods = fs.createWriteStream(targetFileMethods) outputMethods.write('/* THIS FILE WAS AUTO-GENERATED */\n') - state.methods.list.forEach(({ module, name, overload }) => { - if (overload) return + state.methods.list.forEach(({ module, name, overload, isDeclare }) => { + if (overload || isDeclare) return outputMethods.write(`export { ${name} } from '${module}'\n`) + + if (state.exported[module]) { + outputMethods.write(`export type { ${[...state.exported[module]].join(', ')} } from '${module}'\n`) + delete state.exported[module] + } }) await new Promise((resolve) => { outputMethods.end(resolve) }) diff --git a/packages/core/src/highlevel/client.ts b/packages/core/src/highlevel/client.ts index 2eefb85b..c4db21fe 100644 --- a/packages/core/src/highlevel/client.ts +++ b/packages/core/src/highlevel/client.ts @@ -7,8 +7,7 @@ import Long from 'long' import { tdFileId } from '@mtcute/file-id' import { tl } from '@mtcute/tl' -import { MemoryStorage } from '../storage/providers/memory/index.js' -import { MaybeArray, MaybePromise, PartialExcept, PartialOnly } from '../types/index.js' +import { MaybeArray, MaybePromise, MtUnsupportedError, PartialExcept, PartialOnly } from '../types/index.js' import { BaseTelegramClient, BaseTelegramClientOptions } from './base.js' import { ITelegramClient } from './client.types.js' import { checkPassword } from './methods/auth/check-password.js' @@ -95,7 +94,6 @@ import { getPeerDialogs } from './methods/dialogs/get-peer-dialogs.js' import { iterDialogs } from './methods/dialogs/iter-dialogs.js' import { setFoldersOrder } from './methods/dialogs/set-folders-order.js' import { downloadAsBuffer } from './methods/files/download-buffer.js' -import { downloadToFile } from './methods/files/download-file.js' import { downloadAsIterable } from './methods/files/download-iterable.js' import { downloadAsStream } from './methods/files/download-stream.js' import { _normalizeInputFile } from './methods/files/normalize-input-file.js' @@ -314,21 +312,19 @@ import { UserTypingUpdate, } from './types/index.js' import { makeParsedUpdateHandler, ParsedUpdateHandlerParams } from './updates/parsed.js' -import { _defaultStorageFactory } from './utils/platform/storage.js' import { StringSessionData } from './utils/string-session.js' // from methods/_init.ts // @copy type TelegramClientOptions = ( - | (Omit & { + | (PartialOnly, 'transport' | 'crypto'> & { /** * Storage to use for this client. * - * If a string is passed, it will be used as: - * - a path to a JSON file for Node.js - * - IndexedDB database name for browsers + * If a string is passed, it will be used as + * a name for the default platform-specific storage provider to use. * - * If omitted, {@link MemoryStorage} is used + * @default `"client.session"` */ storage?: string | ITelegramStorageProvider }) @@ -2250,6 +2246,7 @@ export interface TelegramClient extends ITelegramClient { * @param params File download parameters */ downloadAsBuffer(location: FileDownloadLocation, params?: FileDownloadParameters): Promise + /** * Download a remote file to a local file (only for NodeJS). * Promise will resolve once the download is complete. @@ -5140,20 +5137,13 @@ export class TelegramClient extends EventEmitter implements ITelegramClient { if ('client' in opts) { this._client = opts.client } else { - let storage: ITelegramStorageProvider - - if (typeof opts.storage === 'string') { - storage = _defaultStorageFactory(opts.storage) - } else if (!opts.storage) { - storage = new MemoryStorage() - } else { - storage = opts.storage + if (!opts.storage || typeof opts.storage === 'string' || !opts.transport || !opts.crypto) { + throw new MtUnsupportedError( + 'You need to explicitly provide storage, transport and crypto for @mtcute/core', + ) } - this._client = new BaseTelegramClient({ - ...opts, - storage, - }) + this._client = new BaseTelegramClient(opts as BaseTelegramClientOptions) } // @ts-expect-error codegen @@ -5448,9 +5438,6 @@ TelegramClient.prototype.setFoldersOrder = function (...args) { TelegramClient.prototype.downloadAsBuffer = function (...args) { return downloadAsBuffer(this._client, ...args) } -TelegramClient.prototype.downloadToFile = function (...args) { - return downloadToFile(this._client, ...args) -} TelegramClient.prototype.downloadAsIterable = function (...args) { return downloadAsIterable(this._client, ...args) } diff --git a/packages/core/src/highlevel/index.ts b/packages/core/src/highlevel/index.ts index 573e6f07..d96f9d22 100644 --- a/packages/core/src/highlevel/index.ts +++ b/packages/core/src/highlevel/index.ts @@ -1,5 +1,4 @@ export * from './base.js' -export * from './client.js' export * from './client.types.js' export * from './storage/index.js' export * from './types/index.js' diff --git a/packages/core/src/highlevel/methods.ts b/packages/core/src/highlevel/methods.ts index e0d5f6a3..0cf2563a 100644 --- a/packages/core/src/highlevel/methods.ts +++ b/packages/core/src/highlevel/methods.ts @@ -86,7 +86,6 @@ export { getPeerDialogs } from './methods/dialogs/get-peer-dialogs.js' export { iterDialogs } from './methods/dialogs/iter-dialogs.js' export { setFoldersOrder } from './methods/dialogs/set-folders-order.js' export { downloadAsBuffer } from './methods/files/download-buffer.js' -export { downloadToFile } from './methods/files/download-file.js' export { downloadAsIterable } from './methods/files/download-iterable.js' export { downloadAsStream } from './methods/files/download-stream.js' export { _normalizeInputFile } from './methods/files/normalize-input-file.js' @@ -96,6 +95,7 @@ export { uploadMedia } from './methods/files/upload-media.js' export { createForumTopic } from './methods/forums/create-forum-topic.js' export { deleteForumTopicHistory } from './methods/forums/delete-forum-topic-history.js' export { editForumTopic } from './methods/forums/edit-forum-topic.js' +export type { GetForumTopicsOffset } from './methods/forums/get-forum-topics.js' export { getForumTopics } from './methods/forums/get-forum-topics.js' export { getForumTopicsById } from './methods/forums/get-forum-topics-by-id.js' export { iterForumTopics } from './methods/forums/iter-forum-topics.js' @@ -109,6 +109,7 @@ export { editInviteLink } from './methods/invite-links/edit-invite-link.js' export { exportInviteLink } from './methods/invite-links/export-invite-link.js' export { getInviteLink } from './methods/invite-links/get-invite-link.js' export { getInviteLinkMembers } from './methods/invite-links/get-invite-link-members.js' +export type { GetInviteLinksOffset } from './methods/invite-links/get-invite-links.js' export { getInviteLinks } from './methods/invite-links/get-invite-links.js' export { getPrimaryInviteLink } from './methods/invite-links/get-primary-invite-link.js' export { hideAllJoinRequests } from './methods/invite-links/hide-all-join-requests.js' @@ -117,15 +118,18 @@ export { iterInviteLinkMembers } from './methods/invite-links/iter-invite-link-m export { iterInviteLinks } from './methods/invite-links/iter-invite-links.js' export { revokeInviteLink } from './methods/invite-links/revoke-invite-link.js' export { closePoll } from './methods/messages/close-poll.js' +export type { DeleteMessagesParams } from './methods/messages/delete-messages.js' export { deleteMessagesById } from './methods/messages/delete-messages.js' export { deleteMessages } from './methods/messages/delete-messages.js' export { deleteScheduledMessages } from './methods/messages/delete-scheduled-messages.js' export { editInlineMessage } from './methods/messages/edit-inline-message.js' export { editMessage } from './methods/messages/edit-message.js' +export type { ForwardMessageOptions } from './methods/messages/forward-messages.js' export { forwardMessagesById } from './methods/messages/forward-messages.js' export { forwardMessages } from './methods/messages/forward-messages.js' export { getCallbackQueryMessage } from './methods/messages/get-callback-query-message.js' export { getDiscussionMessage } from './methods/messages/get-discussion-message.js' +export type { GetHistoryOffset } from './methods/messages/get-history.js' export { getHistory } from './methods/messages/get-history.js' export { getMessageByLink } from './methods/messages/get-message-by-link.js' export { getMessageGroup } from './methods/messages/get-message-group.js' @@ -133,6 +137,7 @@ export { getMessageReactionsById } from './methods/messages/get-message-reaction export { getMessageReactions } from './methods/messages/get-message-reactions.js' export { getMessages } from './methods/messages/get-messages.js' export { getMessagesUnsafe } from './methods/messages/get-messages-unsafe.js' +export type { GetReactionUsersOffset } from './methods/messages/get-reaction-users.js' export { getReactionUsers } from './methods/messages/get-reaction-users.js' export { getReplyTo } from './methods/messages/get-reply-to.js' export { getScheduledMessages } from './methods/messages/get-scheduled-messages.js' @@ -143,7 +148,9 @@ export { iterSearchMessages } from './methods/messages/iter-search-messages.js' export { pinMessage } from './methods/messages/pin-message.js' export { readHistory } from './methods/messages/read-history.js' export { readReactions } from './methods/messages/read-reactions.js' +export type { SearchGlobalOffset } from './methods/messages/search-global.js' export { searchGlobal } from './methods/messages/search-global.js' +export type { SearchMessagesOffset } from './methods/messages/search-messages.js' export { searchMessages } from './methods/messages/search-messages.js' export { answerText } from './methods/messages/send-answer.js' export { answerMedia } from './methods/messages/send-answer.js' @@ -151,10 +158,13 @@ export { answerMediaGroup } from './methods/messages/send-answer.js' export { commentText } from './methods/messages/send-comment.js' export { commentMedia } from './methods/messages/send-comment.js' export { commentMediaGroup } from './methods/messages/send-comment.js' +export type { SendCopyParams } from './methods/messages/send-copy.js' export { sendCopy } from './methods/messages/send-copy.js' +export type { SendCopyGroupParams } from './methods/messages/send-copy-group.js' export { sendCopyGroup } from './methods/messages/send-copy-group.js' export { sendMedia } from './methods/messages/send-media.js' export { sendMediaGroup } from './methods/messages/send-media-group.js' +export type { QuoteParamsFrom } from './methods/messages/send-quote.js' export { quoteWithText } from './methods/messages/send-quote.js' export { quoteWithMedia } from './methods/messages/send-quote.js' export { quoteWithMediaGroup } from './methods/messages/send-quote.js' @@ -179,6 +189,7 @@ export { resendPasswordEmail } from './methods/password/password-email.js' export { cancelPasswordEmail } from './methods/password/password-email.js' export { removeCloudPassword } from './methods/password/remove-cloud-password.js' export { applyBoost } from './methods/premium/apply-boost.js' +export type { CanApplyBoostResult } from './methods/premium/can-apply-boost.js' export { canApplyBoost } from './methods/premium/can-apply-boost.js' export { getBoostStats } from './methods/premium/get-boost-stats.js' export { getBoosts } from './methods/premium/get-boosts.js' @@ -194,6 +205,7 @@ export { getStickerSet } from './methods/stickers/get-sticker-set.js' export { moveStickerInSet } from './methods/stickers/move-sticker-in-set.js' export { setChatStickerSet } from './methods/stickers/set-chat-sticker-set.js' export { setStickerSetThumb } from './methods/stickers/set-sticker-set-thumb.js' +export type { CanSendStoryResult } from './methods/stories/can-send-story.js' export { canSendStory } from './methods/stories/can-send-story.js' export { deleteStories } from './methods/stories/delete-stories.js' export { editStory } from './methods/stories/edit-story.js' diff --git a/packages/core/src/highlevel/methods/_imports.ts b/packages/core/src/highlevel/methods/_imports.ts index cfbd52d6..a8f01698 100644 --- a/packages/core/src/highlevel/methods/_imports.ts +++ b/packages/core/src/highlevel/methods/_imports.ts @@ -6,7 +6,7 @@ import { tdFileId } from '@mtcute/file-id' import { tl } from '@mtcute/tl' // @copy -import { MaybeArray, MaybePromise, PartialExcept, PartialOnly } from '../../types/index.js' +import { MaybeArray, MaybePromise, MtUnsupportedError, PartialExcept, PartialOnly } from '../../types/index.js' // @copy import { BaseTelegramClient, BaseTelegramClientOptions } from '../base.js' // @copy diff --git a/packages/core/src/highlevel/methods/_init.ts b/packages/core/src/highlevel/methods/_init.ts index f5e7588d..c555b1b2 100644 --- a/packages/core/src/highlevel/methods/_init.ts +++ b/packages/core/src/highlevel/methods/_init.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ // @copy -import { MemoryStorage } from '../../storage/providers/memory/index.js' +import { MtUnsupportedError, PartialOnly } from '../../types/index.js' import { BaseTelegramClient, BaseTelegramClientOptions } from '../base.js' import { TelegramClient } from '../client.js' import { ITelegramClient } from '../client.types.js' @@ -11,19 +11,16 @@ import { ITelegramStorageProvider } from '../storage/provider.js' import { Conversation } from '../types/conversation.js' // @copy import { makeParsedUpdateHandler, ParsedUpdateHandlerParams } from '../updates/parsed.js' -// @copy -import { _defaultStorageFactory } from '../utils/platform/storage.js' // @copy -type TelegramClientOptions = ((Omit & { +type TelegramClientOptions = ((PartialOnly, 'transport' | 'crypto'> & { /** * Storage to use for this client. * - * If a string is passed, it will be used as: - * - a path to a JSON file for Node.js - * - IndexedDB database name for browsers + * If a string is passed, it will be used as + * a name for the default platform-specific storage provider to use. * - * If omitted, {@link MemoryStorage} is used + * @default `"client.session"` */ storage?: string | ITelegramStorageProvider }) | ({ client: ITelegramClient })) & { @@ -37,41 +34,17 @@ type TelegramClientOptions = ((Omit & { skipConversationUpdates?: boolean } -// // @initialize=super -// /** @internal */ -// function _initializeClientSuper(this: TelegramClient, opts: TelegramClientOptions) { -// if (typeof opts.storage === 'string') { -// opts.storage = _defaultStorageFactory(opts.storage) -// } else if (!opts.storage) { -// opts.storage = new MemoryStorage() -// } - -// /* eslint-disable @typescript-eslint/no-unsafe-call */ -// // @ts-expect-error codegen -// super(opts) -// /* eslint-enable @typescript-eslint/no-unsafe-call */ -// } - // @initialize /** @internal */ function _initializeClient(this: TelegramClient, opts: TelegramClientOptions) { if ('client' in opts) { this._client = opts.client } else { - let storage: ITelegramStorageProvider - - if (typeof opts.storage === 'string') { - storage = _defaultStorageFactory(opts.storage) - } else if (!opts.storage) { - storage = new MemoryStorage() - } else { - storage = opts.storage + if (!opts.storage || typeof opts.storage === 'string' || !opts.transport || !opts.crypto) { + throw new MtUnsupportedError('You need to explicitly provide storage, transport and crypto for @mtcute/core') } - this._client = new BaseTelegramClient({ - ...opts, - storage, - }) + this._client = new BaseTelegramClient(opts as BaseTelegramClientOptions) } // @ts-expect-error codegen diff --git a/packages/core/src/highlevel/methods/bots/get-callback-answer.ts b/packages/core/src/highlevel/methods/bots/get-callback-answer.ts index 81022b47..805846b0 100644 --- a/packages/core/src/highlevel/methods/bots/get-callback-answer.ts +++ b/packages/core/src/highlevel/methods/bots/get-callback-answer.ts @@ -1,6 +1,6 @@ import { tl } from '@mtcute/tl' -import { utf8EncodeToBuffer } from '@mtcute/tl-runtime' +import { getPlatform } from '../../../platform.js' import { ITelegramClient } from '../../client.types.js' import { InputMessageId, normalizeInputMessageId } from '../../types/index.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -53,7 +53,7 @@ export async function getCallbackAnswer( _: 'messages.getBotCallbackAnswer', peer: await resolvePeer(client, chatId), msgId: message, - data: typeof data === 'string' ? utf8EncodeToBuffer(data) : data, + data: typeof data === 'string' ? getPlatform().utf8Encode(data) : data, password, game: game, }, diff --git a/packages/core/src/highlevel/methods/files/download-file.ts b/packages/core/src/highlevel/methods/files/download-file.ts index 01c127e5..9be869ce 100644 --- a/packages/core/src/highlevel/methods/files/download-file.ts +++ b/packages/core/src/highlevel/methods/files/download-file.ts @@ -1,11 +1,9 @@ -// eslint-disable-next-line no-restricted-imports -import { createWriteStream, rmSync } from 'fs' -import { writeFile } from 'fs/promises' +/* eslint-disable @typescript-eslint/no-unused-vars */ import { ITelegramClient } from '../../client.types.js' -import { FileDownloadLocation, FileDownloadParameters, FileLocation } from '../../types/index.js' -import { downloadAsIterable } from './download-iterable.js' +import { FileDownloadLocation, FileDownloadParameters } from '../../types/index.js' +// @available=both /** * Download a remote file to a local file (only for NodeJS). * Promise will resolve once the download is complete. @@ -13,30 +11,9 @@ import { downloadAsIterable } from './download-iterable.js' * @param filename Local file name to which the remote file will be downloaded * @param params File download parameters */ -export async function downloadToFile( +declare function downloadToFile( client: ITelegramClient, filename: string, location: FileDownloadLocation, params?: FileDownloadParameters, -): Promise { - if (location instanceof FileLocation && ArrayBuffer.isView(location.location)) { - // early return for inline files - await writeFile(filename, location.location) - } - - const output = createWriteStream(filename) - - if (params?.abortSignal) { - params.abortSignal.addEventListener('abort', () => { - client.log.debug('aborting file download %s - cleaning up', filename) - output.destroy() - rmSync(filename) - }) - } - - for await (const chunk of downloadAsIterable(client, location, params)) { - output.write(chunk) - } - - output.end() -} +): Promise diff --git a/packages/core/src/highlevel/methods/files/download-file.web.ts b/packages/core/src/highlevel/methods/files/download-file.web.ts deleted file mode 100644 index cdf92fed..00000000 --- a/packages/core/src/highlevel/methods/files/download-file.web.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { MtUnsupportedError } from '../../../types/errors.js' - -export function downloadToFile() { - throw new MtUnsupportedError('Downloading to file is only supported in NodeJS') -} diff --git a/packages/core/src/highlevel/methods/files/download-iterable.ts b/packages/core/src/highlevel/methods/files/download-iterable.ts index 8f4302cb..a3171477 100644 --- a/packages/core/src/highlevel/methods/files/download-iterable.ts +++ b/packages/core/src/highlevel/methods/files/download-iterable.ts @@ -2,6 +2,7 @@ import { parseFileId } from '@mtcute/file-id' import { tl } from '@mtcute/tl' import { ConnectionKind } from '../../../network/network-manager.js' +import { getPlatform } from '../../../platform.js' import { MtArgumentError, MtUnsupportedError } from '../../../types/errors.js' import { ConditionVariable } from '../../../utils/condition-variable.js' import { ITelegramClient } from '../../client.types.js' @@ -56,7 +57,7 @@ export async function* downloadAsIterable( if (!fileSize) fileSize = input.fileSize location = locationInner } else if (typeof input === 'string') { - const parsed = parseFileId(input) + const parsed = parseFileId(getPlatform(), input) if (parsed.location._ === 'web') { location = fileIdToInputWebFileLocation(parsed) diff --git a/packages/core/src/highlevel/methods/files/normalize-input-media.ts b/packages/core/src/highlevel/methods/files/normalize-input-media.ts index 0e411c21..894cb6ef 100644 --- a/packages/core/src/highlevel/methods/files/normalize-input-media.ts +++ b/packages/core/src/highlevel/methods/files/normalize-input-media.ts @@ -3,6 +3,7 @@ import Long from 'long' import { parseFileId, tdFileId } from '@mtcute/file-id' import { tl } from '@mtcute/tl' +import { getPlatform } from '../../../platform.js' import { assertTypeIs } from '../../../utils/type-assertions.js' import { ITelegramClient } from '../../client.types.js' import { isUploadedFile } from '../../types/files/uploaded-file.js' @@ -303,7 +304,7 @@ export async function _normalizeInputMedia( } else if (typeof input === 'string' && input.match(/^file:/)) { await upload(input.substring(5)) } else { - const parsed = typeof input === 'string' ? parseFileId(input) : input + const parsed = typeof input === 'string' ? parseFileId(getPlatform(), input) : input if (parsed.location._ === 'photo') { return { diff --git a/packages/core/src/highlevel/methods/files/upload-file.ts b/packages/core/src/highlevel/methods/files/upload-file.ts index 8a98e898..1c92c532 100644 --- a/packages/core/src/highlevel/methods/files/upload-file.ts +++ b/packages/core/src/highlevel/methods/files/upload-file.ts @@ -7,7 +7,6 @@ import { UploadedFile, UploadFileLike } from '../../types/index.js' import { guessFileMime } from '../../utils/file-type.js' import { determinePartSize, isProbablyPlainText } from '../../utils/file-utils.js' import { bufferToStream, createChunkedReader, streamToBuffer } from '../../utils/stream-utils.js' -import { _createFileStream, _extractFileStreamMeta, _handleNodeStream, _isFileStream } from './_platform.js' const OVERRIDE_MIME: Record = { // tg doesn't interpret `audio/opus` files as voice messages for some reason @@ -37,9 +36,6 @@ export async function uploadFile( params: { /** * Upload file source. - * - * > **Note**: `fs.ReadStream` is a subclass of `stream.Readable` and contains - * > info about file name, thus you don't need to pass them explicitly. */ file: UploadFileLike @@ -113,19 +109,9 @@ export async function uploadFile( if (typeof File !== 'undefined' && file instanceof File) { fileName = file.name fileSize = file.size - // file is now ReadableStream file = file.stream() } - if (typeof file === 'string') { - file = _createFileStream(file) - } - - if (_isFileStream(file)) { - [fileName, fileSize] = await _extractFileStreamMeta(file) - // fs.ReadStream is a subclass of Readable, will be handled below - } - if (typeof file === 'object' && 'headers' in file && 'body' in file && 'url' in file) { // fetch() response const length = parseInt(file.headers.get('content-length') || '0') @@ -161,8 +147,6 @@ export async function uploadFile( file = file.body } - file = _handleNodeStream(file) - if (!(file instanceof ReadableStream)) { throw new MtArgumentError('Could not convert input `file` to stream!') } diff --git a/packages/core/src/highlevel/types/bots/keyboards.ts b/packages/core/src/highlevel/types/bots/keyboards.ts index f5943232..f19b6f64 100644 --- a/packages/core/src/highlevel/types/bots/keyboards.ts +++ b/packages/core/src/highlevel/types/bots/keyboards.ts @@ -1,6 +1,6 @@ import { tl } from '@mtcute/tl' -import { utf8EncodeToBuffer } from '@mtcute/tl-runtime' +import { getPlatform } from '../../../platform.js' import { assertNever } from '../../../types/utils.js' import { toInputUser } from '../../utils/peer-utils.js' import { BotKeyboardBuilder } from './keyboard-builder.js' @@ -218,7 +218,7 @@ export namespace BotKeyboard { _: 'keyboardButtonCallback', text, requiresPassword, - data: typeof data === 'string' ? utf8EncodeToBuffer(data) : data, + data: typeof data === 'string' ? getPlatform().utf8Encode(data) : data, } } diff --git a/packages/core/src/highlevel/types/media/document.ts b/packages/core/src/highlevel/types/media/document.ts index 0597d3dd..b751e6d3 100644 --- a/packages/core/src/highlevel/types/media/document.ts +++ b/packages/core/src/highlevel/types/media/document.ts @@ -1,6 +1,7 @@ import { tdFileId as td, toFileId, toUniqueFileId } from '@mtcute/file-id' import { tl } from '@mtcute/tl' +import { getPlatform } from '../../../platform.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' import { FileLocation } from '../files/index.js' @@ -123,7 +124,7 @@ export abstract class RawDocument extends FileLocation { * representing this document. */ get fileId(): string { - return toFileId({ + return toFileId(getPlatform(), { type: this._fileIdType(), dcId: this.raw.dcId, fileReference: this.raw.fileReference, @@ -139,7 +140,7 @@ export abstract class RawDocument extends FileLocation { * Get a unique File ID representing this document. */ get uniqueFileId(): string { - return toUniqueFileId(td.FileType.Document, { + return toUniqueFileId(getPlatform(), td.FileType.Document, { _: 'common', id: this.raw.id, }) diff --git a/packages/core/src/highlevel/types/media/thumbnail.ts b/packages/core/src/highlevel/types/media/thumbnail.ts index a9d18421..0367abcc 100644 --- a/packages/core/src/highlevel/types/media/thumbnail.ts +++ b/packages/core/src/highlevel/types/media/thumbnail.ts @@ -3,6 +3,7 @@ import Long from 'long' import { tdFileId as td, toFileId, toUniqueFileId } from '@mtcute/file-id' import { tl } from '@mtcute/tl' +import { getPlatform } from '../../../platform.js' import { MtArgumentError, MtTypeAssertionError } from '../../../types/errors.js' import { assertTypeIs } from '../../../utils/type-assertions.js' import { inflateSvgPath, strippedPhotoToJpg, svgPathToFile } from '../../utils/file-utils.js' @@ -192,7 +193,7 @@ export class Thumbnail extends FileLocation { } if (this._media._ === 'stickerSet') { - return toFileId({ + return toFileId(getPlatform(), { type: td.FileType.Thumbnail, dcId: this.dcId!, fileReference: null, @@ -210,7 +211,7 @@ export class Thumbnail extends FileLocation { }) } - return toFileId({ + return toFileId(getPlatform(), { type: this._media._ === 'photo' ? td.FileType.Photo : td.FileType.Thumbnail, dcId: this.dcId!, fileReference: this._media.fileReference, @@ -239,7 +240,7 @@ export class Thumbnail extends FileLocation { } if (this._media._ === 'stickerSet') { - return toUniqueFileId(td.FileType.Thumbnail, { + return toUniqueFileId(getPlatform(), td.FileType.Thumbnail, { _: 'photo', id: Long.ZERO, source: { @@ -251,7 +252,7 @@ export class Thumbnail extends FileLocation { }) } - return toUniqueFileId(this._media._ === 'photo' ? td.FileType.Photo : td.FileType.Thumbnail, { + return toUniqueFileId(getPlatform(), this._media._ === 'photo' ? td.FileType.Photo : td.FileType.Thumbnail, { _: 'photo', id: this._media.id, source: { diff --git a/packages/core/src/highlevel/types/peers/chat-photo.ts b/packages/core/src/highlevel/types/peers/chat-photo.ts index eb6d34b1..dc297ea1 100644 --- a/packages/core/src/highlevel/types/peers/chat-photo.ts +++ b/packages/core/src/highlevel/types/peers/chat-photo.ts @@ -3,6 +3,7 @@ import Long from 'long' import { tdFileId, toFileId, toUniqueFileId } from '@mtcute/file-id' import { tl } from '@mtcute/tl' +import { getPlatform } from '../../../platform.js' import { MtArgumentError } from '../../../types/errors.js' import { toggleChannelIdMark } from '../../../utils/peer-utils.js' import { strippedPhotoToJpg } from '../../utils/file-utils.js' @@ -62,7 +63,7 @@ export class ChatPhotoSize extends FileLocation { throw new MtArgumentError('Input peer was invalid') } - return toFileId({ + return toFileId(getPlatform(), { dcId: this.obj.dcId, type: tdFileId.FileType.ProfilePhoto, fileReference: null, @@ -84,7 +85,7 @@ export class ChatPhotoSize extends FileLocation { * TDLib and Bot API compatible unique File ID representing this size */ get uniqueFileId(): string { - return toUniqueFileId(tdFileId.FileType.ProfilePhoto, { + return toUniqueFileId(getPlatform(), tdFileId.FileType.ProfilePhoto, { _: 'photo', id: this.obj.photoId, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment diff --git a/packages/core/src/highlevel/types/updates/callback-query.ts b/packages/core/src/highlevel/types/updates/callback-query.ts index e4f165be..adb45ac2 100644 --- a/packages/core/src/highlevel/types/updates/callback-query.ts +++ b/packages/core/src/highlevel/types/updates/callback-query.ts @@ -1,6 +1,6 @@ import { tl } from '@mtcute/tl' -import { utf8Decode } from '@mtcute/tl-runtime' +import { getPlatform } from '../../../platform.js' import { MtArgumentError } from '../../../types/errors.js' import { makeInspectable } from '../../utils/index.js' import { encodeInlineMessageId } from '../../utils/inline-utils.js' @@ -58,7 +58,7 @@ class BaseCallbackQuery { get dataStr(): string | null { if (!this.raw.data) return null - return utf8Decode(this.raw.data) + return getPlatform().utf8Decode(this.raw.data) } /** diff --git a/packages/core/src/highlevel/utils/convert-file-id.ts b/packages/core/src/highlevel/utils/convert-file-id.ts index 0d7276f3..dce6c7e6 100644 --- a/packages/core/src/highlevel/utils/convert-file-id.ts +++ b/packages/core/src/highlevel/utils/convert-file-id.ts @@ -6,6 +6,7 @@ import { tl } from '@mtcute/tl' import { parseMarkedPeerId } from '../../utils/peer-utils.js' import FileType = td.FileType +import { getPlatform } from '../../platform.js' import { assertNever } from '../../types/utils.js' const EMPTY_BUFFER = new Uint8Array(0) @@ -45,7 +46,7 @@ function dialogPhotoToInputPeer( * @param fileId File ID, either parsed or as a string */ export function fileIdToInputWebFileLocation(fileId: string | FileId): tl.RawInputWebFileLocation { - if (typeof fileId === 'string') fileId = parseFileId(fileId) + if (typeof fileId === 'string') fileId = parseFileId(getPlatform(), fileId) if (fileId.location._ !== 'web') { throw new td.ConversionError('inputWebFileLocation') @@ -65,7 +66,7 @@ export function fileIdToInputWebFileLocation(fileId: string | FileId): tl.RawInp * @param fileId File ID, either parsed or as a string */ export function fileIdToInputFileLocation(fileId: string | FileId): tl.TypeInputFileLocation { - if (typeof fileId === 'string') fileId = parseFileId(fileId) + if (typeof fileId === 'string') fileId = parseFileId(getPlatform(), fileId) const loc = fileId.location @@ -219,7 +220,7 @@ export function fileIdToInputFileLocation(fileId: string | FileId): tl.TypeInput * @param fileId File ID, either parsed or as a string */ export function fileIdToInputDocument(fileId: string | FileId): tl.RawInputDocument { - if (typeof fileId === 'string') fileId = parseFileId(fileId) + if (typeof fileId === 'string') fileId = parseFileId(getPlatform(), fileId) if ( fileId.location._ !== 'common' || @@ -256,7 +257,7 @@ export function fileIdToInputDocument(fileId: string | FileId): tl.RawInputDocum * @param fileId File ID, either parsed or as a string */ export function fileIdToInputPhoto(fileId: string | FileId): tl.RawInputPhoto { - if (typeof fileId === 'string') fileId = parseFileId(fileId) + if (typeof fileId === 'string') fileId = parseFileId(getPlatform(), fileId) if (fileId.location._ !== 'photo') { throw new td.ConversionError('inputPhoto') @@ -281,7 +282,7 @@ export function fileIdToInputPhoto(fileId: string | FileId): tl.RawInputPhoto { * @param fileId File ID, either parsed or as a string */ export function fileIdToEncryptedFile(fileId: string | FileId): tl.RawInputEncryptedFile { - if (typeof fileId === 'string') fileId = parseFileId(fileId) + if (typeof fileId === 'string') fileId = parseFileId(getPlatform(), fileId) if (fileId.location._ !== 'common' || fileId.type !== FileType.Encrypted) { throw new td.ConversionError('inputEncryptedFile') @@ -301,7 +302,7 @@ export function fileIdToEncryptedFile(fileId: string | FileId): tl.RawInputEncry * @param fileId File ID, either parsed or as a string */ export function fileIdToSecureFile(fileId: string | FileId): tl.RawInputSecureFile { - if (typeof fileId === 'string') fileId = parseFileId(fileId) + if (typeof fileId === 'string') fileId = parseFileId(getPlatform(), fileId) if (fileId.location._ !== 'common' || (fileId.type !== FileType.Secure && fileId.type !== FileType.SecureRaw)) { throw new td.ConversionError('inputSecureFile') diff --git a/packages/core/src/highlevel/utils/file-type.test.ts b/packages/core/src/highlevel/utils/file-type.test.ts index 8549c554..e3b3703d 100644 --- a/packages/core/src/highlevel/utils/file-type.test.ts +++ b/packages/core/src/highlevel/utils/file-type.test.ts @@ -1,9 +1,10 @@ import { describe, expect, it } from 'vitest' -import { hexDecodeToBuffer } from '@mtcute/tl-runtime' - +import { getPlatform } from '../../platform.js' import { guessFileMime } from './file-type.js' +const p = getPlatform() + describe('guessFileMime', () => { it.each([ ['424d', 'image/bmp'], @@ -60,6 +61,6 @@ describe('guessFileMime', () => { ])('should detect %s as %s', (header, mime) => { header += '00'.repeat(16) - expect(guessFileMime(hexDecodeToBuffer(header))).toEqual(mime) + expect(guessFileMime(p.hexDecode(header))).toEqual(mime) }) }) diff --git a/packages/core/src/highlevel/utils/file-utils.test.ts b/packages/core/src/highlevel/utils/file-utils.test.ts index aeb31ad2..a0dc88b6 100644 --- a/packages/core/src/highlevel/utils/file-utils.test.ts +++ b/packages/core/src/highlevel/utils/file-utils.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest' -import { hexDecodeToBuffer, hexEncode, utf8Decode, utf8EncodeToBuffer } from '@mtcute/tl-runtime' - +import { getPlatform } from '../../platform.js' import { extractFileName, inflateSvgPath, @@ -10,28 +9,30 @@ import { svgPathToFile, } from './file-utils.js' +const p = getPlatform() + describe('isProbablyPlainText', () => { it('should return true for buffers only containing printable ascii', () => { - expect(isProbablyPlainText(utf8EncodeToBuffer('hello this is some ascii text'))).to.be.true - expect(isProbablyPlainText(utf8EncodeToBuffer('hello this is some ascii text\nwith unix new lines'))).to.be.true - expect(isProbablyPlainText(utf8EncodeToBuffer('hello this is some ascii text\r\nwith windows new lines'))).to.be + expect(isProbablyPlainText(p.utf8Encode('hello this is some ascii text'))).to.be.true + expect(isProbablyPlainText(p.utf8Encode('hello this is some ascii text\nwith unix new lines'))).to.be.true + expect(isProbablyPlainText(p.utf8Encode('hello this is some ascii text\r\nwith windows new lines'))).to.be .true - expect(isProbablyPlainText(utf8EncodeToBuffer('hello this is some ascii text\n\twith unix new lines and tabs'))) + expect(isProbablyPlainText(p.utf8Encode('hello this is some ascii text\n\twith unix new lines and tabs'))) .to.be.true expect( isProbablyPlainText( - utf8EncodeToBuffer('hello this is some ascii text\r\n\twith windows new lines and tabs'), + p.utf8Encode('hello this is some ascii text\r\n\twith windows new lines and tabs'), ), ).to.be.true }) it('should return false for buffers containing some binary data', () => { - expect(isProbablyPlainText(utf8EncodeToBuffer('hello this is cedilla: ç'))).to.be.false - expect(isProbablyPlainText(utf8EncodeToBuffer('hello this is some ascii text with emojis 🌸'))).to.be.false + expect(isProbablyPlainText(p.utf8Encode('hello this is cedilla: ç'))).to.be.false + expect(isProbablyPlainText(p.utf8Encode('hello this is some ascii text with emojis 🌸'))).to.be.false // random strings of 16 bytes - expect(isProbablyPlainText(hexDecodeToBuffer('717f80f08eb9d88c3931712c0e2be32f'))).to.be.false - expect(isProbablyPlainText(hexDecodeToBuffer('20e8e218e54254c813b261432b0330d7'))).to.be.false + expect(isProbablyPlainText(p.hexDecode('717f80f08eb9d88c3931712c0e2be32f'))).to.be.false + expect(isProbablyPlainText(p.hexDecode('20e8e218e54254c813b261432b0330d7'))).to.be.false }) }) @@ -53,14 +54,14 @@ describe('svgPathToFile', () => { it('should convert SVG path to a file', () => { const path = 'M 0 0 L 100 0 L 100 100 L 0 100 L 0 0 Z' - expect(utf8Decode(svgPathToFile(path))).toMatchInlineSnapshot( + expect(p.utf8Decode(svgPathToFile(path))).toMatchInlineSnapshot( '""', ) }) }) describe('inflateSvgPath', () => { - const data = hexDecodeToBuffer( + const data = p.hexDecode( '1a05b302dc5f4446068649064247424a6a4c704550535b5e665e5e4c044a024c' + '074e06414d80588863935fad74be4704854684518b528581904695498b488b56' + '965c85438d8191818543894a8f4d834188818a4284498454895d9a6f86074708' + @@ -85,9 +86,9 @@ describe('inflateSvgPath', () => { describe('strippedPhotoToJpg', () => { // strippedThumb of @Channel_Bot - const dataPfp = hexDecodeToBuffer('010808b1f2f95fed673451457033ad1f') + const dataPfp = p.hexDecode('010808b1f2f95fed673451457033ad1f') // photoStrippedSize of a random image - const dataPicture = hexDecodeToBuffer( + const dataPicture = p.hexDecode( '012728b532aacce4b302d8c1099c74a634718675cb6381f73d3ffd557667d9b5' + '816f4c28ce69aa58a863238cf62a334590f999042234cbe1986d03eefe14c68e' + '32847cc00ce709ea7ffad577773f78fe54d6c927f78c3db14ac1ccca91a2ef4f' + @@ -99,7 +100,7 @@ describe('strippedPhotoToJpg', () => { ) it('should inflate stripped jpeg (from profile picture)', () => { - expect(hexEncode(strippedPhotoToJpg(dataPfp))).toMatchInlineSnapshot( + expect(p.hexEncode(strippedPhotoToJpg(dataPfp))).toMatchInlineSnapshot( '"ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e192' + '82321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a' + '0aadaad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2' + @@ -124,7 +125,7 @@ describe('strippedPhotoToJpg', () => { }) it('should inflate stripped jpeg (from a picture)', () => { - expect(hexEncode(strippedPhotoToJpg(dataPicture))).toMatchInlineSnapshot( + expect(p.hexEncode(strippedPhotoToJpg(dataPicture))).toMatchInlineSnapshot( '"ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e192' + '82321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a' + '0aadaad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2' + diff --git a/packages/core/src/highlevel/utils/file-utils.ts b/packages/core/src/highlevel/utils/file-utils.ts index 345843dd..d51c9d46 100644 --- a/packages/core/src/highlevel/utils/file-utils.ts +++ b/packages/core/src/highlevel/utils/file-utils.ts @@ -1,5 +1,4 @@ -import { hexDecodeToBuffer, utf8EncodeToBuffer } from '@mtcute/tl-runtime' - +import { getPlatform } from '../../platform.js' import { MtArgumentError } from '../../types/errors.js' import { concatBuffers } from '../../utils/buffer-utils.js' @@ -33,7 +32,7 @@ export function isProbablyPlainText(buf: Uint8Array): boolean { } // from https://github.com/telegramdesktop/tdesktop/blob/bec39d89e19670eb436dc794a8f20b657cb87c71/Telegram/SourceFiles/ui/image/image.cpp#L225 -const JPEG_HEADER = hexDecodeToBuffer( +const JPEG_HEADER = () => getPlatform().hexDecode( 'ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e1928' + '2321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a0aad' + 'aad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2d2d3c35' + @@ -54,6 +53,7 @@ const JPEG_HEADER = hexDecodeToBuffer( 'b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f' + '3f4f5f6f7f8f9faffda000c03010002110311003f00', ) +let JPEG_HEADER_BYTES: Uint8Array | null = null const JPEG_FOOTER = new Uint8Array([0xff, 0xd9]) /** @@ -64,7 +64,11 @@ export function strippedPhotoToJpg(stripped: Uint8Array): Uint8Array { throw new MtArgumentError('Invalid stripped JPEG') } - const result = concatBuffers([JPEG_HEADER, stripped.slice(3), JPEG_FOOTER]) + if (JPEG_HEADER_BYTES === null) { + JPEG_HEADER_BYTES = JPEG_HEADER() + } + + const result = concatBuffers([JPEG_HEADER_BYTES, stripped.slice(3), JPEG_FOOTER]) result[164] = stripped[1] result[166] = stripped[2] @@ -108,7 +112,7 @@ export function inflateSvgPath(encoded: Uint8Array): string { * @param path */ export function svgPathToFile(path: string): Uint8Array { - return utf8EncodeToBuffer( + return getPlatform().utf8Encode( '' + '' + diff --git a/packages/core/src/highlevel/utils/inline-utils.ts b/packages/core/src/highlevel/utils/inline-utils.ts index 61b782ca..f520ba92 100644 --- a/packages/core/src/highlevel/utils/inline-utils.ts +++ b/packages/core/src/highlevel/utils/inline-utils.ts @@ -1,6 +1,7 @@ import { tl } from '@mtcute/tl' -import { base64DecodeToBuffer, base64Encode, TlBinaryReader, TlBinaryWriter } from '@mtcute/tl-runtime' +import { TlBinaryReader, TlBinaryWriter } from '@mtcute/tl-runtime' +import { getPlatform } from '../../platform.js' import { assertNever } from '../../types/utils.js' /** @@ -9,7 +10,7 @@ import { assertNever } from '../../types/utils.js' * @param id Inline message ID */ export function parseInlineMessageId(id: string): tl.TypeInputBotInlineMessageID { - const buf = base64DecodeToBuffer(id, true) + const buf = getPlatform().base64Decode(id, true) const reader = TlBinaryReader.manual(buf) if (buf.length === 20) { @@ -56,7 +57,7 @@ export function encodeInlineMessageId(id: tl.TypeInputBotInlineMessageID): strin assertNever(id) } - return base64Encode(writer.result(), true) + return getPlatform().base64Encode(writer.result(), true) } export function normalizeInlineId(id: string | tl.TypeInputBotInlineMessageID) { diff --git a/packages/core/src/highlevel/utils/inspectable.ts b/packages/core/src/highlevel/utils/inspectable.ts index 6aa86749..6a072283 100644 --- a/packages/core/src/highlevel/utils/inspectable.ts +++ b/packages/core/src/highlevel/utils/inspectable.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-argument */ -import { base64Encode } from '@mtcute/tl-runtime' +import { getPlatform } from '../../platform.js' const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom') @@ -52,7 +52,7 @@ export function makeInspectable(obj: new (...args: any[]) => T, props?: (keyo if (val && typeof val === 'object') { if (val instanceof Uint8Array) { - val = base64Encode(val) + val = getPlatform().base64Encode(val) } else if (typeof val.toJSON === 'function') { val = val.toJSON(true) } diff --git a/packages/core/src/highlevel/utils/platform/storage.ts b/packages/core/src/highlevel/utils/platform/storage.ts deleted file mode 100644 index d65566e3..00000000 --- a/packages/core/src/highlevel/utils/platform/storage.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { MtUnsupportedError } from '../../../types/errors.js' -import { ITelegramStorageProvider } from '../../storage/provider.js' - -/** @internal */ -export const _defaultStorageFactory = (_name: string): ITelegramStorageProvider => { - throw new MtUnsupportedError('Please provide a storage explicitly (e.g. @mtcute/sqlite)') -} diff --git a/packages/core/src/highlevel/utils/platform/storage.web.ts b/packages/core/src/highlevel/utils/platform/storage.web.ts deleted file mode 100644 index faed84bd..00000000 --- a/packages/core/src/highlevel/utils/platform/storage.web.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IdbStorage } from '../../../storage/index.js' -import { MtUnsupportedError } from '../../../types/errors.js' - -/** @internal */ -export const _defaultStorageFactory = (name: string) => { - if (typeof indexedDB !== 'undefined') { - return new IdbStorage(name) - } - - throw new MtUnsupportedError('No storage available!') -} diff --git a/packages/core/src/highlevel/utils/string-session.ts b/packages/core/src/highlevel/utils/string-session.ts index acbff726..72e3add2 100644 --- a/packages/core/src/highlevel/utils/string-session.ts +++ b/packages/core/src/highlevel/utils/string-session.ts @@ -1,6 +1,7 @@ import { tl } from '@mtcute/tl' -import { base64DecodeToBuffer, base64Encode, TlBinaryReader, TlBinaryWriter, TlReaderMap } from '@mtcute/tl-runtime' +import { TlBinaryReader, TlBinaryWriter, TlReaderMap } from '@mtcute/tl-runtime' +import { getPlatform } from '../../platform.js' import { MtArgumentError } from '../../types/index.js' import { BasicDcOption, DcOptions, parseBasicDcOption, serializeBasicDcOption } from '../../utils/dcs.js' import { CurrentUserInfo } from '../storage/service/current-user.js' @@ -53,11 +54,11 @@ export function writeStringSession(data: StringSessionData): string { writer.bytes(data.authKey) - return base64Encode(writer.result(), true) + return getPlatform().base64Encode(writer.result(), true) } export function readStringSession(readerMap: TlReaderMap, data: string): StringSessionData { - const buf = base64DecodeToBuffer(data, true) + const buf = getPlatform().base64Decode(data, true) const version = buf[0] diff --git a/packages/core/src/highlevel/utils/voice-utils.test.ts b/packages/core/src/highlevel/utils/voice-utils.test.ts index 60036d4a..fd759f01 100644 --- a/packages/core/src/highlevel/utils/voice-utils.test.ts +++ b/packages/core/src/highlevel/utils/voice-utils.test.ts @@ -1,14 +1,15 @@ import { describe, expect, it } from 'vitest' -import { hexDecodeToBuffer, hexEncode } from '@mtcute/tl-runtime' - +import { getPlatform } from '../../platform.js' import { decodeWaveform, encodeWaveform } from './voice-utils.js' +const p = getPlatform() + describe('decodeWaveform', () => { it('should correctly decode telegram-encoded waveform', () => { expect( decodeWaveform( - hexDecodeToBuffer( + p.hexDecode( '0000104210428c310821a51463cc39072184524a4aa9b51663acb5e69c7bef41' + '08618c514a39e7a494d65aadb5f75e8c31ce396badf7de9cf3debbf7feff0f', ), @@ -25,7 +26,7 @@ describe('decodeWaveform', () => { describe('encodeWaveform', () => { it('should correctly decode telegram-encoded waveform', () => { expect( - hexEncode( + p.hexEncode( encodeWaveform([ 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 16, 17, 17, 17, 18, diff --git a/packages/core/src/highlevel/worker/platform/connect.web.ts b/packages/core/src/highlevel/worker/platform/connect.web.ts index b22db3b0..18de002d 100644 --- a/packages/core/src/highlevel/worker/platform/connect.web.ts +++ b/packages/core/src/highlevel/worker/platform/connect.web.ts @@ -1,4 +1,4 @@ -import { beforeExit } from '../../../utils/platform/exit-hook.js' +import { getPlatform } from '../../../platform.js' import { ClientMessageHandler, SendFn, SomeWorker } from '../protocol.js' export function connectToWorker(worker: SomeWorker, handler: ClientMessageHandler): [SendFn, () => void] { @@ -52,7 +52,7 @@ export function connectToWorker(worker: SomeWorker, handler: ClientMessageHandle worker.port.close() } - beforeExit(close) + getPlatform().beforeExit(close) return [send, close] } diff --git a/packages/core/src/network/auth-key.test.ts b/packages/core/src/network/auth-key.test.ts index 9699ea14..0f61d742 100644 --- a/packages/core/src/network/auth-key.test.ts +++ b/packages/core/src/network/auth-key.test.ts @@ -3,22 +3,19 @@ import { describe, expect, it, vi } from 'vitest' import { defaultTestCryptoProvider } from '@mtcute/test' import { - hexDecode, - hexDecodeToBuffer, - hexEncode, TlBinaryReader, TlReaderMap, - utf8Decode, - utf8EncodeToBuffer, } from '@mtcute/tl-runtime' +import { getPlatform } from '../platform.js' import { LogManager } from '../utils/index.js' import { AuthKey } from './auth-key.js' const authKey = new Uint8Array(256) +const p = getPlatform() for (let i = 0; i < 256; i += 32) { - hexDecode(authKey.subarray(i, i + 32), '98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0') + authKey.subarray(i, i + 32).set(p.hexDecode('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0')) } describe('AuthKey', () => { @@ -54,19 +51,19 @@ describe('AuthKey', () => { it('should calculate derivatives', async () => { const key = await create() - expect(hexEncode(key.key)).toEqual(hexEncode(authKey)) - expect(hexEncode(key.clientSalt)).toEqual('f73c3622dec230e098cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4b') - expect(hexEncode(key.serverSalt)).toEqual('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0') - expect(hexEncode(key.id)).toEqual('40fa5bb7cb56a895') + expect(p.hexEncode(key.key)).toEqual(p.hexEncode(authKey)) + expect(p.hexEncode(key.clientSalt)).toEqual('f73c3622dec230e098cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4b') + expect(p.hexEncode(key.serverSalt)).toEqual('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0') + expect(p.hexEncode(key.id)).toEqual('40fa5bb7cb56a895') }) it('should encrypt a message', async () => { - const message = writeMessage(utf8EncodeToBuffer('hello, world!!!!')) + const message = writeMessage(p.utf8Encode('hello, world!!!!')) const key = await create() const msg = key.encryptMessage(message, serverSalt, sessionId) - expect(hexEncode(msg)).toEqual( + expect(p.hexEncode(msg)).toEqual( '40fa5bb7cb56a895f6f5a88914892aadf87c68031cc953ba29d68e118021f329' + 'be386a620d49f3ad3a50c60dcef3733f214e8cefa3e403c11d193637d4971dc1' + '5db7f74b26fd16cb0e8fee30bf7e3f68858fe82927e2cd06', @@ -88,7 +85,7 @@ describe('AuthKey', () => { } it('should decrypt a message', async () => { - const message = hexDecodeToBuffer( + const message = p.hexDecode( '40fa5bb7cb56a8950c394b884f1529efc42fea22d972fea650a714ce6d2d1bdb' + '3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b' + '07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35', @@ -98,11 +95,11 @@ describe('AuthKey', () => { expect(decMsgId).toEqual(msgId) expect(decSeqNo).toEqual(seqNo) - expect(utf8Decode(data.raw(16))).toEqual('hello, world!!!!') + expect(p.utf8Decode(data.raw(16))).toEqual('hello, world!!!!') }) it('should decrypt a message with padding', async () => { - const message = hexDecodeToBuffer( + const message = p.hexDecode( '40fa5bb7cb56a8950c394b884f1529efc42fea22d972fea650a714ce6d2d1bdb' + '3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b' + '07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35' + @@ -113,11 +110,11 @@ describe('AuthKey', () => { expect(decMsgId).toEqual(msgId) expect(decSeqNo).toEqual(seqNo) - expect(utf8Decode(data.raw(16))).toEqual('hello, world!!!!') + expect(p.utf8Decode(data.raw(16))).toEqual('hello, world!!!!') }) it('should ignore messages with invalid message key', async () => { - const message = hexDecodeToBuffer( + const message = p.hexDecode( '40fa5bb7cb56a8950000000000000000000000000000000050a714ce6d2d1bdb' + '3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b' + '07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35', @@ -127,7 +124,7 @@ describe('AuthKey', () => { }) it('should ignore messages with invalid session_id', async () => { - const message = hexDecodeToBuffer( + const message = p.hexDecode( '40fa5bb7cb56a895a986a7e97f4e90aa2769b5e702c6e86f5e1e82c6ff0c6829' + '2521a2ba9704fa37fb341d895cf32662c6cf47ba31cbf27c30d5c03f6c2930f4' + '30fd8858b836b73fe32d4a95b8ebcdbc9ca8908f7964c40a', @@ -137,12 +134,12 @@ describe('AuthKey', () => { }) it('should ignore messages with invalid length', async () => { - const messageTooLong = hexDecodeToBuffer( + const messageTooLong = p.hexDecode( '40fa5bb7cb56a8950d19412233dd5d24be697c73274e08fbe515cf65e0c5f70c' + 'ad75fd2badc18c9f999f287351144eeb1cfcaa9bea33ef5058999ad96a498306' + '08d2859425685a55b21fab413bfabc42ec5da283853b28c0', ) - const messageUnaligned = hexDecodeToBuffer( + const messageUnaligned = p.hexDecode( '40fa5bb7cb56a8957b4e4bec561eee4a5a1025bc8a35d3d0c79a3685d2b90ff0' + '5f638e9c42c9fd9448b0ce8e7d49e7ea1ce458e47b825b5c7fd8ddf5b4fded46' + '2a4bcc02f3ff2e89de6764d6d219f575e457fdcf8c163cdf', @@ -155,7 +152,7 @@ describe('AuthKey', () => { }) it('should ignore messages with invalid padding', async () => { - const message = hexDecodeToBuffer( + const message = p.hexDecode( '40fa5bb7cb56a895133671d1c637a9836e2c64b4d1a0521d8a25a6416fd4dc9e' + '79f9478fb837703cc9efa0a19d12143c2a26e57cb4bc64d7bc972dd8f19c53c590cc258162f44afc', ) diff --git a/packages/core/src/network/client.ts b/packages/core/src/network/client.ts index 9e1c087f..6d79dd60 100644 --- a/packages/core/src/network/client.ts +++ b/packages/core/src/network/client.ts @@ -11,9 +11,7 @@ import { StorageManager, StorageManagerExtraOptions } from '../storage/storage.j import { MustEqual } from '../types/index.js' import { asyncResettable, - CryptoProviderFactory, DcOptions, - defaultCryptoProviderFactory, defaultProductionDc, defaultProductionIpv6Dc, defaultTestDc, @@ -49,10 +47,10 @@ export interface MtClientOptions { storageOptions?: StorageManagerExtraOptions /** - * Cryptography provider factory to allow delegating + * Cryptography provider to allow delegating * crypto to native addon, worker, etc. */ - crypto?: CryptoProviderFactory + crypto: ICryptoProvider /** * Whether to use IPv6 datacenters @@ -96,7 +94,7 @@ export interface MtClientOptions { * * @default platform-specific transport: WebSocket on the web, TCP in node */ - transport?: TransportFactory + transport: TransportFactory /** * Reconnection strategy. @@ -254,7 +252,7 @@ export class MtClient extends EventEmitter { this.log.mgr.level = params.logLevel } - this.crypto = (params.crypto ?? defaultCryptoProviderFactory)() + this.crypto = params.crypto this._testMode = Boolean(params.testMode) let dc = params.defaultDcs diff --git a/packages/core/src/network/network-manager.ts b/packages/core/src/network/network-manager.ts index 31f9c0dc..95f87810 100644 --- a/packages/core/src/network/network-manager.ts +++ b/packages/core/src/network/network-manager.ts @@ -1,6 +1,7 @@ import { mtp, tl } from '@mtcute/tl' import { TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime' +import { getPlatform } from '../platform.js' import { StorageManager } from '../storage/storage.js' import { MtArgumentError, MtcuteError, MtTimeoutError, MtUnsupportedError } from '../types/index.js' import { @@ -18,7 +19,7 @@ import { PersistentConnectionParams } from './persistent-connection.js' import { defaultReconnectionStrategy, ReconnectionStrategy } from './reconnection.js' import { ServerSaltManager } from './server-salt.js' import { SessionConnection, SessionConnectionParams } from './session-connection.js' -import { defaultTransportFactory, TransportFactory } from './transports/index.js' +import { TransportFactory } from './transports/index.js' export type ConnectionKind = 'main' | 'upload' | 'download' | 'downloadSmall' @@ -44,7 +45,7 @@ export interface NetworkManagerParams { enableErrorReporting: boolean apiId: number initConnectionOptions?: Partial> - transport?: TransportFactory + transport: TransportFactory reconnectionStrategy?: ReconnectionStrategy floodSleepThreshold: number maxRetryCount: number @@ -439,15 +440,7 @@ export class NetworkManager { readonly params: NetworkManagerParams & NetworkManagerExtraParams, readonly config: ConfigManager, ) { - let deviceModel = 'mtcute on ' - /* eslint-disable no-restricted-globals */ - if (typeof process !== 'undefined' && typeof require !== 'undefined') { - const os = require('os') as typeof import('os') - deviceModel += `${os.type()} ${os.arch()} ${os.release()}` - /* eslint-enable no-restricted-globals */ - } else if (typeof navigator !== 'undefined') { - deviceModel += navigator.userAgent - } else deviceModel += 'unknown' + const deviceModel = `mtcute on ${getPlatform().getDeviceModel()}` this._initConnectionParams = { _: 'initConnection', @@ -463,7 +456,7 @@ export class NetworkManager { query: null as any, } - this._transportFactory = params.transport ?? defaultTransportFactory + this._transportFactory = params.transport this._reconnectionStrategy = params.reconnectionStrategy ?? defaultReconnectionStrategy this._connectionCount = params.connectionCount ?? defaultConnectionCountDelegate this._updateHandler = params.onUpdate diff --git a/packages/core/src/network/session-connection.ts b/packages/core/src/network/session-connection.ts index cda4381f..c16ab4ed 100644 --- a/packages/core/src/network/session-connection.ts +++ b/packages/core/src/network/session-connection.ts @@ -7,6 +7,7 @@ import { TlBinaryReader, TlBinaryWriter, TlReaderMap, TlSerializationCounter, Tl import { MtArgumentError, MtcuteError, MtTimeoutError } from '../types/index.js' import { createAesIgeForMessageOld } from '../utils/crypto/mtproto.js' +import { reportUnknownError } from '../utils/error-reporting.js' import { concatBuffers, ControllablePromise, @@ -17,7 +18,6 @@ import { randomLong, removeFromLongArray, } from '../utils/index.js' -import { reportUnknownError } from '../utils/platform/error-reporting.js' import { doAuthorization } from './authorization.js' import { MtprotoSession, PendingMessage, PendingRpc } from './mtproto-session.js' import { PersistentConnection, PersistentConnectionParams } from './persistent-connection.js' diff --git a/packages/core/src/network/transports/index.ts b/packages/core/src/network/transports/index.ts index cdcb1bc9..8251b88a 100644 --- a/packages/core/src/network/transports/index.ts +++ b/packages/core/src/network/transports/index.ts @@ -1,12 +1,5 @@ -import { TransportFactory } from './abstract.js' - export * from './abstract.js' export * from './intermediate.js' export * from './obfuscated.js' export * from './streamed.js' export * from './wrapped.js' - -import { _defaultTransportFactory } from '../../utils/platform/transport.js' - -/** Platform-defined default transport factory */ -export const defaultTransportFactory: TransportFactory = _defaultTransportFactory diff --git a/packages/core/src/network/transports/intermediate.test.ts b/packages/core/src/network/transports/intermediate.test.ts index 3f8f3de4..a13e9717 100644 --- a/packages/core/src/network/transports/intermediate.test.ts +++ b/packages/core/src/network/transports/intermediate.test.ts @@ -1,13 +1,15 @@ import { describe, expect, it } from 'vitest' import { defaultTestCryptoProvider, useFakeMathRandom } from '@mtcute/test' -import { hexDecodeToBuffer, hexEncode } from '@mtcute/tl-runtime' import { IntermediatePacketCodec, PaddedIntermediatePacketCodec, TransportError } from '../../index.js' +import { getPlatform } from '../../platform.js' + +const p = getPlatform() describe('IntermediatePacketCodec', () => { it('should return correct tag', () => { - expect(hexEncode(new IntermediatePacketCodec().tag())).eq('eeeeeeee') + expect(p.hexEncode(new IntermediatePacketCodec().tag())).eq('eeeeeeee') }) it('should correctly parse immediate framing', () => @@ -17,7 +19,7 @@ describe('IntermediatePacketCodec', () => { expect([...data]).eql([5, 1, 2, 3, 4]) done() }) - codec.feed(hexDecodeToBuffer('050000000501020304')) + codec.feed(p.hexDecode('050000000501020304')) })) it('should correctly parse incomplete framing', () => @@ -27,8 +29,8 @@ describe('IntermediatePacketCodec', () => { expect([...data]).eql([5, 1, 2, 3, 4]) done() }) - codec.feed(hexDecodeToBuffer('050000000501')) - codec.feed(hexDecodeToBuffer('020304')) + codec.feed(p.hexDecode('050000000501')) + codec.feed(p.hexDecode('020304')) })) it('should correctly parse multiple streamed packets', () => @@ -46,9 +48,9 @@ describe('IntermediatePacketCodec', () => { done() } }) - codec.feed(hexDecodeToBuffer('050000000501')) - codec.feed(hexDecodeToBuffer('020304050000')) - codec.feed(hexDecodeToBuffer('000301020301')) + codec.feed(p.hexDecode('050000000501')) + codec.feed(p.hexDecode('020304050000')) + codec.feed(p.hexDecode('000301020301')) })) it('should correctly parse transport errors', () => @@ -61,7 +63,7 @@ describe('IntermediatePacketCodec', () => { done() }) - codec.feed(hexDecodeToBuffer('040000006cfeffff')) + codec.feed(p.hexDecode('040000006cfeffff')) })) it('should reset when called reset()', () => @@ -73,15 +75,15 @@ describe('IntermediatePacketCodec', () => { done() }) - codec.feed(hexDecodeToBuffer('ff0000001234567812345678')) + codec.feed(p.hexDecode('ff0000001234567812345678')) codec.reset() - codec.feed(hexDecodeToBuffer('050000000102030405')) + codec.feed(p.hexDecode('050000000102030405')) })) it('should correctly frame packets', () => { - const data = hexDecodeToBuffer('6cfeffff') + const data = p.hexDecode('6cfeffff') - expect(hexEncode(new IntermediatePacketCodec().encode(data))).toEqual('040000006cfeffff') + expect(p.hexEncode(new IntermediatePacketCodec().encode(data))).toEqual('040000006cfeffff') }) }) @@ -96,12 +98,12 @@ describe('PaddedIntermediatePacketCodec', () => { } it('should return correct tag', async () => { - expect(hexEncode((await create()).tag())).eq('dddddddd') + expect(p.hexEncode((await create()).tag())).eq('dddddddd') }) it('should correctly frame packets', async () => { - const data = hexDecodeToBuffer('6cfeffff') + const data = p.hexDecode('6cfeffff') - expect(hexEncode((await create()).encode(data))).toEqual('0a0000006cfeffff29afd26df40f') + expect(p.hexEncode((await create()).encode(data))).toEqual('0a0000006cfeffff29afd26df40f') }) }) diff --git a/packages/core/src/network/transports/obfuscated.test.ts b/packages/core/src/network/transports/obfuscated.test.ts index d3ddf5ff..be574df5 100644 --- a/packages/core/src/network/transports/obfuscated.test.ts +++ b/packages/core/src/network/transports/obfuscated.test.ts @@ -2,10 +2,13 @@ import { describe, expect, it, vi } from 'vitest' import { defaultTestCryptoProvider, u8HexDecode } from '@mtcute/test' -import { hexDecodeToBuffer, hexEncode, LogManager } from '../../utils/index.js' +import { getPlatform } from '../../platform.js' +import { LogManager } from '../../utils/index.js' import { IntermediatePacketCodec } from './intermediate.js' import { MtProxyInfo, ObfuscatedPacketCodec } from './obfuscated.js' +const p = getPlatform() + describe('ObfuscatedPacketCodec', () => { const create = async (randomSource?: string, proxy?: MtProxyInfo) => { const codec = new ObfuscatedPacketCodec(new IntermediatePacketCodec(), proxy) @@ -22,7 +25,7 @@ describe('ObfuscatedPacketCodec', () => { const tag = await codec.tag() - expect(hexEncode(tag)).toEqual( + expect(p.hexEncode(tag)).toEqual( 'ff'.repeat(56) + 'fce8ab2203db2bff', // encrypted part ) }) @@ -40,7 +43,7 @@ describe('ObfuscatedPacketCodec', () => { const tag = await codec.tag() - expect(hexEncode(tag)).toEqual( + expect(p.hexEncode(tag)).toEqual( 'ff'.repeat(56) + 'ecec4cbda8bb188b', // encrypted part with dcId = 1 ) }) @@ -57,7 +60,7 @@ describe('ObfuscatedPacketCodec', () => { const tag = await codec.tag() - expect(hexEncode(tag)).toEqual( + expect(p.hexEncode(tag)).toEqual( 'ff'.repeat(56) + 'ecec4cbdb89c188b', // encrypted part with dcId = 10001 ) }) @@ -74,7 +77,7 @@ describe('ObfuscatedPacketCodec', () => { const tag = await codec.tag() - expect(hexEncode(tag)).toEqual( + expect(p.hexEncode(tag)).toEqual( 'ff'.repeat(56) + 'ecec4cbd5644188b', // encrypted part with dcId = -1 ) }) @@ -124,7 +127,7 @@ describe('ObfuscatedPacketCodec', () => { it('should correctly create aes ctr for mtproxy', async () => { const proxy: MtProxyInfo = { dcId: 1, - secret: hexDecodeToBuffer('00112233445566778899aabbccddeeff'), + secret: p.hexDecode('00112233445566778899aabbccddeeff'), test: true, media: false, } @@ -137,20 +140,20 @@ describe('ObfuscatedPacketCodec', () => { expect(spyCreateAesCtr).toHaveBeenCalledTimes(2) expect(spyCreateAesCtr).toHaveBeenNthCalledWith( 1, - hexDecodeToBuffer('dd03188944590983e28dad14d97d0952389d118af4ffcbdb28d56a6a612ef7a6'), + p.hexDecode('dd03188944590983e28dad14d97d0952389d118af4ffcbdb28d56a6a612ef7a6'), u8HexDecode('936b33fa7f97bae025102532233abb26'), true, ) expect(spyCreateAesCtr).toHaveBeenNthCalledWith( 2, - hexDecodeToBuffer('413b8e08021fbb08a2962b6d7187194fe46565c6b329d3bbdfcffd4870c16119'), + p.hexDecode('413b8e08021fbb08a2962b6d7187194fe46565c6b329d3bbdfcffd4870c16119'), u8HexDecode('db6aeee6883f45f95def566dadb4b610'), false, ) }) it('should correctly encrypt the underlying codec', async () => { - const data = hexDecodeToBuffer('6cfeffff') + const data = p.hexDecode('6cfeffff') const msg1 = 'a1020630a410e940' const msg2 = 'f53ff53f371db495' @@ -158,8 +161,8 @@ describe('ObfuscatedPacketCodec', () => { await codec.tag() - expect(hexEncode(await codec.encode(data))).toEqual(msg1) - expect(hexEncode(await codec.encode(data))).toEqual(msg2) + expect(p.hexEncode(await codec.encode(data))).toEqual(msg1) + expect(p.hexEncode(await codec.encode(data))).toEqual(msg2) }) it('should correctly decrypt the underlying codec', async () => { @@ -176,8 +179,8 @@ describe('ObfuscatedPacketCodec', () => { log.push(e.toString()) }) - codec.feed(hexDecodeToBuffer(msg1)) - codec.feed(hexDecodeToBuffer(msg2)) + codec.feed(p.hexDecode(msg1)) + codec.feed(p.hexDecode(msg2)) await vi.waitFor(() => expect(log).toEqual(['Error: Transport error: 404', 'Error: Transport error: 404'])) }) diff --git a/packages/core/src/network/transports/websocket.test.ts b/packages/core/src/network/transports/websocket.test.ts index fc966127..b583568e 100644 --- a/packages/core/src/network/transports/websocket.test.ts +++ b/packages/core/src/network/transports/websocket.test.ts @@ -2,10 +2,13 @@ import { describe, expect, it, Mock, MockedObject, vi } from 'vitest' import { defaultTestCryptoProvider, u8HexDecode } from '@mtcute/test' -import { defaultProductionDc, hexDecodeToBuffer, LogManager } from '../../utils/index.js' +import { getPlatform } from '../../platform.js' +import { defaultProductionDc, LogManager } from '../../utils/index.js' import { TransportState } from './abstract.js' import { WebSocketTransport } from './websocket.js' +const p = getPlatform() + describe('WebSocketTransport', () => { const create = async () => { const fakeWs = vi.fn().mockImplementation(() => ({ @@ -74,9 +77,9 @@ describe('WebSocketTransport', () => { const socket = getLastSocket(ws) await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) - await t.send(hexDecodeToBuffer('00010203040506070809')) + await t.send(p.hexDecode('00010203040506070809')) - expect(socket.send).toHaveBeenCalledWith(hexDecodeToBuffer('af020630c8ef14bcf53af33853ea')) + expect(socket.send).toHaveBeenCalledWith(p.hexDecode('af020630c8ef14bcf53af33853ea')) }) it('should correctly close', async () => { @@ -101,7 +104,7 @@ describe('WebSocketTransport', () => { const socket = getLastSocket(ws) await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) - const data = hexDecodeToBuffer('00010203040506070809') + const data = p.hexDecode('00010203040506070809') const message = new MessageEvent('message', { data }) const onMessageCall = socket.addEventListener.mock.calls.find(([event]) => event === 'message') as unknown as [ diff --git a/packages/core/src/platform.ts b/packages/core/src/platform.ts new file mode 100644 index 00000000..03ef4867 --- /dev/null +++ b/packages/core/src/platform.ts @@ -0,0 +1,34 @@ +import { ITlPlatform, TlBinaryReader, TlBinaryWriter } from '@mtcute/tl-runtime' + +import { MtUnsupportedError } from './types/errors.js' + +export interface ICorePlatform extends ITlPlatform { + beforeExit(fn: () => void): () => void + log(color: number, level: number, tag: string, fmt: string, args: unknown[]): void + getDefaultLogLevel(): number | null + getDeviceModel(): string +} + +let _platform: ICorePlatform | null = null + +export function setPlatform(platform: ICorePlatform): void { + if (_platform) { + if (_platform.constructor !== platform.constructor) { + throw new MtUnsupportedError('Platform may not be changed at runtime!') + } + + return + } + + _platform = platform + TlBinaryReader.platform = platform + TlBinaryWriter.platform = platform +} + +export function getPlatform(): ICorePlatform { + if (!_platform) { + throw new MtUnsupportedError('Platform is not set! Have you instantiated the client?') + } + + return _platform +} diff --git a/packages/core/src/storage/index.ts b/packages/core/src/storage/index.ts index ddf40746..6f27696c 100644 --- a/packages/core/src/storage/index.ts +++ b/packages/core/src/storage/index.ts @@ -1,6 +1,5 @@ export * from './driver.js' +export * from './memory/index.js' export * from './provider.js' -export * from './providers/idb/index.js' -export * from './providers/memory/index.js' export * from './repository/index.js' export * from './storage.js' diff --git a/packages/core/src/storage/providers/memory/driver.ts b/packages/core/src/storage/memory/driver.ts similarity index 87% rename from packages/core/src/storage/providers/memory/driver.ts rename to packages/core/src/storage/memory/driver.ts index 36037cb8..558db076 100644 --- a/packages/core/src/storage/providers/memory/driver.ts +++ b/packages/core/src/storage/memory/driver.ts @@ -1,4 +1,4 @@ -import { IStorageDriver } from '../../driver.js' +import { IStorageDriver } from '../driver.js' export class MemoryStorageDriver implements IStorageDriver { readonly states: Map = new Map() diff --git a/packages/core/src/storage/providers/memory/index.ts b/packages/core/src/storage/memory/index.ts similarity index 87% rename from packages/core/src/storage/providers/memory/index.ts rename to packages/core/src/storage/memory/index.ts index c6b1f0b4..8d74e46f 100644 --- a/packages/core/src/storage/providers/memory/index.ts +++ b/packages/core/src/storage/memory/index.ts @@ -1,5 +1,5 @@ -import { ITelegramStorageProvider } from '../../../highlevel/storage/provider.js' -import { IMtStorageProvider } from '../../provider.js' +import { ITelegramStorageProvider } from '../../highlevel/storage/provider.js' +import { IMtStorageProvider } from '../provider.js' import { MemoryStorageDriver } from './driver.js' import { MemoryAuthKeysRepository } from './repository/auth-keys.js' import { MemoryKeyValueRepository } from './repository/kv.js' diff --git a/packages/core/src/storage/providers/memory/memory.test.ts b/packages/core/src/storage/memory/memory.test.ts similarity index 100% rename from packages/core/src/storage/providers/memory/memory.test.ts rename to packages/core/src/storage/memory/memory.test.ts diff --git a/packages/core/src/storage/providers/memory/repository/auth-keys.ts b/packages/core/src/storage/memory/repository/auth-keys.ts similarity index 96% rename from packages/core/src/storage/providers/memory/repository/auth-keys.ts rename to packages/core/src/storage/memory/repository/auth-keys.ts index a43ec9da..79396198 100644 --- a/packages/core/src/storage/providers/memory/repository/auth-keys.ts +++ b/packages/core/src/storage/memory/repository/auth-keys.ts @@ -1,4 +1,4 @@ -import { IAuthKeysRepository } from '../../../repository/auth-keys.js' +import { IAuthKeysRepository } from '../../repository/auth-keys.js' import { MemoryStorageDriver } from '../driver.js' interface AuthKeysState { diff --git a/packages/core/src/storage/providers/memory/repository/kv.ts b/packages/core/src/storage/memory/repository/kv.ts similarity index 89% rename from packages/core/src/storage/providers/memory/repository/kv.ts rename to packages/core/src/storage/memory/repository/kv.ts index 0e56f912..f97c1b63 100644 --- a/packages/core/src/storage/providers/memory/repository/kv.ts +++ b/packages/core/src/storage/memory/repository/kv.ts @@ -1,4 +1,4 @@ -import { IKeyValueRepository } from '../../../repository/key-value.js' +import { IKeyValueRepository } from '../../repository/key-value.js' import { MemoryStorageDriver } from '../driver.js' export class MemoryKeyValueRepository implements IKeyValueRepository { diff --git a/packages/core/src/storage/providers/memory/repository/peers.ts b/packages/core/src/storage/memory/repository/peers.ts similarity index 95% rename from packages/core/src/storage/providers/memory/repository/peers.ts rename to packages/core/src/storage/memory/repository/peers.ts index 6c5b1ba9..9c5cb621 100644 --- a/packages/core/src/storage/providers/memory/repository/peers.ts +++ b/packages/core/src/storage/memory/repository/peers.ts @@ -1,4 +1,4 @@ -import { IPeersRepository } from '../../../../highlevel/storage/repository/peers.js' +import { IPeersRepository } from '../../../highlevel/storage/repository/peers.js' import { MemoryStorageDriver } from '../driver.js' interface PeersState { diff --git a/packages/core/src/storage/providers/memory/repository/ref-messages.ts b/packages/core/src/storage/memory/repository/ref-messages.ts similarity index 92% rename from packages/core/src/storage/providers/memory/repository/ref-messages.ts rename to packages/core/src/storage/memory/repository/ref-messages.ts index d85be0c7..cb65d409 100644 --- a/packages/core/src/storage/providers/memory/repository/ref-messages.ts +++ b/packages/core/src/storage/memory/repository/ref-messages.ts @@ -1,4 +1,4 @@ -import { IReferenceMessagesRepository } from '../../../../highlevel/storage/repository/ref-messages.js' +import { IReferenceMessagesRepository } from '../../../highlevel/storage/repository/ref-messages.js' import { MemoryStorageDriver } from '../driver.js' interface RefMessagesState { diff --git a/packages/core/src/storage/service/utils.test-utils.ts b/packages/core/src/storage/service/utils.test-utils.ts index 9bf74bea..f5c9e133 100644 --- a/packages/core/src/storage/service/utils.test-utils.ts +++ b/packages/core/src/storage/service/utils.test-utils.ts @@ -2,7 +2,7 @@ import { __tlReaderMap } from '@mtcute/tl/binary/reader.js' import { __tlWriterMap } from '@mtcute/tl/binary/writer.js' import { LogManager } from '../../utils/logger.js' -import { MemoryStorageDriver } from '../providers/memory/driver.js' +import { MemoryStorageDriver } from '../memory/driver.js' import { ServiceOptions } from './base.js' export function testServiceOptions(): ServiceOptions { diff --git a/packages/core/src/storage/storage.ts b/packages/core/src/storage/storage.ts index ad6f96fb..6563a007 100644 --- a/packages/core/src/storage/storage.ts +++ b/packages/core/src/storage/storage.ts @@ -1,6 +1,7 @@ import { TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime' -import { asyncResettable, beforeExit } from '../utils/index.js' +import { getPlatform } from '../platform.js' +import { asyncResettable } from '../utils/index.js' import { Logger } from '../utils/logger.js' import { IMtStorageProvider } from './provider.js' import { AuthKeysService } from './service/auth-keys.js' @@ -62,7 +63,7 @@ export class StorageManager { this.driver.setup?.(this.log) if (this.options.cleanup ?? true) { - this._cleanupRestore = beforeExit(() => { + this._cleanupRestore = getPlatform().beforeExit(() => { this._destroy().catch((err) => this.log.error('cleanup error: %s', err)) }) } diff --git a/packages/core/src/utils/bigint-utils.test.ts b/packages/core/src/utils/bigint-utils.test.ts index 611b1c68..6697763a 100644 --- a/packages/core/src/utils/bigint-utils.test.ts +++ b/packages/core/src/utils/bigint-utils.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from 'vitest' import { defaultTestCryptoProvider } from '@mtcute/test' -import { hexDecodeToBuffer } from '@mtcute/tl-runtime' +import { getPlatform } from '../platform.js' import { bigIntBitLength, bigIntGcd, @@ -16,6 +16,8 @@ import { twoMultiplicity, } from './index.js' +const p = getPlatform() + describe('bigIntBitLength', () => { it('should correctly calculate bit length', () => { expect(bigIntBitLength(0n)).eq(0) @@ -33,7 +35,7 @@ describe('bigIntToBuffer', () => { expect([...bigIntToBuffer(BigInt('10495708'), 8, false)]).eql([0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x26, 0xdc]) expect([...bigIntToBuffer(BigInt('3038102549'), 4, false)]).eql([0xb5, 0x15, 0xc4, 0x15]) expect([...bigIntToBuffer(BigInt('9341376580368336208'), 8, false)]).eql([ - ...hexDecodeToBuffer('81A33C81D2020550'), + ...p.hexDecode('81A33C81D2020550'), ]) }) @@ -43,12 +45,12 @@ describe('bigIntToBuffer', () => { expect([...bigIntToBuffer(BigInt('10495708'), 8, true)]).eql([0xdc, 0x26, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00]) expect([...bigIntToBuffer(BigInt('3038102549'), 4, true)]).eql([0x15, 0xc4, 0x15, 0xb5]) expect([...bigIntToBuffer(BigInt('9341376580368336208'), 8, true)]).eql([ - ...hexDecodeToBuffer('81A33C81D2020550').reverse(), + ...p.hexDecode('81A33C81D2020550').reverse(), ]) }) it('should handle large integers', () => { - const buf = hexDecodeToBuffer( + const buf = p.hexDecode( '1a981ce8bf86bf4a1bd79c2ef829914172f8d0e54cb7ad807552d56977e1c946872e2c7bd77052be30e7e9a7a35c4feff848a25759f5f2f5b0e96538', ) const num = BigInt( @@ -74,7 +76,7 @@ describe('bufferToBigInt', () => { }) it('should handle large integers', () => { - const buf = hexDecodeToBuffer( + const buf = p.hexDecode( '1a981ce8bf86bf4a1bd79c2ef829914172f8d0e54cb7ad807552d56977e1c946872e2c7bd77052be30e7e9a7a35c4feff848a25759f5f2f5b0e96538', ) const num = BigInt( diff --git a/packages/core/src/utils/binary/asn1-parser.ts b/packages/core/src/utils/binary/asn1-parser.ts index ef49cdf2..12218a23 100644 --- a/packages/core/src/utils/binary/asn1-parser.ts +++ b/packages/core/src/utils/binary/asn1-parser.ts @@ -1,13 +1,13 @@ // all available libraries either suck or are extremely large for the use case, so i made my own~ -import { base64DecodeToBuffer, hexEncode } from '@mtcute/tl-runtime' +import { getPlatform } from '../../platform.js' /** * Parses a single PEM block to buffer. * In fact just strips begin/end tags and parses the rest as Base64 */ export function parsePemContents(pem: string): Uint8Array { - return base64DecodeToBuffer(pem.replace(/^-----(BEGIN|END)( RSA)? PUBLIC KEY-----$|\n/gm, '')) + return getPlatform().base64Decode(pem.replace(/^-----(BEGIN|END)( RSA)? PUBLIC KEY-----$|\n/gm, '')) } // based on https://git.coolaj86.com/coolaj86/asn1-parser.js/src/branch/master/asn1-parser.js @@ -66,7 +66,7 @@ export function parseAsn1(data: Uint8Array): Asn1Object { if (0x80 & asn1.length) { asn1.lengthSize = 0x7f & asn1.length // I think that buf->hex->int solves the problem of Endianness... not sure - asn1.length = parseInt(hexEncode(buf.subarray(index, index + asn1.lengthSize)), 16) + asn1.length = parseInt(getPlatform().hexEncode(buf.subarray(index, index + asn1.lengthSize)), 16) index += asn1.lengthSize } diff --git a/packages/core/src/utils/crypto/abstract.ts b/packages/core/src/utils/crypto/abstract.ts index f0c4142a..69bef57f 100644 --- a/packages/core/src/utils/crypto/abstract.ts +++ b/packages/core/src/utils/crypto/abstract.ts @@ -55,5 +55,3 @@ export abstract class BaseCryptoProvider { return buf } } - -export type CryptoProviderFactory = () => ICryptoProvider diff --git a/packages/core/src/utils/crypto/factorization.test.ts b/packages/core/src/utils/crypto/factorization.test.ts index aec3c8f2..b38377a5 100644 --- a/packages/core/src/utils/crypto/factorization.test.ts +++ b/packages/core/src/utils/crypto/factorization.test.ts @@ -1,12 +1,13 @@ import { describe, expect, it } from 'vitest' +import { defaultCryptoProvider } from '@mtcute/test' + import { bigIntToBuffer, bufferToBigInt } from '../bigint-utils.js' import { factorizePQSync } from './factorization.js' -import { defaultCryptoProviderFactory } from './index.js' describe('prime factorization', () => { const testFactorization = (pq: bigint, p: bigint, q: bigint) => { - const [p_, q_] = factorizePQSync(defaultCryptoProviderFactory(), bigIntToBuffer(pq)) + const [p_, q_] = factorizePQSync(defaultCryptoProvider, bigIntToBuffer(pq)) expect(bufferToBigInt(p_)).toBe(p) expect(bufferToBigInt(q_)).toBe(q) } diff --git a/packages/core/src/utils/crypto/index.ts b/packages/core/src/utils/crypto/index.ts index 4e0ba120..d9c8f825 100644 --- a/packages/core/src/utils/crypto/index.ts +++ b/packages/core/src/utils/crypto/index.ts @@ -5,8 +5,4 @@ export * from './miller-rabin.js' export * from './mtproto.js' export * from './password.js' export * from './utils.js' - -import { _defaultCryptoProviderFactory } from '../platform/crypto.js' -import { CryptoProviderFactory } from './abstract.js' - -export const defaultCryptoProviderFactory: CryptoProviderFactory = _defaultCryptoProviderFactory +export * from './wasm.js' diff --git a/packages/core/src/utils/crypto/keys.test.ts b/packages/core/src/utils/crypto/keys.test.ts index 965160b5..40c00dc1 100644 --- a/packages/core/src/utils/crypto/keys.test.ts +++ b/packages/core/src/utils/crypto/keys.test.ts @@ -1,9 +1,10 @@ import { describe, expect, it } from 'vitest' -import { findKeyByFingerprints, parsePublicKey } from '../index.js' -import { NodeCryptoProvider } from './node.js' +import { defaultCryptoProvider } from '@mtcute/test' -const crypto = new NodeCryptoProvider() +import { findKeyByFingerprints, parsePublicKey } from '../index.js' + +const crypto = defaultCryptoProvider describe('parsePublicKey', () => { it('should parse telegram public keys', () => { diff --git a/packages/core/src/utils/crypto/keys.ts b/packages/core/src/utils/crypto/keys.ts index b36d30c3..d79fd10c 100644 --- a/packages/core/src/utils/crypto/keys.ts +++ b/packages/core/src/utils/crypto/keys.ts @@ -1,8 +1,9 @@ import Long from 'long' import { __publicKeyIndex as keysIndex, TlPublicKey } from '@mtcute/tl/binary/rsa-keys.js' -import { hexEncode, TlBinaryWriter } from '@mtcute/tl-runtime' +import { TlBinaryWriter } from '@mtcute/tl-runtime' +import { getPlatform } from '../../platform.js' import { parseAsn1, parsePemContents } from '../binary/asn1-parser.js' import { ICryptoProvider } from './abstract.js' @@ -25,13 +26,15 @@ export function parsePublicKey(crypto: ICryptoProvider, key: string, old = false writer.bytes(modulus) writer.bytes(exponent) + const platform = getPlatform() + const data = writer.result() const sha = crypto.sha1(data) - const fp = hexEncode(sha.slice(-8).reverse()) + const fp = platform.hexEncode(sha.slice(-8).reverse()) return { - modulus: hexEncode(modulus), - exponent: hexEncode(exponent), + modulus: platform.hexEncode(modulus), + exponent: platform.hexEncode(exponent), fingerprint: fp, old, } diff --git a/packages/core/src/utils/crypto/miller-rabin.test.ts b/packages/core/src/utils/crypto/miller-rabin.test.ts index adecd832..4c80d4a0 100644 --- a/packages/core/src/utils/crypto/miller-rabin.test.ts +++ b/packages/core/src/utils/crypto/miller-rabin.test.ts @@ -1,13 +1,14 @@ import { describe, expect, it } from 'vitest' -import { defaultCryptoProviderFactory } from './index.js' +import { defaultCryptoProvider } from '@mtcute/test' + import { millerRabin } from './miller-rabin.js' describe( 'miller-rabin test', function () { // miller-rabin factorization relies on RNG, so we should use a real random number generator - const c = defaultCryptoProviderFactory() + const c = defaultCryptoProvider const testMillerRabin = (n: number | string | bigint, isPrime: boolean) => { expect(millerRabin(c, BigInt(n))).eq(isPrime) diff --git a/packages/core/src/utils/crypto/mtproto.test.ts b/packages/core/src/utils/crypto/mtproto.test.ts index d64b5cf4..8df669a2 100644 --- a/packages/core/src/utils/crypto/mtproto.test.ts +++ b/packages/core/src/utils/crypto/mtproto.test.ts @@ -2,12 +2,15 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { defaultTestCryptoProvider, u8HexDecode } from '@mtcute/test' -import { concatBuffers, hexDecodeToBuffer, hexEncode } from '../index.js' +import { getPlatform } from '../../platform.js' +import { concatBuffers } from '../index.js' import { createAesIgeForMessage, createAesIgeForMessageOld, generateKeyAndIvFromNonce } from './mtproto.js' -const authKeyChunk = hexDecodeToBuffer('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0') +const p = getPlatform() + +const authKeyChunk = p.hexDecode('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0') const authKey = concatBuffers(Array.from({ length: 8 }, () => authKeyChunk)) -const messageKey = hexDecodeToBuffer('25d701f2a29205526757825a99eb2d32') +const messageKey = p.hexDecode('25d701f2a29205526757825a99eb2d32') describe('mtproto 2.0', async () => { const crypto = await defaultTestCryptoProvider() @@ -18,10 +21,10 @@ describe('mtproto 2.0', async () => { it('should correctly derive message key and iv for client', () => { createAesIgeForMessage(crypto, authKey, messageKey, true) - expect(hexEncode(createAesIgeSpy.mock.calls[0][0])).toEqual( + expect(p.hexEncode(createAesIgeSpy.mock.calls[0][0])).toEqual( 'af3f8e1ffa75f4c981eec33a3e5bbaa2ea48f9bb93e91597627eb1f67960a0c9', ) - expect(hexEncode(createAesIgeSpy.mock.calls[0][1])).toEqual( + expect(p.hexEncode(createAesIgeSpy.mock.calls[0][1])).toEqual( '9874d77f95155b35221bff94b7df4594c6996e2a62e44fcb7d93c8c4e41b79ee', ) }) @@ -29,10 +32,10 @@ describe('mtproto 2.0', async () => { it('should correctly derive message key and iv for server', () => { createAesIgeForMessage(crypto, authKey, messageKey, false) - expect(hexEncode(createAesIgeSpy.mock.calls[0][0])).toEqual( + expect(p.hexEncode(createAesIgeSpy.mock.calls[0][0])).toEqual( 'd4b378e1e0525f10ff9d4c42807ccce5b30a033a8088c0b922b5259421751648', ) - expect(hexEncode(createAesIgeSpy.mock.calls[0][1])).toEqual( + expect(p.hexEncode(createAesIgeSpy.mock.calls[0][1])).toEqual( '4d7194f42f0135d2fd83050b403265b4c40ee3e9e9fba56f0f4d8ea6bcb121f5', ) }) @@ -47,10 +50,10 @@ describe('mtproto 1.0', async () => { it('should correctly derive message key and iv for client', () => { createAesIgeForMessageOld(crypto, authKey, messageKey, true) - expect(hexEncode(createAesIgeSpy.mock.calls[0][0])).toEqual( + expect(p.hexEncode(createAesIgeSpy.mock.calls[0][0])).toEqual( '1fc7b40b1d9ffbdaf4d652525a748864259698f89214abf27c0d36cb9d4cd5db', ) - expect(hexEncode(createAesIgeSpy.mock.calls[0][1])).toEqual( + expect(p.hexEncode(createAesIgeSpy.mock.calls[0][1])).toEqual( '7251fbda39ec5e6e089f15ded5963b03d6d8d0f7078898431fc7b40b1d9ffbda', ) }) @@ -58,10 +61,10 @@ describe('mtproto 1.0', async () => { it('should correctly derive message key and iv for server', () => { createAesIgeForMessageOld(crypto, authKey, messageKey, false) - expect(hexEncode(createAesIgeSpy.mock.calls[0][0])).toEqual( + expect(p.hexEncode(createAesIgeSpy.mock.calls[0][0])).toEqual( 'af0e4e01318654be40ab42b125909d43b44bdeef571ff1a5dfb81474ae26d467', ) - expect(hexEncode(createAesIgeSpy.mock.calls[0][1])).toEqual( + expect(p.hexEncode(createAesIgeSpy.mock.calls[0][1])).toEqual( '15c9ba6021d2c5cf04f0842540ae216a970b4eac8f46ef01af0e4e01318654be', ) }) diff --git a/packages/core/src/utils/crypto/password.test.ts b/packages/core/src/utils/crypto/password.test.ts index ffef76b6..97c80f95 100644 --- a/packages/core/src/utils/crypto/password.test.ts +++ b/packages/core/src/utils/crypto/password.test.ts @@ -3,17 +3,19 @@ import { describe, expect, it } from 'vitest' import { defaultTestCryptoProvider } from '@mtcute/test' import { tl } from '@mtcute/tl' -import { hexDecodeToBuffer, hexEncode, utf8EncodeToBuffer } from '@mtcute/tl-runtime' +import { getPlatform } from '../../platform.js' import { computeNewPasswordHash, computePasswordHash, computeSrpParams } from './index.js' +const p = getPlatform() + // a real-world request from an account with "qwe123" password const fakeAlgo: tl.RawPasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow = { _: 'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow', - salt1: hexDecodeToBuffer('9b3accc457c0d5288e8cff31eb21094048bc11902f6614dbb9afb839ee7641c37619537d8ebe749e'), - salt2: hexDecodeToBuffer('6c619bb0786dc4ed1bf211d23f6e4065'), + salt1: p.hexDecode('9b3accc457c0d5288e8cff31eb21094048bc11902f6614dbb9afb839ee7641c37619537d8ebe749e'), + salt2: p.hexDecode('6c619bb0786dc4ed1bf211d23f6e4065'), g: 3, - p: hexDecodeToBuffer( + p: p.hexDecode( 'c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f' + '48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c37' + '20fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f64' + @@ -30,7 +32,7 @@ const fakeRequest: tl.account.RawPassword = { hasSecureValues: false, hasPassword: true, currentAlgo: fakeAlgo, - srpB: hexDecodeToBuffer( + srpB: p.hexDecode( '1476a7b5991d7f028bbee33b3455cad3f2cd0eb3737409fcce92fa7d4cd5c733' + 'ec6d2cb3454e587d4c17eda2fd7ef9a57327215f38292cc8bd5dc77d3e1d31cd' + 'dae2652f8347c4b0093f7c78242f70e6cc13137ee7acc257a49855a63113db8f' + @@ -43,10 +45,10 @@ const fakeRequest: tl.account.RawPassword = { srpId: Long.fromBits(-2046015018, 875006452), newAlgo: { _: 'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow', - salt1: hexDecodeToBuffer('9b3accc457c0d528'), - salt2: hexDecodeToBuffer('6c619bb0786dc4ed1bf211d23f6e4065'), + salt1: p.hexDecode('9b3accc457c0d528'), + salt2: p.hexDecode('6c619bb0786dc4ed1bf211d23f6e4065'), g: 3, - p: hexDecodeToBuffer( + p: p.hexDecode( 'c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f' + '48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c37' + '20fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f64' + @@ -59,7 +61,7 @@ const fakeRequest: tl.account.RawPassword = { }, newSecureAlgo: { _: 'securePasswordKdfAlgoPBKDF2HMACSHA512iter100000', - salt: hexDecodeToBuffer('fdd59abc0bffb24d'), + salt: p.hexDecode('fdd59abc0bffb24d'), }, secureRandom: new Uint8Array(), // unused } @@ -68,16 +70,16 @@ const password = 'qwe123' describe('SRP', () => { it('should correctly compute password hash as defined by MTProto', async () => { const crypto = await defaultTestCryptoProvider() - const hash = await computePasswordHash(crypto, utf8EncodeToBuffer(password), fakeAlgo.salt1, fakeAlgo.salt2) + const hash = await computePasswordHash(crypto, p.utf8Encode(password), fakeAlgo.salt1, fakeAlgo.salt2) - expect(hexEncode(hash)).toEqual('750f1fe282965e63ce17b98427b35549fb864465211840f6a7c1f2fb657cc33b') + expect(p.hexEncode(hash)).toEqual('750f1fe282965e63ce17b98427b35549fb864465211840f6a7c1f2fb657cc33b') }) it('should correctly compute new password hash as defined by MTProto', async () => { const crypto = await defaultTestCryptoProvider() const hash = await computeNewPasswordHash(crypto, fakeAlgo, '123qwe') - expect(hexEncode(hash)).toEqual( + expect(p.hexEncode(hash)).toEqual( '2540539ceeffd4543cd845bf319b8392e6b17bf7cf26bafcf6282ce9ae795368' + '4ff49469c2863b17e6d65ddb16ae6f60bc07cc254c00e5ba389292f6cea0b3aa' + 'c459d1d08984d65319df8c5d124042169bbe2ab8c0c93bc7178827f2ea84e7c3' + @@ -94,7 +96,7 @@ describe('SRP', () => { const params = await computeSrpParams(crypto, fakeRequest, password) expect(params.srpId).toEqual(fakeRequest.srpId) - expect(hexEncode(params.A)).toEqual( + expect(p.hexEncode(params.A)).toEqual( '363976f55edb57cc5cc0c4aaca9b7539eff98a43a93fa84be34860d18ac3a80f' + 'ffd57c4617896ff667677d0552a079eb189d25d147ec96edd4495c946a18652d' + '31d78eede40a8b29da340c19b32ccac78f8482406e392102c03d850d1db87223' + @@ -104,6 +106,6 @@ describe('SRP', () => { '4fa454aa69d9219d9c5fa3625f5c6f1ac03892a70aa17269c76cd9bf2949a961' + 'fad2a71e5fa961824b32db037130c7e9aad4c1e9f02ebc5b832622f98b59597e', ) - expect(hexEncode(params.M1)).toEqual('25a91b21c634ad670a144165a9829192d152e131a716f676abc48cd817f508c6') + expect(p.hexEncode(params.M1)).toEqual('25a91b21c634ad670a144165a9829192d152e131a716f676abc48cd817f508c6') }) }) diff --git a/packages/core/src/utils/crypto/password.ts b/packages/core/src/utils/crypto/password.ts index 6662d628..89c8bd59 100644 --- a/packages/core/src/utils/crypto/password.ts +++ b/packages/core/src/utils/crypto/password.ts @@ -1,6 +1,6 @@ import { tl } from '@mtcute/tl' -import { utf8EncodeToBuffer } from '@mtcute/tl-runtime' +import { getPlatform } from '../../platform.js' import { MtSecurityError, MtUnsupportedError } from '../../types/errors.js' import { bigIntModPow, bigIntToBuffer, bufferToBigInt } from '../bigint-utils.js' import { concatBuffers } from '../buffer-utils.js' @@ -49,7 +49,7 @@ export async function computeNewPasswordHash( crypto.randomFill(salt1.subarray(algo.salt1.length)) ;(algo as tl.Mutable).salt1 = salt1 - const _x = await computePasswordHash(crypto, utf8EncodeToBuffer(password), algo.salt1, algo.salt2) + const _x = await computePasswordHash(crypto, getPlatform().utf8Encode(password), algo.salt1, algo.salt2) const g = BigInt(algo.g) const p = bufferToBigInt(algo.p) @@ -103,7 +103,7 @@ export async function computeSrpParams( const _k = crypto.sha256(concatBuffers([algo.p, _g])) const _u = crypto.sha256(concatBuffers([_gA, request.srpB])) - const _x = await computePasswordHash(crypto, utf8EncodeToBuffer(password), algo.salt1, algo.salt2) + const _x = await computePasswordHash(crypto, getPlatform().utf8Encode(password), algo.salt1, algo.salt2) const k = bufferToBigInt(_k) const u = bufferToBigInt(_u) const x = bufferToBigInt(_x) diff --git a/packages/core/src/utils/crypto/utils.test.ts b/packages/core/src/utils/crypto/utils.test.ts index e457dacf..529668f9 100644 --- a/packages/core/src/utils/crypto/utils.test.ts +++ b/packages/core/src/utils/crypto/utils.test.ts @@ -1,61 +1,62 @@ import { describe, expect, it } from 'vitest' -import { hexEncode, utf8Decode, utf8EncodeToBuffer } from '@mtcute/tl-runtime' - +import { getPlatform } from '../../platform.js' import { xorBuffer, xorBufferInPlace } from './utils.js' +const p = getPlatform() + describe('xorBuffer', () => { it('should xor buffers without modifying original', () => { - const data = utf8EncodeToBuffer('hello') - const key = utf8EncodeToBuffer('xor') + const data = p.utf8Encode('hello') + const key = p.utf8Encode('xor') const xored = xorBuffer(data, key) - expect(utf8Decode(data)).eq('hello') - expect(utf8Decode(key)).eq('xor') - expect(hexEncode(xored)).eq('100a1e6c6f') + expect(p.utf8Decode(data)).eq('hello') + expect(p.utf8Decode(key)).eq('xor') + expect(p.hexEncode(xored)).eq('100a1e6c6f') }) it('should be deterministic', () => { - const data = utf8EncodeToBuffer('hello') - const key = utf8EncodeToBuffer('xor') + const data = p.utf8Encode('hello') + const key = p.utf8Encode('xor') const xored1 = xorBuffer(data, key) - expect(hexEncode(xored1)).eq('100a1e6c6f') + expect(p.hexEncode(xored1)).eq('100a1e6c6f') const xored2 = xorBuffer(data, key) - expect(hexEncode(xored2)).eq('100a1e6c6f') + expect(p.hexEncode(xored2)).eq('100a1e6c6f') }) it('second call should decode content', () => { - const data = utf8EncodeToBuffer('hello') - const key = utf8EncodeToBuffer('xor') + const data = p.utf8Encode('hello') + const key = p.utf8Encode('xor') const xored1 = xorBuffer(data, key) - expect(hexEncode(xored1)).eq('100a1e6c6f') + expect(p.hexEncode(xored1)).eq('100a1e6c6f') const xored2 = xorBuffer(xored1, key) - expect(utf8Decode(xored2)).eq('hello') + expect(p.utf8Decode(xored2)).eq('hello') }) }) describe('xorBufferInPlace', () => { it('should xor buffers by modifying original', () => { - const data = utf8EncodeToBuffer('hello') - const key = utf8EncodeToBuffer('xor') + const data = p.utf8Encode('hello') + const key = p.utf8Encode('xor') xorBufferInPlace(data, key) - expect(hexEncode(data)).eq('100a1e6c6f') - expect(utf8Decode(key)).eq('xor') + expect(p.hexEncode(data)).eq('100a1e6c6f') + expect(p.utf8Decode(key)).eq('xor') }) it('second call should decode content', () => { - const data = utf8EncodeToBuffer('hello') - const key = utf8EncodeToBuffer('xor') + const data = p.utf8Encode('hello') + const key = p.utf8Encode('xor') xorBufferInPlace(data, key) - expect(hexEncode(data)).eq('100a1e6c6f') + expect(p.hexEncode(data)).eq('100a1e6c6f') xorBufferInPlace(data, key) - expect(utf8Decode(data)).eq('hello') + expect(p.utf8Decode(data)).eq('hello') }) }) diff --git a/packages/core/src/utils/crypto/wasm.ts b/packages/core/src/utils/crypto/wasm.ts index 9c2b2ed4..aab86e13 100644 --- a/packages/core/src/utils/crypto/wasm.ts +++ b/packages/core/src/utils/crypto/wasm.ts @@ -6,36 +6,15 @@ import { gunzip, ige256Decrypt, ige256Encrypt, - initAsync, - InitInput, sha1, sha256, } from '@mtcute/wasm' import { BaseCryptoProvider, IAesCtr, ICryptoProvider, IEncryptionScheme } from './abstract.js' -export interface WasmCryptoProviderOptions { - /** - * WASM blob to use for crypto operations. - * - * Must conform to `@mtcute/wasm` interface. - */ - wasmInput?: InitInput -} - export abstract class WasmCryptoProvider extends BaseCryptoProvider implements Partial { - readonly wasmInput?: InitInput - abstract randomFill(buf: Uint8Array): void - - constructor(params?: WasmCryptoProviderOptions) { - super() - this.wasmInput = params?.wasmInput - } - - initialize(): Promise { - return initAsync(this.wasmInput) - } + abstract initialize(): Promise sha1(data: Uint8Array): Uint8Array { return sha1(data) diff --git a/packages/core/src/utils/dcs.ts b/packages/core/src/utils/dcs.ts index 34a71f81..c8928906 100644 --- a/packages/core/src/utils/dcs.ts +++ b/packages/core/src/utils/dcs.ts @@ -49,7 +49,6 @@ export interface DcOptions { media: BasicDcOption } -/** @internal */ export const defaultProductionDc: DcOptions = { main: { ipAddress: '149.154.167.50', @@ -64,7 +63,6 @@ export const defaultProductionDc: DcOptions = { }, } -/** @internal */ export const defaultProductionIpv6Dc: DcOptions = { main: { ipAddress: '2001:067c:04e8:f002:0000:0000:0000:000a', @@ -81,7 +79,6 @@ export const defaultProductionIpv6Dc: DcOptions = { }, } -/** @internal */ export const defaultTestDc: DcOptions = { main: { ipAddress: '149.154.167.40', @@ -96,7 +93,6 @@ export const defaultTestDc: DcOptions = { }, } -/** @internal */ export const defaultTestIpv6Dc: DcOptions = { main: { ipAddress: '2001:67c:4e8:f002::e', diff --git a/packages/core/src/utils/platform/error-reporting.ts b/packages/core/src/utils/error-reporting.ts similarity index 95% rename from packages/core/src/utils/platform/error-reporting.ts rename to packages/core/src/utils/error-reporting.ts index 93b49328..7c9470e9 100644 --- a/packages/core/src/utils/platform/error-reporting.ts +++ b/packages/core/src/utils/error-reporting.ts @@ -1,6 +1,6 @@ import { tl } from '@mtcute/tl' -import { Logger } from '../logger.js' +import { Logger } from './logger.js' export function reportUnknownError(log: Logger, error: tl.RpcError, method: string): void { if (typeof fetch !== 'function') return diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 79ace5cd..2a9c6b56 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,4 +1,10 @@ export * from '../highlevel/utils/index.js' +// todo: remove after 1.0.0 +export * from '../highlevel/storage/service/current-user.js' +export * from '../highlevel/storage/service/updates.js' +export * from '../storage/service/base.js' +export * from '../storage/service/default-dcs.js' +// end todo export * from './async-lock.js' export * from './bigint-utils.js' export * from './buffer-utils.js' @@ -17,7 +23,6 @@ export * from './lru-map.js' export * from './lru-set.js' export * from './misc-utils.js' export * from './peer-utils.js' -export * from './platform/exit-hook.js' export * from './sorted-array.js' export * from './tl-json.js' export * from './type-assertions.js' diff --git a/packages/core/src/utils/logger.ts b/packages/core/src/utils/logger.ts index 044261f9..37285794 100644 --- a/packages/core/src/utils/logger.ts +++ b/packages/core/src/utils/logger.ts @@ -1,25 +1,6 @@ -import { hexEncode } from '@mtcute/tl-runtime' - -import { _defaultLoggingHandler } from './platform/logging.js' - -let defaultLogLevel = 3 - -/* c8 ignore start */ -if (typeof process !== 'undefined') { - const envLogLevel = parseInt(process.env.MTCUTE_LOG_LEVEL ?? '') - - if (!isNaN(envLogLevel)) { - defaultLogLevel = envLogLevel - } -} else if (typeof localStorage !== 'undefined') { - const localLogLevel = parseInt(localStorage.MTCUTE_LOG_LEVEL as string) - - if (!isNaN(localLogLevel)) { - defaultLogLevel = localLogLevel - } -} -/* c8 ignore end */ +import { getPlatform } from '../platform.js' +const DEFAULT_LOG_LEVEL = 3 const FORMATTER_RE = /%[a-zA-Z]/g /** @@ -81,7 +62,7 @@ export class Logger { args.splice(idx, 1) if (m === '%h') { - if (ArrayBuffer.isView(val)) return hexEncode(val as Uint8Array) + if (ArrayBuffer.isView(val)) return this.mgr.platform.hexEncode(val as Uint8Array) if (typeof val === 'number' || typeof val === 'bigint') return val.toString(16) return String(val) @@ -96,10 +77,10 @@ export class Logger { return JSON.stringify(val, (k, v) => { if ( ArrayBuffer.isView(v) || - (typeof v === 'object' && v.type === 'Buffer' && Array.isArray(v.data)) + (typeof v === 'object' && v.type === 'Buffer' && Array.isArray(v.data)) // todo: how can we do this better? ) { // eslint-disable-next-line - let str = v.data ? Buffer.from(v.data as number[]).toString('hex') : hexEncode(v) + let str = v.data ? Buffer.from(v.data as number[]).toString('hex') : this.mgr.platform.hexEncode(v) if (str.length > 300) { str = str.slice(0, 300) + '...' @@ -171,8 +152,10 @@ export class LogManager extends Logger { private _filter: (tag: string) => boolean = defaultFilter - level = defaultLogLevel - handler = _defaultLoggingHandler + readonly platform = getPlatform() + + level = this.platform.getDefaultLogLevel() ?? DEFAULT_LOG_LEVEL + handler = this.platform.log.bind(this.platform) /** * Create a {@link Logger} with the given tag diff --git a/packages/core/src/utils/platform/crypto.ts b/packages/core/src/utils/platform/crypto.ts deleted file mode 100644 index 52abfb45..00000000 --- a/packages/core/src/utils/platform/crypto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { NodeCryptoProvider } from '../crypto/node.js' - -/** @internal */ -export const _defaultCryptoProviderFactory = () => new NodeCryptoProvider() diff --git a/packages/core/src/utils/platform/crypto.web.ts b/packages/core/src/utils/platform/crypto.web.ts deleted file mode 100644 index c00dcc42..00000000 --- a/packages/core/src/utils/platform/crypto.web.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { MtUnsupportedError } from '../../index.js' -import { WebCryptoProvider } from '../crypto/web.js' - -/** @internal */ -export const _defaultCryptoProviderFactory = () => { - if (typeof crypto === 'undefined' || typeof crypto.subtle === 'undefined') { - throw new MtUnsupportedError('WebCrypto API is not available') - } - - return new WebCryptoProvider({ crypto }) -} diff --git a/packages/core/src/utils/platform/transport.ts b/packages/core/src/utils/platform/transport.ts deleted file mode 100644 index 8a9c0244..00000000 --- a/packages/core/src/utils/platform/transport.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { TcpTransport } from '../../network/transports/tcp.js' - -/** @internal */ -export const _defaultTransportFactory = () => new TcpTransport() diff --git a/packages/core/src/utils/platform/transport.web.ts b/packages/core/src/utils/platform/transport.web.ts deleted file mode 100644 index fdeb1614..00000000 --- a/packages/core/src/utils/platform/transport.web.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { WebSocketTransport } from '../../network/transports/websocket.js' -import { MtUnsupportedError } from '../../types/index.js' - -/** @internal */ -export const _defaultTransportFactory = - // if no websocket, throw an error i guess ¯\_(ツ)_/¯ - // (user can still implement something on their own) - typeof WebSocket === 'undefined' ? - () => { - throw new MtUnsupportedError( - 'Neither TCP nor WebSocket are available. Please pass a Transport factory explicitly', - ) - } : - () => new WebSocketTransport() diff --git a/packages/crypto-node/package.json b/packages/crypto-node/package.json index 127c5bb2..ced964fe 100644 --- a/packages/crypto-node/package.json +++ b/packages/crypto-node/package.json @@ -19,20 +19,24 @@ "keepScripts": [ "install" ], + "exports": { + ".": "./src/index.ts", + "./native.js": "./src/native.ts" + }, "distOnlyFields": { "exports": { ".": { "import": "./esm/index.js", "require": "./cjs/index.js" }, - "./native": { + "./native.js": { "import": "./esm/native.cjs", "require": "./cjs/native.cjs" } } }, "dependencies": { - "@mtcute/core": "workspace:^" + "@mtcute/node": "workspace:^" }, "devDependencies": { "@mtcute/test": "workspace:^" diff --git a/packages/crypto-node/src/index.ts b/packages/crypto-node/src/index.ts index 53c39f41..31961840 100644 --- a/packages/crypto-node/src/index.ts +++ b/packages/crypto-node/src/index.ts @@ -1,5 +1,5 @@ -import { BaseNodeCryptoProvider } from '@mtcute/core/src/utils/crypto/node.js' -import { IEncryptionScheme } from '@mtcute/core/utils.js' +import { BaseNodeCryptoProvider } from '@mtcute/node' +import { IEncryptionScheme } from '@mtcute/node/utils.js' import { native } from './native.cjs' diff --git a/packages/dispatcher/src/callback-data-builder.test.ts b/packages/dispatcher/src/callback-data-builder.test.ts index 5275515e..d960dbc2 100644 --- a/packages/dispatcher/src/callback-data-builder.test.ts +++ b/packages/dispatcher/src/callback-data-builder.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import { CallbackQuery, MtArgumentError, PeersIndex } from '@mtcute/core' -import { utf8EncodeToBuffer } from '@mtcute/core/utils.js' +import { getPlatform } from '@mtcute/core/platform.js' import { createStub } from '@mtcute/test' import { CallbackDataBuilder } from './callback-data-builder.js' @@ -53,7 +53,7 @@ describe('CallbackDataBuilder', () => { const createCb = (data: string) => new CallbackQuery( createStub('updateBotCallbackQuery', { - data: utf8EncodeToBuffer(data), + data: getPlatform().utf8Encode(data), }), new PeersIndex(), ) diff --git a/packages/dispatcher/src/context/base.ts b/packages/dispatcher/src/context/base.ts index c99ce7a2..65173b09 100644 --- a/packages/dispatcher/src/context/base.ts +++ b/packages/dispatcher/src/context/base.ts @@ -1,4 +1,5 @@ -import { ParsedUpdate, TelegramClient } from '@mtcute/core' +import { ParsedUpdate } from '@mtcute/core' +import { TelegramClient } from '@mtcute/core/client.js' export type UpdateContext = T & { client: TelegramClient diff --git a/packages/dispatcher/src/context/callback-query.ts b/packages/dispatcher/src/context/callback-query.ts index b305a51d..2018a00a 100644 --- a/packages/dispatcher/src/context/callback-query.ts +++ b/packages/dispatcher/src/context/callback-query.ts @@ -1,4 +1,5 @@ -import { CallbackQuery, InlineCallbackQuery, MaybePromise, Message, TelegramClient } from '@mtcute/core' +import { CallbackQuery, InlineCallbackQuery, MaybePromise, Message } from '@mtcute/core' +import { TelegramClient } from '@mtcute/core/client.js' import { UpdateContext } from './base.js' diff --git a/packages/dispatcher/src/context/chat-join-request.ts b/packages/dispatcher/src/context/chat-join-request.ts index bed8159c..b635d5a8 100644 --- a/packages/dispatcher/src/context/chat-join-request.ts +++ b/packages/dispatcher/src/context/chat-join-request.ts @@ -1,4 +1,5 @@ -import { BotChatJoinRequestUpdate, TelegramClient } from '@mtcute/core' +import { BotChatJoinRequestUpdate } from '@mtcute/core' +import { TelegramClient } from '@mtcute/core/client.js' import { UpdateContext } from './base.js' diff --git a/packages/dispatcher/src/context/chosen-inline-result.ts b/packages/dispatcher/src/context/chosen-inline-result.ts index c6c7b810..20707243 100644 --- a/packages/dispatcher/src/context/chosen-inline-result.ts +++ b/packages/dispatcher/src/context/chosen-inline-result.ts @@ -1,4 +1,5 @@ -import { ChosenInlineResult, MtArgumentError, TelegramClient } from '@mtcute/core' +import { ChosenInlineResult, MtArgumentError } from '@mtcute/core' +import { TelegramClient } from '@mtcute/core/client.js' import { UpdateContext } from './base.js' diff --git a/packages/dispatcher/src/context/inline-query.ts b/packages/dispatcher/src/context/inline-query.ts index 707770d7..1060e0a5 100644 --- a/packages/dispatcher/src/context/inline-query.ts +++ b/packages/dispatcher/src/context/inline-query.ts @@ -1,4 +1,5 @@ -import { InlineQuery, ParametersSkip1, TelegramClient } from '@mtcute/core' +import { InlineQuery, ParametersSkip1 } from '@mtcute/core' +import { TelegramClient } from '@mtcute/core/client.js' import { UpdateContext } from './base.js' diff --git a/packages/dispatcher/src/context/message.ts b/packages/dispatcher/src/context/message.ts index fda723f3..72f20857 100644 --- a/packages/dispatcher/src/context/message.ts +++ b/packages/dispatcher/src/context/message.ts @@ -1,9 +1,6 @@ -import { Message, MtPeerNotFoundError, OmitInputMessageId, ParametersSkip1, Peer, TelegramClient } from '@mtcute/core' -// todo: fix these imports when packaging -import { DeleteMessagesParams } from '@mtcute/core/src/highlevel/methods/messages/delete-messages.js' -import { ForwardMessageOptions } from '@mtcute/core/src/highlevel/methods/messages/forward-messages.js' -import { SendCopyParams } from '@mtcute/core/src/highlevel/methods/messages/send-copy.js' -import { SendCopyGroupParams } from '@mtcute/core/src/highlevel/methods/messages/send-copy-group.js' +import { Message, MtPeerNotFoundError, OmitInputMessageId, ParametersSkip1, Peer } from '@mtcute/core' +import { TelegramClient } from '@mtcute/core/client.js' +import { DeleteMessagesParams, ForwardMessageOptions, SendCopyGroupParams, SendCopyParams } from '@mtcute/core/methods.js' import { UpdateContext } from './base.js' diff --git a/packages/dispatcher/src/context/parse.ts b/packages/dispatcher/src/context/parse.ts index a63e2ba1..d382f64c 100644 --- a/packages/dispatcher/src/context/parse.ts +++ b/packages/dispatcher/src/context/parse.ts @@ -1,4 +1,5 @@ -import { ParsedUpdate, TelegramClient } from '@mtcute/core' +import { ParsedUpdate } from '@mtcute/core' +import { TelegramClient } from '@mtcute/core/client.js' import { UpdateContextDistributed } from './base.js' import { CallbackQueryContext } from './callback-query.js' diff --git a/packages/dispatcher/src/context/pre-checkout-query.ts b/packages/dispatcher/src/context/pre-checkout-query.ts index 05699cbb..9a824810 100644 --- a/packages/dispatcher/src/context/pre-checkout-query.ts +++ b/packages/dispatcher/src/context/pre-checkout-query.ts @@ -1,4 +1,5 @@ -import { PreCheckoutQuery, TelegramClient } from '@mtcute/core' +import { PreCheckoutQuery } from '@mtcute/core' +import { TelegramClient } from '@mtcute/core/client.js' import { UpdateContext } from './base.js' diff --git a/packages/dispatcher/src/dispatcher.ts b/packages/dispatcher/src/dispatcher.ts index cc773ee5..1906d49d 100644 --- a/packages/dispatcher/src/dispatcher.ts +++ b/packages/dispatcher/src/dispatcher.ts @@ -19,11 +19,11 @@ import { PollUpdate, PollVoteUpdate, StoryUpdate, - TelegramClient, tl, UserStatusUpdate, UserTypingUpdate, } from '@mtcute/core' +import { TelegramClient } from '@mtcute/core/client.js' import { UpdateContext } from './context/base.js' import { diff --git a/packages/dispatcher/src/handler.ts b/packages/dispatcher/src/handler.ts index a1229d4f..eb4aaad8 100644 --- a/packages/dispatcher/src/handler.ts +++ b/packages/dispatcher/src/handler.ts @@ -12,11 +12,11 @@ import { PollUpdate, PollVoteUpdate, StoryUpdate, - TelegramClient, tl, UserStatusUpdate, UserTypingUpdate, } from '@mtcute/core' +import { TelegramClient } from '@mtcute/core/client.js' import { UpdateContext } from './context/base.js' import { diff --git a/packages/file-id/package.json b/packages/file-id/package.json index f8ea61ea..751aae6a 100644 --- a/packages/file-id/package.json +++ b/packages/file-id/package.json @@ -21,5 +21,8 @@ "dependencies": { "@mtcute/tl-runtime": "workspace:^", "long": "5.2.3" + }, + "devDependencies": { + "@mtcute/test": "workspace:^" } } diff --git a/packages/file-id/src/parse.test.ts b/packages/file-id/src/parse.test.ts index 5ed848ea..47664620 100644 --- a/packages/file-id/src/parse.test.ts +++ b/packages/file-id/src/parse.test.ts @@ -1,7 +1,7 @@ import Long from 'long' import { describe, expect, it } from 'vitest' -import { hexDecodeToBuffer } from '@mtcute/tl-runtime' +import { defaultPlatform } from '@mtcute/test' import { parseFileId } from './parse.js' import { tdFileId as td } from './types.js' @@ -10,14 +10,14 @@ import { tdFileId as td } from './types.js' describe('parsing file ids', () => { const test = (id: string, expected: td.RawFullRemoteFileLocation) => { - expect(parseFileId(id)).eql(expected) + expect(parseFileId(defaultPlatform, id)).eql(expected) } it('parses common file ids', () => { test('CAACAgIAAxkBAAEJny9gituz1_V_uSKBUuG_nhtzEtFOeQACXFoAAuCjggfYjw_KAAGSnkgfBA', { _: 'remoteFileLocation', dcId: 2, - fileReference: hexDecodeToBuffer('0100099f2f608adbb3d7f57fb9228152e1bf9e1b7312d14e79'), + fileReference: defaultPlatform.hexDecode('0100099f2f608adbb3d7f57fb9228152e1bf9e1b7312d14e79'), location: { _: 'common', accessHash: Long.fromString('5232780349138767832'), @@ -28,7 +28,7 @@ describe('parsing file ids', () => { test('BQACAgIAAxkBAAEJnzNgit00IDsKd07OdSeanwz8osecYAACdAwAAueoWEicaPvNdOYEwB8E', { _: 'remoteFileLocation', dcId: 2, - fileReference: hexDecodeToBuffer('0100099f33608add34203b0a774ece75279a9f0cfca2c79c60'), + fileReference: defaultPlatform.hexDecode('0100099f33608add34203b0a774ece75279a9f0cfca2c79c60'), location: { _: 'common', accessHash: Long.fromString('-4610306729174144868'), @@ -42,7 +42,7 @@ describe('parsing file ids', () => { test('AAMCAgADGQEAAQmfL2CK27PX9X-5IoFS4b-eG3MS0U55AAJcWgAC4KOCB9iPD8oAAZKeSK1c8w4ABAEAB20AA1kCAAIfBA', { _: 'remoteFileLocation', dcId: 2, - fileReference: hexDecodeToBuffer('0100099f2f608adbb3d7f57fb9228152e1bf9e1b7312d14e79'), + fileReference: defaultPlatform.hexDecode('0100099f2f608adbb3d7f57fb9228152e1bf9e1b7312d14e79'), location: { _: 'photo', accessHash: Long.fromString('5232780349138767832'), diff --git a/packages/file-id/src/parse.ts b/packages/file-id/src/parse.ts index 1099c1a4..cc1209a8 100644 --- a/packages/file-id/src/parse.ts +++ b/packages/file-id/src/parse.ts @@ -1,4 +1,4 @@ -import { base64DecodeToBuffer, base64Encode, TlBinaryReader } from '@mtcute/tl-runtime' +import { ITlPlatform, TlBinaryReader } from '@mtcute/tl-runtime' import { tdFileId as td } from './types.js' import { telegramRleDecode } from './utils.js' @@ -11,7 +11,7 @@ function parseWebFileLocation(reader: TlBinaryReader): td.RawWebRemoteFileLocati } } -function parsePhotoSizeSource(reader: TlBinaryReader): td.TypePhotoSizeSource { +function parsePhotoSizeSource(platform: ITlPlatform, reader: TlBinaryReader): td.TypePhotoSizeSource { const variant = reader.int() switch (variant) { @@ -24,14 +24,16 @@ function parsePhotoSizeSource(reader: TlBinaryReader): td.TypePhotoSizeSource { const fileType = reader.int() if (fileType < 0 || fileType >= td.FileType.Size) { - throw new td.UnsupportedError(`Unsupported file type: ${fileType} (${base64Encode(reader.uint8View)})`) + throw new td.UnsupportedError( + `Unsupported file type: ${fileType} (${platform.base64Encode(reader.uint8View)})`, + ) } const thumbnailType = reader.int() if (thumbnailType < 0 || thumbnailType > 255) { throw new td.InvalidFileIdError( - `Wrong thumbnail type: ${thumbnailType} (${base64Encode(reader.uint8View)})`, + `Wrong thumbnail type: ${thumbnailType} (${platform.base64Encode(reader.uint8View)})`, ) } @@ -110,24 +112,28 @@ function parsePhotoSizeSource(reader: TlBinaryReader): td.TypePhotoSizeSource { } default: throw new td.UnsupportedError( - `Unsupported photo size source ${variant} (${base64Encode(reader.uint8View)})`, + `Unsupported photo size source ${variant} (${platform.base64Encode(reader.uint8View)})`, ) } } -function parsePhotoFileLocation(reader: TlBinaryReader, version: number): td.RawPhotoRemoteFileLocation { +function parsePhotoFileLocation( + platform: ITlPlatform, + reader: TlBinaryReader, + version: number, +): td.RawPhotoRemoteFileLocation { const id = reader.long() const accessHash = reader.long() let source: td.TypePhotoSizeSource if (version >= 32) { - source = parsePhotoSizeSource(reader) + source = parsePhotoSizeSource(platform, reader) } else { const volumeId = reader.long() let localId = 0 if (version >= 22) { - source = parsePhotoSizeSource(reader) + source = parsePhotoSizeSource(platform, reader) localId = reader.int() } else { source = { @@ -190,9 +196,9 @@ function parseCommonFileLocation(reader: TlBinaryReader): td.RawCommonRemoteFile } } -function fromPersistentIdV23(binary: Uint8Array, version: number): td.RawFullRemoteFileLocation { +function fromPersistentIdV23(platform: ITlPlatform, binary: Uint8Array, version: number): td.RawFullRemoteFileLocation { if (version < 0 || version > td.CURRENT_VERSION) { - throw new td.UnsupportedError(`Unsupported file ID v3 subversion: ${version} (${base64Encode(binary)})`) + throw new td.UnsupportedError(`Unsupported file ID v3 subversion: ${version} (${platform.base64Encode(binary)})`) } binary = telegramRleDecode(binary) @@ -208,7 +214,7 @@ function fromPersistentIdV23(binary: Uint8Array, version: number): td.RawFullRem fileType &= ~td.FILE_REFERENCE_FLAG if (fileType < 0 || fileType >= td.FileType.Size) { - throw new td.UnsupportedError(`Unsupported file type: ${fileType} (${base64Encode(binary)})`) + throw new td.UnsupportedError(`Unsupported file type: ${fileType} (${platform.base64Encode(binary)})`) } const dcId = reader.int() @@ -237,7 +243,7 @@ function fromPersistentIdV23(binary: Uint8Array, version: number): td.RawFullRem case td.FileType.EncryptedThumbnail: case td.FileType.Wallpaper: { // location_type = photo - location = parsePhotoFileLocation(reader, version) + location = parsePhotoFileLocation(platform, reader, version) // validate switch (location.source._) { @@ -287,7 +293,7 @@ function fromPersistentIdV23(binary: Uint8Array, version: number): td.RawFullRem break } default: - throw new td.UnsupportedError(`Invalid file type: ${fileType} (${base64Encode(binary)})`) + throw new td.UnsupportedError(`Invalid file type: ${fileType} (${platform.base64Encode(binary)})`) } } @@ -300,14 +306,14 @@ function fromPersistentIdV23(binary: Uint8Array, version: number): td.RawFullRem } } -function fromPersistentIdV2(binary: Uint8Array) { - return fromPersistentIdV23(binary.subarray(0, -1), 0) +function fromPersistentIdV2(platform: ITlPlatform, binary: Uint8Array) { + return fromPersistentIdV23(platform, binary.subarray(0, -1), 0) } -function fromPersistentIdV3(binary: Uint8Array) { +function fromPersistentIdV3(platform: ITlPlatform, binary: Uint8Array) { const subversion = binary[binary.length - 2] - return fromPersistentIdV23(binary.subarray(0, -2), subversion) + return fromPersistentIdV23(platform, binary.subarray(0, -2), subversion) } /** @@ -315,18 +321,18 @@ function fromPersistentIdV3(binary: Uint8Array) { * * @param fileId File ID as a base-64 encoded string or Buffer */ -export function parseFileId(fileId: string | Uint8Array): td.RawFullRemoteFileLocation { - if (typeof fileId === 'string') fileId = base64DecodeToBuffer(fileId, true) +export function parseFileId(platform: ITlPlatform, fileId: string | Uint8Array): td.RawFullRemoteFileLocation { + if (typeof fileId === 'string') fileId = platform.base64Decode(fileId, true) const version = fileId[fileId.length - 1] if (version === td.PERSISTENT_ID_VERSION_OLD) { - return fromPersistentIdV2(fileId) + return fromPersistentIdV2(platform, fileId) } if (version === td.PERSISTENT_ID_VERSION) { - return fromPersistentIdV3(fileId) + return fromPersistentIdV3(platform, fileId) } - throw new td.UnsupportedError(`Unsupported file ID version: ${version} (${base64Encode(fileId)})`) + throw new td.UnsupportedError(`Unsupported file ID version: ${version} (${platform.base64Encode(fileId)})`) } diff --git a/packages/file-id/src/serialize-unique.test.ts b/packages/file-id/src/serialize-unique.test.ts index dfec815f..a8a64115 100644 --- a/packages/file-id/src/serialize-unique.test.ts +++ b/packages/file-id/src/serialize-unique.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from 'vitest' +import { defaultPlatform } from '@mtcute/test' + import { parseFileId } from './parse.js' import { toUniqueFileId } from './serialize-unique.js' @@ -7,7 +9,7 @@ import { toUniqueFileId } from './serialize-unique.js' describe('serializing unique file ids', () => { const test = (id: string, expected: string) => { - expect(toUniqueFileId(parseFileId(id))).eql(expected) + expect(toUniqueFileId(defaultPlatform, parseFileId(defaultPlatform, id))).eql(expected) } it('serializes unique ids for old file ids', () => { diff --git a/packages/file-id/src/serialize-unique.ts b/packages/file-id/src/serialize-unique.ts index 9602ac1b..2eebf8a6 100644 --- a/packages/file-id/src/serialize-unique.ts +++ b/packages/file-id/src/serialize-unique.ts @@ -1,4 +1,4 @@ -import { base64Encode, byteLengthUtf8, TlBinaryWriter } from '@mtcute/tl-runtime' +import { ITlPlatform, TlBinaryWriter } from '@mtcute/tl-runtime' import { tdFileId as td } from './types.js' import { assertNever, telegramRleEncode } from './utils.js' @@ -20,10 +20,11 @@ export type InputUniqueLocation = * * @param location Information about file location */ -export function toUniqueFileId(location: Omit): string -export function toUniqueFileId(type: td.FileType, location: InputUniqueLocation): string +export function toUniqueFileId(platform: ITlPlatform, location: Omit): string +export function toUniqueFileId(platform: ITlPlatform, type: td.FileType, location: InputUniqueLocation): string export function toUniqueFileId( + platform: ITlPlatform, first: td.FileType | Omit, second?: InputUniqueLocation, ): string { @@ -140,7 +141,7 @@ export function toUniqueFileId( break } case 'web': - writer = TlBinaryWriter.alloc(undefined, byteLengthUtf8(inputLocation.url) + 8) + writer = TlBinaryWriter.manual(platform.utf8ByteLength(inputLocation.url) + 8) writer.int(type) writer.string(inputLocation.url) break @@ -153,5 +154,5 @@ export function toUniqueFileId( assertNever(inputLocation) } - return base64Encode(telegramRleEncode(writer.result()), true) + return platform.base64Encode(telegramRleEncode(writer.result()), true) } diff --git a/packages/file-id/src/serialize.ts b/packages/file-id/src/serialize.ts index 6b563748..b7294316 100644 --- a/packages/file-id/src/serialize.ts +++ b/packages/file-id/src/serialize.ts @@ -1,4 +1,4 @@ -import { base64Encode, byteLengthUtf8, TlBinaryWriter } from '@mtcute/tl-runtime' +import { ITlPlatform, TlBinaryWriter } from '@mtcute/tl-runtime' import { tdFileId as td } from './types.js' import { assertNever, telegramRleEncode } from './utils.js' @@ -11,7 +11,7 @@ const SUFFIX = new Uint8Array([td.CURRENT_VERSION, td.PERSISTENT_ID_VERSION]) * * @param location Information about file location */ -export function toFileId(location: Omit): string { +export function toFileId(platform: ITlPlatform, location: Omit): string { const loc = location.location let type: number = location.type @@ -25,7 +25,7 @@ export function toFileId(location: Omit): str // // longest file ids are around 80 bytes, so i guess // we are safe with allocating 100 bytes - const writer = TlBinaryWriter.alloc(undefined, loc._ === 'web' ? byteLengthUtf8(loc.url) + 32 : 100) + const writer = TlBinaryWriter.manual(loc._ === 'web' ? platform.utf8ByteLength(loc.url) + 32 : 100) writer.int(type) writer.int(location.dcId) @@ -108,5 +108,5 @@ export function toFileId(location: Omit): str withSuffix.set(result) withSuffix.set(SUFFIX, result.length) - return base64Encode(withSuffix, true) + return platform.base64Encode(withSuffix, true) } diff --git a/packages/file-id/src/utils.test.ts b/packages/file-id/src/utils.test.ts index 59cbccec..68088d99 100644 --- a/packages/file-id/src/utils.test.ts +++ b/packages/file-id/src/utils.test.ts @@ -1,32 +1,34 @@ import { describe, expect, it } from 'vitest' -import { hexDecodeToBuffer, hexEncode } from '@mtcute/tl-runtime' +import { defaultPlatform } from '@mtcute/test' import { telegramRleDecode, telegramRleEncode } from './utils.js' +const p = defaultPlatform + describe('telegramRleEncode', () => { it('should not modify input if there are no \\x00', () => { - expect(hexEncode(telegramRleEncode(hexDecodeToBuffer('aaeeff')))).eq('aaeeff') + expect(p.hexEncode(telegramRleEncode(p.hexDecode('aaeeff')))).eq('aaeeff') }) it('should collapse consecutive \\x00', () => { - expect(hexEncode(telegramRleEncode(hexDecodeToBuffer('00000000aa')))).eq('0004aa') - expect(hexEncode(telegramRleEncode(hexDecodeToBuffer('00000000aa000000aa')))).eq('0004aa0003aa') - expect(hexEncode(telegramRleEncode(hexDecodeToBuffer('00000000aa0000')))).eq('0004aa0002') - expect(hexEncode(telegramRleEncode(hexDecodeToBuffer('00aa00')))).eq('0001aa0001') + expect(p.hexEncode(telegramRleEncode(p.hexDecode('00000000aa')))).eq('0004aa') + expect(p.hexEncode(telegramRleEncode(p.hexDecode('00000000aa000000aa')))).eq('0004aa0003aa') + expect(p.hexEncode(telegramRleEncode(p.hexDecode('00000000aa0000')))).eq('0004aa0002') + expect(p.hexEncode(telegramRleEncode(p.hexDecode('00aa00')))).eq('0001aa0001') }) }) describe('telegramRleDecode', () => { it('should not mofify input if there are no \\x00', () => { - expect(hexEncode(telegramRleDecode(hexDecodeToBuffer('aaeeff')))).eq('aaeeff') + expect(p.hexEncode(telegramRleDecode(p.hexDecode('aaeeff')))).eq('aaeeff') }) it('should expand two-byte sequences starting with \\x00', () => { - expect(hexEncode(telegramRleDecode(hexDecodeToBuffer('0004aa')))).eq('00000000aa') - expect(hexEncode(telegramRleDecode(hexDecodeToBuffer('0004aa0000')))).eq('00000000aa') - expect(hexEncode(telegramRleDecode(hexDecodeToBuffer('0004aa0003aa')))).eq('00000000aa000000aa') - expect(hexEncode(telegramRleDecode(hexDecodeToBuffer('0004aa0002')))).eq('00000000aa0000') - expect(hexEncode(telegramRleDecode(hexDecodeToBuffer('0001aa0001')))).eq('00aa00') + expect(p.hexEncode(telegramRleDecode(p.hexDecode('0004aa')))).eq('00000000aa') + expect(p.hexEncode(telegramRleDecode(p.hexDecode('0004aa0000')))).eq('00000000aa') + expect(p.hexEncode(telegramRleDecode(p.hexDecode('0004aa0003aa')))).eq('00000000aa000000aa') + expect(p.hexEncode(telegramRleDecode(p.hexDecode('0004aa0002')))).eq('00000000aa0000') + expect(p.hexEncode(telegramRleDecode(p.hexDecode('0001aa0001')))).eq('00aa00') }) }) diff --git a/packages/http-proxy/index.ts b/packages/http-proxy/index.ts index 3960e81c..1f363cd3 100644 --- a/packages/http-proxy/index.ts +++ b/packages/http-proxy/index.ts @@ -1,9 +1,7 @@ import { connect as connectTcp } from 'net' import { connect as connectTls, SecureContextOptions } from 'tls' -import { IntermediatePacketCodec, MtcuteError, tl, TransportState } from '@mtcute/core' -import { BaseTcpTransport } from '@mtcute/core/src/network/transports/tcp.js' -import { base64Encode, utf8EncodeToBuffer } from '@mtcute/core/utils.js' +import { BaseTcpTransport, IntermediatePacketCodec, MtcuteError, NodePlatform, tl, TransportState } from '@mtcute/node' /** * An error has occurred while connecting to an HTTP(s) proxy @@ -71,6 +69,8 @@ export abstract class BaseHttpProxyTcpTransport extends BaseTcpTransport { this._proxy = proxy } + private _platform = new NodePlatform() + connect(dc: tl.RawDcOption): void { if (this._state !== TransportState.Idle) { throw new MtcuteError('Transport is not IDLE') @@ -110,7 +110,7 @@ export abstract class BaseHttpProxyTcpTransport extends BaseTcpTransport { if (this._proxy.password) { auth += ':' + this._proxy.password } - headers['Proxy-Authorization'] = 'Basic ' + base64Encode(utf8EncodeToBuffer(auth)) + headers['Proxy-Authorization'] = 'Basic ' + this._platform.base64Encode(this._platform.utf8Encode(auth)) } headers['Proxy-Connection'] = 'Keep-Alive' diff --git a/packages/http-proxy/package.json b/packages/http-proxy/package.json index 109ee401..b9c83507 100644 --- a/packages/http-proxy/package.json +++ b/packages/http-proxy/package.json @@ -20,6 +20,6 @@ } }, "dependencies": { - "@mtcute/core": "workspace:^" + "@mtcute/node": "workspace:^" } } diff --git a/packages/mtproxy/fake-tls.ts b/packages/mtproxy/fake-tls.ts index 94fc403b..5044ffd9 100644 --- a/packages/mtproxy/fake-tls.ts +++ b/packages/mtproxy/fake-tls.ts @@ -1,6 +1,6 @@ /* eslint-disable no-restricted-globals */ -import { IPacketCodec, WrappedCodec } from '@mtcute/core' -import { bigIntModInv, bigIntModPow, bigIntToBuffer, bufferToBigInt, ICryptoProvider } from '@mtcute/core/utils.js' +import { IPacketCodec, WrappedCodec } from '@mtcute/node' +import { bigIntModInv, bigIntModPow, bigIntToBuffer, bufferToBigInt, ICryptoProvider } from '@mtcute/node/utils.js' const MAX_TLS_PACKET_LENGTH = 2878 const TLS_FIRST_PREFIX = Buffer.from('140303000101', 'hex') diff --git a/packages/mtproxy/index.ts b/packages/mtproxy/index.ts index f4475c0c..b99c5d43 100644 --- a/packages/mtproxy/index.ts +++ b/packages/mtproxy/index.ts @@ -4,6 +4,7 @@ import { connect } from 'net' import { + BaseTcpTransport, IntermediatePacketCodec, IPacketCodec, MtcuteError, @@ -13,9 +14,8 @@ import { PaddedIntermediatePacketCodec, tl, TransportState, -} from '@mtcute/core' -import { BaseTcpTransport } from '@mtcute/core/src/network/transports/tcp.js' -import { buffersEqual } from '@mtcute/core/utils.js' +} from '@mtcute/node' +import { buffersEqual } from '@mtcute/node/utils.js' import { FakeTlsPacketCodec, generateFakeTlsHeader } from './fake-tls.js' diff --git a/packages/mtproxy/package.json b/packages/mtproxy/package.json index cf73b048..96b811a8 100644 --- a/packages/mtproxy/package.json +++ b/packages/mtproxy/package.json @@ -20,6 +20,6 @@ } }, "dependencies": { - "@mtcute/core": "workspace:^" + "@mtcute/node": "workspace:^" } } diff --git a/packages/node/package.json b/packages/node/package.json index 68e1606b..364b13e3 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -5,12 +5,16 @@ "description": "Meta-package for Node JS", "author": "Alina Sireneva ", "license": "MIT", - "main": "index.ts", + "main": "src/index.ts", "type": "module", "scripts": { "docs": "typedoc", "build": "pnpm run -w build-package node" }, + "exports": { + ".": "./src/index.ts", + "./utils.js": "./src/utils.ts" + }, "distOnlyFields": { "exports": { ".": { @@ -25,8 +29,12 @@ }, "dependencies": { "@mtcute/core": "workspace:^", + "@mtcute/wasm": "workspace:^", "@mtcute/sqlite": "workspace:^", "@mtcute/markdown-parser": "workspace:^", "@mtcute/html-parser": "workspace:^" + }, + "devDependencies": { + "@mtcute/test": "workspace:^" } } diff --git a/packages/node/index.ts b/packages/node/src/client.ts similarity index 66% rename from packages/node/index.ts rename to packages/node/src/client.ts index da2c1b3a..66d2b048 100644 --- a/packages/node/index.ts +++ b/packages/node/src/client.ts @@ -1,13 +1,18 @@ import { createRequire } from 'module' import { createInterface, Interface as RlInterface } from 'readline' -import { TelegramClient, TelegramClientOptions, User } from '@mtcute/core' +import { FileDownloadLocation, FileDownloadParameters, User } from '@mtcute/core' +import { TelegramClient as TelegramClientBase, TelegramClientOptions } from '@mtcute/core/client.js' +import { setPlatform } from '@mtcute/core/platform.js' import { SqliteStorage } from '@mtcute/sqlite' -export * from '@mtcute/core' -export * from '@mtcute/html-parser' -export * from '@mtcute/markdown-parser' -export { SqliteStorage } +import { downloadToFile } from './methods/download-file.js' +import { uploadFile } from './methods/upload-file.js' +import { NodePlatform } from './platform.js' +import { NodeCryptoProvider } from './utils/crypto.js' +import { TcpTransport } from './utils/tcp.js' + +export type { TelegramClientOptions } // eslint-disable-next-line @typescript-eslint/no-explicit-any let nativeCrypto: any @@ -21,14 +26,12 @@ try { } catch (e) {} /** - * Tiny wrapper over {@link TelegramClient} for usage inside Node JS. - * - * This class automatically manages native - * crypto addon and defaults to SQLite session (unlike `TelegarmClient`, - * which defaults to a JSON file on Node). + * Telegram client for use in Node.js */ -export class NodeTelegramClient extends TelegramClient { +export class TelegramClient extends TelegramClientBase { constructor(opts: TelegramClientOptions) { + setPlatform(new NodePlatform()) + if ('client' in opts) { super(opts) @@ -37,7 +40,8 @@ export class NodeTelegramClient extends TelegramClient { super({ // eslint-disable-next-line - crypto: nativeCrypto ? () => new nativeCrypto() : undefined, + crypto: nativeCrypto ? new nativeCrypto() : new NodeCryptoProvider(), + transport: () => new TcpTransport(), ...opts, storage: typeof opts.storage === 'string' ? @@ -74,7 +78,7 @@ export class NodeTelegramClient extends TelegramClient { return super.close() } - start(params: Parameters[0] = {}): Promise { + start(params: Parameters[0] = {}): Promise { if (!params.botToken) { if (!params.phone) params.phone = () => this.input('phone > ') if (!params.code) params.code = () => this.input('code > ') @@ -107,4 +111,16 @@ export class NodeTelegramClient extends TelegramClient { .then(then) .catch((err) => this.emitError(err)) } + + downloadToFile( + filename: string, + location: FileDownloadLocation, + params?: FileDownloadParameters | undefined, + ): Promise { + return downloadToFile(this, filename, location, params) + } + + uploadFile(params: Parameters[1]) { + return uploadFile(this, params) + } } diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts new file mode 100644 index 00000000..243dda6a --- /dev/null +++ b/packages/node/src/index.ts @@ -0,0 +1,8 @@ +export * from './client.js' +export * from './platform.js' +export * from './utils/tcp.js' +export * from './utils/crypto.js' +export * from '@mtcute/core' +export * from '@mtcute/html-parser' +export * from '@mtcute/markdown-parser' +export * from '@mtcute/sqlite' diff --git a/packages/node/src/methods.ts b/packages/node/src/methods.ts new file mode 100644 index 00000000..e9a3f076 --- /dev/null +++ b/packages/node/src/methods.ts @@ -0,0 +1,5 @@ +/* eslint-disable import/export, simple-import-sort/exports */ +export * from '@mtcute/core/methods.js' + +export { downloadToFile } from './methods/download-file.js' +export { uploadFile } from './methods/upload-file.js' diff --git a/packages/node/src/methods/download-file.ts b/packages/node/src/methods/download-file.ts new file mode 100644 index 00000000..5d90285e --- /dev/null +++ b/packages/node/src/methods/download-file.ts @@ -0,0 +1,40 @@ +import { createWriteStream, rmSync } from 'fs' +import { writeFile } from 'fs/promises' + +import { FileDownloadLocation, FileDownloadParameters, FileLocation, ITelegramClient } from '@mtcute/core' +import { downloadAsIterable } from '@mtcute/core/methods.js' + +/** + * Download a remote file to a local file (only for NodeJS). + * Promise will resolve once the download is complete. + * + * @param filename Local file name to which the remote file will be downloaded + * @param params File download parameters + */ +export async function downloadToFile( + client: ITelegramClient, + filename: string, + location: FileDownloadLocation, + params?: FileDownloadParameters, +): Promise { + if (location instanceof FileLocation && ArrayBuffer.isView(location.location)) { + // early return for inline files + await writeFile(filename, location.location) + } + + const output = createWriteStream(filename) + + if (params?.abortSignal) { + params.abortSignal.addEventListener('abort', () => { + client.log.debug('aborting file download %s - cleaning up', filename) + output.destroy() + rmSync(filename) + }) + } + + for await (const chunk of downloadAsIterable(client, location, params)) { + output.write(chunk) + } + + output.end() +} diff --git a/packages/node/src/methods/upload-file.ts b/packages/node/src/methods/upload-file.ts new file mode 100644 index 00000000..7fe71c6c --- /dev/null +++ b/packages/node/src/methods/upload-file.ts @@ -0,0 +1,41 @@ +import { createReadStream, ReadStream } from 'fs' +import { stat } from 'fs/promises' +import { basename } from 'path' +import { Readable } from 'stream' + +import { ITelegramClient } from '@mtcute/core' +import { uploadFile as uploadFileCore } from '@mtcute/core/methods.js' + +import { nodeStreamToWeb } from '../utils/stream-utils.js' + +export async function uploadFile( + client: ITelegramClient, + params: Parameters[1], +) { + let file = params.file + + if (typeof file === 'string') { + file = createReadStream(file) + } + + if (file instanceof ReadStream) { + const fileName = basename(file.path.toString()) + const fileSize = await stat(file.path.toString()).then((stat) => stat.size) + + return uploadFileCore(client, { + ...params, + file: nodeStreamToWeb(file), + fileName, + fileSize, + }) + } + + if (file instanceof Readable) { + return uploadFileCore(client, { + ...params, + file: nodeStreamToWeb(file), + }) + } + + return uploadFileCore(client, params) +} diff --git a/packages/node/src/platform.ts b/packages/node/src/platform.ts new file mode 100644 index 00000000..80800b75 --- /dev/null +++ b/packages/node/src/platform.ts @@ -0,0 +1,78 @@ +import * as os from 'os' + +import { ICorePlatform } from '@mtcute/core/platform.js' + +import { beforeExit } from './utils/exit-hook.js' +import { defaultLoggingHandler } from './utils/logging.js' + +const BUFFER_BASE64_URL_AVAILABLE = typeof Buffer.isEncoding === 'function' && Buffer.isEncoding('base64url') + +const toBuffer = (buf: Uint8Array): Buffer => Buffer.from( + buf.buffer, + buf.byteOffset, + buf.byteLength, +) + +export class NodePlatform implements ICorePlatform { + // ICorePlatform + log!: typeof defaultLoggingHandler + beforeExit!: typeof beforeExit + + getDeviceModel(): string { + return `${os.type()} ${os.arch()} ${os.release()}` + } + + getDefaultLogLevel(): number | null { + const envLogLevel = parseInt(process.env.MTCUTE_LOG_LEVEL ?? '') + + if (!isNaN(envLogLevel)) { + return envLogLevel + } + + return null + } + + // ITlPlatform + utf8ByteLength(str: string): number { + return Buffer.byteLength(str, 'utf8') + } + utf8Encode(str: string): Uint8Array { + return Buffer.from(str, 'utf8') + } + utf8Decode(buf: Uint8Array): string { + return toBuffer(buf).toString('utf8') + } + + hexEncode(buf: Uint8Array): string { + return toBuffer(buf).toString('hex') + } + hexDecode(str: string): Uint8Array { + return Buffer.from(str, 'hex') + } + + base64Encode(buf: Uint8Array, url = false): string { + const nodeBuffer = toBuffer(buf) + + if (url && BUFFER_BASE64_URL_AVAILABLE) return nodeBuffer.toString('base64url') + + const str = nodeBuffer.toString('base64') + if (url) return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') + + return str + } + base64Decode(string: string, url = false): Uint8Array { + if (url && BUFFER_BASE64_URL_AVAILABLE) { + return Buffer.from(string, 'base64url') + } + + if (url) { + string = string.replace(/-/g, '+').replace(/_/g, '/') + while (string.length % 4) string += '=' + } + + return Buffer.from(string, 'base64') + } +} + +NodePlatform.prototype.log = defaultLoggingHandler +NodePlatform.prototype.beforeExit = beforeExit diff --git a/packages/node/utils.ts b/packages/node/src/utils.ts similarity index 100% rename from packages/node/utils.ts rename to packages/node/src/utils.ts diff --git a/packages/core/src/utils/crypto/node.test.ts b/packages/node/src/utils/crypto.test.ts similarity index 83% rename from packages/core/src/utils/crypto/node.test.ts rename to packages/node/src/utils/crypto.test.ts index b3e7dd79..41e32872 100644 --- a/packages/core/src/utils/crypto/node.test.ts +++ b/packages/node/src/utils/crypto.test.ts @@ -4,7 +4,7 @@ import { testCryptoProvider } from '@mtcute/test' if (import.meta.env.TEST_ENV === 'node' || import.meta.env.TEST_ENV === 'bun') { describe('NodeCryptoProvider', async () => { - const { NodeCryptoProvider } = await import('./node.js') + const { NodeCryptoProvider } = await import('./crypto.js') testCryptoProvider(new NodeCryptoProvider()) }) diff --git a/packages/core/src/utils/crypto/node.ts b/packages/node/src/utils/crypto.ts similarity index 83% rename from packages/core/src/utils/crypto/node.ts rename to packages/node/src/utils/crypto.ts index f63cf7fb..a04d1f13 100644 --- a/packages/core/src/utils/crypto/node.ts +++ b/packages/node/src/utils/crypto.ts @@ -1,11 +1,12 @@ // eslint-disable-next-line no-restricted-imports import { createCipheriv, createHash, createHmac, pbkdf2, randomFillSync } from 'crypto' +import { readFile } from 'fs/promises' +import { createRequire } from 'module' import { deflateSync, gunzipSync } from 'zlib' -import { ige256Decrypt, ige256Encrypt, initAsync, InitInput } from '@mtcute/wasm' - -import { MaybePromise } from '../../types/index.js' -import { BaseCryptoProvider, IAesCtr, ICryptoProvider, IEncryptionScheme } from './abstract.js' +import { MaybePromise } from '@mtcute/core' +import { BaseCryptoProvider, IAesCtr, ICryptoProvider, IEncryptionScheme } from '@mtcute/core/utils.js' +import { ige256Decrypt, ige256Encrypt, initSync } from '@mtcute/wasm' export abstract class BaseNodeCryptoProvider extends BaseCryptoProvider { createAesCtr(key: Uint8Array, iv: Uint8Array): IAesCtr { @@ -45,7 +46,6 @@ export abstract class BaseNodeCryptoProvider extends BaseCryptoProvider { } gzip(data: Uint8Array, maxSize: number): Uint8Array | null { - // todo: test if wasm impl is better fit here try { // telegram accepts both zlib and gzip, but zlib is faster and has less overhead, so we use it here return deflateSync(data, { @@ -72,15 +72,13 @@ export abstract class BaseNodeCryptoProvider extends BaseCryptoProvider { } export class NodeCryptoProvider extends BaseNodeCryptoProvider implements ICryptoProvider { - private wasmInput?: InitInput - - constructor(params?: { wasmInput?: InitInput }) { - super() - this.wasmInput = params?.wasmInput - } - - initialize(): Promise { - return initAsync(this.wasmInput) + async initialize(): Promise { + // @only-if-esm + const require = createRequire(import.meta.url) + // @/only-if-esm + const wasmFile = require.resolve('@mtcute/wasm/mtcute.wasm') + const wasm = await readFile(wasmFile) + initSync(wasm) } createAesIge(key: Uint8Array, iv: Uint8Array): IEncryptionScheme { diff --git a/packages/core/src/utils/platform/exit-hook.ts b/packages/node/src/utils/exit-hook.ts similarity index 100% rename from packages/core/src/utils/platform/exit-hook.ts rename to packages/node/src/utils/exit-hook.ts diff --git a/packages/core/src/utils/platform/logging.ts b/packages/node/src/utils/logging.ts similarity index 95% rename from packages/core/src/utils/platform/logging.ts rename to packages/node/src/utils/logging.ts index 5f75c41d..1f3dfae7 100644 --- a/packages/core/src/utils/platform/logging.ts +++ b/packages/node/src/utils/logging.ts @@ -21,7 +21,7 @@ const LEVEL_NAMES = isTty ? const TAG_COLORS = [6, 2, 3, 4, 5, 1].map((i) => `\x1b[3${i};1m`) /** @internal */ -export const _defaultLoggingHandler = isTty ? +export const defaultLoggingHandler = isTty ? (color: number, level: number, tag: string, fmt: string, args: unknown[]): void => { // eslint-disable-next-line no-console console.log(BASE_FORMAT + fmt, new Date().toISOString(), LEVEL_NAMES[level], TAG_COLORS[color], tag, ...args) diff --git a/packages/node/src/utils/stream-utils.ts b/packages/node/src/utils/stream-utils.ts new file mode 100644 index 00000000..8a450f5e --- /dev/null +++ b/packages/node/src/utils/stream-utils.ts @@ -0,0 +1,26 @@ +import { Readable } from 'stream' + +export function nodeStreamToWeb(stream: Readable): ReadableStream { + if (typeof Readable.toWeb === 'function') { + return Readable.toWeb(stream) + } + + return new ReadableStream({ + start(controller) { + stream.on('data', (chunk) => { + controller.enqueue(chunk) + }) + stream.on('end', () => { + controller.close() + }) + stream.on('error', (err) => { + controller.error(err) + }) + }, + cancel() { + if (typeof stream.destroy === 'function') { + stream.destroy() + } + }, + }) +} diff --git a/packages/core/src/network/transports/tcp.test.ts b/packages/node/src/utils/tcp.test.ts similarity index 92% rename from packages/core/src/network/transports/tcp.test.ts rename to packages/node/src/utils/tcp.test.ts index 1ff723a2..d446b923 100644 --- a/packages/core/src/network/transports/tcp.test.ts +++ b/packages/node/src/utils/tcp.test.ts @@ -1,6 +1,11 @@ -import { Socket } from 'net' +import type { Socket } from 'net' import { describe, expect, it, MockedObject, vi } from 'vitest' +import { TransportState } from '@mtcute/core' +import { getPlatform } from '@mtcute/core/platform.js' +import { defaultProductionDc, LogManager } from '@mtcute/core/utils.js' +import { defaultTestCryptoProvider, u8HexDecode } from '@mtcute/test' + if (import.meta.env.TEST_ENV === 'node' || import.meta.env.TEST_ENV === 'bun') { vi.doMock('net', () => ({ connect: vi.fn().mockImplementation((port: number, ip: string, cb: () => void) => { @@ -21,12 +26,7 @@ if (import.meta.env.TEST_ENV === 'node' || import.meta.env.TEST_ENV === 'bun') { const net = await import('net') const connect = vi.mocked(net.connect) - // we need to do these imports here because basically all of them import `net` - const { defaultTestCryptoProvider, u8HexDecode } = await import('@mtcute/test') - const { TcpTransport } = await import('./tcp.js') - const { defaultProductionDc, hexDecodeToBuffer, LogManager } = await import('../../utils/index.js') - const { TransportState } = await import('./abstract.js') describe('TcpTransport', () => { const getLastSocket = () => { @@ -90,7 +90,7 @@ if (import.meta.env.TEST_ENV === 'node' || import.meta.env.TEST_ENV === 'bun') { t.connect(defaultProductionDc.main, false) await vi.waitFor(() => expect(t.state()).toEqual(TransportState.Ready)) - await t.send(hexDecodeToBuffer('00010203040506070809')) + await t.send(getPlatform().hexDecode('00010203040506070809')) const socket = getLastSocket() diff --git a/packages/core/src/network/transports/tcp.ts b/packages/node/src/utils/tcp.ts similarity index 93% rename from packages/core/src/network/transports/tcp.ts rename to packages/node/src/utils/tcp.ts index bd191ea7..4e925a5e 100644 --- a/packages/core/src/network/transports/tcp.ts +++ b/packages/node/src/utils/tcp.ts @@ -1,10 +1,8 @@ import EventEmitter from 'events' import { connect, Socket } from 'net' -import { MtcuteError } from '../../types/errors.js' -import { BasicDcOption, ICryptoProvider, Logger } from '../../utils/index.js' -import { IPacketCodec, ITelegramTransport, TransportState } from './abstract.js' -import { IntermediatePacketCodec } from './intermediate.js' +import { IntermediatePacketCodec, IPacketCodec, ITelegramTransport, MtcuteError, TransportState } from '@mtcute/core' +import { BasicDcOption, ICryptoProvider, Logger } from '@mtcute/core/utils.js' /** * Base for TCP transports. diff --git a/packages/node/tsconfig.json b/packages/node/tsconfig.json index a67acb6f..b9a14bd0 100644 --- a/packages/node/tsconfig.json +++ b/packages/node/tsconfig.json @@ -1,11 +1,11 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "outDir": "./dist/esm" + "outDir": "./dist/esm", + "rootDir": "./src" }, "include": [ - "./index.ts", - "./utils.ts", + "./src", ], "references": [ { "path": "../core" }, diff --git a/packages/socks-proxy/index.ts b/packages/socks-proxy/index.ts index 74f2ac13..a08ea7f0 100644 --- a/packages/socks-proxy/index.ts +++ b/packages/socks-proxy/index.ts @@ -5,9 +5,10 @@ import { normalize } from 'ip6' import { connect } from 'net' -import { assertNever, IntermediatePacketCodec, MtArgumentError, tl, TransportState } from '@mtcute/core' -import { BaseTcpTransport } from '@mtcute/core/src/network/transports/tcp.js' -import { dataViewFromBuffer, utf8EncodeToBuffer } from '@mtcute/core/utils.js' +import { assertNever, BaseTcpTransport, IntermediatePacketCodec, MtArgumentError, NodePlatform, tl, TransportState } from '@mtcute/node' +import { dataViewFromBuffer } from '@mtcute/node/utils.js' + +const p = new NodePlatform() /** * An error has occurred while connecting to an SOCKS proxy @@ -71,7 +72,7 @@ function writeIpv4(ip: string, buf: Uint8Array, offset: number): void { } function buildSocks4ConnectRequest(ip: string, port: number, username = ''): Uint8Array { - const userId = utf8EncodeToBuffer(username) + const userId = p.utf8Encode(username) const buf = new Uint8Array(9 + userId.length) buf[0] = 0x04 // VER @@ -102,8 +103,8 @@ function buildSocks5Greeting(authAvailable: boolean): Uint8Array { } function buildSocks5Auth(username: string, password: string) { - const usernameBuf = utf8EncodeToBuffer(username) - const passwordBuf = utf8EncodeToBuffer(password) + const usernameBuf = p.utf8Encode(username) + const passwordBuf = p.utf8Encode(password) if (usernameBuf.length > 255) { throw new MtArgumentError(`Too long username (${usernameBuf.length} > 255)`) diff --git a/packages/socks-proxy/package.json b/packages/socks-proxy/package.json index 68ef9eb8..e3c89687 100644 --- a/packages/socks-proxy/package.json +++ b/packages/socks-proxy/package.json @@ -20,7 +20,7 @@ } }, "dependencies": { - "@mtcute/core": "workspace:^", + "@mtcute/node": "workspace:^", "ip6": "0.2.7" } } diff --git a/packages/sqlite/src/driver.ts b/packages/sqlite/src/driver.ts index a6c2d60a..42ddeb83 100644 --- a/packages/sqlite/src/driver.ts +++ b/packages/sqlite/src/driver.ts @@ -1,7 +1,7 @@ import sqlite3, { Database, Options, Statement } from 'better-sqlite3' import { BaseStorageDriver } from '@mtcute/core' -import { beforeExit } from '@mtcute/core/utils.js' +import { getPlatform } from '@mtcute/core/platform.js' export interface SqliteStorageDriverOptions { /** @@ -167,7 +167,7 @@ export class SqliteStorageDriver extends BaseStorageDriver { this.db.transaction(() => this._initialize())() - this._cleanup = beforeExit(() => { + this._cleanup = getPlatform().beforeExit(() => { this._save() this._destroy() }) diff --git a/packages/sqlite/src/repository/kv.ts b/packages/sqlite/src/repository/kv.ts index b4d56b14..45632508 100644 --- a/packages/sqlite/src/repository/kv.ts +++ b/packages/sqlite/src/repository/kv.ts @@ -1,10 +1,7 @@ import { Statement } from 'better-sqlite3' import { IKeyValueRepository } from '@mtcute/core' -import { CurrentUserService } from '@mtcute/core/src/highlevel/storage/service/current-user.js' -import { UpdatesStateService } from '@mtcute/core/src/highlevel/storage/service/updates.js' -import { ServiceOptions } from '@mtcute/core/src/storage/service/base.js' -import { DefaultDcsService } from '@mtcute/core/src/storage/service/default-dcs.js' +import { CurrentUserService, DefaultDcsService, ServiceOptions, UpdatesStateService } from '@mtcute/core/utils.js' import { __tlReaderMap } from '@mtcute/tl/binary/reader.js' import { __tlWriterMap } from '@mtcute/tl/binary/writer.js' diff --git a/packages/test/package.json b/packages/test/package.json index a80bd6a5..0fed5ec2 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -18,9 +18,22 @@ }, "peerDependencies": { "@mtcute/core": "workspace:^", + "@mtcute/node": "workspace:^", + "@mtcute/web": "workspace:^", "@mtcute/tl": "workspace:*", "vitest": "^0.34.6" }, + "peerDependenciesMeta": { + "@mtcute/node": { + "optional": true + }, + "@mtcute/web": { + "optional": true + } + }, + "browser": { + "./src/platform.js": "./src/platform.web.js" + }, "distOnlyFields": { "exports": { ".": { diff --git a/packages/test/src/client.test.ts b/packages/test/src/client.test.ts index 3fab4667..4df3d46e 100644 --- a/packages/test/src/client.test.ts +++ b/packages/test/src/client.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { hexEncode } from '@mtcute/core/utils.js' +import { getPlatform } from '@mtcute/core/platform.js' import { StubTelegramClient } from './client.js' import { createStub } from './stub.js' @@ -24,7 +24,7 @@ describe('client stub', () => { const client = new StubTelegramClient() client.onRawMessage((msg) => { - log.push(`message ctor=${hexEncode(msg.subarray(0, 4))}`) + log.push(`message ctor=${getPlatform().hexEncode(msg.subarray(0, 4))}`) client.close().catch(() => {}) }) diff --git a/packages/test/src/client.ts b/packages/test/src/client.ts index 6c2ab281..a41f4c5d 100644 --- a/packages/test/src/client.ts +++ b/packages/test/src/client.ts @@ -7,6 +7,7 @@ import { tl, } from '@mtcute/core' +import { defaultCryptoProvider } from './platform.js' import { StubMemoryTelegramStorage } from './storage.js' import { StubTelegramTransport } from './transport.js' import { InputResponder } from './types.js' @@ -54,6 +55,7 @@ export class StubTelegramClient extends BaseTelegramClient { return transport }, + crypto: defaultCryptoProvider, ...params, }) } diff --git a/packages/test/src/crypto.ts b/packages/test/src/crypto.ts index ff08ca90..7a2a5bb8 100644 --- a/packages/test/src/crypto.ts +++ b/packages/test/src/crypto.ts @@ -1,15 +1,14 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { gzipSync, inflateSync } from 'zlib' +import { getPlatform } from '@mtcute/core/platform.js' import { dataViewFromBuffer, - defaultCryptoProviderFactory, - hexDecodeToBuffer, - hexEncode, ICryptoProvider, - utf8EncodeToBuffer, } from '@mtcute/core/utils.js' +import { defaultCryptoProvider } from './platform.js' + // some random 1024 bytes of entropy const DEFAULT_ENTROPY = ` 29afd26df40fb8ed10b6b4ad6d56ef5df9453f88e6ee6adb6e0544ba635dc6a8a990c9b8b980c343936b33fa7f97bae025102532233abb269c489920ef99021b @@ -31,7 +30,7 @@ fa3de8e50aac96c1275591a1221c32a60a1513370a33a228e00894341b10cf44a6ae6ac250d17a36 `.replace(/\s/g, '') export function withFakeRandom(provider: ICryptoProvider, source = DEFAULT_ENTROPY): ICryptoProvider { - const sourceBytes = hexDecodeToBuffer(source) + const sourceBytes = getPlatform().hexDecode(source) let offset = 0 function getRandomValues(buf: Uint8Array) { @@ -49,12 +48,14 @@ export function withFakeRandom(provider: ICryptoProvider, source = DEFAULT_ENTRO } export function useFakeMathRandom(source = DEFAULT_ENTROPY): void { + const sourceBytes = getPlatform().hexDecode(source) + const dv = dataViewFromBuffer(sourceBytes) + beforeEach(() => { - const sourceBytes = hexDecodeToBuffer(source) let offset = 0 vi.spyOn(globalThis.Math, 'random').mockImplementation(() => { - const ret = dataViewFromBuffer(sourceBytes).getUint32(offset, true) / 0xffffffff + const ret = dv.getUint32(offset, true) / 0xffffffff offset += 4 return ret @@ -66,7 +67,7 @@ export function useFakeMathRandom(source = DEFAULT_ENTROPY): void { } export async function defaultTestCryptoProvider(source = DEFAULT_ENTROPY): Promise { - const prov = withFakeRandom(defaultCryptoProviderFactory(), source) + const prov = withFakeRandom(defaultCryptoProvider, source) await prov.initialize?.() return prov @@ -75,6 +76,8 @@ export async function defaultTestCryptoProvider(source = DEFAULT_ENTROPY): Promi export function testCryptoProvider(c: ICryptoProvider): void { beforeAll(() => c.initialize?.()) + const p = getPlatform() + function gzipSyncWrap(data: Uint8Array) { if (import.meta.env.TEST_ENV === 'browser') { // @ts-expect-error fucking crutch because @jspm/core uses Buffer.isBuffer for some reason @@ -98,88 +101,88 @@ export function testCryptoProvider(c: ICryptoProvider): void { } it('should calculate sha1', () => { - expect(hexEncode(c.sha1(utf8EncodeToBuffer('')))).to.eq('da39a3ee5e6b4b0d3255bfef95601890afd80709') - expect(hexEncode(c.sha1(utf8EncodeToBuffer('hello')))).to.eq('aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') - expect(hexEncode(c.sha1(hexDecodeToBuffer('aebb1f')))).to.eq('62849d15c5dea495916c5eea8dba5f9551288850') + expect(p.hexEncode(c.sha1(p.utf8Encode('')))).to.eq('da39a3ee5e6b4b0d3255bfef95601890afd80709') + expect(p.hexEncode(c.sha1(p.utf8Encode('hello')))).to.eq('aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d') + expect(p.hexEncode(c.sha1(p.hexDecode('aebb1f')))).to.eq('62849d15c5dea495916c5eea8dba5f9551288850') }) it('should calculate sha256', () => { - expect(hexEncode(c.sha256(utf8EncodeToBuffer('')))).to.eq( + expect(p.hexEncode(c.sha256(p.utf8Encode('')))).to.eq( 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', ) - expect(hexEncode(c.sha256(utf8EncodeToBuffer('hello')))).to.eq( + expect(p.hexEncode(c.sha256(p.utf8Encode('hello')))).to.eq( '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824', ) - expect(hexEncode(c.sha256(hexDecodeToBuffer('aebb1f')))).to.eq( + expect(p.hexEncode(c.sha256(p.hexDecode('aebb1f')))).to.eq( '2d29658aba48f2b286fe8bbddb931b7ad297e5adb5b9a6fc3aab67ef7fbf4e80', ) }) it('should calculate hmac-sha256', async () => { - const key = hexDecodeToBuffer('aaeeff') + const key = p.hexDecode('aaeeff') - expect(hexEncode(await c.hmacSha256(utf8EncodeToBuffer(''), key))).to.eq( + expect(p.hexEncode(await c.hmacSha256(p.utf8Encode(''), key))).to.eq( '642711307c9e4437df09d6ebaa6bdc1b3a810c7f15c50fd1d0f8d7d5490f44dd', ) - expect(hexEncode(await c.hmacSha256(utf8EncodeToBuffer('hello'), key))).to.eq( + expect(p.hexEncode(await c.hmacSha256(p.utf8Encode('hello'), key))).to.eq( '39b00bab151f9868e6501655c580b5542954711181243474d46b894703b1c1c2', ) - expect(hexEncode(await c.hmacSha256(hexDecodeToBuffer('aebb1f'), key))).to.eq( + expect(p.hexEncode(await c.hmacSha256(p.hexDecode('aebb1f'), key))).to.eq( 'a3a7273871808711cab17aba14f58e96f63f3ccfc5097d206f0f00ead2c3dd35', ) }) it('should derive pbkdf2 key', async () => { - expect(hexEncode(await c.pbkdf2(utf8EncodeToBuffer('pbkdf2 test'), utf8EncodeToBuffer('some salt'), 10))).to.eq( + expect(p.hexEncode(await c.pbkdf2(p.utf8Encode('pbkdf2 test'), p.utf8Encode('some salt'), 10))).to.eq( 'e43276cfa27f135f261cec8ddcf593fd74ec251038e459c165461f2308f3a7235e0744ee1aed9710b00db28d1a2112e20fea3601c60e770ac57ffe6b33ca8be1', ) }) it('should encrypt and decrypt aes-ctr', () => { let aes = c.createAesCtr( - hexDecodeToBuffer('d450aae0bf0060a4af1044886b42a13f7c506b35255d134a7e87ab3f23a9493b'), - hexDecodeToBuffer('0182de2bd789c295c3c6c875c5e9e190'), + p.hexDecode('d450aae0bf0060a4af1044886b42a13f7c506b35255d134a7e87ab3f23a9493b'), + p.hexDecode('0182de2bd789c295c3c6c875c5e9e190'), true, ) - const data = hexDecodeToBuffer('7baae571e4c2f4cfadb1931d5923aca7') - expect(hexEncode(aes.process(data))).eq('df5647dbb70bc393f2fb05b72f42286f') - expect(hexEncode(aes.process(data))).eq('3917147082672516b3177150129bc579') - expect(hexEncode(aes.process(data))).eq('2a7a9089270a5de45d5e3dd399cac725') - expect(hexEncode(aes.process(data))).eq('56d085217771398ac13583de4d677dd8') - expect(hexEncode(aes.process(data))).eq('cc639b488126cf36e79c4515e8012b92') - expect(hexEncode(aes.process(data))).eq('01384d100646cd562cc5586ec3f8f8c4') + const data = p.hexDecode('7baae571e4c2f4cfadb1931d5923aca7') + expect(p.hexEncode(aes.process(data))).eq('df5647dbb70bc393f2fb05b72f42286f') + expect(p.hexEncode(aes.process(data))).eq('3917147082672516b3177150129bc579') + expect(p.hexEncode(aes.process(data))).eq('2a7a9089270a5de45d5e3dd399cac725') + expect(p.hexEncode(aes.process(data))).eq('56d085217771398ac13583de4d677dd8') + expect(p.hexEncode(aes.process(data))).eq('cc639b488126cf36e79c4515e8012b92') + expect(p.hexEncode(aes.process(data))).eq('01384d100646cd562cc5586ec3f8f8c4') aes.close?.() aes = c.createAesCtr( - hexDecodeToBuffer('d450aae0bf0060a4af1044886b42a13f7c506b35255d134a7e87ab3f23a9493b'), - hexDecodeToBuffer('0182de2bd789c295c3c6c875c5e9e190'), + p.hexDecode('d450aae0bf0060a4af1044886b42a13f7c506b35255d134a7e87ab3f23a9493b'), + p.hexDecode('0182de2bd789c295c3c6c875c5e9e190'), false, ) - expect(hexEncode(aes.process(hexDecodeToBuffer('df5647dbb70bc393f2fb05b72f42286f')))).eq(hexEncode(data)) - expect(hexEncode(aes.process(hexDecodeToBuffer('3917147082672516b3177150129bc579')))).eq(hexEncode(data)) - expect(hexEncode(aes.process(hexDecodeToBuffer('2a7a9089270a5de45d5e3dd399cac725')))).eq(hexEncode(data)) - expect(hexEncode(aes.process(hexDecodeToBuffer('56d085217771398ac13583de4d677dd8')))).eq(hexEncode(data)) - expect(hexEncode(aes.process(hexDecodeToBuffer('cc639b488126cf36e79c4515e8012b92')))).eq(hexEncode(data)) - expect(hexEncode(aes.process(hexDecodeToBuffer('01384d100646cd562cc5586ec3f8f8c4')))).eq(hexEncode(data)) + expect(p.hexEncode(aes.process(p.hexDecode('df5647dbb70bc393f2fb05b72f42286f')))).eq(p.hexEncode(data)) + expect(p.hexEncode(aes.process(p.hexDecode('3917147082672516b3177150129bc579')))).eq(p.hexEncode(data)) + expect(p.hexEncode(aes.process(p.hexDecode('2a7a9089270a5de45d5e3dd399cac725')))).eq(p.hexEncode(data)) + expect(p.hexEncode(aes.process(p.hexDecode('56d085217771398ac13583de4d677dd8')))).eq(p.hexEncode(data)) + expect(p.hexEncode(aes.process(p.hexDecode('cc639b488126cf36e79c4515e8012b92')))).eq(p.hexEncode(data)) + expect(p.hexEncode(aes.process(p.hexDecode('01384d100646cd562cc5586ec3f8f8c4')))).eq(p.hexEncode(data)) aes.close?.() }) it('should encrypt and decrypt aes-ige', () => { const aes = c.createAesIge( - hexDecodeToBuffer('5468697320697320616E20696D706C655468697320697320616E20696D706C65'), - hexDecodeToBuffer('6D656E746174696F6E206F6620494745206D6F646520666F72204F70656E5353'), + p.hexDecode('5468697320697320616E20696D706C655468697320697320616E20696D706C65'), + p.hexDecode('6D656E746174696F6E206F6620494745206D6F646520666F72204F70656E5353'), ) expect( - hexEncode( - aes.encrypt(hexDecodeToBuffer('99706487a1cde613bc6de0b6f24b1c7aa448c8b9c3403e3467a8cad89340f53b')), + p.hexEncode( + aes.encrypt(p.hexDecode('99706487a1cde613bc6de0b6f24b1c7aa448c8b9c3403e3467a8cad89340f53b')), ), ).to.eq('792ea8ae577b1a66cb3bd92679b8030ca54ee631976bd3a04547fdcb4639fa69') expect( - hexEncode( - aes.decrypt(hexDecodeToBuffer('792ea8ae577b1a66cb3bd92679b8030ca54ee631976bd3a04547fdcb4639fa69')), + p.hexEncode( + aes.decrypt(p.hexDecode('792ea8ae577b1a66cb3bd92679b8030ca54ee631976bd3a04547fdcb4639fa69')), ), ).to.eq('99706487a1cde613bc6de0b6f24b1c7aa448c8b9c3403e3467a8cad89340f53b') }) @@ -187,10 +190,10 @@ export function testCryptoProvider(c: ICryptoProvider): void { it( 'should decompose PQ to prime factors P and Q', async () => { - const testFactorization = async (pq: string, p: string, q: string) => { - const [p1, q1] = await c.factorizePQ(hexDecodeToBuffer(pq)) - expect(hexEncode(p1)).eq(p.toLowerCase()) - expect(hexEncode(q1)).eq(q.toLowerCase()) + const testFactorization = async (pq: string, p_: string, q: string) => { + const [p1, q1] = await c.factorizePQ(p.hexDecode(pq)) + expect(p.hexEncode(p1)).eq(p_.toLowerCase()) + expect(p.hexEncode(q1)).eq(q.toLowerCase()) } // from samples at https://core.telegram.org/mtproto/samples-auth_key @@ -212,7 +215,7 @@ export function testCryptoProvider(c: ICryptoProvider): void { const decompressed = inflateSyncWrap(compressed!) expect(compressed!.length).toBeLessThan(data.length) - expect(hexEncode(decompressed)).toEqual(hexEncode(data)) + expect(p.hexEncode(decompressed)).toEqual(p.hexEncode(data)) }) it('should correctly gunzip', () => { @@ -221,7 +224,7 @@ export function testCryptoProvider(c: ICryptoProvider): void { const compressed = gzipSyncWrap(data) const decompressed = c.gunzip(compressed) - expect(hexEncode(decompressed)).toEqual(hexEncode(data)) + expect(p.hexEncode(decompressed)).toEqual(p.hexEncode(data)) }) describe('randomBytes', () => { @@ -246,7 +249,7 @@ export function testCryptoProvider(c: ICryptoProvider): void { } export function u8HexDecode(hex: string) { - const buf = hexDecodeToBuffer(hex) + const buf = getPlatform().hexDecode(hex) // eslint-disable-next-line no-restricted-globals if ((import.meta.env.TEST_ENV === 'node' || import.meta.env.TEST_ENV === 'bun') && Buffer.isBuffer(buf)) { diff --git a/packages/test/src/index.ts b/packages/test/src/index.ts index e563c9ba..3399704a 100644 --- a/packages/test/src/index.ts +++ b/packages/test/src/index.ts @@ -1,5 +1,7 @@ export * from './client.js' export * from './crypto.js' +export * from './platform.js' +export * from './platform.js' export * from './storage.js' export * from './storage/index.js' export * from './stub.js' diff --git a/packages/test/src/platform.test.ts b/packages/test/src/platform.test.ts new file mode 100644 index 00000000..be282cfa --- /dev/null +++ b/packages/test/src/platform.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from 'vitest' + +import { getPlatform } from '@mtcute/core/platform.js' + +const p = getPlatform() + +describe('base64', () => { + it('should decode base64 string to new buffer', () => { + const buf = p.base64Decode('AQIDBA==') + expect(new Uint8Array(buf)).toEqual(new Uint8Array([1, 2, 3, 4])) + }) + + it('should encode buffer to base64 string', () => { + const buf = new Uint8Array([1, 2, 3, 4]) + expect(p.base64Encode(buf)).toEqual('AQIDBA==') + }) + + it('should decode url-safe base64 string to new buffer', () => { + const buf = p.base64Decode('AQIDBA', true) + expect(new Uint8Array(buf)).toEqual(new Uint8Array([1, 2, 3, 4])) + }) + + it('should encode buffer to url-safe base64 string', () => { + const buf = new Uint8Array([1, 2, 3, 4]) + expect(p.base64Encode(buf, true)).toEqual('AQIDBA') + }) +}) + +describe('hex', () => { + it('should decode hex string to new buffer', () => { + const buf = p.hexDecode('01020304') + expect(new Uint8Array(buf)).toEqual(new Uint8Array([1, 2, 3, 4])) + }) + + it('should encode buffer to hex string', () => { + const buf = new Uint8Array([1, 2, 3, 4]) + expect(p.hexEncode(buf)).toEqual('01020304') + }) +}) + +describe('utf8', () => { + it('should encode utf8 string into new buffer', () => { + const buf = p.utf8Encode('abcd') + expect(new Uint8Array(buf)).toEqual(new Uint8Array([97, 98, 99, 100])) + }) + + it('should decode utf8 string from existing buffer', () => { + const buf = new Uint8Array([97, 98, 99, 100]) + expect(p.utf8Decode(buf)).toEqual('abcd') + }) + + it('should return byte length of utf8 string', () => { + expect(p.utf8ByteLength('abcd')).toEqual(4) + }) + + it('should properly handle utf8 string with non-ascii characters', () => { + expect(p.utf8ByteLength('абвг')).toEqual(8) + expect(p.utf8ByteLength('🌸')).toEqual(4) + }) +}) diff --git a/packages/test/src/platform.ts b/packages/test/src/platform.ts new file mode 100644 index 00000000..217a8039 --- /dev/null +++ b/packages/test/src/platform.ts @@ -0,0 +1,4 @@ +import { NodeCryptoProvider, NodePlatform } from '@mtcute/node' + +export const defaultPlatform = new NodePlatform() +export const defaultCryptoProvider = new NodeCryptoProvider() diff --git a/packages/test/src/platform.web.ts b/packages/test/src/platform.web.ts new file mode 100644 index 00000000..eea14331 --- /dev/null +++ b/packages/test/src/platform.web.ts @@ -0,0 +1,4 @@ +import { WebCryptoProvider, WebPlatform } from '@mtcute/web' + +export const defaultPlatform = new WebPlatform() +export const defaultCryptoProvider = new WebCryptoProvider() diff --git a/packages/test/src/transport.test.ts b/packages/test/src/transport.test.ts index 32f2b7b1..37c48f0d 100644 --- a/packages/test/src/transport.test.ts +++ b/packages/test/src/transport.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest' import { BaseTelegramClient, MemoryStorage } from '@mtcute/core' +import { defaultCryptoProvider } from './platform.js' import { createStub } from './stub.js' import { StubTelegramTransport } from './transport.js' @@ -18,6 +19,7 @@ describe('transport stub', () => { media: createStub('dcOption', { ipAddress: '1.2.3.4', port: 5678 }), }, storage: new MemoryStorage(), + crypto: defaultCryptoProvider, transport: () => new StubTelegramTransport({ onConnect: (dc, testMode) => { diff --git a/packages/tl-runtime/src/platform.test-utils.ts b/packages/tl-runtime/src/platform.test-utils.ts deleted file mode 100644 index e98cac08..00000000 --- a/packages/tl-runtime/src/platform.test-utils.ts +++ /dev/null @@ -1,9 +0,0 @@ -// todo: move to platform-specific packages, add them to dev deps and remove this file - -import { ITlPlatform } from './platform.js' - -export const defaultTlPlatform: ITlPlatform = { - utf8Encode: (str: string) => new TextEncoder().encode(str), - utf8Decode: (buf: Uint8Array) => new TextDecoder().decode(buf), - utf8ByteLength: (str: string) => new TextEncoder().encode(str).length, -} diff --git a/packages/tl-runtime/src/platform.ts b/packages/tl-runtime/src/platform.ts index 34b18533..ab14f994 100644 --- a/packages/tl-runtime/src/platform.ts +++ b/packages/tl-runtime/src/platform.ts @@ -5,4 +5,10 @@ export interface ITlPlatform { utf8Encode(str: string): Uint8Array utf8Decode(buf: Uint8Array): string utf8ByteLength(str: string): number + + hexEncode(buf: Uint8Array): string + hexDecode(str: string): Uint8Array + + base64Encode(buf: Uint8Array, url?: boolean): string + base64Decode(str: string, url?: boolean): Uint8Array } diff --git a/packages/tl-runtime/src/reader.test.ts b/packages/tl-runtime/src/reader.test.ts index 6ec2cad0..390955e2 100644 --- a/packages/tl-runtime/src/reader.test.ts +++ b/packages/tl-runtime/src/reader.test.ts @@ -5,7 +5,6 @@ import Long from 'long' import { describe, expect, it } from 'vitest' -import { defaultTlPlatform } from './platform.test-utils.js' import { TlBinaryReader, TlReaderMap } from './reader.js' // todo: replace with platform-specific packages @@ -27,33 +26,32 @@ if (import.meta.env.TEST_ENV === 'node' || import.meta.env.TEST_ENV === 'bun') { describe('TlBinaryReader', () => { it('should read int32', () => { - expect(TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([0, 0, 0, 0])).int()).toEqual(0) - expect(TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([1, 0, 0, 0])).int()).toEqual(1) - expect(TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([1, 2, 3, 4])).int()).toEqual(67305985) - expect(TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([0xff, 0xff, 0xff, 0xff])).int()).toEqual(-1) + expect(TlBinaryReader.manual(new Uint8Array([0, 0, 0, 0])).int()).toEqual(0) + expect(TlBinaryReader.manual(new Uint8Array([1, 0, 0, 0])).int()).toEqual(1) + expect(TlBinaryReader.manual(new Uint8Array([1, 2, 3, 4])).int()).toEqual(67305985) + expect(TlBinaryReader.manual(new Uint8Array([0xff, 0xff, 0xff, 0xff])).int()).toEqual(-1) }) it('should read uint32', () => { - expect(TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([0, 0, 0, 0])).uint()).toEqual(0) - expect(TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([1, 0, 0, 0])).uint()).toEqual(1) - expect(TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([1, 2, 3, 4])).uint()).toEqual(67305985) - expect(TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([0xff, 0xff, 0xff, 0xff])).uint()).toEqual( + expect(TlBinaryReader.manual(new Uint8Array([0, 0, 0, 0])).uint()).toEqual(0) + expect(TlBinaryReader.manual(new Uint8Array([1, 0, 0, 0])).uint()).toEqual(1) + expect(TlBinaryReader.manual(new Uint8Array([1, 2, 3, 4])).uint()).toEqual(67305985) + expect(TlBinaryReader.manual(new Uint8Array([0xff, 0xff, 0xff, 0xff])).uint()).toEqual( 4294967295, ) }) it('should read int53', () => { - expect(TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0])).int53()).toEqual(0) - expect(TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([1, 0, 0, 0, 0, 0, 0, 0])).int53()).toEqual(1) - expect(TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([1, 2, 3, 4, 0, 0, 0, 0])).int53()).toEqual( + expect(TlBinaryReader.manual(new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0])).int53()).toEqual(0) + expect(TlBinaryReader.manual(new Uint8Array([1, 0, 0, 0, 0, 0, 0, 0])).int53()).toEqual(1) + expect(TlBinaryReader.manual(new Uint8Array([1, 2, 3, 4, 0, 0, 0, 0])).int53()).toEqual( 67305985, ) - expect(TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([1, 0, 1, 0, 1, 0, 1, 0])).int53()).toEqual( + expect(TlBinaryReader.manual(new Uint8Array([1, 0, 1, 0, 1, 0, 1, 0])).int53()).toEqual( 281479271743489, ) expect( TlBinaryReader.manual( - defaultTlPlatform, new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), ).int53(), ).toEqual(-1) @@ -61,32 +59,32 @@ describe('TlBinaryReader', () => { it('should read long', () => { expect( - TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])) + TlBinaryReader.manual(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])) .long() .toString(), ).toEqual('-1') expect( - TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78])) + TlBinaryReader.manual(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78])) .long() .toString(), ).toEqual('8671175386481439762') expect( - TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([0x15, 0xc4, 0x15, 0xb5, 0xc4, 0x1c, 0x03, 0xa3])) + TlBinaryReader.manual(new Uint8Array([0x15, 0xc4, 0x15, 0xb5, 0xc4, 0x1c, 0x03, 0xa3])) .long() .toString(), ).toEqual('-6700480189419895787') }) it('should read float', () => { - expect(TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([0, 0, 0x80, 0x3f])).float()).toBeCloseTo( + expect(TlBinaryReader.manual(new Uint8Array([0, 0, 0x80, 0x3f])).float()).toBeCloseTo( 1, 0.001, ) - expect(TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([0xb6, 0xf3, 0x9d, 0x3f])).float()).toBeCloseTo( + expect(TlBinaryReader.manual(new Uint8Array([0xb6, 0xf3, 0x9d, 0x3f])).float()).toBeCloseTo( 1.234, 0.001, ) - expect(TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([0xfa, 0x7e, 0x2a, 0x3f])).float()).toBeCloseTo( + expect(TlBinaryReader.manual(new Uint8Array([0xfa, 0x7e, 0x2a, 0x3f])).float()).toBeCloseTo( 0.666, 0.001, ) @@ -94,27 +92,26 @@ describe('TlBinaryReader', () => { it('should read double', () => { expect( - TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([0, 0, 0, 0, 0, 0, 0xf0, 0x3f])).double(), + TlBinaryReader.manual(new Uint8Array([0, 0, 0, 0, 0, 0, 0xf0, 0x3f])).double(), ).toBeCloseTo(1, 0.001) expect( - TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([0, 0, 0, 0, 0, 0, 0x25, 0x40])).double(), + TlBinaryReader.manual(new Uint8Array([0, 0, 0, 0, 0, 0, 0x25, 0x40])).double(), ).toBeCloseTo(10.5, 0.001) expect( TlBinaryReader.manual( - defaultTlPlatform, new Uint8Array([0x9a, 0x99, 0x99, 0x99, 0x99, 0x99, 0x21, 0x40]), ).double(), ).toBeCloseTo(8.8, 0.001) }) it('should read raw bytes', () => { - expect([...TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([1, 2, 3, 4])).raw(2)]).toEqual([1, 2]) - expect([...TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([1, 2, 3, 4])).raw()]).toEqual([1, 2, 3, 4]) - expect([...TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([1, 2, 3, 4])).raw(0)]).toEqual([]) + expect([...TlBinaryReader.manual(new Uint8Array([1, 2, 3, 4])).raw(2)]).toEqual([1, 2]) + expect([...TlBinaryReader.manual(new Uint8Array([1, 2, 3, 4])).raw()]).toEqual([1, 2, 3, 4]) + expect([...TlBinaryReader.manual(new Uint8Array([1, 2, 3, 4])).raw(0)]).toEqual([]) }) it('should move cursor', () => { - const reader = TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])) + const reader = TlBinaryReader.manual(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])) reader.int() expect(reader.pos).toEqual(4) @@ -145,10 +142,10 @@ describe('TlBinaryReader', () => { }) it('should read tg-encoded bytes', () => { - expect([...TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([1, 2, 3, 4])).bytes()]).toEqual([2]) + expect([...TlBinaryReader.manual(new Uint8Array([1, 2, 3, 4])).bytes()]).toEqual([2]) const random250bytes = randomBytes(250) - let reader = TlBinaryReader.manual(defaultTlPlatform, new Uint8Array([250, ...random250bytes, 0, 0, 0, 0, 0])) + let reader = TlBinaryReader.manual(new Uint8Array([250, ...random250bytes, 0, 0, 0, 0, 0])) expect([...reader.bytes()]).toEqual([...random250bytes]) expect(reader.pos).toEqual(252) @@ -157,7 +154,7 @@ describe('TlBinaryReader', () => { buffer[0] = 254 new DataView(buffer.buffer).setUint32(1, 1000, true) buffer.set(random1000bytes, 4) - reader = TlBinaryReader.manual(defaultTlPlatform, buffer) + reader = TlBinaryReader.manual(buffer) expect([...reader.bytes()]).toEqual([...random1000bytes]) expect(reader.pos).toEqual(1004) }) @@ -199,7 +196,7 @@ describe('TlBinaryReader', () => { 0x00, 0x00, // int32 2 ]) - const reader = new TlBinaryReader(defaultTlPlatform, stubObjectsMap, buffer) + const reader = new TlBinaryReader(stubObjectsMap, buffer) const deadBeef = reader.object() expect(deadBeef).toEqual({ a: 1, b: 42 }) @@ -261,7 +258,7 @@ describe('TlBinaryReader', () => { 0x00, 0x00, // int32 2 ]) - const reader = new TlBinaryReader(defaultTlPlatform, stubObjectsMap, buffer) + const reader = new TlBinaryReader(stubObjectsMap, buffer) const vector = reader.vector() expect(vector).toEqual([{ a: 1, b: 42 }, 2, { vec: [1, 2] }]) @@ -297,7 +294,7 @@ describe('TlBinaryReader', () => { serverPublicKeyFingerprints: [Long.fromString('c3b42b026ce86b21', false, 16)], } - const r = new TlBinaryReader(defaultTlPlatform, map, hexDecodeToBuffer(input)) + const r = new TlBinaryReader(map, hexDecodeToBuffer(input)) expect(r.long().toString()).toEqual('0') // authKeyId expect(r.long().toString(16)).toEqual('51E57AC91E83C801'.toLowerCase()) // messageId expect(r.uint()).toEqual(64) // messageLength diff --git a/packages/tl-runtime/src/reader.ts b/packages/tl-runtime/src/reader.ts index 3bffd3bf..3189a237 100644 --- a/packages/tl-runtime/src/reader.ts +++ b/packages/tl-runtime/src/reader.ts @@ -27,6 +27,8 @@ export type TlReaderMap = Record unknown> & { * Reader for TL objects. */ export class TlBinaryReader { + static platform: ITlPlatform + readonly dataView: DataView readonly uint8View: Uint8Array @@ -38,7 +40,6 @@ export class TlBinaryReader { * @param start Position to start reading from */ constructor( - readonly platform: ITlPlatform, readonly objectsMap: TlReaderMap | undefined, data: ArrayBuffer, start = 0, @@ -60,8 +61,8 @@ export class TlBinaryReader { * @param data Buffer to read from * @param start Position to start reading from */ - static manual(platform: ITlPlatform, data: ArrayBuffer, start = 0): TlBinaryReader { - return new TlBinaryReader(platform, undefined, data, start) + static manual(data: ArrayBuffer, start = 0): TlBinaryReader { + return new TlBinaryReader(undefined, data, start) } /** @@ -71,8 +72,8 @@ export class TlBinaryReader { * @param data Buffer to read from * @param start Position to start reading from */ - static deserializeObject(platform: ITlPlatform, objectsMap: TlReaderMap, data: Uint8Array, start = 0): T { - return new TlBinaryReader(platform, objectsMap, data, start).object() as T + static deserializeObject(objectsMap: TlReaderMap, data: Uint8Array, start = 0): T { + return new TlBinaryReader(objectsMap, data, start).object() as T } int(): number { @@ -174,7 +175,7 @@ export class TlBinaryReader { } string(): string { - return this.platform.utf8Decode(this.bytes()) + return TlBinaryReader.platform.utf8Decode(this.bytes()) } object(id = this.uint()): unknown { diff --git a/packages/tl-runtime/src/writer.test.ts b/packages/tl-runtime/src/writer.test.ts index 1132b3ab..58d3cd79 100644 --- a/packages/tl-runtime/src/writer.test.ts +++ b/packages/tl-runtime/src/writer.test.ts @@ -2,7 +2,6 @@ import Long from 'long' import { describe, expect, it } from 'vitest' -import { defaultTlPlatform } from './platform.test-utils.js' import { TlBinaryWriter, TlSerializationCounter, TlWriterMap } from './writer.js' // todo: replace with platform-specific packages @@ -24,7 +23,7 @@ if (import.meta.env.TEST_ENV === 'node' || import.meta.env.TEST_ENV === 'bun') { describe('TlBinaryWriter', () => { const testSingleMethod = (size: number, fn: (w: TlBinaryWriter) => void, map?: TlWriterMap): string => { - const w = TlBinaryWriter.alloc(defaultTlPlatform, map, size) + const w = TlBinaryWriter.alloc(map, size) fn(w) expect(w.pos).toEqual(size) @@ -128,8 +127,8 @@ describe('TlBinaryWriter', () => { } const length = - TlSerializationCounter.countNeededBytes(defaultTlPlatform, stubObjectsMap, object1) + - TlSerializationCounter.countNeededBytes(defaultTlPlatform, stubObjectsMap, object2) + TlSerializationCounter.countNeededBytes(stubObjectsMap, object1) + + TlSerializationCounter.countNeededBytes(stubObjectsMap, object2) expect(length).toEqual(20) expect( @@ -160,9 +159,9 @@ describe('TlBinaryWriter', () => { } const length = - TlSerializationCounter.countNeededBytes(defaultTlPlatform, stubObjectsMap, object1) + - TlSerializationCounter.countNeededBytes(defaultTlPlatform, stubObjectsMap, object2) + - TlSerializationCounter.countNeededBytes(defaultTlPlatform, stubObjectsMap, object3) + + TlSerializationCounter.countNeededBytes(stubObjectsMap, object1) + + TlSerializationCounter.countNeededBytes(stubObjectsMap, object2) + + TlSerializationCounter.countNeededBytes(stubObjectsMap, object3) + 8 // because technically in tl vector can't be top-level, but whatever :shrug: expect(length).toEqual(48) @@ -205,7 +204,7 @@ describe('TlBinaryWriter', () => { const length = 20 + // mtproto header - TlSerializationCounter.countNeededBytes(defaultTlPlatform, map, resPq) + TlSerializationCounter.countNeededBytes(map, resPq) expect(length).toEqual(expected.length / 2) expect( diff --git a/packages/tl-runtime/src/writer.ts b/packages/tl-runtime/src/writer.ts index b7a03e63..3d96a34e 100644 --- a/packages/tl-runtime/src/writer.ts +++ b/packages/tl-runtime/src/writer.ts @@ -28,7 +28,6 @@ export class TlSerializationCounter { * @param objectMap Writers map */ constructor( - readonly platform: ITlPlatform, readonly objectMap: TlWriterMap, ) {} @@ -38,8 +37,8 @@ export class TlSerializationCounter { * @param objectMap Writers map * @param obj Object to count bytes for */ - static countNeededBytes(platform: ITlPlatform, objectMap: TlWriterMap, obj: { _: string }): number { - const cnt = new TlSerializationCounter(platform, objectMap) + static countNeededBytes(objectMap: TlWriterMap, obj: { _: string }): number { + const cnt = new TlSerializationCounter(objectMap) cnt.object(obj) return cnt.count @@ -118,7 +117,7 @@ export class TlSerializationCounter { } string(val: string): void { - const length = this.platform.utf8ByteLength(val) + const length = TlBinaryWriter.platform.utf8ByteLength(val) this.count += TlSerializationCounter.countBytesOverhead(length) + length } @@ -137,6 +136,8 @@ export class TlSerializationCounter { * Writer for TL objects. */ export class TlBinaryWriter { + static platform: ITlPlatform + readonly dataView: DataView readonly uint8View: Uint8Array @@ -151,7 +152,6 @@ export class TlBinaryWriter { * @param start Position to start writing at */ constructor( - readonly platform: ITlPlatform, readonly objectMap: TlWriterMap | undefined, data: ArrayBuffer, start = 0, @@ -173,8 +173,8 @@ export class TlBinaryWriter { * @param objectMap Writers map * @param size Size of the writer's buffer */ - static alloc(platform: ITlPlatform, objectMap: TlWriterMap | undefined, size: number): TlBinaryWriter { - return new TlBinaryWriter(platform, objectMap, new ArrayBuffer(size)) + static alloc(objectMap: TlWriterMap | undefined, size: number): TlBinaryWriter { + return new TlBinaryWriter(objectMap, new ArrayBuffer(size)) } /** @@ -183,10 +183,10 @@ export class TlBinaryWriter { * @param buffer Buffer to write to, or its size * @param start Position to start writing at */ - static manual(platform: ITlPlatform, buffer: ArrayBuffer | number, start = 0): TlBinaryWriter { + static manual(buffer: ArrayBuffer | number, start = 0): TlBinaryWriter { if (typeof buffer === 'number') buffer = new ArrayBuffer(buffer) - return new TlBinaryWriter(platform, undefined, buffer, start) + return new TlBinaryWriter(undefined, buffer, start) } /** @@ -197,17 +197,16 @@ export class TlBinaryWriter { * @param knownSize In case the size is known, pass it here */ static serializeObject( - platform: ITlPlatform, objectMap: TlWriterMap, obj: { _: string }, knownSize = -1, ): Uint8Array { if (knownSize === -1) { knownSize = - objectMap._staticSize[obj._] || TlSerializationCounter.countNeededBytes(platform, objectMap, obj) + objectMap._staticSize[obj._] || TlSerializationCounter.countNeededBytes(objectMap, obj) } - const writer = TlBinaryWriter.alloc(platform, objectMap, knownSize) + const writer = TlBinaryWriter.alloc(objectMap, knownSize) writer.object(obj) @@ -308,7 +307,7 @@ export class TlBinaryWriter { } string(val: string): void { - this.bytes(this.platform.utf8Encode(val)) + this.bytes(TlBinaryWriter.platform.utf8Encode(val)) } // hot path, avoid additional runtime checks diff --git a/packages/tl/package.json b/packages/tl/package.json index 4b06ced7..2c21ac8e 100644 --- a/packages/tl/package.json +++ b/packages/tl/package.json @@ -21,6 +21,7 @@ }, "devDependencies": { "@mtcute/core": "workspace:^", + "@mtcute/node": "workspace:^", "@mtcute/tl-utils": "workspace:^", "@types/js-yaml": "^4.0.5", "cheerio": "1.0.0-rc.12", diff --git a/packages/tl/scripts/gen-rsa-keys.ts b/packages/tl/scripts/gen-rsa-keys.ts index b800903c..73355b13 100644 --- a/packages/tl/scripts/gen-rsa-keys.ts +++ b/packages/tl/scripts/gen-rsa-keys.ts @@ -3,8 +3,8 @@ import { writeFile } from 'fs/promises' import { join } from 'path' import readline from 'readline' -import { NodeCryptoProvider } from '@mtcute/core/src/utils/crypto/node.js' import { parsePublicKey } from '@mtcute/core/utils.js' +import { NodeCryptoProvider } from '@mtcute/node' import { TlPublicKey } from '../binary/rsa-keys.js' import { __dirname, ESM_PRELUDE } from './constants.js' diff --git a/packages/wasm/package.json b/packages/wasm/package.json index 83493722..b0c72335 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -12,6 +12,10 @@ "build": "pnpm run -w build-package wasm", "build:wasm": "docker build --output=lib --target=binaries lib" }, + "exports": { + ".": "./src/index.ts", + "./mtcute.wasm": "./lib/mtcute.wasm" + }, "distOnlyFields": { "exports": { ".": { diff --git a/packages/wasm/src/index.ts b/packages/wasm/src/index.ts index f7fa32d5..35428542 100644 --- a/packages/wasm/src/index.ts +++ b/packages/wasm/src/index.ts @@ -34,13 +34,15 @@ function getUint8Memory() { export function initSync(module: SyncInitInput): void { if (wasm !== undefined) return - if (!(module instanceof WebAssembly.Module)) { - module = new WebAssembly.Module(module) + if (!(module instanceof WebAssembly.Instance)) { + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module) + } + + module = new WebAssembly.Instance(module) } - const instance = new WebAssembly.Instance(module) - - wasm = instance.exports as unknown as MtcuteWasmModule + wasm = (module as WebAssembly.Instance).exports as unknown as MtcuteWasmModule initCommon() } diff --git a/packages/wasm/src/types.ts b/packages/wasm/src/types.ts index 0170c3e2..74a45657 100644 --- a/packages/wasm/src/types.ts +++ b/packages/wasm/src/types.ts @@ -28,4 +28,4 @@ export interface MtcuteWasmModule { sha1: (data: number, dataLen: number) => void } -export type SyncInitInput = BufferSource | WebAssembly.Module +export type SyncInitInput = BufferSource | WebAssembly.Module | WebAssembly.Instance diff --git a/packages/web/README.md b/packages/web/README.md new file mode 100644 index 00000000..fe07677a --- /dev/null +++ b/packages/web/README.md @@ -0,0 +1,23 @@ +# @mtcute/node + +📖 [API Reference](https://ref.mtcute.dev/modules/_mtcute_node.html) + +All-in-one package for NodeJS. Includes support for native crypto addon +(must be installed separately, `@mtcute/crypto-node`), terminal I/O via +`readline` and includes HTML and Markdown parsers. + +## Usage + +```typescript +import { NodeTelegramClient } from '@mtcute/node' + +const tg = new NodeTelegramClient({ + apiId: 12345, + apiHash: 'abcdef', + storage: 'my-account' +}) + +tg.run(async (user) => { + console.log(`✨ logged in as ${user.displayName}`) +}) +``` diff --git a/packages/web/build.config.cjs b/packages/web/build.config.cjs new file mode 100644 index 00000000..3b96657a --- /dev/null +++ b/packages/web/build.config.cjs @@ -0,0 +1 @@ +module.exports = () => ({ esmOnlyDirectives: true }) diff --git a/packages/web/package.json b/packages/web/package.json new file mode 100644 index 00000000..1bacc723 --- /dev/null +++ b/packages/web/package.json @@ -0,0 +1,36 @@ +{ + "name": "@mtcute/web", + "private": true, + "version": "0.7.0", + "description": "Meta-package for the web platform", + "author": "Alina Sireneva ", + "license": "MIT", + "main": "src/index.ts", + "type": "module", + "scripts": { + "docs": "typedoc", + "build": "pnpm run -w build-package web" + }, + "exports": { + ".": "./src/index.ts" + }, + "distOnlyFields": { + "exports": { + ".": { + "import": "./esm/index.js", + "require": "./cjs/index.js" + }, + "./utils.js": { + "import": "./esm/utils.js", + "require": "./cjs/utils.js" + } + } + }, + "dependencies": { + "@mtcute/core": "workspace:^", + "@mtcute/wasm": "workspace:^" + }, + "devDependencies": { + "@mtcute/test": "workspace:^" + } +} diff --git a/packages/core/src/utils/crypto/web.test.ts b/packages/web/src/crypto.test.ts similarity index 90% rename from packages/core/src/utils/crypto/web.test.ts rename to packages/web/src/crypto.test.ts index 09ce0a3e..c5d65b28 100644 --- a/packages/core/src/utils/crypto/web.test.ts +++ b/packages/web/src/crypto.test.ts @@ -2,7 +2,7 @@ import { describe } from 'vitest' import { testCryptoProvider } from '@mtcute/test' -import { WebCryptoProvider } from './web.js' +import { WebCryptoProvider } from './crypto.js' describe('WebCryptoProvider', async () => { let crypto = globalThis.crypto diff --git a/packages/core/src/utils/crypto/web.ts b/packages/web/src/crypto.ts similarity index 75% rename from packages/core/src/utils/crypto/web.ts rename to packages/web/src/crypto.ts index e3c1964f..b0e0e3a2 100644 --- a/packages/core/src/utils/crypto/web.ts +++ b/packages/web/src/crypto.ts @@ -1,5 +1,7 @@ -import { ICryptoProvider } from './abstract.js' -import { WasmCryptoProvider, WasmCryptoProviderOptions } from './wasm.js' +import { ICryptoProvider, WasmCryptoProvider } from '@mtcute/core/utils.js' +import { initSync } from '@mtcute/wasm' + +import { loadWasmBinary, WasmInitInput } from './wasm.js' const ALGO_TO_SUBTLE: Record = { sha256: 'SHA-256', @@ -7,17 +9,28 @@ const ALGO_TO_SUBTLE: Record = { sha512: 'SHA-512', } +export interface WebCryptoProviderOptions { + crypto?: Crypto + wasmInput?: WasmInitInput +} + export class WebCryptoProvider extends WasmCryptoProvider implements ICryptoProvider { readonly crypto: Crypto + private _wasmInput?: WasmInitInput - constructor(params?: WasmCryptoProviderOptions & { crypto?: Crypto }) { - super(params) + constructor(params?: WebCryptoProviderOptions) { + super() const crypto = params?.crypto ?? globalThis.crypto if (!crypto || !crypto.subtle) { throw new Error('WebCrypto is not available') } this.crypto = crypto + this._wasmInput = params?.wasmInput + } + + async initialize(): Promise { + initSync(await loadWasmBinary(this._wasmInput)) } async pbkdf2( diff --git a/packages/web/src/encodings/base64.ts b/packages/web/src/encodings/base64.ts new file mode 100644 index 00000000..d9a293d9 --- /dev/null +++ b/packages/web/src/encodings/base64.ts @@ -0,0 +1,132 @@ +/// Based on https://github.com/beatgammit/base64-js, MIT license +const lookup: string[] = [] +const revLookup: number[] = [] + +const code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' + +for (let i = 0, len = code.length; i < len; ++i) { + lookup[i] = code[i] + revLookup[code.charCodeAt(i)] = i +} + +function getLens(b64: string): [number, number] { + const len = b64.length + + if (len % 4 > 0) { + throw new Error('Invalid string. Length must be a multiple of 4') + } + + // Trim off extra bytes after placeholder bytes are found + // See: https://github.com/beatgammit/base64-js/issues/42 + let validLen = b64.indexOf('=') + if (validLen === -1) validLen = len + + const placeHoldersLen = validLen === len ? 0 : 4 - (validLen % 4) + + return [validLen, placeHoldersLen] +} + +function _byteLength(b64: string, validLen: number, placeHoldersLen: number) { + return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen +} + +function toByteArray(b64: string, arr: Uint8Array) { + let tmp + const lens = getLens(b64) + const validLen = lens[0] + const placeHoldersLen = lens[1] + + let curByte = 0 + + // if there are placeholders, only get up to the last complete 4 chars + const len = placeHoldersLen > 0 ? validLen - 4 : validLen + + let i + + for (i = 0; i < len; i += 4) { + tmp = + (revLookup[b64.charCodeAt(i)] << 18) | + (revLookup[b64.charCodeAt(i + 1)] << 12) | + (revLookup[b64.charCodeAt(i + 2)] << 6) | + revLookup[b64.charCodeAt(i + 3)] + arr[curByte++] = (tmp >> 16) & 0xff + arr[curByte++] = (tmp >> 8) & 0xff + arr[curByte++] = tmp & 0xff + } + + if (placeHoldersLen === 2) { + tmp = (revLookup[b64.charCodeAt(i)] << 2) | (revLookup[b64.charCodeAt(i + 1)] >> 4) + arr[curByte++] = tmp & 0xff + } + + if (placeHoldersLen === 1) { + tmp = + (revLookup[b64.charCodeAt(i)] << 10) | + (revLookup[b64.charCodeAt(i + 1)] << 4) | + (revLookup[b64.charCodeAt(i + 2)] >> 2) + arr[curByte++] = (tmp >> 8) & 0xff + arr[curByte++] = tmp & 0xff + } + + return arr +} + +function tripletToBase64(num: number) { + return lookup[(num >> 18) & 0x3f] + lookup[(num >> 12) & 0x3f] + lookup[(num >> 6) & 0x3f] + lookup[num & 0x3f] +} + +function encodeChunk(uint8: Uint8Array, start: number, end: number) { + let tmp + const output = [] + + for (let i = start; i < end; i += 3) { + tmp = ((uint8[i] << 16) & 0xff0000) + ((uint8[i + 1] << 8) & 0xff00) + (uint8[i + 2] & 0xff) + output.push(tripletToBase64(tmp)) + } + + return output.join('') +} + +function fromByteArray(uint8: Uint8Array) { + let tmp + const len = uint8.length + const extraBytes = len % 3 // if we have 1 byte left, pad 2 bytes + const parts = [] + const maxChunkLength = 16383 // must be multiple of 3 + + // go through the array every three bytes, we'll deal with trailing stuff later + for (let i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) { + parts.push(encodeChunk(uint8, i, i + maxChunkLength > len2 ? len2 : i + maxChunkLength)) + } + + // pad the end with zeros, but make sure to not forget the extra bytes + if (extraBytes === 1) { + tmp = uint8[len - 1] + parts.push(lookup[tmp >> 2] + lookup[(tmp << 4) & 0x3f] + '==') + } else if (extraBytes === 2) { + tmp = (uint8[len - 2] << 8) + uint8[len - 1] + parts.push(lookup[tmp >> 10] + lookup[(tmp >> 4) & 0x3f] + lookup[(tmp << 2) & 0x3f] + '=') + } + + return parts.join('') +} + +export function base64Encode(buf: Uint8Array, url = false): string { + const str = fromByteArray(buf) + if (url) return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') + + return str +} + +export function base64Decode(string: string, url = false): Uint8Array { + if (url) { + string = string.replace(/-/g, '+').replace(/_/g, '/') + while (string.length % 4) string += '=' + } + + const buf = new Uint8Array(_byteLength(string, ...getLens(string))) + + toByteArray(string, buf) + + return buf +} diff --git a/packages/web/src/encodings/hex.ts b/packages/web/src/encodings/hex.ts new file mode 100644 index 00000000..a80886a3 --- /dev/null +++ b/packages/web/src/encodings/hex.ts @@ -0,0 +1,78 @@ +/// Based on https://github.com/feross/buffer, MIT license + +const hexSliceLookupTable = (function () { + const alphabet = '0123456789abcdef' + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const table: string[] = new Array(256) + + for (let i = 0; i < 16; ++i) { + const i16 = i * 16 + + for (let j = 0; j < 16; ++j) { + table[i16 + j] = alphabet[i] + alphabet[j] + } + } + + return table +})() + +const hexCharValueTable: Record = { + '0': 0, + '1': 1, + '2': 2, + '3': 3, + '4': 4, + '5': 5, + '6': 6, + '7': 7, + '8': 8, + '9': 9, + a: 10, + b: 11, + c: 12, + d: 13, + e: 14, + f: 15, + A: 10, + B: 11, + C: 12, + D: 13, + E: 14, + F: 15, +} + +export function hexEncode(buf: Uint8Array): string { + let out = '' + + for (let i = 0; i < buf.byteLength; ++i) { + out += hexSliceLookupTable[buf[i]] + } + + return out +} + +function hexDecodeInner(buf: Uint8Array, string: string): void { + const strLen = string.length + const length = Math.min(buf.length, strLen / 2) + + let i + + for (i = 0; i < length; ++i) { + const a = hexCharValueTable[string[i * 2]] + const b = hexCharValueTable[string[i * 2 + 1]] + + if (a === undefined || b === undefined) { + return + } + buf[i] = (a << 4) | b + } + + return +} + +export function hexDecode(string: string): Uint8Array { + const buf = new Uint8Array(Math.ceil(string.length / 2)) + hexDecodeInner(buf, string) + + return buf +} diff --git a/packages/web/src/encodings/utf8.ts b/packages/web/src/encodings/utf8.ts new file mode 100644 index 00000000..a4cb1168 --- /dev/null +++ b/packages/web/src/encodings/utf8.ts @@ -0,0 +1,24 @@ +const sharedEncoder = new TextEncoder() +const sharedDecoder = new TextDecoder('utf8') + +export function utf8ByteLength(str: string) { + // https://stackoverflow.com/a/23329386 + let s = str.length + + for (let i = str.length - 1; i >= 0; i--) { + const code = str.charCodeAt(i) + if (code > 0x7f && code <= 0x7ff) s++ + else if (code > 0x7ff && code <= 0xffff) s += 2 + if (code >= 0xDC00 && code <= 0xDFFF) i-- //trail surrogate + } + + return s +} + +export function utf8Decode(buf: Uint8Array): string { + return sharedDecoder.decode(buf) +} + +export function utf8Encode(str: string): Uint8Array { + return sharedEncoder.encode(str) +} diff --git a/packages/core/src/utils/platform/exit-hook.web.ts b/packages/web/src/exit-hook.ts similarity index 100% rename from packages/core/src/utils/platform/exit-hook.web.ts rename to packages/web/src/exit-hook.ts diff --git a/packages/core/src/storage/providers/idb/driver.ts b/packages/web/src/idb/driver.ts similarity index 97% rename from packages/core/src/storage/providers/idb/driver.ts rename to packages/web/src/idb/driver.ts index 90b706fb..a35d7a1f 100644 --- a/packages/core/src/storage/providers/idb/driver.ts +++ b/packages/web/src/idb/driver.ts @@ -1,5 +1,5 @@ -import { MtUnsupportedError } from '../../../types/errors.js' -import { BaseStorageDriver } from '../../driver.js' +import { BaseStorageDriver, MtUnsupportedError } from '@mtcute/core' + import { txToPromise } from './utils.js' export type PostMigrationFunction = (db: IDBDatabase) => Promise diff --git a/packages/core/src/storage/providers/idb/idb.test.ts b/packages/web/src/idb/idb.test.ts similarity index 100% rename from packages/core/src/storage/providers/idb/idb.test.ts rename to packages/web/src/idb/idb.test.ts diff --git a/packages/core/src/storage/providers/idb/index.ts b/packages/web/src/idb/index.ts similarity index 94% rename from packages/core/src/storage/providers/idb/index.ts rename to packages/web/src/idb/index.ts index a0d36e8e..3d76b78d 100644 --- a/packages/core/src/storage/providers/idb/index.ts +++ b/packages/web/src/idb/index.ts @@ -1,4 +1,5 @@ -import { IMtStorageProvider } from '../../provider.js' +import { IMtStorageProvider } from '@mtcute/core' + import { IdbStorageDriver } from './driver.js' import { IdbAuthKeysRepository } from './repository/auth-keys.js' import { IdbKvRepository } from './repository/kv.js' diff --git a/packages/core/src/storage/providers/idb/repository/auth-keys.ts b/packages/web/src/idb/repository/auth-keys.ts similarity index 97% rename from packages/core/src/storage/providers/idb/repository/auth-keys.ts rename to packages/web/src/idb/repository/auth-keys.ts index a2fb7474..842986be 100644 --- a/packages/core/src/storage/providers/idb/repository/auth-keys.ts +++ b/packages/web/src/idb/repository/auth-keys.ts @@ -1,4 +1,5 @@ -import { IAuthKeysRepository } from '../../../repository/auth-keys.js' +import { IAuthKeysRepository } from '@mtcute/core' + import { IdbStorageDriver } from '../driver.js' import { reqToPromise, txToPromise } from '../utils.js' diff --git a/packages/core/src/storage/providers/idb/repository/kv.ts b/packages/web/src/idb/repository/kv.ts similarity index 94% rename from packages/core/src/storage/providers/idb/repository/kv.ts rename to packages/web/src/idb/repository/kv.ts index 94e58b5f..64f4a79c 100644 --- a/packages/core/src/storage/providers/idb/repository/kv.ts +++ b/packages/web/src/idb/repository/kv.ts @@ -1,4 +1,5 @@ -import { IKeyValueRepository } from '../../../repository/key-value.js' +import { IKeyValueRepository } from '@mtcute/core' + import { IdbStorageDriver } from '../driver.js' import { reqToPromise } from '../utils.js' diff --git a/packages/core/src/storage/providers/idb/repository/peers.ts b/packages/web/src/idb/repository/peers.ts similarity index 94% rename from packages/core/src/storage/providers/idb/repository/peers.ts rename to packages/web/src/idb/repository/peers.ts index 010a27d1..fff96ba6 100644 --- a/packages/core/src/storage/providers/idb/repository/peers.ts +++ b/packages/web/src/idb/repository/peers.ts @@ -1,4 +1,5 @@ -import { IPeersRepository } from '../../../../highlevel/storage/repository/peers.js' +import { IPeersRepository } from '@mtcute/core' + import { IdbStorageDriver } from '../driver.js' import { reqToPromise } from '../utils.js' diff --git a/packages/core/src/storage/providers/idb/repository/ref-messages.ts b/packages/web/src/idb/repository/ref-messages.ts similarity index 95% rename from packages/core/src/storage/providers/idb/repository/ref-messages.ts rename to packages/web/src/idb/repository/ref-messages.ts index b7c18a17..8de88620 100644 --- a/packages/core/src/storage/providers/idb/repository/ref-messages.ts +++ b/packages/web/src/idb/repository/ref-messages.ts @@ -1,4 +1,5 @@ -import { IReferenceMessagesRepository } from '../../../../highlevel/storage/repository/ref-messages.js' +import { IReferenceMessagesRepository } from '@mtcute/core' + import { IdbStorageDriver } from '../driver.js' import { cursorToIterator, reqToPromise, txToPromise } from '../utils.js' diff --git a/packages/core/src/storage/providers/idb/utils.ts b/packages/web/src/idb/utils.ts similarity index 100% rename from packages/core/src/storage/providers/idb/utils.ts rename to packages/web/src/idb/utils.ts diff --git a/packages/web/src/index.ts b/packages/web/src/index.ts new file mode 100644 index 00000000..31f603e6 --- /dev/null +++ b/packages/web/src/index.ts @@ -0,0 +1,2 @@ +export * from './crypto.js' +export * from './platform.js' diff --git a/packages/core/src/utils/platform/logging.web.ts b/packages/web/src/logging.ts similarity index 95% rename from packages/core/src/utils/platform/logging.web.ts rename to packages/web/src/logging.ts index 4a28557b..83d4e0af 100644 --- a/packages/core/src/utils/platform/logging.web.ts +++ b/packages/web/src/logging.ts @@ -25,7 +25,7 @@ const TAG_COLORS = [ ] /** @internal */ -export const _defaultLoggingHandler = ( +export const defaultLoggingHandler = ( color: number, level: number, tag: string, diff --git a/packages/web/src/platform.ts b/packages/web/src/platform.ts new file mode 100644 index 00000000..e230866e --- /dev/null +++ b/packages/web/src/platform.ts @@ -0,0 +1,51 @@ +import { ICorePlatform } from '@mtcute/core/platform.js' + +import { base64Decode, base64Encode } from './encodings/base64.js' +import { hexDecode, hexEncode } from './encodings/hex.js' +import { utf8ByteLength, utf8Decode, utf8Encode } from './encodings/utf8.js' +import { beforeExit } from './exit-hook.js' +import { defaultLoggingHandler } from './logging.js' + +export class WebPlatform implements ICorePlatform { + // ICorePlatform + log!: typeof defaultLoggingHandler + beforeExit!: typeof beforeExit + + getDeviceModel(): string { + if (typeof navigator === 'undefined') return 'Browser' + + return navigator.userAgent + } + + getDefaultLogLevel(): number | null { + if (typeof localStorage !== 'undefined') { + const localLogLevel = parseInt(localStorage.MTCUTE_LOG_LEVEL as string) + + if (!isNaN(localLogLevel)) { + return localLogLevel + } + } + + return null + } + + // ITlPlatform + utf8ByteLength!: typeof utf8ByteLength + utf8Encode!: typeof utf8Encode + utf8Decode!: typeof utf8Decode + hexEncode!: typeof hexEncode + hexDecode!: typeof hexDecode + + base64Encode!: typeof base64Encode + base64Decode!: typeof base64Decode +} + +WebPlatform.prototype.log = defaultLoggingHandler +WebPlatform.prototype.beforeExit = beforeExit +WebPlatform.prototype.utf8ByteLength = utf8ByteLength +WebPlatform.prototype.utf8Encode = utf8Encode +WebPlatform.prototype.utf8Decode = utf8Decode +WebPlatform.prototype.hexEncode = hexEncode +WebPlatform.prototype.hexDecode = hexDecode +WebPlatform.prototype.base64Encode = base64Encode +WebPlatform.prototype.base64Decode = base64Decode diff --git a/packages/web/src/wasm.ts b/packages/web/src/wasm.ts new file mode 100644 index 00000000..9b75f543 --- /dev/null +++ b/packages/web/src/wasm.ts @@ -0,0 +1,42 @@ +export type WasmInitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module + +export async function loadWasmBinary(input?: WasmInitInput): Promise { + if (typeof input === 'undefined') { + input = new URL('@mtcute/wasm/mtcute.wasm', import.meta.url) + } + + if ( + typeof input === 'string' || + (typeof Request === 'function' && input instanceof Request) || + (typeof URL === 'function' && input instanceof URL) + ) { + input = await fetch(input) + } + + if (typeof Response === 'function' && input instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + const { instance } = await WebAssembly.instantiateStreaming(input) + + return instance + } catch (e) { + if (input.headers.get('Content-Type') !== 'application/wasm') { + console.warn( + '`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n', + e, + ) + } else { + throw e + } + } + } + + const bytes = await input.arrayBuffer() + + const { instance } = await WebAssembly.instantiate(bytes) + + return instance + } + + return await WebAssembly.instantiate(input) +} diff --git a/packages/web/tsconfig.json b/packages/web/tsconfig.json new file mode 100644 index 00000000..919db0b1 --- /dev/null +++ b/packages/web/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist/esm", + "rootDir": "./src" + }, + "include": [ + "./src", + ], + "references": [ + { "path": "../core" }, + ] +} diff --git a/packages/web/typedoc.cjs b/packages/web/typedoc.cjs new file mode 100644 index 00000000..cc62716c --- /dev/null +++ b/packages/web/typedoc.cjs @@ -0,0 +1,10 @@ +module.exports = { + extends: ['../../.config/typedoc/config.base.cjs'], + entryPoints: ['./index.ts'], + externalPattern: [ + '../core/**', + '../html-parser/**', + '../markdown-parser/**', + '../sqlite/**', + ], +} diff --git a/packages/web/utils.ts b/packages/web/utils.ts new file mode 100644 index 00000000..3356b98c --- /dev/null +++ b/packages/web/utils.ts @@ -0,0 +1 @@ +export * from '@mtcute/core/utils.js' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc2e86b6..50e5c5f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -182,9 +182,9 @@ importers: packages/crypto-node: dependencies: - '@mtcute/core': + '@mtcute/node': specifier: workspace:^ - version: link:../core + version: link:../node devDependencies: '@mtcute/test': specifier: workspace:^ @@ -214,6 +214,10 @@ importers: long: specifier: 5.2.3 version: 5.2.3 + devDependencies: + '@mtcute/test': + specifier: workspace:^ + version: link:../test packages/html-parser: dependencies: @@ -230,9 +234,9 @@ importers: packages/http-proxy: dependencies: - '@mtcute/core': + '@mtcute/node': specifier: workspace:^ - version: link:../core + version: link:../node packages/i18n: devDependencies: @@ -255,9 +259,9 @@ importers: packages/mtproxy: dependencies: - '@mtcute/core': + '@mtcute/node': specifier: workspace:^ - version: link:../core + version: link:../node packages/node: dependencies: @@ -273,12 +277,19 @@ importers: '@mtcute/sqlite': specifier: workspace:^ version: link:../sqlite + '@mtcute/wasm': + specifier: workspace:^ + version: link:../wasm + devDependencies: + '@mtcute/test': + specifier: workspace:^ + version: link:../test packages/socks-proxy: dependencies: - '@mtcute/core': + '@mtcute/node': specifier: workspace:^ - version: link:../core + version: link:../node ip6: specifier: 0.2.7 version: 0.2.7 @@ -310,9 +321,15 @@ importers: '@mtcute/core': specifier: workspace:^ version: link:../core + '@mtcute/node': + specifier: workspace:^ + version: link:../node '@mtcute/tl': specifier: workspace:* version: link:../tl + '@mtcute/web': + specifier: workspace:^ + version: link:../web long: specifier: 5.2.3 version: 5.2.3 @@ -333,6 +350,9 @@ importers: '@mtcute/core': specifier: workspace:^ version: link:../core + '@mtcute/node': + specifier: workspace:^ + version: link:../node '@mtcute/tl-utils': specifier: workspace:^ version: link:../tl-utils @@ -374,6 +394,19 @@ importers: specifier: workspace:^ version: link:../tl-runtime + packages/web: + dependencies: + '@mtcute/core': + specifier: workspace:^ + version: link:../core + '@mtcute/wasm': + specifier: workspace:^ + version: link:../wasm + devDependencies: + '@mtcute/test': + specifier: workspace:^ + version: link:../test + packages: /@aashutoshrathi/word-wrap@1.2.6: