479 lines
21 KiB
Python
479 lines
21 KiB
Python
"""
|
||
Обработчики для работы с ответами на вопросы
|
||
"""
|
||
from datetime import datetime
|
||
from aiogram import Router, F
|
||
from aiogram.types import Message, CallbackQuery
|
||
from aiogram.filters import StateFilter
|
||
from aiogram.fsm.context import FSMContext
|
||
from aiogram.fsm.state import State, StatesGroup
|
||
|
||
from config import config
|
||
from models.question import Question, QuestionStatus
|
||
from services.infrastructure.database import DatabaseService
|
||
from services.auth.auth_new import AuthService
|
||
from services.utils import is_valid_answer_text, format_question_info, send_answer_to_author, escape_html
|
||
from services.infrastructure.logger import get_logger
|
||
from services.infrastructure.metrics import get_metrics_service, track_answer_processing
|
||
from dependencies import inject_answer_services, inject_main_menu_services
|
||
from keyboards.inline import get_question_view_keyboard
|
||
from keyboards.reply import get_main_keyboard_for_user, get_cancel_keyboard
|
||
|
||
logger = get_logger(__name__)
|
||
router = Router()
|
||
|
||
|
||
class AnswerStates(StatesGroup):
|
||
"""Состояния для работы с ответами"""
|
||
waiting_for_answer = State()
|
||
editing_answer = State()
|
||
confirming_delete = State()
|
||
|
||
|
||
@router.callback_query(F.data.startswith("view_question_"))
|
||
async def view_question_callback(callback: CallbackQuery):
|
||
"""Обработчик просмотра конкретного вопроса"""
|
||
try:
|
||
question_id = int(callback.data.split("_")[2])
|
||
|
||
from dependencies import get_database
|
||
db = get_database()
|
||
|
||
# Получаем вопрос
|
||
question = await db.get_question(question_id)
|
||
|
||
if not question:
|
||
await callback.answer("❌ Вопрос не найден", show_alert=True)
|
||
return
|
||
|
||
if question.to_user_id != callback.from_user.id:
|
||
await callback.answer("❌ У вас нет прав на этот вопрос", show_alert=True)
|
||
return
|
||
|
||
# Формируем текст сообщения
|
||
question_text = format_question_info(question, show_answer=True)
|
||
|
||
# Получаем клавиатуру
|
||
keyboard = get_question_view_keyboard(question)
|
||
|
||
# Обновляем сообщение
|
||
await callback.message.edit_text(
|
||
question_text,
|
||
reply_markup=keyboard,
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
await callback.answer()
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при просмотре вопроса: {e}")
|
||
await callback.answer("❌ Произошла ошибка", show_alert=True)
|
||
|
||
|
||
@router.callback_query(F.data.startswith("answer_"))
|
||
async def answer_callback(callback: CallbackQuery, state: FSMContext):
|
||
"""Обработчик создания нового ответа"""
|
||
try:
|
||
logger.info(f"Получен callback для ответа: {callback.data}")
|
||
question_id = int(callback.data.split("_")[1])
|
||
logger.info(f"Извлечен question_id: {question_id}")
|
||
|
||
# Получаем базу данных
|
||
from dependencies import get_database
|
||
db = get_database()
|
||
|
||
# Получаем вопрос
|
||
question = await db.get_question(question_id)
|
||
if not question:
|
||
await callback.answer("❌ Вопрос не найден", show_alert=True)
|
||
return
|
||
|
||
# Проверяем права доступа
|
||
if question.to_user_id != callback.from_user.id:
|
||
await callback.answer("❌ У вас нет прав на этот вопрос", show_alert=True)
|
||
return
|
||
|
||
# Проверяем, что вопрос еще не отвечен
|
||
if question.status == QuestionStatus.ANSWERED:
|
||
await callback.answer("❌ На этот вопрос уже отвечено", show_alert=True)
|
||
return
|
||
|
||
# Устанавливаем состояние ожидания ответа
|
||
logger.info(f"Устанавливаем состояние waiting_for_answer для вопроса {question_id}")
|
||
await state.set_state(AnswerStates.waiting_for_answer)
|
||
await state.update_data(question_id=question_id)
|
||
logger.info(f"Состояние установлено, данные сохранены")
|
||
|
||
# Отправляем сообщение с просьбой ввести ответ
|
||
logger.info(f"Отправляем сообщение с просьбой ввести ответ для вопроса {question_id}")
|
||
await callback.message.edit_text(
|
||
f"✏️ <b>Ответ на вопрос #{question_id}</b>\n\n"
|
||
f"❓ <b>Вопрос:</b>\n{escape_html(question.message_text)}\n\n"
|
||
f"💬 <b>Введите ваш ответ:</b>",
|
||
reply_markup=get_cancel_keyboard(),
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
await callback.answer()
|
||
logger.info(f"Callback обработан успешно для вопроса {question_id}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при создании ответа: {e}")
|
||
await callback.answer("❌ Произошла ошибка", show_alert=True)
|
||
|
||
|
||
@router.callback_query(F.data.startswith("edit_answer_"))
|
||
async def edit_answer_callback(callback: CallbackQuery, state: FSMContext):
|
||
"""Обработчик редактирования ответа"""
|
||
try:
|
||
question_id = int(callback.data.split("_")[2])
|
||
|
||
from dependencies import get_database
|
||
db = get_database()
|
||
|
||
# Получаем вопрос
|
||
question = await db.get_question(question_id)
|
||
|
||
if not question:
|
||
await callback.answer("❌ Вопрос не найден", show_alert=True)
|
||
return
|
||
|
||
if question.to_user_id != callback.from_user.id:
|
||
await callback.answer("❌ У вас нет прав на этот вопрос", show_alert=True)
|
||
return
|
||
|
||
if question.status != QuestionStatus.ANSWERED:
|
||
await callback.answer("❌ На этот вопрос еще не отвечено", show_alert=True)
|
||
return
|
||
|
||
# Устанавливаем состояние редактирования ответа
|
||
await state.set_state(AnswerStates.editing_answer)
|
||
await state.update_data(question_id=question_id, current_answer=question.answer_text)
|
||
|
||
# Отправляем сообщение с просьбой ввести новый ответ
|
||
await callback.message.edit_text(
|
||
f"✏️ <b>Редактирование ответа на вопрос #{question_id}</b>\n\n"
|
||
f"📝 <b>Вопрос:</b>\n{escape_html(question.message_text)}\n\n"
|
||
f"💬 <b>Текущий ответ:</b>\n{escape_html(question.answer_text)}\n\n"
|
||
f"✍️ <b>Введите новый ответ:</b>",
|
||
reply_markup=None,
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
await callback.answer()
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при редактировании ответа: {e}")
|
||
await callback.answer("❌ Произошла ошибка", show_alert=True)
|
||
|
||
|
||
@router.message(StateFilter(AnswerStates.waiting_for_answer))
|
||
@inject_answer_services
|
||
async def process_new_answer(message: Message, state: FSMContext, validator, **kwargs):
|
||
"""Обработка нового ответа"""
|
||
try:
|
||
logger.info(f"Получено сообщение в состоянии waiting_for_answer: {message.text[:50]}...")
|
||
# Получаем данные из состояния
|
||
data = await state.get_data()
|
||
question_id = data.get('question_id')
|
||
logger.info(f"Получен question_id из состояния: {question_id}")
|
||
|
||
if not question_id:
|
||
await message.answer(
|
||
"❌ Ошибка: не найден вопрос для ответа.",
|
||
reply_markup=get_main_keyboard_for_user(message.from_user.id)
|
||
)
|
||
await state.clear()
|
||
return
|
||
|
||
# Валидируем текст ответа
|
||
validation_result = validator.validate_answer_text(
|
||
message.text,
|
||
config.MAX_ANSWER_LENGTH
|
||
)
|
||
|
||
if not validation_result:
|
||
logger.warning(f"⚠️ Невалидный ответ от пользователя {message.from_user.id}: {validation_result.error_message}")
|
||
await message.answer(
|
||
f"❌ {validation_result.error_message}\n\n"
|
||
"Попробуйте отправить ответ еще раз:",
|
||
reply_markup=get_cancel_keyboard()
|
||
)
|
||
return
|
||
|
||
# Используем санитизированный текст
|
||
sanitized_answer_text = validation_result.sanitized_value
|
||
|
||
# Сохраняем ответ в БД
|
||
from dependencies import get_database
|
||
db = get_database()
|
||
|
||
question = await db.get_question(question_id)
|
||
if question:
|
||
question.answer_text = sanitized_answer_text
|
||
question.answered_at = datetime.now()
|
||
question.mark_as_answered() # Устанавливаем статус ANSWERED
|
||
await db.update_question(question)
|
||
|
||
# Отправляем ответ автору вопроса
|
||
logger.info(f"Попытка отправить ответ автору вопроса {question.id}, from_user_id: {question.from_user_id}")
|
||
await send_answer_to_author(message.bot, question, question.answer_text)
|
||
|
||
# Отправляем подтверждение
|
||
await message.answer(
|
||
"✅ <b>Ответ отправлен!</b>\n\n"
|
||
"💬 Ваш ответ был успешно отправлен автору вопроса.\n\n"
|
||
"📋 Используйте 'Мои вопросы' для просмотра всех вопросов и ответов.",
|
||
reply_markup=get_main_keyboard_for_user(message.from_user.id),
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
await state.clear()
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при обработке нового ответа: {e}")
|
||
await message.answer(
|
||
"❌ Произошла ошибка при отправке ответа. Попробуйте позже.",
|
||
reply_markup=get_main_keyboard_for_user(message.from_user.id)
|
||
)
|
||
await state.clear()
|
||
|
||
|
||
@router.message(StateFilter(AnswerStates.editing_answer))
|
||
@inject_answer_services
|
||
async def process_edited_answer(message: Message, state: FSMContext, validator, **kwargs):
|
||
"""Обработка отредактированного ответа"""
|
||
try:
|
||
# Получаем данные из состояния
|
||
data = await state.get_data()
|
||
question_id = data.get('question_id')
|
||
current_answer = data.get('current_answer')
|
||
|
||
if not question_id:
|
||
await message.answer(
|
||
"❌ Ошибка: не найден вопрос для редактирования.",
|
||
reply_markup=get_main_keyboard_for_user(message.from_user.id)
|
||
)
|
||
await state.clear()
|
||
return
|
||
|
||
# Валидируем текст ответа
|
||
validation_result = validator.validate_answer_text(
|
||
message.text,
|
||
config.MAX_ANSWER_LENGTH
|
||
)
|
||
|
||
if not validation_result:
|
||
logger.warning(f"⚠️ Невалидный отредактированный ответ от пользователя {message.from_user.id}: {validation_result.error_message}")
|
||
await message.answer(
|
||
f"❌ {validation_result.error_message}\n\n"
|
||
"Попробуйте отправить ответ еще раз:",
|
||
reply_markup=get_cancel_keyboard()
|
||
)
|
||
return
|
||
|
||
# Используем санитизированный текст
|
||
sanitized_answer_text = validation_result.sanitized_value
|
||
|
||
# Сохраняем отредактированный ответ
|
||
from dependencies import get_database
|
||
db = get_database()
|
||
|
||
question = await db.get_question(question_id)
|
||
if question:
|
||
question.answer_text = sanitized_answer_text
|
||
question.answered_at = datetime.now() # Обновляем время ответа
|
||
await db.update_question(question)
|
||
|
||
# Отправляем ответ автору вопроса
|
||
logger.info(f"Попытка отправить ответ автору вопроса {question.id}, from_user_id: {question.from_user_id}")
|
||
await send_answer_to_author(message.bot, question, question.answer_text)
|
||
|
||
# Отправляем подтверждение
|
||
await message.answer(
|
||
"✅ <b>Ответ обновлен!</b>\n\n"
|
||
"💬 Ваш ответ был успешно обновлен.\n\n"
|
||
"📋 Используйте 'Мои вопросы' для просмотра всех вопросов и ответов.",
|
||
reply_markup=get_main_keyboard_for_user(message.from_user.id),
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
await state.clear()
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при обработке отредактированного ответа: {e}")
|
||
await message.answer(
|
||
"❌ Произошла ошибка при обновлении ответа. Попробуйте позже.",
|
||
reply_markup=get_main_keyboard_for_user(message.from_user.id)
|
||
)
|
||
await state.clear()
|
||
|
||
|
||
@router.callback_query(F.data.startswith("confirm_delete_"))
|
||
async def confirm_delete_callback(callback: CallbackQuery):
|
||
"""Обработчик подтверждения удаления вопроса"""
|
||
try:
|
||
question_id = int(callback.data.split("_")[2])
|
||
|
||
from dependencies import get_database
|
||
db = get_database()
|
||
|
||
# Получаем вопрос
|
||
question = await db.get_question(question_id)
|
||
|
||
if not question:
|
||
await callback.answer("❌ Вопрос не найден", show_alert=True)
|
||
return
|
||
|
||
if question.to_user_id != callback.from_user.id:
|
||
await callback.answer("❌ У вас нет прав на этот вопрос", show_alert=True)
|
||
return
|
||
|
||
# Удаляем вопрос
|
||
question.mark_as_deleted()
|
||
await db.update_question(question)
|
||
|
||
# Обновляем сообщение
|
||
await callback.message.edit_text(
|
||
f"🗑️ <b>Вопрос #{question.user_question_number if question.user_question_number is not None else question_id} удален</b>\n\n"
|
||
f"📅 Удален: {question.answered_at.strftime('%d.%m.%Y %H:%M')}",
|
||
reply_markup=None,
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
await callback.answer("🗑️ Вопрос удален")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при подтверждении удаления: {e}")
|
||
await callback.answer("❌ Произошла ошибка", show_alert=True)
|
||
|
||
|
||
@router.callback_query(F.data.startswith("cancel_delete_"))
|
||
async def cancel_delete_callback(callback: CallbackQuery):
|
||
"""Обработчик отмены удаления вопроса"""
|
||
try:
|
||
question_id = int(callback.data.split("_")[2])
|
||
|
||
from dependencies import get_database
|
||
db = get_database()
|
||
|
||
# Получаем вопрос
|
||
question = await db.get_question(question_id)
|
||
|
||
if not question:
|
||
await callback.answer("❌ Вопрос не найден", show_alert=True)
|
||
return
|
||
|
||
if question.to_user_id != callback.from_user.id:
|
||
await callback.answer("❌ У вас нет прав на этот вопрос", show_alert=True)
|
||
return
|
||
|
||
# Возвращаемся к просмотру вопроса
|
||
question_text = format_question_info(question, show_answer=True)
|
||
keyboard = get_question_view_keyboard(question)
|
||
|
||
await callback.message.edit_text(
|
||
question_text,
|
||
reply_markup=keyboard,
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
await callback.answer("❌ Удаление отменено")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при отмене удаления: {e}")
|
||
await callback.answer("❌ Произошла ошибка", show_alert=True)
|
||
|
||
|
||
@router.callback_query(F.data == "back_to_questions")
|
||
async def back_to_questions_callback(callback: CallbackQuery):
|
||
"""Обработчик кнопки 'Назад к списку'"""
|
||
try:
|
||
from dependencies import get_database
|
||
db = get_database()
|
||
|
||
# Получаем вопросы пользователя
|
||
questions = await db.get_user_questions(callback.from_user.id, limit=10)
|
||
|
||
if not questions:
|
||
await callback.message.edit_text(
|
||
"📭 У вас пока нет вопросов.\n\n"
|
||
"🔗 Поделитесь своей ссылкой, чтобы получать анонимные вопросы!",
|
||
reply_markup=None
|
||
)
|
||
else:
|
||
questions_text = f"📋 <b>Ваши вопросы ({len(questions)}):</b>\n\n"
|
||
|
||
for i, question in enumerate(questions, 1):
|
||
status_emoji = {
|
||
'pending': '⏳',
|
||
'answered': '✅',
|
||
'rejected': '❌',
|
||
'deleted': '🗑️'
|
||
}
|
||
|
||
emoji = status_emoji.get(question.status.value, '❓')
|
||
preview = question.get_question_preview(50)
|
||
|
||
# Используем user_question_number для отображения, если он есть
|
||
display_number = question.user_question_number if question.user_question_number is not None else i
|
||
questions_text += f"{i}. {emoji} <b>#{display_number}</b>\n"
|
||
questions_text += f" {preview}\n"
|
||
questions_text += f" 📅 {question.created_at.strftime('%d.%m.%Y %H:%M')}\n\n"
|
||
|
||
questions_text += "💡 Нажмите на номер вопроса для просмотра деталей."
|
||
|
||
await callback.message.edit_text(
|
||
questions_text,
|
||
reply_markup=None,
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
await callback.answer()
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при возврате к списку вопросов: {e}")
|
||
await callback.answer("❌ Произошла ошибка", show_alert=True)
|
||
|
||
|
||
@router.callback_query(
|
||
F.data == "back_to_main",
|
||
)
|
||
@inject_main_menu_services
|
||
async def back_to_main_callback(
|
||
callback: CallbackQuery,
|
||
auth: AuthService,
|
||
**kwargs
|
||
):
|
||
"""Обработчик кнопки 'Назад' в главное меню"""
|
||
try:
|
||
# Используем инжектированную систему авторизации
|
||
is_admin = auth.is_admin(callback.from_user.id)
|
||
|
||
if is_admin:
|
||
from keyboards.inline import get_admin_keyboard
|
||
keyboard = get_admin_keyboard()
|
||
text = "🏠 <b>Главное меню (Админ)</b>\n\nВыберите действие:"
|
||
else:
|
||
keyboard = get_main_keyboard_for_user(callback.from_user.id)
|
||
text = "🏠 <b>Главное меню</b>\n\nВыберите действие:"
|
||
|
||
await callback.message.edit_text(
|
||
text,
|
||
reply_markup=None,
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
# Отправляем новое сообщение с клавиатурой
|
||
await callback.message.answer(
|
||
text,
|
||
reply_markup=keyboard,
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
await callback.answer()
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при возврате в главное меню: {e}")
|
||
await callback.answer("❌ Произошла ошибка", show_alert=True)
|