Platform agnostic #19

Merged
teidesu merged 25 commits from platform-agnostic into master 2024-03-07 05:54:20 +03:00
251 changed files with 2447 additions and 1681 deletions

View file

@ -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: { rules: {
'no-console': 'off', 'no-console': 'off',
'no-restricted-imports': [ 'no-restricted-imports': [
@ -273,7 +273,7 @@ module.exports = {
}, },
}, },
{ {
files: ['e2e/**'], files: ['e2e/**', 'packages/node/**'],
rules: { rules: {
'no-restricted-globals': 'off', 'no-restricted-globals': 'off',
}, },

View file

@ -1,7 +1,7 @@
{ {
"extends": "../tsconfig.json", "extends": "../tsconfig.json",
"exclude": [ "exclude": [
"**/*.test.ts", "../**/*.test.ts",
"**/*.test-utils.ts" "../**/*.test-utils.ts"
] ]
} }

View file

@ -11,6 +11,9 @@ export default defineConfig({
'packages/**/*.test-d.ts', 'packages/**/*.test-d.ts',
], ],
}, },
setupFiles: [
'./.config/vitest.setup.mts'
]
}, },
define: { define: {
'import.meta.env.TEST_ENV': '"node"' 'import.meta.env.TEST_ENV': '"node"'

9
.config/vitest.setup.mts Normal file
View file

@ -0,0 +1,9 @@
import { setPlatform } from '../packages/core/src/platform.js'
// @ts-expect-error no .env here
const TEST_ENV = import.meta.env.TEST_ENV
if (TEST_ENV === 'browser') {
setPlatform(new (await import('../packages/web/src/platform.js')).WebPlatform())
} else {
setPlatform(new (await import('../packages/node/src/platform.js')).NodePlatform())
}

View file

@ -27,10 +27,10 @@
> releases may not follow semver just yet, so please pin the versions for now > releases may not follow semver just yet, so please pin the versions for now
```ts ```ts
import { NodeTelegramClient } from '@mtcute/node' import { TelegramClient } from '@mtcute/node'
import { Dispatcher, filters } from '@mtcute/dispatcher' import { Dispatcher, filters } from '@mtcute/dispatcher'
const tg = new NodeTelegramClient({ const tg = new TelegramClient({
apiId: parseInt(process.env.API_ID), apiId: parseInt(process.env.API_ID),
apiHash: process.env.API_HASH, apiHash: process.env.API_HASH,
storage: 'my-account' storage: 'my-account'
@ -64,9 +64,9 @@ pnpm add @mtcute/node
pnpm add @mtcute/crypto-node pnpm add @mtcute/crypto-node
``` ```
✨ building something for web? use the client directly: ✨ building something for web? use the web package:
```bash ```bash
pnpm add @mtcute/client pnpm add @mtcute/web
``` ```
learn more: [guide](https://mtcute.dev/guide/) learn more: [guide](https://mtcute.dev/guide/)

View file

@ -1,4 +1,4 @@
const { BaseTelegramClient } = require('@mtcute/core') const { BaseTelegramClient } = require('@mtcute/core/client.js')
const { describe, it } = require('mocha') const { describe, it } = require('mocha')
const { expect } = require('chai') const { expect } = require('chai')

View file

@ -1,35 +1,24 @@
const { const { TlBinaryReader, TlBinaryWriter, TlSerializationCounter } = require('@mtcute/tl-runtime')
TlBinaryReader,
TlBinaryWriter,
TlSerializationCounter,
hexEncode,
hexDecode,
hexDecodeToBuffer,
} = require('@mtcute/tl-runtime')
const Long = require('long') const Long = require('long')
const { describe, it } = require('mocha') const { describe, it } = require('mocha')
const { expect } = require('chai') const { expect } = require('chai')
const { NodePlatform } = require('@mtcute/node')
// here we primarily want to check that everything imports properly, // here we primarily want to check that everything imports properly,
// and that the code is actually executable. The actual correctness // and that the code is actually executable. The actual correctness
// of the implementation is covered tested by unit tests // of the implementation is covered tested by unit tests
const p = new NodePlatform()
describe('@mtcute/tl-runtime', () => { describe('@mtcute/tl-runtime', () => {
describe('encodings', () => { describe('encodings', () => {
it('works with Buffers', () => { it('works with Buffers', () => {
const buf = Buffer.alloc(5) expect(p.hexEncode(Buffer.from('hello'))).to.equal('68656c6c6f')
hexDecode(buf, '0102030405') expect(p.hexDecode('0102030405')).eql(Buffer.from([1, 2, 3, 4, 5]))
expect(hexEncode(Buffer.from('hello'))).to.equal('68656c6c6f')
expect(buf).eql(Buffer.from([1, 2, 3, 4, 5]))
}) })
it('works with Uint8Arrays', () => { it('works with Uint8Arrays', () => {
const buf = new Uint8Array(5) expect(p.hexEncode(new Uint8Array([1, 2, 3, 4, 5]))).to.equal('0102030405')
hexDecode(buf, '0102030405')
expect(hexEncode(new Uint8Array([1, 2, 3, 4, 5]))).to.equal('0102030405')
expect(buf).eql(new Uint8Array([1, 2, 3, 4, 5]))
}) })
}) })
@ -61,7 +50,7 @@ describe('@mtcute/tl-runtime', () => {
}) })
it('should work with Uint8Arrays', () => { it('should work with Uint8Arrays', () => {
const buf = hexDecodeToBuffer(data) const buf = p.hexDecode(data)
const r = new TlBinaryReader(map, buf, 8) const r = new TlBinaryReader(map, buf, 8)
@ -96,7 +85,7 @@ describe('@mtcute/tl-runtime', () => {
w.long(Long.fromString('51E57AC91E83C801', true, 16)) // messageId w.long(Long.fromString('51E57AC91E83C801', true, 16)) // messageId
w.object(obj) w.object(obj)
expect(hexEncode(w.result())).eq( expect(p.hexEncode(w.result())).eq(
'000000000000000001c8831ec97ae551632416050817ed48941a08f98100000015c4b51c01000000216be86c022bb4c3', '000000000000000001c8831ec97ae551632416050817ed48941a08f98100000015c4b51c01000000216be86c022bb4c3',
) )
}) })
@ -104,7 +93,7 @@ describe('@mtcute/tl-runtime', () => {
it('should work with Uint8Arrays', () => { it('should work with Uint8Arrays', () => {
const obj = { const obj = {
_: 'mt_resPQ', _: 'mt_resPQ',
pq: hexDecodeToBuffer('17ED48941A08F981'), pq: p.hexDecode('17ED48941A08F981'),
serverPublicKeyFingerprints: [Long.fromString('c3b42b026ce86b21', 16)], serverPublicKeyFingerprints: [Long.fromString('c3b42b026ce86b21', 16)],
} }
@ -115,7 +104,7 @@ describe('@mtcute/tl-runtime', () => {
w.long(Long.fromString('51E57AC91E83C801', true, 16)) // messageId w.long(Long.fromString('51E57AC91E83C801', true, 16)) // messageId
w.object(obj) w.object(obj)
expect(hexEncode(w.result())).eq( expect(p.hexEncode(w.result())).eq(
'000000000000000001c8831ec97ae551632416050817ed48941a08f98100000015c4b51c01000000216be86c022bb4c3', '000000000000000001c8831ec97ae551632416050817ed48941a08f98100000015c4b51c01000000216be86c022bb4c3',
) )
}) })

View file

@ -1,18 +1,17 @@
const Long = require('long') const Long = require('long')
const { const { TlBinaryReader, TlBinaryWriter } = require('@mtcute/tl-runtime')
TlBinaryReader,
TlBinaryWriter,
hexEncode,
} = require('@mtcute/tl-runtime')
const { tl } = require('@mtcute/tl') const { tl } = require('@mtcute/tl')
const { __tlReaderMap } = require('@mtcute/tl/binary/reader') const { __tlReaderMap } = require('@mtcute/tl/binary/reader')
const { __tlWriterMap } = require('@mtcute/tl/binary/writer') const { __tlWriterMap } = require('@mtcute/tl/binary/writer')
const { describe, it } = require('mocha') const { describe, it } = require('mocha')
const { expect } = require('chai') const { expect } = require('chai')
const { NodePlatform } = require('@mtcute/node')
// here we primarily want to check that @mtcute/tl correctly works with @mtcute/tl-runtime // here we primarily want to check that @mtcute/tl correctly works with @mtcute/tl-runtime
const p = new NodePlatform()
describe('@mtcute/tl', () => { describe('@mtcute/tl', () => {
it('writers map works with TlBinaryWriter', () => { it('writers map works with TlBinaryWriter', () => {
const obj = { const obj = {
@ -21,7 +20,9 @@ describe('@mtcute/tl', () => {
accessHash: Long.fromNumber(456), accessHash: Long.fromNumber(456),
} }
expect(hexEncode(TlBinaryWriter.serializeObject(__tlWriterMap, obj))).to.equal('4ca5e8dd7b00000000000000c801000000000000') expect(p.hexEncode(TlBinaryWriter.serializeObject(__tlWriterMap, obj))).to.equal(
'4ca5e8dd7b00000000000000c801000000000000',
)
}) })
it('readers map works with TlBinaryReader', () => { it('readers map works with TlBinaryReader', () => {

View file

@ -1,8 +1,11 @@
const wasm = require('@mtcute/wasm') const wasm = require('@mtcute/wasm')
const { describe, it, before } = require('mocha') const { describe, it, before } = require('mocha')
const { expect } = require('chai') const { expect } = require('chai')
const { NodeCryptoProvider } = require('@mtcute/node')
before(() => wasm.initAsync()) before(async () => {
await new NodeCryptoProvider().initialize()
})
describe('@mtcute/wasm', () => { describe('@mtcute/wasm', () => {
const key = Buffer.from('5468697320697320616E20696D706C655468697320697320616E20696D706C65', 'hex') const key = Buffer.from('5468697320697320616E20696D706C655468697320697320616E20696D706C65', 'hex')

View file

@ -1,16 +1,22 @@
const { MemoryStorage } = require('@mtcute/core') const { MemoryStorage } = require('@mtcute/core')
const { setPlatform } = require('@mtcute/core/platform.js')
const { LogManager } = require('@mtcute/core/utils.js') const { LogManager } = require('@mtcute/core/utils.js')
const { NodeCryptoProvider, NodePlatform, TcpTransport } = require('@mtcute/node')
exports.getApiParams = () => { exports.getApiParams = () => {
if (!process.env.API_ID || !process.env.API_HASH) { if (!process.env.API_ID || !process.env.API_HASH) {
throw new Error('API_ID and API_HASH env variables must be set') throw new Error('API_ID and API_HASH env variables must be set')
} }
setPlatform(new NodePlatform())
return { return {
apiId: parseInt(process.env.API_ID), apiId: parseInt(process.env.API_ID),
apiHash: process.env.API_HASH, apiHash: process.env.API_HASH,
testMode: true, testMode: true,
storage: new MemoryStorage(), storage: new MemoryStorage(),
logLevel: LogManager.DEBUG, logLevel: LogManager.DEBUG,
transport: () => new TcpTransport(),
crypto: new NodeCryptoProvider(),
} }
} }

View file

@ -11,6 +11,8 @@ module.exports = {
getFiles: () => 'tests/**/*.ts', getFiles: () => 'tests/**/*.ts',
beforeAll: () => ['tsc', 'node build-esm.cjs'], beforeAll: () => ['tsc', 'node build-esm.cjs'],
runFile: (file) => { runFile: (file) => {
if (require('path').basename(file)[0] === '_') return null
if (file.startsWith('tests/packaging/')) { if (file.startsWith('tests/packaging/')) {
// packaging tests - we need to make sure everything imports and works // packaging tests - we need to make sure everything imports and works
return [ return [

View file

@ -1,7 +1,7 @@
import { expect } from 'chai' import { expect } from 'chai'
import { describe, it } from 'mocha' import { describe, it } from 'mocha'
import { BaseTelegramClient } from '@mtcute/core' import { BaseTelegramClient } from '@mtcute/core/client.js'
import { getApiParams } from '../utils.js' import { getApiParams } from '../utils.js'

View file

@ -2,34 +2,23 @@ import { expect } from 'chai'
import Long from 'long' import Long from 'long'
import { describe, it } from 'mocha' import { describe, it } from 'mocha'
import { import { NodePlatform } from '@mtcute/node'
hexDecode, import { TlBinaryReader, TlBinaryWriter, TlSerializationCounter } from '@mtcute/tl-runtime'
hexDecodeToBuffer,
hexEncode,
TlBinaryReader,
TlBinaryWriter,
TlSerializationCounter,
} from '@mtcute/tl-runtime'
// here we primarily want to check that everything imports properly, // here we primarily want to check that everything imports properly,
// and that the code is actually executable. The actual correctness // and that the code is actually executable. The actual correctness
// of the implementation is covered tested by unit tests // of the implementation is covered tested by unit tests
const p = new NodePlatform()
describe('encodings', () => { describe('encodings', () => {
it('works with Buffers', () => { it('works with Buffers', () => {
const buf = Buffer.alloc(5) expect(p.hexEncode(Buffer.from('hello'))).to.equal('68656c6c6f')
hexDecode(buf, '0102030405') expect(p.hexDecode('0102030405')).eql(Buffer.from([1, 2, 3, 4, 5]))
expect(hexEncode(Buffer.from('hello'))).to.equal('68656c6c6f')
expect(buf).eql(Buffer.from([1, 2, 3, 4, 5]))
}) })
it('works with Uint8Arrays', () => { it('works with Uint8Arrays', () => {
const buf = new Uint8Array(5) expect(p.hexEncode(new Uint8Array([1, 2, 3, 4, 5]))).to.equal('0102030405')
hexDecode(buf, '0102030405')
expect(hexEncode(new Uint8Array([1, 2, 3, 4, 5]))).to.equal('0102030405')
expect(buf).eql(new Uint8Array([1, 2, 3, 4, 5]))
}) })
}) })
@ -61,7 +50,7 @@ describe('TlBinaryReader', () => {
}) })
it('should work with Uint8Arrays', () => { it('should work with Uint8Arrays', () => {
const buf = hexDecodeToBuffer(data) const buf = p.hexDecode(data)
const r = new TlBinaryReader(map, buf, 8) const r = new TlBinaryReader(map, buf, 8)
@ -96,7 +85,7 @@ describe('TlBinaryWriter', () => {
w.long(Long.fromString('51E57AC91E83C801', true, 16)) // messageId w.long(Long.fromString('51E57AC91E83C801', true, 16)) // messageId
w.object(obj) w.object(obj)
expect(hexEncode(w.result())).eq( expect(p.hexEncode(w.result())).eq(
'000000000000000001c8831ec97ae551632416050817ed48941a08f98100000015c4b51c01000000216be86c022bb4c3', '000000000000000001c8831ec97ae551632416050817ed48941a08f98100000015c4b51c01000000216be86c022bb4c3',
) )
}) })
@ -104,7 +93,7 @@ describe('TlBinaryWriter', () => {
it('should work with Uint8Arrays', () => { it('should work with Uint8Arrays', () => {
const obj = { const obj = {
_: 'mt_resPQ', _: 'mt_resPQ',
pq: hexDecodeToBuffer('17ED48941A08F981'), pq: p.hexDecode('17ED48941A08F981'),
serverPublicKeyFingerprints: [Long.fromString('c3b42b026ce86b21', 16)], serverPublicKeyFingerprints: [Long.fromString('c3b42b026ce86b21', 16)],
} }
@ -115,7 +104,7 @@ describe('TlBinaryWriter', () => {
w.long(Long.fromString('51E57AC91E83C801', true, 16)) // messageId w.long(Long.fromString('51E57AC91E83C801', true, 16)) // messageId
w.object(obj) w.object(obj)
expect(hexEncode(w.result())).eq( expect(p.hexEncode(w.result())).eq(
'000000000000000001c8831ec97ae551632416050817ed48941a08f98100000015c4b51c01000000216be86c022bb4c3', '000000000000000001c8831ec97ae551632416050817ed48941a08f98100000015c4b51c01000000216be86c022bb4c3',
) )
}) })

View file

@ -2,13 +2,16 @@ import { expect } from 'chai'
import Long from 'long' import Long from 'long'
import { describe, it } from 'mocha' import { describe, it } from 'mocha'
import { NodePlatform } from '@mtcute/node'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { __tlReaderMap } from '@mtcute/tl/binary/reader.js' import { __tlReaderMap } from '@mtcute/tl/binary/reader.js'
import { __tlWriterMap } from '@mtcute/tl/binary/writer.js' import { __tlWriterMap } from '@mtcute/tl/binary/writer.js'
import { hexDecodeToBuffer, hexEncode, TlBinaryReader, TlBinaryWriter } from '@mtcute/tl-runtime' import { TlBinaryReader, TlBinaryWriter } from '@mtcute/tl-runtime'
// here we primarily want to check that @mtcute/tl correctly works with @mtcute/tl-runtime // here we primarily want to check that @mtcute/tl correctly works with @mtcute/tl-runtime
const p = new NodePlatform()
describe('@mtcute/tl', () => { describe('@mtcute/tl', () => {
it('writers map works with TlBinaryWriter', () => { it('writers map works with TlBinaryWriter', () => {
const obj = { const obj = {
@ -17,11 +20,13 @@ describe('@mtcute/tl', () => {
accessHash: Long.fromNumber(456), accessHash: Long.fromNumber(456),
} }
expect(hexEncode(TlBinaryWriter.serializeObject(__tlWriterMap, obj))).to.equal('4ca5e8dd7b00000000000000c801000000000000') expect(p.hexEncode(TlBinaryWriter.serializeObject(__tlWriterMap, obj))).to.equal(
'4ca5e8dd7b00000000000000c801000000000000',
)
}) })
it('readers map works with TlBinaryReader', () => { it('readers map works with TlBinaryReader', () => {
const buf = hexDecodeToBuffer('4ca5e8dd7b00000000000000c801000000000000') const buf = p.hexDecode('4ca5e8dd7b00000000000000c801000000000000')
const obj = TlBinaryReader.deserializeObject(__tlReaderMap, buf) const obj = TlBinaryReader.deserializeObject(__tlReaderMap, buf)
expect(obj._).equal('inputPeerUser') expect(obj._).equal('inputPeerUser')

View file

@ -1,9 +1,12 @@
import { expect } from 'chai' import { expect } from 'chai'
import { before, describe, it } from 'mocha' import { before, describe, it } from 'mocha'
import { ige256Decrypt, ige256Encrypt, initAsync } from '@mtcute/wasm' import { NodeCryptoProvider } from '@mtcute/node'
import { ige256Decrypt, ige256Encrypt } from '@mtcute/wasm'
before(() => initAsync()) before(async () => {
await new NodeCryptoProvider().initialize()
})
describe('@mtcute/wasm', () => { describe('@mtcute/wasm', () => {
const key = Buffer.from('5468697320697320616E20696D706C655468697320697320616E20696D706C65', 'hex') const key = Buffer.from('5468697320697320616E20696D706C655468697320697320616E20696D706C65', 'hex')

View file

@ -1,16 +1,22 @@
import { MemoryStorage } from '@mtcute/core' import { MemoryStorage } from '@mtcute/core'
import { setPlatform } from '@mtcute/core/platform.js'
import { LogManager } from '@mtcute/core/utils.js' import { LogManager } from '@mtcute/core/utils.js'
import { NodeCryptoProvider, NodePlatform, TcpTransport } from '@mtcute/node'
export const getApiParams = () => { export const getApiParams = () => {
if (!process.env.API_ID || !process.env.API_HASH) { if (!process.env.API_ID || !process.env.API_HASH) {
throw new Error('API_ID and API_HASH env variables must be set') throw new Error('API_ID and API_HASH env variables must be set')
} }
setPlatform(new NodePlatform())
return { return {
apiId: parseInt(process.env.API_ID), apiId: parseInt(process.env.API_ID),
apiHash: process.env.API_HASH, apiHash: process.env.API_HASH,
testMode: true, testMode: true,
storage: new MemoryStorage(), storage: new MemoryStorage(),
logLevel: LogManager.DEBUG, logLevel: LogManager.DEBUG,
transport: () => new TcpTransport(),
crypto: new NodeCryptoProvider(),
} }
} }

View file

@ -18,6 +18,7 @@
"@mtcute/tl-runtime": "*", "@mtcute/tl-runtime": "*",
"@mtcute/tl-utils": "*", "@mtcute/tl-utils": "*",
"@mtcute/wasm": "*", "@mtcute/wasm": "*",
"@mtcute/web": "*",
"@types/chai": "^4.3.8", "@types/chai": "^4.3.8",
"@types/mocha": "^10.0.2", "@types/mocha": "^10.0.2",
"chai": "^4.3.10", "chai": "^4.3.10",

View file

@ -21,6 +21,10 @@ function runForFile(dir, file, single = true) {
let cmds = runFile(file) let cmds = runFile(file)
if (!cmds) {
return
}
const options = { const options = {
env: { env: {
...env, ...env,

View file

@ -1,7 +1,8 @@
import { expect } from 'chai' import { expect } from 'chai'
import { describe, it } from 'mocha' import { describe, it } from 'mocha'
import { BaseTelegramClient, MtUnsupportedError, TelegramClient } from '@mtcute/core' import { MtUnsupportedError } from '@mtcute/core'
import { BaseTelegramClient, TelegramClient } from '@mtcute/core/client.js'
import { getApiParams } from '../utils.js' import { getApiParams } from '../utils.js'

View file

@ -1,7 +1,7 @@
import { expect } from 'chai' import { expect } from 'chai'
import { describe, it } from 'mocha' import { describe, it } from 'mocha'
import { TelegramClient } from '@mtcute/core' import { TelegramClient } from '@mtcute/core/client.js'
import { getApiParams } from '../utils.js' import { getApiParams } from '../utils.js'

View file

@ -3,7 +3,8 @@ import { expect } from 'chai'
import { createHash } from 'crypto' import { createHash } from 'crypto'
import { describe, it } from 'mocha' import { describe, it } from 'mocha'
import { FileDownloadLocation, TelegramClient, Thumbnail } from '@mtcute/core' import { FileDownloadLocation, Thumbnail } from '@mtcute/core'
import { TelegramClient } from '@mtcute/core/client.js'
import { sleep } from '@mtcute/core/utils.js' import { sleep } from '@mtcute/core/utils.js'
import { getApiParams } from '../utils.js' import { getApiParams } from '../utils.js'

View file

@ -1,7 +1,8 @@
import { expect } from 'chai' import { expect } from 'chai'
import { describe, it } from 'mocha' import { describe, it } from 'mocha'
import { Message, TelegramClient } from '@mtcute/core' import { Message } from '@mtcute/core'
import { TelegramClient } from '@mtcute/core/client.js'
import { getApiParams, waitFor } from '../utils.js' import { getApiParams, waitFor } from '../utils.js'

85
e2e/ts/tests/05.worker.ts Normal file
View file

@ -0,0 +1,85 @@
/* eslint-disable no-restricted-imports */
import { expect } from 'chai'
import { describe, it } from 'mocha'
import path from 'path'
import { Worker } from 'worker_threads'
import { TelegramClient } from '@mtcute/core/client.js'
import { Message, TelegramWorkerPort, tl } from '@mtcute/node'
import { getApiParams, waitFor } from '../utils.js'
import type { CustomMethods } from './_worker.js'
describe('5. worker', async function () {
this.timeout(300_000)
const worker = new Worker(path.resolve(__dirname, '_worker.js'))
const port = new TelegramWorkerPort<CustomMethods>({
worker,
})
const portClient = new TelegramClient({ client: port })
it('should make api calls', async function () {
const res = await port.call({ _: 'help.getConfig' })
expect(res._).to.equal('config')
})
it('should call custom methods', async function () {
const hello = await port.invokeCustom('hello')
expect(hello).to.equal('world')
const sum = await port.invokeCustom('sum', 2, 3)
expect(sum).to.equal(5)
})
it('should throw errors', async function () {
try {
await port.call({ _: 'test.useError' })
throw new Error('should have thrown')
} catch (e) {
expect(e).to.be.an.instanceOf(tl.RpcError)
}
})
it('should receive updates', async function () {
const client2 = new TelegramClient(getApiParams('dc2.session'))
try {
await client2.connect()
await port.startUpdatesLoop()
const me = await portClient.getMe()
let username = me.username
if (!username) {
username = `mtcute_e2e_${Math.random().toString(36).slice(2, 8)}`
github-advanced-security[bot] commented 2024-03-04 01:42:34 +03:00 (Migrated from github.com)
Review

Insecure randomness

This uses a cryptographically insecure random number generated at Math.random() in a security context.

Show more details

## Insecure randomness This uses a cryptographically insecure random number generated at [Math.random()](1) in a security context. [Show more details](https://github.com/mtcute/mtcute/security/code-scanning/30)
await portClient.setMyUsername(username)
}
const msgs: Message[] = []
portClient.on('new_message', (msg) => {
msgs.push(msg)
})
const testText = `test ${Math.random()}`
await client2.sendText(username, testText)
await waitFor(() => {
expect(msgs.length).to.be.greaterThan(0)
expect(msgs[0].text).to.equal(testText)
})
} catch (e) {
await client2.close()
throw e
}
await client2.close()
})
this.afterAll(async () => {
await port.close()
worker.terminate()
})
})

18
e2e/ts/tests/_worker.ts Normal file
View file

@ -0,0 +1,18 @@
import { WorkerCustomMethods } from '@mtcute/core/worker.js'
import { BaseTelegramClient, TelegramWorker } from '@mtcute/node'
import { getApiParams } from '../utils.js'
const customMethods = {
hello: async () => 'world',
sum: async (a: number, b: number) => a + b,
} as const satisfies WorkerCustomMethods
export type CustomMethods = typeof customMethods
const client = new BaseTelegramClient(getApiParams('dc1.session'))
// eslint-disable-next-line no-new
new TelegramWorker({
client,
customMethods,
})

View file

@ -1,7 +1,7 @@
import { expect } from 'chai' import { expect } from 'chai'
import { describe, it } from 'mocha' import { describe, it } from 'mocha'
import { BaseTelegramClient } from '@mtcute/core' import { BaseTelegramClient } from '@mtcute/core/client.js'
// @fix-import // @fix-import
import { getApiParams } from '../../utils' import { getApiParams } from '../../utils'

View file

@ -3,34 +3,25 @@ import { expect } from 'chai'
import Long from 'long' import Long from 'long'
import { describe, it } from 'mocha' import { describe, it } from 'mocha'
import { import { TlBinaryReader, TlBinaryWriter, TlSerializationCounter } from '@mtcute/tl-runtime'
hexDecode, import { NodePlatform } from '@mtcute/node'
hexDecodeToBuffer, import { setPlatform } from '@mtcute/core/platform.js'
hexEncode,
TlBinaryReader,
TlBinaryWriter,
TlSerializationCounter,
} from '@mtcute/tl-runtime'
// here we primarily want to check that everything imports properly, // here we primarily want to check that everything imports properly,
// and that the code is actually executable. The actual correctness // and that the code is actually executable. The actual correctness
// of the implementation is covered tested by unit tests // of the implementation is covered tested by unit tests
const p = new NodePlatform()
setPlatform(p)
describe('encodings', () => { describe('encodings', () => {
it('works with Buffers', () => { it('works with Buffers', () => {
const buf = Buffer.alloc(5) expect(p.hexEncode(Buffer.from('hello'))).to.equal('68656c6c6f')
hexDecode(buf, '0102030405') expect(p.hexDecode('0102030405')).eql(Buffer.from([1, 2, 3, 4, 5]))
expect(hexEncode(Buffer.from('hello'))).to.equal('68656c6c6f')
expect(buf).eql(Buffer.from([1, 2, 3, 4, 5]))
}) })
it('works with Uint8Arrays', () => { it('works with Uint8Arrays', () => {
const buf = new Uint8Array(5) expect(p.hexEncode(new Uint8Array([1, 2, 3, 4, 5]))).to.equal('0102030405')
hexDecode(buf, '0102030405')
expect(hexEncode(new Uint8Array([1, 2, 3, 4, 5]))).to.equal('0102030405')
expect(buf).eql(new Uint8Array([1, 2, 3, 4, 5]))
}) })
}) })
@ -62,7 +53,7 @@ describe('TlBinaryReader', () => {
}) })
it('should work with Uint8Arrays', () => { it('should work with Uint8Arrays', () => {
const buf = hexDecodeToBuffer(data) const buf = p.hexDecode(data)
const r = new TlBinaryReader(map, buf, 8) const r = new TlBinaryReader(map, buf, 8)
@ -98,7 +89,7 @@ describe('TlBinaryWriter', () => {
w.long(Long.fromString('51E57AC91E83C801', true, 16)) // messageId w.long(Long.fromString('51E57AC91E83C801', true, 16)) // messageId
w.object(obj) w.object(obj)
expect(hexEncode(w.result())).eq( expect(p.hexEncode(w.result())).eq(
'000000000000000001c8831ec97ae551632416050817ed48941a08f98100000015c4b51c01000000216be86c022bb4c3', '000000000000000001c8831ec97ae551632416050817ed48941a08f98100000015c4b51c01000000216be86c022bb4c3',
) )
}) })
@ -106,7 +97,7 @@ describe('TlBinaryWriter', () => {
it('should work with Uint8Arrays', () => { it('should work with Uint8Arrays', () => {
const obj = { const obj = {
_: 'mt_resPQ', _: 'mt_resPQ',
pq: hexDecodeToBuffer('17ED48941A08F981'), pq: p.hexDecode('17ED48941A08F981'),
serverPublicKeyFingerprints: [Long.fromString('c3b42b026ce86b21', 16)], serverPublicKeyFingerprints: [Long.fromString('c3b42b026ce86b21', 16)],
} }
@ -117,7 +108,7 @@ describe('TlBinaryWriter', () => {
w.long(Long.fromString('51E57AC91E83C801', true, 16)) // messageId w.long(Long.fromString('51E57AC91E83C801', true, 16)) // messageId
w.object(obj) w.object(obj)
expect(hexEncode(w.result())).eq( expect(p.hexEncode(w.result())).eq(
'000000000000000001c8831ec97ae551632416050817ed48941a08f98100000015c4b51c01000000216be86c022bb4c3', '000000000000000001c8831ec97ae551632416050817ed48941a08f98100000015c4b51c01000000216be86c022bb4c3',
) )
}) })

View file

@ -2,13 +2,18 @@ import { expect } from 'chai'
import Long from 'long' import Long from 'long'
import { describe, it } from 'mocha' import { describe, it } from 'mocha'
import { setPlatform } from '@mtcute/core/platform.js'
import { NodePlatform } from '@mtcute/node'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { __tlReaderMap } from '@mtcute/tl/binary/reader.js' import { __tlReaderMap } from '@mtcute/tl/binary/reader.js'
import { __tlWriterMap } from '@mtcute/tl/binary/writer.js' import { __tlWriterMap } from '@mtcute/tl/binary/writer.js'
import { hexDecodeToBuffer, hexEncode, TlBinaryReader, TlBinaryWriter } from '@mtcute/tl-runtime' import { TlBinaryReader, TlBinaryWriter } from '@mtcute/tl-runtime'
// here we primarily want to check that @mtcute/tl correctly works with @mtcute/tl-runtime // here we primarily want to check that @mtcute/tl correctly works with @mtcute/tl-runtime
const p = new NodePlatform()
setPlatform(p)
describe('@mtcute/tl', () => { describe('@mtcute/tl', () => {
it('writers map works with TlBinaryWriter', () => { it('writers map works with TlBinaryWriter', () => {
const obj = { const obj = {
@ -17,13 +22,13 @@ describe('@mtcute/tl', () => {
accessHash: Long.fromNumber(456), accessHash: Long.fromNumber(456),
} }
expect(hexEncode(TlBinaryWriter.serializeObject(__tlWriterMap, obj))).to.equal( expect(p.hexEncode(TlBinaryWriter.serializeObject(__tlWriterMap, obj))).to.equal(
'4ca5e8dd7b00000000000000c801000000000000', '4ca5e8dd7b00000000000000c801000000000000',
) )
}) })
it('readers map works with TlBinaryReader', () => { it('readers map works with TlBinaryReader', () => {
const buf = hexDecodeToBuffer('4ca5e8dd7b00000000000000c801000000000000') const buf = p.hexDecode('4ca5e8dd7b00000000000000c801000000000000')
// eslint-disable-next-line // eslint-disable-next-line
const obj = TlBinaryReader.deserializeObject<any>(__tlReaderMap, buf) const obj = TlBinaryReader.deserializeObject<any>(__tlReaderMap, buf)

View file

@ -1,9 +1,12 @@
import { expect } from 'chai' import { expect } from 'chai'
import { before, describe, it } from 'mocha' import { before, describe, it } from 'mocha'
import { ige256Decrypt, ige256Encrypt, initAsync } from '@mtcute/wasm' import { NodeCryptoProvider } from '@mtcute/node'
import { ige256Decrypt, ige256Encrypt } from '@mtcute/wasm'
before(() => initAsync()) before(async () => {
await new NodeCryptoProvider().initialize()
})
describe('@mtcute/wasm', () => { describe('@mtcute/wasm', () => {
const key = Buffer.from('5468697320697320616E20696D706C655468697320697320616E20696D706C65', 'hex') const key = Buffer.from('5468697320697320616E20696D706C655468697320697320616E20696D706C65', 'hex')

View file

@ -2,7 +2,9 @@
import { join } from 'path' import { join } from 'path'
import { MaybePromise, MemoryStorage } from '@mtcute/core' import { MaybePromise, MemoryStorage } from '@mtcute/core'
import { setPlatform } from '@mtcute/core/platform.js'
import { LogManager, sleep } from '@mtcute/core/utils.js' import { LogManager, sleep } from '@mtcute/core/utils.js'
import { NodeCryptoProvider, NodePlatform, TcpTransport } from '@mtcute/node'
import { SqliteStorage } from '@mtcute/sqlite' import { SqliteStorage } from '@mtcute/sqlite'
export const getApiParams = (storage?: string) => { export const getApiParams = (storage?: string) => {
@ -10,12 +12,16 @@ export const getApiParams = (storage?: string) => {
throw new Error('API_ID and API_HASH env variables must be set') throw new Error('API_ID and API_HASH env variables must be set')
} }
setPlatform(new NodePlatform())
return { return {
apiId: parseInt(process.env.API_ID), apiId: parseInt(process.env.API_ID),
apiHash: process.env.API_HASH, apiHash: process.env.API_HASH,
testMode: true, testMode: true,
storage: storage ? new SqliteStorage(join(__dirname, storage)) : new MemoryStorage(), storage: storage ? new SqliteStorage(join(__dirname, storage)) : new MemoryStorage(),
logLevel: LogManager.VERBOSE, logLevel: LogManager.VERBOSE,
transport: () => new TcpTransport(),
crypto: new NodeCryptoProvider(),
} }
} }

View file

@ -2,25 +2,48 @@
📖 [API Reference](https://ref.mtcute.dev/modules/_mtcute_core.html) 📖 [API Reference](https://ref.mtcute.dev/modules/_mtcute_core.html)
Basic low-level MTProto implementation and auxiliary utilities. Platform-agnostic MTProto implementation and auxiliary utilities.
## Features ## Features
- **MTProto 2.0**: Implements the full MTProto protocol, including all the encryption and serialization - **MTProto 2.0**: Implements the full MTProto protocol, including all the encryption and serialization
- **2FA support**: Provides utilities for 2-step verification - **2FA support**: Provides utilities for 2-step verification
- **Hackable**: Bring your own storage, transport, and other components to customize the library to your needs - **Hackable**: Bring your own storage, transport, and other components to customize the library to your needs
- **Magical**: Handles reconnections, connection pooling, DC redirections and other stuff for you - **Magical**: Handles reconnections, connection pooling, DC redirections and other stuff for you
- **Web support**: Works in the browser with no additional configuration - **Updates handling**: Implements proper updates handling, including ordering and gap recovery (learn more)
- **High-level**: Includes a high-level API that wrap the MTProto APIs and provide a clean interface
- **Tree-shaking**: You can import just the methods you need, and the rest will not be included into the bundle
## Usage ## Usage
```ts ```ts
import { BaseTelegramClient } from '@mtcute/core' import { BaseTelegramClient } from '@mtcute/core/client.js'
const tg = new BaseTelegramClient({ const tg = new BaseTelegramClient({
apiId: 12345, apiId: 12345,
apiHash: '0123456789abcdef0123456789abcdef', apiHash: '0123456789abcdef0123456789abcdef',
crypto: new MyCryptoProvider(),
storage: new MyStorage(),
transport: () => new MyTransport(),
}) })
tg.call({ _: 'help.getConfig' }) tg.call({ _: 'help.getConfig' })
.then(console.log) .then(console.log)
``` ```
## Usage with high-level API
```ts
import { TelegramClient } from '@mtcute/core/client.js'
const tg = new TelegramClient({
// ... same options as above
})
tg.run({
phone: '+1234567890',
code: () => prompt('Enter the code:'),
password: 'my-password',
}, async (user) => {
console.log(`✨ logged in as ${user.displayName}`)
})
```

View file

@ -1,4 +1,6 @@
module.exports = ({ path, transformFile, packageDir, outDir }) => ({ const KNOWN_DECORATORS = ['memoizeGetters', 'makeInspectable']
module.exports = ({ path, glob, transformFile, packageDir, outDir }) => ({
esmOnlyDirectives: true, esmOnlyDirectives: true,
esmImportDirectives: true, esmImportDirectives: true,
final() { final() {
@ -7,5 +9,56 @@ module.exports = ({ path, transformFile, packageDir, outDir }) => ({
transformFile(path.join(outDir, 'cjs/network/network-manager.js'), replaceVersion) transformFile(path.join(outDir, 'cjs/network/network-manager.js'), replaceVersion)
transformFile(path.join(outDir, 'esm/network/network-manager.js'), replaceVersion) transformFile(path.join(outDir, 'esm/network/network-manager.js'), replaceVersion)
// make decorators properly tree-shakeable
// very fragile, but it works for now :D
const decoratorsRegex = new RegExp(
`(${KNOWN_DECORATORS.join('|')})\\((.+?)\\);`,
'gs',
)
const replaceDecorators = (content, file) => {
if (!KNOWN_DECORATORS.some((d) => content.includes(d))) return null
const countPerClass = new Map()
content = content.replace(decoratorsRegex, (_, name, args) => {
const [clsName_, ...rest] = args.split(',')
const clsName = clsName_.trim()
const count = (countPerClass.get(clsName) || 0) + 1
countPerClass.set(clsName, count)
const prevName = count === 1 ? clsName : `${clsName}$${count - 1}`
const localName = `${clsName}$${count}`
return `const ${localName} = /*#__PURE__*/${name}(${prevName}, ${rest.join(',')});`
})
if (countPerClass.size === 0) {
throw new Error('No decorator usages found, but known names were used')
}
const customExports = []
for (const [clsName, count] of countPerClass) {
const needle = new RegExp(`^export class(?= ${clsName} ({|extends ))`, 'm')
if (!content.match(needle)) {
throw new Error(`Class ${clsName} not found in ${file}`)
}
content = content.replace(needle, 'class')
customExports.push(
`export { ${clsName}$${count} as ${clsName} }`,
)
}
return content + '\n' + customExports.join('\n') + '\n'
}
for (const f of glob.sync(path.join(outDir, 'esm/highlevel/types/**/*.js'))) {
transformFile(f, replaceDecorators)
}
}, },
}) })

View file

@ -7,23 +7,19 @@
"license": "MIT", "license": "MIT",
"main": "src/index.ts", "main": "src/index.ts",
"type": "module", "type": "module",
"sideEffects": false,
"scripts": { "scripts": {
"build": "pnpm run -w build-package core", "build": "pnpm run -w build-package core",
"gen-client": "node ./scripts/generate-client.cjs", "gen-client": "node ./scripts/generate-client.cjs",
"gen-updates": "node ./scripts/generate-updates.cjs" "gen-updates": "node ./scripts/generate-updates.cjs"
}, },
"browser": { "exports": {
"./src/utils/platform/crypto.js": "./src/utils/platform/crypto.web.js", ".": "./src/index.ts",
"./src/utils/platform/transport.js": "./src/utils/platform/transport.web.js", "./utils.js": "./src/utils/index.ts",
"./src/utils/platform/logging.js": "./src/utils/platform/logging.web.js", "./client.js": "./src/highlevel/client.ts",
"./src/utils/platform/random.js": "./src/utils/platform/random.web.js", "./worker.js": "./src/highlevel/worker/index.ts",
"./src/utils/platform/exit-hook.js": "./src/utils/platform/exit-hook.web.js", "./methods.js": "./src/highlevel/methods.ts",
"./src/highlevel/worker/platform/connect.js": "./src/highlevel/worker/platform/connect.web.js", "./platform.js": "./src/platform.ts"
"./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
}, },
"distOnlyFields": { "distOnlyFields": {
"exports": { "exports": {
@ -35,32 +31,27 @@
"import": "./esm/utils/index.js", "import": "./esm/utils/index.js",
"require": "./cjs/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": { "./methods.js": {
"import": "./esm/highlevel/methods.js", "import": "./esm/highlevel/methods.js",
"require": "./cjs/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"
},
"./worker.js": {
"import": "./esm/highlevel/worker/index.js",
"require": "./cjs/highlevel/worker/index.js"
} }
} }
}, },
"dependencies": { "dependencies": {
"@mtcute/tl": "workspace:^", "@mtcute/tl": "workspace:^",
"@mtcute/tl-runtime": "workspace:^", "@mtcute/tl-runtime": "workspace:^",
"@mtcute/wasm": "workspace:^",
"@mtcute/file-id": "workspace:^", "@mtcute/file-id": "workspace:^",
"@types/events": "3.0.0", "@types/events": "3.0.0",
"events": "3.2.0", "events": "3.2.0",

View file

@ -273,6 +273,7 @@ async function addSingleMethod(state, fileName) {
} }
const isExported = (stmt.modifiers || []).find((mod) => mod.kind === ts.SyntaxKind.ExportKeyword) 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 isInitialize = checkForFlag(stmt, '@initialize')
const isManualImpl = checkForFlag(stmt, '@manual-impl') const isManualImpl = checkForFlag(stmt, '@manual-impl')
const isInitializeSuper = isInitialize === 'super' 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] const firstArg = stmt.parameters[0]
@ -344,7 +345,7 @@ async function addSingleMethod(state, fileName) {
state.methods.used[name] = relPath state.methods.used[name] = relPath
} }
if (isExported) { if (isExported || isDeclare) {
const isPrivate = checkForFlag(stmt, '@internal') const isPrivate = checkForFlag(stmt, '@internal')
const isManual = checkForFlag(stmt, '@manual') const isManual = checkForFlag(stmt, '@manual')
const isNoemit = checkForFlag(stmt, '@noemit') const isNoemit = checkForFlag(stmt, '@noemit')
@ -358,6 +359,7 @@ async function addSingleMethod(state, fileName) {
isPrivate, isPrivate,
isManual, isManual,
isNoemit, isNoemit,
isDeclare,
shouldEmit, shouldEmit,
func: stmt, func: stmt,
comment: getLeadingComments(stmt), comment: getLeadingComments(stmt),
@ -369,12 +371,14 @@ async function addSingleMethod(state, fileName) {
hasOverloads: hasOverloads[name] && !isOverload, hasOverloads: hasOverloads[name] && !isOverload,
}) })
if (!(module in state.imports)) { if (!isDeclare) {
state.imports[module] = new Set() if (!(module in state.imports)) {
} state.imports[module] = new Set()
}
if (!isManual || isManual.split('=')[1] !== 'noemit') { if (!isManual || isManual.split('=')[1] !== 'noemit') {
state.imports[module].add(name) state.imports[module].add(name)
}
} }
} }
} }
@ -399,6 +403,9 @@ async function addSingleMethod(state, fileName) {
} }
state.imports[module].add(stmt.name.escapedText) state.imports[module].add(stmt.name.escapedText)
state.exported[module] = state.exported[module] || new Set()
state.exported[module].add(stmt.name.escapedText)
continue continue
} }
@ -429,6 +436,9 @@ async function addSingleMethod(state, fileName) {
} }
state.imports[module].add(stmt.name.escapedText) 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) { } else if (isCopy) {
state.copy.push({ from: relPath, code: stmt.getFullText().trim() }) state.copy.push({ from: relPath, code: stmt.getFullText().trim() })
} else if (isTypeExported) { } else if (isTypeExported) {
@ -442,6 +452,7 @@ async function main() {
const output = fs.createWriteStream(targetFile) const output = fs.createWriteStream(targetFile)
const state = { const state = {
imports: {}, imports: {},
exported: {},
fields: [], fields: [],
init: [], init: [],
methods: { methods: {
@ -527,6 +538,7 @@ on(name: string, handler: (...args: any[]) => void): this\n`)
available, available,
rawApiMethods, rawApiMethods,
dependencies, dependencies,
isDeclare,
}) => { }) => {
if (!available && !overload) { if (!available && !overload) {
// no @available directive // no @available directive
@ -659,7 +671,7 @@ on(name: string, handler: (...args: any[]) => void): this\n`)
output.write(`${name}${generics}(${parameters})${returnType}\n`) output.write(`${name}${generics}(${parameters})${returnType}\n`)
} }
if (!overload && !isManual) { if (!overload && !isManual && !isDeclare) {
if (hasOverloads) { if (hasOverloads) {
classProtoDecls.push('// @ts-expect-error this kinda breaks typings for overloads, idc') classProtoDecls.push('// @ts-expect-error this kinda breaks typings for overloads, idc')
} }
@ -677,6 +689,7 @@ on(name: string, handler: (...args: any[]) => void): this\n`)
output.write('}\n') output.write('}\n')
output.write('\nexport type { TelegramClientOptions }\n') output.write('\nexport type { TelegramClientOptions }\n')
output.write('\nexport * from "./base.js"\n')
output.write('\nexport class TelegramClient extends EventEmitter implements ITelegramClient {\n') output.write('\nexport class TelegramClient extends EventEmitter implements ITelegramClient {\n')
output.write(' _client: ITelegramClient\n') output.write(' _client: ITelegramClient\n')
@ -737,9 +750,14 @@ on(name: string, handler: (...args: any[]) => void): this\n`)
const outputMethods = fs.createWriteStream(targetFileMethods) const outputMethods = fs.createWriteStream(targetFileMethods)
outputMethods.write('/* THIS FILE WAS AUTO-GENERATED */\n') outputMethods.write('/* THIS FILE WAS AUTO-GENERATED */\n')
state.methods.list.forEach(({ module, name, overload }) => { state.methods.list.forEach(({ module, name, overload, isDeclare }) => {
if (overload) return if (overload || isDeclare) return
outputMethods.write(`export { ${name} } from '${module}'\n`) 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) }) await new Promise((resolve) => { outputMethods.end(resolve) })

View file

@ -7,8 +7,7 @@ import Long from 'long'
import { tdFileId } from '@mtcute/file-id' import { tdFileId } from '@mtcute/file-id'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { MemoryStorage } from '../storage/providers/memory/index.js' import { MaybeArray, MaybePromise, MtUnsupportedError, PartialExcept, PartialOnly } from '../types/index.js'
import { MaybeArray, MaybePromise, PartialExcept, PartialOnly } from '../types/index.js'
import { BaseTelegramClient, BaseTelegramClientOptions } from './base.js' import { BaseTelegramClient, BaseTelegramClientOptions } from './base.js'
import { ITelegramClient } from './client.types.js' import { ITelegramClient } from './client.types.js'
import { checkPassword } from './methods/auth/check-password.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 { iterDialogs } from './methods/dialogs/iter-dialogs.js'
import { setFoldersOrder } from './methods/dialogs/set-folders-order.js' import { setFoldersOrder } from './methods/dialogs/set-folders-order.js'
import { downloadAsBuffer } from './methods/files/download-buffer.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 { downloadAsIterable } from './methods/files/download-iterable.js'
import { downloadAsStream } from './methods/files/download-stream.js' import { downloadAsStream } from './methods/files/download-stream.js'
import { _normalizeInputFile } from './methods/files/normalize-input-file.js' import { _normalizeInputFile } from './methods/files/normalize-input-file.js'
@ -314,21 +312,19 @@ import {
UserTypingUpdate, UserTypingUpdate,
} from './types/index.js' } from './types/index.js'
import { makeParsedUpdateHandler, ParsedUpdateHandlerParams } from './updates/parsed.js' import { makeParsedUpdateHandler, ParsedUpdateHandlerParams } from './updates/parsed.js'
import { _defaultStorageFactory } from './utils/platform/storage.js'
import { StringSessionData } from './utils/string-session.js' import { StringSessionData } from './utils/string-session.js'
// from methods/_init.ts // from methods/_init.ts
// @copy // @copy
type TelegramClientOptions = ( type TelegramClientOptions = (
| (Omit<BaseTelegramClientOptions, 'storage'> & { | (PartialOnly<Omit<BaseTelegramClientOptions, 'storage'>, 'transport' | 'crypto'> & {
/** /**
* Storage to use for this client. * Storage to use for this client.
* *
* If a string is passed, it will be used as: * If a string is passed, it will be used as
* - a path to a JSON file for Node.js * a name for the default platform-specific storage provider to use.
* - IndexedDB database name for browsers
* *
* If omitted, {@link MemoryStorage} is used * @default `"client.session"`
*/ */
storage?: string | ITelegramStorageProvider storage?: string | ITelegramStorageProvider
}) })
@ -2250,8 +2246,9 @@ export interface TelegramClient extends ITelegramClient {
* @param params File download parameters * @param params File download parameters
*/ */
downloadAsBuffer(location: FileDownloadLocation, params?: FileDownloadParameters): Promise<Uint8Array> downloadAsBuffer(location: FileDownloadLocation, params?: FileDownloadParameters): Promise<Uint8Array>
/** /**
* Download a remote file to a local file (only for NodeJS). * Download a remote file to a local file (only for Node.js).
* Promise will resolve once the download is complete. * Promise will resolve once the download is complete.
* *
* **Available**: both users and bots * **Available**: both users and bots
@ -2270,6 +2267,15 @@ export interface TelegramClient extends ITelegramClient {
* @param params Download parameters * @param params Download parameters
*/ */
downloadAsIterable(input: FileDownloadLocation, params?: FileDownloadParameters): AsyncIterableIterator<Uint8Array> downloadAsIterable(input: FileDownloadLocation, params?: FileDownloadParameters): AsyncIterableIterator<Uint8Array>
/**
* Download a remote file as a Node.js Readable stream.
*
* **Available**: both users and bots
*
* @param params File download parameters
*/
downloadAsNodeStream(location: FileDownloadLocation, params?: FileDownloadParameters): import('stream').Readable
/** /**
* Download a file and return it as a readable stream, * Download a file and return it as a readable stream,
* streaming file contents. * streaming file contents.
@ -2323,9 +2329,6 @@ export interface TelegramClient extends ITelegramClient {
uploadFile(params: { uploadFile(params: {
/** /**
* Upload file source. * 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 file: UploadFileLike
@ -5132,6 +5135,8 @@ export interface TelegramClient extends ITelegramClient {
export type { TelegramClientOptions } export type { TelegramClientOptions }
export * from './base.js'
export class TelegramClient extends EventEmitter implements ITelegramClient { export class TelegramClient extends EventEmitter implements ITelegramClient {
_client: ITelegramClient _client: ITelegramClient
constructor(opts: TelegramClientOptions) { constructor(opts: TelegramClientOptions) {
@ -5140,20 +5145,13 @@ export class TelegramClient extends EventEmitter implements ITelegramClient {
if ('client' in opts) { if ('client' in opts) {
this._client = opts.client this._client = opts.client
} else { } else {
let storage: ITelegramStorageProvider if (!opts.storage || typeof opts.storage === 'string' || !opts.transport || !opts.crypto) {
throw new MtUnsupportedError(
if (typeof opts.storage === 'string') { 'You need to explicitly provide storage, transport and crypto for @mtcute/core',
storage = _defaultStorageFactory(opts.storage) )
} else if (!opts.storage) {
storage = new MemoryStorage()
} else {
storage = opts.storage
} }
this._client = new BaseTelegramClient({ this._client = new BaseTelegramClient(opts as BaseTelegramClientOptions)
...opts,
storage,
})
} }
// @ts-expect-error codegen // @ts-expect-error codegen
@ -5448,9 +5446,6 @@ TelegramClient.prototype.setFoldersOrder = function (...args) {
TelegramClient.prototype.downloadAsBuffer = function (...args) { TelegramClient.prototype.downloadAsBuffer = function (...args) {
return downloadAsBuffer(this._client, ...args) return downloadAsBuffer(this._client, ...args)
} }
TelegramClient.prototype.downloadToFile = function (...args) {
return downloadToFile(this._client, ...args)
}
TelegramClient.prototype.downloadAsIterable = function (...args) { TelegramClient.prototype.downloadAsIterable = function (...args) {
return downloadAsIterable(this._client, ...args) return downloadAsIterable(this._client, ...args)
} }

View file

@ -1,5 +1,3 @@
export * from './base.js'
export * from './client.js'
export * from './client.types.js' export * from './client.types.js'
export * from './storage/index.js' export * from './storage/index.js'
export * from './types/index.js' export * from './types/index.js'

View file

@ -86,7 +86,6 @@ export { getPeerDialogs } from './methods/dialogs/get-peer-dialogs.js'
export { iterDialogs } from './methods/dialogs/iter-dialogs.js' export { iterDialogs } from './methods/dialogs/iter-dialogs.js'
export { setFoldersOrder } from './methods/dialogs/set-folders-order.js' export { setFoldersOrder } from './methods/dialogs/set-folders-order.js'
export { downloadAsBuffer } from './methods/files/download-buffer.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 { downloadAsIterable } from './methods/files/download-iterable.js'
export { downloadAsStream } from './methods/files/download-stream.js' export { downloadAsStream } from './methods/files/download-stream.js'
export { _normalizeInputFile } from './methods/files/normalize-input-file.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 { createForumTopic } from './methods/forums/create-forum-topic.js'
export { deleteForumTopicHistory } from './methods/forums/delete-forum-topic-history.js' export { deleteForumTopicHistory } from './methods/forums/delete-forum-topic-history.js'
export { editForumTopic } from './methods/forums/edit-forum-topic.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 { getForumTopics } from './methods/forums/get-forum-topics.js'
export { getForumTopicsById } from './methods/forums/get-forum-topics-by-id.js' export { getForumTopicsById } from './methods/forums/get-forum-topics-by-id.js'
export { iterForumTopics } from './methods/forums/iter-forum-topics.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 { exportInviteLink } from './methods/invite-links/export-invite-link.js'
export { getInviteLink } from './methods/invite-links/get-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 { 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 { getInviteLinks } from './methods/invite-links/get-invite-links.js'
export { getPrimaryInviteLink } from './methods/invite-links/get-primary-invite-link.js' export { getPrimaryInviteLink } from './methods/invite-links/get-primary-invite-link.js'
export { hideAllJoinRequests } from './methods/invite-links/hide-all-join-requests.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 { iterInviteLinks } from './methods/invite-links/iter-invite-links.js'
export { revokeInviteLink } from './methods/invite-links/revoke-invite-link.js' export { revokeInviteLink } from './methods/invite-links/revoke-invite-link.js'
export { closePoll } from './methods/messages/close-poll.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 { deleteMessagesById } from './methods/messages/delete-messages.js'
export { deleteMessages } from './methods/messages/delete-messages.js' export { deleteMessages } from './methods/messages/delete-messages.js'
export { deleteScheduledMessages } from './methods/messages/delete-scheduled-messages.js' export { deleteScheduledMessages } from './methods/messages/delete-scheduled-messages.js'
export { editInlineMessage } from './methods/messages/edit-inline-message.js' export { editInlineMessage } from './methods/messages/edit-inline-message.js'
export { editMessage } from './methods/messages/edit-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 { forwardMessagesById } from './methods/messages/forward-messages.js'
export { forwardMessages } 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 { getCallbackQueryMessage } from './methods/messages/get-callback-query-message.js'
export { getDiscussionMessage } from './methods/messages/get-discussion-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 { getHistory } from './methods/messages/get-history.js'
export { getMessageByLink } from './methods/messages/get-message-by-link.js' export { getMessageByLink } from './methods/messages/get-message-by-link.js'
export { getMessageGroup } from './methods/messages/get-message-group.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 { getMessageReactions } from './methods/messages/get-message-reactions.js'
export { getMessages } from './methods/messages/get-messages.js' export { getMessages } from './methods/messages/get-messages.js'
export { getMessagesUnsafe } from './methods/messages/get-messages-unsafe.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 { getReactionUsers } from './methods/messages/get-reaction-users.js'
export { getReplyTo } from './methods/messages/get-reply-to.js' export { getReplyTo } from './methods/messages/get-reply-to.js'
export { getScheduledMessages } from './methods/messages/get-scheduled-messages.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 { pinMessage } from './methods/messages/pin-message.js'
export { readHistory } from './methods/messages/read-history.js' export { readHistory } from './methods/messages/read-history.js'
export { readReactions } from './methods/messages/read-reactions.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 { 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 { searchMessages } from './methods/messages/search-messages.js'
export { answerText } from './methods/messages/send-answer.js' export { answerText } from './methods/messages/send-answer.js'
export { answerMedia } 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 { commentText } from './methods/messages/send-comment.js'
export { commentMedia } from './methods/messages/send-comment.js' export { commentMedia } from './methods/messages/send-comment.js'
export { commentMediaGroup } 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 { 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 { sendCopyGroup } from './methods/messages/send-copy-group.js'
export { sendMedia } from './methods/messages/send-media.js' export { sendMedia } from './methods/messages/send-media.js'
export { sendMediaGroup } from './methods/messages/send-media-group.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 { quoteWithText } from './methods/messages/send-quote.js'
export { quoteWithMedia } from './methods/messages/send-quote.js' export { quoteWithMedia } from './methods/messages/send-quote.js'
export { quoteWithMediaGroup } 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 { cancelPasswordEmail } from './methods/password/password-email.js'
export { removeCloudPassword } from './methods/password/remove-cloud-password.js' export { removeCloudPassword } from './methods/password/remove-cloud-password.js'
export { applyBoost } from './methods/premium/apply-boost.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 { canApplyBoost } from './methods/premium/can-apply-boost.js'
export { getBoostStats } from './methods/premium/get-boost-stats.js' export { getBoostStats } from './methods/premium/get-boost-stats.js'
export { getBoosts } from './methods/premium/get-boosts.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 { moveStickerInSet } from './methods/stickers/move-sticker-in-set.js'
export { setChatStickerSet } from './methods/stickers/set-chat-sticker-set.js' export { setChatStickerSet } from './methods/stickers/set-chat-sticker-set.js'
export { setStickerSetThumb } from './methods/stickers/set-sticker-set-thumb.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 { canSendStory } from './methods/stories/can-send-story.js'
export { deleteStories } from './methods/stories/delete-stories.js' export { deleteStories } from './methods/stories/delete-stories.js'
export { editStory } from './methods/stories/edit-story.js' export { editStory } from './methods/stories/edit-story.js'

View file

@ -6,7 +6,7 @@ import { tdFileId } from '@mtcute/file-id'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
// @copy // @copy
import { MaybeArray, MaybePromise, PartialExcept, PartialOnly } from '../../types/index.js' import { MaybeArray, MaybePromise, MtUnsupportedError, PartialExcept, PartialOnly } from '../../types/index.js'
// @copy // @copy
import { BaseTelegramClient, BaseTelegramClientOptions } from '../base.js' import { BaseTelegramClient, BaseTelegramClientOptions } from '../base.js'
// @copy // @copy

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
// @copy // @copy
import { MemoryStorage } from '../../storage/providers/memory/index.js' import { MtUnsupportedError, PartialOnly } from '../../types/index.js'
import { BaseTelegramClient, BaseTelegramClientOptions } from '../base.js' import { BaseTelegramClient, BaseTelegramClientOptions } from '../base.js'
import { TelegramClient } from '../client.js' import { TelegramClient } from '../client.js'
import { ITelegramClient } from '../client.types.js' import { ITelegramClient } from '../client.types.js'
@ -11,19 +11,16 @@ import { ITelegramStorageProvider } from '../storage/provider.js'
import { Conversation } from '../types/conversation.js' import { Conversation } from '../types/conversation.js'
// @copy // @copy
import { makeParsedUpdateHandler, ParsedUpdateHandlerParams } from '../updates/parsed.js' import { makeParsedUpdateHandler, ParsedUpdateHandlerParams } from '../updates/parsed.js'
// @copy
import { _defaultStorageFactory } from '../utils/platform/storage.js'
// @copy // @copy
type TelegramClientOptions = ((Omit<BaseTelegramClientOptions, 'storage'> & { type TelegramClientOptions = ((PartialOnly<Omit<BaseTelegramClientOptions, 'storage'>, 'transport' | 'crypto'> & {
/** /**
* Storage to use for this client. * Storage to use for this client.
* *
* If a string is passed, it will be used as: * If a string is passed, it will be used as
* - a path to a JSON file for Node.js * a name for the default platform-specific storage provider to use.
* - IndexedDB database name for browsers
* *
* If omitted, {@link MemoryStorage} is used * @default `"client.session"`
*/ */
storage?: string | ITelegramStorageProvider storage?: string | ITelegramStorageProvider
}) | ({ client: ITelegramClient })) & { }) | ({ client: ITelegramClient })) & {
@ -37,41 +34,17 @@ type TelegramClientOptions = ((Omit<BaseTelegramClientOptions, 'storage'> & {
skipConversationUpdates?: boolean 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 // @initialize
/** @internal */ /** @internal */
function _initializeClient(this: TelegramClient, opts: TelegramClientOptions) { function _initializeClient(this: TelegramClient, opts: TelegramClientOptions) {
if ('client' in opts) { if ('client' in opts) {
this._client = opts.client this._client = opts.client
} else { } else {
let storage: ITelegramStorageProvider 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')
if (typeof opts.storage === 'string') {
storage = _defaultStorageFactory(opts.storage)
} else if (!opts.storage) {
storage = new MemoryStorage()
} else {
storage = opts.storage
} }
this._client = new BaseTelegramClient({ this._client = new BaseTelegramClient(opts as BaseTelegramClientOptions)
...opts,
storage,
})
} }
// @ts-expect-error codegen // @ts-expect-error codegen

View file

@ -1,6 +1,6 @@
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { utf8EncodeToBuffer } from '@mtcute/tl-runtime'
import { getPlatform } from '../../../platform.js'
import { ITelegramClient } from '../../client.types.js' import { ITelegramClient } from '../../client.types.js'
import { InputMessageId, normalizeInputMessageId } from '../../types/index.js' import { InputMessageId, normalizeInputMessageId } from '../../types/index.js'
import { resolvePeer } from '../users/resolve-peer.js' import { resolvePeer } from '../users/resolve-peer.js'
@ -53,7 +53,7 @@ export async function getCallbackAnswer(
_: 'messages.getBotCallbackAnswer', _: 'messages.getBotCallbackAnswer',
peer: await resolvePeer(client, chatId), peer: await resolvePeer(client, chatId),
msgId: message, msgId: message,
data: typeof data === 'string' ? utf8EncodeToBuffer(data) : data, data: typeof data === 'string' ? getPlatform().utf8Encode(data) : data,
password, password,
game: game, game: game,
}, },

View file

@ -1,32 +0,0 @@
import { createReadStream, promises, ReadStream } from 'node:fs'
import { basename } from 'node:path'
import { Readable } from 'node:stream'
import { nodeReadableToWeb } from '../../utils/stream-utils.js'
/** @internal */
export function _createFileStream(path: string): ReadStream {
return createReadStream(path)
}
/** @internal */
export function _isFileStream(stream: unknown): stream is ReadStream {
return stream instanceof ReadStream
}
/** @internal */
export async function _extractFileStreamMeta(stream: ReadStream): Promise<[string, number]> {
const fileName = basename(stream.path.toString())
const fileSize = await promises.stat(stream.path.toString()).then((stat) => stat.size)
return [fileName, fileSize]
}
/** @internal */
export function _handleNodeStream<T>(val: T | Readable): T | ReadableStream<Uint8Array> {
if (val instanceof Readable) {
return nodeReadableToWeb(val)
}
return val
}

View file

@ -1,23 +0,0 @@
import { MtArgumentError } from '../../../types/errors.js'
/** @internal */
export function _createFileStream(): never {
throw new MtArgumentError('Cannot create file stream on web platform')
}
/** @internal */
export function _isFileStream() {
return false
}
/** @internal */
export function _extractFileStreamMeta(): never {
throw new Error('UNREACHABLE')
}
/** @internal */
export function _handleNodeStream(val: unknown) {
return val
}
// all the above functions shall be inlined by terser

View file

@ -1,42 +1,19 @@
// eslint-disable-next-line no-restricted-imports /* eslint-disable @typescript-eslint/no-unused-vars */
import { createWriteStream, rmSync } from 'fs'
import { writeFile } from 'fs/promises'
import { ITelegramClient } from '../../client.types.js' import { ITelegramClient } from '../../client.types.js'
import { FileDownloadLocation, FileDownloadParameters, FileLocation } from '../../types/index.js' import { FileDownloadLocation, FileDownloadParameters } from '../../types/index.js'
import { downloadAsIterable } from './download-iterable.js'
// @available=both
/** /**
* Download a remote file to a local file (only for NodeJS). * Download a remote file to a local file (only for Node.js).
* Promise will resolve once the download is complete. * Promise will resolve once the download is complete.
* *
* @param filename Local file name to which the remote file will be downloaded * @param filename Local file name to which the remote file will be downloaded
* @param params File download parameters * @param params File download parameters
*/ */
export async function downloadToFile( declare function downloadToFile(
client: ITelegramClient, client: ITelegramClient,
filename: string, filename: string,
location: FileDownloadLocation, location: FileDownloadLocation,
params?: FileDownloadParameters, params?: FileDownloadParameters,
): Promise<void> { ): Promise<void>
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()
}

View file

@ -1,5 +0,0 @@
import { MtUnsupportedError } from '../../../types/errors.js'
export function downloadToFile() {
throw new MtUnsupportedError('Downloading to file is only supported in NodeJS')
}

View file

@ -2,6 +2,7 @@ import { parseFileId } from '@mtcute/file-id'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { ConnectionKind } from '../../../network/network-manager.js' import { ConnectionKind } from '../../../network/network-manager.js'
import { getPlatform } from '../../../platform.js'
import { MtArgumentError, MtUnsupportedError } from '../../../types/errors.js' import { MtArgumentError, MtUnsupportedError } from '../../../types/errors.js'
import { ConditionVariable } from '../../../utils/condition-variable.js' import { ConditionVariable } from '../../../utils/condition-variable.js'
import { ITelegramClient } from '../../client.types.js' import { ITelegramClient } from '../../client.types.js'
@ -56,7 +57,7 @@ export async function* downloadAsIterable(
if (!fileSize) fileSize = input.fileSize if (!fileSize) fileSize = input.fileSize
location = locationInner location = locationInner
} else if (typeof input === 'string') { } else if (typeof input === 'string') {
const parsed = parseFileId(input) const parsed = parseFileId(getPlatform(), input)
if (parsed.location._ === 'web') { if (parsed.location._ === 'web') {
location = fileIdToInputWebFileLocation(parsed) location = fileIdToInputWebFileLocation(parsed)

View file

@ -0,0 +1,16 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { ITelegramClient } from '../../client.types.js'
import { FileDownloadLocation, FileDownloadParameters } from '../../types/index.js'
// @available=both
/**
* Download a remote file as a Node.js Readable stream.
*
* @param params File download parameters
*/
declare function downloadAsNodeStream(
client: ITelegramClient,
location: FileDownloadLocation,
params?: FileDownloadParameters,
): import('stream').Readable

View file

@ -3,6 +3,7 @@ import Long from 'long'
import { parseFileId, tdFileId } from '@mtcute/file-id' import { parseFileId, tdFileId } from '@mtcute/file-id'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { getPlatform } from '../../../platform.js'
import { assertTypeIs } from '../../../utils/type-assertions.js' import { assertTypeIs } from '../../../utils/type-assertions.js'
import { ITelegramClient } from '../../client.types.js' import { ITelegramClient } from '../../client.types.js'
import { isUploadedFile } from '../../types/files/uploaded-file.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:/)) { } else if (typeof input === 'string' && input.match(/^file:/)) {
await upload(input.substring(5)) await upload(input.substring(5))
} else { } else {
const parsed = typeof input === 'string' ? parseFileId(input) : input const parsed = typeof input === 'string' ? parseFileId(getPlatform(), input) : input
if (parsed.location._ === 'photo') { if (parsed.location._ === 'photo') {
return { return {

View file

@ -1,5 +1,6 @@
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { getPlatform } from '../../../platform.js'
import { MtArgumentError } from '../../../types/errors.js' import { MtArgumentError } from '../../../types/errors.js'
import { randomLong } from '../../../utils/long-utils.js' import { randomLong } from '../../../utils/long-utils.js'
import { ITelegramClient } from '../../client.types.js' import { ITelegramClient } from '../../client.types.js'
@ -7,7 +8,6 @@ import { UploadedFile, UploadFileLike } from '../../types/index.js'
import { guessFileMime } from '../../utils/file-type.js' import { guessFileMime } from '../../utils/file-type.js'
import { determinePartSize, isProbablyPlainText } from '../../utils/file-utils.js' import { determinePartSize, isProbablyPlainText } from '../../utils/file-utils.js'
import { bufferToStream, createChunkedReader, streamToBuffer } from '../../utils/stream-utils.js' import { bufferToStream, createChunkedReader, streamToBuffer } from '../../utils/stream-utils.js'
import { _createFileStream, _extractFileStreamMeta, _handleNodeStream, _isFileStream } from './_platform.js'
const OVERRIDE_MIME: Record<string, string> = { const OVERRIDE_MIME: Record<string, string> = {
// tg doesn't interpret `audio/opus` files as voice messages for some reason // tg doesn't interpret `audio/opus` files as voice messages for some reason
@ -22,6 +22,10 @@ const REQUESTS_PER_CONNECTION = 3
const MAX_PART_COUNT = 4000 // 512 kb * 4000 = 2000 MiB const MAX_PART_COUNT = 4000 // 512 kb * 4000 = 2000 MiB
const MAX_PART_COUNT_PREMIUM = 8000 // 512 kb * 8000 = 4000 MiB const MAX_PART_COUNT_PREMIUM = 8000 // 512 kb * 8000 = 4000 MiB
// platform-specific
const HAS_FILE = typeof File !== 'undefined'
const HAS_RESPONSE = typeof Response !== 'undefined'
// @available=both // @available=both
/** /**
* Upload a file to Telegram servers, without actually * Upload a file to Telegram servers, without actually
@ -37,9 +41,6 @@ export async function uploadFile(
params: { params: {
/** /**
* Upload file source. * 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 file: UploadFileLike
@ -105,29 +106,30 @@ export async function uploadFile(
let fileName = DEFAULT_FILE_NAME let fileName = DEFAULT_FILE_NAME
let fileMime = params.fileMime let fileMime = params.fileMime
const platform = getPlatform()
if (platform.normalizeFile) {
const res = await platform.normalizeFile(file)
if (res?.file) {
file = res.file
if (res.fileSize) fileSize = res.fileSize
if (res.fileName) fileName = res.fileName
}
}
if (ArrayBuffer.isView(file)) { if (ArrayBuffer.isView(file)) {
fileSize = file.length fileSize = file.length
file = bufferToStream(file) file = bufferToStream(file)
} }
if (typeof File !== 'undefined' && file instanceof File) { if (HAS_FILE && file instanceof File) {
fileName = file.name fileName = file.name
fileSize = file.size fileSize = file.size
// file is now ReadableStream
file = file.stream() file = file.stream()
} }
if (typeof file === 'string') { if (HAS_RESPONSE && file instanceof Response) {
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') const length = parseInt(file.headers.get('content-length') || '0')
if (!isNaN(length) && length) fileSize = length if (!isNaN(length) && length) fileSize = length
@ -161,8 +163,6 @@ export async function uploadFile(
file = file.body file = file.body
} }
file = _handleNodeStream(file)
if (!(file instanceof ReadableStream)) { if (!(file instanceof ReadableStream)) {
throw new MtArgumentError('Could not convert input `file` to stream!') throw new MtArgumentError('Could not convert input `file` to stream!')
} }

View file

@ -1,6 +1,6 @@
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { utf8EncodeToBuffer } from '@mtcute/tl-runtime'
import { getPlatform } from '../../../platform.js'
import { assertNever } from '../../../types/utils.js' import { assertNever } from '../../../types/utils.js'
import { toInputUser } from '../../utils/peer-utils.js' import { toInputUser } from '../../utils/peer-utils.js'
import { BotKeyboardBuilder } from './keyboard-builder.js' import { BotKeyboardBuilder } from './keyboard-builder.js'
@ -218,7 +218,7 @@ export namespace BotKeyboard {
_: 'keyboardButtonCallback', _: 'keyboardButtonCallback',
text, text,
requiresPassword, requiresPassword,
data: typeof data === 'string' ? utf8EncodeToBuffer(data) : data, data: typeof data === 'string' ? getPlatform().utf8Encode(data) : data,
} }
} }

View file

@ -1,6 +1,7 @@
import { tdFileId as td, toFileId, toUniqueFileId } from '@mtcute/file-id' import { tdFileId as td, toFileId, toUniqueFileId } from '@mtcute/file-id'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { getPlatform } from '../../../platform.js'
import { makeInspectable } from '../../utils/index.js' import { makeInspectable } from '../../utils/index.js'
import { memoizeGetters } from '../../utils/memoize.js' import { memoizeGetters } from '../../utils/memoize.js'
import { FileLocation } from '../files/index.js' import { FileLocation } from '../files/index.js'
@ -123,7 +124,7 @@ export abstract class RawDocument extends FileLocation {
* representing this document. * representing this document.
*/ */
get fileId(): string { get fileId(): string {
return toFileId({ return toFileId(getPlatform(), {
type: this._fileIdType(), type: this._fileIdType(),
dcId: this.raw.dcId, dcId: this.raw.dcId,
fileReference: this.raw.fileReference, fileReference: this.raw.fileReference,
@ -139,7 +140,7 @@ export abstract class RawDocument extends FileLocation {
* Get a unique File ID representing this document. * Get a unique File ID representing this document.
*/ */
get uniqueFileId(): string { get uniqueFileId(): string {
return toUniqueFileId(td.FileType.Document, { return toUniqueFileId(getPlatform(), td.FileType.Document, {
_: 'common', _: 'common',
id: this.raw.id, id: this.raw.id,
}) })

View file

@ -3,6 +3,7 @@ import Long from 'long'
import { tdFileId as td, toFileId, toUniqueFileId } from '@mtcute/file-id' import { tdFileId as td, toFileId, toUniqueFileId } from '@mtcute/file-id'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { getPlatform } from '../../../platform.js'
import { MtArgumentError, MtTypeAssertionError } from '../../../types/errors.js' import { MtArgumentError, MtTypeAssertionError } from '../../../types/errors.js'
import { assertTypeIs } from '../../../utils/type-assertions.js' import { assertTypeIs } from '../../../utils/type-assertions.js'
import { inflateSvgPath, strippedPhotoToJpg, svgPathToFile } from '../../utils/file-utils.js' import { inflateSvgPath, strippedPhotoToJpg, svgPathToFile } from '../../utils/file-utils.js'
@ -192,7 +193,7 @@ export class Thumbnail extends FileLocation {
} }
if (this._media._ === 'stickerSet') { if (this._media._ === 'stickerSet') {
return toFileId({ return toFileId(getPlatform(), {
type: td.FileType.Thumbnail, type: td.FileType.Thumbnail,
dcId: this.dcId!, dcId: this.dcId!,
fileReference: null, 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, type: this._media._ === 'photo' ? td.FileType.Photo : td.FileType.Thumbnail,
dcId: this.dcId!, dcId: this.dcId!,
fileReference: this._media.fileReference, fileReference: this._media.fileReference,
@ -239,7 +240,7 @@ export class Thumbnail extends FileLocation {
} }
if (this._media._ === 'stickerSet') { if (this._media._ === 'stickerSet') {
return toUniqueFileId(td.FileType.Thumbnail, { return toUniqueFileId(getPlatform(), td.FileType.Thumbnail, {
_: 'photo', _: 'photo',
id: Long.ZERO, id: Long.ZERO,
source: { 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', _: 'photo',
id: this._media.id, id: this._media.id,
source: { source: {

View file

@ -3,6 +3,7 @@ import Long from 'long'
import { tdFileId, toFileId, toUniqueFileId } from '@mtcute/file-id' import { tdFileId, toFileId, toUniqueFileId } from '@mtcute/file-id'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { getPlatform } from '../../../platform.js'
import { MtArgumentError } from '../../../types/errors.js' import { MtArgumentError } from '../../../types/errors.js'
import { toggleChannelIdMark } from '../../../utils/peer-utils.js' import { toggleChannelIdMark } from '../../../utils/peer-utils.js'
import { strippedPhotoToJpg } from '../../utils/file-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') throw new MtArgumentError('Input peer was invalid')
} }
return toFileId({ return toFileId(getPlatform(), {
dcId: this.obj.dcId, dcId: this.obj.dcId,
type: tdFileId.FileType.ProfilePhoto, type: tdFileId.FileType.ProfilePhoto,
fileReference: null, fileReference: null,
@ -84,7 +85,7 @@ export class ChatPhotoSize extends FileLocation {
* TDLib and Bot API compatible unique File ID representing this size * TDLib and Bot API compatible unique File ID representing this size
*/ */
get uniqueFileId(): string { get uniqueFileId(): string {
return toUniqueFileId(tdFileId.FileType.ProfilePhoto, { return toUniqueFileId(getPlatform(), tdFileId.FileType.ProfilePhoto, {
_: 'photo', _: 'photo',
id: this.obj.photoId, id: this.obj.photoId,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment

View file

@ -1,6 +1,6 @@
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { utf8Decode } from '@mtcute/tl-runtime'
import { getPlatform } from '../../../platform.js'
import { MtArgumentError } from '../../../types/errors.js' import { MtArgumentError } from '../../../types/errors.js'
import { makeInspectable } from '../../utils/index.js' import { makeInspectable } from '../../utils/index.js'
import { encodeInlineMessageId } from '../../utils/inline-utils.js' import { encodeInlineMessageId } from '../../utils/inline-utils.js'
@ -58,7 +58,7 @@ class BaseCallbackQuery {
get dataStr(): string | null { get dataStr(): string | null {
if (!this.raw.data) return null if (!this.raw.data) return null
return utf8Decode(this.raw.data) return getPlatform().utf8Decode(this.raw.data)
} }
/** /**

View file

@ -46,4 +46,4 @@ export class UserStatusUpdate {
} }
memoizeGetters(UserStatusUpdate, ['_parsedStatus' as keyof UserStatusUpdate]) memoizeGetters(UserStatusUpdate, ['_parsedStatus' as keyof UserStatusUpdate])
makeInspectable(UserStatusUpdate) makeInspectable(UserStatusUpdate, undefined, ['_parsedStatus' as keyof UserStatusUpdate])

View file

@ -6,6 +6,7 @@ import { tl } from '@mtcute/tl'
import { parseMarkedPeerId } from '../../utils/peer-utils.js' import { parseMarkedPeerId } from '../../utils/peer-utils.js'
import FileType = td.FileType import FileType = td.FileType
import { getPlatform } from '../../platform.js'
import { assertNever } from '../../types/utils.js' import { assertNever } from '../../types/utils.js'
const EMPTY_BUFFER = new Uint8Array(0) const EMPTY_BUFFER = new Uint8Array(0)
@ -45,7 +46,7 @@ function dialogPhotoToInputPeer(
* @param fileId File ID, either parsed or as a string * @param fileId File ID, either parsed or as a string
*/ */
export function fileIdToInputWebFileLocation(fileId: string | FileId): tl.RawInputWebFileLocation { 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') { if (fileId.location._ !== 'web') {
throw new td.ConversionError('inputWebFileLocation') 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 * @param fileId File ID, either parsed or as a string
*/ */
export function fileIdToInputFileLocation(fileId: string | FileId): tl.TypeInputFileLocation { 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 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 * @param fileId File ID, either parsed or as a string
*/ */
export function fileIdToInputDocument(fileId: string | FileId): tl.RawInputDocument { export function fileIdToInputDocument(fileId: string | FileId): tl.RawInputDocument {
if (typeof fileId === 'string') fileId = parseFileId(fileId) if (typeof fileId === 'string') fileId = parseFileId(getPlatform(), fileId)
if ( if (
fileId.location._ !== 'common' || 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 * @param fileId File ID, either parsed or as a string
*/ */
export function fileIdToInputPhoto(fileId: string | FileId): tl.RawInputPhoto { 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') { if (fileId.location._ !== 'photo') {
throw new td.ConversionError('inputPhoto') 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 * @param fileId File ID, either parsed or as a string
*/ */
export function fileIdToEncryptedFile(fileId: string | FileId): tl.RawInputEncryptedFile { 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) { if (fileId.location._ !== 'common' || fileId.type !== FileType.Encrypted) {
throw new td.ConversionError('inputEncryptedFile') 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 * @param fileId File ID, either parsed or as a string
*/ */
export function fileIdToSecureFile(fileId: string | FileId): tl.RawInputSecureFile { 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)) { if (fileId.location._ !== 'common' || (fileId.type !== FileType.Secure && fileId.type !== FileType.SecureRaw)) {
throw new td.ConversionError('inputSecureFile') throw new td.ConversionError('inputSecureFile')

View file

@ -1,9 +1,10 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { hexDecodeToBuffer } from '@mtcute/tl-runtime' import { getPlatform } from '../../platform.js'
import { guessFileMime } from './file-type.js' import { guessFileMime } from './file-type.js'
const p = getPlatform()
describe('guessFileMime', () => { describe('guessFileMime', () => {
it.each([ it.each([
['424d', 'image/bmp'], ['424d', 'image/bmp'],
@ -60,6 +61,6 @@ describe('guessFileMime', () => {
])('should detect %s as %s', (header, mime) => { ])('should detect %s as %s', (header, mime) => {
header += '00'.repeat(16) header += '00'.repeat(16)
expect(guessFileMime(hexDecodeToBuffer(header))).toEqual(mime) expect(guessFileMime(p.hexDecode(header))).toEqual(mime)
}) })
}) })

View file

@ -1,7 +1,6 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { hexDecodeToBuffer, hexEncode, utf8Decode, utf8EncodeToBuffer } from '@mtcute/tl-runtime' import { getPlatform } from '../../platform.js'
import { import {
extractFileName, extractFileName,
inflateSvgPath, inflateSvgPath,
@ -10,28 +9,30 @@ import {
svgPathToFile, svgPathToFile,
} from './file-utils.js' } from './file-utils.js'
const p = getPlatform()
describe('isProbablyPlainText', () => { describe('isProbablyPlainText', () => {
it('should return true for buffers only containing printable ascii', () => { it('should return true for buffers only containing printable ascii', () => {
expect(isProbablyPlainText(utf8EncodeToBuffer('hello this is some ascii text'))).to.be.true expect(isProbablyPlainText(p.utf8Encode('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(p.utf8Encode('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\r\nwith windows new lines'))).to.be
.true .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 .to.be.true
expect( expect(
isProbablyPlainText( 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 ).to.be.true
}) })
it('should return false for buffers containing some binary data', () => { it('should return false for buffers containing some binary data', () => {
expect(isProbablyPlainText(utf8EncodeToBuffer('hello this is cedilla: ç'))).to.be.false expect(isProbablyPlainText(p.utf8Encode('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 some ascii text with emojis 🌸'))).to.be.false
// random strings of 16 bytes // random strings of 16 bytes
expect(isProbablyPlainText(hexDecodeToBuffer('717f80f08eb9d88c3931712c0e2be32f'))).to.be.false expect(isProbablyPlainText(p.hexDecode('717f80f08eb9d88c3931712c0e2be32f'))).to.be.false
expect(isProbablyPlainText(hexDecodeToBuffer('20e8e218e54254c813b261432b0330d7'))).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', () => { 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' 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(
'"<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?><svg version=\\"1.1\\" xmlns=\\"http://www.w3.org/2000/svg\\" xmlns:xlink=\\"http://www.w3.org/1999/xlink\\"viewBox=\\"0 0 512 512\\" xml:space=\\"preserve\\"><path d=\\"M 0 0 L 100 0 L 100 100 L 0 100 L 0 0 Z\\"/></svg>"', '"<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?><svg version=\\"1.1\\" xmlns=\\"http://www.w3.org/2000/svg\\" xmlns:xlink=\\"http://www.w3.org/1999/xlink\\"viewBox=\\"0 0 512 512\\" xml:space=\\"preserve\\"><path d=\\"M 0 0 L 100 0 L 100 100 L 0 100 L 0 0 Z\\"/></svg>"',
) )
}) })
}) })
describe('inflateSvgPath', () => { describe('inflateSvgPath', () => {
const data = hexDecodeToBuffer( const data = p.hexDecode(
'1a05b302dc5f4446068649064247424a6a4c704550535b5e665e5e4c044a024c' + '1a05b302dc5f4446068649064247424a6a4c704550535b5e665e5e4c044a024c' +
'074e06414d80588863935fad74be4704854684518b528581904695498b488b56' + '074e06414d80588863935fad74be4704854684518b528581904695498b488b56' +
'965c85438d8191818543894a8f4d834188818a4284498454895d9a6f86074708' + '965c85438d8191818543894a8f4d834188818a4284498454895d9a6f86074708' +
@ -85,9 +86,9 @@ describe('inflateSvgPath', () => {
describe('strippedPhotoToJpg', () => { describe('strippedPhotoToJpg', () => {
// strippedThumb of @Channel_Bot // strippedThumb of @Channel_Bot
const dataPfp = hexDecodeToBuffer('010808b1f2f95fed673451457033ad1f') const dataPfp = p.hexDecode('010808b1f2f95fed673451457033ad1f')
// photoStrippedSize of a random image // photoStrippedSize of a random image
const dataPicture = hexDecodeToBuffer( const dataPicture = p.hexDecode(
'012728b532aacce4b302d8c1099c74a634718675cb6381f73d3ffd557667d9b5' + '012728b532aacce4b302d8c1099c74a634718675cb6381f73d3ffd557667d9b5' +
'816f4c28ce69aa58a863238cf62a334590f999042234cbe1986d03eefe14c68e' + '816f4c28ce69aa58a863238cf62a334590f999042234cbe1986d03eefe14c68e' +
'32847cc00ce709ea7ffad577773f78fe54d6c927f78c3db14ac1ccca91a2ef4f' + '32847cc00ce709ea7ffad577773f78fe54d6c927f78c3db14ac1ccca91a2ef4f' +
@ -99,7 +100,7 @@ describe('strippedPhotoToJpg', () => {
) )
it('should inflate stripped jpeg (from profile picture)', () => { it('should inflate stripped jpeg (from profile picture)', () => {
expect(hexEncode(strippedPhotoToJpg(dataPfp))).toMatchInlineSnapshot( expect(p.hexEncode(strippedPhotoToJpg(dataPfp))).toMatchInlineSnapshot(
'"ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e192' + '"ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e192' +
'82321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a' + '82321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a' +
'0aadaad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2' + '0aadaad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2' +
@ -124,7 +125,7 @@ describe('strippedPhotoToJpg', () => {
}) })
it('should inflate stripped jpeg (from a picture)', () => { it('should inflate stripped jpeg (from a picture)', () => {
expect(hexEncode(strippedPhotoToJpg(dataPicture))).toMatchInlineSnapshot( expect(p.hexEncode(strippedPhotoToJpg(dataPicture))).toMatchInlineSnapshot(
'"ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e192' + '"ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e192' +
'82321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a' + '82321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a' +
'0aadaad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2' + '0aadaad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2' +

View file

@ -1,5 +1,4 @@
import { hexDecodeToBuffer, utf8EncodeToBuffer } from '@mtcute/tl-runtime' import { getPlatform } from '../../platform.js'
import { MtArgumentError } from '../../types/errors.js' import { MtArgumentError } from '../../types/errors.js'
import { concatBuffers } from '../../utils/buffer-utils.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 // 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' + 'ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e1928' +
'2321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a0aad' + '2321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a0aad' +
'aad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2d2d3c35' + 'aad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2d2d3c35' +
@ -54,6 +53,7 @@ const JPEG_HEADER = hexDecodeToBuffer(
'b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f' + 'b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f' +
'3f4f5f6f7f8f9faffda000c03010002110311003f00', '3f4f5f6f7f8f9faffda000c03010002110311003f00',
) )
let JPEG_HEADER_BYTES: Uint8Array | null = null
const JPEG_FOOTER = new Uint8Array([0xff, 0xd9]) const JPEG_FOOTER = new Uint8Array([0xff, 0xd9])
/** /**
@ -64,7 +64,11 @@ export function strippedPhotoToJpg(stripped: Uint8Array): Uint8Array {
throw new MtArgumentError('Invalid stripped JPEG') 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[164] = stripped[1]
result[166] = stripped[2] result[166] = stripped[2]
@ -108,7 +112,7 @@ export function inflateSvgPath(encoded: Uint8Array): string {
* @param path * @param path
*/ */
export function svgPathToFile(path: string): Uint8Array { export function svgPathToFile(path: string): Uint8Array {
return utf8EncodeToBuffer( return getPlatform().utf8Encode(
'<?xml version="1.0" encoding="utf-8"?>' + '<?xml version="1.0" encoding="utf-8"?>' +
'<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"' + '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"' +
'viewBox="0 0 512 512" xml:space="preserve">' + 'viewBox="0 0 512 512" xml:space="preserve">' +

View file

@ -1,6 +1,7 @@
import { tl } from '@mtcute/tl' 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' import { assertNever } from '../../types/utils.js'
/** /**
@ -9,7 +10,7 @@ import { assertNever } from '../../types/utils.js'
* @param id Inline message ID * @param id Inline message ID
*/ */
export function parseInlineMessageId(id: string): tl.TypeInputBotInlineMessageID { export function parseInlineMessageId(id: string): tl.TypeInputBotInlineMessageID {
const buf = base64DecodeToBuffer(id, true) const buf = getPlatform().base64Decode(id, true)
const reader = TlBinaryReader.manual(buf) const reader = TlBinaryReader.manual(buf)
if (buf.length === 20) { if (buf.length === 20) {
@ -56,7 +57,7 @@ export function encodeInlineMessageId(id: tl.TypeInputBotInlineMessageID): strin
assertNever(id) assertNever(id)
} }
return base64Encode(writer.result(), true) return getPlatform().base64Encode(writer.result(), true)
} }
export function normalizeInlineId(id: string | tl.TypeInputBotInlineMessageID) { export function normalizeInlineId(id: string | tl.TypeInputBotInlineMessageID) {

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-argument */ /* 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') const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom')
@ -33,7 +33,11 @@ function getAllGettersNames<T>(obj: T): (keyof T)[] {
* > (getter that caches after its first invocation is also * > (getter that caches after its first invocation is also
* > considered pure in this case) * > considered pure in this case)
*/ */
export function makeInspectable<T>(obj: new (...args: any[]) => T, props?: (keyof T)[], hide?: (keyof T)[]): void { export function makeInspectable<T>(
obj: new (...args: any[]) => T,
props?: (keyof T)[],
hide?: (keyof T)[],
): typeof obj {
const getters: (keyof T)[] = props ? props : [] const getters: (keyof T)[] = props ? props : []
for (const key of getAllGettersNames<T>(obj.prototype)) { for (const key of getAllGettersNames<T>(obj.prototype)) {
@ -52,7 +56,7 @@ export function makeInspectable<T>(obj: new (...args: any[]) => T, props?: (keyo
if (val && typeof val === 'object') { if (val && typeof val === 'object') {
if (val instanceof Uint8Array) { if (val instanceof Uint8Array) {
val = base64Encode(val) val = getPlatform().base64Encode(val)
} else if (typeof val.toJSON === 'function') { } else if (typeof val.toJSON === 'function') {
val = val.toJSON(true) val = val.toJSON(true)
} }
@ -67,4 +71,6 @@ export function makeInspectable<T>(obj: new (...args: any[]) => T, props?: (keyo
return ret return ret
} }
obj.prototype[customInspectSymbol] = obj.prototype.toJSON obj.prototype[customInspectSymbol] = obj.prototype.toJSON
return obj
} }

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-assignment */
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function memoizeGetters<T>(cls: new (...args: any[]) => T, fields: (keyof T)[]) { export function memoizeGetters<T>(cls: new (...args: any[]) => T, fields: (keyof T)[]): typeof cls {
for (const field of fields) { for (const field of fields) {
const desc = Object.getOwnPropertyDescriptor(cls.prototype, field) const desc = Object.getOwnPropertyDescriptor(cls.prototype, field)
if (!desc) continue if (!desc) continue
@ -25,4 +25,6 @@ export function memoizeGetters<T>(cls: new (...args: any[]) => T, fields: (keyof
configurable: true, configurable: true,
}) })
} }
return cls
} }

View file

@ -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)')
}

View file

@ -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!')
}

View file

@ -1,7 +1,6 @@
import { Readable } from 'node:stream'
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { createChunkedReader, nodeReadableToWeb } from './stream-utils.js' import { createChunkedReader } from './stream-utils.js'
describe('createChunkedReader', () => { describe('createChunkedReader', () => {
it('should correctly handle chunks smaller than chunkSize', async () => { it('should correctly handle chunks smaller than chunkSize', async () => {
@ -82,26 +81,3 @@ describe('createChunkedReader', () => {
expect(await reader.read()).to.be.null expect(await reader.read()).to.be.null
}) })
}) })
if (import.meta.env.TEST_ENV === 'node' || import.meta.env.TEST_ENV === 'bun') {
describe('nodeReadableToWeb', () => {
it('should correctly convert a readable stream', async () => {
const stream = new Readable({
read() {
// eslint-disable-next-line no-restricted-globals
this.push(Buffer.from([1, 2, 3]))
// eslint-disable-next-line no-restricted-globals
this.push(Buffer.from([4, 5, 6]))
this.push(null)
},
})
const webStream = nodeReadableToWeb(stream)
const reader = webStream.getReader()
expect(await reader.read()).to.deep.equal({ value: new Uint8Array([1, 2, 3]), done: false })
expect(await reader.read()).to.deep.equal({ value: new Uint8Array([4, 5, 6]), done: false })
expect(await reader.read()).to.deep.equal({ value: undefined, done: true })
})
})
}

View file

@ -135,36 +135,3 @@ export function createChunkedReader(stream: ReadableStream<Uint8Array>, chunkSiz
read: readLocked, read: readLocked,
} }
} }
export function nodeReadableToWeb(stream: NodeJS.ReadableStream): ReadableStream {
// using .constructor here to avoid import hacks
const ctor = stream.constructor as {
toWeb?: (stream: NodeJS.ReadableStream) => ReadableStream
}
if (ctor.toWeb) {
// use `Readable.toWeb` if available
return ctor.toWeb(stream)
}
// otherwise, use a silly little adapter
stream.pause()
return new ReadableStream({
start(c) {
stream.on('data', (chunk) => {
c.enqueue(chunk)
})
stream.on('end', () => {
c.close()
})
stream.on('error', (err) => {
c.error(err)
})
},
pull() {
stream.resume()
},
})
}

View file

@ -1,6 +1,7 @@
import { tl } from '@mtcute/tl' 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 { MtArgumentError } from '../../types/index.js'
import { BasicDcOption, DcOptions, parseBasicDcOption, serializeBasicDcOption } from '../../utils/dcs.js' import { BasicDcOption, DcOptions, parseBasicDcOption, serializeBasicDcOption } from '../../utils/dcs.js'
import { CurrentUserInfo } from '../storage/service/current-user.js' import { CurrentUserInfo } from '../storage/service/current-user.js'
@ -53,11 +54,11 @@ export function writeStringSession(data: StringSessionData): string {
writer.bytes(data.authKey) writer.bytes(data.authKey)
return base64Encode(writer.result(), true) return getPlatform().base64Encode(writer.result(), true)
} }
export function readStringSession(readerMap: TlReaderMap, data: string): StringSessionData { export function readStringSession(readerMap: TlReaderMap, data: string): StringSessionData {
const buf = base64DecodeToBuffer(data, true) const buf = getPlatform().base64Decode(data, true)
const version = buf[0] const version = buf[0]

View file

@ -1,14 +1,15 @@
import { describe, expect, it } from 'vitest' 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' import { decodeWaveform, encodeWaveform } from './voice-utils.js'
const p = getPlatform()
describe('decodeWaveform', () => { describe('decodeWaveform', () => {
it('should correctly decode telegram-encoded waveform', () => { it('should correctly decode telegram-encoded waveform', () => {
expect( expect(
decodeWaveform( decodeWaveform(
hexDecodeToBuffer( p.hexDecode(
'0000104210428c310821a51463cc39072184524a4aa9b51663acb5e69c7bef41' + '0000104210428c310821a51463cc39072184524a4aa9b51663acb5e69c7bef41' +
'08618c514a39e7a494d65aadb5f75e8c31ce396badf7de9cf3debbf7feff0f', '08618c514a39e7a494d65aadb5f75e8c31ce396badf7de9cf3debbf7feff0f',
), ),
@ -25,7 +26,7 @@ describe('decodeWaveform', () => {
describe('encodeWaveform', () => { describe('encodeWaveform', () => {
it('should correctly decode telegram-encoded waveform', () => { it('should correctly decode telegram-encoded waveform', () => {
expect( expect(
hexEncode( p.hexEncode(
encodeWaveform([ 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, 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, 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,

View file

@ -0,0 +1,3 @@
export * from './port.js'
export * from './protocol.js'
export * from './worker.js'

View file

@ -1,20 +0,0 @@
import { Worker } from 'worker_threads'
import { ClientMessageHandler, SendFn, SomeWorker } from '../protocol.js'
export function connectToWorker(worker: SomeWorker, handler: ClientMessageHandler): [SendFn, () => void] {
if (!(worker instanceof Worker)) {
throw new Error('Only worker_threads are supported')
}
const send: SendFn = worker.postMessage.bind(worker)
worker.on('message', handler)
return [
send,
() => {
worker.off('message', handler)
},
]
}

View file

@ -1,61 +0,0 @@
import { beforeExit } from '../../../utils/platform/exit-hook.js'
import { ClientMessageHandler, SendFn, SomeWorker } from '../protocol.js'
export function connectToWorker(worker: SomeWorker, handler: ClientMessageHandler): [SendFn, () => void] {
if (worker instanceof Worker) {
const send: SendFn = worker.postMessage.bind(worker)
const messageHandler = (ev: MessageEvent) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
handler(ev.data)
}
worker.addEventListener('message', messageHandler)
return [
send,
() => {
worker.removeEventListener('message', messageHandler)
},
]
}
if (worker instanceof SharedWorker) {
const send: SendFn = worker.port.postMessage.bind(worker.port)
const pingInterval = setInterval(() => {
worker.port.postMessage({ __type__: 'ping' })
}, 10000)
const messageHandler = (ev: MessageEvent) => {
if (ev.data.__type__ === 'timeout') {
// we got disconnected from the worker due to timeout
// if the page is still alive (which is unlikely), we should reconnect
// however it's not really possible with SharedWorker API without re-creating the worker
// so we just reload the page for now
location.reload()
return
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
handler(ev.data)
}
worker.port.addEventListener('message', messageHandler)
worker.port.start()
const close = () => {
clearInterval(pingInterval)
worker.port.postMessage({ __type__: 'close' })
worker.port.removeEventListener('message', messageHandler)
worker.port.close()
}
beforeExit(close)
return [send, close]
}
throw new Error('Only workers and shared workers are supported')
}

View file

@ -1,23 +0,0 @@
import { parentPort } from 'worker_threads'
import { RespondFn, WorkerMessageHandler } from '../protocol.js'
const registered = false
export function registerWorker(handler: WorkerMessageHandler): RespondFn {
if (!parentPort) {
throw new Error('registerWorker() must be called from a worker thread')
}
if (registered) {
throw new Error('registerWorker() must be called only once')
}
const port = parentPort
const respond: RespondFn = port.postMessage.bind(port)
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
parentPort.on('message', (message) => handler(message, respond))
return respond
}

View file

@ -1,80 +0,0 @@
import { RespondFn, WorkerMessageHandler } from '../protocol.js'
const registered = false
export function registerWorker(handler: WorkerMessageHandler): RespondFn {
if (registered) {
throw new Error('registerWorker() must be called only once')
}
if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) {
const respond: RespondFn = self.postMessage.bind(self)
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
self.addEventListener('message', (message) => handler(message.data, respond))
return respond
}
if (typeof SharedWorkerGlobalScope !== 'undefined' && self instanceof SharedWorkerGlobalScope) {
const connections: MessagePort[] = []
const broadcast = (message: unknown) => {
for (const port of connections) {
port.postMessage(message)
}
}
self.onconnect = (event) => {
const port = event.ports[0]
connections.push(port)
const respond = port.postMessage.bind(port)
// not very reliable, but better than nothing
// SharedWorker API doesn't provide a way to detect when the client closes the connection
// so we just assume that the client is done when it sends a 'close' message
// and keep a timeout for the case when the client closes without sending a 'close' message
const onClose = () => {
port.close()
const idx = connections.indexOf(port)
if (idx >= 0) {
connections.splice(connections.indexOf(port), 1)
}
}
const onTimeout = () => {
console.warn('some connection timed out!')
respond({ __type__: 'timeout' })
onClose()
}
// 60s should be a reasonable timeout considering that the client should send a ping every 10s
// so even if the browser has suspended the timers, we should still get a ping within a minute
let timeout = setTimeout(onTimeout, 60000)
port.addEventListener('message', (message) => {
if (message.data.__type__ === 'close') {
onClose()
return
}
if (message.data.__type__ === 'ping') {
clearTimeout(timeout)
timeout = setTimeout(onTimeout, 60000)
return
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
handler(message.data, respond)
})
}
return broadcast
}
throw new Error('registerWorker() must be called from a worker')
}

View file

@ -6,17 +6,18 @@ import { PeersIndex } from '../types/peers/peers-index.js'
import { RawUpdateHandler } from '../updates/types.js' import { RawUpdateHandler } from '../updates/types.js'
import { AppConfigManagerProxy } from './app-config.js' import { AppConfigManagerProxy } from './app-config.js'
import { WorkerInvoker } from './invoker.js' import { WorkerInvoker } from './invoker.js'
import { connectToWorker } from './platform/connect.js' import { ClientMessageHandler, SendFn, SomeWorker, WorkerCustomMethods } from './protocol.js'
import { ClientMessageHandler, SomeWorker, WorkerCustomMethods } from './protocol.js'
import { TelegramStorageProxy } from './storage.js' import { TelegramStorageProxy } from './storage.js'
export interface TelegramWorkerPortOptions { export interface TelegramWorkerPortOptions {
worker: SomeWorker worker: SomeWorker
} }
export class TelegramWorkerPort<Custom extends WorkerCustomMethods> implements ITelegramClient { export abstract class TelegramWorkerPort<Custom extends WorkerCustomMethods> implements ITelegramClient {
constructor(readonly options: TelegramWorkerPortOptions) {} constructor(readonly options: TelegramWorkerPortOptions) {}
abstract connectToWorker(worker: SomeWorker, handler: ClientMessageHandler): [SendFn, () => void]
readonly log = new LogManager('worker') readonly log = new LogManager('worker')
private _serverUpdatesHandler: (updates: tl.TypeUpdates) => void = () => {} private _serverUpdatesHandler: (updates: tl.TypeUpdates) => void = () => {}
@ -60,7 +61,7 @@ export class TelegramWorkerPort<Custom extends WorkerCustomMethods> implements I
} }
} }
private _connection = connectToWorker(this.options.worker, this._onMessage) private _connection = this.connectToWorker(this.options.worker, this._onMessage)
private _invoker = new WorkerInvoker(this._connection[0]) private _invoker = new WorkerInvoker(this._connection[0])
private _bind = this._invoker.makeBinder<ITelegramClient>('client') private _bind = this._invoker.makeBinder<ITelegramClient>('client')

View file

@ -46,4 +46,5 @@ export type ClientMessageHandler = (message: WorkerOutboundMessage) => void
export type RespondFn = (message: WorkerOutboundMessage) => void export type RespondFn = (message: WorkerOutboundMessage) => void
export type WorkerMessageHandler = (message: WorkerInboundMessage, respond: RespondFn) => void export type WorkerMessageHandler = (message: WorkerInboundMessage, respond: RespondFn) => void
export type WorkerCustomMethods = Record<string, (...args: unknown[]) => Promise<unknown>> // eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WorkerCustomMethods = Record<string, (...args: any[]) => Promise<any>>

View file

@ -1,40 +1,88 @@
import { BaseTelegramClient, BaseTelegramClientOptions } from '../base.js' import { BaseTelegramClient } from '../base.js'
import { serializeError } from './errors.js' import { serializeError } from './errors.js'
import { registerWorker } from './platform/register.js'
import { RespondFn, WorkerCustomMethods, WorkerInboundMessage, WorkerMessageHandler } from './protocol.js' import { RespondFn, WorkerCustomMethods, WorkerInboundMessage, WorkerMessageHandler } from './protocol.js'
export interface TelegramWorkerOptions<T extends WorkerCustomMethods> { export interface TelegramWorkerOptions<T extends WorkerCustomMethods> {
client: BaseTelegramClient | BaseTelegramClientOptions client: BaseTelegramClient
customMethods?: T customMethods?: T
} }
export function makeTelegramWorker<T extends WorkerCustomMethods>(params: TelegramWorkerOptions<T>) { export abstract class TelegramWorker<T extends WorkerCustomMethods> {
const { client: client_, customMethods } = params readonly client: BaseTelegramClient
readonly broadcast: RespondFn
const client = client_ instanceof BaseTelegramClient ? client_ : new BaseTelegramClient(client_) abstract registerWorker(handler: WorkerMessageHandler): RespondFn
const onInvoke = (msg: Extract<WorkerInboundMessage, { type: 'invoke' }>, respond: RespondFn) => { constructor(readonly params: TelegramWorkerOptions<T>) {
this.broadcast = this.registerWorker((message, respond) => {
switch (message.type) {
case 'invoke':
this.onInvoke(message, respond)
break
}
})
const client = params.client
this.client = client
client.log.mgr.handler = (color, level, tag, fmt, args) =>
this.broadcast({
type: 'log',
color,
level,
tag,
fmt,
args,
})
client.onError((err) =>
this.broadcast({
type: 'error',
error: err,
}),
)
if (client.updates) {
client.onUpdate((update, peers) =>
this.broadcast({
type: 'update',
update,
users: peers.users,
chats: peers.chats,
hasMin: peers.hasMin,
}),
)
} else {
client.onServerUpdate((update) =>
this.broadcast({
type: 'server_update',
update,
}),
)
}
}
private onInvoke(msg: Extract<WorkerInboundMessage, { type: 'invoke' }>, respond: RespondFn) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
let target: any let target: any
switch (msg.target) { switch (msg.target) {
case 'custom': case 'custom':
target = customMethods target = this.params.customMethods
break break
case 'client': case 'client':
target = client target = this.client
break break
case 'storage': case 'storage':
target = client.storage target = this.client.storage
break break
case 'storage-self': case 'storage-self':
target = client.storage.self target = this.client.storage.self
break break
case 'storage-peers': case 'storage-peers':
target = client.storage.peers target = this.client.storage.peers
break break
case 'app-config': case 'app-config':
target = client.appConfig target = this.client.appConfig
break break
default: { default: {
@ -80,49 +128,4 @@ export function makeTelegramWorker<T extends WorkerCustomMethods>(params: Telegr
}) })
}) })
} }
const onMessage: WorkerMessageHandler = (message, respond) => {
switch (message.type) {
case 'invoke':
onInvoke(message, respond)
break
}
}
const broadcast = registerWorker(onMessage)
client.log.mgr.handler = (color, level, tag, fmt, args) =>
broadcast({
type: 'log',
color,
level,
tag,
fmt,
args,
})
client.onError((err) =>
broadcast({
type: 'error',
error: err,
}),
)
if (client.updates) {
client.onUpdate((update, peers) =>
broadcast({
type: 'update',
update,
users: peers.users,
chats: peers.chats,
hasMin: peers.hasMin,
}),
)
} else {
client.onServerUpdate((update) =>
broadcast({
type: 'server_update',
update,
}),
)
}
} }

View file

@ -3,22 +3,19 @@ import { describe, expect, it, vi } from 'vitest'
import { defaultTestCryptoProvider } from '@mtcute/test' import { defaultTestCryptoProvider } from '@mtcute/test'
import { import {
hexDecode,
hexDecodeToBuffer,
hexEncode,
TlBinaryReader, TlBinaryReader,
TlReaderMap, TlReaderMap,
utf8Decode,
utf8EncodeToBuffer,
} from '@mtcute/tl-runtime' } from '@mtcute/tl-runtime'
import { getPlatform } from '../platform.js'
import { LogManager } from '../utils/index.js' import { LogManager } from '../utils/index.js'
import { AuthKey } from './auth-key.js' import { AuthKey } from './auth-key.js'
const authKey = new Uint8Array(256) const authKey = new Uint8Array(256)
const p = getPlatform()
for (let i = 0; i < 256; i += 32) { 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', () => { describe('AuthKey', () => {
@ -54,19 +51,19 @@ describe('AuthKey', () => {
it('should calculate derivatives', async () => { it('should calculate derivatives', async () => {
const key = await create() const key = await create()
expect(hexEncode(key.key)).toEqual(hexEncode(authKey)) expect(p.hexEncode(key.key)).toEqual(p.hexEncode(authKey))
expect(hexEncode(key.clientSalt)).toEqual('f73c3622dec230e098cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4b') expect(p.hexEncode(key.clientSalt)).toEqual('f73c3622dec230e098cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4b')
expect(hexEncode(key.serverSalt)).toEqual('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0') expect(p.hexEncode(key.serverSalt)).toEqual('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0')
expect(hexEncode(key.id)).toEqual('40fa5bb7cb56a895') expect(p.hexEncode(key.id)).toEqual('40fa5bb7cb56a895')
}) })
it('should encrypt a message', async () => { it('should encrypt a message', async () => {
const message = writeMessage(utf8EncodeToBuffer('hello, world!!!!')) const message = writeMessage(p.utf8Encode('hello, world!!!!'))
const key = await create() const key = await create()
const msg = key.encryptMessage(message, serverSalt, sessionId) const msg = key.encryptMessage(message, serverSalt, sessionId)
expect(hexEncode(msg)).toEqual( expect(p.hexEncode(msg)).toEqual(
'40fa5bb7cb56a895f6f5a88914892aadf87c68031cc953ba29d68e118021f329' + '40fa5bb7cb56a895f6f5a88914892aadf87c68031cc953ba29d68e118021f329' +
'be386a620d49f3ad3a50c60dcef3733f214e8cefa3e403c11d193637d4971dc1' + 'be386a620d49f3ad3a50c60dcef3733f214e8cefa3e403c11d193637d4971dc1' +
'5db7f74b26fd16cb0e8fee30bf7e3f68858fe82927e2cd06', '5db7f74b26fd16cb0e8fee30bf7e3f68858fe82927e2cd06',
@ -88,7 +85,7 @@ describe('AuthKey', () => {
} }
it('should decrypt a message', async () => { it('should decrypt a message', async () => {
const message = hexDecodeToBuffer( const message = p.hexDecode(
'40fa5bb7cb56a8950c394b884f1529efc42fea22d972fea650a714ce6d2d1bdb' + '40fa5bb7cb56a8950c394b884f1529efc42fea22d972fea650a714ce6d2d1bdb' +
'3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b' + '3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b' +
'07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35', '07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35',
@ -98,11 +95,11 @@ describe('AuthKey', () => {
expect(decMsgId).toEqual(msgId) expect(decMsgId).toEqual(msgId)
expect(decSeqNo).toEqual(seqNo) 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 () => { it('should decrypt a message with padding', async () => {
const message = hexDecodeToBuffer( const message = p.hexDecode(
'40fa5bb7cb56a8950c394b884f1529efc42fea22d972fea650a714ce6d2d1bdb' + '40fa5bb7cb56a8950c394b884f1529efc42fea22d972fea650a714ce6d2d1bdb' +
'3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b' + '3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b' +
'07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35' + '07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35' +
@ -113,11 +110,11 @@ describe('AuthKey', () => {
expect(decMsgId).toEqual(msgId) expect(decMsgId).toEqual(msgId)
expect(decSeqNo).toEqual(seqNo) 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 () => { it('should ignore messages with invalid message key', async () => {
const message = hexDecodeToBuffer( const message = p.hexDecode(
'40fa5bb7cb56a8950000000000000000000000000000000050a714ce6d2d1bdb' + '40fa5bb7cb56a8950000000000000000000000000000000050a714ce6d2d1bdb' +
'3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b' + '3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b' +
'07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35', '07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35',
@ -127,7 +124,7 @@ describe('AuthKey', () => {
}) })
it('should ignore messages with invalid session_id', async () => { it('should ignore messages with invalid session_id', async () => {
const message = hexDecodeToBuffer( const message = p.hexDecode(
'40fa5bb7cb56a895a986a7e97f4e90aa2769b5e702c6e86f5e1e82c6ff0c6829' + '40fa5bb7cb56a895a986a7e97f4e90aa2769b5e702c6e86f5e1e82c6ff0c6829' +
'2521a2ba9704fa37fb341d895cf32662c6cf47ba31cbf27c30d5c03f6c2930f4' + '2521a2ba9704fa37fb341d895cf32662c6cf47ba31cbf27c30d5c03f6c2930f4' +
'30fd8858b836b73fe32d4a95b8ebcdbc9ca8908f7964c40a', '30fd8858b836b73fe32d4a95b8ebcdbc9ca8908f7964c40a',
@ -137,12 +134,12 @@ describe('AuthKey', () => {
}) })
it('should ignore messages with invalid length', async () => { it('should ignore messages with invalid length', async () => {
const messageTooLong = hexDecodeToBuffer( const messageTooLong = p.hexDecode(
'40fa5bb7cb56a8950d19412233dd5d24be697c73274e08fbe515cf65e0c5f70c' + '40fa5bb7cb56a8950d19412233dd5d24be697c73274e08fbe515cf65e0c5f70c' +
'ad75fd2badc18c9f999f287351144eeb1cfcaa9bea33ef5058999ad96a498306' + 'ad75fd2badc18c9f999f287351144eeb1cfcaa9bea33ef5058999ad96a498306' +
'08d2859425685a55b21fab413bfabc42ec5da283853b28c0', '08d2859425685a55b21fab413bfabc42ec5da283853b28c0',
) )
const messageUnaligned = hexDecodeToBuffer( const messageUnaligned = p.hexDecode(
'40fa5bb7cb56a8957b4e4bec561eee4a5a1025bc8a35d3d0c79a3685d2b90ff0' + '40fa5bb7cb56a8957b4e4bec561eee4a5a1025bc8a35d3d0c79a3685d2b90ff0' +
'5f638e9c42c9fd9448b0ce8e7d49e7ea1ce458e47b825b5c7fd8ddf5b4fded46' + '5f638e9c42c9fd9448b0ce8e7d49e7ea1ce458e47b825b5c7fd8ddf5b4fded46' +
'2a4bcc02f3ff2e89de6764d6d219f575e457fdcf8c163cdf', '2a4bcc02f3ff2e89de6764d6d219f575e457fdcf8c163cdf',
@ -155,7 +152,7 @@ describe('AuthKey', () => {
}) })
it('should ignore messages with invalid padding', async () => { it('should ignore messages with invalid padding', async () => {
const message = hexDecodeToBuffer( const message = p.hexDecode(
'40fa5bb7cb56a895133671d1c637a9836e2c64b4d1a0521d8a25a6416fd4dc9e' + '40fa5bb7cb56a895133671d1c637a9836e2c64b4d1a0521d8a25a6416fd4dc9e' +
'79f9478fb837703cc9efa0a19d12143c2a26e57cb4bc64d7bc972dd8f19c53c590cc258162f44afc', '79f9478fb837703cc9efa0a19d12143c2a26e57cb4bc64d7bc972dd8f19c53c590cc258162f44afc',
) )

View file

@ -11,9 +11,7 @@ import { StorageManager, StorageManagerExtraOptions } from '../storage/storage.j
import { MustEqual } from '../types/index.js' import { MustEqual } from '../types/index.js'
import { import {
asyncResettable, asyncResettable,
CryptoProviderFactory,
DcOptions, DcOptions,
defaultCryptoProviderFactory,
defaultProductionDc, defaultProductionDc,
defaultProductionIpv6Dc, defaultProductionIpv6Dc,
defaultTestDc, defaultTestDc,
@ -49,10 +47,10 @@ export interface MtClientOptions {
storageOptions?: StorageManagerExtraOptions storageOptions?: StorageManagerExtraOptions
/** /**
* Cryptography provider factory to allow delegating * Cryptography provider to allow delegating
* crypto to native addon, worker, etc. * crypto to native addon, worker, etc.
*/ */
crypto?: CryptoProviderFactory crypto: ICryptoProvider
/** /**
* Whether to use IPv6 datacenters * Whether to use IPv6 datacenters
@ -96,7 +94,7 @@ export interface MtClientOptions {
* *
* @default platform-specific transport: WebSocket on the web, TCP in node * @default platform-specific transport: WebSocket on the web, TCP in node
*/ */
transport?: TransportFactory transport: TransportFactory
/** /**
* Reconnection strategy. * Reconnection strategy.
@ -254,7 +252,7 @@ export class MtClient extends EventEmitter {
this.log.mgr.level = params.logLevel this.log.mgr.level = params.logLevel
} }
this.crypto = (params.crypto ?? defaultCryptoProviderFactory)() this.crypto = params.crypto
this._testMode = Boolean(params.testMode) this._testMode = Boolean(params.testMode)
let dc = params.defaultDcs let dc = params.defaultDcs

View file

@ -1,6 +1,7 @@
import { mtp, tl } from '@mtcute/tl' import { mtp, tl } from '@mtcute/tl'
import { TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime' import { TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime'
import { getPlatform } from '../platform.js'
import { StorageManager } from '../storage/storage.js' import { StorageManager } from '../storage/storage.js'
import { MtArgumentError, MtcuteError, MtTimeoutError, MtUnsupportedError } from '../types/index.js' import { MtArgumentError, MtcuteError, MtTimeoutError, MtUnsupportedError } from '../types/index.js'
import { import {
@ -18,7 +19,7 @@ import { PersistentConnectionParams } from './persistent-connection.js'
import { defaultReconnectionStrategy, ReconnectionStrategy } from './reconnection.js' import { defaultReconnectionStrategy, ReconnectionStrategy } from './reconnection.js'
import { ServerSaltManager } from './server-salt.js' import { ServerSaltManager } from './server-salt.js'
import { SessionConnection, SessionConnectionParams } from './session-connection.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' export type ConnectionKind = 'main' | 'upload' | 'download' | 'downloadSmall'
@ -44,7 +45,7 @@ export interface NetworkManagerParams {
enableErrorReporting: boolean enableErrorReporting: boolean
apiId: number apiId: number
initConnectionOptions?: Partial<Omit<tl.RawInitConnectionRequest, 'apiId' | 'query'>> initConnectionOptions?: Partial<Omit<tl.RawInitConnectionRequest, 'apiId' | 'query'>>
transport?: TransportFactory transport: TransportFactory
reconnectionStrategy?: ReconnectionStrategy<PersistentConnectionParams> reconnectionStrategy?: ReconnectionStrategy<PersistentConnectionParams>
floodSleepThreshold: number floodSleepThreshold: number
maxRetryCount: number maxRetryCount: number
@ -439,15 +440,7 @@ export class NetworkManager {
readonly params: NetworkManagerParams & NetworkManagerExtraParams, readonly params: NetworkManagerParams & NetworkManagerExtraParams,
readonly config: ConfigManager, readonly config: ConfigManager,
) { ) {
let deviceModel = 'mtcute on ' const deviceModel = `mtcute on ${getPlatform().getDeviceModel()}`
/* 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'
this._initConnectionParams = { this._initConnectionParams = {
_: 'initConnection', _: 'initConnection',
@ -463,7 +456,7 @@ export class NetworkManager {
query: null as any, query: null as any,
} }
this._transportFactory = params.transport ?? defaultTransportFactory this._transportFactory = params.transport
this._reconnectionStrategy = params.reconnectionStrategy ?? defaultReconnectionStrategy this._reconnectionStrategy = params.reconnectionStrategy ?? defaultReconnectionStrategy
this._connectionCount = params.connectionCount ?? defaultConnectionCountDelegate this._connectionCount = params.connectionCount ?? defaultConnectionCountDelegate
this._updateHandler = params.onUpdate this._updateHandler = params.onUpdate

View file

@ -7,6 +7,7 @@ import { TlBinaryReader, TlBinaryWriter, TlReaderMap, TlSerializationCounter, Tl
import { MtArgumentError, MtcuteError, MtTimeoutError } from '../types/index.js' import { MtArgumentError, MtcuteError, MtTimeoutError } from '../types/index.js'
import { createAesIgeForMessageOld } from '../utils/crypto/mtproto.js' import { createAesIgeForMessageOld } from '../utils/crypto/mtproto.js'
import { reportUnknownError } from '../utils/error-reporting.js'
import { import {
concatBuffers, concatBuffers,
ControllablePromise, ControllablePromise,
@ -17,7 +18,6 @@ import {
randomLong, randomLong,
removeFromLongArray, removeFromLongArray,
} from '../utils/index.js' } from '../utils/index.js'
import { reportUnknownError } from '../utils/platform/error-reporting.js'
import { doAuthorization } from './authorization.js' import { doAuthorization } from './authorization.js'
import { MtprotoSession, PendingMessage, PendingRpc } from './mtproto-session.js' import { MtprotoSession, PendingMessage, PendingRpc } from './mtproto-session.js'
import { PersistentConnection, PersistentConnectionParams } from './persistent-connection.js' import { PersistentConnection, PersistentConnectionParams } from './persistent-connection.js'

View file

@ -1,12 +1,5 @@
import { TransportFactory } from './abstract.js'
export * from './abstract.js' export * from './abstract.js'
export * from './intermediate.js' export * from './intermediate.js'
export * from './obfuscated.js' export * from './obfuscated.js'
export * from './streamed.js' export * from './streamed.js'
export * from './wrapped.js' export * from './wrapped.js'
import { _defaultTransportFactory } from '../../utils/platform/transport.js'
/** Platform-defined default transport factory */
export const defaultTransportFactory: TransportFactory = _defaultTransportFactory

View file

@ -1,13 +1,15 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { defaultTestCryptoProvider, useFakeMathRandom } from '@mtcute/test' import { defaultTestCryptoProvider, useFakeMathRandom } from '@mtcute/test'
import { hexDecodeToBuffer, hexEncode } from '@mtcute/tl-runtime'
import { IntermediatePacketCodec, PaddedIntermediatePacketCodec, TransportError } from '../../index.js' import { IntermediatePacketCodec, PaddedIntermediatePacketCodec, TransportError } from '../../index.js'
import { getPlatform } from '../../platform.js'
const p = getPlatform()
describe('IntermediatePacketCodec', () => { describe('IntermediatePacketCodec', () => {
it('should return correct tag', () => { 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', () => it('should correctly parse immediate framing', () =>
@ -17,7 +19,7 @@ describe('IntermediatePacketCodec', () => {
expect([...data]).eql([5, 1, 2, 3, 4]) expect([...data]).eql([5, 1, 2, 3, 4])
done() done()
}) })
codec.feed(hexDecodeToBuffer('050000000501020304')) codec.feed(p.hexDecode('050000000501020304'))
})) }))
it('should correctly parse incomplete framing', () => it('should correctly parse incomplete framing', () =>
@ -27,8 +29,8 @@ describe('IntermediatePacketCodec', () => {
expect([...data]).eql([5, 1, 2, 3, 4]) expect([...data]).eql([5, 1, 2, 3, 4])
done() done()
}) })
codec.feed(hexDecodeToBuffer('050000000501')) codec.feed(p.hexDecode('050000000501'))
codec.feed(hexDecodeToBuffer('020304')) codec.feed(p.hexDecode('020304'))
})) }))
it('should correctly parse multiple streamed packets', () => it('should correctly parse multiple streamed packets', () =>
@ -46,9 +48,9 @@ describe('IntermediatePacketCodec', () => {
done() done()
} }
}) })
codec.feed(hexDecodeToBuffer('050000000501')) codec.feed(p.hexDecode('050000000501'))
codec.feed(hexDecodeToBuffer('020304050000')) codec.feed(p.hexDecode('020304050000'))
codec.feed(hexDecodeToBuffer('000301020301')) codec.feed(p.hexDecode('000301020301'))
})) }))
it('should correctly parse transport errors', () => it('should correctly parse transport errors', () =>
@ -61,7 +63,7 @@ describe('IntermediatePacketCodec', () => {
done() done()
}) })
codec.feed(hexDecodeToBuffer('040000006cfeffff')) codec.feed(p.hexDecode('040000006cfeffff'))
})) }))
it('should reset when called reset()', () => it('should reset when called reset()', () =>
@ -73,15 +75,15 @@ describe('IntermediatePacketCodec', () => {
done() done()
}) })
codec.feed(hexDecodeToBuffer('ff0000001234567812345678')) codec.feed(p.hexDecode('ff0000001234567812345678'))
codec.reset() codec.reset()
codec.feed(hexDecodeToBuffer('050000000102030405')) codec.feed(p.hexDecode('050000000102030405'))
})) }))
it('should correctly frame packets', () => { 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 () => { 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 () => { 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')
}) })
}) })

View file

@ -2,10 +2,13 @@ import { describe, expect, it, vi } from 'vitest'
import { defaultTestCryptoProvider, u8HexDecode } from '@mtcute/test' 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 { IntermediatePacketCodec } from './intermediate.js'
import { MtProxyInfo, ObfuscatedPacketCodec } from './obfuscated.js' import { MtProxyInfo, ObfuscatedPacketCodec } from './obfuscated.js'
const p = getPlatform()
describe('ObfuscatedPacketCodec', () => { describe('ObfuscatedPacketCodec', () => {
const create = async (randomSource?: string, proxy?: MtProxyInfo) => { const create = async (randomSource?: string, proxy?: MtProxyInfo) => {
const codec = new ObfuscatedPacketCodec(new IntermediatePacketCodec(), proxy) const codec = new ObfuscatedPacketCodec(new IntermediatePacketCodec(), proxy)
@ -22,7 +25,7 @@ describe('ObfuscatedPacketCodec', () => {
const tag = await codec.tag() const tag = await codec.tag()
expect(hexEncode(tag)).toEqual( expect(p.hexEncode(tag)).toEqual(
'ff'.repeat(56) + 'fce8ab2203db2bff', // encrypted part 'ff'.repeat(56) + 'fce8ab2203db2bff', // encrypted part
) )
}) })
@ -40,7 +43,7 @@ describe('ObfuscatedPacketCodec', () => {
const tag = await codec.tag() const tag = await codec.tag()
expect(hexEncode(tag)).toEqual( expect(p.hexEncode(tag)).toEqual(
'ff'.repeat(56) + 'ecec4cbda8bb188b', // encrypted part with dcId = 1 'ff'.repeat(56) + 'ecec4cbda8bb188b', // encrypted part with dcId = 1
) )
}) })
@ -57,7 +60,7 @@ describe('ObfuscatedPacketCodec', () => {
const tag = await codec.tag() const tag = await codec.tag()
expect(hexEncode(tag)).toEqual( expect(p.hexEncode(tag)).toEqual(
'ff'.repeat(56) + 'ecec4cbdb89c188b', // encrypted part with dcId = 10001 'ff'.repeat(56) + 'ecec4cbdb89c188b', // encrypted part with dcId = 10001
) )
}) })
@ -74,7 +77,7 @@ describe('ObfuscatedPacketCodec', () => {
const tag = await codec.tag() const tag = await codec.tag()
expect(hexEncode(tag)).toEqual( expect(p.hexEncode(tag)).toEqual(
'ff'.repeat(56) + 'ecec4cbd5644188b', // encrypted part with dcId = -1 'ff'.repeat(56) + 'ecec4cbd5644188b', // encrypted part with dcId = -1
) )
}) })
@ -124,7 +127,7 @@ describe('ObfuscatedPacketCodec', () => {
it('should correctly create aes ctr for mtproxy', async () => { it('should correctly create aes ctr for mtproxy', async () => {
const proxy: MtProxyInfo = { const proxy: MtProxyInfo = {
dcId: 1, dcId: 1,
secret: hexDecodeToBuffer('00112233445566778899aabbccddeeff'), secret: p.hexDecode('00112233445566778899aabbccddeeff'),
test: true, test: true,
media: false, media: false,
} }
@ -137,20 +140,20 @@ describe('ObfuscatedPacketCodec', () => {
expect(spyCreateAesCtr).toHaveBeenCalledTimes(2) expect(spyCreateAesCtr).toHaveBeenCalledTimes(2)
expect(spyCreateAesCtr).toHaveBeenNthCalledWith( expect(spyCreateAesCtr).toHaveBeenNthCalledWith(
1, 1,
hexDecodeToBuffer('dd03188944590983e28dad14d97d0952389d118af4ffcbdb28d56a6a612ef7a6'), p.hexDecode('dd03188944590983e28dad14d97d0952389d118af4ffcbdb28d56a6a612ef7a6'),
u8HexDecode('936b33fa7f97bae025102532233abb26'), u8HexDecode('936b33fa7f97bae025102532233abb26'),
true, true,
) )
expect(spyCreateAesCtr).toHaveBeenNthCalledWith( expect(spyCreateAesCtr).toHaveBeenNthCalledWith(
2, 2,
hexDecodeToBuffer('413b8e08021fbb08a2962b6d7187194fe46565c6b329d3bbdfcffd4870c16119'), p.hexDecode('413b8e08021fbb08a2962b6d7187194fe46565c6b329d3bbdfcffd4870c16119'),
u8HexDecode('db6aeee6883f45f95def566dadb4b610'), u8HexDecode('db6aeee6883f45f95def566dadb4b610'),
false, false,
) )
}) })
it('should correctly encrypt the underlying codec', async () => { it('should correctly encrypt the underlying codec', async () => {
const data = hexDecodeToBuffer('6cfeffff') const data = p.hexDecode('6cfeffff')
const msg1 = 'a1020630a410e940' const msg1 = 'a1020630a410e940'
const msg2 = 'f53ff53f371db495' const msg2 = 'f53ff53f371db495'
@ -158,8 +161,8 @@ describe('ObfuscatedPacketCodec', () => {
await codec.tag() await codec.tag()
expect(hexEncode(await codec.encode(data))).toEqual(msg1) expect(p.hexEncode(await codec.encode(data))).toEqual(msg1)
expect(hexEncode(await codec.encode(data))).toEqual(msg2) expect(p.hexEncode(await codec.encode(data))).toEqual(msg2)
}) })
it('should correctly decrypt the underlying codec', async () => { it('should correctly decrypt the underlying codec', async () => {
@ -176,8 +179,8 @@ describe('ObfuscatedPacketCodec', () => {
log.push(e.toString()) log.push(e.toString())
}) })
codec.feed(hexDecodeToBuffer(msg1)) codec.feed(p.hexDecode(msg1))
codec.feed(hexDecodeToBuffer(msg2)) codec.feed(p.hexDecode(msg2))
await vi.waitFor(() => expect(log).toEqual(['Error: Transport error: 404', 'Error: Transport error: 404'])) await vi.waitFor(() => expect(log).toEqual(['Error: Transport error: 404', 'Error: Transport error: 404']))
}) })

View file

@ -0,0 +1,54 @@
import { ITlPlatform, TlBinaryReader, TlBinaryWriter } from '@mtcute/tl-runtime'
import { UploadFileLike } from './highlevel/types/files/utils.js'
import { MtUnsupportedError } from './types/errors.js'
import { MaybePromise } from './types/index.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
normalizeFile?(file: UploadFileLike): MaybePromise<{
file?: UploadFileLike
fileSize?: number
fileName?: string
} | null>
}
// eslint-disable-next-line
const globalObject = (0, eval)('this')
// NB: when using with some bundlers (e.g. vite) re-importing this module will not return the same object
// so we need to store the platform in a global object to be able to survive hot-reloads etc.
// try to use Symbol if available, otherwise fallback to a string
const platformKey = typeof Symbol !== 'undefined' ? Symbol.for('mtcute.platform') : '__MTCUTE_PLATFORM__'
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
let _platform: ICorePlatform | null = globalObject?.[platformKey] ?? 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
if (globalObject) {
globalObject[platformKey] = platform
}
}
export function getPlatform(): ICorePlatform {
if (!_platform) {
throw new MtUnsupportedError('Platform is not set! Have you instantiated the client?')
}
return _platform
}

View file

@ -1,6 +1,5 @@
export * from './driver.js' export * from './driver.js'
export * from './memory/index.js'
export * from './provider.js' export * from './provider.js'
export * from './providers/idb/index.js'
export * from './providers/memory/index.js'
export * from './repository/index.js' export * from './repository/index.js'
export * from './storage.js' export * from './storage.js'

View file

@ -1,4 +1,4 @@
import { IStorageDriver } from '../../driver.js' import { IStorageDriver } from '../driver.js'
export class MemoryStorageDriver implements IStorageDriver { export class MemoryStorageDriver implements IStorageDriver {
readonly states: Map<string, object> = new Map() readonly states: Map<string, object> = new Map()

View file

@ -1,5 +1,5 @@
import { ITelegramStorageProvider } from '../../../highlevel/storage/provider.js' import { ITelegramStorageProvider } from '../../highlevel/storage/provider.js'
import { IMtStorageProvider } from '../../provider.js' import { IMtStorageProvider } from '../provider.js'
import { MemoryStorageDriver } from './driver.js' import { MemoryStorageDriver } from './driver.js'
import { MemoryAuthKeysRepository } from './repository/auth-keys.js' import { MemoryAuthKeysRepository } from './repository/auth-keys.js'
import { MemoryKeyValueRepository } from './repository/kv.js' import { MemoryKeyValueRepository } from './repository/kv.js'

View file

@ -1,4 +1,4 @@
import { IAuthKeysRepository } from '../../../repository/auth-keys.js' import { IAuthKeysRepository } from '../../repository/auth-keys.js'
import { MemoryStorageDriver } from '../driver.js' import { MemoryStorageDriver } from '../driver.js'
interface AuthKeysState { interface AuthKeysState {

View file

@ -1,4 +1,4 @@
import { IKeyValueRepository } from '../../../repository/key-value.js' import { IKeyValueRepository } from '../../repository/key-value.js'
import { MemoryStorageDriver } from '../driver.js' import { MemoryStorageDriver } from '../driver.js'
export class MemoryKeyValueRepository implements IKeyValueRepository { export class MemoryKeyValueRepository implements IKeyValueRepository {

View file

@ -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' import { MemoryStorageDriver } from '../driver.js'
interface PeersState { interface PeersState {

View file

@ -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' import { MemoryStorageDriver } from '../driver.js'
interface RefMessagesState { interface RefMessagesState {

View file

@ -2,7 +2,7 @@ import { __tlReaderMap } from '@mtcute/tl/binary/reader.js'
import { __tlWriterMap } from '@mtcute/tl/binary/writer.js' import { __tlWriterMap } from '@mtcute/tl/binary/writer.js'
import { LogManager } from '../../utils/logger.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' import { ServiceOptions } from './base.js'
export function testServiceOptions(): ServiceOptions { export function testServiceOptions(): ServiceOptions {

View file

@ -1,6 +1,7 @@
import { TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime' 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 { Logger } from '../utils/logger.js'
import { IMtStorageProvider } from './provider.js' import { IMtStorageProvider } from './provider.js'
import { AuthKeysService } from './service/auth-keys.js' import { AuthKeysService } from './service/auth-keys.js'
@ -62,7 +63,7 @@ export class StorageManager {
this.driver.setup?.(this.log) this.driver.setup?.(this.log)
if (this.options.cleanup ?? true) { if (this.options.cleanup ?? true) {
this._cleanupRestore = beforeExit(() => { this._cleanupRestore = getPlatform().beforeExit(() => {
this._destroy().catch((err) => this.log.error('cleanup error: %s', err)) this._destroy().catch((err) => this.log.error('cleanup error: %s', err))
}) })
} }

View file

@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { defaultTestCryptoProvider } from '@mtcute/test' import { defaultTestCryptoProvider } from '@mtcute/test'
import { hexDecodeToBuffer } from '@mtcute/tl-runtime'
import { getPlatform } from '../platform.js'
import { import {
bigIntBitLength, bigIntBitLength,
bigIntGcd, bigIntGcd,
@ -16,6 +16,8 @@ import {
twoMultiplicity, twoMultiplicity,
} from './index.js' } from './index.js'
const p = getPlatform()
describe('bigIntBitLength', () => { describe('bigIntBitLength', () => {
it('should correctly calculate bit length', () => { it('should correctly calculate bit length', () => {
expect(bigIntBitLength(0n)).eq(0) 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('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('3038102549'), 4, false)]).eql([0xb5, 0x15, 0xc4, 0x15])
expect([...bigIntToBuffer(BigInt('9341376580368336208'), 8, false)]).eql([ 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('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('3038102549'), 4, true)]).eql([0x15, 0xc4, 0x15, 0xb5])
expect([...bigIntToBuffer(BigInt('9341376580368336208'), 8, true)]).eql([ expect([...bigIntToBuffer(BigInt('9341376580368336208'), 8, true)]).eql([
...hexDecodeToBuffer('81A33C81D2020550').reverse(), ...p.hexDecode('81A33C81D2020550').reverse(),
]) ])
}) })
it('should handle large integers', () => { it('should handle large integers', () => {
const buf = hexDecodeToBuffer( const buf = p.hexDecode(
'1a981ce8bf86bf4a1bd79c2ef829914172f8d0e54cb7ad807552d56977e1c946872e2c7bd77052be30e7e9a7a35c4feff848a25759f5f2f5b0e96538', '1a981ce8bf86bf4a1bd79c2ef829914172f8d0e54cb7ad807552d56977e1c946872e2c7bd77052be30e7e9a7a35c4feff848a25759f5f2f5b0e96538',
) )
const num = BigInt( const num = BigInt(
@ -74,7 +76,7 @@ describe('bufferToBigInt', () => {
}) })
it('should handle large integers', () => { it('should handle large integers', () => {
const buf = hexDecodeToBuffer( const buf = p.hexDecode(
'1a981ce8bf86bf4a1bd79c2ef829914172f8d0e54cb7ad807552d56977e1c946872e2c7bd77052be30e7e9a7a35c4feff848a25759f5f2f5b0e96538', '1a981ce8bf86bf4a1bd79c2ef829914172f8d0e54cb7ad807552d56977e1c946872e2c7bd77052be30e7e9a7a35c4feff848a25759f5f2f5b0e96538',
) )
const num = BigInt( const num = BigInt(

View file

@ -1,13 +1,13 @@
// all available libraries either suck or are extremely large for the use case, so i made my own~ // 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. * Parses a single PEM block to buffer.
* In fact just strips begin/end tags and parses the rest as Base64 * In fact just strips begin/end tags and parses the rest as Base64
*/ */
export function parsePemContents(pem: string): Uint8Array { 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 // 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) { if (0x80 & asn1.length) {
asn1.lengthSize = 0x7f & asn1.length asn1.lengthSize = 0x7f & asn1.length
// I think that buf->hex->int solves the problem of Endianness... not sure // 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 index += asn1.lengthSize
} }

View file

@ -55,5 +55,3 @@ export abstract class BaseCryptoProvider {
return buf return buf
} }
} }
export type CryptoProviderFactory = () => ICryptoProvider

View file

@ -1,12 +1,13 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { defaultCryptoProvider } from '@mtcute/test'
import { bigIntToBuffer, bufferToBigInt } from '../bigint-utils.js' import { bigIntToBuffer, bufferToBigInt } from '../bigint-utils.js'
import { factorizePQSync } from './factorization.js' import { factorizePQSync } from './factorization.js'
import { defaultCryptoProviderFactory } from './index.js'
describe('prime factorization', () => { describe('prime factorization', () => {
const testFactorization = (pq: bigint, p: bigint, q: bigint) => { 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(p_)).toBe(p)
expect(bufferToBigInt(q_)).toBe(q) expect(bufferToBigInt(q_)).toBe(q)
} }

View file

@ -5,8 +5,3 @@ export * from './miller-rabin.js'
export * from './mtproto.js' export * from './mtproto.js'
export * from './password.js' export * from './password.js'
export * from './utils.js' export * from './utils.js'
import { _defaultCryptoProviderFactory } from '../platform/crypto.js'
import { CryptoProviderFactory } from './abstract.js'
export const defaultCryptoProviderFactory: CryptoProviderFactory = _defaultCryptoProviderFactory

View file

@ -1,9 +1,14 @@
import { describe, expect, it } from 'vitest' import { beforeAll, describe, expect, it } from 'vitest'
import { defaultCryptoProvider } from '@mtcute/test'
import { findKeyByFingerprints, parsePublicKey } from '../index.js' import { findKeyByFingerprints, parsePublicKey } from '../index.js'
import { NodeCryptoProvider } from './node.js'
const crypto = new NodeCryptoProvider() const crypto = defaultCryptoProvider
beforeAll(async () => {
await crypto.initialize()
})
describe('parsePublicKey', () => { describe('parsePublicKey', () => {
it('should parse telegram public keys', () => { it('should parse telegram public keys', () => {

View file

@ -1,8 +1,9 @@
import Long from 'long' import Long from 'long'
import { __publicKeyIndex as keysIndex, TlPublicKey } from '@mtcute/tl/binary/rsa-keys.js' 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 { parseAsn1, parsePemContents } from '../binary/asn1-parser.js'
import { ICryptoProvider } from './abstract.js' import { ICryptoProvider } from './abstract.js'
@ -25,13 +26,15 @@ export function parsePublicKey(crypto: ICryptoProvider, key: string, old = false
writer.bytes(modulus) writer.bytes(modulus)
writer.bytes(exponent) writer.bytes(exponent)
const platform = getPlatform()
const data = writer.result() const data = writer.result()
const sha = crypto.sha1(data) const sha = crypto.sha1(data)
const fp = hexEncode(sha.slice(-8).reverse()) const fp = platform.hexEncode(sha.slice(-8).reverse())
return { return {
modulus: hexEncode(modulus), modulus: platform.hexEncode(modulus),
exponent: hexEncode(exponent), exponent: platform.hexEncode(exponent),
fingerprint: fp, fingerprint: fp,
old, old,
} }

View file

@ -1,13 +1,14 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { defaultCryptoProviderFactory } from './index.js' import { defaultCryptoProvider } from '@mtcute/test'
import { millerRabin } from './miller-rabin.js' import { millerRabin } from './miller-rabin.js'
describe( describe(
'miller-rabin test', 'miller-rabin test',
function () { function () {
// miller-rabin factorization relies on RNG, so we should use a real random number generator // 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) => { const testMillerRabin = (n: number | string | bigint, isPrime: boolean) => {
expect(millerRabin(c, BigInt(n))).eq(isPrime) expect(millerRabin(c, BigInt(n))).eq(isPrime)

Some files were not shown because too many files have changed in this diff Show more