"""Telegram: статус, веб-авторизация, диалоги и мониторинг (п.4.2, 4.3 ТЗ).""" from __future__ import annotations import asyncio from fastapi import APIRouter, Depends, HTTPException, Response from pydantic import BaseModel from ..auth import current_login from ..services.telegram import tg router = APIRouter(prefix="/api/tg", tags=["telegram"]) def _qr_svg(url: str) -> str: """Реальный QR-код (SVG, без внешних растровых зависимостей).""" import io import qrcode import qrcode.image.svg qr = qrcode.QRCode(version=None, box_size=12, border=1, error_correction=qrcode.constants.ERROR_CORRECT_M) qr.add_data(url) qr.make(fit=True) buf = io.StringIO() qr.make_image(image_factory=qrcode.image.svg.SvgPathImage).save(buf) return buf.getvalue() @router.get("/qr-image") def qr_image(_: str = Depends(current_login)) -> Response: """SVG-картинка QR для сканирования (фаза входа 'qr').""" if tg.phase != "qr" or not tg.qr_url: raise HTTPException(404, "QR не активен — начните вход по QR") return Response( _qr_svg(tg.qr_url), media_type="image/svg+xml", headers={"Cache-Control": "no-store", "Content-Disposition": "inline"}, ) class PhoneBody(BaseModel): phone: str class CodeBody(BaseModel): code: str class PasswordBody(BaseModel): password: str class MonitorBody(BaseModel): enabled: bool class PreviewBody(BaseModel): dialogId: str limit: int = 24 @router.get("/status") def status(_: str = Depends(current_login)) -> dict: return tg.status() @router.post("/start-phone") async def start_phone(body: PhoneBody, _: str = Depends(current_login)) -> dict: try: await tg.start_phone(body.phone.strip()) except Exception as exc: # noqa: BLE001 raise HTTPException(400, tg.error or str(exc)) from exc return {"phase": tg.phase} @router.post("/start-qr") async def start_qr(_: str = Depends(current_login)) -> dict: try: url = await tg.qr_start() except Exception as exc: # noqa: BLE001 raise HTTPException(400, tg.error or str(exc)) from exc return {"phase": tg.phase, "qrUrl": url} @router.post("/send-code") async def send_code(body: CodeBody, _: str = Depends(current_login)) -> dict: try: await tg.submit_code(body.code.strip()) except ValueError as exc: raise HTTPException(400, str(exc)) from exc except Exception as exc: # noqa: BLE001 raise HTTPException(400, tg.error or str(exc)) from exc return {"phase": tg.phase} @router.post("/send-password") async def send_password(body: PasswordBody, _: str = Depends(current_login)) -> dict: try: await tg.submit_password(body.password) except ValueError as exc: raise HTTPException(400, str(exc)) from exc return {"phase": tg.phase} @router.post("/logout") async def logout(_: str = Depends(current_login)) -> dict: await tg.disconnect() return {"ok": True} @router.get("/dialogs") def dialogs(_: str = Depends(current_login)) -> dict: return {"items": tg.list_dialogs()} @router.post("/dialogs/refresh") async def dialogs_refresh(_: str = Depends(current_login)) -> dict: if not tg.client or not tg.client.is_connected(): return {"ok": False, "reason": "not-connected", "count": 0} count = await asyncio.wait_for(tg.refresh_dialogs(), timeout=60) return {"ok": True, "count": count} @router.post("/dialogs/monitor-all") def monitor_all(body: MonitorBody, _: str = Depends(current_login)) -> dict: """Включить/выключить мониторинг сразу для всех каналов.""" count = tg.set_monitor_all(body.enabled) return {"ok": True, "count": count, "enabled": body.enabled} @router.post("/dialogs/backfill-all") def backfill_all(_: str = Depends(current_login)) -> dict: """Перечитать последние 10 сообщений всех включённых каналов (кнопка).""" count = tg.backfill_monitored() return {"ok": True, "count": count} @router.post("/dialogs/{dialog_id}/monitor") def set_monitor(dialog_id: str, body: MonitorBody, _: str = Depends(current_login)) -> dict: tg.set_monitor(dialog_id, body.enabled) return {"ok": True, "enabled": body.enabled} @router.post("/dialogs/{dialog_id}/backfill") async def backfill(dialog_id: str, _: str = Depends(current_login)) -> dict: processed = await tg.backfill_dialog(dialog_id) return {"ok": True, "processed": processed} @router.post("/dialogs/preview") async def preview(body: PreviewBody, _: str = Depends(current_login)) -> dict: items = await tg.dialog_messages(body.dialogId, min(max(body.limit, 1), 50)) return {"items": items}