From 55ca3f7ed090bebec0fd979f6dec3fc27cfc12a2 Mon Sep 17 00:00:00 2001 From: trojvn Date: Sun, 25 Jan 2026 22:48:13 +0300 Subject: [PATCH] initial commit --- .gitignore | 86 +++++++++++++++++++ README.md | 0 pyproject.toml | 22 +++++ thon/__init__.py | 35 ++++++++ thon/converter.py | 80 +++++++++++++++++ thon/data.py | 113 ++++++++++++++++++++++++ thon/exceptions.py | 13 +++ thon/models.py | 0 thon/models/__init__.py | 10 +++ thon/models/misc.py | 11 +++ thon/models/options.py | 12 +++ thon/models/proxy.py | 14 +++ thon/models/session.py | 9 ++ thon/process.py | 185 ++++++++++++++++++++++++++++++++++++++++ thon/proxy.py | 99 +++++++++++++++++++++ thon/session.py | 30 +++++++ thon/utils.py | 40 +++++++++ uv.lock | 89 +++++++++++++++++++ 18 files changed, 848 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 thon/__init__.py create mode 100644 thon/converter.py create mode 100644 thon/data.py create mode 100644 thon/exceptions.py create mode 100644 thon/models.py create mode 100644 thon/models/__init__.py create mode 100644 thon/models/misc.py create mode 100644 thon/models/options.py create mode 100644 thon/models/proxy.py create mode 100644 thon/models/session.py create mode 100644 thon/process.py create mode 100644 thon/proxy.py create mode 100644 thon/session.py create mode 100644 thon/utils.py create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd716f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,86 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv +*.py[cod] +*$py.class +*.so +.Python +develop-eggs/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +*.manifest +*.spec +pip-log.txt +pip-delete-this-directory.txt +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ +*.mo +*.pot +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +instance/ +.webassets-cache +.scrapy +docs/_build/ +.pybuilder/ +target/ +.ipynb_checkpoints +profile_default/ +ipython_config.py +.pdm.toml +.pdm-python +.pdm-build/ +__pypackages__/ +celerybeat-schedule +celerybeat.pid +*.sage.py +.env +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.spyderproject +.spyproject +.ropeproject +/site +.mypy_cache/ +.dmypy.json +dmypy.json +.pyre/ +.pytype/ +cython_debug/ + +.DS_Store +.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..85e747f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "thon" +version = "0.1.0" +description = "Thon helper" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "aiofiles>=25.1.0", + "async-timeout>=5.0.1", + "python-socks==2.1.1", + "telethon>=1.42.0", +] + +[project.scripts] +thon = "thon:main" + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["."] diff --git a/thon/__init__.py b/thon/__init__.py new file mode 100644 index 0000000..769a9e6 --- /dev/null +++ b/thon/__init__.py @@ -0,0 +1,35 @@ +from pathlib import Path +from typing import AsyncGenerator, Callable + +from thon.converter import Converter +from thon.models import ThonSession +from thon.models.options import ThonOptions +from thon.process import ProcessThon +from thon.utils import move + + +class Thon: + def __init__( + self, + options: ThonOptions, + callback_error: Callable | None = None, + sessions_folder: Path = Path("sessions"), + move_banned_folder: bool = True, + proxy: str = "", + ): + self.__converter, self.__options = Converter(sessions_folder, proxy), options + self.__move_banned_folder = move_banned_folder + self.__callback_error = callback_error + + async def __aiter__(self) -> AsyncGenerator[ThonSession, None]: + async for session in self.__converter: + async with ProcessThon(session, self.__options) as thon: + if isinstance(thon, str): + if "Banned" in thon and self.__move_banned_folder: + await move(session.item) + await move(session.json_file) + if self.__callback_error is not None: + self.__callback_error() + continue + continue + yield ThonSession(session, thon) diff --git a/thon/converter.py b/thon/converter.py new file mode 100644 index 0000000..1dd00f4 --- /dev/null +++ b/thon/converter.py @@ -0,0 +1,80 @@ +import asyncio +import contextlib +from pathlib import Path +from typing import AsyncGenerator + +from telethon import TelegramClient +from telethon.sessions import StringSession + +from thon.models import Session +from thon.proxy import ProxyParser +from thon.session import ThonSession +from thon.utils import json_write + + +class Converter: + def __init__(self, sessions_folder: Path = Path("sessions"), proxy: str = ""): + """ + Конвертер сессий в json + sessions_folder: директория с сессиями + proxy: прокси для подключения формат: http:ip:port:user:pswd + если proxy "debug", то прокси не используется + """ + self.__thon_session = ThonSession(sessions_folder) + self.__api_id, self.__api_hash = 2040, "b18441a1ff607e10a989891a5462e627" + try: + self.__is_debug = proxy == "debug" + proxy_parser = ProxyParser(proxy) + self.__proxy = proxy_parser.thon + except Exception as e: + if not self.__is_debug: + raise e + self.__proxy = {} + + async def _main(self, session: Session) -> Session: + """ + Конвертация сессии в json (string_session) + Возвращает объект Session + """ + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + client = TelegramClient(str(session.item), self.__api_id, self.__api_hash) + ss = StringSession() + ss._server_address = client.session.server_address # type: ignore + ss._takeout_id = client.session.takeout_id # type: ignore + ss._auth_key = client.session.auth_key # type: ignore + ss._dc_id = client.session.dc_id # type: ignore + ss._port = client.session.port # type: ignore + string_session = ss.save() + with contextlib.suppress(Exception): + await client.disconnect() # type: ignore # ty:ignore[unused-ignore-comment] + del client, ss + loop.close() + session.json_data["proxy"] = self.__proxy + session.json_data["string_session"] = string_session + await json_write(session.json_file, session.json_data) + if not self.__proxy and self.__is_debug: + session.json_data["proxy"] = "debug" + return session + + async def __aiter__(self) -> AsyncGenerator[Session, None]: + """ + Поиск сессий в sessions_folder + конвертация сессии в json (string_session) + Возвращает генератор с объектами Session + json_data содержит: + string_session: str + proxy: dict + """ + async for session in self.__thon_session: + yield await self._main(session) + + async def main(self) -> int: + """ + Основная функция для записи сессий в json файлы + Возвращает количество записанных сессий + """ + count = 0 + async for session in self.__thon_session: + await self._main(session) + count += 1 + return count diff --git a/thon/data.py b/thon/data.py new file mode 100644 index 0000000..a5ab08e --- /dev/null +++ b/thon/data.py @@ -0,0 +1,113 @@ +from telethon.sessions import StringSession + +from thon.exceptions import JsonThonError + + +class Data: + def __init__(self, json_data: dict): + self.__json_data = json_data + + @property + def json_data(self) -> dict: + return self.__json_data + + @property + def session_file(self) -> str: + if not (session_file := self.json_data.get("session_file")): + return "" + return session_file + + @property + def string_session(self) -> StringSession: + if not (string_session := self.json_data.get("string_session")): + return StringSession() + return StringSession(string_session) + + @property + def app_id(self) -> int: + """Api Id""" + if api_id := self.json_data.get("api_id"): + return api_id + if not (app_id := self.json_data.get("app_id")): + raise JsonThonError("AppId not found / Не найден AppId", self.session_file) + return app_id + + @property + def app_hash(self) -> str: + """Api Hash""" + if api_hash := self.json_data.get("api_hash"): + return api_hash + if not (app_hash := self.json_data.get("app_hash")): + raise JsonThonError( + "AppHash not found / Не найден AppHash", self.session_file + ) + return app_hash + + @property + def device(self) -> str: + """Device Model""" + if device_model := self.json_data.get("device_model"): + return device_model + if not (device := self.json_data.get("device")): + raise JsonThonError( + "Device Model not found / Не найден Device Model", self.session_file + ) + return device + + @property + def sdk(self) -> str: + """System Version""" + if system_version := self.json_data.get("system_version"): + return system_version + if not (sdk := self.json_data.get("sdk")): + raise JsonThonError( + "System Version not found / Не найден System Version", self.session_file + ) + return sdk + + @property + def app_version(self) -> str: + """App Version""" + if not (app_version := self.json_data.get("app_version")): + raise JsonThonError( + "App Version not found / Не найден App Version", self.session_file + ) + return app_version + + @property + def lang_pack(self) -> str: + """Lang Pack""" + if lang_code := self.json_data.get("lang_code"): + return lang_code + return self.json_data.get("lang_pack", "en") + + @property + def system_lang_code(self) -> str: + """System Lang Code""" + if system_lang_code := self.json_data.get("system_lang_code"): + return system_lang_code + return self.json_data.get("system_lang_pack", "en-us") + + @property + def twostep(self) -> str | None: + """2FA""" + if password := self.json_data.get("password"): + return password + if twofa := self.json_data.get("twoFA"): + return twofa + if twostep := self.json_data.get("twostep"): + return twostep + + @property + def proxy(self) -> dict | tuple: + if not (proxy := self.json_data.get("proxy")): + raise JsonThonError("Proxy not found / Не найден Proxy", self.session_file) + if proxy == "debug": + return {} + return proxy + + @property + def tz_offset(self) -> int: + if tz_offset := self.json_data.get("tz_offset", 0): + return tz_offset + return 0 diff --git a/thon/exceptions.py b/thon/exceptions.py new file mode 100644 index 0000000..95548a5 --- /dev/null +++ b/thon/exceptions.py @@ -0,0 +1,13 @@ +from telethon.errors import UserDeactivatedBanError, UserDeactivatedError + +ThonBannedError = (UserDeactivatedError, UserDeactivatedBanError) + + +class JsonThonError(Exception): + def __init__(self, message: str, name: str): + super().__init__(f"{name} | JsonThonError | Ошибка парсинга JSON -> {message}") + + +class ParseProxyError(Exception): + def __init__(self, message: str): + super().__init__(f"ParseProxyError | Ошибка парсинга прокси -> {message}") diff --git a/thon/models.py b/thon/models.py new file mode 100644 index 0000000..e69de29 diff --git a/thon/models/__init__.py b/thon/models/__init__.py new file mode 100644 index 0000000..a8a1215 --- /dev/null +++ b/thon/models/__init__.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from thon.models.session import Session +from thon.process import ProcessThon + + +@dataclass +class ThonSession: + session: Session + thon: ProcessThon diff --git a/thon/models/misc.py b/thon/models/misc.py new file mode 100644 index 0000000..e5bab13 --- /dev/null +++ b/thon/models/misc.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass + + +@dataclass +class ThonSearchCodeOptions: + limit: int = 1 + date_delta: int = 80 + wait_time: int = 300 + entity: int | str = 777000 + regexp: str = r"\b\d{5,6}\b" + sleep_time: tuple[int, int] = (20, 30) diff --git a/thon/models/options.py b/thon/models/options.py new file mode 100644 index 0000000..dd21f55 --- /dev/null +++ b/thon/models/options.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass + + +@dataclass +class ThonOptions: + retries: int = 10 + timeout: int = 10 + check_timeout: int | None = 0 + + def __post_init__(self): + if self.check_timeout == 0: + self.check_timeout = self.retries * self.timeout diff --git a/thon/models/proxy.py b/thon/models/proxy.py new file mode 100644 index 0000000..0efda89 --- /dev/null +++ b/thon/models/proxy.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass +from typing import Literal + +TYPE = Literal["http", "socks5"] + + +@dataclass +class ThonProxy: + proxy_type: TYPE | str + addr: str + port: int + username: str | None + password: str | None + rdns: bool = True diff --git a/thon/models/session.py b/thon/models/session.py new file mode 100644 index 0000000..c7c41b2 --- /dev/null +++ b/thon/models/session.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class Session: + item: Path + json_file: Path + json_data: dict diff --git a/thon/process.py b/thon/process.py new file mode 100644 index 0000000..aa37bd3 --- /dev/null +++ b/thon/process.py @@ -0,0 +1,185 @@ +import asyncio +import contextlib +import logging +from datetime import datetime, timedelta, timezone +from random import randint +from typing import Self + +from telethon import TelegramClient +from telethon.tl.functions.account import UpdateStatusRequest +from telethon.tl.functions.help import GetCountriesListRequest, GetNearestDcRequest +from telethon.tl.functions.langpack import GetLangPackRequest +from telethon.tl.types import ( + InputPeerUser, + JsonNumber, + JsonObject, + JsonObjectValue, + JsonString, + User, +) + +from thon.data import Data +from thon.exceptions import ThonBannedError +from thon.models.misc import ThonSearchCodeOptions +from thon.models.options import ThonOptions +from thon.models.session import Session +from thon.utils import extract_verification_code + +API_PACKS = { + 4: "android", + 5: "android", + 6: "android", + 8: "ios", + 9: "macos", + 2834: "macos", + 2040: "tdesktop", + 10840: "ios", + 17349: "tdesktop", + 21724: "android", + 16623: "android", + 2496: "", +} + + +class ProcessThon(Data): + def __init__(self, session: Session, options: ThonOptions): + self.__item = session.item + self.__retries = options.retries + self.__timeout = options.timeout + self._logger = logging.getLogger("thon") + self._async_check_timeout = options.check_timeout + super().__init__(session.json_data) + self.__client = self.__get_client() + self.me: User | InputPeerUser | None = None + + @property + def client(self) -> TelegramClient: + return self.__client + + def __get_client(self) -> TelegramClient: + __session = str(self.__item) if self.__item else self.string_session + proxy = self.proxy + print(proxy) + client = TelegramClient( + session=__session, + api_id=self.app_id, + api_hash=self.app_hash, + device_model=self.device, + app_version=self.app_version, + system_lang_code=self.system_lang_code, + lang_code=self.lang_pack, + connection_retries=self.__retries, + request_retries=self.__retries, + proxy=proxy, + timeout=self.__timeout, + ) + installer = JsonObjectValue("installer", JsonString("com.android.vending")) + if self.app_id in (1, 8): + installer = JsonObjectValue("installer", JsonString("com.apple.AppStore")) + + package = JsonObjectValue("package_id", JsonString("org.telegram.messenger")) + if self.app_id in (1, 8): + package = JsonObjectValue("package_id", JsonString("ph.telegra.Telegraph")) + + perf_cat = JsonObjectValue("perf_cat", JsonNumber(2)) + tz_offset = JsonObjectValue("tz_offset", JsonNumber(self.tz_offset)) + if self.tz_offset: + # noinspection PyProtectedMember + client._init_request.params = JsonObject([tz_offset]) + + if self.app_id in (4, 6): + _list = [installer, package, perf_cat] + if self.tz_offset: + _list.append(tz_offset) + # noinspection PyProtectedMember + client._init_request.params = JsonObject(_list) + + # noinspection PyProtectedMember + client._init_request.lang_pack = API_PACKS.get(self.app_id, "android") + return client + + async def get_additional_data(self): + lang_pack = API_PACKS.get(self.app_id, "") + with contextlib.suppress(Exception): + await self.client(GetLangPackRequest(lang_pack, self.lang_pack)) + with contextlib.suppress(Exception): + await self.client(GetNearestDcRequest()) + with contextlib.suppress(Exception): + await self.client(GetCountriesListRequest(self.lang_pack, 0)) + + async def __check(self) -> str: + try: + await self.client.connect() + if not await self.client.is_user_authorized(): + return f"{self.session_file} | Забанен / Banned" + await self.get_additional_data() + with contextlib.suppress(Exception): + await self.client(UpdateStatusRequest(offline=False)) + self.me = await self.client.get_me() + return "OK" + except ConnectionError: + await self.disconnect() + return f"{self.session_file} | Ошибка подключения / ConnectionError" + except ThonBannedError: + await self.disconnect() + return f"{self.session_file} | Забанен / Banned" + except Exception as e: + await self.disconnect() + self._logger.exception(e) + return f"{self.session_file} | Ошибка авторизации / AuthorizationError: {e}" + + async def check(self) -> str: + if not self._async_check_timeout: + return await self.__check() + try: + return await asyncio.wait_for(self.__check(), self._async_check_timeout) + except asyncio.TimeoutError: + return f"{self.session_file} | Таймаут / Timeout" + + @property + def phone(self) -> str: + if not self.me: + return "" + if isinstance(self.me, User): + return self.me.phone or "" + return "" + + async def search_code(self, options: ThonSearchCodeOptions | None = None) -> str: + options = options or ThonSearchCodeOptions() + end_time = datetime.now() + timedelta(seconds=options.wait_time) + + while datetime.now() < end_time: + async for m in self.client.iter_messages( + entity=options.entity, limit=options.limit + ): + if not m.date: + continue + + cutoff_date = datetime.now(tz=timezone.utc) - timedelta( + seconds=options.date_delta + ) + + if m.date < cutoff_date: + continue + + if code := extract_verification_code(m.text, options.regexp): + return code + + await asyncio.sleep(randint(*options.sleep_time)) + return "" + + async def disconnect(self): + with contextlib.suppress(Exception): + await self.client(UpdateStatusRequest(offline=True)) + with contextlib.suppress(Exception): + await self.client.disconnect() # type: ignore # ty:ignore[unused-ignore-comment] + + async def __aenter__(self) -> Self | str: + r = await self.check() + if r != "OK": + await self.disconnect() + return r + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.disconnect() diff --git a/thon/proxy.py b/thon/proxy.py new file mode 100644 index 0000000..4a42a31 --- /dev/null +++ b/thon/proxy.py @@ -0,0 +1,99 @@ +from dataclasses import asdict + +from thon.exceptions import ParseProxyError +from thon.models.proxy import TYPE, ThonProxy + +URL_PREFIXES = ("http://", "socks5://") +PREFIXES = ("http", "socks5") + + +class ProxyParser: + def __init__(self, proxy: str): + self.__raw_proxy = proxy.replace("https:", "http:") + self.__type: TYPE = "http" + self.__ip: str = "" + self.__port: int = 0 + self.__user: str | None = None + self.__pswd: str | None = None + self.__parse() + + def __parse(self): + parts = self._extract_parts() + + if not parts: + raise ParseProxyError(f"Пустой прокси: {self.__raw_proxy}") + + if parts[0] in PREFIXES: + # noinspection PydanticTypeChecker + self.__type = parts[0] # type: ignore # ty:ignore[unused-ignore-comment] + parts = parts[1:] + else: + self.__type = "http" + + if not parts: + raise ParseProxyError(f"Не найден IP: {self.__raw_proxy}") + self.__ip = parts[0] + + if len(parts) < 2: + raise ParseProxyError("Отсутствует порт?!") + try: + self.__port = int(parts[1]) + except ValueError as e: + raise ParseProxyError(f"Некорректный порт: {self.__raw_proxy}") from e + + if len(parts) > 2: + self.__user = parts[2] + if len(parts) > 3: + self.__pswd = parts[3] + + def _extract_parts(self) -> list[str]: + proxy = self.__raw_proxy + + for prefix in URL_PREFIXES: + if not proxy.startswith(prefix): + continue + + # Remove protocol to handle the rest + # http://user:pass@ip:port -> user:pass@ip:port + rest = proxy[len(prefix) :] + scheme = prefix.replace("://", "") + + if "@" in rest: + creds, endpoint = rest.split("@", maxsplit=1) + # scheme + [ip, port] + [user, pass] + return [scheme, *endpoint.split(":"), *creds.split(":")] + + # http://ip:port -> [http, ip, port] + return [scheme, *rest.split(":")] + + return proxy.split(":") + + @property + def type(self) -> TYPE: + return self.__type + + @property + def ip(self) -> str: + return self.__ip + + @property + def port(self) -> int: + return self.__port + + @property + def user(self) -> str | None: + return self.__user + + @property + def pswd(self) -> str | None: + return self.__pswd + + @property + def url(self) -> str: + if not self.user or not self.pswd: + return f"{self.type}://{self.ip}:{self.port}" + return f"{self.type}://{self.user}:{self.pswd}@{self.ip}:{self.port}" + + @property + def thon(self) -> dict: + return asdict(ThonProxy(self.type, self.ip, self.port, self.user, self.pswd)) diff --git a/thon/session.py b/thon/session.py new file mode 100644 index 0000000..9a68735 --- /dev/null +++ b/thon/session.py @@ -0,0 +1,30 @@ +from pathlib import Path +from typing import AsyncGenerator + +from thon.models import Session +from thon.utils import json_read + + +class ThonSession: + def __init__(self, sessions_folder: Path = Path("sessions")): + __sessions_folder = Path("сессии") + if __sessions_folder.exists(): + sessions_folder = __sessions_folder + self.__sessions_folder = sessions_folder + self.__sessions_folder.mkdir(exist_ok=True) + + async def __aiter__(self) -> AsyncGenerator[Session, None]: + """ + Поиск сессий в sessions_folder + Возвращает генератор с объектами Session + """ + for item in self.__sessions_folder.glob("*.session"): + json_file = item.with_suffix(".json") + + if not json_file.is_file(): + continue + + if not (json_data := await json_read(json_file)): + continue + + yield Session(item, json_file, json_data) diff --git a/thon/utils.py b/thon/utils.py new file mode 100644 index 0000000..f89c110 --- /dev/null +++ b/thon/utils.py @@ -0,0 +1,40 @@ +import asyncio +import json +import os +import re +from pathlib import Path + +import aiofiles + + +async def json_read(path: Path) -> dict: + try: + async with aiofiles.open(path, encoding="utf-8") as f: + return json.loads(await f.read()) + except Exception: + return {} + + +async def json_write(path: Path, data: dict) -> None: + async with aiofiles.open(path, "w", encoding="utf-8") as f: + await f.write(json.dumps(data, ensure_ascii=False, indent=4)) + + +def extract_verification_code(text: str, regexp: str) -> str | None: + if not (match := re.search(regexp, text)): + return + code = match.group().replace(" ", "").replace("-", "").replace("_", "") + return code if code.isdigit() else None + + +async def move(path: Path, move_folder_name: str = "banned"): + _folder = Path(f"sessions/{move_folder_name}") + __folder = Path(f"сессии/{move_folder_name}") + if __folder.exists(): + _folder = __folder + _folder.mkdir(parents=True, exist_ok=True) + loop = asyncio.get_running_loop() + try: + await loop.run_in_executor(None, os.rename, path, _folder / path.name) + except OSError as e: + print(f"{path.name} | Error moving file / Не удалось переместить файл: {e}") diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..1a3596f --- /dev/null +++ b/uv.lock @@ -0,0 +1,89 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "aiofiles" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "pyaes" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/66/2c17bae31c906613795711fc78045c285048168919ace2220daa372c7d72/pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f", size = 28536, upload-time = "2017-09-20T21:17:54.23Z" } + +[[package]] +name = "pyasn1" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, +] + +[[package]] +name = "python-socks" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/54/dc01d98b5eec803d1ce1c213ab1bad4cad41901bc929d93766f31cc1c9f9/python-socks-2.1.1.tar.gz", hash = "sha256:3bb68964c97690d5a3eab6c12a772f415725f295b148cbe1ca8870cb47ebcb96", size = 25118, upload-time = "2022-12-19T12:10:52.758Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/1d/9289f0a5ac4b1c822837adee12a091c88bc3c99b8468c631a3c9fbd7588c/python_socks-2.1.1-py3-none-any.whl", hash = "sha256:6174278b0e005bd36b5d43681a0a3d816922852c9bccc2eceb17c60f4c8d4048", size = 49689, upload-time = "2022-12-19T12:10:51.22Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "telethon" +version = "1.42.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyaes" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/10/8c8c9476bfce767a856d8aaf9eae8ea1869df4e970da16f1c5b638fd1b0c/telethon-1.42.0.tar.gz", hash = "sha256:032e95511261d5ead719f75494c6c85ece2ce71816b54f3c65d6ccc371d6994d", size = 672734, upload-time = "2025-11-05T19:15:19.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/e4/8ce0ff55251381966a7c3f88bd5b34abda79b225a8e7fb51ddef3b849c94/telethon-1.42.0-py3-none-any.whl", hash = "sha256:cf361c94586bcacd6d0fc8959a2bce509d1bb37007fe6476a80c4fb4a2decc29", size = 748466, upload-time = "2025-11-05T19:15:18.241Z" }, +] + +[[package]] +name = "thon" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "aiofiles" }, + { name = "async-timeout" }, + { name = "python-socks" }, + { name = "telethon" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiofiles", specifier = ">=25.1.0" }, + { name = "async-timeout", specifier = ">=5.0.1" }, + { name = "python-socks", specifier = "==2.1.1" }, + { name = "telethon", specifier = ">=1.42.0" }, +]