922b1b26a6
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
277 lines
9.6 KiB
Python
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()
|