Files
meeting-protocol-service/backend/app/messengers/telegram/handlers.py
T

277 lines
9.6 KiB
Python

import os
import uuid
from aiogram import Bot, F, Router
from aiogram.filters import Command
from aiogram.types import CallbackQuery, Message
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.config import settings
from app.messengers.telegram.keyboards import (
protocol_actions_keyboard,
recording_list_keyboard,
template_choice_keyboard,
)
from app.models.prompt_template import PromptTemplate
from app.models.protocol import Protocol, ProtocolStatus
from app.models.recording import Recording, RecordingSource, RecordingStatus
from app.models.user import User
from app.services.auth import get_user_by_username, verify_password
from app.services.recording import get_upload_path
from app.tasks.transcription import transcribe_recording
from app.tasks.transcription import SyncSession
router = Router()
_user_sessions: dict[int, uuid.UUID] = {}
_pending_files: dict[int, str] = {}
@router.message(Command("start"))
async def cmd_start(message: Message):
await message.answer(
"Добро пожаловать в Meeting Protocol Bot!\n\n"
"Для авторизации отправьте логин и пароль в формате:\n"
"/login username password"
)
@router.message(Command("login"))
async def cmd_login(message: Message):
parts = (message.text or "").split(maxsplit=2)
if len(parts) < 3:
await message.answer("Формат: /login username password")
return
username, password = parts[1], parts[2]
with SyncSession() as db:
from sqlalchemy import select as sync_select
result = db.execute(sync_select(User).where(User.username == username))
user = result.scalar_one_or_none()
if not user or not verify_password(password, user.password_hash):
await message.answer("❌ Неверный логин или пароль")
return
_user_sessions[message.from_user.id] = user.id
await message.answer(f"✅ Авторизация успешна! Привет, {user.username}.\n\nОтправьте аудио/видео файл для обработки.")
@router.message(Command("list"))
async def cmd_list(message: Message):
user_id = _user_sessions.get(message.from_user.id)
if not user_id:
await message.answer("Сначала авторизуйтесь: /login username password")
return
with SyncSession() as db:
result = db.execute(
select(Recording)
.where(Recording.user_id == user_id)
.order_by(Recording.created_at.desc())
.limit(10)
)
recordings = result.scalars().all()
if not recordings:
await message.answer("У вас пока нет записей.")
return
recs = [{"id": str(r.id), "title": r.title, "status": r.status.value} for r in recordings]
await message.answer("📋 Ваши записи:", reply_markup=recording_list_keyboard(recs))
@router.message(Command("protocols"))
async def cmd_protocols(message: Message):
user_id = _user_sessions.get(message.from_user.id)
if not user_id:
await message.answer("Сначала авторизуйтесь: /login username password")
return
with SyncSession() as db:
result = db.execute(
select(Protocol)
.join(Protocol.transcription)
.join(Recording)
.where(Recording.user_id == user_id)
.order_by(Protocol.created_at.desc())
.limit(10)
)
protocols = result.scalars().all()
if not protocols:
await message.answer("Протоколов пока нет.")
return
text_parts = []
for p in protocols:
status_emoji = {"generating": "⚙️", "done": "", "error": ""}
emoji = status_emoji.get(p.status.value, "")
text_parts.append(f"{emoji} `{str(p.id)[:8]}` — {p.llm_model or 'N/A'}")
await message.answer(
"📋 Ваши протоколы:\n\n" + "\n".join(text_parts) +
"\n\nДля просмотра: /view <id>",
parse_mode="Markdown",
)
@router.message(Command("templates"))
async def cmd_templates(message: Message):
with SyncSession() as db:
result = db.execute(select(PromptTemplate))
templates = result.scalars().all()
text_parts = [f"{t.name}{t.description}" for t in templates]
await message.answer("📐 Доступные шаблоны:\n\n" + "\n".join(text_parts))
@router.message(Command("help"))
async def cmd_help(message: Message):
await message.answer(
"📖 Команды:\n\n"
"/start — приветствие\n"
"/login username password — авторизация\n"
"/list — список записей\n"
"/protocols — список протоколов\n"
"/templates — список шаблонов\n"
"/view <id> — просмотр протокола\n"
"/help — эта справка\n"
"/logout — выход\n\n"
"Или просто отправьте аудио/видео файл!"
)
@router.message(Command("logout"))
async def cmd_logout(message: Message):
_user_sessions.pop(message.from_user.id, None)
await message.answer("Вы вышли из системы.")
@router.message(F.document | F.audio | F.video | F.voice | F.video_note)
async def handle_file(message: Message, bot: Bot):
user_id = _user_sessions.get(message.from_user.id)
if not user_id:
await message.answer("Сначала авторизуйтесь: /login username password")
return
if message.document:
file_obj = message.document
filename = file_obj.file_name or "document"
elif message.audio:
file_obj = message.audio
filename = file_obj.file_name or "audio.mp3"
elif message.video:
file_obj = message.video
filename = f"video_{message.video.file_unique_id}.mp4"
elif message.voice:
file_obj = message.voice
filename = f"voice_{message.voice.file_unique_id}.ogg"
elif message.video_note:
file_obj = message.video_note
filename = f"videonote_{message.video_note.file_unique_id}.mp4"
else:
return
if file_obj.file_size and file_obj.file_size > 50 * 1024 * 1024:
await message.answer(
f"⚠️ Файл слишком большой для Telegram (лимит 50 МБ).\n"
f"Загрузите через веб-интерфейс: {settings.app_url}"
)
return
await message.answer("⏬ Загружаю файл...")
file = await bot.get_file(file_obj.file_id)
file_path = get_upload_path(filename)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
await bot.download_file(file.file_path, file_path)
file_size = os.path.getsize(file_path)
_pending_files[message.from_user.id] = file_path
with SyncSession() as db:
result = db.execute(select(PromptTemplate))
templates = result.scalars().all()
tpls = [{"id": str(t.id), "name": t.name} for t in templates]
await message.answer(
"📁 Файл получен! Выберите тип протокола:",
reply_markup=template_choice_keyboard(tpls),
)
_pending_files[message.from_user.id] = {
"file_path": file_path,
"filename": filename,
"file_size": file_size,
}
@router.callback_query(F.data.startswith("template:"))
async def on_template_selected(callback: CallbackQuery):
user_id = _user_sessions.get(callback.from_user.id)
if not user_id:
await callback.answer("Сначала авторизуйтесь")
return
file_info = _pending_files.pop(callback.from_user.id, None)
if not file_info or not isinstance(file_info, dict):
await callback.answer("Файл не найден, отправьте заново")
return
template_id = callback.data.split(":")[1]
from app.services.recording import get_format_from_filename
fmt = get_format_from_filename(file_info["filename"])
with SyncSession() as db:
recording = Recording(
user_id=user_id,
title=file_info["filename"],
file_path=file_info["file_path"],
file_size=file_info["file_size"],
format=fmt,
source=RecordingSource.telegram,
)
db.add(recording)
db.commit()
db.refresh(recording)
recording_id = recording.id
await callback.message.edit_text("⏳ Принято! Обработка началась...")
transcribe_recording.delay(str(recording_id))
await callback.answer()
@router.callback_query(F.data.startswith("export:"))
async def on_export_requested(callback: CallbackQuery, bot: Bot):
parts = callback.data.split(":")
protocol_id, fmt = parts[1], parts[2]
from app.services.export import export_docx, export_markdown, export_pdf
from app.models.export_file import ExportFormat
with SyncSession() as db:
result = db.execute(select(Protocol).where(Protocol.id == uuid.UUID(protocol_id)))
protocol = result.scalar_one_or_none()
if not protocol:
await callback.answer("Протокол не найден")
return
exporters = {"docx": export_docx, "pdf": export_pdf, "md": export_markdown}
exporter = exporters.get(fmt)
if not exporter:
await callback.answer("Неизвестный формат")
return
file_path = exporter(protocol.content, protocol.id)
from aiogram.types import FSInputFile
await bot.send_document(
callback.message.chat.id,
FSInputFile(file_path, filename=f"protocol.{fmt}"),
)
await callback.answer()