initial commit
This commit is contained in:
86
.gitignore
vendored
Normal file
86
.gitignore
vendored
Normal 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/
|
||||||
22
pyproject.toml
Normal file
22
pyproject.toml
Normal 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
35
thon/__init__.py
Normal 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
80
thon/converter.py
Normal 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
113
thon/data.py
Normal 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
13
thon/exceptions.py
Normal 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
0
thon/models.py
Normal file
10
thon/models/__init__.py
Normal file
10
thon/models/__init__.py
Normal 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
11
thon/models/misc.py
Normal 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
12
thon/models/options.py
Normal 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
14
thon/models/proxy.py
Normal 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
9
thon/models/session.py
Normal 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
185
thon/process.py
Normal 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
99
thon/proxy.py
Normal 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
30
thon/session.py
Normal 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
40
thon/utils.py
Normal 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
89
uv.lock
generated
Normal 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" },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user