feat(api,userbot,frontend): add accounts from the web ui and isolate per-account settings
This commit is contained in:
1 parent
92fd20137e
commit
9c265af3d3
19 files changed
+917
-268
No files matched your search
@@ -0,0 +1,108 @@
|
||||
import contextlib
|
||||
import secrets
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from pyrogram.errors import SessionPasswordNeeded
|
||||
from pyrogram.types import User
|
||||
|
||||
from userbot import PyroClient
|
||||
from utils.env import env
|
||||
|
||||
LOGIN_TTL_SECONDS = 900
|
||||
PENDING_DIRNAME = "pending"
|
||||
|
||||
|
||||
class LoginError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class PendingLogin:
|
||||
client: PyroClient
|
||||
phone: str
|
||||
phone_code_hash: str
|
||||
started_at: float
|
||||
|
||||
|
||||
def _sessions_dir() -> Path:
|
||||
path = Path(env.tg.sessions_dir)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def _pending_dir() -> Path:
|
||||
path = _sessions_dir() / PENDING_DIRNAME
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
class LoginManager:
|
||||
def __init__(self) -> None:
|
||||
self._logins: dict[str, PendingLogin] = {}
|
||||
|
||||
def _get(self, login_id: str) -> PendingLogin:
|
||||
login = self._logins.get(login_id)
|
||||
if login is None:
|
||||
msg = "Сессия входа истекла, начните заново"
|
||||
raise LoginError(msg)
|
||||
return login
|
||||
|
||||
async def start(self, phone: str) -> str:
|
||||
await self._sweep()
|
||||
login_id = secrets.token_hex(8)
|
||||
client = PyroClient(login_id, workdir=str(_pending_dir()), load_handlers=False)
|
||||
await client.connect()
|
||||
try:
|
||||
sent = await client.send_code(phone)
|
||||
except Exception:
|
||||
await self._discard(login_id, client)
|
||||
raise
|
||||
self._logins[login_id] = PendingLogin(
|
||||
client, phone, sent.phone_code_hash, time.monotonic()
|
||||
)
|
||||
return login_id
|
||||
|
||||
async def submit_code(self, login_id: str, code: str) -> User | None:
|
||||
login = self._get(login_id)
|
||||
try:
|
||||
user = await login.client.sign_in(login.phone, login.phone_code_hash, code)
|
||||
except SessionPasswordNeeded:
|
||||
return None
|
||||
if not isinstance(user, User):
|
||||
await self.cancel(login_id)
|
||||
msg = "Этот номер не зарегистрирован в Telegram"
|
||||
raise LoginError(msg)
|
||||
return user
|
||||
|
||||
async def submit_password(self, login_id: str, password: str) -> User:
|
||||
return await self._get(login_id).client.check_password(password)
|
||||
|
||||
async def finalize(self, login_id: str, session_name: str) -> None:
|
||||
login = self._logins.pop(login_id)
|
||||
await login.client.disconnect()
|
||||
source = _pending_dir() / f"{login_id}.session"
|
||||
source.replace(_sessions_dir() / f"{session_name}.session")
|
||||
|
||||
async def cancel(self, login_id: str) -> None:
|
||||
login = self._logins.pop(login_id, None)
|
||||
if login is not None:
|
||||
await self._discard(login_id, login.client)
|
||||
|
||||
async def _discard(self, login_id: str, client: PyroClient) -> None:
|
||||
with contextlib.suppress(Exception):
|
||||
await client.disconnect()
|
||||
(_pending_dir() / f"{login_id}.session").unlink(missing_ok=True)
|
||||
|
||||
async def _sweep(self) -> None:
|
||||
now = time.monotonic()
|
||||
for login_id, login in list(self._logins.items()):
|
||||
if now - login.started_at > LOGIN_TTL_SECONDS:
|
||||
await self.cancel(login_id)
|
||||
for path in _pending_dir().glob("*.session"):
|
||||
if path.stem not in self._logins:
|
||||
path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
login_manager = LoginManager()
|
||||
Reference in new issue
Block a user