Platform agnostic #19
251 changed files with 2447 additions and 1681 deletions
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"exclude": [
|
||||
"**/*.test.ts",
|
||||
"**/*.test-utils.ts"
|
||||
"../**/*.test.ts",
|
||||
"../**/*.test-utils.ts"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
9
.config/vitest.setup.mts
Normal 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())
|
||||
}
|
|
@ -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/)
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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',
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 [
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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',
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -21,6 +21,10 @@ function runForFile(dir, file, single = true) {
|
|||
|
||||
let cmds = runFile(file)
|
||||
|
||||
if (!cmds) {
|
||||
return
|
||||
}
|
||||
|
||||
const options = {
|
||||
env: {
|
||||
...env,
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
85
e2e/ts/tests/05.worker.ts
Normal 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)}`
|
||||
|
||||
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
18
e2e/ts/tests/_worker.ts
Normal 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,
|
||||
})
|
|
@ -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'
|
||||
|
|
|
@ -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',
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}`)
|
||||
})
|
||||
```
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,6 +371,7 @@ async function addSingleMethod(state, fileName) {
|
|||
hasOverloads: hasOverloads[name] && !isOverload,
|
||||
})
|
||||
|
||||
if (!isDeclare) {
|
||||
if (!(module in state.imports)) {
|
||||
state.imports[module] = new Set()
|
||||
}
|
||||
|
@ -378,6 +381,7 @@ async function addSingleMethod(state, fileName) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (stmt.kind === ts.SyntaxKind.InterfaceDeclaration) {
|
||||
if (isCopy) {
|
||||
state.copy.push({
|
||||
|
@ -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) })
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import { MtUnsupportedError } from '../../../types/errors.js'
|
||||
|
||||
export function downloadToFile() {
|
||||
throw new MtUnsupportedError('Downloading to file is only supported in NodeJS')
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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 {
|
||||
|
|
|
@ -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!')
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -46,4 +46,4 @@ export class UserStatusUpdate {
|
|||
}
|
||||
|
||||
memoizeGetters(UserStatusUpdate, ['_parsedStatus' as keyof UserStatusUpdate])
|
||||
makeInspectable(UserStatusUpdate)
|
||||
makeInspectable(UserStatusUpdate, undefined, ['_parsedStatus' as keyof UserStatusUpdate])
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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' +
|
||||
|
|
|
@ -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">' +
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)')
|
||||
}
|
|
@ -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!')
|
||||
}
|
|
@ -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 })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
3
packages/core/src/highlevel/worker/index.ts
Normal file
3
packages/core/src/highlevel/worker/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './port.js'
|
||||
export * from './protocol.js'
|
||||
export * from './worker.js'
|
|
@ -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)
|
||||
},
|
||||
]
|
||||
}
|
|
@ -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')
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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')
|
||||
}
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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>>
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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']))
|
||||
})
|
||||
|
|
54
packages/core/src/platform.ts
Normal file
54
packages/core/src/platform.ts
Normal 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
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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()
|
|
@ -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'
|
|
@ -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 {
|
|
@ -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 {
|
|
@ -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 {
|
|
@ -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 {
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -55,5 +55,3 @@ export abstract class BaseCryptoProvider {
|
|||
return buf
|
||||
}
|
||||
}
|
||||
|
||||
export type CryptoProviderFactory = () => ICryptoProvider
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue
Insecure randomness
This uses a cryptographically insecure random number generated at Math.random() in a security context.
Show more details