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

View file

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

View file

@ -11,6 +11,9 @@ export default defineConfig({
'packages/**/*.test-d.ts',
],
},
setupFiles: [
'./.config/vitest.setup.mts'
]
},
define: {
'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
```ts
import { NodeTelegramClient } from '@mtcute/node'
import { TelegramClient } from '@mtcute/node'
import { Dispatcher, filters } from '@mtcute/dispatcher'
const tg = new NodeTelegramClient({
const tg = new TelegramClient({
apiId: parseInt(process.env.API_ID),
apiHash: process.env.API_HASH,
storage: 'my-account'
@ -64,9 +64,9 @@ pnpm add @mtcute/node
pnpm add @mtcute/crypto-node
```
✨ building something for web? use the client directly:
✨ building something for web? use the web package:
```bash
pnpm add @mtcute/client
pnpm add @mtcute/web
```
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 { expect } = require('chai')

View file

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

View file

@ -1,18 +1,17 @@
const Long = require('long')
const {
TlBinaryReader,
TlBinaryWriter,
hexEncode,
} = require('@mtcute/tl-runtime')
const { TlBinaryReader, TlBinaryWriter } = require('@mtcute/tl-runtime')
const { tl } = require('@mtcute/tl')
const { __tlReaderMap } = require('@mtcute/tl/binary/reader')
const { __tlWriterMap } = require('@mtcute/tl/binary/writer')
const { describe, it } = require('mocha')
const { expect } = require('chai')
const { NodePlatform } = require('@mtcute/node')
// here we primarily want to check that @mtcute/tl correctly works with @mtcute/tl-runtime
const p = new NodePlatform()
describe('@mtcute/tl', () => {
it('writers map works with TlBinaryWriter', () => {
const obj = {
@ -21,7 +20,9 @@ describe('@mtcute/tl', () => {
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', () => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,13 +2,16 @@ import { expect } from 'chai'
import Long from 'long'
import { describe, it } from 'mocha'
import { NodePlatform } from '@mtcute/node'
import { tl } from '@mtcute/tl'
import { __tlReaderMap } from '@mtcute/tl/binary/reader.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
const p = new NodePlatform()
describe('@mtcute/tl', () => {
it('writers map works with TlBinaryWriter', () => {
const obj = {
@ -17,11 +20,13 @@ describe('@mtcute/tl', () => {
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', () => {
const buf = hexDecodeToBuffer('4ca5e8dd7b00000000000000c801000000000000')
const buf = p.hexDecode('4ca5e8dd7b00000000000000c801000000000000')
const obj = TlBinaryReader.deserializeObject(__tlReaderMap, buf)
expect(obj._).equal('inputPeerUser')

View file

@ -1,9 +1,12 @@
import { expect } from 'chai'
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', () => {
const key = Buffer.from('5468697320697320616E20696D706C655468697320697320616E20696D706C65', 'hex')

View file

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

View file

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

View file

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

View file

@ -1,7 +1,8 @@
import { expect } from 'chai'
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'

View file

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

View file

@ -3,7 +3,8 @@ import { expect } from 'chai'
import { createHash } from 'crypto'
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 { getApiParams } from '../utils.js'

View file

@ -1,7 +1,8 @@
import { expect } from 'chai'
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'

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 { describe, it } from 'mocha'
import { BaseTelegramClient } from '@mtcute/core'
import { BaseTelegramClient } from '@mtcute/core/client.js'
// @fix-import
import { getApiParams } from '../../utils'

View file

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

View file

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

View file

@ -1,9 +1,12 @@
import { expect } from 'chai'
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', () => {
const key = Buffer.from('5468697320697320616E20696D706C655468697320697320616E20696D706C65', 'hex')

View file

@ -2,7 +2,9 @@
import { join } from 'path'
import { MaybePromise, MemoryStorage } from '@mtcute/core'
import { setPlatform } from '@mtcute/core/platform.js'
import { LogManager, sleep } from '@mtcute/core/utils.js'
import { NodeCryptoProvider, NodePlatform, TcpTransport } from '@mtcute/node'
import { SqliteStorage } from '@mtcute/sqlite'
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')
}
setPlatform(new NodePlatform())
return {
apiId: parseInt(process.env.API_ID),
apiHash: process.env.API_HASH,
testMode: true,
storage: storage ? new SqliteStorage(join(__dirname, storage)) : new MemoryStorage(),
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)
Basic low-level MTProto implementation and auxiliary utilities.
Platform-agnostic MTProto implementation and auxiliary utilities.
## Features
- **MTProto 2.0**: Implements the full MTProto protocol, including all the encryption and serialization
- **2FA support**: Provides utilities for 2-step verification
- **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
- **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
```ts
import { BaseTelegramClient } from '@mtcute/core'
import { BaseTelegramClient } from '@mtcute/core/client.js'
const tg = new BaseTelegramClient({
apiId: 12345,
apiHash: '0123456789abcdef0123456789abcdef',
crypto: new MyCryptoProvider(),
storage: new MyStorage(),
transport: () => new MyTransport(),
})
tg.call({ _: 'help.getConfig' })
.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,
esmImportDirectives: true,
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, '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",
"main": "src/index.ts",
"type": "module",
"sideEffects": false,
"scripts": {
"build": "pnpm run -w build-package core",
"gen-client": "node ./scripts/generate-client.cjs",
"gen-updates": "node ./scripts/generate-updates.cjs"
},
"browser": {
"./src/utils/platform/crypto.js": "./src/utils/platform/crypto.web.js",
"./src/utils/platform/transport.js": "./src/utils/platform/transport.web.js",
"./src/utils/platform/logging.js": "./src/utils/platform/logging.web.js",
"./src/utils/platform/random.js": "./src/utils/platform/random.web.js",
"./src/utils/platform/exit-hook.js": "./src/utils/platform/exit-hook.web.js",
"./src/highlevel/worker/platform/connect.js": "./src/highlevel/worker/platform/connect.web.js",
"./src/highlevel/worker/platform/register.js": "./src/highlevel/worker/platform/register.web.js",
"./src/highlevel/methods/files/_platform.js": "./src/highlevel/methods/files/_platform.web.js",
"./src/highlevel/methods/files/download-file.js": "./src/highlevel/methods/files/download-file.web.js",
"./src/highlevel/utils/platform/storage.js": "./src/highlevel/utils/platform/storage.web.js",
"./src/storage/json-file.js": false
"exports": {
".": "./src/index.ts",
"./utils.js": "./src/utils/index.ts",
"./client.js": "./src/highlevel/client.ts",
"./worker.js": "./src/highlevel/worker/index.ts",
"./methods.js": "./src/highlevel/methods.ts",
"./platform.js": "./src/platform.ts"
},
"distOnlyFields": {
"exports": {
@ -35,32 +31,27 @@
"import": "./esm/utils/index.js",
"require": "./cjs/utils/index.js"
},
"./utils/crypto/*": {
"import": "./esm/utils/crypto/*",
"require": "./cjs/utils/crypto/*"
},
"./network/transports/*": {
"import": "./esm/network/transports/*",
"require": "./cjs/network/transports/*"
},
"./storage/*": {
"import": "./esm/storage/*",
"require": "./cjs/storage/*"
},
"./highlevel/*": {
"import": "./esm/highlevel/*",
"require": "./cjs/highlevel/*"
},
"./methods.js": {
"import": "./esm/highlevel/methods.js",
"require": "./cjs/highlevel/methods.js"
},
"./platform.js": {
"import": "./esm/platform.js",
"require": "./cjs/platform.js"
},
"./client.js": {
"import": "./esm/highlevel/client.js",
"require": "./cjs/highlevel/client.js"
},
"./worker.js": {
"import": "./esm/highlevel/worker/index.js",
"require": "./cjs/highlevel/worker/index.js"
}
}
},
"dependencies": {
"@mtcute/tl": "workspace:^",
"@mtcute/tl-runtime": "workspace:^",
"@mtcute/wasm": "workspace:^",
"@mtcute/file-id": "workspace:^",
"@types/events": "3.0.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 isDeclare = (stmt.modifiers || []).find((mod) => mod.kind === ts.SyntaxKind.DeclareKeyword)
const isInitialize = checkForFlag(stmt, '@initialize')
const isManualImpl = checkForFlag(stmt, '@manual-impl')
const isInitializeSuper = isInitialize === 'super'
@ -327,7 +328,7 @@ async function addSingleMethod(state, fileName) {
})
}
if (!isExported) continue
if (!isExported && !isDeclare) continue
const firstArg = stmt.parameters[0]
@ -344,7 +345,7 @@ async function addSingleMethod(state, fileName) {
state.methods.used[name] = relPath
}
if (isExported) {
if (isExported || isDeclare) {
const isPrivate = checkForFlag(stmt, '@internal')
const isManual = checkForFlag(stmt, '@manual')
const isNoemit = checkForFlag(stmt, '@noemit')
@ -358,6 +359,7 @@ async function addSingleMethod(state, fileName) {
isPrivate,
isManual,
isNoemit,
isDeclare,
shouldEmit,
func: stmt,
comment: getLeadingComments(stmt),
@ -369,12 +371,14 @@ async function addSingleMethod(state, fileName) {
hasOverloads: hasOverloads[name] && !isOverload,
})
if (!(module in state.imports)) {
state.imports[module] = new Set()
}
if (!isDeclare) {
if (!(module in state.imports)) {
state.imports[module] = new Set()
}
if (!isManual || isManual.split('=')[1] !== 'noemit') {
state.imports[module].add(name)
if (!isManual || isManual.split('=')[1] !== 'noemit') {
state.imports[module].add(name)
}
}
}
}
@ -399,6 +403,9 @@ async function addSingleMethod(state, fileName) {
}
state.imports[module].add(stmt.name.escapedText)
state.exported[module] = state.exported[module] || new Set()
state.exported[module].add(stmt.name.escapedText)
continue
}
@ -429,6 +436,9 @@ async function addSingleMethod(state, fileName) {
}
state.imports[module].add(stmt.name.escapedText)
state.exported[module] = state.exported[module] || new Set()
state.exported[module].add(stmt.name.escapedText)
} else if (isCopy) {
state.copy.push({ from: relPath, code: stmt.getFullText().trim() })
} else if (isTypeExported) {
@ -442,6 +452,7 @@ async function main() {
const output = fs.createWriteStream(targetFile)
const state = {
imports: {},
exported: {},
fields: [],
init: [],
methods: {
@ -527,6 +538,7 @@ on(name: string, handler: (...args: any[]) => void): this\n`)
available,
rawApiMethods,
dependencies,
isDeclare,
}) => {
if (!available && !overload) {
// no @available directive
@ -659,7 +671,7 @@ on(name: string, handler: (...args: any[]) => void): this\n`)
output.write(`${name}${generics}(${parameters})${returnType}\n`)
}
if (!overload && !isManual) {
if (!overload && !isManual && !isDeclare) {
if (hasOverloads) {
classProtoDecls.push('// @ts-expect-error this kinda breaks typings for overloads, idc')
}
@ -677,6 +689,7 @@ on(name: string, handler: (...args: any[]) => void): this\n`)
output.write('}\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(' _client: ITelegramClient\n')
@ -737,9 +750,14 @@ on(name: string, handler: (...args: any[]) => void): this\n`)
const outputMethods = fs.createWriteStream(targetFileMethods)
outputMethods.write('/* THIS FILE WAS AUTO-GENERATED */\n')
state.methods.list.forEach(({ module, name, overload }) => {
if (overload) return
state.methods.list.forEach(({ module, name, overload, isDeclare }) => {
if (overload || isDeclare) return
outputMethods.write(`export { ${name} } from '${module}'\n`)
if (state.exported[module]) {
outputMethods.write(`export type { ${[...state.exported[module]].join(', ')} } from '${module}'\n`)
delete state.exported[module]
}
})
await new Promise((resolve) => { outputMethods.end(resolve) })

View file

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

View file

@ -1,5 +1,3 @@
export * from './base.js'
export * from './client.js'
export * from './client.types.js'
export * from './storage/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 { setFoldersOrder } from './methods/dialogs/set-folders-order.js'
export { downloadAsBuffer } from './methods/files/download-buffer.js'
export { downloadToFile } from './methods/files/download-file.js'
export { downloadAsIterable } from './methods/files/download-iterable.js'
export { downloadAsStream } from './methods/files/download-stream.js'
export { _normalizeInputFile } from './methods/files/normalize-input-file.js'
@ -96,6 +95,7 @@ export { uploadMedia } from './methods/files/upload-media.js'
export { createForumTopic } from './methods/forums/create-forum-topic.js'
export { deleteForumTopicHistory } from './methods/forums/delete-forum-topic-history.js'
export { editForumTopic } from './methods/forums/edit-forum-topic.js'
export type { GetForumTopicsOffset } from './methods/forums/get-forum-topics.js'
export { getForumTopics } from './methods/forums/get-forum-topics.js'
export { getForumTopicsById } from './methods/forums/get-forum-topics-by-id.js'
export { iterForumTopics } from './methods/forums/iter-forum-topics.js'
@ -109,6 +109,7 @@ export { editInviteLink } from './methods/invite-links/edit-invite-link.js'
export { exportInviteLink } from './methods/invite-links/export-invite-link.js'
export { getInviteLink } from './methods/invite-links/get-invite-link.js'
export { getInviteLinkMembers } from './methods/invite-links/get-invite-link-members.js'
export type { GetInviteLinksOffset } from './methods/invite-links/get-invite-links.js'
export { getInviteLinks } from './methods/invite-links/get-invite-links.js'
export { getPrimaryInviteLink } from './methods/invite-links/get-primary-invite-link.js'
export { hideAllJoinRequests } from './methods/invite-links/hide-all-join-requests.js'
@ -117,15 +118,18 @@ export { iterInviteLinkMembers } from './methods/invite-links/iter-invite-link-m
export { iterInviteLinks } from './methods/invite-links/iter-invite-links.js'
export { revokeInviteLink } from './methods/invite-links/revoke-invite-link.js'
export { closePoll } from './methods/messages/close-poll.js'
export type { DeleteMessagesParams } from './methods/messages/delete-messages.js'
export { deleteMessagesById } from './methods/messages/delete-messages.js'
export { deleteMessages } from './methods/messages/delete-messages.js'
export { deleteScheduledMessages } from './methods/messages/delete-scheduled-messages.js'
export { editInlineMessage } from './methods/messages/edit-inline-message.js'
export { editMessage } from './methods/messages/edit-message.js'
export type { ForwardMessageOptions } from './methods/messages/forward-messages.js'
export { forwardMessagesById } from './methods/messages/forward-messages.js'
export { forwardMessages } from './methods/messages/forward-messages.js'
export { getCallbackQueryMessage } from './methods/messages/get-callback-query-message.js'
export { getDiscussionMessage } from './methods/messages/get-discussion-message.js'
export type { GetHistoryOffset } from './methods/messages/get-history.js'
export { getHistory } from './methods/messages/get-history.js'
export { getMessageByLink } from './methods/messages/get-message-by-link.js'
export { getMessageGroup } from './methods/messages/get-message-group.js'
@ -133,6 +137,7 @@ export { getMessageReactionsById } from './methods/messages/get-message-reaction
export { getMessageReactions } from './methods/messages/get-message-reactions.js'
export { getMessages } from './methods/messages/get-messages.js'
export { getMessagesUnsafe } from './methods/messages/get-messages-unsafe.js'
export type { GetReactionUsersOffset } from './methods/messages/get-reaction-users.js'
export { getReactionUsers } from './methods/messages/get-reaction-users.js'
export { getReplyTo } from './methods/messages/get-reply-to.js'
export { getScheduledMessages } from './methods/messages/get-scheduled-messages.js'
@ -143,7 +148,9 @@ export { iterSearchMessages } from './methods/messages/iter-search-messages.js'
export { pinMessage } from './methods/messages/pin-message.js'
export { readHistory } from './methods/messages/read-history.js'
export { readReactions } from './methods/messages/read-reactions.js'
export type { SearchGlobalOffset } from './methods/messages/search-global.js'
export { searchGlobal } from './methods/messages/search-global.js'
export type { SearchMessagesOffset } from './methods/messages/search-messages.js'
export { searchMessages } from './methods/messages/search-messages.js'
export { answerText } from './methods/messages/send-answer.js'
export { answerMedia } from './methods/messages/send-answer.js'
@ -151,10 +158,13 @@ export { answerMediaGroup } from './methods/messages/send-answer.js'
export { commentText } from './methods/messages/send-comment.js'
export { commentMedia } from './methods/messages/send-comment.js'
export { commentMediaGroup } from './methods/messages/send-comment.js'
export type { SendCopyParams } from './methods/messages/send-copy.js'
export { sendCopy } from './methods/messages/send-copy.js'
export type { SendCopyGroupParams } from './methods/messages/send-copy-group.js'
export { sendCopyGroup } from './methods/messages/send-copy-group.js'
export { sendMedia } from './methods/messages/send-media.js'
export { sendMediaGroup } from './methods/messages/send-media-group.js'
export type { QuoteParamsFrom } from './methods/messages/send-quote.js'
export { quoteWithText } from './methods/messages/send-quote.js'
export { quoteWithMedia } from './methods/messages/send-quote.js'
export { quoteWithMediaGroup } from './methods/messages/send-quote.js'
@ -179,6 +189,7 @@ export { resendPasswordEmail } from './methods/password/password-email.js'
export { cancelPasswordEmail } from './methods/password/password-email.js'
export { removeCloudPassword } from './methods/password/remove-cloud-password.js'
export { applyBoost } from './methods/premium/apply-boost.js'
export type { CanApplyBoostResult } from './methods/premium/can-apply-boost.js'
export { canApplyBoost } from './methods/premium/can-apply-boost.js'
export { getBoostStats } from './methods/premium/get-boost-stats.js'
export { getBoosts } from './methods/premium/get-boosts.js'
@ -194,6 +205,7 @@ export { getStickerSet } from './methods/stickers/get-sticker-set.js'
export { moveStickerInSet } from './methods/stickers/move-sticker-in-set.js'
export { setChatStickerSet } from './methods/stickers/set-chat-sticker-set.js'
export { setStickerSetThumb } from './methods/stickers/set-sticker-set-thumb.js'
export type { CanSendStoryResult } from './methods/stories/can-send-story.js'
export { canSendStory } from './methods/stories/can-send-story.js'
export { deleteStories } from './methods/stories/delete-stories.js'
export { editStory } from './methods/stories/edit-story.js'

View file

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

View file

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

View file

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

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

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

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

View file

@ -1,5 +1,6 @@
import { tl } from '@mtcute/tl'
import { getPlatform } from '../../../platform.js'
import { MtArgumentError } from '../../../types/errors.js'
import { randomLong } from '../../../utils/long-utils.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 { determinePartSize, isProbablyPlainText } from '../../utils/file-utils.js'
import { bufferToStream, createChunkedReader, streamToBuffer } from '../../utils/stream-utils.js'
import { _createFileStream, _extractFileStreamMeta, _handleNodeStream, _isFileStream } from './_platform.js'
const OVERRIDE_MIME: Record<string, string> = {
// 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_PREMIUM = 8000 // 512 kb * 8000 = 4000 MiB
// platform-specific
const HAS_FILE = typeof File !== 'undefined'
const HAS_RESPONSE = typeof Response !== 'undefined'
// @available=both
/**
* Upload a file to Telegram servers, without actually
@ -37,9 +41,6 @@ export async function uploadFile(
params: {
/**
* Upload file source.
*
* > **Note**: `fs.ReadStream` is a subclass of `stream.Readable` and contains
* > info about file name, thus you don't need to pass them explicitly.
*/
file: UploadFileLike
@ -105,29 +106,30 @@ export async function uploadFile(
let fileName = DEFAULT_FILE_NAME
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)) {
fileSize = file.length
file = bufferToStream(file)
}
if (typeof File !== 'undefined' && file instanceof File) {
if (HAS_FILE && file instanceof File) {
fileName = file.name
fileSize = file.size
// file is now ReadableStream
file = file.stream()
}
if (typeof file === 'string') {
file = _createFileStream(file)
}
if (_isFileStream(file)) {
[fileName, fileSize] = await _extractFileStreamMeta(file)
// fs.ReadStream is a subclass of Readable, will be handled below
}
if (typeof file === 'object' && 'headers' in file && 'body' in file && 'url' in file) {
// fetch() response
if (HAS_RESPONSE && file instanceof Response) {
const length = parseInt(file.headers.get('content-length') || '0')
if (!isNaN(length) && length) fileSize = length
@ -161,8 +163,6 @@ export async function uploadFile(
file = file.body
}
file = _handleNodeStream(file)
if (!(file instanceof ReadableStream)) {
throw new MtArgumentError('Could not convert input `file` to stream!')
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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 { concatBuffers } from '../../utils/buffer-utils.js'
@ -33,7 +32,7 @@ export function isProbablyPlainText(buf: Uint8Array): boolean {
}
// from https://github.com/telegramdesktop/tdesktop/blob/bec39d89e19670eb436dc794a8f20b657cb87c71/Telegram/SourceFiles/ui/image/image.cpp#L225
const JPEG_HEADER = hexDecodeToBuffer(
const JPEG_HEADER = () => getPlatform().hexDecode(
'ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e1928' +
'2321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a0aad' +
'aad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2d2d3c35' +
@ -54,6 +53,7 @@ const JPEG_HEADER = hexDecodeToBuffer(
'b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f' +
'3f4f5f6f7f8f9faffda000c03010002110311003f00',
)
let JPEG_HEADER_BYTES: Uint8Array | null = null
const JPEG_FOOTER = new Uint8Array([0xff, 0xd9])
/**
@ -64,7 +64,11 @@ export function strippedPhotoToJpg(stripped: Uint8Array): Uint8Array {
throw new MtArgumentError('Invalid stripped JPEG')
}
const result = concatBuffers([JPEG_HEADER, stripped.slice(3), JPEG_FOOTER])
if (JPEG_HEADER_BYTES === null) {
JPEG_HEADER_BYTES = JPEG_HEADER()
}
const result = concatBuffers([JPEG_HEADER_BYTES, stripped.slice(3), JPEG_FOOTER])
result[164] = stripped[1]
result[166] = stripped[2]
@ -108,7 +112,7 @@ export function inflateSvgPath(encoded: Uint8Array): string {
* @param path
*/
export function svgPathToFile(path: string): Uint8Array {
return utf8EncodeToBuffer(
return getPlatform().utf8Encode(
'<?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">' +

View file

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

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-argument */
import { base64Encode } from '@mtcute/tl-runtime'
import { getPlatform } from '../../platform.js'
const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom')
@ -33,7 +33,11 @@ function getAllGettersNames<T>(obj: T): (keyof T)[] {
* > (getter that caches after its first invocation is also
* > 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 : []
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 instanceof Uint8Array) {
val = base64Encode(val)
val = getPlatform().base64Encode(val)
} else if (typeof val.toJSON === 'function') {
val = val.toJSON(true)
}
@ -67,4 +71,6 @@ export function makeInspectable<T>(obj: new (...args: any[]) => T, props?: (keyo
return ret
}
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-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) {
const desc = Object.getOwnPropertyDescriptor(cls.prototype, field)
if (!desc) continue
@ -25,4 +25,6 @@ export function memoizeGetters<T>(cls: new (...args: any[]) => T, fields: (keyof
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 { createChunkedReader, nodeReadableToWeb } from './stream-utils.js'
import { createChunkedReader } from './stream-utils.js'
describe('createChunkedReader', () => {
it('should correctly handle chunks smaller than chunkSize', async () => {
@ -82,26 +81,3 @@ describe('createChunkedReader', () => {
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,
}
}
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 { base64DecodeToBuffer, base64Encode, TlBinaryReader, TlBinaryWriter, TlReaderMap } from '@mtcute/tl-runtime'
import { TlBinaryReader, TlBinaryWriter, TlReaderMap } from '@mtcute/tl-runtime'
import { getPlatform } from '../../platform.js'
import { MtArgumentError } from '../../types/index.js'
import { BasicDcOption, DcOptions, parseBasicDcOption, serializeBasicDcOption } from '../../utils/dcs.js'
import { CurrentUserInfo } from '../storage/service/current-user.js'
@ -53,11 +54,11 @@ export function writeStringSession(data: StringSessionData): string {
writer.bytes(data.authKey)
return base64Encode(writer.result(), true)
return getPlatform().base64Encode(writer.result(), true)
}
export function readStringSession(readerMap: TlReaderMap, data: string): StringSessionData {
const buf = base64DecodeToBuffer(data, true)
const buf = getPlatform().base64Decode(data, true)
const version = buf[0]

View file

@ -1,14 +1,15 @@
import { describe, expect, it } from 'vitest'
import { hexDecodeToBuffer, hexEncode } from '@mtcute/tl-runtime'
import { getPlatform } from '../../platform.js'
import { decodeWaveform, encodeWaveform } from './voice-utils.js'
const p = getPlatform()
describe('decodeWaveform', () => {
it('should correctly decode telegram-encoded waveform', () => {
expect(
decodeWaveform(
hexDecodeToBuffer(
p.hexDecode(
'0000104210428c310821a51463cc39072184524a4aa9b51663acb5e69c7bef41' +
'08618c514a39e7a494d65aadb5f75e8c31ce396badf7de9cf3debbf7feff0f',
),
@ -25,7 +26,7 @@ describe('decodeWaveform', () => {
describe('encodeWaveform', () => {
it('should correctly decode telegram-encoded waveform', () => {
expect(
hexEncode(
p.hexEncode(
encodeWaveform([
0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 10,
10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 16, 17, 17, 17, 18,

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 { AppConfigManagerProxy } from './app-config.js'
import { WorkerInvoker } from './invoker.js'
import { connectToWorker } from './platform/connect.js'
import { ClientMessageHandler, SomeWorker, WorkerCustomMethods } from './protocol.js'
import { ClientMessageHandler, SendFn, SomeWorker, WorkerCustomMethods } from './protocol.js'
import { TelegramStorageProxy } from './storage.js'
export interface TelegramWorkerPortOptions {
worker: SomeWorker
}
export class TelegramWorkerPort<Custom extends WorkerCustomMethods> implements ITelegramClient {
export abstract class TelegramWorkerPort<Custom extends WorkerCustomMethods> implements ITelegramClient {
constructor(readonly options: TelegramWorkerPortOptions) {}
abstract connectToWorker(worker: SomeWorker, handler: ClientMessageHandler): [SendFn, () => void]
readonly log = new LogManager('worker')
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 _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 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 { registerWorker } from './platform/register.js'
import { RespondFn, WorkerCustomMethods, WorkerInboundMessage, WorkerMessageHandler } from './protocol.js'
export interface TelegramWorkerOptions<T extends WorkerCustomMethods> {
client: BaseTelegramClient | BaseTelegramClientOptions
client: BaseTelegramClient
customMethods?: T
}
export function makeTelegramWorker<T extends WorkerCustomMethods>(params: TelegramWorkerOptions<T>) {
const { client: client_, customMethods } = params
export abstract class TelegramWorker<T extends WorkerCustomMethods> {
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
let target: any
switch (msg.target) {
case 'custom':
target = customMethods
target = this.params.customMethods
break
case 'client':
target = client
target = this.client
break
case 'storage':
target = client.storage
target = this.client.storage
break
case 'storage-self':
target = client.storage.self
target = this.client.storage.self
break
case 'storage-peers':
target = client.storage.peers
target = this.client.storage.peers
break
case 'app-config':
target = client.appConfig
target = this.client.appConfig
break
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 {
hexDecode,
hexDecodeToBuffer,
hexEncode,
TlBinaryReader,
TlReaderMap,
utf8Decode,
utf8EncodeToBuffer,
} from '@mtcute/tl-runtime'
import { getPlatform } from '../platform.js'
import { LogManager } from '../utils/index.js'
import { AuthKey } from './auth-key.js'
const authKey = new Uint8Array(256)
const p = getPlatform()
for (let i = 0; i < 256; i += 32) {
hexDecode(authKey.subarray(i, i + 32), '98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0')
authKey.subarray(i, i + 32).set(p.hexDecode('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0'))
}
describe('AuthKey', () => {
@ -54,19 +51,19 @@ describe('AuthKey', () => {
it('should calculate derivatives', async () => {
const key = await create()
expect(hexEncode(key.key)).toEqual(hexEncode(authKey))
expect(hexEncode(key.clientSalt)).toEqual('f73c3622dec230e098cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4b')
expect(hexEncode(key.serverSalt)).toEqual('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0')
expect(hexEncode(key.id)).toEqual('40fa5bb7cb56a895')
expect(p.hexEncode(key.key)).toEqual(p.hexEncode(authKey))
expect(p.hexEncode(key.clientSalt)).toEqual('f73c3622dec230e098cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4b')
expect(p.hexEncode(key.serverSalt)).toEqual('98cb29c6ffa89e79da695a54f572e6cb101e81c688b63a4bf73c3622dec230e0')
expect(p.hexEncode(key.id)).toEqual('40fa5bb7cb56a895')
})
it('should encrypt a message', async () => {
const message = writeMessage(utf8EncodeToBuffer('hello, world!!!!'))
const message = writeMessage(p.utf8Encode('hello, world!!!!'))
const key = await create()
const msg = key.encryptMessage(message, serverSalt, sessionId)
expect(hexEncode(msg)).toEqual(
expect(p.hexEncode(msg)).toEqual(
'40fa5bb7cb56a895f6f5a88914892aadf87c68031cc953ba29d68e118021f329' +
'be386a620d49f3ad3a50c60dcef3733f214e8cefa3e403c11d193637d4971dc1' +
'5db7f74b26fd16cb0e8fee30bf7e3f68858fe82927e2cd06',
@ -88,7 +85,7 @@ describe('AuthKey', () => {
}
it('should decrypt a message', async () => {
const message = hexDecodeToBuffer(
const message = p.hexDecode(
'40fa5bb7cb56a8950c394b884f1529efc42fea22d972fea650a714ce6d2d1bdb' +
'3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b' +
'07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35',
@ -98,11 +95,11 @@ describe('AuthKey', () => {
expect(decMsgId).toEqual(msgId)
expect(decSeqNo).toEqual(seqNo)
expect(utf8Decode(data.raw(16))).toEqual('hello, world!!!!')
expect(p.utf8Decode(data.raw(16))).toEqual('hello, world!!!!')
})
it('should decrypt a message with padding', async () => {
const message = hexDecodeToBuffer(
const message = p.hexDecode(
'40fa5bb7cb56a8950c394b884f1529efc42fea22d972fea650a714ce6d2d1bdb' +
'3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b' +
'07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35' +
@ -113,11 +110,11 @@ describe('AuthKey', () => {
expect(decMsgId).toEqual(msgId)
expect(decSeqNo).toEqual(seqNo)
expect(utf8Decode(data.raw(16))).toEqual('hello, world!!!!')
expect(p.utf8Decode(data.raw(16))).toEqual('hello, world!!!!')
})
it('should ignore messages with invalid message key', async () => {
const message = hexDecodeToBuffer(
const message = p.hexDecode(
'40fa5bb7cb56a8950000000000000000000000000000000050a714ce6d2d1bdb' +
'3d98ff5929b8768c401771a69795f36a7e720dcafac2efbccd0ba368e8a7f48b' +
'07362cac1a32ffcabe188b51a36cc4d54e1d0633cf9eaf35',
@ -127,7 +124,7 @@ describe('AuthKey', () => {
})
it('should ignore messages with invalid session_id', async () => {
const message = hexDecodeToBuffer(
const message = p.hexDecode(
'40fa5bb7cb56a895a986a7e97f4e90aa2769b5e702c6e86f5e1e82c6ff0c6829' +
'2521a2ba9704fa37fb341d895cf32662c6cf47ba31cbf27c30d5c03f6c2930f4' +
'30fd8858b836b73fe32d4a95b8ebcdbc9ca8908f7964c40a',
@ -137,12 +134,12 @@ describe('AuthKey', () => {
})
it('should ignore messages with invalid length', async () => {
const messageTooLong = hexDecodeToBuffer(
const messageTooLong = p.hexDecode(
'40fa5bb7cb56a8950d19412233dd5d24be697c73274e08fbe515cf65e0c5f70c' +
'ad75fd2badc18c9f999f287351144eeb1cfcaa9bea33ef5058999ad96a498306' +
'08d2859425685a55b21fab413bfabc42ec5da283853b28c0',
)
const messageUnaligned = hexDecodeToBuffer(
const messageUnaligned = p.hexDecode(
'40fa5bb7cb56a8957b4e4bec561eee4a5a1025bc8a35d3d0c79a3685d2b90ff0' +
'5f638e9c42c9fd9448b0ce8e7d49e7ea1ce458e47b825b5c7fd8ddf5b4fded46' +
'2a4bcc02f3ff2e89de6764d6d219f575e457fdcf8c163cdf',
@ -155,7 +152,7 @@ describe('AuthKey', () => {
})
it('should ignore messages with invalid padding', async () => {
const message = hexDecodeToBuffer(
const message = p.hexDecode(
'40fa5bb7cb56a895133671d1c637a9836e2c64b4d1a0521d8a25a6416fd4dc9e' +
'79f9478fb837703cc9efa0a19d12143c2a26e57cb4bc64d7bc972dd8f19c53c590cc258162f44afc',
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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 './memory/index.js'
export * from './provider.js'
export * from './providers/idb/index.js'
export * from './providers/memory/index.js'
export * from './repository/index.js'
export * from './storage.js'

View file

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

View file

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

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'
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'
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'
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'
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 { LogManager } from '../../utils/logger.js'
import { MemoryStorageDriver } from '../providers/memory/driver.js'
import { MemoryStorageDriver } from '../memory/driver.js'
import { ServiceOptions } from './base.js'
export function testServiceOptions(): ServiceOptions {

View file

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

View file

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

View file

@ -1,13 +1,13 @@
// all available libraries either suck or are extremely large for the use case, so i made my own~
import { base64DecodeToBuffer, hexEncode } from '@mtcute/tl-runtime'
import { getPlatform } from '../../platform.js'
/**
* Parses a single PEM block to buffer.
* In fact just strips begin/end tags and parses the rest as Base64
*/
export function parsePemContents(pem: string): Uint8Array {
return base64DecodeToBuffer(pem.replace(/^-----(BEGIN|END)( RSA)? PUBLIC KEY-----$|\n/gm, ''))
return getPlatform().base64Decode(pem.replace(/^-----(BEGIN|END)( RSA)? PUBLIC KEY-----$|\n/gm, ''))
}
// based on https://git.coolaj86.com/coolaj86/asn1-parser.js/src/branch/master/asn1-parser.js
@ -66,7 +66,7 @@ export function parseAsn1(data: Uint8Array): Asn1Object {
if (0x80 & asn1.length) {
asn1.lengthSize = 0x7f & asn1.length
// I think that buf->hex->int solves the problem of Endianness... not sure
asn1.length = parseInt(hexEncode(buf.subarray(index, index + asn1.lengthSize)), 16)
asn1.length = parseInt(getPlatform().hexEncode(buf.subarray(index, index + asn1.lengthSize)), 16)
index += asn1.lengthSize
}

View file

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

View file

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

View file

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

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 { NodeCryptoProvider } from './node.js'
const crypto = new NodeCryptoProvider()
const crypto = defaultCryptoProvider
beforeAll(async () => {
await crypto.initialize()
})
describe('parsePublicKey', () => {
it('should parse telegram public keys', () => {

View file

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

View file

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

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