feat(api,userbot,frontend): rename session device from the web ui

This commit is contained in:
hh
2026-08-06 00:51:58 +02:00
parent b1848a6620
commit 1898a51a9d
11 changed files with 207 additions and 13 deletions
+22
View File
@@ -9,6 +9,7 @@ from pyrogram.errors import FloodWait, RPCError
from pyrogram.types import User
from api.login import LoginError, login_manager
from userbot import DEVICE_MODEL_LIMIT
from utils.read import accounts
from utils.read.models import AccountView
@@ -35,6 +36,10 @@ class PasswordRequest(BaseModel):
password: str
class DeviceModelRequest(BaseModel):
device_model: str
class LoginState(BaseModel):
login_id: str
stage: Literal["code", "qr", "password", "done"]
@@ -121,6 +126,23 @@ async def cancel_login(login_id: str) -> None:
await login_manager.cancel(login_id)
@router.patch("/accounts/{account_id}/device")
async def rename_device(
account_id: int, body: DeviceModelRequest, pool: FromDishka[asyncpg.Pool]
) -> AccountView:
device_model = " ".join(body.device_model.split())
if not device_model or len(device_model) > DEVICE_MODEL_LIMIT:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
f"Имя устройства: от 1 до {DEVICE_MODEL_LIMIT} символов",
)
account = await accounts.set_device_model(pool, account_id, device_model)
if account is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Аккаунт не найден")
await accounts.notify_accounts_changed(pool)
return account
@router.delete("/accounts/{account_id}", status_code=status.HTTP_204_NO_CONTENT)
async def logout_account(account_id: int, pool: FromDishka[asyncpg.Pool]) -> None:
if await accounts.deactivate_account(pool, account_id) is None:
+2 -2
View File
@@ -1,3 +1,3 @@
from userbot.modules.client import PyroClient
from userbot.modules.client import DEVICE_MODEL, DEVICE_MODEL_LIMIT, PyroClient
__all__ = ["PyroClient"]
__all__ = ["DEVICE_MODEL", "DEVICE_MODEL_LIMIT", "PyroClient"]
+15 -4
View File
@@ -6,19 +6,30 @@ if TYPE_CHECKING:
from userbot.modules.capture import CaptureContext
DEVICE_MODEL = "Beavergram"
DEVICE_MODEL_LIMIT = 32
class PyroClient(Client):
def __init__(
self, name: str, *, workdir: str = "sessions", load_handlers: bool = True
self,
name: str,
*,
workdir: str = "sessions",
device_model: str | None = None,
load_handlers: bool = True,
) -> None:
super().__init__(
name,
workdir=workdir,
api_id=2040,
api_hash="b18441a1ff607e10a989891a5462e627",
device_model="Desktop",
device_model=device_model or DEVICE_MODEL,
system_version="Windows 11 x64",
app_version="6.7.8 x64",
app_version="7.0.8 x64",
lang_pack="tdesktop",
lang_code="en",
system_lang_code="en-US",
client_platform=enums.ClientPlatform.DESKTOP,
)
self.capture: CaptureContext | None = None
@@ -30,4 +41,4 @@ class PyroClient(Client):
self.add_handler(*handler)
__all__ = ["PyroClient"]
__all__ = ["DEVICE_MODEL", "DEVICE_MODEL_LIMIT", "PyroClient"]
+15 -5
View File
@@ -18,6 +18,7 @@ from utils.logging import logger, setup_logging
from utils.read.accounts import (
ACCOUNTS_CHANGED_CHANNEL,
inactive_session_names,
session_device_models,
sync_account,
)
from utils.read.watches import WATCHES_CHANGED_CHANNEL
@@ -30,6 +31,7 @@ setup_logging()
class RunningAccount:
client: PyroClient
consumer_task: asyncio.Task
device_model: str | None
def _sessions_dir() -> Path:
@@ -70,14 +72,20 @@ class AccountRegistry:
async def sync(self) -> None:
async with self._lock:
inactive = await inactive_session_names(self._pool)
models = await session_device_models(self._pool)
present: set[str] = set()
for path in sorted(_sessions_dir().glob("*.session")):
if path.stem in inactive:
await self._log_out(path)
continue
present.add(path.stem)
if path.stem not in self._running:
await self._start(path)
device_model = models.get(path.stem)
running = self._running.get(path.stem)
if running is not None and running.device_model != device_model:
await self._stop(path.stem)
running = None
if running is None:
await self._start(path, device_model)
for session_name in set(self._running) - present:
await self._stop(session_name)
@@ -85,9 +93,11 @@ class AccountRegistry:
for session_name in list(self._running):
await self._stop(session_name)
async def _start(self, path: Path) -> None:
async def _start(self, path: Path, device_model: str | None) -> None:
session_name = path.stem
client = PyroClient(session_name, workdir=str(path.parent))
client = PyroClient(
session_name, workdir=str(path.parent), device_model=device_model
)
try:
await client.start()
me = client.me
@@ -105,7 +115,7 @@ class AccountRegistry:
return
consumer = JobConsumer(client, self._pool, account_id)
self._running[session_name] = RunningAccount(
client, asyncio.create_task(consumer.run())
client, asyncio.create_task(consumer.run()), device_model
)
logger.info(f"[green]Client started:[/] {me.full_name} ({me.id})")
await _enqueue_once(self._pool, account_id, "sync_dialogs")
+21 -1
View File
@@ -7,7 +7,7 @@ from utils.read.models import AccountView
ACCOUNTS_CHANGED_CHANNEL = "accounts_changed"
_ACCOUNT_COLS = "account_id, label, phone, tg_user_id, is_active"
_ACCOUNT_COLS = "account_id, label, phone, tg_user_id, is_active, device_model"
_UPSERT_ACCOUNT = """
INSERT INTO accounts
@@ -23,6 +23,12 @@ ON CONFLICT (tg_user_id) DO UPDATE SET
RETURNING account_id
"""
_SET_DEVICE_MODEL = f"""
UPDATE accounts SET device_model = $2, updated_at = now()
WHERE account_id = $1
RETURNING {_ACCOUNT_COLS}
""" # noqa: S608
async def self_user_id(pool: asyncpg.Pool, account_id: int) -> int | None:
return await pool.fetchval(
@@ -61,6 +67,20 @@ async def sync_account(pool: asyncpg.Pool, me: User, session_name: str) -> int:
)
async def set_device_model(
pool: asyncpg.Pool, account_id: int, device_model: str
) -> AccountView | None:
row = await pool.fetchrow(_SET_DEVICE_MODEL, account_id, device_model)
return AccountView(**dict(row)) if row else None
async def session_device_models(pool: asyncpg.Pool) -> dict[str, str]:
rows = await pool.fetch(
"SELECT session_name, device_model FROM accounts WHERE device_model IS NOT NULL"
)
return {row["session_name"]: row["device_model"] for row in rows}
async def deactivate_account(pool: asyncpg.Pool, account_id: int) -> str | None:
return await pool.fetchval(
"UPDATE accounts SET is_active = FALSE, updated_at = now() "
+1
View File
@@ -22,6 +22,7 @@ class AccountView(BaseModel):
phone: str | None
tg_user_id: int | None
is_active: bool
device_model: str | None
class ChatListItem(BaseModel):