initial commit

This commit is contained in:
2026-01-25 22:48:13 +03:00
commit 55ca3f7ed0
18 changed files with 848 additions and 0 deletions

86
.gitignore vendored Normal file
View File

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

0
README.md Normal file
View File

22
pyproject.toml Normal file
View File

@ -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 = ["."]

35
thon/__init__.py Normal file
View File

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

80
thon/converter.py Normal file
View File

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

113
thon/data.py Normal file
View File

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

13
thon/exceptions.py Normal file
View File

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

0
thon/models.py Normal file
View File

10
thon/models/__init__.py Normal file
View File

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

11
thon/models/misc.py Normal file
View File

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

12
thon/models/options.py Normal file
View File

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

14
thon/models/proxy.py Normal file
View File

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

9
thon/models/session.py Normal file
View File

@ -0,0 +1,9 @@
from dataclasses import dataclass
from pathlib import Path
@dataclass
class Session:
item: Path
json_file: Path
json_data: dict

185
thon/process.py Normal file
View File

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

99
thon/proxy.py Normal file
View File

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

30
thon/session.py Normal file
View File

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

40
thon/utils.py Normal file
View File

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

89
uv.lock generated Normal file
View File

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