feat(api,userbot,frontend): rename session device from the web ui
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
"""account device model
|
||||
|
||||
Revision ID: b9e4d1a70c26
|
||||
Revises: e7b4c2a9f861
|
||||
Create Date: 2026-08-06 01:20:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "b9e4d1a70c26"
|
||||
down_revision: str | None = "e7b4c2a9f861"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute("ALTER TABLE accounts ADD COLUMN device_model text")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("ALTER TABLE accounts DROP COLUMN device_model")
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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() "
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user