Skip to content

Commit

Permalink
added tdata convertor
Browse files Browse the repository at this point in the history
  • Loading branch information
kanewi11 committed Dec 4, 2022
1 parent 92cc0a2 commit d99ea45
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 39 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
.idea/**/dictionaries
.idea/**/shelf

tdatas
tdatas/*
test_sessions/*
sessions/*
test.py
Expand Down
42 changes: 24 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
</a>
</p>

**Automatically converts TDATA to Pyrogram session!**

**Automatically converts Telethon sessions to Pyrogram (may not be stable).**

**As long as the configuration file has the same name as the session file (see below). If you do not comply, it will not work at all 🙃**

_This script sends reactions to a new post or message in selected open groups and channels, as well as automatically subscribes to them._
Expand All @@ -23,27 +26,30 @@ _This script sends reactions to a new post or message in selected open groups an
5. `pip install -r requirements.txt`
6. Add your channel name to `config.py`
7. `mkdir sessions`
8. **Sessions must be for [pyrogram](https://github.com/pyrogram/pyrogram)!**
8. **Sessions must be for [pyrogram](https://github.com/pyrogram/pyrogram)!**

Add the session file and its configuration file to the `/sessions` directory ( _which we created in step 7_ ).
Add the session file and its configuration file to the `/sessions` directory ( _which we created in step 7_ ).

**These two files must have the same name!** Here is an example:
**These two files must have the same name!** Here is an example:

```
your_dir
└───reactionbot.py
└───sessions
│ │ 8888888888.ini
│ │ 8888888888.session
│ │ 9999999999.ini
│ │ 9999999999.session
│ │ 98767242365.json
│ │ 98767242365.session
│ │ ...
...
```
```
your_dir
└───reactionbot.py
└───sessions
│ │ 8888888888.ini
│ │ 8888888888.session
│ │ 9999999999.ini
│ │ 9999999999.session
│ │ 98767242365.json
│ │ 98767242365.session
│ │ ...
└───tdatas
│ └───your_tdata
│ │ │ ...
...
```
9. `nohup python reactionbot.py &`
## Create a session file manually.
Expand Down
5 changes: 5 additions & 0 deletions converters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
__license__ = 'GNU Lesser General Public License v3.0 (LGPL-3.0)'
__copyright__ = 'Copyright (C) 2022-present Nikita <https://github.com/kanewi11>'

from .telethon_to_pyrogram import SessionConvertor
from .tdata_to_telethon import convert_tdata
256 changes: 256 additions & 0 deletions converters/tdata_to_telethon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import io
import json
import os
import struct
import hashlib
import asyncio
import ipaddress
from typing import Union
from pathlib import Path
from base64 import urlsafe_b64encode

import cryptg
from telethon.sync import TelegramClient
from telethon.sessions import StringSession

from .telethon_to_pyrogram import SessionConvertor

API_HASH = 'f2417bb89325164867e779bf8dbb34f8'
API_ID = 29702912

DC_TABLE = {
1: ('149.154.175.50', 443),
2: ('149.154.167.51', 443),
3: ('149.154.175.100', 443),
4: ('149.154.167.91', 443),
5: ('149.154.171.5', 443),
}


class QDataStream:
def __init__(self, data):
self.stream = io.BytesIO(data)

def read(self, n=None):
if n < 0:
n = 0
data = self.stream.read(n)
if n != 0 and len(data) == 0:
return None
if n is not None and len(data) != n:
raise Exception('unexpected eof')
return data

def read_buffer(self):
length_bytes = self.read(4)
if length_bytes is None:
return None
length = int.from_bytes(length_bytes, 'big', signed=True)
data = self.read(length)
if data is None:
raise Exception('unexpected eof')
return data

def read_uint32(self):
data = self.read(4)
if data is None:
return None
return int.from_bytes(data, 'big')

def read_uint64(self):
data = self.read(8)
if data is None:
return None
return int.from_bytes(data, 'big')

def read_int32(self):
data = self.read(4)
if data is None:
return None
return int.from_bytes(data, 'big', signed=True)


def create_local_key(passcode, salt):
if passcode:
iterations = 100_000
else:
iterations = 1
_hash = hashlib.sha512(salt + passcode + salt).digest()
return hashlib.pbkdf2_hmac('sha512', _hash, salt, iterations, 256)


def prepare_aes_oldmtp(auth_key, msg_key, send):
if send:
x = 0
else:
x = 8

sha1 = hashlib.sha1()
sha1.update(msg_key)
sha1.update(auth_key[x:][:32])
a = sha1.digest()

sha1 = hashlib.sha1()
sha1.update(auth_key[32 + x:][:16])
sha1.update(msg_key)
sha1.update(auth_key[48 + x:][:16])
b = sha1.digest()

sha1 = hashlib.sha1()
sha1.update(auth_key[64 + x:][:32])
sha1.update(msg_key)
c = sha1.digest()

sha1 = hashlib.sha1()
sha1.update(msg_key)
sha1.update(auth_key[96 + x:][:32])
d = sha1.digest()

key = a[:8] + b[8:] + c[4:16]
iv = a[8:] + b[:8] + c[16:] + d[:8]
return key, iv


def aes_decrypt_local(ciphertext, auth_key, key_128):
key, iv = prepare_aes_oldmtp(auth_key, key_128, False)
return cryptg.decrypt_ige(ciphertext, key, iv)


def decrypt_local(data, key):
encrypted_key = data[:16]
data = aes_decrypt_local(data[16:], key, encrypted_key)
sha1 = hashlib.sha1()
sha1.update(data)
if encrypted_key != sha1.digest()[:16]:
raise Exception('failed to decrypt')
length = int.from_bytes(data[:4], 'little')
data = data[4:length]
return QDataStream(data)


def read_file(name):
with open(name, 'rb') as f:
magic = f.read(4)
if magic != b'TDF$':
raise Exception('invalid magic')
version_bytes = f.read(4)
data = f.read()
data, digest = data[:-16], data[-16:]
data_len_bytes = len(data).to_bytes(4, 'little')
md5 = hashlib.md5()
md5.update(data)
md5.update(data_len_bytes)
md5.update(version_bytes)
md5.update(magic)
digest = md5.digest()
if md5.digest() != digest:
raise Exception('invalid digest')
return QDataStream(data)


def read_encrypted_file(name, key):
stream = read_file(name)
encrypted_data = stream.read_buffer()
return decrypt_local(encrypted_data, key)


def account_data_string(index=0):
s = 'data'
if index > 0:
s += f'#{index + 1}'
md5 = hashlib.md5()
md5.update(bytes(s, 'utf-8'))
digest = md5.digest()
return digest[:8][::-1].hex().upper()[::-1]


def read_user_auth(directory, local_key, index=0):
name = account_data_string(index)
path = os.path.join(directory, f'{name}s')
stream = read_encrypted_file(path, local_key)
if stream.read_uint32() != 0x4B:
raise Exception('unsupported user auth config')

stream = QDataStream(stream.read_buffer())
user_id = stream.read_uint32()
main_dc = stream.read_uint32()
if user_id == 0xFFFFFFFF and main_dc == 0xFFFFFFFF:
user_id = stream.read_uint64()
main_dc = stream.read_uint32()
if main_dc not in DC_TABLE:
raise Exception(f'unsupported main dc: {main_dc}')

length = stream.read_uint32()
for _ in range(length):
auth_dc = stream.read_uint32()
auth_key = stream.read(256)
if auth_dc == main_dc:
return auth_dc, auth_key
raise Exception('invalid user auth config')


def build_session(dc, ip, port, key):
ip_bytes = ipaddress.ip_address(ip).packed
data = struct.pack('>B4sH256s', dc, ip_bytes, port, key)
encoded_data = urlsafe_b64encode(data).decode('ascii')
return '1' + encoded_data


async def convert_tdata(path: Union[str, Path], work_dir: Path):
stream = read_file(os.path.join(path, 'key_datas'))
salt = stream.read_buffer()
if len(salt) != 32:
raise Exception('invalid salt length')
key_encrypted = stream.read_buffer()
info_encrypted = stream.read_buffer()

passcode_key = create_local_key(b'', salt)
key_inner_data = decrypt_local(key_encrypted, passcode_key)
local_key = key_inner_data.read(256)
if len(local_key) != 256:
raise Exception('invalid local key')

info_data = decrypt_local(info_encrypted, local_key)
count = info_data.read_uint32()
auth_key = []
for _ in range(count):
index = info_data.read_uint32()
dc, key = read_user_auth(path, local_key, index)
ip, port = DC_TABLE[dc]
session = build_session(dc, ip, port, key)
auth_key.append(session)

await convert_telethon_session_to_pyrogram(auth_key, work_dir)


def save_config(work_dir: Path, phone: str, config: dict):
config_path = work_dir.joinpath(phone + '.json')
with open(config_path, 'w') as config_file:
json.dump(config, config_file)


async def convert_telethon_session_to_pyrogram(auth_key, work_dir: Path):
session = StringSession(auth_key[0])
async with TelegramClient(session, api_hash=API_HASH, api_id=API_ID) as client:
try:
await client.connect()
_ = await client.get_me()
except Exception as error:
raise error

user_data = await client.get_me()
string_session = StringSession.save(client.session)
session_data = StringSession(string_session)
phone = user_data.phone
if phone is None:
raise Exception('no phone')
session_path = work_dir.joinpath(f'{phone}.session')
config = {
'phone': phone,
'api_id': API_ID,
'api_hash': API_HASH,
}
save_config(work_dir, phone, config)
converter = SessionConvertor(session_path, config, work_dir)
converted_session = await converter.get_converted_sting_session(session_data, user_data)
await converter.save_pyrogram_session_file(converted_session, session_data)
18 changes: 10 additions & 8 deletions convertor.py → converters/telethon_to_pyrogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,20 @@

class SessionConvertor:
def __init__(self, session_path: Path, config: Dict, work_dir: Path):
self.session_path = session_path
if work_dir is None:
work_dir = Path(__file__).parent.parent.joinpath('sessions')
self.session_path = session_path if session_path else work_dir
self.inappropriate_sessions_path = work_dir.joinpath('unnecessary_sessions')
self.api_id = config['api_id']
self.api_hash = config['api_hash']
self.api_id = config['api_id'] if config else None
self.api_hash = config['api_hash'] if config else None
self.work_dir = work_dir

async def convert(self) -> None:
"""Main func"""
user_data, session_data = await self.__get_data_telethon_session()
converted_sting_session = await self.__get_converted_sting_session(session_data, user_data)
converted_sting_session = await self.get_converted_sting_session(session_data, user_data)
await self.move_file_to_unnecessary(self.session_path)
await self.__save_pyrogram_session_file(converted_sting_session, session_data)
await self.save_pyrogram_session_file(converted_sting_session, session_data)

async def move_file_to_unnecessary(self, file_path: Path):
"""Move the unnecessary Telethon session file to the directory with the unnecessary sessions"""
Expand All @@ -39,8 +41,8 @@ async def __get_data_telethon_session(self) -> Tuple[User, StringSession]:
session_data = StringSession(string_session)
return user_data, session_data

async def __save_pyrogram_session_file(self, session_string: Union[str, Coroutine[Any, Any, str]],
session_data: StringSession):
async def save_pyrogram_session_file(self, session_string: Union[str, Coroutine[Any, Any, str]],
session_data: StringSession):
"""Create session file for pyrogram"""
async with Client(self.session_path.stem, session_string=session_string, api_id=self.api_id,
api_hash=self.api_hash, workdir=self.work_dir.__str__()) as client:
Expand All @@ -57,7 +59,7 @@ async def __save_pyrogram_session_file(self, session_string: Union[str, Coroutin
await client.storage.save()

@staticmethod
async def __get_converted_sting_session(session_data: StringSession, user_data: User) -> str:
async def get_converted_sting_session(session_data: StringSession, user_data: User) -> str:
"""Convert to sting session"""
pack = [
Storage.SESSION_STRING_FORMAT,
Expand Down
Loading

0 comments on commit d99ea45

Please sign in to comment.