From 2fe476cf3cf3e7d84d4fdacecf60bc94563a4fa6 Mon Sep 17 00:00:00 2001 From: Alina Sireneva Date: Thu, 7 Mar 2024 09:32:16 +0300 Subject: [PATCH 1/2] feat: initial support for session conversion --- .config/tsconfig.build.json | 3 +- packages/convert/README.md | 22 ++++ packages/convert/package.json | 29 +++++ packages/convert/src/dcs.ts | 113 ++++++++++++++++++ .../src/gramjs/__fixtures__/generate.cjs | 46 +++++++ .../src/gramjs/__fixtures__/session.ts | 2 + packages/convert/src/gramjs/convert.test.ts | 72 +++++++++++ packages/convert/src/gramjs/convert.ts | 29 +++++ packages/convert/src/gramjs/parse.test.ts | 27 +++++ packages/convert/src/gramjs/parse.ts | 35 ++++++ packages/convert/src/gramjs/serialize.test.ts | 29 +++++ packages/convert/src/gramjs/serialize.ts | 28 +++++ packages/convert/src/gramjs/types.ts | 7 ++ .../src/mtkruto/__fixtures__/generate.js | 15 +++ .../src/mtkruto/__fixtures__/session.ts | 2 + packages/convert/src/mtkruto/convert.test.ts | 70 +++++++++++ packages/convert/src/mtkruto/convert.ts | 32 +++++ packages/convert/src/mtkruto/parse.test.ts | 25 ++++ packages/convert/src/mtkruto/parse.ts | 31 +++++ .../convert/src/mtkruto/serialize.test.ts | 27 +++++ packages/convert/src/mtkruto/serialize.ts | 16 +++ packages/convert/src/mtkruto/types.ts | 5 + .../src/pyrogram/__fixtures__/generate.py | 20 ++++ .../src/pyrogram/__fixtures__/generate_old.py | 24 ++++ .../src/pyrogram/__fixtures__/session.ts | 2 + .../src/pyrogram/__fixtures__/session_old.ts | 2 + packages/convert/src/pyrogram/convert.test.ts | 80 +++++++++++++ packages/convert/src/pyrogram/convert.ts | 46 +++++++ packages/convert/src/pyrogram/parse.test.ts | 48 ++++++++ packages/convert/src/pyrogram/parse.ts | 62 ++++++++++ .../convert/src/pyrogram/serialize.test.ts | 52 ++++++++ packages/convert/src/pyrogram/serialize.ts | 45 +++++++ packages/convert/src/pyrogram/types.ts | 8 ++ .../src/telethon/__fixtures__/generate.py | 22 ++++ .../src/telethon/__fixtures__/generate_v6.py | 26 ++++ .../src/telethon/__fixtures__/session.ts | 2 + .../src/telethon/__fixtures__/session_v6.ts | 2 + packages/convert/src/telethon/convert.test.ts | 72 +++++++++++ packages/convert/src/telethon/convert.ts | 46 +++++++ packages/convert/src/telethon/parse.test.ts | 47 ++++++++ packages/convert/src/telethon/parse.ts | 38 ++++++ .../convert/src/telethon/serialize.test.ts | 51 ++++++++ packages/convert/src/telethon/serialize.ts | 37 ++++++ packages/convert/src/telethon/types.ts | 7 ++ packages/convert/src/types.ts | 0 packages/convert/src/utils/ip.ts | 48 ++++++++ packages/convert/src/utils/rle.ts | 56 +++++++++ packages/convert/tsconfig.json | 13 ++ packages/convert/typedoc.cjs | 4 + pnpm-lock.yaml | 13 ++ 50 files changed, 1537 insertions(+), 1 deletion(-) create mode 100644 packages/convert/README.md create mode 100644 packages/convert/package.json create mode 100644 packages/convert/src/dcs.ts create mode 100644 packages/convert/src/gramjs/__fixtures__/generate.cjs create mode 100644 packages/convert/src/gramjs/__fixtures__/session.ts create mode 100644 packages/convert/src/gramjs/convert.test.ts create mode 100644 packages/convert/src/gramjs/convert.ts create mode 100644 packages/convert/src/gramjs/parse.test.ts create mode 100644 packages/convert/src/gramjs/parse.ts create mode 100644 packages/convert/src/gramjs/serialize.test.ts create mode 100644 packages/convert/src/gramjs/serialize.ts create mode 100644 packages/convert/src/gramjs/types.ts create mode 100644 packages/convert/src/mtkruto/__fixtures__/generate.js create mode 100644 packages/convert/src/mtkruto/__fixtures__/session.ts create mode 100644 packages/convert/src/mtkruto/convert.test.ts create mode 100644 packages/convert/src/mtkruto/convert.ts create mode 100644 packages/convert/src/mtkruto/parse.test.ts create mode 100644 packages/convert/src/mtkruto/parse.ts create mode 100644 packages/convert/src/mtkruto/serialize.test.ts create mode 100644 packages/convert/src/mtkruto/serialize.ts create mode 100644 packages/convert/src/mtkruto/types.ts create mode 100644 packages/convert/src/pyrogram/__fixtures__/generate.py create mode 100644 packages/convert/src/pyrogram/__fixtures__/generate_old.py create mode 100644 packages/convert/src/pyrogram/__fixtures__/session.ts create mode 100644 packages/convert/src/pyrogram/__fixtures__/session_old.ts create mode 100644 packages/convert/src/pyrogram/convert.test.ts create mode 100644 packages/convert/src/pyrogram/convert.ts create mode 100644 packages/convert/src/pyrogram/parse.test.ts create mode 100644 packages/convert/src/pyrogram/parse.ts create mode 100644 packages/convert/src/pyrogram/serialize.test.ts create mode 100644 packages/convert/src/pyrogram/serialize.ts create mode 100644 packages/convert/src/pyrogram/types.ts create mode 100644 packages/convert/src/telethon/__fixtures__/generate.py create mode 100644 packages/convert/src/telethon/__fixtures__/generate_v6.py create mode 100644 packages/convert/src/telethon/__fixtures__/session.ts create mode 100644 packages/convert/src/telethon/__fixtures__/session_v6.ts create mode 100644 packages/convert/src/telethon/convert.test.ts create mode 100644 packages/convert/src/telethon/convert.ts create mode 100644 packages/convert/src/telethon/parse.test.ts create mode 100644 packages/convert/src/telethon/parse.ts create mode 100644 packages/convert/src/telethon/serialize.test.ts create mode 100644 packages/convert/src/telethon/serialize.ts create mode 100644 packages/convert/src/telethon/types.ts create mode 100644 packages/convert/src/types.ts create mode 100644 packages/convert/src/utils/ip.ts create mode 100644 packages/convert/src/utils/rle.ts create mode 100644 packages/convert/tsconfig.json create mode 100644 packages/convert/typedoc.cjs diff --git a/.config/tsconfig.build.json b/.config/tsconfig.build.json index 18f56a25..24ed8f74 100644 --- a/.config/tsconfig.build.json +++ b/.config/tsconfig.build.json @@ -2,6 +2,7 @@ "extends": "../tsconfig.json", "exclude": [ "../**/*.test.ts", - "../**/*.test-utils.ts" + "../**/*.test-utils.ts", + "../**/__fixtures__/**", ] } diff --git a/packages/convert/README.md b/packages/convert/README.md new file mode 100644 index 00000000..c080b07b --- /dev/null +++ b/packages/convert/README.md @@ -0,0 +1,22 @@ +# @mtcute/file-id + +📖 [API Reference](https://ref.mtcute.dev/modules/_mtcute_file_id.html) + +This package is used internally by `@mtcute/core` to parse, serialize +and manipulate TDLib and Bot API compatible File IDs, but can also be used +for any other purposes. + +## Acknowledgements +This is basically a port of a portion of TDLib APIs, but greatly +simplified in usage and made to work seamlessly with the rest of the +mtcute APIs. + +This is a list of files from TDLib repository, from which most of the code was taken: + - [td/telegram/files/FileManager.cpp](https://github.com/tdlib/td/blob/master/td/telegram/files/FileManager.cpp) + - [td/telegram/files/FileLocation.hpp](https://github.com/tdlib/td/blob/master/td/telegram/files/FileLocation.hpp) + - [td/telegram/PhotoSizeSource.h](https://github.com/tdlib/td/blob/master/td/telegram/PhotoSizeSource.h) + - [td/telegram/PhotoSizeSource.hpp](https://github.com/tdlib/td/blob/master/td/telegram/PhotoSizeSource.hpp) + - [td/telegram/Version.h](https://github.com/tdlib/td/blob/master/td/telegram/Version.h) + +Additionally, some of the test cases were taken from a similar Python +library, [luckydonald/telegram_file_id](https://github.com/luckydonald/telegram_file_id) diff --git a/packages/convert/package.json b/packages/convert/package.json new file mode 100644 index 00000000..8c88175c --- /dev/null +++ b/packages/convert/package.json @@ -0,0 +1,29 @@ +{ + "name": "@mtcute/convert", + "private": true, + "version": "0.7.0", + "description": "Cross-library session conversion utilities", + "author": "Alina Sireneva ", + "license": "MIT", + "main": "src/index.ts", + "type": "module", + "sideEffects": false, + "scripts": { + "build": "pnpm run -w build-package convert" + }, + "distOnlyFields": { + "exports": { + ".": { + "import": "./esm/index.js", + "require": "./cjs/index.js" + } + } + }, + "dependencies": { + "@mtcute/core": "workspace:^", + "@mtcute/tl": "*" + }, + "devDependencies": { + "@mtcute/test": "workspace:^" + } +} diff --git a/packages/convert/src/dcs.ts b/packages/convert/src/dcs.ts new file mode 100644 index 00000000..a02f883b --- /dev/null +++ b/packages/convert/src/dcs.ts @@ -0,0 +1,113 @@ +import { DcOptions } from '@mtcute/core/utils.js' + +// some libraries only store the DCs in the source code, so we need to map them to the correct DCs +// this may not be very accurate, but it's better than nothing +// we *could* always map those to the primary dc (the client should handle that gracefully), +// but imo it's better to be as accurate as possible +// we'll also only map to ipv4 since that's more portable + +export const DC_MAPPING_PROD: Record = { + '1': { + main: { + id: 1, + ipAddress: '149.154.175.56', + port: 443, + }, + media: { + id: 1, + ipAddress: '149.154.175.211', + port: 443, + }, + }, + '2': { + main: { + id: 2, + ipAddress: '149.154.167.41', + port: 443, + }, + media: { + id: 2, + ipAddress: '149.154.167.35', + port: 443, + }, + }, + '3': { + main: { + id: 3, + ipAddress: '149.154.175.100', + port: 443, + }, + media: { + id: 3, + ipAddress: '149.154.175.100', + port: 443, + }, + }, + '4': { + main: { + id: 4, + ipAddress: '149.154.167.91', + port: 443, + }, + media: { + id: 4, + ipAddress: '149.154.167.255', + port: 443, + }, + }, + '5': { + main: { + id: 5, + ipAddress: '91.108.56.179', + port: 443, + }, + media: { + id: 5, + ipAddress: '149.154.171.255', + port: 443, + }, + }, +} + +export const DC_MAPPING_TEST: Record = { + '1': { + main: { + id: 1, + ipAddress: '149.154.175.10', + port: 80, + }, + media: { + id: 1, + ipAddress: '149.154.175.10', + port: 80, + }, + }, + '2': { + main: { + id: 2, + ipAddress: '149.154.167.40', + port: 443, + }, + media: { + id: 2, + ipAddress: '149.154.167.40', + port: 443, + }, + }, + '3': { + main: { + id: 3, + ipAddress: '149.154.175.117', + port: 443, + }, + media: { + id: 3, + ipAddress: '149.154.175.117', + port: 443, + }, + }, +} + +export function isTestDc(ip: string): boolean { + return Object.values(DC_MAPPING_TEST).some((dc) => dc.main.ipAddress === ip || dc.media.ipAddress === ip) +} diff --git a/packages/convert/src/gramjs/__fixtures__/generate.cjs b/packages/convert/src/gramjs/__fixtures__/generate.cjs new file mode 100644 index 00000000..982fba2d --- /dev/null +++ b/packages/convert/src/gramjs/__fixtures__/generate.cjs @@ -0,0 +1,46 @@ +/* eslint-disable no-console */ +const { execSync } = require('child_process') +const fs = require('fs') + +const VERSION = '2.19.20' +const TMP_DIR = '/tmp/gramjs' + +async function main() { + if (!fs.existsSync(TMP_DIR)) { + execSync(`mkdir -p ${TMP_DIR}`) + execSync(`npm install telegram@${VERSION}`, { + cwd: TMP_DIR, + stdio: 'inherit', + }) + console.log('Installed gramjs') + } + + // crutches for webpack + const gramjs = require(`${TMP_DIR}/node_modules/telegram`) + + const apiId = Number(process.env.API_ID) + const apiHash = process.env.API_HASH + const stringSession = new gramjs.sessions.StringSession('') + stringSession.setDC(2, '149.154.167.40', 443) + + const client = new gramjs.TelegramClient(stringSession, apiId, apiHash, { + connectionRetries: 5, + }) + + await client.start({ + phoneNumber: async () => '9996621234', + phoneCode: async () => '22222', + onError: console.error, + }) + + const session = stringSession.save() + + fs.writeFileSync( + `${__dirname}/session.ts`, + `export const GRAMJS_SESSION = '${session}'\n`, + ) + + await client.destroy() +} + +main().catch(console.error) diff --git a/packages/convert/src/gramjs/__fixtures__/session.ts b/packages/convert/src/gramjs/__fixtures__/session.ts new file mode 100644 index 00000000..b05d3643 --- /dev/null +++ b/packages/convert/src/gramjs/__fixtures__/session.ts @@ -0,0 +1,2 @@ +export const GRAMJS_SESSION = + '1AgAOMTQ5LjE1NC4xNjcuNDABu60obcEYS8Yb/I7YlCwaLvW84dXCX2oGnBYG+zuMciJhHP99c8ZJvwxJgH8yU1QrqI+Gh0kK0JAuQucIpDfq/jJVLZ1ZRimq5yy1XbeEs65gtZA1+SUwZRXahh+NzGbPmOVUMCnCtRONo9GNvcx/QxSXRrh7T/K0YYN1iHsK1vJDk8/SUnthvTNmRycC+JLn4fMtctqP4Le2WPOH/deYbUF0BlwmR77M7fv1GZSInqCgWReaIl5nvn0IqA4mOCTkdOgcvwOiB2UmXwiyInxRuLdBIyLbBUDCuTlmL1m3FJqbuEpZEUJnoJf2YDFZ1wR6TfL0MUS1VwnjOcy3WIIFwwg=' diff --git a/packages/convert/src/gramjs/convert.test.ts b/packages/convert/src/gramjs/convert.test.ts new file mode 100644 index 00000000..1572fbf6 --- /dev/null +++ b/packages/convert/src/gramjs/convert.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from 'vitest' + +import { getPlatform } from '@mtcute/core/platform.js' + +import { GRAMJS_SESSION } from './__fixtures__/session.js' +import { convertFromGramjsSession, convertToGramjsSession } from './convert.js' + +describe('gramjs/convert', () => { + it('should correctly convert from gramjs sessions', () => { + expect(convertFromGramjsSession(GRAMJS_SESSION)).toEqual({ + authKey: getPlatform().hexDecode( + 'ad286dc1184bc61bfc8ed8942c1a2ef5bce1d5c25f6a069c1606fb3b8c722261' + + '1cff7d73c649bf0c49807f3253542ba88f8687490ad0902e42e708a437eafe32' + + '552d9d594629aae72cb55db784b3ae60b59035f925306515da861f8dcc66cf98' + + 'e5543029c2b5138da3d18dbdcc7f43149746b87b4ff2b4618375887b0ad6f243' + + '93cfd2527b61bd3366472702f892e7e1f32d72da8fe0b7b658f387fdd7986d41' + + '74065c2647beccedfbf51994889ea0a059179a225e67be7d08a80e263824e474' + + 'e81cbf03a20765265f08b2227c51b8b7412322db0540c2b939662f59b7149a9b' + + 'b84a59114267a097f6603159d7047a4df2f43144b55709e339ccb7588205c308', + ), + primaryDcs: { + main: { + id: 2, + ipAddress: '149.154.167.40', + ipv6: false, + port: 443, + }, + media: { + id: 2, + ipAddress: '149.154.167.40', + ipv6: false, + port: 443, + }, + }, + testMode: true, + version: 3, + }) + }) + + it('should correctly convert to gramjs sessions', () => { + expect( + convertToGramjsSession({ + authKey: getPlatform().hexDecode( + 'ad286dc1184bc61bfc8ed8942c1a2ef5bce1d5c25f6a069c1606fb3b8c722261' + + '1cff7d73c649bf0c49807f3253542ba88f8687490ad0902e42e708a437eafe32' + + '552d9d594629aae72cb55db784b3ae60b59035f925306515da861f8dcc66cf98' + + 'e5543029c2b5138da3d18dbdcc7f43149746b87b4ff2b4618375887b0ad6f243' + + '93cfd2527b61bd3366472702f892e7e1f32d72da8fe0b7b658f387fdd7986d41' + + '74065c2647beccedfbf51994889ea0a059179a225e67be7d08a80e263824e474' + + 'e81cbf03a20765265f08b2227c51b8b7412322db0540c2b939662f59b7149a9b' + + 'b84a59114267a097f6603159d7047a4df2f43144b55709e339ccb7588205c308', + ), + primaryDcs: { + main: { + id: 2, + ipAddress: '149.154.167.40', + ipv6: false, + port: 443, + }, + media: { + id: 2, + ipAddress: '149.154.167.40', + ipv6: false, + port: 443, + }, + }, + testMode: true, + version: 3, + }), + ).toEqual(GRAMJS_SESSION) + }) +}) diff --git a/packages/convert/src/gramjs/convert.ts b/packages/convert/src/gramjs/convert.ts new file mode 100644 index 00000000..18020487 --- /dev/null +++ b/packages/convert/src/gramjs/convert.ts @@ -0,0 +1,29 @@ +import { readStringSession, StringSessionData } from '@mtcute/core/utils.js' +import { __tlReaderMap } from '@mtcute/tl/binary/reader.js' + +import { convertFromTelethonSession } from '../telethon/convert.js' +import { TelethonSession } from '../telethon/types.js' +import { parseGramjsSession } from './parse.js' +import { serializeGramjsSession } from './serialize.js' + +export function convertFromGramjsSession(session: TelethonSession | string): StringSessionData { + if (typeof session === 'string') { + session = parseGramjsSession(session) + } + + return convertFromTelethonSession(session) +} + +export function convertToGramjsSession(session: StringSessionData | string): string { + if (typeof session === 'string') { + session = readStringSession(__tlReaderMap, session) + } + + return serializeGramjsSession({ + dcId: session.primaryDcs.main.id, + ipAddress: session.primaryDcs.main.ipAddress, + port: session.primaryDcs.main.port, + ipv6: session.primaryDcs.main.ipv6 ?? false, + authKey: session.authKey, + }) +} diff --git a/packages/convert/src/gramjs/parse.test.ts b/packages/convert/src/gramjs/parse.test.ts new file mode 100644 index 00000000..ec4dead1 --- /dev/null +++ b/packages/convert/src/gramjs/parse.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest' + +import { getPlatform } from '@mtcute/core/platform.js' + +import { GRAMJS_SESSION } from './__fixtures__/session.js' +import { parseGramjsSession } from './parse.js' + +describe('gramjs/parse', () => { + it('should correctly parse sessions', () => { + expect(parseGramjsSession(GRAMJS_SESSION)).toEqual({ + dcId: 2, + ipAddress: '149.154.167.40', + port: 443, + ipv6: false, + authKey: getPlatform().hexDecode( + 'ad286dc1184bc61bfc8ed8942c1a2ef5bce1d5c25f6a069c1606fb3b8c722261' + + '1cff7d73c649bf0c49807f3253542ba88f8687490ad0902e42e708a437eafe32' + + '552d9d594629aae72cb55db784b3ae60b59035f925306515da861f8dcc66cf98' + + 'e5543029c2b5138da3d18dbdcc7f43149746b87b4ff2b4618375887b0ad6f243' + + '93cfd2527b61bd3366472702f892e7e1f32d72da8fe0b7b658f387fdd7986d41' + + '74065c2647beccedfbf51994889ea0a059179a225e67be7d08a80e263824e474' + + 'e81cbf03a20765265f08b2227c51b8b7412322db0540c2b939662f59b7149a9b' + + 'b84a59114267a097f6603159d7047a4df2f43144b55709e339ccb7588205c308', + ), + }) + }) +}) diff --git a/packages/convert/src/gramjs/parse.ts b/packages/convert/src/gramjs/parse.ts new file mode 100644 index 00000000..950a975c --- /dev/null +++ b/packages/convert/src/gramjs/parse.ts @@ -0,0 +1,35 @@ +import { MtArgumentError } from '@mtcute/core' +import { getPlatform } from '@mtcute/core/platform.js' +import { dataViewFromBuffer } from '@mtcute/core/utils.js' + +import { TelethonSession } from '../telethon/types.js' + +export function parseGramjsSession(session: string): TelethonSession { + if (session[0] !== '1') { + // version + throw new MtArgumentError(`Invalid session string (version = ${session[0]})`) + } + + session = session.slice(1) + + const data = getPlatform().base64Decode(session) + const dv = dataViewFromBuffer(data) + + const dcId = dv.getUint8(0) + + const ipSize = dv.getUint16(1) + let pos = 3 + ipSize + + const ip = getPlatform().utf8Decode(data.subarray(3, pos)) + const port = dv.getUint16(pos) + pos += 2 + const authKey = data.subarray(pos, pos + 256) + + return { + dcId, + ipAddress: ip, + ipv6: ip.includes(':'), // dumb check but gramjs does this + port, + authKey, + } +} diff --git a/packages/convert/src/gramjs/serialize.test.ts b/packages/convert/src/gramjs/serialize.test.ts new file mode 100644 index 00000000..57936fe2 --- /dev/null +++ b/packages/convert/src/gramjs/serialize.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest' + +import { getPlatform } from '@mtcute/core/platform.js' + +import { GRAMJS_SESSION } from './__fixtures__/session.js' +import { serializeGramjsSession } from './serialize.js' + +describe('gramjs/serialize', () => { + it('should correctly serialize sessions', () => { + expect( + serializeGramjsSession({ + dcId: 2, + ipAddress: '149.154.167.40', + port: 443, + ipv6: false, + authKey: getPlatform().hexDecode( + 'ad286dc1184bc61bfc8ed8942c1a2ef5bce1d5c25f6a069c1606fb3b8c722261' + + '1cff7d73c649bf0c49807f3253542ba88f8687490ad0902e42e708a437eafe32' + + '552d9d594629aae72cb55db784b3ae60b59035f925306515da861f8dcc66cf98' + + 'e5543029c2b5138da3d18dbdcc7f43149746b87b4ff2b4618375887b0ad6f243' + + '93cfd2527b61bd3366472702f892e7e1f32d72da8fe0b7b658f387fdd7986d41' + + '74065c2647beccedfbf51994889ea0a059179a225e67be7d08a80e263824e474' + + 'e81cbf03a20765265f08b2227c51b8b7412322db0540c2b939662f59b7149a9b' + + 'b84a59114267a097f6603159d7047a4df2f43144b55709e339ccb7588205c308', + ), + }), + ).toEqual(GRAMJS_SESSION) + }) +}) diff --git a/packages/convert/src/gramjs/serialize.ts b/packages/convert/src/gramjs/serialize.ts new file mode 100644 index 00000000..adaa0178 --- /dev/null +++ b/packages/convert/src/gramjs/serialize.ts @@ -0,0 +1,28 @@ +import { MtArgumentError } from '@mtcute/core' +import { getPlatform } from '@mtcute/core/platform.js' +import { dataViewFromBuffer } from '@mtcute/core/utils.js' + +import { TelethonSession } from '../telethon/types.js' + +export function serializeGramjsSession(session: TelethonSession) { + if (session.authKey.length !== 256) { + throw new MtArgumentError('authKey must be 256 bytes long') + } + + const ipEncoded = getPlatform().utf8Encode(session.ipAddress) + + const u8 = new Uint8Array(261 + ipEncoded.length) + const dv = dataViewFromBuffer(u8) + + dv.setUint8(0, session.dcId) + dv.setUint16(1, ipEncoded.length) + u8.set(ipEncoded, 3) + + let pos = 3 + ipEncoded.length + + dv.setUint16(pos, session.port) + pos += 2 + u8.set(session.authKey, pos) + + return '1' + getPlatform().base64Encode(u8) +} diff --git a/packages/convert/src/gramjs/types.ts b/packages/convert/src/gramjs/types.ts new file mode 100644 index 00000000..96389f6b --- /dev/null +++ b/packages/convert/src/gramjs/types.ts @@ -0,0 +1,7 @@ +export interface GramjsSession { + dcId: number + ipAddress: string + ipv6: boolean + port: number + authKey: Uint8Array +} diff --git a/packages/convert/src/mtkruto/__fixtures__/generate.js b/packages/convert/src/mtkruto/__fixtures__/generate.js new file mode 100644 index 00000000..b0e353ab --- /dev/null +++ b/packages/convert/src/mtkruto/__fixtures__/generate.js @@ -0,0 +1,15 @@ +/* eslint-disable import/no-unresolved, no-undef, no-console */ + +import { Client, StorageMemory } from 'https://deno.land/x/mtkruto@0.1.157/mod.ts' + +const client = new Client(new StorageMemory(), Number(Deno.env.get('API_ID')), Deno.env.get('API_HASH'), { + initialDc: '2-test', +}) + +await client.start({ + phone: () => '9996621234', + code: () => '22222', +}) + +const authString = await client.exportAuthString() +console.log('The auth string is', authString) diff --git a/packages/convert/src/mtkruto/__fixtures__/session.ts b/packages/convert/src/mtkruto/__fixtures__/session.ts new file mode 100644 index 00000000..348e0acd --- /dev/null +++ b/packages/convert/src/mtkruto/__fixtures__/session.ts @@ -0,0 +1,2 @@ +export const MTKRUTO_SESSION = + 'BjItdGVzdAAB_gABAQABWEIKa07Ch-9zoA024mDOpsv20TW4YwuoRRROqSi41YQCbD3c4nKnz7BcFIu1mfn6f6Xm3OTVqoT0zib4p_AuZD9H-t8j5AagecRg-oSpQlmjoiUazKQSxnxWotGWf1mPNntAeOvDNa5t1NjXUxmqdB3e2AjYLF_E2jDESVgUuDBQUMBHIDc_xFBAlz6kVxCZ6iINJHbnyJ2F19tbEPFJvSM999RKaFj5lUUVs0qKNXEUmsFYUuIdPBzjWilY8Uvf9nYU_xXd9CUAAXS5_i4aaWlHoTIf3zn8ZEINhDIU1DMauh5vhSWt7F0fkxODjtou-7PdIunuDtqyQm4steuNJc8' diff --git a/packages/convert/src/mtkruto/convert.test.ts b/packages/convert/src/mtkruto/convert.test.ts new file mode 100644 index 00000000..939f0699 --- /dev/null +++ b/packages/convert/src/mtkruto/convert.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from 'vitest' + +import { u8HexDecode } from '@mtcute/test' + +import { MTKRUTO_SESSION } from './__fixtures__/session.js' +import { convertFromMtkrutoSession, convertToMtkrutoSession } from './convert.js' + +describe('mtkruto/convert', () => { + it('should correctly convert from mtkruto sessions', () => { + expect(convertFromMtkrutoSession(MTKRUTO_SESSION)).toEqual({ + authKey: u8HexDecode( + '58420a6b4ec287ef73a00d36e260cea6cbf6d135b8630ba845144ea928b8d584' + + '026c3ddce272a7cfb05c148bb599f9fa7fa5e6dce4d5aa84f4ce26f8a7f02e64' + + '3f47fadf23e406a079c460fa84a94259a3a2251acca412c67c56a2d1967f598f' + + '367b4078ebc335ae6dd4d8d75319aa741dded808d82c5fc4da30c4495814b830' + + '5050c04720373fc45040973ea4571099ea220d2476e7c89d85d7db5b10f149bd' + + '233df7d44a6858f9954515b34a8a3571149ac15852e21d3c1ce35a2958f14bdf' + + 'f67614ff15ddf4250074b9fe2e1a696947a1321fdf39fc64420d843214d4331a' + + 'ba1e6f8525adec5d1f9313838eda2efbb3dd22e9ee0edab2426e2cb5eb8d25cf', + ), + primaryDcs: { + main: { + id: 2, + ipAddress: '149.154.167.40', + port: 443, + }, + media: { + id: 2, + ipAddress: '149.154.167.40', + port: 443, + }, + }, + testMode: true, + version: 3, + }) + }) + + it('should correctly convert to mtkruto sessions', () => { + expect( + convertToMtkrutoSession({ + authKey: u8HexDecode( + '58420a6b4ec287ef73a00d36e260cea6cbf6d135b8630ba845144ea928b8d584' + + '026c3ddce272a7cfb05c148bb599f9fa7fa5e6dce4d5aa84f4ce26f8a7f02e64' + + '3f47fadf23e406a079c460fa84a94259a3a2251acca412c67c56a2d1967f598f' + + '367b4078ebc335ae6dd4d8d75319aa741dded808d82c5fc4da30c4495814b830' + + '5050c04720373fc45040973ea4571099ea220d2476e7c89d85d7db5b10f149bd' + + '233df7d44a6858f9954515b34a8a3571149ac15852e21d3c1ce35a2958f14bdf' + + 'f67614ff15ddf4250074b9fe2e1a696947a1321fdf39fc64420d843214d4331a' + + 'ba1e6f8525adec5d1f9313838eda2efbb3dd22e9ee0edab2426e2cb5eb8d25cf', + ), + primaryDcs: { + main: { + id: 2, + ipAddress: '149.154.167.40', + ipv6: false, + port: 443, + }, + media: { + id: 2, + ipAddress: '149.154.167.40', + ipv6: false, + port: 443, + }, + }, + testMode: true, + version: 3, + }), + ).toEqual(MTKRUTO_SESSION) + }) +}) diff --git a/packages/convert/src/mtkruto/convert.ts b/packages/convert/src/mtkruto/convert.ts new file mode 100644 index 00000000..b53d6791 --- /dev/null +++ b/packages/convert/src/mtkruto/convert.ts @@ -0,0 +1,32 @@ +import { readStringSession, StringSessionData } from '@mtcute/core/utils.js' +import { __tlReaderMap } from '@mtcute/tl/binary/reader.js' + +import { DC_MAPPING_PROD, DC_MAPPING_TEST } from '../dcs.js' +import { parseMtkrutoSession } from './parse.js' +import { serializeMtkrutoSession } from './serialize.js' +import { MtkrutoSession } from './types.js' + +export function convertFromMtkrutoSession(session: MtkrutoSession | string): StringSessionData { + if (typeof session === 'string') { + session = parseMtkrutoSession(session) + } + + return { + version: 3, + testMode: session.isTest, + primaryDcs: (session.isTest ? DC_MAPPING_TEST : DC_MAPPING_PROD)[session.dcId], + authKey: session.authKey, + } +} + +export function convertToMtkrutoSession(session: StringSessionData | string): string { + if (typeof session === 'string') { + session = readStringSession(__tlReaderMap, session) + } + + return serializeMtkrutoSession({ + dcId: session.primaryDcs.main.id, + isTest: session.testMode, + authKey: session.authKey, + }) +} diff --git a/packages/convert/src/mtkruto/parse.test.ts b/packages/convert/src/mtkruto/parse.test.ts new file mode 100644 index 00000000..77aa19d4 --- /dev/null +++ b/packages/convert/src/mtkruto/parse.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest' + +import { u8HexDecode } from '@mtcute/test' + +import { MTKRUTO_SESSION } from './__fixtures__/session.js' +import { parseMtkrutoSession } from './parse.js' + +describe('mtkruto/parse', () => { + it('should correctly parse sessions', () => { + expect(parseMtkrutoSession(MTKRUTO_SESSION)).toEqual({ + dcId: 2, + isTest: true, + authKey: u8HexDecode( + '58420a6b4ec287ef73a00d36e260cea6cbf6d135b8630ba845144ea928b8d584' + + '026c3ddce272a7cfb05c148bb599f9fa7fa5e6dce4d5aa84f4ce26f8a7f02e64' + + '3f47fadf23e406a079c460fa84a94259a3a2251acca412c67c56a2d1967f598f' + + '367b4078ebc335ae6dd4d8d75319aa741dded808d82c5fc4da30c4495814b830' + + '5050c04720373fc45040973ea4571099ea220d2476e7c89d85d7db5b10f149bd' + + '233df7d44a6858f9954515b34a8a3571149ac15852e21d3c1ce35a2958f14bdf' + + 'f67614ff15ddf4250074b9fe2e1a696947a1321fdf39fc64420d843214d4331a' + + 'ba1e6f8525adec5d1f9313838eda2efbb3dd22e9ee0edab2426e2cb5eb8d25cf', + ), + }) + }) +}) diff --git a/packages/convert/src/mtkruto/parse.ts b/packages/convert/src/mtkruto/parse.ts new file mode 100644 index 00000000..796e00e9 --- /dev/null +++ b/packages/convert/src/mtkruto/parse.ts @@ -0,0 +1,31 @@ +import { MtArgumentError } from '@mtcute/core' +import { getPlatform } from '@mtcute/core/platform.js' +import { TlBinaryReader } from '@mtcute/core/utils.js' + +import { telegramRleDecode } from '../utils/rle.js' +import { MtkrutoSession } from './types.js' + +export function parseMtkrutoSession(session: string): MtkrutoSession { + const data = telegramRleDecode(getPlatform().base64Decode(session, true)) + const reader = TlBinaryReader.manual(data) + + let dcIdStr = reader.string() + const authKey = reader.bytes() + + const isTest = dcIdStr.endsWith('-test') + + if (isTest) { + dcIdStr = dcIdStr.slice(0, -5) + } + const dcId = Number(dcIdStr) + + if (isNaN(dcId)) { + throw new MtArgumentError(`Invalid DC ID: ${dcIdStr}`) + } + + return { + dcId, + isTest, + authKey, + } +} diff --git a/packages/convert/src/mtkruto/serialize.test.ts b/packages/convert/src/mtkruto/serialize.test.ts new file mode 100644 index 00000000..fa945c17 --- /dev/null +++ b/packages/convert/src/mtkruto/serialize.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest' + +import { u8HexDecode } from '@mtcute/test' + +import { MTKRUTO_SESSION } from './__fixtures__/session.js' +import { serializeMtkrutoSession } from './serialize.js' + +describe('mtkruto/serialize', () => { + it('should correctly serialize sessions', () => { + expect( + serializeMtkrutoSession({ + dcId: 2, + isTest: true, + authKey: u8HexDecode( + '58420a6b4ec287ef73a00d36e260cea6cbf6d135b8630ba845144ea928b8d584' + + '026c3ddce272a7cfb05c148bb599f9fa7fa5e6dce4d5aa84f4ce26f8a7f02e64' + + '3f47fadf23e406a079c460fa84a94259a3a2251acca412c67c56a2d1967f598f' + + '367b4078ebc335ae6dd4d8d75319aa741dded808d82c5fc4da30c4495814b830' + + '5050c04720373fc45040973ea4571099ea220d2476e7c89d85d7db5b10f149bd' + + '233df7d44a6858f9954515b34a8a3571149ac15852e21d3c1ce35a2958f14bdf' + + 'f67614ff15ddf4250074b9fe2e1a696947a1321fdf39fc64420d843214d4331a' + + 'ba1e6f8525adec5d1f9313838eda2efbb3dd22e9ee0edab2426e2cb5eb8d25cf', + ), + }), + ).toEqual(MTKRUTO_SESSION) + }) +}) diff --git a/packages/convert/src/mtkruto/serialize.ts b/packages/convert/src/mtkruto/serialize.ts new file mode 100644 index 00000000..1e7986cc --- /dev/null +++ b/packages/convert/src/mtkruto/serialize.ts @@ -0,0 +1,16 @@ +import { getPlatform } from '@mtcute/core/platform.js' +import { TlBinaryWriter } from '@mtcute/core/utils.js' + +import { telegramRleEncode } from '../utils/rle.js' +import { MtkrutoSession } from './types.js' + +export function serializeMtkrutoSession(session: MtkrutoSession): string { + const dcIdStr = `${session.dcId}${session.isTest ? '-test' : ''}` + + const writer = TlBinaryWriter.manual(session.authKey.length + dcIdStr.length + 8) + + writer.string(dcIdStr) + writer.bytes(session.authKey) + + return getPlatform().base64Encode(telegramRleEncode(writer.result()), true) +} diff --git a/packages/convert/src/mtkruto/types.ts b/packages/convert/src/mtkruto/types.ts new file mode 100644 index 00000000..ff85b7a2 --- /dev/null +++ b/packages/convert/src/mtkruto/types.ts @@ -0,0 +1,5 @@ +export interface MtkrutoSession { + dcId: number + isTest: boolean + authKey: Uint8Array +} diff --git a/packages/convert/src/pyrogram/__fixtures__/generate.py b/packages/convert/src/pyrogram/__fixtures__/generate.py new file mode 100644 index 00000000..63b6d6de --- /dev/null +++ b/packages/convert/src/pyrogram/__fixtures__/generate.py @@ -0,0 +1,20 @@ +from pyrogram import Client, filters +import asyncio +import os + +async def main(): + async with Client( + "my_account_test", + api_id=int(os.environ["API_ID"]), + api_hash=os.environ["API_HASH"], + test_mode=True, + in_memory=True, + phone_number="9996621234", + phone_code="22222" + ) as app: + session = await app.export_session_string() + open("session.ts", "w").write( + 'export const PYROGRAM_TEST_SESSION = \'' + session + '\'\n' + ) + +asyncio.get_event_loop().run_until_complete(main()) \ No newline at end of file diff --git a/packages/convert/src/pyrogram/__fixtures__/generate_old.py b/packages/convert/src/pyrogram/__fixtures__/generate_old.py new file mode 100644 index 00000000..0c0c5d7d --- /dev/null +++ b/packages/convert/src/pyrogram/__fixtures__/generate_old.py @@ -0,0 +1,24 @@ +# pip install pyrogram==1.4.16 +# also it needs to be patched a bit because its broken smh +from pyrogram import Client, filters +import asyncio +import os +import logging + +logging.basicConfig(level=logging.DEBUG) + +async def main(): + async with Client( + ":memory:", + api_id=int(os.environ["API_ID"]), + api_hash=os.environ["API_HASH"], + test_mode=True, + phone_number="9996621234", + phone_code="22222" + ) as app: + session = await app.export_session_string() + open("session_old.ts", "w").write( + 'export const PYROGRAM_TEST_SESSION_OLD = \'' + session + '\'\n' + ) + +asyncio.get_event_loop().run_until_complete(main()) \ No newline at end of file diff --git a/packages/convert/src/pyrogram/__fixtures__/session.ts b/packages/convert/src/pyrogram/__fixtures__/session.ts new file mode 100644 index 00000000..58642802 --- /dev/null +++ b/packages/convert/src/pyrogram/__fixtures__/session.ts @@ -0,0 +1,2 @@ +export const PYROGRAM_TEST_SESSION = + 'AgAyyvcBTk6KssqikKPxEhxfXJpkoFIgQ_o8VpCk_4g0tcHe0rVCXx34AaDKvaNOlbkJOZ4jA3AI8iDYkI2opuifbM_7S2u9MMdnrjfg5jpfkXfI9-wF8DK_UBGIe1zk_Ibn0IHLRz-lkb-QqZNhh8O8Ggb8cieamatEYwLrkjkZR7JG53q76F0ktUd22L6_bUlp9p_qgXqBg8vZdkIIs9T1OiShw2X6TNO0lYqfJVaczMVQcT9Zt0FiyrAMpovFuT7-96OFKWcQ9gzrs_SHfz9HrQgBwvNSdkVziXTtxLJXsaNz3smGeyh-CEuEgdF3enIECnzftlvvUClLN_ylcPir1bi4_wAAAAEqEi1JAA' diff --git a/packages/convert/src/pyrogram/__fixtures__/session_old.ts b/packages/convert/src/pyrogram/__fixtures__/session_old.ts new file mode 100644 index 00000000..6951698f --- /dev/null +++ b/packages/convert/src/pyrogram/__fixtures__/session_old.ts @@ -0,0 +1,2 @@ +export const PYROGRAM_TEST_SESSION_OLD = + 'AgEWdHMtuA1pC01YkNiHpL1bC0yBC3wzGZCwSRWKlA_a69RhePUN3M51NpnwSXrW3pZV9FS8WjAwUkA23uT_49t8c7Umw3ihhKD6-hTpZ5wXC2MuC0EsF0-Z6WshYhT3gmN6QhEt0jlXo5cW1BJ3MYmXtsTWNf_hJfd3_wF_ZFa58ntVV-3qd08wQRhiL_IxM7L5YazjPw0dg2z92CqRARku_oq5D29V6W6bo8T-SLzF_ujj5ZcAQL25mJtCcXfhhjp9atxcrqnKzEs05xyrehnlJZKoGmnX0mF2P_6wUHqZC9tcTBUV4AmFcbuy7m_4SYLnJ8MbftNs7aWHHNcB1R4fAAAAASoSLUkA' diff --git a/packages/convert/src/pyrogram/convert.test.ts b/packages/convert/src/pyrogram/convert.test.ts new file mode 100644 index 00000000..547990ac --- /dev/null +++ b/packages/convert/src/pyrogram/convert.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from 'vitest' + +import { getPlatform } from '@mtcute/core/platform.js' + +import { PYROGRAM_TEST_SESSION_OLD } from './__fixtures__/session_old.js' +import { convertFromPyrogramSession, convertToPyrogramSession } from './convert.js' + +describe('pyrogram/convert', () => { + it('should correctly convert from pyrogram sessions', () => { + expect(convertFromPyrogramSession(PYROGRAM_TEST_SESSION_OLD)).toEqual({ + authKey: getPlatform().hexDecode( + '1674732db80d690b4d5890d887a4bd5b0b4c810b7c331990b049158a940fdaeb' + + 'd46178f50ddcce753699f0497ad6de9655f454bc5a3030524036dee4ffe3db7c' + + '73b526c378a184a0fafa14e9679c170b632e0b412c174f99e96b216214f78263' + + '7a42112dd23957a39716d41277318997b6c4d635ffe125f777ff017f6456b9f2' + + '7b5557edea774f304118622ff23133b2f961ace33f0d1d836cfdd82a9101192e' + + 'fe8ab90f6f55e96e9ba3c4fe48bcc5fee8e3e5970040bdb9989b427177e1863a' + + '7d6adc5caea9cacc4b34e71cab7a19e52592a81a69d7d261763ffeb0507a990b' + + 'db5c4c1515e0098571bbb2ee6ff84982e727c31b7ed36ceda5871cd701d51e1f', + ), + primaryDcs: { + main: { + id: 2, + ipAddress: '149.154.167.40', + port: 443, + }, + media: { + id: 2, + ipAddress: '149.154.167.40', + port: 443, + }, + }, + self: { + isBot: false, + isPremium: false, + userId: 5000801609, + usernames: [], + }, + testMode: true, + version: 3, + }) + }) + + it('should correctly convert to pyrogram sessions', () => { + expect( + convertToPyrogramSession({ + authKey: getPlatform().hexDecode( + '1674732db80d690b4d5890d887a4bd5b0b4c810b7c331990b049158a940fdaeb' + + 'd46178f50ddcce753699f0497ad6de9655f454bc5a3030524036dee4ffe3db7c' + + '73b526c378a184a0fafa14e9679c170b632e0b412c174f99e96b216214f78263' + + '7a42112dd23957a39716d41277318997b6c4d635ffe125f777ff017f6456b9f2' + + '7b5557edea774f304118622ff23133b2f961ace33f0d1d836cfdd82a9101192e' + + 'fe8ab90f6f55e96e9ba3c4fe48bcc5fee8e3e5970040bdb9989b427177e1863a' + + '7d6adc5caea9cacc4b34e71cab7a19e52592a81a69d7d261763ffeb0507a990b' + + 'db5c4c1515e0098571bbb2ee6ff84982e727c31b7ed36ceda5871cd701d51e1f', + ), + primaryDcs: { + main: { + id: 2, + ipAddress: '149.154.167.40', + port: 443, + }, + media: { + id: 2, + ipAddress: '149.154.167.40', + port: 443, + }, + }, + self: { + isBot: false, + isPremium: false, + userId: 5000801609, + usernames: [], + }, + testMode: true, + version: 3, + }), + ).toEqual(PYROGRAM_TEST_SESSION_OLD) + }) +}) diff --git a/packages/convert/src/pyrogram/convert.ts b/packages/convert/src/pyrogram/convert.ts new file mode 100644 index 00000000..ea54dc36 --- /dev/null +++ b/packages/convert/src/pyrogram/convert.ts @@ -0,0 +1,46 @@ +import { readStringSession, StringSessionData } from '@mtcute/core/utils.js' +import { __tlReaderMap } from '@mtcute/tl/binary/reader.js' + +import { DC_MAPPING_PROD, DC_MAPPING_TEST } from '../dcs.js' +import { parsePyrogramSession } from './parse.js' +import { serializePyrogramSession } from './serialize.js' +import { PyrogramSession } from './types.js' + +export function convertFromPyrogramSession(session: PyrogramSession | string): StringSessionData { + if (typeof session === 'string') { + session = parsePyrogramSession(session) + } + + return { + version: 3, + testMode: session.isTest, + primaryDcs: (session.isTest ? DC_MAPPING_TEST : DC_MAPPING_PROD)[session.dcId], + authKey: session.authKey, + self: { + userId: session.userId, + isBot: session.isBot, + isPremium: false, + usernames: [], + }, + } +} + +export function convertToPyrogramSession( + session: StringSessionData | string, + params?: { + apiId?: number + }, +): string { + if (typeof session === 'string') { + session = readStringSession(__tlReaderMap, session) + } + + return serializePyrogramSession({ + apiId: params?.apiId, + isBot: session.self?.isBot ?? false, + isTest: session.testMode, + userId: session.self?.userId ?? 0, + dcId: session.primaryDcs.main.id, + authKey: session.authKey, + }) +} diff --git a/packages/convert/src/pyrogram/parse.test.ts b/packages/convert/src/pyrogram/parse.test.ts new file mode 100644 index 00000000..69fb4a71 --- /dev/null +++ b/packages/convert/src/pyrogram/parse.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest' + +import { getPlatform } from '@mtcute/core/platform.js' + +import { PYROGRAM_TEST_SESSION } from './__fixtures__/session.js' +import { PYROGRAM_TEST_SESSION_OLD } from './__fixtures__/session_old.js' +import { parsePyrogramSession } from './parse.js' + +describe('pyrogram/parse', () => { + it('should correctly parse old sessions', () => { + expect(parsePyrogramSession(PYROGRAM_TEST_SESSION_OLD)).toEqual({ + isBot: false, + isTest: true, + userId: 5000801609, + dcId: 2, + authKey: getPlatform().hexDecode( + '1674732db80d690b4d5890d887a4bd5b0b4c810b7c331990b049158a940fdaeb' + + 'd46178f50ddcce753699f0497ad6de9655f454bc5a3030524036dee4ffe3db7c' + + '73b526c378a184a0fafa14e9679c170b632e0b412c174f99e96b216214f78263' + + '7a42112dd23957a39716d41277318997b6c4d635ffe125f777ff017f6456b9f2' + + '7b5557edea774f304118622ff23133b2f961ace33f0d1d836cfdd82a9101192e' + + 'fe8ab90f6f55e96e9ba3c4fe48bcc5fee8e3e5970040bdb9989b427177e1863a' + + '7d6adc5caea9cacc4b34e71cab7a19e52592a81a69d7d261763ffeb0507a990b' + + 'db5c4c1515e0098571bbb2ee6ff84982e727c31b7ed36ceda5871cd701d51e1f', + ), + }) + }) + + it('should correctly parse new sessions', () => { + expect(parsePyrogramSession(PYROGRAM_TEST_SESSION)).toEqual({ + apiId: 3328759, + isBot: false, + isTest: true, + userId: 5000801609, + dcId: 2, + authKey: getPlatform().hexDecode( + '4e4e8ab2caa290a3f1121c5f5c9a64a0522043fa3c5690a4ff8834b5c1ded2b5' + + '425f1df801a0cabda34e95b909399e23037008f220d8908da8a6e89f6ccffb4b' + + '6bbd30c767ae37e0e63a5f9177c8f7ec05f032bf5011887b5ce4fc86e7d081cb' + + '473fa591bf90a9936187c3bc1a06fc72279a99ab446302eb92391947b246e77a' + + 'bbe85d24b54776d8bebf6d4969f69fea817a8183cbd9764208b3d4f53a24a1c3' + + '65fa4cd3b4958a9f25569cccc550713f59b74162cab00ca68bc5b93efef7a385' + + '296710f60cebb3f4877f3f47ad0801c2f3527645738974edc4b257b1a373dec9' + + '867b287e084b8481d1777a72040a7cdfb65bef50294b37fca570f8abd5b8b8ff', + ), + }) + }) +}) diff --git a/packages/convert/src/pyrogram/parse.ts b/packages/convert/src/pyrogram/parse.ts new file mode 100644 index 00000000..1363f893 --- /dev/null +++ b/packages/convert/src/pyrogram/parse.ts @@ -0,0 +1,62 @@ +// source: https://github.com/pyrogram/pyrogram/blob/master/pyrogram/storage/storage.py + +import { Long } from '@mtcute/core' +import { getPlatform } from '@mtcute/core/platform.js' +import { dataViewFromBuffer, longFromBuffer } from '@mtcute/core/utils.js' + +import { PyrogramSession } from './types.js' + +const SESSION_STRING_SIZE = 351 +const SESSION_STRING_SIZE_64 = 356 + +export function parsePyrogramSession(session: string): PyrogramSession { + const data = getPlatform().base64Decode(session, true) + const dv = dataViewFromBuffer(data) + + if (session.length === SESSION_STRING_SIZE || session.length === SESSION_STRING_SIZE_64) { + // old format + // const OLD_SESSION_STRING_FORMAT = '>B?256sI?' + // const OLD_SESSION_STRING_FORMAT_64 = '>B?256sQ?' + const dcId = dv.getUint8(0) + const isTest = dv.getUint8(1) !== 0 + const authKey = data.subarray(2, 258) + + let userId + + if (data.length === SESSION_STRING_SIZE) { + userId = dv.getUint32(258) + } else { + const high = dv.getUint32(258) + const low = dv.getUint32(262) + userId = Long.fromBits(low, high).toNumber() + } + + const isBot = dv.getUint8(data.length - 1) !== 0 + + return { + dcId, + isTest, + authKey, + userId, + isBot, + } + } + + // new format + // const SESSION_STRING_FORMAT = '>BI?256sQ?' + const dcId = dv.getUint8(0) + const apiId = dv.getUint32(1) + const isTest = dv.getUint8(5) !== 0 + const authKey = data.subarray(6, 262) + const userId = longFromBuffer(data.subarray(262, 270), true, false).toNumber() + const isBot = dv.getUint8(270) !== 0 + + return { + dcId, + apiId, + isTest, + authKey, + userId, + isBot, + } +} diff --git a/packages/convert/src/pyrogram/serialize.test.ts b/packages/convert/src/pyrogram/serialize.test.ts new file mode 100644 index 00000000..3af64870 --- /dev/null +++ b/packages/convert/src/pyrogram/serialize.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from 'vitest' + +import { getPlatform } from '@mtcute/core/platform.js' + +import { PYROGRAM_TEST_SESSION } from './__fixtures__/session.js' +import { PYROGRAM_TEST_SESSION_OLD } from './__fixtures__/session_old.js' +import { serializePyrogramSession } from './serialize.js' + +describe('pyrogram/serialize', () => { + it('should correctly serialize old sessions', () => { + expect( + serializePyrogramSession({ + isBot: false, + isTest: true, + userId: 5000801609, + dcId: 2, + authKey: getPlatform().hexDecode( + '1674732db80d690b4d5890d887a4bd5b0b4c810b7c331990b049158a940fdaeb' + + 'd46178f50ddcce753699f0497ad6de9655f454bc5a3030524036dee4ffe3db7c' + + '73b526c378a184a0fafa14e9679c170b632e0b412c174f99e96b216214f78263' + + '7a42112dd23957a39716d41277318997b6c4d635ffe125f777ff017f6456b9f2' + + '7b5557edea774f304118622ff23133b2f961ace33f0d1d836cfdd82a9101192e' + + 'fe8ab90f6f55e96e9ba3c4fe48bcc5fee8e3e5970040bdb9989b427177e1863a' + + '7d6adc5caea9cacc4b34e71cab7a19e52592a81a69d7d261763ffeb0507a990b' + + 'db5c4c1515e0098571bbb2ee6ff84982e727c31b7ed36ceda5871cd701d51e1f', + ), + }), + ).toEqual(PYROGRAM_TEST_SESSION_OLD) + }) + + it('should correctly parse new sessions', () => { + expect( + serializePyrogramSession({ + apiId: 3328759, + isBot: false, + isTest: true, + userId: 5000801609, + dcId: 2, + authKey: getPlatform().hexDecode( + '4e4e8ab2caa290a3f1121c5f5c9a64a0522043fa3c5690a4ff8834b5c1ded2b5' + + '425f1df801a0cabda34e95b909399e23037008f220d8908da8a6e89f6ccffb4b' + + '6bbd30c767ae37e0e63a5f9177c8f7ec05f032bf5011887b5ce4fc86e7d081cb' + + '473fa591bf90a9936187c3bc1a06fc72279a99ab446302eb92391947b246e77a' + + 'bbe85d24b54776d8bebf6d4969f69fea817a8183cbd9764208b3d4f53a24a1c3' + + '65fa4cd3b4958a9f25569cccc550713f59b74162cab00ca68bc5b93efef7a385' + + '296710f60cebb3f4877f3f47ad0801c2f3527645738974edc4b257b1a373dec9' + + '867b287e084b8481d1777a72040a7cdfb65bef50294b37fca570f8abd5b8b8ff', + ), + }), + ).toEqual(PYROGRAM_TEST_SESSION) + }) +}) diff --git a/packages/convert/src/pyrogram/serialize.ts b/packages/convert/src/pyrogram/serialize.ts new file mode 100644 index 00000000..97f4c307 --- /dev/null +++ b/packages/convert/src/pyrogram/serialize.ts @@ -0,0 +1,45 @@ +import { Long, MtArgumentError } from '@mtcute/core' +import { getPlatform } from '@mtcute/core/platform.js' +import { dataViewFromBuffer } from '@mtcute/core/utils.js' + +import { PyrogramSession } from './types.js' + +const SESSION_STRING_SIZE_OLD = 267 +const SESSION_STRING_SIZE = 271 + +export function serializePyrogramSession(session: PyrogramSession): string { + if (session.authKey.length !== 256) { + throw new MtArgumentError('authKey must be 256 bytes long') + } + + const userIdLong = Long.fromNumber(session.userId, true) + + let u8: Uint8Array + + if (session.apiId === undefined) { + // old format + u8 = new Uint8Array(SESSION_STRING_SIZE_OLD) + const dv = dataViewFromBuffer(u8) + + dv.setUint8(0, session.dcId) + dv.setUint8(1, session.isTest ? 1 : 0) + u8.set(session.authKey, 2) + dv.setUint32(258, userIdLong.high) + dv.setUint32(262, userIdLong.low) + dv.setUint8(266, session.isBot ? 1 : 0) + } else { + u8 = new Uint8Array(SESSION_STRING_SIZE) + const dv = dataViewFromBuffer(u8) + + dv.setUint8(0, session.dcId) + dv.setUint32(1, session.apiId) + dv.setUint8(5, session.isTest ? 1 : 0) + u8.set(session.authKey, 6) + + dv.setUint32(262, userIdLong.high) + dv.setUint32(266, userIdLong.low) + dv.setUint8(270, session.isBot ? 1 : 0) + } + + return getPlatform().base64Encode(u8, true) +} diff --git a/packages/convert/src/pyrogram/types.ts b/packages/convert/src/pyrogram/types.ts new file mode 100644 index 00000000..bb8d665e --- /dev/null +++ b/packages/convert/src/pyrogram/types.ts @@ -0,0 +1,8 @@ +export interface PyrogramSession { + apiId?: number + dcId: number + isTest: boolean + authKey: Uint8Array + userId: number + isBot: boolean +} diff --git a/packages/convert/src/telethon/__fixtures__/generate.py b/packages/convert/src/telethon/__fixtures__/generate.py new file mode 100644 index 00000000..5f3cc480 --- /dev/null +++ b/packages/convert/src/telethon/__fixtures__/generate.py @@ -0,0 +1,22 @@ +from telethon.sync import TelegramClient +from telethon.sessions import StringSession +import os +import asyncio + +async def main(): + client = TelegramClient( + StringSession(), + api_id=int(os.environ["API_ID"]), + api_hash=os.environ["API_HASH"], + ) + client.session.set_dc(2, '149.154.167.40', 80) + await client.start( + phone='9996621234', + code_callback=lambda: '22222' + ) + session = client.session.save() + open("session.ts", "w").write( + 'export const TELETHON_TEST_SESSION = \'' + session + '\'\n' + ) + +asyncio.get_event_loop().run_until_complete(main()) \ No newline at end of file diff --git a/packages/convert/src/telethon/__fixtures__/generate_v6.py b/packages/convert/src/telethon/__fixtures__/generate_v6.py new file mode 100644 index 00000000..359a25f9 --- /dev/null +++ b/packages/convert/src/telethon/__fixtures__/generate_v6.py @@ -0,0 +1,26 @@ +from telethon.sync import TelegramClient +from telethon.sessions import StringSession +import os +import asyncio +import logging + +# logging.basicConfig(level=logging.DEBUG) + +async def main(): + client = TelegramClient( + StringSession(), + api_id=int(os.environ["API_ID"]), + api_hash=os.environ["API_HASH"], + use_ipv6=True + ) + client.session.set_dc(1, '2001:0b28:f23d:f001:0000:0000:0000:000e', 443) + await client.start( + phone='9996611234', + code_callback=lambda: '11111' + ) + session = client.session.save() + open("session_v6.ts", "w").write( + 'export const TELETHON_TEST_SESSION_V6 = \'' + session + '\'\n' + ) + +asyncio.get_event_loop().run_until_complete(main()) \ No newline at end of file diff --git a/packages/convert/src/telethon/__fixtures__/session.ts b/packages/convert/src/telethon/__fixtures__/session.ts new file mode 100644 index 00000000..f53df8c3 --- /dev/null +++ b/packages/convert/src/telethon/__fixtures__/session.ts @@ -0,0 +1,2 @@ +export const TELETHON_TEST_SESSION = + '1ApWapygAUChJS1_xwUK01Is4cOvQa1JKTn1POabdMUCfLmXNYFUyvG3v9Z_qbFNFp3zYP--3aVpTYI2DpB2Ib46p_bwSC0j1QEjvdQxJj26cVj8NfslrCkYrdV3glOhdczSq08kp31eqBGXMPhA7wy7DOcSLLAoy-Jf3Q_V_Q3y2a8_64ArFJe8PFfSqkdO56VQutajNLscFUtTQXUQFLJ7ft6vIl__UOc9tpQZEiFW7jWmID79WkfYLHFjuChTVKGMLDa8YcZj6z5Sq-pXPE9VbAbJ5L1JRqXOey3QGtZgJeIEww_WWD5nMMUfhLIydD2i7eDmVoUE5EIZPpsevJmjiGLw4vJk=' diff --git a/packages/convert/src/telethon/__fixtures__/session_v6.ts b/packages/convert/src/telethon/__fixtures__/session_v6.ts new file mode 100644 index 00000000..14495480 --- /dev/null +++ b/packages/convert/src/telethon/__fixtures__/session_v6.ts @@ -0,0 +1,2 @@ +export const TELETHON_TEST_SESSION_V6 = + '1ASABCyjyPfABAAAAAAAAAA4Bu4pveAFWSE51_trKsrRQeMvGXMl8fI6NsGaWqdrXXeqyaXne9qNthqnrBmH56kHfOhFUCPSoVzNNrGgnQr67AYQbkhpP_Yml2EDd8epdc6Gywh4q2NBgYyW6VBT8UKg89-FebYTO6n47I1cJMGsSZ1ddxEOpIpHXsSmPdGBSTz6uaHbLYo0jnxd59PQn4H4dKb8FxuOQsUVa3vY_o79HMVMQRVT1IksUKFg5gAe5ZJ0yx6W4pMviVbC-TYZC0HInmv2fFMv-S3rQyg1C7qpU-Gbo1P6UZC4KZGmu2pMJooFNyfRbFgl3BI5Z-FNx9TKu4UFrF9G6Q0l8PjPXOZm4j-c=' diff --git a/packages/convert/src/telethon/convert.test.ts b/packages/convert/src/telethon/convert.test.ts new file mode 100644 index 00000000..904ed375 --- /dev/null +++ b/packages/convert/src/telethon/convert.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from 'vitest' + +import { getPlatform } from '@mtcute/core/platform.js' + +import { TELETHON_TEST_SESSION } from './__fixtures__/session.js' +import { convertFromTelethonSession, convertToTelethonSession } from './convert.js' + +describe('telethon/convert', () => { + it('should correctly convert from telethon sessions', () => { + expect(convertFromTelethonSession(TELETHON_TEST_SESSION)).toEqual({ + authKey: getPlatform().hexDecode( + '28494b5ff1c142b4d48b3870ebd06b524a4e7d4f39a6dd31409f2e65cd605532' + + 'bc6deff59fea6c5345a77cd83fefb7695a53608d83a41d886f8ea9fdbc120b48' + + 'f54048ef750c498f6e9c563f0d7ec96b0a462b755de094e85d7334aad3c929df' + + '57aa0465cc3e103bc32ec339c48b2c0a32f897f743f57f437cb66bcffae00ac5' + + '25ef0f15f4aa91d3b9e9542eb5a8cd2ec70552d4d05d44052c9edfb7abc897ff' + + 'd439cf6da506448855bb8d69880fbf5691f60b1c58ee0a14d528630b0daf1871' + + '98facf94aafa95cf13d55b01b2792f5251a9739ecb7406b59809788130c3f596' + + '0f99cc3147e12c8c9d0f68bb783995a1413910864fa6c7af2668e218bc38bc99', + ), + primaryDcs: { + main: { + id: 2, + ipAddress: '149.154.167.40', + ipv6: false, + port: 80, + }, + media: { + id: 2, + ipAddress: '149.154.167.40', + ipv6: false, + port: 80, + }, + }, + testMode: true, + version: 3, + }) + }) + + it('should correctly convert to telethon sessions', () => { + expect( + convertToTelethonSession({ + authKey: getPlatform().hexDecode( + '28494b5ff1c142b4d48b3870ebd06b524a4e7d4f39a6dd31409f2e65cd605532' + + 'bc6deff59fea6c5345a77cd83fefb7695a53608d83a41d886f8ea9fdbc120b48' + + 'f54048ef750c498f6e9c563f0d7ec96b0a462b755de094e85d7334aad3c929df' + + '57aa0465cc3e103bc32ec339c48b2c0a32f897f743f57f437cb66bcffae00ac5' + + '25ef0f15f4aa91d3b9e9542eb5a8cd2ec70552d4d05d44052c9edfb7abc897ff' + + 'd439cf6da506448855bb8d69880fbf5691f60b1c58ee0a14d528630b0daf1871' + + '98facf94aafa95cf13d55b01b2792f5251a9739ecb7406b59809788130c3f596' + + '0f99cc3147e12c8c9d0f68bb783995a1413910864fa6c7af2668e218bc38bc99', + ), + primaryDcs: { + main: { + id: 2, + ipAddress: '149.154.167.40', + ipv6: false, + port: 80, + }, + media: { + id: 2, + ipAddress: '149.154.167.40', + ipv6: false, + port: 80, + }, + }, + testMode: true, + version: 3, + }), + ).toEqual(TELETHON_TEST_SESSION) + }) +}) diff --git a/packages/convert/src/telethon/convert.ts b/packages/convert/src/telethon/convert.ts new file mode 100644 index 00000000..938ea49c --- /dev/null +++ b/packages/convert/src/telethon/convert.ts @@ -0,0 +1,46 @@ +import { BasicDcOption, readStringSession, StringSessionData } from '@mtcute/core/utils.js' +import { __tlReaderMap } from '@mtcute/tl/binary/reader.js' + +import { isTestDc } from '../dcs.js' +import { parseTelethonSession } from './parse.js' +import { serializeTelethonSession } from './serialize.js' +import { TelethonSession } from './types.js' + +export function convertFromTelethonSession(session: TelethonSession | string): StringSessionData { + if (typeof session === 'string') { + session = parseTelethonSession(session) + } + + const dc: BasicDcOption = { + id: session.dcId, + ipAddress: session.ipAddress, + port: session.port, + ipv6: session.ipv6, + } + + return { + version: 3, + // we don't exactly have that information. try to deduce it from DC_MAPPING_TEST + // todo: we should maybe check this at connect? + testMode: isTestDc(session.ipAddress), + primaryDcs: { + main: dc, + media: dc, + }, + authKey: session.authKey, + } +} + +export function convertToTelethonSession(session: StringSessionData | string): string { + if (typeof session === 'string') { + session = readStringSession(__tlReaderMap, session) + } + + return serializeTelethonSession({ + dcId: session.primaryDcs.main.id, + ipAddress: session.primaryDcs.main.ipAddress, + port: session.primaryDcs.main.port, + ipv6: session.primaryDcs.main.ipv6 ?? false, + authKey: session.authKey, + }) +} diff --git a/packages/convert/src/telethon/parse.test.ts b/packages/convert/src/telethon/parse.test.ts new file mode 100644 index 00000000..58a56e61 --- /dev/null +++ b/packages/convert/src/telethon/parse.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest' + +import { getPlatform } from '@mtcute/core/platform.js' + +import { TELETHON_TEST_SESSION } from './__fixtures__/session.js' +import { TELETHON_TEST_SESSION_V6 } from './__fixtures__/session_v6.js' +import { parseTelethonSession } from './parse.js' + +describe('telethon/parse', () => { + it('should correctly parse ipv4 sessions', () => { + expect(parseTelethonSession(TELETHON_TEST_SESSION)).toEqual({ + dcId: 2, + ipAddress: '149.154.167.40', + port: 80, + ipv6: false, + authKey: getPlatform().hexDecode( + '28494b5ff1c142b4d48b3870ebd06b524a4e7d4f39a6dd31409f2e65cd605532' + + 'bc6deff59fea6c5345a77cd83fefb7695a53608d83a41d886f8ea9fdbc120b48' + + 'f54048ef750c498f6e9c563f0d7ec96b0a462b755de094e85d7334aad3c929df' + + '57aa0465cc3e103bc32ec339c48b2c0a32f897f743f57f437cb66bcffae00ac5' + + '25ef0f15f4aa91d3b9e9542eb5a8cd2ec70552d4d05d44052c9edfb7abc897ff' + + 'd439cf6da506448855bb8d69880fbf5691f60b1c58ee0a14d528630b0daf1871' + + '98facf94aafa95cf13d55b01b2792f5251a9739ecb7406b59809788130c3f596' + + '0f99cc3147e12c8c9d0f68bb783995a1413910864fa6c7af2668e218bc38bc99', + ), + }) + }) + + it('should correctly parse ipv6 sessions', () => { + expect(parseTelethonSession(TELETHON_TEST_SESSION_V6)).toEqual({ + dcId: 1, + ipAddress: '2001:0b28:f23d:f001:0000:0000:0000:000e', + port: 443, + ipv6: true, + authKey: getPlatform().hexDecode( + '8a6f780156484e75fedacab2b45078cbc65cc97c7c8e8db06696a9dad75deab2' + + '6979def6a36d86a9eb0661f9ea41df3a115408f4a857334dac682742bebb0184' + + '1b921a4ffd89a5d840ddf1ea5d73a1b2c21e2ad8d0606325ba5414fc50a83cf7' + + 'e15e6d84ceea7e3b235709306b1267575dc443a92291d7b1298f7460524f3eae' + + '6876cb628d239f1779f4f427e07e1d29bf05c6e390b1455adef63fa3bf473153' + + '104554f5224b142858398007b9649d32c7a5b8a4cbe255b0be4d8642d072279a' + + 'fd9f14cbfe4b7ad0ca0d42eeaa54f866e8d4fe94642e0a6469aeda9309a2814d' + + 'c9f45b160977048e59f85371f532aee1416b17d1ba43497c3e33d73999b88fe7', + ), + }) + }) +}) diff --git a/packages/convert/src/telethon/parse.ts b/packages/convert/src/telethon/parse.ts new file mode 100644 index 00000000..b630cd72 --- /dev/null +++ b/packages/convert/src/telethon/parse.ts @@ -0,0 +1,38 @@ +import { MtArgumentError } from '@mtcute/core' +import { getPlatform } from '@mtcute/core/platform.js' +import { dataViewFromBuffer } from '@mtcute/core/utils.js' + +import { parseIpFromBytes } from '../utils/ip.js' +import { TelethonSession } from './types.js' + +export function parseTelethonSession(session: string): TelethonSession { + if (session[0] !== '1') { + // version + throw new MtArgumentError(`Invalid session string (version = ${session[0]})`) + } + + session = session.slice(1) + + const data = getPlatform().base64Decode(session, true) + const dv = dataViewFromBuffer(data) + + const dcId = dv.getUint8(0) + + const ipSize = session.length === 352 ? 4 : 16 + let pos = 1 + ipSize + + const ipBytes = data.subarray(1, pos) + const port = dv.getUint16(pos) + pos += 2 + const authKey = data.subarray(pos, pos + 256) + + const ip = parseIpFromBytes(ipBytes) + + return { + dcId, + ipAddress: ip, + ipv6: ipSize === 16, + port, + authKey, + } +} diff --git a/packages/convert/src/telethon/serialize.test.ts b/packages/convert/src/telethon/serialize.test.ts new file mode 100644 index 00000000..66ed90bc --- /dev/null +++ b/packages/convert/src/telethon/serialize.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from 'vitest' + +import { getPlatform } from '@mtcute/core/platform.js' + +import { TELETHON_TEST_SESSION } from './__fixtures__/session.js' +import { TELETHON_TEST_SESSION_V6 } from './__fixtures__/session_v6.js' +import { serializeTelethonSession } from './serialize.js' + +describe('telethon/serialize', () => { + it('should correctly serialize ipv4 sessions', () => { + expect( + serializeTelethonSession({ + dcId: 2, + ipAddress: '149.154.167.40', + port: 80, + ipv6: false, + authKey: getPlatform().hexDecode( + '28494b5ff1c142b4d48b3870ebd06b524a4e7d4f39a6dd31409f2e65cd605532' + + 'bc6deff59fea6c5345a77cd83fefb7695a53608d83a41d886f8ea9fdbc120b48' + + 'f54048ef750c498f6e9c563f0d7ec96b0a462b755de094e85d7334aad3c929df' + + '57aa0465cc3e103bc32ec339c48b2c0a32f897f743f57f437cb66bcffae00ac5' + + '25ef0f15f4aa91d3b9e9542eb5a8cd2ec70552d4d05d44052c9edfb7abc897ff' + + 'd439cf6da506448855bb8d69880fbf5691f60b1c58ee0a14d528630b0daf1871' + + '98facf94aafa95cf13d55b01b2792f5251a9739ecb7406b59809788130c3f596' + + '0f99cc3147e12c8c9d0f68bb783995a1413910864fa6c7af2668e218bc38bc99', + ), + }), + ).toEqual(TELETHON_TEST_SESSION) + }) + + it('should correctly serialize ipv6 sessions', () => { + expect( + serializeTelethonSession({ + dcId: 1, + ipAddress: '2001:0b28:f23d:f001:0000:0000:0000:000e', + port: 443, + ipv6: true, + authKey: getPlatform().hexDecode( + '8a6f780156484e75fedacab2b45078cbc65cc97c7c8e8db06696a9dad75deab2' + + '6979def6a36d86a9eb0661f9ea41df3a115408f4a857334dac682742bebb0184' + + '1b921a4ffd89a5d840ddf1ea5d73a1b2c21e2ad8d0606325ba5414fc50a83cf7' + + 'e15e6d84ceea7e3b235709306b1267575dc443a92291d7b1298f7460524f3eae' + + '6876cb628d239f1779f4f427e07e1d29bf05c6e390b1455adef63fa3bf473153' + + '104554f5224b142858398007b9649d32c7a5b8a4cbe255b0be4d8642d072279a' + + 'fd9f14cbfe4b7ad0ca0d42eeaa54f866e8d4fe94642e0a6469aeda9309a2814d' + + 'c9f45b160977048e59f85371f532aee1416b17d1ba43497c3e33d73999b88fe7', + ), + }), + ).toEqual(TELETHON_TEST_SESSION_V6) + }) +}) diff --git a/packages/convert/src/telethon/serialize.ts b/packages/convert/src/telethon/serialize.ts new file mode 100644 index 00000000..442612c7 --- /dev/null +++ b/packages/convert/src/telethon/serialize.ts @@ -0,0 +1,37 @@ +import { MtArgumentError } from '@mtcute/core' +import { getPlatform } from '@mtcute/core/platform.js' +import { dataViewFromBuffer } from '@mtcute/core/utils.js' + +import { serializeIpv4ToBytes, serializeIpv6ToBytes } from '../utils/ip.js' +import { TelethonSession } from './types.js' + +export function serializeTelethonSession(session: TelethonSession) { + if (session.authKey.length !== 256) { + throw new MtArgumentError('authKey must be 256 bytes long') + } + + const ipSize = session.ipv6 ? 16 : 4 + const u8 = new Uint8Array(259 + ipSize) + const dv = dataViewFromBuffer(u8) + + dv.setUint8(0, session.dcId) + + let pos + + if (session.ipv6) { + serializeIpv6ToBytes(session.ipAddress, u8.subarray(1, 17)) + pos = 17 + } else { + serializeIpv4ToBytes(session.ipAddress, u8.subarray(1, 5)) + pos = 5 + } + + dv.setUint16(pos, session.port) + pos += 2 + u8.set(session.authKey, pos) + + let b64 = getPlatform().base64Encode(u8, true) + while (b64.length % 4 !== 0) b64 += '=' // for some reason telethon uses padding + + return '1' + b64 +} diff --git a/packages/convert/src/telethon/types.ts b/packages/convert/src/telethon/types.ts new file mode 100644 index 00000000..ba5c7be9 --- /dev/null +++ b/packages/convert/src/telethon/types.ts @@ -0,0 +1,7 @@ +export interface TelethonSession { + dcId: number + ipAddress: string + ipv6: boolean + port: number + authKey: Uint8Array +} diff --git a/packages/convert/src/types.ts b/packages/convert/src/types.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/convert/src/utils/ip.ts b/packages/convert/src/utils/ip.ts new file mode 100644 index 00000000..8f01b386 --- /dev/null +++ b/packages/convert/src/utils/ip.ts @@ -0,0 +1,48 @@ +import { MtArgumentError } from '@mtcute/core' + +export function parseIpFromBytes(data: Uint8Array): string { + if (data.length === 4) { + return `${data[0]}.${data[1]}.${data[2]}.${data[3]}` + } + + if (data.length === 16) { + let res = '' + + for (let i = 0; i < 16; i += 2) { + res += data[i].toString(16).padStart(2, '0') + res += data[i + 1].toString(16).padStart(2, '0') + if (i < 14) res += ':' + } + + return res + } + + throw new MtArgumentError('Invalid IP address length') +} + +export function serializeIpv4ToBytes(ip: string, buf: Uint8Array) { + const parts = ip.split('.') + + if (parts.length !== 4) { + throw new MtArgumentError('Invalid IPv4 address') + } + + buf[0] = Number(parts[0]) + buf[1] = Number(parts[1]) + buf[2] = Number(parts[2]) + buf[3] = Number(parts[3]) +} + +export function serializeIpv6ToBytes(ip: string, buf: Uint8Array) { + const parts = ip.split(':') + + if (parts.length !== 8) { + throw new MtArgumentError('Invalid IPv6 address') + } + + for (let i = 0; i < 8; i++) { + const val = parseInt(parts[i], 16) + buf[i * 2] = val >> 8 + buf[i * 2 + 1] = val & 0xff + } +} diff --git a/packages/convert/src/utils/rle.ts b/packages/convert/src/utils/rle.ts new file mode 100644 index 00000000..7661c34a --- /dev/null +++ b/packages/convert/src/utils/rle.ts @@ -0,0 +1,56 @@ +// tdlib's RLE only encodes consecutive \x00 + +export function telegramRleEncode(buf: Uint8Array): Uint8Array { + const len = buf.length + const ret: number[] = [] + let count = 0 + + for (let i = 0; i < len; i++) { + const cur = buf[i] + + if (cur === 0) { + count += 1 + } else { + if (count > 0) { + ret.push(0, count) + count = 0 + } + + ret.push(cur) + } + } + + if (count > 0) { + ret.push(0, count) + } + + return new Uint8Array(ret) +} + +export function telegramRleDecode(buf: Uint8Array): Uint8Array { + const len = buf.length + const ret: number[] = [] + let prev = -1 + + for (let i = 0; i < len; i++) { + const cur = buf[i] + + if (prev === 0) { + for (let j = 0; j < cur; j++) { + ret.push(prev) + } + prev = -1 + } else { + if (prev !== -1) ret.push(prev) + prev = cur + } + } + + if (prev !== -1) ret.push(prev) + + return new Uint8Array(ret) +} + +export function assertNever(_: never): never { + throw new Error('unreachable') +} diff --git a/packages/convert/tsconfig.json b/packages/convert/tsconfig.json new file mode 100644 index 00000000..798d46cb --- /dev/null +++ b/packages/convert/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist/esm", + "rootDir": "./src", + }, + "include": [ + "./src", + ], + "references": [ + { "path": "../core" }, + ], +} diff --git a/packages/convert/typedoc.cjs b/packages/convert/typedoc.cjs new file mode 100644 index 00000000..c062faa9 --- /dev/null +++ b/packages/convert/typedoc.cjs @@ -0,0 +1,4 @@ +module.exports = { + extends: ['../../.config/typedoc/config.base.cjs'], + entryPoints: ['./src/index.ts'], +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e711b11..73002f7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -115,6 +115,19 @@ importers: specifier: 0.34.6 version: 0.34.6(@vitest/browser@0.34.6)(@vitest/ui@0.34.6)(playwright@1.40.1) + packages/convert: + dependencies: + '@mtcute/core': + specifier: workspace:^ + version: link:../core + '@mtcute/tl': + specifier: '*' + version: link:../tl + devDependencies: + '@mtcute/test': + specifier: workspace:^ + version: link:../test + packages/core: dependencies: '@mtcute/file-id': -- 2.45.2 From 149fa6b49e17a54909a2dd2073e900db13323dd9 Mon Sep 17 00:00:00 2001 From: Alina Sireneva Date: Thu, 7 Mar 2024 09:53:30 +0300 Subject: [PATCH 2/2] chore: readme + exports --- packages/convert/README.md | 58 ++++++++++++++++++-------- packages/convert/src/gramjs/index.ts | 4 ++ packages/convert/src/index.ts | 4 ++ packages/convert/src/mtkruto/index.ts | 4 ++ packages/convert/src/pyrogram/index.ts | 4 ++ packages/convert/src/telethon/index.ts | 4 ++ 6 files changed, 61 insertions(+), 17 deletions(-) create mode 100644 packages/convert/src/gramjs/index.ts create mode 100644 packages/convert/src/index.ts create mode 100644 packages/convert/src/mtkruto/index.ts create mode 100644 packages/convert/src/pyrogram/index.ts create mode 100644 packages/convert/src/telethon/index.ts diff --git a/packages/convert/README.md b/packages/convert/README.md index c080b07b..d741f46e 100644 --- a/packages/convert/README.md +++ b/packages/convert/README.md @@ -1,22 +1,46 @@ -# @mtcute/file-id +# @mtcute/convert -📖 [API Reference](https://ref.mtcute.dev/modules/_mtcute_file_id.html) +📖 [API Reference](https://ref.mtcute.dev/modules/_mtcute_convert.html) -This package is used internally by `@mtcute/core` to parse, serialize -and manipulate TDLib and Bot API compatible File IDs, but can also be used -for any other purposes. +This package can be used to convert other libraries sessions to/from mtcute sessions -## Acknowledgements -This is basically a port of a portion of TDLib APIs, but greatly -simplified in usage and made to work seamlessly with the rest of the -mtcute APIs. +Currently only the libraries that support exporting sessions to strings are supported, namely: -This is a list of files from TDLib repository, from which most of the code was taken: - - [td/telegram/files/FileManager.cpp](https://github.com/tdlib/td/blob/master/td/telegram/files/FileManager.cpp) - - [td/telegram/files/FileLocation.hpp](https://github.com/tdlib/td/blob/master/td/telegram/files/FileLocation.hpp) - - [td/telegram/PhotoSizeSource.h](https://github.com/tdlib/td/blob/master/td/telegram/PhotoSizeSource.h) - - [td/telegram/PhotoSizeSource.hpp](https://github.com/tdlib/td/blob/master/td/telegram/PhotoSizeSource.hpp) - - [td/telegram/Version.h](https://github.com/tdlib/td/blob/master/td/telegram/Version.h) +## [Telethon](https://github.com/LonamiWebs/Telethon) -Additionally, some of the test cases were taken from a similar Python -library, [luckydonald/telegram_file_id](https://github.com/luckydonald/telegram_file_id) +> Telethon v2 seems to have removed the ability to export sessions, +> so it's currently not supported + +```ts +import { convertFromTelethonSession } from '@mtcute/convert' + +const client = new TelegramClient({ ... }) +await client.importSession(convertFromTelethonSession("...")) +``` + +## [Pyrogram](https://github.com/gram-js/gramjs) + +```ts +import { convertFromPyrogramSession } from '@mtcute/convert' + +const client = new TelegramClient({ ... }) +await client.importSession(convertFromPyrogramSession("...")) +``` + +## GramJS + +```ts +import { convertFromGramjsSession } from '@mtcute/convert' + +const client = new TelegramClient({ ... }) +await client.importSession(convertFromGramjsSession("...")) +``` + +## [MTKruto](https://github.com/MTKruto/MTKruto) + +```ts +import { convertFromMtkrutoSession } from '@mtcute/convert' + +const client = new TelegramClient({ ... }) +await client.importSession(convertFromMtkrutoSession("...")) +``` \ No newline at end of file diff --git a/packages/convert/src/gramjs/index.ts b/packages/convert/src/gramjs/index.ts new file mode 100644 index 00000000..ad163bf1 --- /dev/null +++ b/packages/convert/src/gramjs/index.ts @@ -0,0 +1,4 @@ +export * from './convert.js' +export * from './parse.js' +export * from './serialize.js' +export * from './types.js' diff --git a/packages/convert/src/index.ts b/packages/convert/src/index.ts new file mode 100644 index 00000000..73a452a0 --- /dev/null +++ b/packages/convert/src/index.ts @@ -0,0 +1,4 @@ +export * from './gramjs/index.js' +export * from './mtkruto/index.js' +export * from './pyrogram/index.js' +export * from './telethon/index.js' diff --git a/packages/convert/src/mtkruto/index.ts b/packages/convert/src/mtkruto/index.ts new file mode 100644 index 00000000..ad163bf1 --- /dev/null +++ b/packages/convert/src/mtkruto/index.ts @@ -0,0 +1,4 @@ +export * from './convert.js' +export * from './parse.js' +export * from './serialize.js' +export * from './types.js' diff --git a/packages/convert/src/pyrogram/index.ts b/packages/convert/src/pyrogram/index.ts new file mode 100644 index 00000000..ad163bf1 --- /dev/null +++ b/packages/convert/src/pyrogram/index.ts @@ -0,0 +1,4 @@ +export * from './convert.js' +export * from './parse.js' +export * from './serialize.js' +export * from './types.js' diff --git a/packages/convert/src/telethon/index.ts b/packages/convert/src/telethon/index.ts new file mode 100644 index 00000000..ad163bf1 --- /dev/null +++ b/packages/convert/src/telethon/index.ts @@ -0,0 +1,4 @@ +export * from './convert.js' +export * from './parse.js' +export * from './serialize.js' +export * from './types.js' -- 2.45.2