Session conversion #20

Merged
teidesu merged 2 commits from convert into master 2024-03-07 11:16:57 +03:00
55 changed files with 1581 additions and 1 deletions

View file

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

View file

@ -0,0 +1,46 @@
# @mtcute/convert
📖 [API Reference](https://ref.mtcute.dev/modules/_mtcute_convert.html)
This package can be used to convert other libraries sessions to/from mtcute sessions
Currently only the libraries that support exporting sessions to strings are supported, namely:
## [Telethon](https://github.com/LonamiWebs/Telethon)
> 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("..."))
```

View file

@ -0,0 +1,29 @@
{
"name": "@mtcute/convert",
"private": true,
"version": "0.7.0",
"description": "Cross-library session conversion utilities",
"author": "Alina Sireneva <alina@tei.su>",
"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:^"
}
}

113
packages/convert/src/dcs.ts Normal file
View file

@ -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<number, DcOptions> = {
'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<number, DcOptions> = {
'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)
}

View file

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

View file

@ -0,0 +1,2 @@
export const GRAMJS_SESSION =
'1AgAOMTQ5LjE1NC4xNjcuNDABu60obcEYS8Yb/I7YlCwaLvW84dXCX2oGnBYG+zuMciJhHP99c8ZJvwxJgH8yU1QrqI+Gh0kK0JAuQucIpDfq/jJVLZ1ZRimq5yy1XbeEs65gtZA1+SUwZRXahh+NzGbPmOVUMCnCtRONo9GNvcx/QxSXRrh7T/K0YYN1iHsK1vJDk8/SUnthvTNmRycC+JLn4fMtctqP4Le2WPOH/deYbUF0BlwmR77M7fv1GZSInqCgWReaIl5nvn0IqA4mOCTkdOgcvwOiB2UmXwiyInxRuLdBIyLbBUDCuTlmL1m3FJqbuEpZEUJnoJf2YDFZ1wR6TfL0MUS1VwnjOcy3WIIFwwg='

View file

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

View file

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

View file

@ -0,0 +1,4 @@
export * from './convert.js'
export * from './parse.js'
export * from './serialize.js'
export * from './types.js'

View file

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

View file

@ -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,
}
}

View file

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

View file

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

View file

@ -0,0 +1,7 @@
export interface GramjsSession {
dcId: number
ipAddress: string
ipv6: boolean
port: number
authKey: Uint8Array
}

View file

@ -0,0 +1,4 @@
export * from './gramjs/index.js'
export * from './mtkruto/index.js'
export * from './pyrogram/index.js'
export * from './telethon/index.js'

View file

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

View file

@ -0,0 +1,2 @@
export const MTKRUTO_SESSION =
'BjItdGVzdAAB_gABAQABWEIKa07Ch-9zoA024mDOpsv20TW4YwuoRRROqSi41YQCbD3c4nKnz7BcFIu1mfn6f6Xm3OTVqoT0zib4p_AuZD9H-t8j5AagecRg-oSpQlmjoiUazKQSxnxWotGWf1mPNntAeOvDNa5t1NjXUxmqdB3e2AjYLF_E2jDESVgUuDBQUMBHIDc_xFBAlz6kVxCZ6iINJHbnyJ2F19tbEPFJvSM999RKaFj5lUUVs0qKNXEUmsFYUuIdPBzjWilY8Uvf9nYU_xXd9CUAAXS5_i4aaWlHoTIf3zn8ZEINhDIU1DMauh5vhSWt7F0fkxODjtou-7PdIunuDtqyQm4steuNJc8'

View file

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

View file

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

View file

@ -0,0 +1,4 @@
export * from './convert.js'
export * from './parse.js'
export * from './serialize.js'
export * from './types.js'

View file

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

View file

@ -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,
}
}

View file

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

View file

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

View file

@ -0,0 +1,5 @@
export interface MtkrutoSession {
dcId: number
isTest: boolean
authKey: Uint8Array
}

View file

@ -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())

View file

@ -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())

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,4 @@
export * from './convert.js'
export * from './parse.js'
export * from './serialize.js'
export * from './types.js'

View file

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

View file

@ -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,
}
}

View file

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

View file

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

View file

@ -0,0 +1,8 @@
export interface PyrogramSession {
apiId?: number
dcId: number
isTest: boolean
authKey: Uint8Array
userId: number
isBot: boolean
}

View file

@ -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())

View file

@ -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())

View file

@ -0,0 +1,2 @@
export const TELETHON_TEST_SESSION =
'1ApWapygAUChJS1_xwUK01Is4cOvQa1JKTn1POabdMUCfLmXNYFUyvG3v9Z_qbFNFp3zYP--3aVpTYI2DpB2Ib46p_bwSC0j1QEjvdQxJj26cVj8NfslrCkYrdV3glOhdczSq08kp31eqBGXMPhA7wy7DOcSLLAoy-Jf3Q_V_Q3y2a8_64ArFJe8PFfSqkdO56VQutajNLscFUtTQXUQFLJ7ft6vIl__UOc9tpQZEiFW7jWmID79WkfYLHFjuChTVKGMLDa8YcZj6z5Sq-pXPE9VbAbJ5L1JRqXOey3QGtZgJeIEww_WWD5nMMUfhLIydD2i7eDmVoUE5EIZPpsevJmjiGLw4vJk='

View file

@ -0,0 +1,2 @@
export const TELETHON_TEST_SESSION_V6 =
'1ASABCyjyPfABAAAAAAAAAA4Bu4pveAFWSE51_trKsrRQeMvGXMl8fI6NsGaWqdrXXeqyaXne9qNthqnrBmH56kHfOhFUCPSoVzNNrGgnQr67AYQbkhpP_Yml2EDd8epdc6Gywh4q2NBgYyW6VBT8UKg89-FebYTO6n47I1cJMGsSZ1ddxEOpIpHXsSmPdGBSTz6uaHbLYo0jnxd59PQn4H4dKb8FxuOQsUVa3vY_o79HMVMQRVT1IksUKFg5gAe5ZJ0yx6W4pMviVbC-TYZC0HInmv2fFMv-S3rQyg1C7qpU-Gbo1P6UZC4KZGmu2pMJooFNyfRbFgl3BI5Z-FNx9TKu4UFrF9G6Q0l8PjPXOZm4j-c='

View file

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

View file

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

View file

@ -0,0 +1,4 @@
export * from './convert.js'
export * from './parse.js'
export * from './serialize.js'
export * from './types.js'

View file

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

View file

@ -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,
}
}

View file

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

View file

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

View file

@ -0,0 +1,7 @@
export interface TelethonSession {
dcId: number
ipAddress: string
ipv6: boolean
port: number
authKey: Uint8Array
}

View file

View file

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

View file

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

View file

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist/esm",
"rootDir": "./src",
},
"include": [
"./src",
],
"references": [
{ "path": "../core" },
],
}

View file

@ -0,0 +1,4 @@
module.exports = {
extends: ['../../.config/typedoc/config.base.cjs'],
entryPoints: ['./src/index.ts'],
}

View file

@ -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':