├── .gitignore ├── README.md ├── dist ├── tlapi-0.0.1-py3-none-any.whl └── tlapi-0.0.1.tar.gz ├── requirements.txt ├── setup.py └── src ├── __init__.py ├── api.py ├── debug.py ├── devices.py ├── exception.py ├── td ├── __init__.py ├── account.py ├── auth.py ├── configs.py ├── mtp.py ├── shared.py ├── storage.py └── tdesktop.py ├── tl ├── __init__.py ├── configs.py ├── shared.py └── telethon.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | src/tests.py 3 | tests.py 4 | test.py 5 | __pycache__/ 6 | *.pyc 7 | tlapi.egg-info/* 8 | build/* 9 | pydoc-markdown.yml 10 | build_script* 11 | .generated-files.txt 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TLAPI 2 | 3 | ![Static Badge](https://img.shields.io/badge/fork-opentele-blue) 4 | ![Static Badge](https://img.shields.io/badge/customize-HashemDalijeh-red) 5 | ![Static Badge](https://img.shields.io/badge/version-0.0.1-yellow) 6 | 7 | A **Python Telegram API Library** for converting between **tdata** and **telethon** sessions, with built-in **official Telegram APIs**. [**Read the documentation**](https://opentele.readthedocs.io/en/latest/documentation/telegram-desktop/tdesktop/). 8 | 9 | ## Features 10 | - **[tlapi]** - add random app version 11 | - **[tlapi]** - add new Api ID - Api Hash 12 | - **[opentele]** - Convert [Telegram Desktop](https://github.com/telegramdesktop/tdesktop) **tdata** sessions to [telethon](https://github.com/LonamiWebs/Telethon) sessions and vice versa. 13 | - **[opentele]** - Use **telethon** with [official APIs](#authorization) to avoid bot detection. 14 | - **[opentele]** - Randomize [device info](https://opentele.readthedocs.io/en/latest/documentation/authorization/api/#generate) using real data that recognized by Telegram server. 15 | 16 | ## Dependencies 17 | 18 | - [telethon](https://github.com/LonamiWebs/Telethon) - Widely used Telegram's API library for Python. 19 | - [tgcrypto](https://github.com/pyrogram/tgcrypto) - AES-256-IGE encryption to works with `tdata`. 20 | - [pyQt5](https://www.riverbankcomputing.com/software/pyqt/) - Used by Telegram Desktop to streams data from files. 21 | 22 | ## Installation 23 | - Install from [PyPI](https://pypi.org/project/tlapi/): 24 | ```pip title="pip" 25 | pip install --upgrade tlapi 26 | ``` 27 | 28 | ## Examples 29 | The best way to learn anything is by looking at the examples. Am I right? 30 | 31 | - Example on [readthedocs](https://opentele.readthedocs.io/en/latest/examples/) 32 | -------------------------------------------------------------------------------- /dist/tlapi-0.0.1-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyhashem/tlapi/ffd2402d3cbfd3a980c4fcd9ca784757df0fc417/dist/tlapi-0.0.1-py3-none-any.whl -------------------------------------------------------------------------------- /dist/tlapi-0.0.1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyhashem/tlapi/ffd2402d3cbfd3a980c4fcd9ca784757df0fc417/dist/tlapi-0.0.1.tar.gz -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyqt5 2 | telethon 3 | tgcrypto -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from setuptools import setup 3 | import re 4 | 5 | README = (pathlib.Path(__file__).parent / "README.md").read_text() 6 | 7 | PACKAGE_NAME = "tlapi" 8 | VERSION = "0.0.2" 9 | SOURCE_DIRECTORY = "src" 10 | 11 | with open("requirements.txt") as data: 12 | requirements = [ 13 | line for line in data.read().split("\n") if line and not line.startswith("#") 14 | ] 15 | 16 | setup( 17 | name=PACKAGE_NAME, 18 | version=VERSION, 19 | license="MIT", 20 | description="A Python Telegram API Library for converting between tdata and telethon sessions, with built-in official Telegram APIs.", 21 | long_description=README, 22 | long_description_content_type="text/markdown", 23 | url="https://github.com/hashemdalijeh/tlapi", 24 | author="hashemdalijeh", 25 | author_email="hashemdalijeh@gmail.com", 26 | classifiers=[ 27 | "License :: OSI Approved :: MIT License", 28 | "Operating System :: Microsoft :: Windows", 29 | "Operating System :: MacOS", 30 | "Operating System :: POSIX :: Linux", 31 | "Programming Language :: Python :: 3", 32 | "Programming Language :: Python :: 3.10", 33 | ], 34 | keywords=[ 35 | "tdata", 36 | "tdesktop", 37 | "telegram", 38 | "telethon", 39 | "opentele", 40 | "tlapi", 41 | "official_Telegram_APIs", 42 | 'tl_api', 43 | 'Telegram_API', 44 | ], 45 | include_package_data=True, 46 | packages=[PACKAGE_NAME, PACKAGE_NAME+'.td', PACKAGE_NAME+'.tl'], 47 | package_dir={PACKAGE_NAME: SOURCE_DIRECTORY}, 48 | install_requires=requirements, 49 | ) 50 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | from . import td 2 | from . import tl 3 | -------------------------------------------------------------------------------- /src/api.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import os 3 | 4 | import platform 5 | 6 | from typing import Any, List, Dict, Type, TypeVar, Union, Optional 7 | from .devices import * 8 | from .exception import * 9 | from .utils import * 10 | 11 | import typing 12 | 13 | 14 | _T = TypeVar("_T") 15 | _RT = TypeVar("_RT") 16 | 17 | 18 | class BaseAPIMetaClass(BaseMetaClass): 19 | """Super high level tactic metaclass""" 20 | 21 | def __new__( 22 | cls: Type[_T], clsName: str, bases: Tuple[type], attrs: Dict[str, Any] 23 | ) -> _T: 24 | 25 | result = super().__new__(cls, clsName, bases, attrs) 26 | result._clsMakePID() # type: ignore 27 | result.__str__ = BaseAPIMetaClass.__str__ # type: ignore 28 | 29 | return result 30 | 31 | @sharemethod 32 | def __str__(glob) -> str: 33 | 34 | if isinstance(glob, type): 35 | cls = glob 36 | result = f"{cls.__name__} {{\n" 37 | else: 38 | cls = glob.__class__ 39 | result = f"{cls.__name__}() = {{\n" 40 | 41 | for attr, val in glob.__dict__.items(): 42 | 43 | if ( 44 | attr.startswith(f"_{cls.__base__.__name__}__") 45 | or attr.startswith(f"_{cls.__name__}__") 46 | or attr.startswith("__") 47 | and attr.endswith("__") 48 | or type(val) == classmethod 49 | or callable(val) 50 | ): 51 | continue 52 | 53 | result += f" {attr}: {val}\n" 54 | 55 | return result + "}" 56 | 57 | 58 | class APIData(object, metaclass=BaseAPIMetaClass): 59 | """ 60 | API configuration to connect to `TelegramClient` and `TDesktop` 61 | 62 | ### Attributes: 63 | api_id (`int`): 64 | [API_ID](https://core.telegram.org/api/obtaining_api_id#obtaining-api-id) 65 | 66 | api_hash (`str`): 67 | [API_HASH](https://core.telegram.org/api/obtaining_api_id#obtaining-api-id) 68 | 69 | device_model (`str`): 70 | Device model name 71 | 72 | system_version (`str`): 73 | Operating System version 74 | 75 | app_version (`str`): 76 | Current app version 77 | 78 | lang_code (`str`): 79 | Language code of the client 80 | 81 | system_lang_code (`str`): 82 | Language code of operating system 83 | 84 | lang_pack (`str`): 85 | Language pack 86 | 87 | ### Methods: 88 | `Generate()`: Generate random device model and system version 89 | """ 90 | 91 | CustomInitConnectionList: List[Union[Type[APIData], APIData]] = [] 92 | 93 | api_id: int = None # type: ignore 94 | api_hash: str = None # type: ignore 95 | device_model: str = None # type: ignore 96 | system_version: str = None # type: ignore 97 | app_version: str = None # type: ignore 98 | lang_code: str = None # type: ignore 99 | system_lang_code: str = None # type: ignore 100 | lang_pack: str = None # type: ignore 101 | 102 | @typing.overload 103 | def __init__(self, api_id: int, api_hash: str) -> None: 104 | pass 105 | 106 | @typing.overload 107 | def __init__( 108 | self, 109 | api_id: int, 110 | api_hash: str, 111 | device_model: str = None, 112 | system_version: str = None, 113 | app_version: str = None, 114 | lang_code: str = None, 115 | system_lang_code: str = None, 116 | lang_pack: str = None, 117 | ) -> None: 118 | """ 119 | Create your own customized API 120 | 121 | ### Arguments: 122 | api_id (`int`): 123 | [API_ID](https://core.telegram.org/api/obtaining_api_id#obtaining-api-id) 124 | 125 | api_hash (`str`): 126 | [API_HASH](https://core.telegram.org/api/obtaining_api_id#obtaining-api-id) 127 | 128 | device_model (`str`, default=`None`): 129 | `[Device model name](API.device_model)` 130 | 131 | system_version (`str`, default=`None`): 132 | `[Operating System version](API.system_version)` 133 | 134 | app_version (`str`, default=`None`): 135 | `[Current app version](API.app_version)` 136 | 137 | lang_code (`str`, default=`"en"`): 138 | `[Language code of the client](API.app_version)` 139 | 140 | system_lang_code (`str`, default=`"en"`): 141 | `[Language code of operating system](API.system_lang_code)` 142 | 143 | lang_pack (`str`, default=`""`): 144 | `[Language pack](API.lang_pack)` 145 | 146 | ### Warning: 147 | Use at your own risk!: 148 | Using the wrong API can lead to your account banned. 149 | If the session was created using an official API, you must continue using official APIs for that session. 150 | Otherwise that account is at risk of getting banned. 151 | """ 152 | 153 | def __init__( 154 | self, 155 | api_id: int = None, 156 | api_hash: str = None, 157 | device_model: str = None, 158 | system_version: str = None, 159 | app_version: str = None, 160 | lang_code: str = None, 161 | system_lang_code: str = None, 162 | lang_pack: str = None, 163 | ) -> None: 164 | 165 | Expects( 166 | (self.__class__ != APIData) or (api_id != None and api_hash != None), 167 | NoInstanceMatched("No instace of API matches the arguments"), 168 | ) 169 | 170 | cls = self.get_cls() 171 | 172 | self.api_id = api_id if api_id else cls.api_id 173 | self.api_hash = api_hash if api_hash else cls.api_hash 174 | self.device_model = device_model if device_model else cls.device_model 175 | self.system_version = system_version if system_version else cls.system_version 176 | self.app_version = app_version if app_version else cls.app_version 177 | self.system_lang_code = ( 178 | system_lang_code if system_lang_code else cls.system_lang_code 179 | ) 180 | self.lang_pack = lang_pack if lang_pack else cls.lang_pack 181 | self.lang_code = lang_code if lang_code else cls.lang_code 182 | 183 | if self.device_model == None: 184 | system = platform.uname() 185 | 186 | if system.machine in ("x86_64", "AMD64"): 187 | self.device_model = "PC 64bit" 188 | elif system.machine in ("i386", "i686", "x86"): 189 | self.device_model = "PC 32bit" 190 | else: 191 | self.device_model = system.machine 192 | 193 | self._makePID() 194 | 195 | @sharemethod 196 | def copy(glob: Union[Type[_T], _T] = _T) -> _T: # type: ignore 197 | 198 | cls = glob if isinstance(glob, type) else glob.__class__ 199 | 200 | return cls( 201 | glob.api_id, # type: ignore 202 | glob.api_hash, # type: ignore 203 | glob.device_model, # type: ignore 204 | glob.system_version, # type: ignore 205 | glob.app_version, # type: ignore 206 | glob.lang_code, # type: ignore 207 | glob.system_lang_code, # type: ignore 208 | glob.lang_pack, # type: ignore 209 | ) # type: ignore 210 | 211 | @sharemethod 212 | def get_cls(glob: Union[Type[_T], _T]) -> Type[_T]: # type: ignore 213 | return glob if isinstance(glob, type) else glob.__class__ 214 | 215 | @sharemethod 216 | def destroy(glob: Union[Type[_T], _T]): # type: ignore 217 | if isinstance(glob, type): 218 | return 219 | 220 | # might cause conflict, disabled for now, it won"t be a problem 221 | # if (API.findData(self.pid) != None): 222 | # API.CustomInitConnectionList.remove(self) 223 | 224 | def __eq__(self, __o: APIData) -> bool: 225 | if not isinstance(__o, APIData): 226 | return False 227 | return self.pid == __o.pid 228 | 229 | def __del__(self): 230 | self.destroy() 231 | 232 | @classmethod 233 | def _makePIDEnsure(cls) -> int: 234 | while True: 235 | pid = int.from_bytes(os.urandom(8), "little") 236 | if cls.findData(pid) == None: 237 | break 238 | return pid 239 | 240 | @classmethod 241 | def _clsMakePID(cls: Type[APIData]): 242 | cls.pid = cls._makePIDEnsure() 243 | cls.CustomInitConnectionList.append(cls) 244 | 245 | def _makePID(self): 246 | self.pid = self.get_cls()._makePIDEnsure() 247 | self.get_cls().CustomInitConnectionList.append(self) 248 | 249 | @classmethod 250 | def Generate(cls: Type[_T], unique_id: str = None) -> _T: 251 | """ 252 | Generate random device model and system version 253 | 254 | ### Arguments: 255 | unique_id (`str`, default=`None`): 256 | The unique ID to generate - can be anything.\\ 257 | This will be used to ensure that it will generate the same data everytime.\\ 258 | If not set then the data will be randomized each time we runs it. 259 | 260 | ### Raises: 261 | `NotImplementedError`: Not supported for web browser yet 262 | 263 | ### Returns: 264 | `APIData`: Return a copy of the api with random device data 265 | 266 | ### Examples: 267 | Create a `TelegramClient` with custom API: 268 | ```python 269 | api = API.TelegramIOS.Generate(unique_id="new.session") 270 | client = TelegramClient(session="new.session" api=api) 271 | client.start() 272 | ``` 273 | """ 274 | if cls == API.TelegramAndroid: 275 | deviceInfo = AndroidDevice.RandomDevice(unique_id) 276 | 277 | elif cls == API.TelegramAndroidX: 278 | deviceInfo = AndroidDeviceX.RandomDevice(unique_id) 279 | 280 | elif cls == API.TelegramAndroidBeta: 281 | deviceInfo = AndroidDeviceBeta.RandomDevice(unique_id) 282 | 283 | elif cls == API.TelegramIOS: 284 | deviceInfo = iOSDeivce.RandomDevice(unique_id) 285 | 286 | elif cls == API.TelegramMacOS: 287 | deviceInfo = macOSDevice.RandomDevice(unique_id) 288 | 289 | # elif cls == API.TelegramWeb_K or cls == API.TelegramWeb_Z or cls == API.Webogram: 290 | else: 291 | raise NotImplementedError( 292 | f"{cls.__name__} device not supported for randomize yet" 293 | ) 294 | 295 | return cls(device_model=deviceInfo.model, system_version=deviceInfo.version, app_version=deviceInfo.app_version) 296 | 297 | @classmethod 298 | def findData(cls: Type[_T], pid: int) -> Optional[_T]: 299 | for x in cls.CustomInitConnectionList: # type: ignore 300 | if x.pid == pid: 301 | return x 302 | return None 303 | 304 | 305 | class API(BaseObject): 306 | """ 307 | #### Built-in templates for Telegram API 308 | - **`opentele`** offers the ability to use **`official APIs`**, which are used by `official apps`. 309 | - According to [Telegram TOS](https://core.telegram.org/api/obtaining_api_id#using-the-api-id): *all accounts that sign up or log in using unofficial Telegram API clients are automatically put under observation to avoid violations of the Terms of Service*. 310 | - It also uses the **[lang_pack](https://core.telegram.org/method/initConnection)** parameter, of which [telethon can't use](https://github.com/LonamiWebs/Telethon/blob/master/telethon/_client/telegrambaseclient.py#L192) because it's for official apps only. 311 | - Therefore, **there are no differences** between using `opentele` and `official apps`, the server can't tell you apart. 312 | - You can use `TelegramClient.PrintSessions()` to check this out. 313 | 314 | ### Attributes: 315 | TelegramDesktop (`API`): 316 | Official Telegram for Desktop (Windows, macOS and Linux) [View on GitHub](https://github.com/telegramdesktop/tdesktop) 317 | 318 | TelegramAndroid (`API`): 319 | Official Telegram for Android [View on GitHub](https://github.com/DrKLO/Telegram) 320 | 321 | TelegramAndroidX (`API`): 322 | Official TelegramX for Android [View on GitHub](https://github.com/DrKLO/Telegram) 323 | 324 | TelegramIOS (`API`): 325 | Official Telegram for iOS [View on GitHub](https://github.com/TelegramMessenger/Telegram-iOS) 326 | 327 | TelegramMacOS (`API`): 328 | Official Telegram-Swift For MacOS [View on GitHub](https://github.com/overtake/TelegramSwift) 329 | 330 | TelegramWeb_Z (`API`): 331 | Default Official Telegram Web Z For Browsers [View on GitHub](https://github.com/Ajaxy/telegram-tt) | [Visit on Telegram](https://web.telegram.org/z/) 332 | 333 | TelegramWeb_K (`API`): 334 | Official Telegram Web K For Browsers [View on GitHub](https://github.com/morethanwords/tweb) | [Visit on Telegram](https://web.telegram.org/k/) 335 | 336 | Webogram (`API`): 337 | Old Telegram For Browsers [View on GitHub](https://github.com/zhukov/webogram) | [Vist on Telegram](https://web.telegram.org/?legacy=1#/im) 338 | """ 339 | 340 | class TelegramDesktop(APIData): 341 | """ 342 | Official Telegram for Desktop (Windows, macOS and Linux) 343 | [View on GitHub](https://github.com/telegramdesktop/tdesktop) 344 | 345 | ### Attributes: 346 | api_id (`int`) : `2040` 347 | api_hash (`str`) : `"b18441a1ff607e10a989891a5462e627"` 348 | device_model (`str`) : `"Desktop"` 349 | system_version (`str`) : `"Windows 10"` 350 | app_version (`str`) : `"3.4.3 x64"` 351 | lang_code (`str`) : `"en"` 352 | system_lang_code (`str`) : `"en-US"` 353 | lang_pack (`str`) : `"tdesktop"` 354 | 355 | ### Methods: 356 | `Generate()`: Generate random device data for `Windows`, `macOS` and `Linux` 357 | """ 358 | 359 | api_id = 2040 360 | api_hash = "b18441a1ff607e10a989891a5462e627" 361 | device_model = "Desktop" 362 | system_version = "Windows 10" 363 | app_version = "4.12.2 x64" 364 | lang_code = "en" 365 | system_lang_code = "en-US" 366 | lang_pack = "tdesktop" 367 | 368 | @typing.overload 369 | @classmethod 370 | def Generate( 371 | cls: Type[_T], system: str = "windows", unique_id: str = None 372 | ) -> _T: 373 | """ 374 | Generate random TelegramDesktop devices 375 | ### Arguments: 376 | system (`str`, default=`"random"`): 377 | Which OS to generate, either `"windows"`, `"macos"`, or `"linux"`.\\ 378 | Default is `None` or `"random"` - which means it will be selected randomly. 379 | 380 | unique_id (`str`, default=`None`): 381 | The unique ID to generate - can be anything.\\ 382 | This ID will be used to ensure that it will generate the same data every single time.\\ 383 | If not set then the data will be randomized each time we runs it. 384 | 385 | ### Returns: 386 | `APIData`: Return a copy of the api with random device data 387 | 388 | ### Examples: 389 | Save a telethon session to tdata: 390 | ```python 391 | # unique_id will ensure that this data will always be the same (per unique_id). 392 | # You can use the session file name, or user_id as a unique_id. 393 | # If unique_id isn't specify, the device data will be randomized each time we runs it. 394 | oldAPI = API.TelegramDesktop.Generate(system="windows", unique_id="old.session") 395 | oldclient = TelegramClient("old.session", api=oldAPI) 396 | await oldClient.connect() 397 | 398 | # We can safely CreateNewSession with a different API. 399 | # Be aware that you should not use UseCurrentSession with a different API than the one that first authorized it. 400 | # You can print(newAPI) to see what it had generated. 401 | newAPI = API.TelegramDesktop.Generate("macos", "new_tdata") 402 | tdesk = oldclient.ToTDesktop(oldclient, flag=CreateNewSession, api=newAPI) 403 | 404 | # Save the new session to a folder named "new_tdata" 405 | tdesk.SaveTData("new_tdata") 406 | ``` 407 | """ 408 | 409 | @typing.overload 410 | @classmethod 411 | def Generate(cls: Type[_T], system: str = "macos", unique_id: str = None) -> _T: 412 | pass 413 | 414 | @typing.overload 415 | @classmethod 416 | def Generate(cls: Type[_T], system: str = "linux", unique_id: str = None) -> _T: 417 | pass 418 | 419 | @typing.overload 420 | @classmethod 421 | def Generate( 422 | cls: Type[_T], system: str = "random", unique_id: str = None 423 | ) -> _T: 424 | pass 425 | 426 | @classmethod 427 | def Generate(cls: Type[_T], system: str = None, unique_id: str = None) -> _T: 428 | 429 | validList = ["windows", "macos", "linux"] 430 | if system == None or system not in validList: 431 | system = SystemInfo._hashtovalue( 432 | SystemInfo._strtohashid(unique_id), validList 433 | ) 434 | 435 | system = system.lower() 436 | 437 | if system == "windows": 438 | deviceInfo = WindowsDevice.RandomDevice(unique_id) 439 | 440 | elif system == "macos": 441 | deviceInfo = macOSDevice.RandomDevice(unique_id) 442 | 443 | else: 444 | deviceInfo = LinuxDevice.RandomDevice(unique_id) 445 | 446 | return cls(device_model=deviceInfo.model, system_version=deviceInfo.version, app_version=deviceInfo.app_version) 447 | 448 | class TelegramAndroid(APIData): 449 | """ 450 | Official Telegram for Android 451 | [View on GitHub](https://github.com/DrKLO/Telegram) 452 | 453 | ### Attributes: 454 | api_id (`int`) : `6` 455 | api_hash (`str`) : `"eb06d4abfb49dc3eeb1aeb98ae0f581e"` 456 | device_model (`str`) : `"Samsung SM-G998B"` 457 | system_version (`str`) : `"SDK 31"` 458 | app_version (`str`) : `"10.2.8 (40851)"` 459 | lang_code (`str`) : `"en"` 460 | system_lang_code (`str`) : `"en-US"` 461 | lang_pack (`str`) : `"android"` 462 | """ 463 | 464 | api_id = 6 465 | api_hash = "eb06d4abfb49dc3eeb1aeb98ae0f581e" 466 | device_model = "Samsung SM-G998B" 467 | system_version = "SDK 31" 468 | app_version = "10.2.8 (40851)" 469 | lang_code = "en" 470 | system_lang_code = "en-US" 471 | lang_pack = "android" 472 | 473 | class TelegramAndroidBeta(APIData): 474 | """ 475 | Official Telegram Beta for Android 476 | 477 | ### Attributes: 478 | api_id (`int`) : `4` 479 | api_hash (`str`) : `"014b35b6184100b085b0d0572f9b5103"` 480 | device_model (`str`) : `"Samsung SM-G998B"` 481 | system_version (`str`) : `"SDK 31"` 482 | app_version (`str`) : `"10.3.2"` 483 | lang_code (`str`) : `"en"` 484 | system_lang_code (`str`) : `"en-US"` 485 | lang_pack (`str`) : `"android"` 486 | """ 487 | 488 | api_id = 4 489 | api_hash = "014b35b6184100b085b0d0572f9b5103" 490 | device_model = "Samsung SM-G998B" 491 | system_version = "SDK 31" 492 | app_version = "10.3.2" 493 | lang_code = "en" 494 | system_lang_code = "en-US" 495 | lang_pack = "android" 496 | 497 | class TelegramAndroidX(APIData): 498 | """ 499 | Official TelegramX for Android 500 | [View on GitHub](https://github.com/DrKLO/Telegram) 501 | 502 | ### Attributes: 503 | api_id (`int`) : `21724` 504 | api_hash (`str`) : `"3e0cb5efcd52300aec5994fdfc5bdc16"` 505 | device_model (`str`) : `"Samsung SM-G998B"` 506 | system_version (`str`) : `"SDK 31"` 507 | app_version (`str`) : `"0.26.3.1668-arm64-v8a"` 508 | lang_code (`str`) : `"en"` 509 | system_lang_code (`str`) : `"en-US"` 510 | lang_pack (`str`) : `"android"` 511 | """ 512 | 513 | api_id = 21724 514 | api_hash = "3e0cb5efcd52300aec5994fdfc5bdc16" 515 | device_model = "Samsung SM-G998B" 516 | system_version = "SDK 31" 517 | app_version = "0.26.3.1668-arm64-v8a" 518 | lang_code = "en" 519 | system_lang_code = "en-US" 520 | lang_pack = "android" 521 | 522 | class TelegramIOS(APIData): 523 | """ 524 | Official Telegram for iOS 525 | [View on GitHub](https://github.com/TelegramMessenger/Telegram-iOS) 526 | 527 | ### Attributes: 528 | api_id (`int`) : `10840` 529 | api_hash (`str`) : `"33c45224029d59cb3ad0c16134215aeb"` 530 | device_model (`str`) : `"iPhone 13 Pro Max"` 531 | system_version (`str`) : `"14.8.1"` 532 | app_version (`str`) : `"10.3.1 (27850)"` 533 | lang_code (`str`) : `"en"` 534 | system_lang_code (`str`) : `"en-US"` 535 | lang_pack (`str`) : `"ios"` 536 | """ 537 | 538 | # api_id = 8 539 | # api_hash = "7245de8e747a0d6fbe11f7cc14fcc0bb" 540 | api_id = 10840 541 | api_hash = "33c45224029d59cb3ad0c16134215aeb" 542 | device_model = "iPhone 13 Pro Max" 543 | system_version = "14.8.1" 544 | app_version = "10.3.1 (27850)" 545 | lang_code = "en" 546 | system_lang_code = "en-US" 547 | lang_pack = "ios" 548 | 549 | class TelegramMacOS(APIData): 550 | """ 551 | Official Telegram-Swift For MacOS 552 | [View on GitHub](https://github.com/overtake/TelegramSwift) 553 | 554 | ### Attributes: 555 | api_id (`int`) : `2834` 556 | api_hash (`str`) : `"68875f756c9b437a8b916ca3de215815"` 557 | device_model (`str`) : `"MacBook Pro"` 558 | system_version (`str`) : `"macOS 12.0.1"` 559 | app_version (`str`) : `"10.3 (build 256373)"` 560 | lang_code (`str`) : `"en"` 561 | system_lang_code (`str`) : `"en-US"` 562 | lang_pack (`str`) : `"macos"` 563 | """ 564 | 565 | api_id = 2834 566 | api_hash = "68875f756c9b437a8b916ca3de215815" 567 | # api_id = 9 | 568 | # api_hash = "3975f648bb682ee889f35483bc618d1c" | Telegram for macOS uses this api, but it"s unofficial api, why? 569 | device_model = "MacBook Pro" 570 | system_version = "macOS 12.0.1" 571 | app_version = "10.3 (build 256373)" 572 | lang_code = "en" 573 | system_lang_code = "en-US" 574 | lang_pack = "macos" 575 | 576 | class TelegramWeb_Z(APIData): 577 | """ 578 | Default Official Telegram Web Z For Browsers 579 | [View on GitHub](https://github.com/Ajaxy/telegram-tt) | [Visit on Telegram](https://web.telegram.org/z/) 580 | 581 | ### Attributes: 582 | api_id (`int`) : `2496` 583 | api_hash (`str`) : `"8da85b0d5bfe62527e5b244c209159c3"` 584 | device_model (`str`) : `"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36"` 585 | system_version (`str`) : `"Windows"` 586 | app_version (`str`) : `"1.28.3 Z"` 587 | lang_code (`str`) : `"en"` 588 | system_lang_code (`str`) : `"en-US"` 589 | lang_pack (`str`) : `""` 590 | """ 591 | 592 | api_id = 2496 593 | api_hash = "8da85b0d5bfe62527e5b244c209159c3" 594 | device_model = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36" 595 | system_version = "Windows" 596 | app_version = "1.28.3 Z" 597 | lang_code = "en" 598 | system_lang_code = "en-US" 599 | lang_pack = "" # I don"t understand why Telegram Z doesn"t use langPack 600 | # You can read its source here: https://github.com/Ajaxy/telegram-tt/blob/f7bc473d51c0fe3a3e8b22678b62d2360225aa7c/src/lib/gramjs/client/TelegramClient.js#L131 601 | 602 | class TelegramWeb_K(APIData): 603 | """ 604 | Official Telegram Web K For Browsers 605 | [View on GitHub](https://github.com/morethanwords/tweb) | [Visit on Telegram](https://web.telegram.org/k/) 606 | 607 | ### Attributes: 608 | api_id (`int`) : `2496` 609 | api_hash (`str`) : `"8da85b0d5bfe62527e5b244c209159c3"` 610 | device_model (`str`) : `"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36"` 611 | system_version (`str`) : `"Win32"` 612 | app_version (`str`) : `"1.0.1 K"` 613 | lang_code (`str`) : `"en"` 614 | system_lang_code (`str`) : `"en-US"` 615 | lang_pack (`str`) : `"macos"` 616 | """ 617 | 618 | api_id = 2496 619 | api_hash = "8da85b0d5bfe62527e5b244c209159c3" 620 | device_model = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36" 621 | system_version = "Win32" 622 | app_version = "1.0.1 K" 623 | lang_code = "en" 624 | system_lang_code = "en-US" 625 | lang_pack = "macos" # I"m totally confused, why macos? https://github.dev/morethanwords/tweb/blob/26582590e647766f5890c79e1611c54c1e6e800c/src/config/app.ts#L23 626 | 627 | class Webogram(APIData): 628 | """ 629 | Old Telegram For Browsers 630 | [View on GitHub](https://github.com/zhukov/webogram) | [Vist on Telegram](https://web.telegram.org/?legacy=1#/im) 631 | 632 | ### Attributes: 633 | api_id (`int`) : `2496` 634 | api_hash (`str`) : `"8da85b0d5bfe62527e5b244c209159c3"` 635 | device_model (`str`) : `"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36"` 636 | system_version (`str`) : `"Win32"` 637 | app_version (`str`) : `"0.7.0"` 638 | lang_code (`str`) : `"en"` 639 | system_lang_code (`str`) : `"en-US"` 640 | lang_pack (`str`) : `""` 641 | """ 642 | 643 | api_id = 2496 644 | api_hash = "8da85b0d5bfe62527e5b244c209159c3" 645 | device_model = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36" 646 | system_version = "Win32" 647 | app_version = "0.7.0" 648 | lang_code = "en" 649 | system_lang_code = "en-US" 650 | lang_pack = "" # The same problem as TelegramWeb_K, as TelegramWeb_K was built on Webogram 651 | 652 | 653 | class LoginFlag(int): 654 | """ 655 | Login flag for converting sessions between `TDesktop` and `TelegramClient`. 656 | 657 | ### Attributes: 658 | UseCurrentSession (LoginFlag): Use the current session. 659 | CreateNewSession (LoginFlag): Create a new session. 660 | 661 | ### Related: 662 | - `TDesktop.ToTelethon()` 663 | - `TDesktop.FromTelethon()` 664 | - `TelegramClient.ToTDesktop()` 665 | - `TelegramClient.FromTDesktop()` 666 | 667 | """ 668 | 669 | 670 | class UseCurrentSession(LoginFlag): 671 | """ 672 | Use the current session. 673 | - Convert an already-logged in session of `Telegram Desktop` to `Telethon` and vice versa. 674 | - The "session" is just an 256-bytes `AuthKey` that get stored in `tdata folder` or Telethon `session files` [(under sqlite3 format)](https://docs.telethon.dev/en/latest/concepts/sessions.html?highlight=sqlite3#what-are-sessions). 675 | - `UseCurrentSession`'s only job is to read this key and convert it to one another. 676 | 677 | ### Warning: 678 | Use at your own risk!: 679 | You should only use the same consistant API through out the session. 680 | Don't use a same session with multiple different APIs, you might be banned. 681 | 682 | 683 | """ 684 | 685 | 686 | class CreateNewSession(LoginFlag): 687 | """ 688 | Create a new session. 689 | - Use the `current session` to authorize the `new session` by [Login via QR code](https://core.telegram.org/api/qr-login). 690 | - This works just like when you signing into `Telegram` using `QR Login` on mobile devices. 691 | - Although `Telegram Desktop` doesn't let you authorize other sessions via `QR Code` *(or it doesn't have that feature)*, it is still available across all platforms `(``[APIs](API)``)`. 692 | 693 | ### Done: 694 | Safe to use: 695 | You can always use `CreateNewSessions` with any APIs, it can be different from the API that originally created the session. 696 | """ 697 | -------------------------------------------------------------------------------- /src/debug.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import os 3 | 4 | import time 5 | import atexit 6 | 7 | import typing as t 8 | 9 | IS_DEBUG_MODE = False 10 | # IS_DEBUG_MODE = True 11 | 12 | if IS_DEBUG_MODE: 13 | from rich import print # pragma: no cover 14 | 15 | _F = t.TypeVar("_F") 16 | _T = t.TypeVar("_T") 17 | _R = t.TypeVar("_R") 18 | 19 | class DebugInfo(object): 20 | def __init__(self) -> None: 21 | self.list: t.List[t.List[str, int, int, int]] = [] 22 | 23 | def add(self, name, total_called, total_time) -> None: 24 | self.list.append( 25 | ( 26 | name, 27 | total_called, 28 | total_time, 29 | round(total_time / total_called * 1000, 4), 30 | ) 31 | ) 32 | 33 | def on_exit(self): 34 | self.list = sorted(self.list, key=lambda item: item[3]) 35 | 36 | for item in self.list: 37 | 38 | name = item[0] 39 | total_called = item[1] 40 | total_time = item[2] 41 | avg_time = item[3] 42 | 43 | text = name.ljust(40) 44 | text += f"called {total_called}".ljust(15) 45 | text += f"average time: {avg_time} ms".ljust(25) 46 | print(text) 47 | 48 | dbgInfo = DebugInfo() 49 | atexit.register(dbgInfo.on_exit) 50 | 51 | class DebugMethod(type): # pragma: no cover 52 | def __get__(self, obj, cls): 53 | self.__owner__ = obj if obj else cls 54 | return self 55 | 56 | def __call2__(self, *args, **kwargs): 57 | begin = time.perf_counter() 58 | result = self.__fget__(*args, **kwargs) # type: ignore 59 | diff = time.perf_counter() - begin 60 | 61 | return result, diff 62 | 63 | def __call__(self, *args, **kwargs) -> t.Any: 64 | 65 | # stack = inspect.stack()[1] 66 | 67 | # location = f"{os.path.basename(stack.filename)[:-3]}" 68 | # lineno = f"{stack.lineno}." 69 | 70 | # if len(location) >= 15: 71 | # location = ".." + location[-15:] 72 | 73 | # location = location.ljust(15) + "| " + lineno.ljust(6) 74 | 75 | # context = stack.code_context[0][:-1].strip() # type: ignore 76 | # print(location + context, f"took {diff}ms") 77 | 78 | if hasattr(self, "__owner__"): 79 | result, diff = DebugMethod.__call2__( 80 | self, self.__owner__, *args, **kwargs 81 | ) 82 | else: 83 | result, diff = DebugMethod.__call2__(self, *args, **kwargs) 84 | 85 | self.__total_called += 1 86 | self.__total_time += diff 87 | 88 | return result 89 | 90 | def __getfullname(self): 91 | if hasattr(self, "__ownername__"): 92 | if self.__fname__ == "__init__": 93 | return f"{self.__ownername__}()" 94 | 95 | return f"{self.__ownername__}.{self.__fname__}()" 96 | else: 97 | return f"{self.__fget__.__name__}()" 98 | 99 | def on_exit(self): 100 | if self.__total_called > 0: 101 | dbgInfo.add( 102 | DebugMethod.__getfullname(self), 103 | self.__total_called, 104 | self.__total_time, 105 | ) 106 | 107 | def __set_name__(self, owner, name): 108 | self.__owner__: str = owner 109 | self.__ownername__: str = owner.__name__ 110 | self.__fname__: str = name 111 | if self.__fname__.startswith(f"_{self.__ownername__}__"): 112 | self.__fname__ = self.__fname__[len(self.__ownername__) + 1 :] 113 | 114 | def __new__(cls, decorated_func: _F) -> _F: 115 | 116 | firstdct = dict(decorated_func.__dict__) 117 | for i, x in cls.__dict__.items(): 118 | firstdct[i] = x 119 | 120 | result = type.__new__( 121 | cls, 122 | decorated_func.__class__.__name__, 123 | decorated_func.__class__.__bases__, 124 | firstdct, 125 | ) 126 | 127 | result.__fget__ = decorated_func 128 | result.__total_time = 0 129 | result.__total_called = 0 130 | atexit.register(result.on_exit, result) 131 | return result 132 | 133 | def parse_arg(value) -> str: # pragma: no cover 134 | if isinstance(value, type): 135 | return value.__name__ 136 | elif isinstance(value, str): 137 | return f"'{value}'" 138 | elif isinstance(value, int): 139 | return f"{value}" 140 | elif isinstance(value, object): 141 | return value.__class__.__name__ + "(...)" 142 | return value 143 | 144 | class runtime(type): # pragma: no cover 145 | def __get__(self, obj, cls): 146 | self.__owner__ = obj if obj else cls 147 | return self 148 | 149 | def __call__(self, *args, **kwargs) -> t.Any: 150 | 151 | begin = time.perf_counter() 152 | result = self.__fget__(self.__owner__, *args, **kwargs) # type: ignore 153 | diff = round((time.perf_counter() - begin) * 1000, 2) 154 | print(f"{self.__fname__} took {diff}ms") 155 | return result 156 | 157 | def __set_name__(self, owner, name): 158 | print("set_name") 159 | self.__owner__ = owner 160 | self.__ownername__ = owner.__name__ 161 | self.__fname__ = name 162 | 163 | def __new__(cls, decorated_func): 164 | 165 | firstdct = dict(decorated_func.__dict__) 166 | for i, x in cls.__dict__.items(): 167 | firstdct[i] = x 168 | 169 | result = type.__new__( 170 | cls, 171 | decorated_func.__name__, 172 | decorated_func.__class__.__bases__, 173 | firstdct, 174 | ) 175 | result.__fget__ = decorated_func # type: ignore 176 | return result 177 | -------------------------------------------------------------------------------- /src/exception.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import inspect 3 | import types 4 | import typing 5 | from PyQt5.QtCore import QDataStream 6 | 7 | 8 | class OpenTeleException(BaseException): # nocov 9 | """ 10 | Base exception of the library. 11 | """ 12 | 13 | def __init__(self, message: str = None, stack_index: int = 1) -> None: 14 | 15 | super().__init__(message if (message != None) else "") 16 | 17 | self.message = message 18 | self.desc = self.__class__.__name__ 19 | 20 | self.frame = inspect.currentframe() 21 | for i in range(stack_index): 22 | self.frame = self.frame.f_back 23 | 24 | self._caller_class = ( 25 | self.frame.f_locals["self"].__class__ 26 | if "self" in self.frame.f_locals 27 | else None 28 | ) 29 | self._caller_method = self.frame.f_code.co_name 30 | 31 | if self._caller_method != "": 32 | args, _, _, locals = inspect.getargvalues(self.frame) 33 | parameters = {arg: locals[arg] for arg in args} 34 | self._caller_method_params = "".join( 35 | f"{i}={parameters[i]}, " for i in parameters 36 | )[:-2] 37 | else: 38 | self._caller_method = "__main__" 39 | self._caller_method_params = "" 40 | 41 | if self.desc == "OpenTeleException": 42 | self.desc = "Unexpected Exception" 43 | 44 | def __str__(self): 45 | reason = self.desc.__str__() 46 | 47 | if self.message != None: 48 | reason += f": {self.message}" 49 | 50 | reason += " [ Called by " 51 | if self._caller_class != None: 52 | 53 | parent_list = [] 54 | base = self._caller_class 55 | while hasattr(base, "__name__"): 56 | parent_list.append(base.__name__) 57 | base = base.__base__ 58 | 59 | parent_list.reverse() 60 | reason += "".join(f"{i}." for i in parent_list[1:]) 61 | reason += self._caller_method + "() ]" 62 | # reason += f"\n>\t{self._caller_method}({self._caller_method_params})" 63 | else: 64 | reason += f"{self._caller_method}() ]" 65 | # reason += f"{self._caller_method}({self._caller_method_params}) ]" 66 | return reason 67 | 68 | 69 | class TFileNotFound(OpenTeleException): 70 | """ 71 | Could not find or open the file 72 | """ 73 | 74 | 75 | class TDataInvalidMagic(OpenTeleException): 76 | """ 77 | TData file has an invalid magic data, which is the first 4 bytes of the file\n 78 | This usually mean that the file is corrupted or not in the supported formats 79 | """ 80 | 81 | 82 | class TDataInvalidCheckSum(OpenTeleException): 83 | """ 84 | TData file has an invalid checksum\n 85 | This usually mean that the file is corrupted or not in the supported formats 86 | """ 87 | 88 | 89 | class TDataBadDecryptKey(OpenTeleException): 90 | """ 91 | Could not decrypt the data with this key\n 92 | This usually mean that the file is password-encrypted 93 | """ 94 | 95 | 96 | class TDataWrongPasscode(OpenTeleException): 97 | """ 98 | Wrong passcode to decrypt tdata folder\n 99 | """ 100 | 101 | 102 | class TDataBadEncryptedDataSize(OpenTeleException): 103 | """ 104 | The encrypted data size part of the file is corrupted 105 | """ 106 | 107 | 108 | class TDataBadDecryptedDataSize(OpenTeleException): 109 | """ 110 | The decrypted data size part of the file is corrupted 111 | """ 112 | 113 | 114 | class TDataBadConfigData(OpenTeleException): 115 | """ 116 | TData contains bad config data that couldn't be parsed 117 | """ 118 | 119 | 120 | class QDataStreamFailed(OpenTeleException): 121 | """ 122 | Could not stream data from QDataStream\n 123 | Please check the QDataStream.status() for more information 124 | """ 125 | 126 | 127 | class AccountAuthKeyNotFound(OpenTeleException): 128 | """ 129 | Account.authKey is missing, something went wrong 130 | """ 131 | 132 | 133 | class TDataReadMapDataFailed(OpenTeleException): 134 | """ 135 | Could not read map data 136 | """ 137 | 138 | 139 | class TDataReadMapDataIncorrectPasscode(OpenTeleException): 140 | """ 141 | Could not read map data because of incorrect passcode 142 | """ 143 | 144 | 145 | class TDataAuthKeyNotFound(OpenTeleException): 146 | """ 147 | Could not find authKey in TData 148 | """ 149 | 150 | 151 | class MaxAccountLimit(OpenTeleException): 152 | """ 153 | Maxed out limit for accounts per tdesktop client 154 | """ 155 | 156 | 157 | class TDesktopUnauthorized(OpenTeleException): 158 | """ 159 | TDesktop client is unauthorized 160 | """ 161 | 162 | 163 | class TelethonUnauthorized(OpenTeleException): 164 | """ 165 | Telethon client is unauthorized 166 | """ 167 | 168 | 169 | class TDataSaveFailed(OpenTeleException): 170 | """ 171 | Could not save TDesktop to tdata folder 172 | """ 173 | 174 | 175 | class TDesktopNotLoaded(OpenTeleException): 176 | """ 177 | TDesktop instance has no account 178 | """ 179 | 180 | 181 | class TDesktopHasNoAccount(OpenTeleException): 182 | """ 183 | TDesktop instance has no account 184 | """ 185 | 186 | 187 | class TDAccountNotLoaded(OpenTeleException): 188 | """ 189 | TDesktop account hasn't been loaded yet 190 | """ 191 | 192 | 193 | class NoPasswordProvided(OpenTeleException): 194 | """ 195 | You can't live without a password bro 196 | """ 197 | 198 | 199 | class PasswordIncorrect(OpenTeleException): 200 | """ 201 | incorrect passwrd 202 | """ 203 | 204 | 205 | class LoginFlagInvalid(OpenTeleException): 206 | """ 207 | Invalid login flag 208 | """ 209 | 210 | 211 | class NoInstanceMatched(OpenTeleException): 212 | """ 213 | Invalid login flag 214 | """ 215 | 216 | 217 | @typing.overload 218 | def Expects( 219 | condition: bool, 220 | message: str = None, 221 | done: typing.Callable[[], None] = None, 222 | fail: typing.Callable[[OpenTeleException], None] = None, 223 | silent: bool = False, 224 | stack_index: int = 1, 225 | ) -> bool: 226 | """Expect a condition to be `True`, raise an `OpenTeleException` if it's not. 227 | 228 | ### Arguments: 229 | condition (bool): 230 | Condition that you're expecting. 231 | 232 | message (str, default=None): 233 | Custom exception message 234 | 235 | done (`lambda`, default=None): 236 | lambda to execute when done without error 237 | 238 | fail (`lambda`, default=None): 239 | lambda to execute when the condition is False, the lambda will be execute before raising the exception. 240 | 241 | silent (`bool`, default=False): 242 | if True then it won't raise the exception, only execute fail lambda. 243 | 244 | stack_index (`int`, default=1): 245 | stack index to raise the exception with trace back to where it happens, intended for internal usage. 246 | 247 | ### Raises: 248 | `OpenTeleException`: exception 249 | """ 250 | 251 | 252 | @typing.overload 253 | def Expects( 254 | condition: bool, 255 | exception: OpenTeleException = None, 256 | done: typing.Callable[[], None] = None, 257 | fail: typing.Callable[[OpenTeleException], None] = None, 258 | silent: bool = False, 259 | stack_index: int = 1, 260 | ) -> bool: 261 | 262 | """Expect a condition to be `True`, raise an `OpenTeleException` if it's not. 263 | 264 | ### Arguments: 265 | condition (bool): 266 | Condition that you're expecting. 267 | 268 | message (OpenTeleException, default=None): 269 | Custom exception. 270 | 271 | done (`lambda`, default=None): 272 | lambda to execute when done without error. 273 | 274 | fail (`lambda`, default=None): 275 | lambda to execute when the condition is False, the lambda will be execute before raising the exception. 276 | 277 | silent (`bool`, default=False): 278 | if True then it won't raise the exception, only execute fail lambda. 279 | 280 | stack_index (`int`, default=1): 281 | stack index to raise the exception with trace back to where it happens, intended for internal usage. 282 | 283 | ### Raises: 284 | `OpenTeleException`: exception 285 | """ 286 | 287 | 288 | def Expects( 289 | condition: bool, 290 | exception: typing.Union[OpenTeleException, str] = None, 291 | done: typing.Callable[[], None] = None, 292 | fail: typing.Callable[[OpenTeleException], None] = None, 293 | silent: bool = False, 294 | stack_index: int = 1, 295 | ) -> bool: # nocov 296 | 297 | if condition: 298 | if done != None: 299 | done() 300 | return condition 301 | 302 | if isinstance(exception, str): 303 | exception = OpenTeleException(exception, 2) 304 | 305 | elif exception != None and not isinstance(exception, OpenTeleException): 306 | raise OpenTeleException("No instance of Expects() match the arguments given", 2) 307 | 308 | if exception == None: 309 | exception = OpenTeleException("Unexpected error", 2) 310 | 311 | # no raise exception 312 | if silent: 313 | if fail != None: 314 | fail(exception) 315 | return condition 316 | 317 | else: 318 | stack = inspect.stack() 319 | frame = stack[stack_index].frame 320 | tb = types.TracebackType(None, frame, frame.f_lasti, frame.f_lineno) # type: ignore 321 | exception = exception.with_traceback(tb) 322 | 323 | if fail != None: 324 | fail(exception) 325 | 326 | raise exception 327 | 328 | 329 | def ExpectStreamStatus(stream: QDataStream, message: str = "Could not stream data"): 330 | Expects( 331 | stream.status() == QDataStream.Status.Ok, 332 | stack_index=2, 333 | exception=QDataStreamFailed( 334 | "Could not read keys count from mtp authorization.", stack_index=2 335 | ), 336 | ) 337 | -------------------------------------------------------------------------------- /src/td/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from .shared import * 3 | -------------------------------------------------------------------------------- /src/td/auth.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from .configs import * 3 | from . import shared as td 4 | 5 | import hashlib 6 | 7 | # if TYPE_CHECKING: 8 | # from ..opentele import * 9 | 10 | 11 | class AuthKeyType(IntEnum): 12 | """ 13 | Type of `AuthKey` 14 | 15 | ### Attributes: 16 | Generated (`IntEnum`): 17 | Generated key 18 | 19 | Temporary (`IntEnum`): 20 | Temporary key 21 | 22 | ReadFromFile (`IntEnum`): 23 | Key red from file 24 | 25 | Local (`IntEnum`): 26 | Local key 27 | """ 28 | 29 | Generated = 0 30 | Temporary = 1 31 | ReadFromFile = 2 32 | Local = 3 33 | 34 | 35 | class AuthKey(BaseObject): 36 | """ 37 | Authorization key used for [MTProto](https://core.telegram.org/mtproto) 38 | It's also used to encrypt and decrypt local tdata 39 | 40 | ### Attributes: 41 | DcId (DcId): 42 | Data Center ID (from 1 to 5). 43 | 44 | type (AuthKeyType): 45 | Type of the key. 46 | 47 | key (bytes): 48 | The actual key, 256 `bytes` in length. 49 | 50 | """ 51 | 52 | kSize = 256 53 | 54 | def __init__(self, key: bytes = bytes(), type: AuthKeyType = AuthKeyType.Generated, dcId: DcId = DcId.Invalid) -> None: # type: ignore 55 | self.__type = type 56 | self.__dcId = dcId 57 | self.__key = key 58 | # if (type == self.Type.Generated) or (type == self.Type.Temporary): 59 | # self.__creationtime = ... 60 | self.__countKeyId() 61 | 62 | @property 63 | def dcId(self) -> DcId: 64 | return self.__dcId 65 | 66 | @property 67 | def type(self) -> AuthKeyType: 68 | return self.__type 69 | 70 | @property 71 | def key(self) -> bytes: 72 | return self.__key 73 | 74 | def write(self, to: QDataStream) -> None: 75 | to.writeRawData(self.key) 76 | 77 | def __countKeyId(self) -> None: 78 | hash = hashlib.sha1(self.__key).digest() 79 | self.__keyId = int.from_bytes(hash[12 : 12 + 8], "little") 80 | 81 | def prepareAES_oldmtp( 82 | self, msgKey: bytes, send: bool 83 | ) -> typing.Tuple[bytes, bytes]: 84 | x = 0 if send else 8 85 | sha1_a = hashlib.sha1(msgKey[:16] + self.__key[x : x + 32]).digest() 86 | 87 | sha1_b = hashlib.sha1( 88 | self.__key[x + 32 : x + 32 + 16] 89 | + msgKey[:16] 90 | + self.__key[x + 48 : x + 48 + 16] 91 | ).digest() 92 | 93 | sha1_c = hashlib.sha1(self.__key[x + 64 : x + 64 + 32] + msgKey[:16]).digest() 94 | sha1_d = hashlib.sha1(msgKey[:16] + self.__key[x + 96 : x + 96 + 32]).digest() 95 | 96 | aesKey = sha1_a[:8] + sha1_b[8 : 8 + 12] + sha1_c[4 : 4 + 12] 97 | aesIv = sha1_a[8 : 8 + 12] + sha1_b[:8] + sha1_c[16 : 16 + 4] + sha1_d[:8] 98 | 99 | return aesKey, aesIv 100 | 101 | @staticmethod 102 | def FromStream( 103 | stream: QDataStream, 104 | type: AuthKeyType = AuthKeyType.ReadFromFile, 105 | dcId: DcId = DcId(0), 106 | ) -> AuthKey: 107 | keyData = stream.readRawData(AuthKey.kSize) 108 | return AuthKey(keyData, type, dcId) 109 | -------------------------------------------------------------------------------- /src/td/configs.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from enum import IntEnum 4 | 5 | from ..exception import * 6 | from ..utils import * 7 | from ..api import * 8 | from .. import tl 9 | 10 | from typing import ( 11 | Union, 12 | Callable, 13 | TypeVar, 14 | Type, 15 | Optional, 16 | List, 17 | Dict, 18 | Any, 19 | TYPE_CHECKING, 20 | ) 21 | from ctypes import ( 22 | sizeof, 23 | c_int32 as int32, 24 | c_int64 as int64, 25 | c_uint32 as uint32, 26 | c_uint64 as uint64, 27 | c_short as short, 28 | c_ushort as ushort, 29 | ) 30 | from PyQt5.QtCore import ( 31 | QByteArray, 32 | QDataStream, 33 | QBuffer, 34 | QIODevice, 35 | QSysInfo, 36 | QDir, 37 | QFile, 38 | ) 39 | from types import FunctionType 40 | 41 | import asyncio 42 | 43 | 44 | APP_VERSION = 3004000 45 | TDF_MAGIC = b"TDF$" 46 | 47 | 48 | _T = TypeVar("_T") 49 | _T2 = TypeVar("_T2") 50 | _TCLS = TypeVar("_TCLS", bound=type) 51 | _RT = TypeVar("_RT") 52 | 53 | _F = TypeVar("_F", bound=Callable[..., Any]) 54 | 55 | 56 | class BareId(int): # nocov 57 | """ 58 | BareId 59 | """ 60 | 61 | 62 | class ChatIdType(BaseObject): # nocov 63 | """ 64 | ChatIdType 65 | """ 66 | 67 | bare = BareId(0) 68 | kShift = BareId(0) 69 | kReservedBit = BareId(0x80) 70 | 71 | def __init__(self, value: BareId) -> None: 72 | self.bare = value 73 | 74 | 75 | class UserId(ChatIdType): # nocov 76 | kShift = BareId(0) 77 | 78 | 79 | class ChatId(ChatIdType): # nocov 80 | kShift = BareId(1) 81 | 82 | 83 | class ChannelId(ChatIdType): # nocov 84 | kShift = BareId(2) 85 | 86 | 87 | class FakeChatId(ChatIdType): # nocov 88 | kShift = BareId(0x7F) 89 | 90 | 91 | class PeerId(int): # nocov 92 | """ 93 | PeerId 94 | """ 95 | 96 | kChatTypeMask = BareId(0xFFFFFFFFFFFF) 97 | 98 | def __init__(self, value) -> None: 99 | self.value = value 100 | 101 | def Serialize(self): 102 | Expects(not (self.value & (UserId.kReservedBit << 48))) 103 | return self.value | (UserId.kReservedBit << 48) 104 | 105 | @staticmethod 106 | def FromChatIdType( 107 | id: typing.Union[UserId, ChatId, ChannelId, FakeChatId] 108 | ) -> PeerId: 109 | return PeerId(id.bare | (BareId(id.kShift) << 48)) 110 | 111 | @staticmethod 112 | def FromSerialized(serialized: int) -> PeerId: 113 | flag = UserId.kReservedBit << 48 114 | legacy = not (serialized & (UserId.kReservedBit << 48)) 115 | 116 | if not legacy: 117 | return PeerId(serialized & (~flag)) 118 | 119 | PeerIdMask = int(0xFFFFFFFF) 120 | PeerIdTypeMask = 0xF00000000 121 | PeerIdUserShift = 0x000000000 122 | PeerIdChatShift = 0x100000000 123 | PeerIdChannelShift = 0x200000000 124 | PeerIdFakeShift = 0xF00000000 125 | 126 | if (serialized & PeerIdTypeMask) == PeerIdUserShift: 127 | return PeerId.FromChatIdType(UserId(BareId(serialized & PeerIdMask))) 128 | 129 | elif (serialized & PeerIdTypeMask) == PeerIdChatShift: 130 | return PeerId.FromChatIdType(ChatId(BareId(serialized & PeerIdMask))) 131 | 132 | elif (serialized & PeerIdTypeMask) == PeerIdChannelShift: 133 | return PeerId.FromChatIdType(ChannelId(BareId(serialized & PeerIdMask))) 134 | 135 | elif (serialized & PeerIdTypeMask) == PeerIdFakeShift: 136 | return PeerId.FromChatIdType(FakeChatId(BareId(serialized & PeerIdMask))) 137 | 138 | return PeerId(0) 139 | 140 | 141 | class FileKey(int): # nocov 142 | pass 143 | 144 | 145 | class DcId(int): # nocov 146 | """ 147 | Data Center ID 148 | """ 149 | 150 | kDcShift: DcId = 10000 # type: ignore 151 | Invalid: DcId = 0 # type: ignore 152 | _0: DcId = 0 # type: ignore 153 | _1: DcId = 1 # type: ignore 154 | _2: DcId = 2 # type: ignore 155 | _3: DcId = 3 # type: ignore 156 | _4: DcId = 4 # type: ignore 157 | _5: DcId = 5 # type: ignore 158 | 159 | @staticmethod 160 | def BareDcId(shiftedDcId: Union[ShiftedDcId, DcId]) -> DcId: 161 | return DcId(shiftedDcId % DcId.kDcShift) 162 | 163 | 164 | class ShiftedDcId(DcId): # nocov 165 | """ 166 | Shifted Data Center ID 167 | """ 168 | 169 | @staticmethod 170 | def ShiftDcId(dcId: DcId, value: int) -> ShiftedDcId: 171 | return ShiftedDcId(dcId + DcId.kDcShift * value) 172 | 173 | 174 | class BuiltInDc(BaseObject): # type: ignore # nocov 175 | """ 176 | Default DC that is hard-coded 177 | """ 178 | 179 | def __init__(self, id: DcId, ip: str, port: int): 180 | self.id = id 181 | self.ip = ip 182 | self.port = port 183 | 184 | 185 | class BuiltInDc(BuiltInDc): # nocov 186 | kBuiltInDcs = [ 187 | BuiltInDc(DcId._1, "149.154.175.50", 443), 188 | BuiltInDc(DcId._2, "149.154.167.51", 443), 189 | BuiltInDc(DcId._2, "95.161.76.100", 443), 190 | BuiltInDc(DcId._3, "149.154.175.100", 443), 191 | BuiltInDc(DcId._4, "149.154.167.91", 443), 192 | BuiltInDc(DcId._5, "149.154.171.5", 443), 193 | ] 194 | 195 | kBuiltInDcsIPv6 = [ 196 | BuiltInDc(DcId._1, "2001:0b28:f23d:f001:0000:0000:0000:000a", 443), 197 | BuiltInDc(DcId._2, "2001:067c:04e8:f002:0000:0000:0000:000a", 443), 198 | BuiltInDc(DcId._3, "2001:0b28:f23d:f003:0000:0000:0000:000a", 443), 199 | BuiltInDc(DcId._4, "2001:067c:04e8:f004:0000:0000:0000:000a", 443), 200 | BuiltInDc(DcId._5, "2001:0b28:f23f:f005:0000:0000:0000:000a", 443), 201 | ] 202 | 203 | kBuiltInDcsTest = [ 204 | BuiltInDc(DcId._1, "149.154.175.10", 443), 205 | BuiltInDc(DcId._2, "149.154.167.40", 443), 206 | BuiltInDc(DcId._3, "149.154.175.117", 443), 207 | ] 208 | 209 | kBuiltInDcsIPv6Test = [ 210 | BuiltInDc(DcId._1, "2001:0b28:f23d:f001:0000:0000:0000:000e", 443), 211 | BuiltInDc(DcId._2, "2001:067c:04e8:f002:0000:0000:0000:000e", 443), 212 | BuiltInDc(DcId._3, "2001:0b28:f23d:f003:0000:0000:0000:000e", 443), 213 | ] 214 | 215 | 216 | class dbi(int): # nocov 217 | Key = 0x00 218 | User = 0x01 219 | DcOptionOldOld = 0x02 220 | ChatSizeMaxOld = 0x03 221 | MutePeerOld = 0x04 222 | SendKeyOld = 0x05 223 | AutoStart = 0x06 224 | StartMinimized = 0x07 225 | SoundFlashBounceNotifyOld = 0x08 226 | WorkModeOld = 0x09 227 | SeenTrayTooltip = 0x0A 228 | DesktopNotifyOld = 0x0B 229 | AutoUpdate = 0x0C 230 | LastUpdateCheck = 0x0D 231 | WindowPositionOld = 0x0E 232 | ConnectionTypeOldOld = 0x0F 233 | # 0x10 reserved 234 | DefaultAttach = 0x11 235 | CatsAndDogsOld = 0x12 236 | ReplaceEmojiOld = 0x13 237 | AskDownloadPathOld = 0x14 238 | DownloadPathOldOld = 0x15 239 | ScaleOld = 0x16 240 | EmojiTabOld = 0x17 241 | RecentEmojiOldOldOld = 0x18 242 | LoggedPhoneNumberOld = 0x19 243 | MutedPeersOld = 0x1A 244 | # 0x1b reserved 245 | NotifyViewOld = 0x1C 246 | SendToMenu = 0x1D 247 | CompressPastedImageOld = 0x1E 248 | LangOld = 0x1F 249 | LangFileOld = 0x20 250 | TileBackgroundOld = 0x21 251 | AutoLockOld = 0x22 252 | DialogLastPath = 0x23 253 | RecentEmojiOldOld = 0x24 254 | EmojiVariantsOldOld = 0x25 255 | RecentStickers = 0x26 256 | DcOptionOld = 0x27 257 | TryIPv6Old = 0x28 258 | SongVolumeOld = 0x29 259 | WindowsNotificationsOld = 0x30 260 | IncludeMutedOld = 0x31 261 | MegagroupSizeMaxOld = 0x32 262 | DownloadPathOld = 0x33 263 | AutoDownloadOld = 0x34 264 | SavedGifsLimitOld = 0x35 265 | ShowingSavedGifsOld = 0x36 266 | AutoPlayOld = 0x37 267 | AdaptiveForWideOld = 0x38 268 | HiddenPinnedMessagesOld = 0x39 269 | RecentEmojiOld = 0x3A 270 | EmojiVariantsOld = 0x3B 271 | DialogsModeOld = 0x40 272 | ModerateModeOld = 0x41 273 | VideoVolumeOld = 0x42 274 | StickersRecentLimitOld = 0x43 275 | NativeNotificationsOld = 0x44 276 | NotificationsCountOld = 0x45 277 | NotificationsCornerOld = 0x46 278 | ThemeKeyOld = 0x47 279 | DialogsWidthRatioOld = 0x48 280 | UseExternalVideoPlayer = 0x49 281 | DcOptionsOld = 0x4A 282 | MtpAuthorization = 0x4B 283 | LastSeenWarningSeenOld = 0x4C 284 | SessionSettings = 0x4D 285 | LangPackKey = 0x4E 286 | ConnectionTypeOld = 0x4F 287 | StickersFavedLimitOld = 0x50 288 | SuggestStickersByEmojiOld = 0x51 289 | SuggestEmojiOld = 0x52 290 | TxtDomainStringOldOld = 0x53 291 | ThemeKey = 0x54 292 | TileBackground = 0x55 293 | CacheSettingsOld = 0x56 294 | AnimationsDisabled = 0x57 295 | ScalePercent = 0x58 296 | PlaybackSpeedOld = 0x59 297 | LanguagesKey = 0x5A 298 | CallSettingsOld = 0x5B 299 | CacheSettings = 0x5C 300 | TxtDomainStringOld = 0x5D 301 | ApplicationSettings = 0x5E 302 | DialogsFiltersOld = 0x5F 303 | FallbackProductionConfig = 0x60 304 | BackgroundKey = 0x61 305 | 306 | EncryptedWithSalt = 333 307 | Encrypted = 444 308 | 309 | Version = 666 310 | 311 | 312 | class lskType(int): # nocov 313 | lskUserMap = 0x00 314 | lskDraft = 0x01 # data: PeerId peer 315 | lskDraftPosition = 0x02 # data: PeerId peer 316 | lskLegacyImages = 0x03 # legacy 317 | lskLocations = 0x04 # no data 318 | lskLegacyStickerImages = 0x05 # legacy 319 | lskLegacyAudios = 0x06 # legacy 320 | lskRecentStickersOld = 0x07 # no data 321 | lskBackgroundOldOld = 0x08 # no data 322 | lskUserSettings = 0x09 # no data 323 | lskRecentHashtagsAndBots = 0x0A # no data 324 | lskStickersOld = 0x0B # no data 325 | lskSavedPeersOld = 0x0C # no data 326 | lskReportSpamStatusesOld = 0x0D # no data 327 | lskSavedGifsOld = 0x0E # no data 328 | lskSavedGifs = 0x0F # no data 329 | lskStickersKeys = 0x10 # no data 330 | lskTrustedBots = 0x11 # no data 331 | lskFavedStickers = 0x12 # no data 332 | lskExportSettings = 0x13 # no data 333 | lskBackgroundOld = 0x14 # no data 334 | lskSelfSerialized = 0x15 # serialized self 335 | lskMasksKeys = 0x16 # no data 336 | lskCustomEmojiKeys = 0x17 # no data 337 | lskSearchSuggestions = 0x18 # no data 338 | lskWebviewTokens = 0x19 # data: QByteArray bots, QByteArray other 339 | 340 | 341 | class BotTrustFlag(int): # nocov 342 | NoOpenGame = 1 << 0 343 | Payment = 1 << 1 344 | 345 | 346 | # class QByteArray(QByteArray): 347 | # def dump(self): 348 | 349 | # print(f" ---- hexdump ----: [0x{hex(self.size())}:{self.size()}] ({id(self)})") 350 | 351 | # size = self.size() 352 | # data = self.data() 353 | # out = "" 354 | # asc = "" 355 | 356 | # for i in range(size): 357 | 358 | # if i % 16 == 0: 359 | # if i != 0: 360 | # out += " | " 361 | # out += asc + "\n" 362 | # asc = "" 363 | 364 | # out += " %0.4x " % i 365 | 366 | # out += " %0.2x" % data[i] 367 | 368 | # if (i > 0) and ((i % 8) == 7) and ((i % 16) != 15): 369 | # out += " " 370 | 371 | # if (data[i] < 0x20) or (data[i] > 0x7E): 372 | # asc += "." 373 | # else: 374 | # asc += chr(data[i]) 375 | 376 | # i = size - 1 377 | # if ((i % 16) != 0) and ((i % 16) == (i % 8)): 378 | # out += " " 379 | 380 | # while (i % 16) != 0: 381 | # out += " " 382 | # i += 1 383 | 384 | # out += " | " 385 | # print(out) 386 | 387 | 388 | # def hexdump(byte: bytes): 389 | 390 | # if isinstance(byte, QByteArray): 391 | # byte = byte.data() 392 | 393 | # print( 394 | # f" ---- hexdump ----: [0x{hex(byte.__len__())}:{byte.__len__()}] ({id(byte)})" 395 | # ) 396 | 397 | # size = byte.__len__() 398 | # data = byte 399 | # out = "" 400 | # asc = "" 401 | 402 | # for i in range(size): 403 | 404 | # if i % 16 == 0: 405 | # if i != 0: 406 | # out += " | " 407 | # out += asc + "\n" 408 | # asc = "" 409 | 410 | # out += " %0.4x " % i 411 | 412 | # out += " %0.2x" % data[i] 413 | 414 | # if (i > 0) and ((i % 8) == 7) and ((i % 16) != 15): 415 | # out += " " 416 | 417 | # if (data[i] < 0x20) or (data[i] > 0x7E): 418 | # asc += "." 419 | # else: 420 | # asc += chr(data[i]) 421 | 422 | # i = size - 1 423 | # if ((i % 16) != 0) and ((i % 16) == (i % 8)): 424 | # out += " " 425 | 426 | # while (i % 16) != 0: 427 | # out += " " 428 | # i += 1 429 | 430 | # out += " | " 431 | # print(out) 432 | -------------------------------------------------------------------------------- /src/td/mtp.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .configs import * 4 | from . import shared as td 5 | 6 | 7 | class MTP(BaseObject): # nocov 8 | """ 9 | [MTProto Protocal](https://core.telegram.org/mtproto) 10 | 11 | This class is for further future developments and has no usage for now. 12 | 13 | ### Attributes: 14 | `Environment` (`class`): MTP Enviroment 15 | `RSAPublicKey` (`class`): RSAPublicKey 16 | `DcOptions` (`class`): DcOptions 17 | `ConfigFields` (`class`): ConfigFields 18 | `Config` (`class`): Config 19 | """ 20 | 21 | class Environment(IntEnum): 22 | """ 23 | Enviroment flag for MTP.Config 24 | ### Attributes: 25 | Production (`IntEnum`): Production Enviroment 26 | Test (`IntEnum`): Test Enviroment 27 | """ 28 | 29 | Production = 0 30 | Test = 1 31 | 32 | class RSAPublicKey(BaseObject): 33 | """ 34 | To be added 35 | """ 36 | 37 | class DcOptions(BaseObject): 38 | """ 39 | Data Center Options, providing information about DC ip, port,.. etc 40 | """ 41 | 42 | kVersion = 2 43 | 44 | def __init__(self, enviroment: MTP.Environment) -> None: 45 | self._enviroment = enviroment 46 | self._publicKeys: typing.Dict[DcId, MTP.RSAPublicKey] = {} 47 | self._cdnPublicKeys: typing.Dict[DcId, MTP.RSAPublicKey] = {} 48 | self._data: typing.Dict[DcId, typing.List[MTP.DcOptions.Endpoint]] = {} 49 | 50 | self.constructFromBuiltIn() 51 | 52 | def isTestMode(self): 53 | return self._enviroment != MTP.Environment.Production 54 | 55 | def constructAddOne( 56 | self, id: DcId, flags: MTP.DcOptions.Flag, ip: str, port: int, secret: bytes 57 | ): 58 | self.applyOneGuarded(DcId.BareDcId(id), flags, ip, port, secret) 59 | 60 | def applyOneGuarded( 61 | self, id: DcId, flags: MTP.DcOptions.Flag, ip: str, port: int, secret: bytes 62 | ): 63 | 64 | if not id in self._data: 65 | self._data[id] = [] 66 | else: 67 | for endpoint in self._data[id]: 68 | if (endpoint.ip == ip) and (endpoint.port == port): 69 | # skip this endpoint because it's already exists in the self._data 70 | continue 71 | 72 | endpoint = MTP.DcOptions.Endpoint(id, flags, ip, port, bytes()) 73 | self._data[id].append(endpoint) 74 | 75 | def constructFromBuiltIn(self) -> None: 76 | 77 | # TO BE ADDED 78 | # self.readBuiltInPublicKeys() 79 | 80 | def addToData(dcs: List[BuiltInDc], flags: MTP.DcOptions.Flag): 81 | 82 | for dc in dcs: 83 | self.applyOneGuarded(dc.id, flags, dc.ip, dc.port, bytes()) 84 | 85 | if self.isTestMode(): 86 | addToData(BuiltInDc.kBuiltInDcsTest, MTP.DcOptions.Flag.f_static | 0) # type: ignore 87 | addToData(BuiltInDc.kBuiltInDcsIPv6Test, MTP.DcOptions.Flag.f_static | MTP.DcOptions.Flag.f_ipv6) # type: ignore 88 | else: 89 | addToData(BuiltInDc.kBuiltInDcs, MTP.DcOptions.Flag.f_static | 0) # type: ignore 90 | addToData(BuiltInDc.kBuiltInDcsIPv6, MTP.DcOptions.Flag.f_static | MTP.DcOptions.Flag.f_ipv6) # type: ignore 91 | 92 | def constructFromSerialized(self, serialized: QByteArray): 93 | stream = QDataStream(serialized) 94 | stream.setVersion(QDataStream.Version.Qt_5_1) 95 | 96 | minusVersion = stream.readInt32() 97 | version = (-minusVersion) if (minusVersion < 0) else 0 98 | 99 | count = stream.readInt32() if version > 0 else minusVersion 100 | 101 | self._data.clear() 102 | 103 | for i in range(0, count): 104 | dcId = DcId(stream.readInt32()) 105 | flags = MTP.DcOptions.Flag(stream.readInt32()) 106 | port = stream.readInt32() 107 | ipSize = stream.readInt32() 108 | 109 | kMaxIpSize = 45 110 | Expects( 111 | condition=((ipSize > 0) and (ipSize <= kMaxIpSize)), 112 | exception=TDataBadConfigData("Bad ipSize data"), 113 | ) 114 | 115 | ip = stream.readRawData(ipSize).decode("ascii") 116 | 117 | kMaxSecretSize = 32 118 | secret = bytes() 119 | if version > 0: 120 | secretSize = stream.readInt32() 121 | 122 | Expects( 123 | condition=( 124 | (secretSize >= 0) and (secretSize <= kMaxSecretSize) 125 | ), 126 | exception=TDataBadConfigData("Bad secretSize data"), 127 | ) 128 | 129 | if secretSize > 0: 130 | secret = stream.readRawData(secretSize) 131 | 132 | ExpectStreamStatus(stream, "Could not stream config data") 133 | 134 | self.applyOneGuarded(dcId, flags, ip, port, secret) 135 | 136 | # TO BE ADDED 137 | # Read CDN config 138 | 139 | def Serialize(self) -> QByteArray: 140 | 141 | optionsCount = 0 142 | for dcId, endpoints in self._data.items(): 143 | if DcId.BareDcId(dcId) > 1000: 144 | continue 145 | optionsCount += len(endpoints) 146 | 147 | result = QByteArray() 148 | stream = QDataStream(result, QIODevice.OpenModeFlag.WriteOnly) 149 | stream.setVersion(QDataStream.Version.Qt_5_1) 150 | 151 | stream.writeInt32(-MTP.DcOptions.kVersion) # -2 152 | 153 | # Dc options. 154 | stream.writeInt32(optionsCount) 155 | for dcId, endpoints in self._data.items(): 156 | if DcId.BareDcId(dcId) > 1000: 157 | continue 158 | 159 | # write endpoints 160 | for endpoint in endpoints: 161 | stream.writeInt32(endpoint.id) 162 | stream.writeInt32(endpoint.flags) 163 | 164 | stream.writeInt32(len(endpoint.ip)) 165 | stream.writeRawData(endpoint.ip.encode("ascii")) 166 | 167 | stream.writeInt32(len(endpoint.secret)) 168 | stream.writeRawData(endpoint.secret) 169 | 170 | # CDN public keys. 171 | # TO BE ADDED 172 | publicKeys = [] 173 | stream.writeInt32(len(publicKeys)) 174 | 175 | # for (auto &key : publicKeys) { 176 | # stream << qint32(key.dcId) 177 | # << Serialize::bytes(key.n) 178 | # << Serialize::bytes(key.e); 179 | # } 180 | 181 | return result 182 | 183 | class Address(int): 184 | """ 185 | Connection flag used for MTP.DcOptions.Endpoint 186 | 187 | ### Attributes: 188 | IPv4 (`int`): IPv4 connection 189 | IPv6 (`int`): IPv6 connection 190 | """ 191 | 192 | IPv4 = 0 193 | IPv6 = 1 194 | 195 | class Protocol(int): 196 | """ 197 | Protocal flag used for MTP.DcOptions.Endpoint 198 | 199 | ### Attributes: 200 | Tcp (`int`): Tcp connection 201 | Http (`int`): Http connection 202 | """ 203 | 204 | Tcp = 0 205 | Http = 1 206 | 207 | class Flag(int): 208 | """ 209 | Flag used for MTP.DcOptions.Endpoint 210 | 211 | ### Attributes: 212 | f_ipv6 (`int`): f_ipv6 213 | f_media_only (`int`): f_media_only 214 | f_tcpo_only (`int`): f_tcpo_only 215 | f_cdn (`int`): f_cdn 216 | f_static (`int`): f_static 217 | f_secret (`int`): f_secret 218 | MAX_FIELD (`int`): MAX_FIELD 219 | """ 220 | 221 | f_ipv6 = 1 << 0 222 | f_media_only = 1 << 1 223 | f_tcpo_only = 1 << 2 224 | f_cdn = 1 << 3 225 | f_static = 1 << 4 226 | f_secret = 1 << 10 227 | MAX_FIELD = 1 << 10 228 | 229 | class Endpoint(BaseObject): 230 | """ 231 | Data center endpoint 232 | 233 | ### Attributes: 234 | id (`DcId`): Data Center ID 235 | flags (`Flag`): `Flag` 236 | ip (`str`): IP address of the data center 237 | port (`int`): Port to connect to 238 | secret (`bytes`): secret 239 | """ 240 | 241 | def __init__( 242 | self, 243 | id: int, 244 | flags: MTP.DcOptions.Flag, 245 | ip: str, 246 | port: int, 247 | secret: bytes, 248 | ) -> None: 249 | self.id = id 250 | self.flags = flags 251 | self.ip = ip 252 | self.port = port 253 | self.secret = secret 254 | 255 | class ConfigFields(BaseObject): 256 | """ 257 | Configuration data for `MTP.Config` 258 | 259 | ### Attributes: 260 | chatSizeMax (`int`): `200` 261 | megagroupSizeMax (`int`): `10000` 262 | forwardedCountMax (`int`): `100` 263 | onlineUpdatePeriod (`int`): `120000` 264 | offlineBlurTimeout (`int`): `5000` 265 | offlineIdleTimeout (`int`): `30000` 266 | onlineFocusTimeout (`int`): `1000` `# Not from the server config.` 267 | onlineCloudTimeout (`int`): `300000` 268 | notifyCloudDelay (`int`): `30000` 269 | notifyDefaultDelay (`int`): `1500` 270 | savedGifsLimit (`int`): `200` 271 | editTimeLimit (`int`): `172800` 272 | revokeTimeLimit (`int`): `172800` 273 | revokePrivateTimeLimit (`int`): `172800` 274 | revokePrivateInbox (`bool`): `False` 275 | stickersRecentLimit (`int`): `30` 276 | stickersFavedLimit (`int`): `5` 277 | pinnedDialogsCountMax (`int`): `5` 278 | pinnedDialogsInFolderMax (`int`): `100` 279 | internalLinksDomain (`str`): `"https://t.me/"` 280 | channelsReadMediaPeriod (`int`): `86400 * 7` 281 | callReceiveTimeoutMs (`int`): `20000` 282 | callRingTimeoutMs (`int`): `90000` 283 | callConnectTimeoutMs (`int`): `30000` 284 | callPacketTimeoutMs (`int`): `10000` 285 | webFileDcId (`int`): `4` 286 | txtDomainString (`str`): `str()` 287 | phoneCallsEnabled (`bool`): `True` 288 | blockedMode (`bool`): `False` 289 | captionLengthMax (`int`): `1024` 290 | """ 291 | 292 | def __init__(self) -> None: 293 | self.chatSizeMax = 200 294 | self.megagroupSizeMax = 10000 295 | self.forwardedCountMax = 100 296 | self.onlineUpdatePeriod = 120000 297 | self.offlineBlurTimeout = 5000 298 | self.offlineIdleTimeout = 30000 299 | self.onlineFocusTimeout = 1000 # Not from the server config. 300 | self.onlineCloudTimeout = 300000 301 | self.notifyCloudDelay = 30000 302 | self.notifyDefaultDelay = 1500 303 | self.savedGifsLimit = 200 304 | self.editTimeLimit = 172800 305 | self.revokeTimeLimit = 172800 306 | self.revokePrivateTimeLimit = 172800 307 | self.revokePrivateInbox = False 308 | self.stickersRecentLimit = 30 309 | self.stickersFavedLimit = 5 310 | self.pinnedDialogsCountMax = 5 311 | self.pinnedDialogsInFolderMax = 100 312 | self.internalLinksDomain = "https://t.me/" 313 | self.channelsReadMediaPeriod = 86400 * 7 314 | self.callReceiveTimeoutMs = 20000 315 | self.callRingTimeoutMs = 90000 316 | self.callConnectTimeoutMs = 30000 317 | self.callPacketTimeoutMs = 10000 318 | self.webFileDcId = 4 319 | self.txtDomainString = str() 320 | self.phoneCallsEnabled = True 321 | self.blockedMode = False 322 | self.captionLengthMax = 1024 323 | 324 | class Config(BaseObject): 325 | """ 326 | Configuration of MTProto 327 | ### Attributes: 328 | kVersion (`int`): `1` 329 | """ 330 | 331 | kVersion = 1 332 | 333 | def __init__(self, enviroment: MTP.Environment) -> None: 334 | self._dcOptions = MTP.DcOptions(enviroment) 335 | self._fields = MTP.ConfigFields() 336 | self._fields.webFileDcId = 2 if self._dcOptions.isTestMode() else 4 337 | self._fields.txtDomainString = ( 338 | "tapv3.stel.com" if self._dcOptions.isTestMode() else "apv3.stel.com" 339 | ) 340 | 341 | def endpoints( 342 | self, dcId: DcId = DcId._0 343 | ) -> Dict[ 344 | MTP.DcOptions.Address, 345 | Dict[MTP.DcOptions.Protocol, List[MTP.DcOptions.Endpoint]], 346 | ]: 347 | 348 | endpoints = self._dcOptions._data[dcId] 349 | 350 | Address = MTP.DcOptions.Address 351 | Protocol = MTP.DcOptions.Protocol 352 | Flag = MTP.DcOptions.Flag 353 | Endpoint = MTP.DcOptions.Endpoint 354 | 355 | results: Dict[Address, Dict[Protocol, List[Endpoint]]] = {} 356 | results[Address.IPv4] = {Protocol.Tcp: [], Protocol.Http: []} # type: ignore 357 | results[Address.IPv6] = {Protocol.Tcp: [], Protocol.Http: []} # type: ignore 358 | 359 | for endpoint in endpoints: 360 | 361 | if dcId == 0 or endpoint.id == dcId: 362 | 363 | flags = endpoint.flags 364 | address = Address.IPv6 if (flags & Flag.f_ipv6) else Address.IPv4 365 | results[address][Protocol.Tcp].append(endpoint) # type: ignore 366 | 367 | if not (flags & (Flag.f_tcpo_only | Flag.f_secret)): 368 | results[address][Protocol.Http].append(endpoint) # type: ignore 369 | 370 | return results 371 | 372 | def Serialize(self) -> QByteArray: 373 | options = self._dcOptions.Serialize() 374 | size = sizeof(int32) * 2 375 | size += td.Serialize.bytearraySize(options) 376 | size += 28 * sizeof(int32) 377 | size += td.Serialize.stringSize(self._fields.internalLinksDomain) 378 | size += td.Serialize.stringSize(self._fields.txtDomainString) 379 | 380 | result = QByteArray() 381 | stream = QDataStream(result, QIODevice.OpenModeFlag.WriteOnly) 382 | stream.setVersion(QDataStream.Version.Qt_5_1) 383 | 384 | stream.writeInt32(MTP.Config.kVersion) 385 | stream.writeInt32( 386 | MTP.Environment.Test 387 | if self._dcOptions.isTestMode() 388 | else MTP.Environment.Production 389 | ) 390 | 391 | stream << options 392 | 393 | stream.writeInt32(self._fields.chatSizeMax) 394 | stream.writeInt32(self._fields.megagroupSizeMax) 395 | stream.writeInt32(self._fields.forwardedCountMax) 396 | stream.writeInt32(self._fields.onlineUpdatePeriod) 397 | stream.writeInt32(self._fields.offlineBlurTimeout) 398 | stream.writeInt32(self._fields.offlineIdleTimeout) 399 | stream.writeInt32(self._fields.onlineFocusTimeout) 400 | stream.writeInt32(self._fields.onlineCloudTimeout) 401 | stream.writeInt32(self._fields.notifyCloudDelay) 402 | stream.writeInt32(self._fields.notifyDefaultDelay) 403 | stream.writeInt32(self._fields.savedGifsLimit) 404 | stream.writeInt32(self._fields.editTimeLimit) 405 | stream.writeInt32(self._fields.revokeTimeLimit) 406 | stream.writeInt32(self._fields.revokePrivateTimeLimit) 407 | stream.writeInt32(1 if self._fields.revokePrivateInbox else 0) 408 | stream.writeInt32(self._fields.stickersRecentLimit) 409 | stream.writeInt32(self._fields.stickersFavedLimit) 410 | stream.writeInt32(self._fields.pinnedDialogsCountMax) 411 | stream.writeInt32(self._fields.pinnedDialogsInFolderMax) 412 | stream.writeQString(self._fields.internalLinksDomain) 413 | # stream << self._fields.internalLinksDomain 414 | stream.writeInt32(self._fields.channelsReadMediaPeriod) 415 | stream.writeInt32(self._fields.callReceiveTimeoutMs) 416 | stream.writeInt32(self._fields.callRingTimeoutMs) 417 | stream.writeInt32(self._fields.callConnectTimeoutMs) 418 | stream.writeInt32(self._fields.callPacketTimeoutMs) 419 | stream.writeInt32(self._fields.webFileDcId) 420 | stream.writeQString(self._fields.txtDomainString) 421 | # stream << self._fields.txtDomainString 422 | stream.writeInt32(1 if self._fields.phoneCallsEnabled else 0) 423 | stream.writeInt32(1 if self._fields.blockedMode else 0) 424 | stream.writeInt32(self._fields.captionLengthMax) 425 | 426 | return result 427 | 428 | @staticmethod 429 | def FromSerialized(serialized: QByteArray) -> MTP.Config: 430 | 431 | stream = QDataStream(serialized) 432 | stream.setVersion(QDataStream.Version.Qt_5_1) 433 | 434 | version = stream.readInt32() 435 | Expects( 436 | version == MTP.Config.kVersion, 437 | "version != kVersion, something went wrong", 438 | ) 439 | 440 | enviroment = MTP.Environment(stream.readInt32()) 441 | result = MTP.Config(enviroment) 442 | 443 | def read(field: _T) -> _T: 444 | vtype = type(field) 445 | if vtype == int: 446 | return stream.readInt32() # type: ignore 447 | elif vtype == bool: 448 | return stream.readInt32() == 1 # type: ignore 449 | elif vtype == str: 450 | return stream.readQString() # type: ignore 451 | 452 | raise ValueError() 453 | 454 | dcOptionsSerialized = QByteArray() 455 | stream >> dcOptionsSerialized 456 | 457 | fileds = result._fields 458 | fileds.chatSizeMax = read(fileds.chatSizeMax) 459 | fileds.megagroupSizeMax = read(fileds.megagroupSizeMax) 460 | fileds.forwardedCountMax = read(fileds.forwardedCountMax) 461 | fileds.onlineUpdatePeriod = read(fileds.onlineUpdatePeriod) 462 | fileds.offlineBlurTimeout = read(fileds.offlineBlurTimeout) 463 | fileds.offlineIdleTimeout = read(fileds.offlineIdleTimeout) 464 | fileds.onlineFocusTimeout = read(fileds.onlineFocusTimeout) 465 | fileds.onlineCloudTimeout = read(fileds.onlineCloudTimeout) 466 | fileds.notifyCloudDelay = read(fileds.notifyCloudDelay) 467 | fileds.notifyDefaultDelay = read(fileds.notifyDefaultDelay) 468 | fileds.savedGifsLimit = read(fileds.savedGifsLimit) 469 | fileds.editTimeLimit = read(fileds.editTimeLimit) 470 | fileds.revokeTimeLimit = read(fileds.revokeTimeLimit) 471 | fileds.revokePrivateTimeLimit = read(fileds.revokePrivateTimeLimit) 472 | fileds.revokePrivateInbox = read(fileds.revokePrivateInbox) 473 | fileds.stickersRecentLimit = read(fileds.stickersRecentLimit) 474 | fileds.stickersFavedLimit = read(fileds.stickersFavedLimit) 475 | fileds.pinnedDialogsCountMax = read(fileds.pinnedDialogsCountMax) 476 | fileds.pinnedDialogsInFolderMax = read(fileds.pinnedDialogsInFolderMax) 477 | fileds.internalLinksDomain = read(fileds.internalLinksDomain) 478 | fileds.channelsReadMediaPeriod = read(fileds.channelsReadMediaPeriod) 479 | fileds.callReceiveTimeoutMs = read(fileds.callReceiveTimeoutMs) 480 | fileds.callRingTimeoutMs = read(fileds.callRingTimeoutMs) 481 | fileds.callConnectTimeoutMs = read(fileds.callConnectTimeoutMs) 482 | fileds.callPacketTimeoutMs = read(fileds.callPacketTimeoutMs) 483 | fileds.webFileDcId = read(fileds.webFileDcId) 484 | fileds.txtDomainString = read(fileds.txtDomainString) 485 | fileds.phoneCallsEnabled = read(fileds.phoneCallsEnabled) 486 | fileds.blockedMode = read(fileds.blockedMode) 487 | fileds.captionLengthMax = read(fileds.captionLengthMax) 488 | 489 | # print(fileds.chatSizeMax) 490 | # print(fileds.megagroupSizeMax) 491 | # print(fileds.forwardedCountMax) 492 | # print(fileds.onlineUpdatePeriod) 493 | # print(fileds.offlineBlurTimeout) 494 | # print(fileds.offlineIdleTimeout) 495 | # print(fileds.onlineFocusTimeout) 496 | # print(fileds.onlineCloudTimeout) 497 | # print(fileds.notifyCloudDelay) 498 | # print(fileds.notifyDefaultDelay) 499 | # print(fileds.savedGifsLimit) 500 | # print(fileds.editTimeLimit) 501 | # print(fileds.revokeTimeLimit) 502 | # print(fileds.revokePrivateTimeLimit) 503 | # print(fileds.revokePrivateInbox) 504 | # print(fileds.stickersRecentLimit) 505 | # print(fileds.stickersFavedLimit) 506 | # print(fileds.pinnedDialogsCountMax) 507 | # print(fileds.pinnedDialogsInFolderMax) 508 | # print(fileds.internalLinksDomain) 509 | # print(fileds.channelsReadMediaPeriod) 510 | # print(fileds.callReceiveTimeoutMs) 511 | # print(fileds.callRingTimeoutMs) 512 | # print(fileds.callConnectTimeoutMs) 513 | # print(fileds.callPacketTimeoutMs) 514 | # print(fileds.webFileDcId) 515 | # print(fileds.txtDomainString) 516 | # print(fileds.phoneCallsEnabled) 517 | # print(fileds.blockedMode) 518 | # print(fileds.captionLengthMax) 519 | 520 | ExpectStreamStatus(stream, "Could not stream MtpData serialized") 521 | result._dcOptions.constructFromSerialized(dcOptionsSerialized) 522 | return result 523 | -------------------------------------------------------------------------------- /src/td/shared.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | 4 | from .account import Account, StorageAccount, MapData 5 | from .auth import AuthKey, AuthKeyType 6 | from .mtp import MTP 7 | from .storage import Storage, Serialize 8 | from .tdesktop import TDesktop 9 | from . import configs 10 | 11 | from .. import exception as excpt 12 | 13 | from ..api import APIData, API 14 | 15 | from typing import Optional 16 | 17 | import struct 18 | import os 19 | -------------------------------------------------------------------------------- /src/td/storage.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from .configs import * 3 | from . import shared as td 4 | 5 | import hashlib 6 | import os 7 | import tgcrypto 8 | 9 | # if TYPE_CHECKING: 10 | # from ..opentele import * 11 | 12 | 13 | class Serialize(BaseObject): 14 | @staticmethod 15 | def bytearraySize(arr: QByteArray): 16 | return sizeof(uint32) + arr.size() 17 | 18 | @staticmethod 19 | def bytesSize(arr: bytes): 20 | return sizeof(uint32) + len(arr) 21 | 22 | @staticmethod 23 | def stringSize(arr: str): 24 | return sizeof(uint32) + len(arr) + sizeof(ushort) 25 | 26 | 27 | class Storage(BaseObject): 28 | class ReadSettingsContext(BaseObject): # pragma: no cover 29 | def __init__(self) -> None: 30 | 31 | self.fallbackConfigLegacyDcOptions: td.MTP.DcOptions = td.MTP.DcOptions( 32 | td.MTP.Environment.Production 33 | ) 34 | self.fallbackConfigLegacyChatSizeMax = 0 35 | self.fallbackConfigLegacySavedGifsLimit = 0 36 | self.fallbackConfigLegacyStickersRecentLimit = 0 37 | self.fallbackConfigLegacyStickersFavedLimit = 0 38 | self.fallbackConfigLegacyMegagroupSizeMax = 0 39 | self.fallbackConfigLegacyTxtDomainString = str() 40 | self.fallbackConfig = QByteArray() 41 | 42 | self.cacheTotalSizeLimit = 0 43 | self.cacheTotalTimeLimit = 0 44 | self.cacheBigFileTotalSizeLimit = 0 45 | self.cacheBigFileTotalTimeLimit = 0 46 | 47 | self.themeKeyLegacy = FileKey(0) 48 | self.themeKeyDay = FileKey(0) 49 | self.themeKeyNight = FileKey(0) 50 | self.backgroundKeyDay = FileKey(0) 51 | self.backgroundKeyNight = FileKey(0) 52 | 53 | self.backgroundKeysRead = False 54 | self.tileDay = False 55 | self.tileNight = True 56 | self.tileRead = False 57 | 58 | self.langPackKey = FileKey(0) 59 | self.languagesKey = FileKey(0) 60 | 61 | self.mtpAuthorization = QByteArray() 62 | self.mtpLegacyKeys: typing.List[td.AuthKey] = [] 63 | 64 | self.mtpLegacyMainDcId = 0 65 | self.mtpLegacyUserId = 0 66 | self.legacyRead = False 67 | 68 | class FileReadDescriptor(BaseObject): 69 | def __init__(self) -> None: 70 | self.__version = 0 71 | self.__data = QByteArray() 72 | self.__buffer = QBuffer() 73 | self.__stream = QDataStream() 74 | 75 | @property 76 | def data(self) -> QByteArray: 77 | return self.__data 78 | 79 | @data.setter 80 | def data(self, value): 81 | self.__data = value 82 | 83 | @property 84 | def version(self) -> int: 85 | return self.__version 86 | 87 | @version.setter 88 | def version(self, value): 89 | self.__version = value 90 | 91 | @property 92 | def buffer(self) -> QBuffer: 93 | return self.__buffer 94 | 95 | @property 96 | def stream(self) -> QDataStream: 97 | return self.__stream 98 | 99 | class EncryptedDescriptor(BaseObject): 100 | def __init__(self, size: int = None) -> None: 101 | self.__data = QByteArray() 102 | self.__buffer = QBuffer() 103 | self.__stream = QDataStream() 104 | 105 | if size: 106 | fullSize = 4 + size 107 | if fullSize & 0x0F: 108 | fullSize += 0x10 - (fullSize & 0x0F) 109 | 110 | self.__data.reserve(fullSize) 111 | self.__data.resize(4) 112 | self.__buffer.setBuffer(self.__data) 113 | self.__buffer.open(QIODevice.OpenModeFlag.WriteOnly) 114 | self.__buffer.seek(4) 115 | self.__stream.setDevice(self.__buffer) 116 | self.__stream.setVersion(QDataStream.Version.Qt_5_1) 117 | 118 | def finish(self) -> None: 119 | if self.__stream.device(): 120 | self.__stream.setDevice(None) # type: ignore 121 | if self.__buffer.isOpen(): 122 | self.__buffer.close() 123 | self.__buffer.setBuffer(None) # type: ignore 124 | 125 | @property 126 | def data(self) -> QByteArray: 127 | return self.__data 128 | 129 | @data.setter 130 | def data(self, value): 131 | self.__data = value 132 | 133 | @property 134 | def buffer(self) -> QBuffer: 135 | return self.__buffer 136 | 137 | @property 138 | def stream(self) -> QDataStream: 139 | return self.__stream 140 | 141 | class FileWriteDescriptor(BaseObject): 142 | def __init__(self, fileName: str, basePath: str, sync: bool = False) -> None: 143 | self.fileName = fileName 144 | self.basePath = basePath 145 | self.sync = sync 146 | self.init(fileName) 147 | 148 | def init(self, fileName: str): 149 | self.base = Storage.PathJoin(self.basePath, fileName) 150 | self.safeData = QByteArray() 151 | self.buffer = QBuffer() 152 | self.buffer.setBuffer(self.safeData) 153 | 154 | result = self.buffer.open(QIODevice.OpenModeFlag.WriteOnly) 155 | Expects(result, "Can not open buffer, something went wrong") 156 | 157 | self.stream = QDataStream() 158 | self.stream.setDevice(self.buffer) 159 | 160 | self.md5 = bytes() 161 | self.fullSize = 0 162 | 163 | def writeData(self, data: QByteArray): 164 | Expects(self.stream.device() != None, "stream.device is missing") 165 | 166 | self.stream << data 167 | len = 0xFFFFFFFF if data.isNull() else data.size() 168 | 169 | if QSysInfo.Endian.ByteOrder != QSysInfo.Endian.BigEndian: 170 | 171 | def qbswap(source: int) -> int: 172 | return ( 173 | 0 174 | | ((source & 0x000000FF) << 24) 175 | | ((source & 0x0000FF00) << 8) 176 | | ((source & 0x00FF0000) >> 8) 177 | | ((source & 0xFF000000) >> 24) 178 | ) 179 | 180 | len = qbswap(len) 181 | 182 | self.md5 += len.to_bytes(4, "little") 183 | self.md5 += data.data() 184 | self.fullSize += 4 + data.size() 185 | 186 | def writeEncrypted(self, data: Storage.EncryptedDescriptor, key: td.AuthKey): 187 | self.writeData(Storage.PrepareEncrypted(data, key)) 188 | 189 | def finish(self): 190 | if not self.stream.device(): 191 | return 192 | self.stream.setDevice(None) # type: ignore 193 | 194 | self.md5 += self.fullSize.to_bytes(4, "little") 195 | self.md5 += APP_VERSION.to_bytes(4, "little") 196 | self.md5 += TDF_MAGIC 197 | self.md5 = hashlib.md5(self.md5).digest() 198 | 199 | self.buffer.close() 200 | 201 | finalData: bytes = self.safeData.data() + self.md5 202 | Storage.WriteFile(self.fileName, self.basePath, finalData) 203 | 204 | @staticmethod 205 | def PrepareEncrypted(data: EncryptedDescriptor, key: td.AuthKey) -> QByteArray: 206 | data.finish() 207 | 208 | toEncrypt = QByteArray(data.data) 209 | 210 | # prepare for encryption 211 | size = toEncrypt.size() 212 | fullSize = size 213 | if fullSize & 0x0F: 214 | fullSize += 0x10 - (fullSize & 0x0F) 215 | toEncrypt.resize(fullSize) 216 | # TO BE ADDED 217 | # base::RandomFill(toEncrypt.data() + size, fullSize - size); 218 | 219 | # *(uint32*)toEncrypt.data() = size; 220 | toEncrypt = QByteArray(size.to_bytes(4, "little")) + toEncrypt[4:] 221 | 222 | hashData = hashlib.sha1(toEncrypt) 223 | encrypted = QByteArray(hashData.digest()) 224 | 225 | encrypted.resize(0x10 + fullSize) # 128bit of sha1 - key128, sizeof(data), data 226 | 227 | encrypted = encrypted[:0x10] + Storage.aesEncryptLocal( 228 | toEncrypt, key, encrypted 229 | ) 230 | 231 | return encrypted 232 | 233 | @staticmethod 234 | def WriteFile(fileName: str, basePath: str, data: bytes): 235 | 236 | dir = QDir(basePath) 237 | if not dir.exists(): 238 | dir.mkpath(basePath) 239 | 240 | file = QFile(Storage.PathJoin(basePath, fileName + "s")) 241 | if file.open(QIODevice.OpenModeFlag.WriteOnly): 242 | file.write(TDF_MAGIC) 243 | file.write(APP_VERSION.to_bytes(4, "little")) 244 | file.write(data) 245 | file.close() 246 | return True 247 | 248 | raise OpenTeleException("what") 249 | 250 | @staticmethod 251 | def ReadFile(fileName: str, basePath: str) -> FileReadDescriptor: 252 | 253 | to_try = ["s", "1", "0"] 254 | tries_exception = None 255 | for chr in to_try: 256 | try: 257 | 258 | file = QFile(Storage.PathJoin(basePath, fileName + chr)) 259 | if not file.open(QIODevice.OpenModeFlag.ReadOnly): 260 | tries_exception = TFileNotFound(f"Could not open {fileName}") 261 | continue 262 | 263 | magic = file.read(4) 264 | 265 | if magic != TDF_MAGIC: 266 | tries_exception = TDataInvalidMagic( 267 | "Invalid magic {magic} in file {fileName}" 268 | ) 269 | file.close() 270 | continue 271 | 272 | version = int.from_bytes(file.read(4), "little") 273 | 274 | bytesdata = QByteArray(file.read(file.size())) 275 | 276 | dataSize = bytesdata.size() - 16 277 | 278 | check_md5 = bytesdata.data()[:dataSize] 279 | check_md5 += int(dataSize).to_bytes(4, "little") 280 | check_md5 += int(version).to_bytes(4, "little") 281 | check_md5 += magic 282 | check_md5 = hashlib.md5(check_md5) 283 | 284 | md5 = bytesdata.data()[dataSize:] 285 | 286 | if check_md5.hexdigest() != md5.hex(): 287 | tries_exception = TDataInvalidCheckSum( 288 | "Invalid checksum {check_md5.hexdigest()} in file {fileName}" 289 | ) 290 | file.close() 291 | continue 292 | 293 | result = Storage.FileReadDescriptor() 294 | result.version = version 295 | 296 | bytesdata.resize(dataSize) 297 | result.data = bytesdata 298 | bytesdata = QByteArray() 299 | 300 | result.buffer.setBuffer(result.data) 301 | 302 | result.buffer.open(QIODevice.OpenModeFlag.ReadOnly) 303 | result.stream.setDevice(result.buffer) 304 | result.stream.setVersion(QDataStream.Version.Qt_5_1) 305 | 306 | file.close() 307 | return result 308 | except IOError as e: 309 | pass 310 | 311 | raise tries_exception if tries_exception else TFileNotFound( 312 | f"Could not open {fileName}" 313 | ) 314 | 315 | @staticmethod 316 | def ReadEncryptedFile( 317 | fileName: str, basePath: str, authKey: td.AuthKey 318 | ) -> FileReadDescriptor: 319 | 320 | result = Storage.ReadFile(fileName, basePath) 321 | encrypted = QByteArray() 322 | result.stream >> encrypted 323 | 324 | try: 325 | data = Storage.DecryptLocal(encrypted, authKey) 326 | except OpenTeleException as e: 327 | result.stream.setDevice(None) # type: ignore 328 | if result.buffer.isOpen(): 329 | result.buffer.close() 330 | result.buffer.setBuffer(None) # type: ignore 331 | result.data = QByteArray() 332 | result.version = 0 333 | raise e 334 | 335 | result.stream.setDevice(None) # type: ignore 336 | if result.buffer.isOpen(): 337 | result.buffer.close() 338 | result.buffer.setBuffer(None) # type: ignore 339 | result.data = data.data 340 | 341 | result.buffer.setBuffer(result.data) 342 | result.buffer.open(QIODevice.OpenModeFlag.ReadOnly) 343 | result.buffer.seek(data.buffer.pos()) 344 | result.stream.setDevice(result.buffer) 345 | result.stream.setVersion(QDataStream.Version.Qt_5_1) 346 | 347 | return result 348 | 349 | @staticmethod 350 | def ReadSetting( 351 | blockId: int, stream: QDataStream, version: int, context: ReadSettingsContext 352 | ) -> bool: # pragma: no cover 353 | 354 | if blockId == dbi.DcOptionOldOld: 355 | dcId = DcId(stream.readUInt32()) 356 | host = stream.readQString() 357 | ip = stream.readQString() 358 | port = stream.readUInt32() 359 | ExpectStreamStatus(stream) 360 | 361 | context.fallbackConfigLegacyDcOptions.constructAddOne( 362 | dcId, td.MTP.DcOptions.Flag(0), ip, port, bytes() 363 | ) 364 | context.legacyRead = True 365 | 366 | elif blockId == dbi.DcOptionOld: 367 | dcIdWithShift = ShiftedDcId(stream.readUInt32()) 368 | flags = td.MTP.DcOptions.Flag(stream.readInt32()) 369 | ip = stream.readQString() 370 | port = stream.readUInt32() 371 | 372 | ExpectStreamStatus(stream) 373 | 374 | context.fallbackConfigLegacyDcOptions.constructAddOne( 375 | dcIdWithShift, flags, ip, port, bytes() 376 | ) 377 | context.legacyRead = True 378 | 379 | elif blockId == dbi.DcOptionsOld: 380 | serialized = QByteArray() 381 | stream >> serialized 382 | ExpectStreamStatus(stream) 383 | 384 | context.fallbackConfigLegacyDcOptions.constructFromSerialized(serialized) 385 | context.legacyRead = True 386 | 387 | elif blockId == dbi.ApplicationSettings: 388 | serialized = QByteArray() 389 | stream >> serialized 390 | ExpectStreamStatus(stream) 391 | 392 | # TO BE ADDED 393 | 394 | elif blockId == dbi.ChatSizeMaxOld: 395 | maxSize = stream.readInt32() 396 | ExpectStreamStatus(stream) 397 | 398 | context.fallbackConfigLegacyChatSizeMax = maxSize 399 | context.legacyRead = True 400 | 401 | elif blockId == dbi.SavedGifsLimitOld: 402 | limit = stream.readInt32() 403 | ExpectStreamStatus(stream) 404 | 405 | context.fallbackConfigLegacySavedGifsLimit = limit 406 | context.legacyRead = True 407 | 408 | elif blockId == dbi.StickersRecentLimitOld: 409 | limit = stream.readInt32() 410 | ExpectStreamStatus(stream) 411 | 412 | context.fallbackConfigLegacyStickersRecentLimit = limit 413 | context.legacyRead = True 414 | 415 | elif blockId == dbi.StickersFavedLimitOld: 416 | limit = stream.readInt32() 417 | ExpectStreamStatus(stream) 418 | 419 | context.fallbackConfigLegacyStickersFavedLimit = limit 420 | context.legacyRead = True 421 | 422 | elif blockId == dbi.MegagroupSizeMaxOld: 423 | maxSize = stream.readInt32() 424 | ExpectStreamStatus(stream) 425 | 426 | context.fallbackConfigLegacyMegagroupSizeMax = maxSize 427 | context.legacyRead = True 428 | 429 | elif blockId == dbi.User: 430 | userId = stream.readInt32() 431 | dcId = stream.readUInt32() 432 | ExpectStreamStatus(stream) 433 | 434 | context.mtpLegacyMainDcId = dcId 435 | context.mtpLegacyUserId = userId 436 | 437 | elif blockId == dbi.Key: 438 | dcId = DcId(stream.readInt32()) 439 | key = stream.readRawData(256) 440 | ExpectStreamStatus(stream) 441 | 442 | context.mtpLegacyKeys.append( 443 | td.AuthKey(key, td.AuthKeyType.ReadFromFile, dcId) 444 | ) 445 | 446 | elif blockId == dbi.MtpAuthorization: 447 | serialized = QByteArray() 448 | stream >> serialized 449 | ExpectStreamStatus(stream) 450 | 451 | context.mtpAuthorization = serialized 452 | 453 | return True 454 | 455 | @staticmethod 456 | def CreateLocalKey( 457 | salt: QByteArray, passcode: QByteArray = QByteArray() 458 | ) -> td.AuthKey: 459 | hashKey = hashlib.sha512(salt) 460 | hashKey.update(passcode) 461 | hashKey.update(salt) 462 | 463 | iterationsCount = 1 if passcode.isEmpty() else 100000 464 | return td.AuthKey( 465 | hashlib.pbkdf2_hmac("sha512", hashKey.digest(), salt, iterationsCount, 256) 466 | ) 467 | 468 | @staticmethod 469 | def CreateLegacyLocalKey( 470 | salt: QByteArray, passcode: QByteArray = QByteArray() 471 | ) -> td.AuthKey: 472 | 473 | iterationsCount = 1 if passcode.isEmpty() else 100000 474 | return td.AuthKey( 475 | hashlib.pbkdf2_hmac( 476 | "sha512", passcode.data(), salt.data(), iterationsCount, 256 477 | ) 478 | ) 479 | 480 | @staticmethod 481 | def aesEncryptLocal( 482 | src: QByteArray, authKey: td.AuthKey, key128: QByteArray 483 | ) -> QByteArray: 484 | aesKey, aesIv = authKey.prepareAES_oldmtp(key128, False) 485 | encrypted = tgcrypto.ige256_encrypt(src, aesKey, aesIv) 486 | return QByteArray(encrypted) 487 | 488 | @staticmethod 489 | def aesDecryptLocal( 490 | src: QByteArray, authKey: td.AuthKey, key128: QByteArray 491 | ) -> QByteArray: 492 | aesKey, aesIv = authKey.prepareAES_oldmtp(key128, False) 493 | decrypted = tgcrypto.ige256_decrypt(src, aesKey, aesIv) 494 | return QByteArray(decrypted) 495 | 496 | @staticmethod 497 | def DecryptLocal(encrypted: QByteArray, authKey: td.AuthKey) -> EncryptedDescriptor: 498 | 499 | encryptedSize = encrypted.size() 500 | if (encryptedSize <= 16) or (encryptedSize & 0x0F): 501 | raise TDataBadEncryptedDataSize("Bad encrypted part size: {encryptedSize}") 502 | 503 | fullLen = encryptedSize - 16 504 | encryptedKey = encrypted[:16] 505 | encryptedData = encrypted[16:] 506 | 507 | decrypted = Storage.aesDecryptLocal(encryptedData, authKey, encryptedKey) 508 | checkHash = hashlib.sha1(decrypted).digest()[:16] 509 | if checkHash != encryptedKey: 510 | raise TDataBadDecryptKey( 511 | "Bad decrypt key, data not decrypted - incorrect password?" 512 | ) 513 | 514 | dataLen = int.from_bytes( 515 | decrypted[:4], "little" 516 | ) # *(const uint32*)decrypted.constData(); 517 | if (dataLen > decrypted.size()) or (dataLen <= fullLen - 16) or (dataLen < 4): 518 | raise TDataBadDecryptedDataSize( 519 | "Bad decrypted part size: {encryptedSize}, fullLen: {fullLen}, decrypted size: {decrypted.__len__()}" 520 | ) 521 | 522 | decrypted.resize(dataLen) 523 | result = Storage.EncryptedDescriptor() 524 | result.data = decrypted 525 | 526 | decrypted = QByteArray() 527 | 528 | result.buffer.setBuffer(result.data) 529 | result.buffer.open(QIODevice.OpenModeFlag.ReadOnly) 530 | result.buffer.seek(4) # skip len 531 | result.stream.setDevice(result.buffer) 532 | result.stream.setVersion(QDataStream.Version.Qt_5_1) 533 | 534 | return result 535 | 536 | @staticmethod 537 | def ComposeDataString(dataName: str, index: int): 538 | result = dataName 539 | result.replace("#", "") 540 | if index > 0: 541 | result += f"#{index + 1}" 542 | return result 543 | 544 | @staticmethod 545 | def ComputeDataNameKey(dataName: str) -> int: 546 | md5 = hashlib.md5(dataName.encode("utf-8")).digest() 547 | return int.from_bytes(md5, "little") 548 | 549 | @staticmethod 550 | def ToFilePart(val: int): 551 | result = str() 552 | for i in range(0, 0x10): 553 | v = val & 0xF 554 | if v < 0x0A: 555 | result += chr(ord("0") + v) 556 | else: 557 | result += chr(ord("A") + (v - 0x0A)) 558 | val >>= 4 559 | return result 560 | 561 | @staticmethod 562 | def RandomGenerate(size: int) -> QByteArray: 563 | return QByteArray(os.urandom(size)) 564 | 565 | @staticmethod 566 | def GetAbsolutePath(path: str = None) -> str: 567 | 568 | if path == None or path == "": 569 | path = os.getcwd() 570 | 571 | path = os.path.abspath(path) 572 | return path 573 | 574 | @staticmethod 575 | def PathJoin(path: str, filename: str) -> str: 576 | return os.path.join(path, filename) 577 | -------------------------------------------------------------------------------- /src/td/tdesktop.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | from .configs import * 5 | from . import shared as td 6 | 7 | import logging 8 | from telethon.network.connection.connection import Connection 9 | from telethon.network.connection.tcpfull import ConnectionTcpFull 10 | from telethon.sessions.abstract import Session 11 | 12 | # if TYPE_CHECKING: 13 | # from . import * 14 | 15 | 16 | class TDesktop(BaseObject): 17 | """ 18 | Telegram Desktop client. 19 | 20 | A client can have multiple accounts, up to 3 - according to official Telegram Desktop client. 21 | 22 | ### Attributes: 23 | api (`API`): 24 | The API this client is using. 25 | 26 | accountsCount (`int`): 27 | The numbers of accounts in this client. 28 | 29 | accounts (`List[Account]`): 30 | List of accounts in this client. 31 | 32 | mainAccount (`Account`): 33 | The main account of this client. 34 | 35 | basePath (`str`): 36 | The path to `tdata folder`. 37 | 38 | passcode (`str`): 39 | Passcode of the client, the same as Local Passcode on `Telegram Desktop`.\\ 40 | Use to encrypt and decrypt `tdata files`. 41 | 42 | AppVersion (`int`): 43 | App version of the client. 44 | 45 | kMaxAccounts (`int`): 46 | See `kMaxAccounts`. 47 | 48 | keyFile (`str`): 49 | See `keyFile`. 50 | 51 | kDefaultKeyFile (`str`): 52 | Default value for `keyFile`. 53 | 54 | kPerformanceMode (`bool`): 55 | Performance mode. When enabled, `SavaTData()` will be 200x faster. 56 | See `kPerformanceMode`. 57 | 58 | ### Methods: 59 | LoadTData(): 60 | Load the client from `tdata folder`. \\ 61 | Use this if you didn't set the `basePath` when initializing the client. 62 | 63 | SaveTData(): 64 | Save the client session to `tdata folder` - which can be used by `Telegram Desktop`. 65 | 66 | isLoaded(): 67 | Return `True` if the client has successfully loaded accounts from `tdata` or `TelegramClient`. 68 | 69 | ToTelethon(): 70 | Convert this session to `TelegramClient`. 71 | 72 | FromTelethon(): 73 | Create a new session from `TelegramClient`. 74 | 75 | PerformanceMode(): 76 | Enable/disable performance mode. When enabled, `SavaTData()` will be 5000x faster. 77 | 78 | """ 79 | 80 | @typing.overload 81 | def __init__(self) -> None: 82 | pass 83 | 84 | @typing.overload 85 | def __init__( 86 | self, 87 | basePath: str = None, 88 | api: Union[Type[APIData], APIData] = API.TelegramDesktop, 89 | ) -> None: 90 | pass 91 | 92 | @typing.overload 93 | def __init__( 94 | self, 95 | basePath: str = None, 96 | api: Union[Type[APIData], APIData] = API.TelegramDesktop, 97 | passcode: str = None, 98 | keyFile: str = None, 99 | ) -> None: 100 | pass 101 | 102 | def __init__( 103 | self, 104 | basePath: str = None, 105 | api: Union[Type[APIData], APIData] = API.TelegramDesktop, 106 | passcode: str = None, 107 | keyFile: str = None, 108 | ) -> None: 109 | """ 110 | Initialize a `TDesktop` client 111 | 112 | ### Arguments: 113 | basePath (`str`, default=`None`): 114 | The path to the `tdata folder`. 115 | If the path doesn't exists or its data is corrupted, a new instance will be created. 116 | 117 | api (`API`, default=`TelegramDesktop`): 118 | Which API to use. Read more `[here](API)`. 119 | 120 | passcode (str, default=`None`): 121 | The passcode for tdata, same as the Local Passcode on `Telegram Desktop`. 122 | 123 | keyFile (str, default="data"): 124 | See `keyFile`. 125 | """ 126 | self.__accounts: typing.List[td.Account] = [] 127 | self.__basePath = basePath 128 | self.__keyFile = keyFile if (keyFile != None) else TDesktop.kDefaultKeyFile 129 | self.__passcode = passcode if (passcode != None) else str("") 130 | self.__passcodeBytes = self.__passcode.encode("ascii") 131 | self.__mainAccount: Optional[td.Account] = None 132 | self.__active_index = -1 133 | self.__passcodeKey = None 134 | self.__localKey = None 135 | self.__AppVersion = None 136 | self.__isLoaded = False 137 | self.__api = api.copy() 138 | 139 | if basePath != None: 140 | self.__basePath = td.Storage.GetAbsolutePath(basePath) 141 | self.LoadTData() 142 | 143 | def isLoaded(self) -> bool: 144 | """ 145 | Return `True` if the client has successfully loaded accounts from `tdata` or `TelegramClient` 146 | """ 147 | return self.__isLoaded 148 | 149 | def LoadTData( 150 | self, basePath: str = None, passcode: str = None, keyFile: str = None 151 | ): 152 | """ 153 | Loads accounts from `tdata folder` 154 | 155 | ### Arguments: 156 | basePath (`str`, default=`None`): 157 | The path to the folder. 158 | 159 | passcode (`str`, default=`None`): 160 | Read more `[here](passcode)`. 161 | 162 | keyFile (`str`, default=`None`): 163 | Read more `[here](keyFile)`. 164 | 165 | ### Raises: 166 | `TDataBadDecryptKey`: The `tdata folder` is password-encrypted, please the set the argument `passcode` to decrypt it. 167 | 168 | ### Warning: 169 | This function is not recommended to use: 170 | You should load tdata using `TDesktop(basePath="path")`. 171 | Don't manually load tdata using this function, bugs might pop up out of nowhere. 172 | 173 | ### Examples: 174 | ```python 175 | # Using the API that we've generated before. Please refer to method API.Generate() to learn more. 176 | oldAPI = API.TelegramDesktop.Generate(system="windows", unique_id="old.session") 177 | oldclient = TelegramClient("old.session", api=oldAPI) 178 | await oldClient.connect() 179 | 180 | # We can safely use CreateNewSession with a different API. 181 | # Be aware that you should not use UseCurrentSession with a different API than the one that first authorized it. 182 | newAPI = API.TelegramAndroid.Generate("new_tdata") 183 | tdesk = await TDesktop.FromTelethon(oldclient, flag=CreateNewSession, api=newAPI) 184 | 185 | # Save the new session to a folder named "new_tdata" 186 | tdesk.SaveTData("new_tdata") 187 | ``` 188 | """ 189 | 190 | if basePath == None: 191 | basePath = self.basePath 192 | 193 | Expects(basePath != None and basePath != "", "No folder provided to load tdata") 194 | 195 | if keyFile != None and self.__keyFile != keyFile: 196 | self.__keyFile = keyFile 197 | 198 | if passcode != None and self.__passcode != passcode: 199 | self.__passcode = passcode 200 | self.__passcodeBytes = passcode.encode("ascii") 201 | 202 | try: 203 | self.__loadFromTData() 204 | 205 | except OpenTeleException as e: 206 | if isinstance(e, TDataBadDecryptKey): 207 | if self.passcode == "": 208 | raise TDataBadDecryptKey( 209 | "The tdata folder is password-encrypted, please the set the argument 'passcode' to decrypt it" 210 | ) 211 | else: 212 | raise TDataBadDecryptKey( 213 | "Failed to decrypt tdata folder because of invalid passcode" 214 | ) 215 | else: 216 | raise e 217 | 218 | Expects(self.isLoaded(), "Failed to load? Something went seriously wrong") 219 | 220 | def SaveTData( 221 | self, basePath: str = None, passcode: str = None, keyFile: str = None 222 | ) -> bool: 223 | """ 224 | Save the client session to a folder. 225 | 226 | ### Arguments: 227 | basePath (`str`, default=None): 228 | Path to the folder\\ 229 | If None then the data will be saved at the basePath given at creation. 230 | 231 | passcode (`str`, default=`None`): 232 | Read more `[here](passcode)`. 233 | 234 | keyFile (`str`, default=`None`): 235 | Read more `[here](keyFile)`. 236 | 237 | ### Examples: 238 | Save a telethon session to tdata: 239 | ```python 240 | # Using the API that we've generated before. Please refer to method API.Generate() to learn more. 241 | oldAPI = API.TelegramDesktop.Generate(system="windows", unique_id="old.session") 242 | oldclient = TelegramClient("old.session", api=oldAPI) 243 | await oldClient.connect() 244 | 245 | # We can safely CreateNewSession with a different API. 246 | # Be aware that you should not use UseCurrentSession with a different API than the one that first authorized it. 247 | newAPI = API.TelegramAndroid.Generate("new_tdata") 248 | tdesk = await TDesktop.FromTelethon(oldclient, flag=CreateNewSession, api=newAPI) 249 | 250 | # Save the new session to a folder named "new_tdata" 251 | tdesk.SaveTData("new_tdata") 252 | ``` 253 | """ 254 | if basePath == None: 255 | basePath = self.basePath 256 | 257 | self.__keyFile = ( 258 | keyFile if (keyFile != None and self.keyFile != keyFile) else self.keyFile 259 | ) 260 | 261 | Expects(basePath != None and basePath != "", "No folder provided to save tdata") 262 | 263 | if passcode != None and self.__passcode != passcode: 264 | self.__passcode = passcode 265 | self.__passcodeBytes = passcode.encode("ascii") 266 | self.__isLoaded = False # to generate new localKey 267 | 268 | if not self.isLoaded(): 269 | self.__generateLocalKey() 270 | Expects(self.isLoaded(), "Failed to load? Something went seriously wrong") 271 | 272 | try: 273 | 274 | basePath = td.Storage.GetAbsolutePath(basePath) 275 | if not self.basePath: 276 | self.__basePath = basePath 277 | 278 | self.__writeAccounts(basePath, keyFile) 279 | return True 280 | 281 | except OpenTeleException as e: 282 | raise TDataSaveFailed("Could not save tdata, something went wrong") from e 283 | 284 | def __writeAccounts(self, basePath: str, keyFile: str = None) -> None: 285 | # Intended for internal usage only 286 | 287 | Expects(len(self.accounts) > 0) 288 | Expects(basePath != None and basePath != "", "No folder provided to save tdata") 289 | 290 | for account in self.accounts: 291 | account._writeData(basePath, keyFile) 292 | 293 | key = td.Storage.FileWriteDescriptor("key_" + self.keyFile, basePath) 294 | key.writeData(self.__passcodeKeySalt) 295 | key.writeData(self.__passcodeKeyEncrypted) 296 | 297 | keySize = sizeof(int32) + sizeof(int32) * len(self.accounts) 298 | keyData = td.Storage.EncryptedDescriptor(keySize) 299 | keyData.stream.writeInt32(len(self.accounts)) 300 | 301 | for account in self.accounts: 302 | keyData.stream.writeInt32(account.index) 303 | 304 | keyData.stream.writeInt32(self.__active_index) 305 | key.writeEncrypted(keyData, self.__localKey) # type: ignore 306 | key.finish() 307 | 308 | def __generateLocalKey(self) -> None: 309 | # Intended for internal usage only 310 | 311 | Expects(not self.isLoaded()) 312 | 313 | if self.kPerformanceMode and len(self.__passcodeBytes) == 0: 314 | 315 | self.__localKey = td.AuthKey( 316 | b"\xd8\x74\x59\x44\x51\x9e\x0d\x2d\x71\x30\x9d\x6c\x8d\x27\x2d\xc6\x49\x48\xf5\xe3\xeb\xa7\x68\x53\x24\xd5\xc6\x91\xad\x81\x0c\x20" 317 | b"\x3b\x31\xd1\x9d\x29\xae\xd6\xac\x33\xc0\x14\xbe\x6e\x09\x84\x32\x93\xf6\xfa\x32\xdb\xe4\x2b\x6a\x04\xe0\x04\x81\xfa\xe9\x95\x11" 318 | b"\x4c\xaf\x63\x42\xbd\x98\xe9\x6d\x29\x3d\xd0\x62\xc4\x58\x68\x9b\x3a\xbd\x23\xa5\xcf\x23\x0c\x75\x52\x7c\x05\xbf\x5f\x90\xf3\x8c" 319 | b"\xd9\x39\x52\xcf\x61\xaa\xac\x1c\xfe\xaa\xe4\x60\x85\x92\xe3\x63\xde\xd3\x5f\x8d\x8c\x45\x23\x4d\xef\x53\x23\x1d\xec\xb3\x55\x92" 320 | b"\xaf\xc4\x0d\x06\x01\xbb\xed\x11\x09\x09\x69\xf7\x4d\x9a\xb0\xcc\x97\x82\x75\x46\xf4\x41\x24\x2d\x2c\xfb\x8e\x05\xa0\x61\x0e\x97" 321 | b"\x66\x9c\x0d\xa1\xad\xcc\xb5\x6e\x39\xe1\x0c\x69\xe2\x94\x23\x87\xff\x49\x22\xf8\xc5\x5d\xcb\x88\x90\xe3\x45\xef\x31\x82\x66\xf4" 322 | b"\xb3\x83\x14\x30\xea\x21\x0c\x86\x3c\x17\x62\x4c\x04\x94\xcd\xea\xd8\x1f\x52\x34\x30\xb5\xf7\x4c\x15\xda\x32\x3d\x76\x6b\xd0\x1c" 323 | b"\xb5\xb8\x8b\x9d\x2a\x73\x1f\x6d\x85\x33\x80\xad\x30\x6a\x86\x47\xfa\x61\x4c\xc4\x01\x7f\x08\x90\x2c\x1e\x1f\x99\x7e\xe1\x2e\x3c" 324 | ) 325 | self.__passcodeKeySalt = QByteArray( 326 | b"\xae\xd1\xe0\x82\x99\x42\x81\xd9\x75\x76\x0e\x72\x95\x60\xd2\xc8\xd0\x08\xf2\xa9\xdd\x3f\xf4\xd8\x32\x45\xe2\x2e\xed\xb6\x67\x16" 327 | ) 328 | self.__passcodeKey = td.AuthKey( 329 | b"\x27\x3e\x64\x83\xb3\x13\xc9\xdb\xc4\xac\xd9\x17\x38\x64\xc0\x42\x2f\x17\x28\x81\xbf\xe1\xc6\x64\x9c\xa5\x53\x86\x54\xd0\xbd\x6e" 330 | b"\xbc\xfb\xcc\x8d\xab\xbb\x66\x97\x17\xce\x53\xb4\x1c\xa8\xaf\xbf\x9d\x15\xc2\x3f\xa2\xb0\x6a\x6e\x16\x6c\xc6\x1f\x62\xe9\x98\x05" 331 | b"\x58\xcd\x42\xcf\x10\x22\xe0\x5b\x46\x68\x11\xea\x29\xe9\x35\x9f\xc1\xf9\x46\x68\xe5\xc8\x51\x55\xbc\xe6\x38\xfe\x7a\xa4\x2a\xa8" 332 | b"\x80\xca\x83\xe0\xd2\xe5\x34\x19\x8a\x0a\x89\x01\x39\xc8\xf6\x79\xa5\x7d\x3b\xa0\x6d\x75\xfe\x5f\xaa\x6e\x25\x01\x01\xa5\x8b\xc6" 333 | b"\xe4\x2b\x96\x96\x60\x4d\xe1\xe3\x4a\xe4\x0f\x6b\xba\x14\x4a\x28\x4f\x3a\xd8\x84\x32\x53\xec\x9b\x39\x71\x86\x3a\x2c\x40\x92\x08" 334 | b"\xc2\x56\x39\x67\xb3\x58\x7e\x50\x9b\x42\xa4\x2a\x60\x40\xd2\x3f\xf6\x96\xad\x55\x2a\x24\x00\x84\xfa\x3f\x95\x02\x40\xf4\x99\xb2" 335 | b"\x3c\xd6\xd2\x7e\x70\x10\xcb\xde\x07\xda\xae\x06\x67\xa7\xdf\x8a\x51\x15\xa6\x0b\x26\x5c\x58\xf5\xd9\x29\x1f\x7a\x98\xfc\x3c\x60" 336 | b"\x1e\x2a\x4a\x32\xf1\x88\x1b\x82\x18\xc8\x55\x23\x9d\x7b\x53\x29\x59\x60\x9e\x6a\xb5\x2e\x48\xad\x69\x1c\x25\x83\xb5\x66\xc8\xf9" 337 | ) 338 | self.__passcodeKeyEncrypted = QByteArray( 339 | b"\x97\xdf\x0c\xd2\xe3\x10\x91\x49\xb7\x7b\x52\x87\x99\x4d\x9c\x1c\xa2\x40\xc5\x1e\x87\x48\x8e\x79" 340 | b"\xdd\x02\x9b\xea\x65\xfb\x9d\x27\x89\xbb\x5a\xbc\xfe\x65\xe8\x71\xd7\x52\xbd\x93\x8d\x83\x31\x3c" 341 | b"\x79\x4c\x89\x93\xa7\x34\xce\x12\x16\xf2\xe6\x60\x47\x3f\x31\x43\xaf\x9a\x33\x36\x10\xa1\x79\x95" 342 | b"\x87\x6e\x17\x21\xce\x1f\x61\x1d\x1c\x69\xd8\xc1\xa2\xf5\x9f\x94\x93\x11\x97\x04\x27\x4e\x2c\xb5" 343 | b"\xf3\x6c\x20\xdf\x43\x9d\x15\x6d\xef\xf7\xa3\x43\x71\xdc\x44\xbc\x86\xf8\x73\x0c\xeb\xf9\xb0\x28" 344 | b"\xeb\x7a\x1e\xd6\x62\x1d\x99\xad\xb6\x2b\x3b\x2c\xf2\x29\x5d\xbb\xb2\x4b\xf1\x32\xd3\x7f\xff\xc1" 345 | b"\x7a\x0b\xdc\xcc\x84\xbb\xea\x6e\xa3\x47\x37\xa2\x36\xb5\x82\x48\xa7\xab\x4c\x14\x36\x3c\x20\x54" 346 | b"\x1c\xb4\x53\x38\x67\x7f\x33\x97\x82\xb2\x05\xe3\x55\x18\x96\x58\xdd\x45\xea\x3e\x80\x05\xf8\x51" 347 | b"\x14\x8e\x7e\x15\xf4\x31\x90\x4f\xa7\x9c\x68\x27\xee\x42\x6d\x3a\xb9\xcb\xa9\x36\xeb\x33\xd4\x85" 348 | b"\xdb\x88\xa6\xf0\xff\x97\x22\xa6\xd6\x2f\xf7\x88\x34\x7e\x27\xc8\x2e\x9e\x13\x9e\xb0\x3a\xe5\x21" 349 | b"\x53\x9b\xf3\xd3\x63\xb4\xba\xea\x76\xe5\xe8\x84\xcf\x66\xfe\x6b\xcd\x8a\x9e\x08\x9d\x36\x40\x5d" 350 | b"\xb9\x9d\x01\xdb\x20\x46\x4f\xb6\xca\xbb\xdc\xe4\xf6\x7e\x4e\xc3\x74\x2f\x91\x3a\x1d\xd2\xda\xc5" 351 | ) 352 | 353 | else: 354 | LocalEncryptSaltSize = 32 355 | 356 | _pass = td.Storage.RandomGenerate(td.AuthKey.kSize) 357 | _salt = td.Storage.RandomGenerate(LocalEncryptSaltSize) 358 | self.__localKey = td.Storage.CreateLocalKey(_salt, _pass) 359 | 360 | self.__passcodeKeySalt = td.Storage.RandomGenerate( 361 | LocalEncryptSaltSize 362 | ) # LocalEncryptSaltSize = 32 363 | self.__passcodeKey = td.Storage.CreateLocalKey( 364 | self.__passcodeKeySalt, QByteArray(self.__passcodeBytes) 365 | ) 366 | 367 | passKeyData = td.Storage.EncryptedDescriptor(td.AuthKey.kSize) 368 | self.__localKey.write(passKeyData.stream) 369 | 370 | self.__passcodeKeyEncrypted = td.Storage.PrepareEncrypted( 371 | passKeyData, self.__passcodeKey 372 | ) 373 | 374 | # set new local key for self.accounts 375 | for account in self.accounts: 376 | account.localKey = self.localKey 377 | 378 | self.__isLoaded = True 379 | 380 | def _addSingleAccount(self, account: td.Account): 381 | # Intended for internal usage only 382 | 383 | # Expects(self.isLoaded(), "Could not add account because i haven't been loaded") 384 | Expects( 385 | account.isLoaded(), 386 | "Could not add account because the account hasn't been loaded", 387 | ) 388 | 389 | account.localKey = self.localKey 390 | 391 | self.__accounts.append(account) 392 | 393 | if self.mainAccount == None: 394 | self.__mainAccount = self.__accounts[0] 395 | 396 | def __loadFromTData(self) -> None: 397 | # Intended for internal usage only 398 | 399 | Expects( 400 | self.basePath != None and self.basePath != "", 401 | "No folder provided to load tdata", 402 | ) 403 | 404 | self.accounts.clear() 405 | 406 | # READ KEY_DATA 407 | keyData = td.Storage.ReadFile("key_" + self.keyFile, self.basePath) # type: ignore 408 | 409 | salt, keyEncrypted, infoEncrypted = QByteArray(), QByteArray(), QByteArray() 410 | 411 | keyData.stream >> salt >> keyEncrypted >> infoEncrypted 412 | 413 | Expects( 414 | keyData.stream.status() == QDataStream.Status.Ok, 415 | QDataStreamFailed("Failed to stream keyData"), 416 | ) 417 | 418 | self.__AppVersion = keyData.version 419 | self.__passcodeKey = td.Storage.CreateLocalKey( 420 | salt, QByteArray(self.__passcodeBytes) 421 | ) 422 | 423 | keyInnerData = td.Storage.DecryptLocal(keyEncrypted, self.passcodeKey) # type: ignore 424 | 425 | self.__localKey = td.AuthKey(keyInnerData.stream.readRawData(256)) 426 | self.__passcodeKeyEncrypted = keyEncrypted 427 | self.__passcodeKeySalt = salt 428 | 429 | info = td.Storage.DecryptLocal(infoEncrypted, self.localKey) # type: ignore 430 | count = info.stream.readInt32() 431 | 432 | Expects(count > 0, "accountsCount is zero, the data might has been corrupted") 433 | 434 | for i in range(count): 435 | index = info.stream.readInt32() 436 | if (index >= 0) and (index < TDesktop.kMaxAccounts): 437 | try: 438 | account = td.Account( 439 | self, 440 | basePath=self.basePath, 441 | api=self.api, 442 | keyFile=self.keyFile, 443 | index=index, 444 | ) 445 | account.prepareToStart(self.__localKey) 446 | 447 | if account.isLoaded(): 448 | self.accounts.append(account) 449 | 450 | except OpenTeleException as e: 451 | pass 452 | 453 | Expects(len(self.accounts) > 0, "No account has been loaded") 454 | 455 | self.__active_index = 0 456 | if not info.stream.atEnd(): 457 | self.__active_index = info.stream.readInt32() 458 | 459 | for account in self.accounts: 460 | if account.index == self.__active_index: 461 | self.__mainAccount = account 462 | break 463 | 464 | if not self.__mainAccount: 465 | self.__mainAccount = self.accounts[0] 466 | 467 | self.__isLoaded = True 468 | 469 | # EXTENDED FUNCTION ==================================================================== 470 | # @extend_classs 471 | # class TDesktop: 472 | 473 | @typing.overload 474 | async def ToTelethon( 475 | self, 476 | session: Union[str, Session] = None, 477 | flag: Type[LoginFlag] = CreateNewSession, 478 | api: Union[Type[APIData], APIData] = API.TelegramDesktop, 479 | password: str = None, 480 | ) -> tl.TelegramClient: 481 | """ 482 | 483 | ### Arguments: 484 | session (`str`, `Session`, default=`None`): 485 | The file name of the `session file` to be used, if `None` then the session will not be saved.\\ 486 | Read more [here](https://docs.telethon.dev/en/latest/concepts/sessions.html?highlight=session#what-are-sessions). 487 | 488 | flag (`LoginFlag`, default=`CreateNewSession`): 489 | The login flag. Read more `[here](LoginFlag)`. 490 | 491 | api (`APIData`, default=`TelegramDesktop`): 492 | Which API to use. Read more `[here](API)`. 493 | 494 | password (`str`, default=`None`): 495 | Two-step verification password if needed. 496 | 497 | ### Returns: 498 | - Return an instance of `TelegramClient` on success 499 | 500 | ### Examples: 501 | Create a telethon session from tdata folder: 502 | ```python 503 | # Using the API that we've generated before. Please refer to method API.Generate() to learn more. 504 | oldAPI = API.TelegramDesktop.Generate(system="windows", unique_id="old_tdata") 505 | tdesk = TDesktop("old_tdata", api=oldAPI) 506 | 507 | # We can safely authorize the new client with a different API. 508 | newAPI = API.TelegramAndroid.Generate(unique_id="new.session") 509 | client = await tdesk.ToTelethon(session="new.session", flag=CreateNewSession, api=newAPI) 510 | await client.connect() 511 | await client.PrintSessions() 512 | ``` 513 | """ 514 | 515 | @typing.overload 516 | async def ToTelethon( 517 | self, 518 | session: Union[str, Session] = None, 519 | flag: Type[LoginFlag] = CreateNewSession, 520 | api: Union[Type[APIData], APIData] = API.TelegramDesktop, 521 | password: str = None, 522 | *, 523 | connection: typing.Type[Connection] = ConnectionTcpFull, 524 | use_ipv6: bool = False, 525 | proxy: Union[tuple, dict] = None, 526 | local_addr: Union[str, tuple] = None, 527 | timeout: int = 10, 528 | request_retries: int = 5, 529 | connection_retries: int = 5, 530 | retry_delay: int = 1, 531 | auto_reconnect: bool = True, 532 | sequential_updates: bool = False, 533 | flood_sleep_threshold: int = 60, 534 | raise_last_call_error: bool = False, 535 | loop: asyncio.AbstractEventLoop = None, 536 | base_logger: Union[str, logging.Logger] = None, 537 | receive_updates: bool = True 538 | ) -> tl.TelegramClient: 539 | pass 540 | 541 | async def ToTelethon( 542 | self, 543 | session: Union[str, Session] = None, 544 | flag: Type[LoginFlag] = CreateNewSession, 545 | api: Union[Type[APIData], APIData] = API.TelegramDesktop, 546 | password: str = None, 547 | **kwargs 548 | ) -> tl.TelegramClient: 549 | 550 | Expects( 551 | self.isLoaded(), 552 | TDesktopNotLoaded("You need to load accounts from a tdata folder first"), 553 | ) 554 | Expects( 555 | self.accountsCount > 0, 556 | TDesktopHasNoAccount("There is no account in this instance of TDesktop"), 557 | ) 558 | assert self.mainAccount 559 | 560 | return await tl.TelegramClient.FromTDesktop( 561 | self.mainAccount, 562 | session=session, 563 | flag=flag, 564 | api=api, 565 | password=password, 566 | **kwargs 567 | ) 568 | 569 | @staticmethod 570 | async def FromTelethon( 571 | telethonClient: tl.TelegramClient, 572 | flag: Type[LoginFlag] = CreateNewSession, 573 | api: Union[Type[APIData], APIData] = API.TelegramDesktop, 574 | password: str = None, 575 | ) -> TDesktop: 576 | """ 577 | Create an instance of `TDesktop` from `TelegramClient`. 578 | 579 | ### Arguments: 580 | telethonClient (`TelegramClient`): 581 | The `TelegramClient` you want to convert from. 582 | 583 | flag (`LoginFlag`, default=`CreateNewSession`): 584 | The login flag. Read more `[here](LoginFlag)`. 585 | 586 | api (`APIData`, default=`API.TelegramDesktop`): 587 | Which API to use. Read more `[here](API)`. 588 | 589 | password (`str`, default=`None`): 590 | Two-step verification password if needed. 591 | 592 | ### Examples: 593 | Save a telethon session to tdata: 594 | ```python 595 | # Using the API that we've generated before. Please refer to method API.Generate() to learn more. 596 | oldAPI = API.TelegramDesktop.Generate(system="windows", unique_id="old.session") 597 | oldclient = TelegramClient("old.session", api=oldAPI) 598 | await oldClient.connect() 599 | 600 | # We can safely CreateNewSession with a different API. 601 | # Be aware that you should not use UseCurrentSession with a different API than the one that first authorized it. 602 | newAPI = API.TelegramAndroid.Generate("new_tdata") 603 | tdesk = await TDesktop.FromTelethon(oldclient, flag=CreateNewSession, api=newAPI) 604 | 605 | # Save the new session to a folder named "new_tdata" 606 | tdesk.SaveTData("new_tdata") 607 | ``` 608 | """ 609 | 610 | Expects( 611 | (flag == CreateNewSession) or (flag == UseCurrentSession), 612 | LoginFlagInvalid("LoginFlag invalid"), 613 | ) 614 | 615 | _self = TDesktop() 616 | _self.__generateLocalKey() 617 | 618 | await td.Account.FromTelethon( 619 | telethonClient, flag=flag, api=api, password=password, owner=_self 620 | ) 621 | 622 | return _self 623 | 624 | @classmethod 625 | def PerformanceMode(cls, enabled: bool = True): 626 | """ 627 | Enable or disable performance mode. See `kPerformanceMode`. 628 | It is enabled by default. 629 | 630 | ### Arguments: 631 | enabled (`bool`, default=`True`): 632 | Either enable or disable performance mode. 633 | 634 | """ 635 | cls.kPerformanceMode = enabled 636 | 637 | kMaxAccounts: int = int(3) 638 | """The maximum amount of accounts a client can have""" 639 | 640 | kDefaultKeyFile: str = "data" 641 | """See `TDesktop.keyFile`""" 642 | 643 | kPerformanceMode: bool = True 644 | """ 645 | When enabled, `SaveTData()` will be 5000x faster. 646 | - What it does is using a constant `localKey` rather than generating it everytime when saving tdata. 647 | - The average time for generating `localKey` is about `250` to `350` ms, depend on your CPU. 648 | - When in performance mode, the average time to generate `localKey` is `0.0628` ms. Which is 5000x faster 649 | - Of course this comes with a catch, your `tdata files` will always use a same constant `localKey`. Basicly no protection at all, but who cares? 650 | 651 | ### Notes: 652 | Note: Performance mode will be disabled if `passcode` is set. 653 | """ 654 | 655 | @property 656 | def api(self) -> APIData: 657 | """ 658 | The API this client is using. 659 | """ 660 | return self.__api 661 | 662 | @api.setter 663 | def api(self, value) -> None: 664 | self.__api = value 665 | for account in self.accounts: 666 | account.api = value 667 | 668 | @property 669 | def basePath(self) -> Optional[str]: 670 | """ 671 | Base folder of `TDesktop`, this is where data stored 672 | Same as tdata folder of `Telegram Desktop` 673 | """ 674 | return self.__basePath 675 | 676 | @property 677 | def passcode(self) -> str: 678 | """ 679 | Passcode used to encrypt and decrypt data 680 | Same as the Local Passcode of `Telegram Desktop` 681 | """ 682 | return self.__passcode 683 | 684 | @property 685 | def keyFile(self) -> str: 686 | """ 687 | The default value is `"data"`, this argument is rarely ever used. 688 | It is used by `Telegram Desktop` by running it with the `"-key"` argument. 689 | I don't know what's the use cases of it, maybe this was a legacy feature of `Telegram Desktop`. 690 | """ 691 | return self.__keyFile 692 | 693 | @property 694 | def passcodeKey(self) -> Optional[td.AuthKey]: 695 | return self.__passcodeKey 696 | 697 | @property 698 | def localKey(self) -> Optional[td.AuthKey]: 699 | """ 700 | The key used to encrypt/decrypt data 701 | """ 702 | return self.__localKey 703 | 704 | @property 705 | def AppVersion(self) -> Optional[int]: 706 | """ 707 | App version of TDesktop client 708 | """ 709 | return self.__AppVersion 710 | 711 | @property 712 | def AppVersionString(self) -> Optional[str]: 713 | raise NotImplementedError() 714 | return self.__AppVersion 715 | 716 | @property 717 | def accountsCount(self) -> int: 718 | """ 719 | The number of accounts this client has 720 | """ 721 | # return self.__accountsCount 722 | return len(self.__accounts) 723 | 724 | @property 725 | def accounts(self) -> List[td.Account]: 726 | """ 727 | List of accounts this client has\n 728 | If you want to get the main account, please use .mainAccount instead 729 | """ 730 | return self.__accounts 731 | 732 | @property 733 | def mainAccount(self) -> Optional[td.Account]: 734 | """ 735 | The main account of the client 736 | """ 737 | return self.__mainAccount 738 | -------------------------------------------------------------------------------- /src/tl/__init__.py: -------------------------------------------------------------------------------- 1 | from .shared import * 2 | -------------------------------------------------------------------------------- /src/tl/configs.py: -------------------------------------------------------------------------------- 1 | from ..exception import * 2 | from ..utils import * 3 | from ..api import APIData, API, LoginFlag, CreateNewSession, UseCurrentSession 4 | from .. import td 5 | 6 | from typing import Union, Callable, TypeVar, Type, List, Dict, Any, TYPE_CHECKING 7 | from ctypes import ( 8 | sizeof, 9 | c_int32 as int32, 10 | c_int64 as int64, 11 | c_uint32 as uint32, 12 | c_uint64 as uint64, 13 | c_short as short, 14 | c_ushort as ushort, 15 | ) 16 | 17 | import telethon 18 | from telethon.sessions import StringSession 19 | from telethon.crypto import AuthKey 20 | from telethon import tl, functions, types, utils, password as pwd_mod 21 | 22 | from telethon.network.connection.connection import Connection 23 | from telethon.network.connection.tcpfull import ConnectionTcpFull 24 | 25 | 26 | from telethon.sessions.abstract import Session 27 | from telethon.sessions.sqlite import SQLiteSession 28 | from telethon.sessions.memory import MemorySession 29 | 30 | import asyncio 31 | -------------------------------------------------------------------------------- /src/tl/shared.py: -------------------------------------------------------------------------------- 1 | from .telethon import TelegramClient 2 | -------------------------------------------------------------------------------- /src/tl/telethon.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .configs import * 4 | from . import shared as tl 5 | 6 | from telethon.errors.rpcerrorlist import ( 7 | PasswordHashInvalidError, 8 | AuthTokenAlreadyAcceptedError, 9 | AuthTokenExpiredError, 10 | AuthTokenInvalidError, 11 | FreshResetAuthorisationForbiddenError, 12 | HashInvalidError, 13 | ) 14 | from telethon.tl.types import TypeInputClientProxy, TypeJSONValue 15 | from telethon.tl.types.auth import LoginTokenMigrateTo 16 | import logging 17 | import warnings 18 | from typing import Awaitable 19 | 20 | 21 | @extend_override_class 22 | class CustomInitConnectionRequest(functions.InitConnectionRequest): 23 | def __init__( 24 | self, 25 | api_id: int, 26 | device_model: str, 27 | system_version: str, 28 | app_version: str, 29 | system_lang_code: str, 30 | lang_pack: str, 31 | lang_code: str, 32 | query, 33 | proxy: TypeInputClientProxy = None, 34 | params: TypeJSONValue = None, 35 | ): 36 | 37 | # our hook pass pid as device_model 38 | data = APIData.findData(device_model) # type: ignore 39 | if data != None: 40 | self.api_id = data.api_id 41 | self.device_model = data.device_model if data.device_model else device_model 42 | self.system_version = ( 43 | data.system_version if data.system_version else system_version 44 | ) 45 | self.app_version = data.app_version if data.app_version else app_version 46 | self.system_lang_code = ( 47 | data.system_lang_code if data.system_lang_code else system_lang_code 48 | ) 49 | self.lang_pack = data.lang_pack if data.lang_pack else lang_pack 50 | self.lang_code = data.lang_code if data.lang_code else lang_code 51 | data.destroy() 52 | else: 53 | self.api_id = api_id 54 | self.device_model = device_model 55 | self.system_version = system_version 56 | self.app_version = app_version 57 | self.system_lang_code = system_lang_code 58 | self.lang_pack = lang_pack 59 | self.lang_code = lang_code 60 | 61 | self.query = query 62 | self.proxy = proxy 63 | self.params = params 64 | 65 | 66 | @extend_class 67 | class TelegramClient(telethon.TelegramClient, BaseObject): 68 | """ 69 | Extended version of [telethon.TelegramClient](https://github.com/LonamiWebs/Telethon/blob/master/telethon/_client/telegramclient.py#L23) 70 | 71 | ### Methods: 72 | FromTDesktop(): 73 | Create an instance of `TelegramClient` from `TDesktop`. 74 | 75 | ToTDesktop(): 76 | Convert this `TelegramClient` instance to `TDesktop`. 77 | 78 | QRLoginToNewClient(): 79 | Return `True` if logged-in using an `[official API](API)`. 80 | 81 | GetSessions(): 82 | Get all logged in sessions. 83 | 84 | GetCurrentSession(): 85 | Get current logged-in session. 86 | 87 | TerminateSession(): 88 | Terminate a specific session. 89 | 90 | TerminateAllSessions(): 91 | Terminate all other sessions. 92 | 93 | PrintSessions(): 94 | Pretty-print all logged-in sessions. 95 | 96 | is_official_app(): 97 | Return `True` if logged-in using an `[official API](API)`. 98 | 99 | """ 100 | 101 | @typing.overload 102 | def __init__( 103 | self: TelegramClient, 104 | session: Union[str, Session] = None, 105 | api: Union[Type[APIData], APIData] = API.TelegramDesktop, 106 | ): 107 | """Start TelegramClient with customized api. 108 | 109 | Read more at [opentele GitHub](https://github.com/thedemons/opentele#authorization) 110 | 111 | ### Arguments: 112 | session (`str` | `Session`): 113 | The file name of the `session file` to be used, if a string is\\ 114 | given (it may be a full path), or the `Session` instance to be used\\ 115 | Otherwise, if it's `None`, the `session` will not be saved,\\ 116 | and you should call method `.log_out()` when you're done. 117 | 118 | Read more [here](https://docs.telethon.dev/en/latest/concepts/sessions.html?highlight=session#what-are-sessions). 119 | 120 | api (`API`, default=`TelegramDesktop`): 121 | Which API to use. Read more `[here](API)`. 122 | """ 123 | 124 | @typing.overload 125 | def __init__( 126 | self, 127 | session: Union[str, Session] = None, 128 | api: Union[Type[APIData], APIData] = None, 129 | api_id: int = 0, 130 | api_hash: str = None, 131 | *, 132 | connection: typing.Type[Connection] = ConnectionTcpFull, 133 | use_ipv6: bool = False, 134 | proxy: Union[tuple, dict] = None, 135 | local_addr: Union[str, tuple] = None, 136 | timeout: int = 10, 137 | request_retries: int = 5, 138 | connection_retries: int = 5, 139 | retry_delay: int = 1, 140 | auto_reconnect: bool = True, 141 | sequential_updates: bool = False, 142 | flood_sleep_threshold: int = 60, 143 | raise_last_call_error: bool = False, 144 | device_model: str = None, 145 | system_version: str = None, 146 | app_version: str = None, 147 | lang_code: str = "en", 148 | system_lang_code: str = "en", 149 | loop: asyncio.AbstractEventLoop = None, 150 | base_logger: Union[str, logging.Logger] = None, 151 | receive_updates: bool = True, 152 | ): 153 | """ 154 | !skip 155 | This is the abstract base class for the client. It defines some 156 | basic stuff like connecting, switching data center, etc, and 157 | leaves the `__call__` unimplemented. 158 | 159 | ### Arguments: 160 | session (`str` | `Session`, default=`None`): 161 | The file name of the `session file` to be used, if a string is\\ 162 | given (it may be a full path), or the `Session` instance to be used\\ 163 | Otherwise, if it's `None`, the `session` will not be saved,\\ 164 | and you should call method `.log_out()` when you're done. 165 | 166 | Note that if you pass a string it will be a file in the current working directory, although you can also pass absolute paths.\\ 167 | 168 | The session file contains enough information for you to login\\ 169 | without re-sending the code, so if you have to enter the code\\ 170 | more than once, maybe you're changing the working directory,\\ 171 | renaming or removing the file, or using random names. 172 | 173 | api (`API`, default=None): 174 | Use custom api_id and api_hash for better experience.\n 175 | These arguments will be ignored if it is set in the API: `api_id`, `api_hash`, `device_model`, `system_version`, `app_version`, `lang_code`, `system_lang_code` 176 | Read more at [opentele GitHub](https://github.com/thedemons/opentele#authorization) 177 | 178 | api_id (`int` | `str`, default=0): 179 | The API ID you obtained from https://my.telegram.org. 180 | 181 | api_hash (`str`, default=None): 182 | The API hash you obtained from https://my.telegram.org. 183 | 184 | connection (`telethon.network.connection.common.Connection`, default=ConnectionTcpFull): 185 | The connection instance to be used when creating a new connection 186 | to the servers. It **must** be a type. 187 | 188 | Defaults to `telethon.network.connection.tcpfull.ConnectionTcpFull`. 189 | 190 | use_ipv6 (`bool`, default=False): 191 | Whether to connect to the servers through IPv6 or not. 192 | By default this is `False` as IPv6 support is not 193 | too widespread yet. 194 | 195 | proxy (`tuple` | `list` | `dict`, default=None): 196 | An iterable consisting of the proxy info. If `connection` is 197 | one of `MTProxy`, then it should contain MTProxy credentials: 198 | ``('hostname', port, 'secret')``. Otherwise, it's meant to store 199 | function parameters for PySocks, like ``(type, 'hostname', port)``. 200 | See https://github.com/Anorov/PySocks#usage-1 for more. 201 | 202 | local_addr (`str` | `tuple`, default=None): 203 | Local host address (and port, optionally) used to bind the socket to locally. 204 | You only need to use this if you have multiple network cards and 205 | want to use a specific one. 206 | 207 | timeout (`int` | `float`, default=10): 208 | The timeout in seconds to be used when connecting. 209 | This is **not** the timeout to be used when ``await``'ing for 210 | invoked requests, and you should use ``asyncio.wait`` or 211 | ``asyncio.wait_for`` for that. 212 | 213 | request_retries (`int` | `None`, default=5): 214 | How many times a request should be retried. Request are retried 215 | when Telegram is having internal issues (due to either 216 | ``errors.ServerError`` or ``errors.RpcCallFailError``), 217 | when there is a ``errors.FloodWaitError`` less than 218 | `flood_sleep_threshold`, or when there's a migrate error. 219 | 220 | May take a negative or `None` value for infinite retries, but 221 | this is not recommended, since some requests can always trigger 222 | a call fail (such as searching for messages). 223 | 224 | connection_retries (`int` | `None`, default=5): 225 | How many times the reconnection should retry, either on the 226 | initial connection or when Telegram disconnects us. May be 227 | set to a negative or `None` value for infinite retries, but 228 | this is not recommended, since the program can get stuck in an 229 | infinite loop. 230 | 231 | retry_delay (`int` | `float`, default=1): 232 | The delay in seconds to sleep between automatic reconnections. 233 | 234 | auto_reconnect (`bool`, default=True): 235 | Whether reconnection should be retried `connection_retries` 236 | times automatically if Telegram disconnects us or not. 237 | 238 | sequential_updates (`bool`, default=False): 239 | By default every incoming update will create a new task, so 240 | you can handle several updates in parallel. Some scripts need 241 | the order in which updates are processed to be sequential, and 242 | this setting allows them to do so. 243 | 244 | If set to `True`, incoming updates will be put in a queue 245 | and processed sequentially. This means your event handlers 246 | should *not* perform long-running operations since new 247 | updates are put inside of an unbounded queue. 248 | 249 | flood_sleep_threshold (`int` | `float`, default=60): 250 | The threshold below which the library should automatically 251 | sleep on flood wait and slow mode wait errors (inclusive). For instance, if a 252 | ``FloodWaitError`` for 17s occurs and `flood_sleep_threshold` 253 | is 20s, the library will ``sleep`` automatically. If the error 254 | was for 21s, it would ``raise FloodWaitError`` instead. Values 255 | larger than a day (like ``float('inf')``) will be changed to a day. 256 | 257 | raise_last_call_error (`bool`, default=False): 258 | When API calls fail in a way that causes Telethon to retry 259 | automatically, should the RPC error of the last attempt be raised 260 | instead of a generic ValueError. This is mostly useful for 261 | detecting when Telegram has internal issues. 262 | 263 | device_model (`str`, default=None): 264 | "Device model" to be sent when creating the initial connection. 265 | Defaults to 'PC (n)bit' derived from ``platform.uname().machine``, or its direct value if unknown. 266 | 267 | system_version (`str`, default=None): 268 | "System version" to be sent when creating the initial connection. 269 | Defaults to ``platform.uname().release`` stripped of everything ahead of -. 270 | 271 | app_version (`str`, default=None): 272 | "App version" to be sent when creating the initial connection. 273 | Defaults to `telethon.version.__version__`. 274 | 275 | lang_code (`str`, default='en'): 276 | "Language code" to be sent when creating the initial connection. 277 | Defaults to ``'en'``. 278 | 279 | system_lang_code (`str`, default='en'): 280 | "System lang code" to be sent when creating the initial connection. 281 | Defaults to `lang_code`. 282 | 283 | loop (`asyncio.AbstractEventLoop`, default=None): 284 | Asyncio event loop to use. Defaults to `asyncio.get_event_loop()`. 285 | This argument is ignored. 286 | 287 | base_logger (`str` | `logging.Logger`, default=None): 288 | Base logger name or instance to use. 289 | If a `str` is given, it'll be passed to `logging.getLogger()`. If a 290 | `logging.Logger` is given, it'll be used directly. If something 291 | else or nothing is given, the default logger will be used. 292 | 293 | receive_updates (`bool`, default=True): 294 | Whether the client will receive updates or not. By default, updates 295 | will be received from Telegram as they occur. 296 | 297 | Turning this off means that Telegram will not send updates at all 298 | so event handlers, conversations, and QR login will not work. 299 | However, certain scripts don't need updates, so this will reduce 300 | the amount of bandwidth used. 301 | 302 | """ 303 | 304 | @override 305 | def __init__( 306 | self, 307 | session: Union[str, Session] = None, 308 | api: Union[Type[APIData], APIData] = None, 309 | api_id: int = 0, 310 | api_hash: str = None, 311 | **kwargs, 312 | ): 313 | # Use API.TelegramDesktop by default. 314 | 315 | if api != None: 316 | if isinstance(api, APIData) or ( 317 | isinstance(api, type) 318 | and APIData.__subclasscheck__(api) 319 | and api != APIData 320 | ): 321 | api_id = api.api_id 322 | api_hash = api.api_hash 323 | 324 | # pass our hook id through the device_model 325 | kwargs["device_model"] = api.pid # type: ignore 326 | 327 | else: 328 | if ( 329 | (isinstance(api, int) or isinstance(api, str)) 330 | and api_id 331 | and isinstance(api_id, str) 332 | ): 333 | api_id = api 334 | api_hash = api_id 335 | api = None 336 | 337 | elif api_id == 0 and api_hash == None: 338 | api = API.TelegramDesktop 339 | api_id = api.api_id 340 | api_hash = api.api_hash 341 | 342 | # pass our hook id through the device_model 343 | kwargs["device_model"] = api.pid # type: ignore 344 | 345 | self._user_id = None 346 | self.__TelegramClient____init__(session, api_id, api_hash, **kwargs) 347 | 348 | @property 349 | def UserId(self): 350 | return self._self_id if self._self_id else self._user_id 351 | 352 | @UserId.setter 353 | def UserId(self, id): 354 | self._user_id = id 355 | 356 | async def GetSessions(self) -> Optional[types.account.Authorizations]: 357 | """ 358 | Get all logged-in sessions. 359 | 360 | ### Returns: 361 | - Return an instance of `Authorizations` on success 362 | """ 363 | return await self(functions.account.GetAuthorizationsRequest()) # type: ignore 364 | 365 | async def GetCurrentSession(self) -> Optional[types.Authorization]: 366 | """ 367 | Get current logged-in session. 368 | 369 | ### Returns: 370 | Return `telethon.types.Authorization` on success. 371 | Return `None` on failure. 372 | """ 373 | results = await self.GetSessions() 374 | 375 | return ( 376 | next((auth for auth in results.authorizations if auth.current), None) 377 | if results != None 378 | else None 379 | ) 380 | 381 | async def TerminateSession(self, hash: int): 382 | """ 383 | Terminate a specific session 384 | 385 | ### Arguments: 386 | hash (`int`): 387 | The `session`'s hash to terminate 388 | 389 | ### Raises: 390 | `FreshResetAuthorisationForbiddenError`: You can't log out other `sessions` if less than `24 hours` have passed since you logged on to the `current session`. 391 | `HashInvalidError`: The provided hash is invalid. 392 | """ 393 | 394 | try: 395 | await self(functions.account.ResetAuthorizationRequest(hash)) 396 | 397 | except FreshResetAuthorisationForbiddenError as e: 398 | raise FreshResetAuthorisationForbiddenError( 399 | "You can't logout other sessions if less than 24 hours have passed since you logged on the current session." 400 | ) 401 | 402 | except HashInvalidError as e: 403 | raise HashInvalidError("The provided hash is invalid.") 404 | 405 | async def TerminateAllSessions(self) -> bool: 406 | """ 407 | Terminate all other sessions. 408 | """ 409 | sessions = await self.GetSessions() 410 | if sessions == None: 411 | return False 412 | 413 | for ss in sessions.authorizations: 414 | if not ss.current: 415 | await self.TerminateSession(ss.hash) 416 | 417 | return True 418 | 419 | async def PrintSessions(self, sessions: types.account.Authorizations = None): 420 | """ 421 | Pretty-print all logged-in sessions. 422 | 423 | ### Arguments: 424 | sessions (`Authorizations`, default=`None`): 425 | `Sessions` that return by `GetSessions()`, if `None` then it will `GetSessions()` first. 426 | 427 | ### Returns: 428 | On success, it should prints the sessions table as the code below. 429 | ``` 430 | |---------+-----------------------------+----------+----------------+--------+----------------------------+--------------| 431 | | | Device | Platform | System | API_ID | App name | Official App | 432 | |---------+-----------------------------+----------+----------------+--------+----------------------------+--------------| 433 | | Current | MacBook Pro | macOS | 10.15.6 | 2834 | Telegram macOS 8.4 | ✔ | 434 | |---------+-----------------------------+----------+----------------+--------+----------------------------+--------------| 435 | | 1 | Chrome 96 | Windows | | 2496 | Telegram Web 1.28.3 Z | ✔ | 436 | | 2 | iMac | macOS | 11.3.1 | 2834 | Telegram macOS 8.4 | ✔ | 437 | | 3 | MacBook Pro | macOS | 10.12 | 2834 | Telegram macOS 8.4 | ✔ | 438 | | 4 | Huawei Y360-U93 | Android | 7.1 N MR1 (25) | 21724 | Telegram Android X 8.4.1 | ✔ | 439 | | 5 | Samsung Galaxy Spica | Android | 6.0 M (23) | 6 | Telegram Android 8.4.1 | ✔ | 440 | | 6 | Xiaomi Redmi Note 8 | Android | 10 Q (29) | 6 | Telegram Android 8.4.1 | ✔ | 441 | | 7 | Samsung Galaxy Tab A (2017) | Android | 7.0 N (24) | 6 | Telegram Android 8.4.1 | ✔ | 442 | | 8 | Samsung Galaxy XCover Pro | Android | 8.0 O (26) | 6 | Telegram Android 8.4.1 | ✔ | 443 | | 9 | iPhone X | iOS | 13.1.3 | 10840 | Telegram iOS 8.4 | ✔ | 444 | | 10 | iPhone XS Max | iOS | 12.11.0 | 10840 | Telegram iOS 8.4 | ✔ | 445 | | 11 | iPhone 11 Pro Max | iOS | 14.4.2 | 10840 | Telegram iOS 8.4 | ✔ | 446 | |---------+-----------------------------+----------+----------------+--------+----------------------------+--------------| 447 | ``` 448 | 449 | """ 450 | if (sessions == None) or not isinstance(sessions, types.account.Authorizations): 451 | sessions = await self.GetSessions() 452 | 453 | assert sessions 454 | 455 | table = [] 456 | 457 | index = 0 458 | for session in sessions.authorizations: 459 | table.append( 460 | { 461 | " ": "Current" if session.current else index, 462 | "Device": session.device_model, 463 | "Platform": session.platform, 464 | "System": session.system_version, 465 | "API_ID": session.api_id, 466 | "App name": "{} {}".format(session.app_name, session.app_version), 467 | "Official App": "✔" if session.official_app else "✖", 468 | } 469 | ) 470 | index += 1 471 | 472 | print(PrettyTable(table, [1])) 473 | 474 | async def is_official_app(self) -> bool: 475 | """ 476 | Return `True` if this session was logged-in using an official app (`API`). 477 | """ 478 | auth = await self.GetCurrentSession() 479 | 480 | return False if auth == None else bool(auth.official_app) 481 | 482 | @typing.overload 483 | async def QRLoginToNewClient( 484 | self, 485 | session: Union[str, Session] = None, 486 | api: Union[Type[APIData], APIData] = API.TelegramDesktop, 487 | password: str = None, 488 | ) -> TelegramClient: 489 | """ 490 | Create a new session using the current session. 491 | 492 | ### Arguments: 493 | session (`str`, `Session`, default=`None`): 494 | description 495 | 496 | api (`API`, default=`TelegramDesktop`): 497 | Which API to use. Read more `[here](API)`. 498 | 499 | password (`str`, default=`None`): 500 | Two-step verification password, set if needed. 501 | 502 | ### Raises: 503 | - `NoPasswordProvided`: The account's two-step verification is enabled and no `password` was provided. Please set the `password` parameters. 504 | - `PasswordIncorrect`: The two-step verification `password` is incorrect. 505 | - `TimeoutError`: Time out waiting for the client to be authorized. 506 | 507 | ### Returns: 508 | - Return an instance of `TelegramClient` on success. 509 | 510 | ### Examples: 511 | Use to current session to authorize a new session: 512 | ```python 513 | # Using the API that we've generated before. Please refer to method API.Generate() to learn more. 514 | oldAPI = API.TelegramDesktop.Generate(system="windows", unique_id="old.session") 515 | oldclient = TelegramClient("old.session", api=oldAPI) 516 | await oldClient.connect() 517 | 518 | # We can safely authorize the new client with a different API. 519 | newAPI = API.TelegramAndroid.Generate(unique_id="new.session") 520 | newClient = await client.QRLoginToNewClient(session="new.session", api=newAPI) 521 | await newClient.connect() 522 | await newClient.PrintSessions() 523 | ``` 524 | """ 525 | 526 | @typing.overload 527 | async def QRLoginToNewClient( 528 | self, 529 | session: Union[str, Session] = None, 530 | api: Union[Type[APIData], APIData] = API.TelegramDesktop, 531 | password: str = None, 532 | *, 533 | connection: typing.Type[Connection] = ConnectionTcpFull, 534 | use_ipv6: bool = False, 535 | proxy: Union[tuple, dict] = None, 536 | local_addr: Union[str, tuple] = None, 537 | timeout: int = 10, 538 | request_retries: int = 5, 539 | connection_retries: int = 5, 540 | retry_delay: int = 1, 541 | auto_reconnect: bool = True, 542 | sequential_updates: bool = False, 543 | flood_sleep_threshold: int = 60, 544 | raise_last_call_error: bool = False, 545 | loop: asyncio.AbstractEventLoop = None, 546 | base_logger: Union[str, logging.Logger] = None, 547 | receive_updates: bool = True, 548 | ) -> TelegramClient: 549 | pass 550 | 551 | async def QRLoginToNewClient( 552 | self, 553 | session: Union[str, Session] = None, 554 | api: Union[Type[APIData], APIData] = API.TelegramDesktop, 555 | password: str = None, 556 | **kwargs, 557 | ) -> TelegramClient: 558 | 559 | newClient = TelegramClient(session, api=api, **kwargs) 560 | 561 | try: 562 | await newClient.connect() 563 | # switch DC for now because i can't handle LoginTokenMigrateTo... 564 | if newClient.session.dc_id != self.session.dc_id: 565 | await newClient._switch_dc(self.session.dc_id) 566 | except OSError as e: 567 | raise BaseException("Cannot connect") 568 | 569 | if await newClient.is_user_authorized(): # nocov 570 | 571 | currentAuth = await newClient.GetCurrentSession() 572 | if currentAuth != None: 573 | 574 | if currentAuth.api_id == api.api_id: 575 | warnings.warn( 576 | "\nCreateNewSession - a session file with the same name " 577 | "is already existed, returning the old session" 578 | ) 579 | else: 580 | warnings.warn( 581 | "\nCreateNewSession - a session file with the same name " 582 | "is already existed, but its api_id is different from " 583 | "the current one, it will be overwritten" 584 | ) 585 | 586 | disconnect = newClient.disconnect() 587 | if disconnect: 588 | await disconnect 589 | await newClient.disconnected 590 | 591 | newClient.session.close() 592 | newClient.session.delete() 593 | 594 | newClient = await self.QRLoginToNewClient( 595 | session=session, api=api, password=password, **kwargs 596 | ) 597 | 598 | return newClient 599 | 600 | if not self._self_id: 601 | oldMe = await self.get_me() 602 | 603 | timeout_err = None 604 | 605 | # try to generate the qr token muiltiple times to work around timeout error. 606 | # this happens when we're logging in from a mismatched DC. 607 | request_retries = ( 608 | kwargs["request_retries"] if "request_retries" in kwargs else 5 609 | ) # default value for request_retries 610 | for attempt in range(request_retries): # nocov 611 | 612 | try: 613 | # we could have been already authorized, but it still raised an timeouterror (??!) 614 | if attempt > 0 and await newClient.is_user_authorized(): 615 | break 616 | 617 | qr_login = await newClient.qr_login() 618 | 619 | # if we encountered timeout error in the first try, it might be because of mismatched DcId, we're gonna have to switch_dc 620 | if isinstance(qr_login._resp, types.auth.LoginTokenMigrateTo): 621 | await newClient._switch_dc(qr_login._resp.dc_id) 622 | qr_login._resp = await newClient( 623 | functions.auth.ImportLoginTokenRequest(qr_login._resp.token) 624 | ) 625 | 626 | # for the above reason, we should check if we're already authorized 627 | if isinstance(qr_login._resp, types.auth.LoginTokenSuccess): 628 | coro = newClient._on_login(qr_login._resp.authorization.user) 629 | if isinstance(coro, Awaitable): 630 | await coro 631 | break 632 | 633 | # calculate when will the qr token expire 634 | import datetime 635 | 636 | time_now = datetime.datetime.now(datetime.timezone.utc) 637 | time_out = (qr_login.expires - time_now).seconds + 5 638 | 639 | resp = await self( 640 | functions.auth.AcceptLoginTokenRequest(qr_login.token) 641 | ) 642 | 643 | await qr_login.wait(time_out) 644 | 645 | # break the loop on success 646 | break 647 | 648 | except ( 649 | AuthTokenAlreadyAcceptedError, 650 | AuthTokenExpiredError, 651 | AuthTokenInvalidError, 652 | ) as e: 653 | # AcceptLoginTokenRequest exception handler 654 | raise e 655 | 656 | except (TimeoutError, asyncio.TimeoutError) as e: 657 | 658 | warnings.warn( 659 | "\nQRLoginToNewClient attemp {} failed because {}".format( 660 | attempt + 1, type(e) 661 | ) 662 | ) 663 | timeout_err = TimeoutError( 664 | "Something went wrong, i couldn't perform the QR login process" 665 | ) 666 | 667 | except telethon.errors.SessionPasswordNeededError as e: 668 | 669 | # requires an 2fa password 670 | 671 | Expects( 672 | password != None, 673 | NoPasswordProvided( 674 | "Two-step verification is enabled for this account.\n" 675 | "You need to provide the `password` to argument" 676 | ), 677 | ) 678 | 679 | # two-step verification 680 | try: 681 | pwd: types.account.Password = await newClient(functions.account.GetPasswordRequest()) # type: ignore 682 | result = await newClient( 683 | functions.auth.CheckPasswordRequest( 684 | pwd_mod.compute_check(pwd, password) # type: ignore 685 | ) 686 | ) 687 | 688 | # successful log in 689 | coro = newClient._on_login(result.user) # type: ignore 690 | if isinstance(coro, Awaitable): 691 | await coro 692 | break 693 | 694 | except PasswordHashInvalidError as e: 695 | raise PasswordIncorrect(e.__str__()) from e 696 | 697 | warnings.warn( 698 | "\nQRLoginToNewClient attemp {} failed. Retrying..".format(attempt + 1) 699 | ) 700 | 701 | if timeout_err: 702 | raise timeout_err 703 | 704 | return newClient 705 | 706 | async def ToTDesktop( 707 | self, 708 | flag: Type[LoginFlag] = CreateNewSession, 709 | api: Union[Type[APIData], APIData] = API.TelegramDesktop, 710 | password: str = None, 711 | ) -> td.TDesktop: 712 | """ 713 | Convert this instance of `TelegramClient` to `TDesktop` 714 | 715 | ### Arguments: 716 | flag (`LoginFlag`, default=`CreateNewSession`): 717 | The login flag. Read more `[here](LoginFlag)`. 718 | 719 | api (`API`, default=`TelegramDesktop`): 720 | Which API to use. Read more `[here](API)`. 721 | 722 | password (`str`, default=`None`): 723 | Two-step verification `password` if needed. 724 | 725 | ### Returns: 726 | - Return an instance of `TDesktop` on success 727 | 728 | ### Examples: 729 | Save a telethon session to tdata: 730 | ```python 731 | # Using the API that we've generated before. Please refer to method API.Generate() to learn more. 732 | oldAPI = API.TelegramDesktop.Generate(system="windows", unique_id="old.session") 733 | oldclient = TelegramClient("old.session", api=oldAPI) 734 | await oldClient.connect() 735 | 736 | # We can safely CreateNewSession with a different API. 737 | # Be aware that you should not use UseCurrentSession with a different API than the one that first authorized it. 738 | newAPI = API.TelegramAndroid.Generate(unique_id="new_tdata") 739 | tdesk = await oldClient.ToTDesktop(flag=CreateNewSession, api=newAPI) 740 | 741 | # Save the new session to a folder named "new_tdata" 742 | tdesk.SaveTData("new_tdata") 743 | ``` 744 | """ 745 | 746 | return await td.TDesktop.FromTelethon( 747 | self, flag=flag, api=api, password=password 748 | ) 749 | 750 | @typing.overload 751 | @staticmethod 752 | async def FromTDesktop( 753 | account: Union[td.TDesktop, td.Account], 754 | session: Union[str, Session] = None, 755 | flag: Type[LoginFlag] = CreateNewSession, 756 | api: Union[Type[APIData], APIData] = API.TelegramDesktop, 757 | password: str = None, 758 | ) -> TelegramClient: 759 | """ 760 | 761 | ### Arguments: 762 | account (`TDesktop`, `Account`): 763 | The `TDesktop` or `Account` you want to convert from. 764 | 765 | session (`str`, `Session`, default=`None`): 766 | The file name of the `session file` to be used, if `None` then the session will not be saved.\\ 767 | Read more [here](https://docs.telethon.dev/en/latest/concepts/sessions.html?highlight=session#what-are-sessions). 768 | 769 | flag (`LoginFlag`, default=`CreateNewSession`): 770 | The login flag. Read more `[here](LoginFlag)`. 771 | 772 | api (`API`, default=`TelegramDesktop`): 773 | Which API to use. Read more `[here](API)`. 774 | 775 | password (`str`, default=`None`): 776 | Two-step verification password if needed. 777 | 778 | ### Returns: 779 | - Return an instance of `TelegramClient` on success 780 | 781 | ### Examples: 782 | Create a telethon session using tdata folder: 783 | ```python 784 | # Using the API that we've generated before. Please refer to method API.Generate() to learn more. 785 | oldAPI = API.TelegramDesktop.Generate(system="windows", unique_id="old_tdata") 786 | tdesk = TDesktop("old_tdata", api=oldAPI) 787 | 788 | # We can safely authorize the new client with a different API. 789 | newAPI = API.TelegramAndroid.Generate(unique_id="new.session") 790 | client = await TelegramClient.FromTDesktop(tdesk, session="new.session", flag=CreateNewSession, api=newAPI) 791 | await client.connect() 792 | await client.PrintSessions() 793 | ``` 794 | """ 795 | 796 | @typing.overload 797 | @staticmethod 798 | async def FromTDesktop( 799 | account: Union[td.TDesktop, td.Account], 800 | session: Union[str, Session] = None, 801 | flag: Type[LoginFlag] = CreateNewSession, 802 | api: Union[Type[APIData], APIData] = API.TelegramDesktop, 803 | password: str = None, 804 | *, 805 | connection: typing.Type[Connection] = ConnectionTcpFull, 806 | use_ipv6: bool = False, 807 | proxy: Union[tuple, dict] = None, 808 | local_addr: Union[str, tuple] = None, 809 | timeout: int = 10, 810 | request_retries: int = 5, 811 | connection_retries: int = 5, 812 | retry_delay: int = 1, 813 | auto_reconnect: bool = True, 814 | sequential_updates: bool = False, 815 | flood_sleep_threshold: int = 60, 816 | raise_last_call_error: bool = False, 817 | loop: asyncio.AbstractEventLoop = None, 818 | base_logger: Union[str, logging.Logger] = None, 819 | receive_updates: bool = True, 820 | ) -> TelegramClient: 821 | pass 822 | 823 | @staticmethod 824 | async def FromTDesktop( 825 | account: Union[td.TDesktop, td.Account], 826 | session: Union[str, Session] = None, 827 | flag: Type[LoginFlag] = CreateNewSession, 828 | api: Union[Type[APIData], APIData] = API.TelegramDesktop, 829 | password: str = None, 830 | **kwargs, 831 | ) -> TelegramClient: 832 | 833 | Expects( 834 | (flag == CreateNewSession) or (flag == UseCurrentSession), 835 | LoginFlagInvalid("LoginFlag invalid"), 836 | ) 837 | 838 | if isinstance(account, td.TDesktop): 839 | Expects( 840 | account.isLoaded(), 841 | TDesktopNotLoaded( 842 | "You need to load accounts from a tdata folder first" 843 | ), 844 | ) 845 | Expects( 846 | account.accountsCount > 0, 847 | TDesktopHasNoAccount( 848 | "There is no account in this instance of TDesktop" 849 | ), 850 | ) 851 | assert account.mainAccount 852 | account = account.mainAccount 853 | 854 | if (flag == UseCurrentSession) and not ( 855 | isinstance(api, APIData) or APIData.__subclasscheck__(api) 856 | ): # nocov 857 | 858 | warnings.warn( # type: ignore 859 | "\nIf you use an existing Telegram Desktop session " 860 | "with unofficial API_ID and API_HASH, " 861 | "Telegram might ban your account because of suspicious activities.\n" 862 | "Please use the default APIs to get rid of this." 863 | ) 864 | 865 | endpoints = account._local.config.endpoints(account.MainDcId) 866 | address = td.MTP.DcOptions.Address.IPv4 867 | protocol = td.MTP.DcOptions.Protocol.Tcp 868 | 869 | # Expects( 870 | # connection == ConnectionTcpFull, 871 | # "Other connection type is not supported yet", 872 | # ) 873 | Expects(len(endpoints[address][protocol]) > 0, "Couldn't find endpoint for this account, something went wrong?") # type: ignore 874 | 875 | endpoint = endpoints[address][protocol][0] # type: ignore 876 | 877 | # If we're gonna create a new session any way, then this session is only 878 | # created to accept the qr login for the new session 879 | if flag == CreateNewSession: 880 | auth_session = MemorySession() 881 | else: 882 | # COPY STRAIGHT FROM TELETHON 883 | # Determine what session object we have 884 | if isinstance(session, str) or session is None: 885 | try: 886 | auth_session = SQLiteSession(session) 887 | except ImportError: 888 | warnings.warn( 889 | "The sqlite3 module is not available under this " 890 | "Python installation and no custom session " 891 | "instance was given; using MemorySession.\n" 892 | "You will need to re-login every time unless " 893 | "you use another session storage" 894 | ) 895 | auth_session = MemorySession() 896 | elif not isinstance(session, Session): 897 | raise TypeError( 898 | "The given session must be a str or a Session instance." 899 | ) 900 | else: # session is instance of Session 901 | auth_session = session 902 | 903 | auth_session.set_dc(endpoint.id, endpoint.ip, endpoint.port) # type: ignore 904 | auth_session.auth_key = AuthKey(account.authKey.key) # type: ignore 905 | 906 | client = TelegramClient(auth_session, api=account.api, **kwargs) 907 | 908 | if flag == UseCurrentSession: 909 | client.UserId = account.UserId 910 | return client 911 | 912 | await client.connect() 913 | Expects( 914 | await client.is_user_authorized(), 915 | TDesktopUnauthorized("TDesktop client is unauthorized"), 916 | ) 917 | 918 | # create new session by qrlogin 919 | return await client.QRLoginToNewClient( 920 | session=session, api=api, password=password, **kwargs 921 | ) 922 | 923 | 924 | def PrettyTable(table: List[Dict[str, Any]], addSplit: List[int] = []): 925 | 926 | # ! Warning: SUPER DIRTY CODE AHEAD 927 | padding = {} 928 | 929 | result = "" 930 | 931 | for label in table[0]: 932 | padding[label] = len(label) 933 | 934 | for row in table: 935 | for label, value in row.items(): 936 | text = str(value) 937 | if padding[label] < len(text): 938 | padding[label] = len(text) 939 | 940 | def addpadding(text: str, spaces: int): 941 | if not isinstance(text, str): 942 | text = text.__str__() 943 | spaceLeft = spaces - len(text) 944 | padLeft = spaceLeft / 2 945 | padLeft = round(padLeft - (padLeft % 1)) 946 | padRight = spaceLeft - padLeft 947 | return padLeft * " " + text + " " * padRight 948 | 949 | header = "|".join( 950 | addpadding(label, spaces + 2) for label, spaces in padding.items() 951 | ) 952 | splitter = "+".join(("-" * (spaces + 2)) for label, spaces in padding.items()) 953 | rows = [] 954 | for row in table: 955 | rows.append( 956 | "|".join( 957 | addpadding(row[label], spaces + 2) for label, spaces in padding.items() 958 | ) 959 | ) 960 | 961 | result += f"|{splitter}|\n" 962 | result += f"|{header}|\n" 963 | result += f"|{splitter}|\n" 964 | 965 | index = 0 966 | for row in rows: 967 | if index in addSplit: 968 | result += f"|{splitter}|\n" 969 | result += f"|{row}|\n" 970 | index += 1 971 | 972 | result += f"|{splitter}|" 973 | 974 | return result 975 | -------------------------------------------------------------------------------- /src/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from . import debug 4 | 5 | from typing import Coroutine, Tuple, Type, Callable, TypeVar, Optional, List, Any, Dict 6 | from types import FunctionType 7 | 8 | import abc 9 | 10 | APP_VERSION = 3004000 11 | TDF_MAGIC = b"TDF$" 12 | 13 | _T = TypeVar("_T") 14 | _TCLS = TypeVar("_TCLS", bound=type) 15 | _RT = TypeVar("_RT") 16 | _F = TypeVar("_F", bound=Callable[..., Any]) 17 | 18 | 19 | class BaseMetaClass(abc.ABCMeta): # pragma: no cover 20 | def __new__( 21 | cls: Type[_T], clsName: str, bases: Tuple[type], attrs: Dict[str, Any] 22 | ) -> _T: 23 | 24 | # Hook all subclass methods 25 | if debug.IS_DEBUG_MODE: # pragma: no cover 26 | ignore_list = [ 27 | "__new__", 28 | "__del__", 29 | "__get__", 30 | "__call__", 31 | "__set_name__", 32 | "__str__", 33 | "__repr__", 34 | ] 35 | 36 | for attr, val in attrs.items(): 37 | if ( 38 | not attr in ignore_list 39 | and callable(val) 40 | and not isinstance(val, type) 41 | ): 42 | newVal = debug.DebugMethod(val) 43 | attrs[attr] = newVal 44 | 45 | result = super().__new__(cls, clsName, bases, attrs) 46 | 47 | return result 48 | 49 | 50 | class BaseObject(object, metaclass=BaseMetaClass): 51 | pass 52 | 53 | 54 | class override(object): # nocov 55 | """ 56 | To use inside a class decorated with @extend_class\n 57 | Any attributes decorated with @override will be replaced 58 | """ 59 | 60 | def __new__(cls, decorated_func: _F) -> _F: 61 | 62 | # check if decorated_cls really is a function 63 | if not isinstance(decorated_func, FunctionType): 64 | raise BaseException( 65 | "@override decorator is only for functions, not classes" 66 | ) 67 | 68 | decorated_func.__isOverride__ = True # type: ignore 69 | return decorated_func # type: ignore 70 | 71 | @staticmethod 72 | def isOverride(func: _F) -> bool: 73 | if not hasattr(func, "__isOverride__"): 74 | return False 75 | return func.__isOverride__ 76 | 77 | 78 | class extend_class(object): # nocov 79 | """ 80 | Extend a class, all attributes will be added to its parents\n 81 | This won't override attributes that are already existed, please refer to @override or @extend_override_class to do this 82 | """ 83 | 84 | def __new__(cls, decorated_cls: _TCLS, isOverride: bool = False) -> _TCLS: 85 | 86 | # check if decorated_cls really is a class (type) 87 | if not isinstance(cls, type): 88 | raise BaseException( 89 | "@extend_class decorator is only for classes, not functions" 90 | ) 91 | 92 | newAttributes = dict(decorated_cls.__dict__) 93 | crossDelete = ["__abstractmethods__", "__module__", "_abc_impl", "__doc__"] 94 | [ 95 | (newAttributes.pop(cross) if cross in newAttributes else None) 96 | for cross in crossDelete 97 | ] 98 | 99 | crossDelete = {} 100 | 101 | base = decorated_cls.__bases__[0] 102 | 103 | if not isOverride: 104 | # loop through its parents and add attributes 105 | 106 | for attributeName, attributeValue in newAttributes.items(): 107 | 108 | # check if class base already has this attribute 109 | result = extend_class.getattr(base, attributeName) 110 | 111 | if result != None: 112 | if id(result["value"]) == id(attributeValue): 113 | crossDelete[attributeName] = attributeValue 114 | else: 115 | 116 | # if not override this attribute 117 | if not override.isOverride(attributeValue): 118 | print( 119 | f"[{attributeName}] {id(result['value'])} - {id(attributeValue)}" 120 | ) 121 | raise BaseException("err") 122 | 123 | [newAttributes.pop(cross) for cross in crossDelete] 124 | 125 | for attributeName, attributeValue in newAttributes.items(): 126 | 127 | # let's backup this attribute for future uses 128 | result = extend_class.getattr(base, attributeName) 129 | 130 | if result != None: 131 | # ! dirty code, gonna fix it later, it's okay for now 132 | setattr( 133 | base, 134 | f"__{decorated_cls.__name__}__{attributeName}", 135 | result["value"], 136 | ) 137 | setattr( 138 | decorated_cls, 139 | f"__{decorated_cls.__name__}__{attributeName}", 140 | result["value"], 141 | ) 142 | 143 | setattr(base, attributeName, attributeValue) 144 | 145 | return decorated_cls 146 | 147 | @staticmethod 148 | def object_hierarchy_getattr(obj: object, attributeName: str) -> List[str]: 149 | 150 | results = [] 151 | if type(obj) == object: 152 | return results 153 | 154 | if attributeName in obj.__dict__: 155 | val = obj.__dict__[attributeName] 156 | results.append({"owner": obj, "value": val}) 157 | 158 | if attributeName in obj.__class__.__dict__: 159 | val = obj.__class__.__dict__[attributeName] 160 | results.append({"owner": obj, "value": val}) 161 | 162 | for base in obj.__bases__: # type: ignore 163 | results += extend_class.object_hierarchy_getattr(base, attributeName) 164 | 165 | results.reverse() 166 | return results 167 | 168 | @staticmethod 169 | def getattr(obj: object, attributeName: str) -> Optional[dict]: 170 | try: 171 | value = getattr(obj, attributeName) 172 | return {"owner": obj, "value": value} 173 | except BaseException as e: 174 | return None 175 | 176 | 177 | class extend_override_class(extend_class): 178 | """ 179 | Extend a class, all attributes will be added to its parents\n 180 | If those attributes are already existed, they will be replaced by the new one 181 | """ 182 | 183 | def __new__(cls, decorated_cls: _TCLS) -> _TCLS: 184 | return super().__new__(cls, decorated_cls, True) 185 | 186 | 187 | class sharemethod(type): 188 | def __get__(self, obj, cls): 189 | self.__owner__ = obj if obj else cls 190 | return self 191 | 192 | def __call__(self, *args) -> Any: 193 | return self.__fget__.__get__(self.__owner__)(*args) # type: ignore 194 | 195 | def __set_name__(self, owner, name): 196 | self.__owner__ = owner 197 | 198 | def __new__(cls: Type[_T], func: _F) -> Type[_F]: 199 | 200 | clsName = func.__class__.__name__ 201 | bases = func.__class__.__bases__ 202 | attrs = func.__dict__ 203 | # attrs = dict(func.__class__.__dict__) 204 | result = super().__new__(cls, clsName, bases, attrs) 205 | result.__fget__ = func 206 | 207 | return result 208 | --------------------------------------------------------------------------------