Implement user-specific question numbering and update database schema. Added triggers for automatic question numbering and adjustments upon deletion. Enhanced CRUD operations to manage user_question_number effectively.

This commit is contained in:
2025-09-06 18:35:12 +03:00
parent 50be010026
commit 596a2fa813
111 changed files with 16847 additions and 65 deletions

7
handlers/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
"""
Обработчики для бота анонимных вопросов
"""
from . import start, questions, answers, admin, errors
__all__ = ['start', 'questions', 'answers', 'admin', 'errors']

660
handlers/admin.py Normal file
View File

@@ -0,0 +1,660 @@
"""
Обработчики для администраторов
"""
from datetime import datetime
from aiogram import F, Router
from aiogram.filters import Command
from aiogram.types import CallbackQuery, Message
from config import config
from config.constants import DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, MIN_PAGE_NUMBER, PERCENTAGE_DECIMAL_PLACES, TIME_DECIMAL_PLACES
from keyboards.inline import (
get_admin_keyboard, get_ban_confirm_keyboard, get_ban_duration_keyboard,
get_ban_user_keyboard, get_rate_limit_keyboard, get_stats_keyboard,
get_superuser_assignment_keyboard, get_superuser_confirm_keyboard, get_unban_keyboard
)
from services.auth.auth_new import AuthService
from services.infrastructure.database import DatabaseService
from services.infrastructure.logger import get_logger
from services.business.message_service import MessageService
from services.business.pagination_service import PaginationService
from services.permissions.decorators import require_admin_or_superuser, require_permission
from services.rate_limiting.rate_limit_service import RateLimitService
from services.business.user_service import UserService
from services.utils import format_stats
from dependencies import inject_admin_services
logger = get_logger(__name__)
router = Router()
async def _format_users_list(
users: list,
page: int,
per_page: int,
pagination_service: PaginationService
) -> str:
"""Форматирование списка пользователей для отображения"""
try:
# Рассчитываем пагинацию
page_users, total_users, current_page, total_pages = pagination_service.calculate_pagination(
users, page, per_page
)
# Формируем информацию о пагинации
start_idx = current_page * per_page
end_idx = min(start_idx + per_page, total_users)
pagination_info = pagination_service.format_pagination_info(
current_page, total_pages, start_idx, end_idx, total_users
)
# Формируем текст сообщения
users_text = f"🔍 <b>Управление суперпользователями</b>\n\n"
users_text += pagination_info
# Добавляем информацию о пользователях
for i, user in enumerate(page_users, start_idx + 1):
status_emoji = "🔍" if user.is_superuser else "👤"
users_text += f"{i}. {status_emoji} {user.display_name}\n"
if user.username:
users_text += f" @{user.username}\n"
created_at_str = user.created_at.strftime('%d.%m.%Y') if user.created_at else 'Неизвестно'
users_text += f" 📅 {created_at_str}\n\n"
users_text += "💡 Нажмите на пользователя для изменения его статуса."
return users_text
except Exception as e:
logger.error(f"Ошибка при форматировании списка пользователей: {e}")
return "❌ Ошибка при форматировании списка пользователей."
# Функция is_admin теперь импортируется из services.auth
@router.message(Command("stats"))
@require_permission("view_stats", "У вас нет прав для выполнения этой команды.")
async def cmd_stats(message: Message, database: DatabaseService = None):
"""Обработчик команды /stats"""
await show_stats(message, database)
@router.message(F.text == "👑 Админ панель")
@require_permission("admin_panel", "У вас нет прав для доступа к админ панели.")
async def admin_panel_button(message: Message):
"""Обработчик кнопки 'Админ панель'"""
admin_text = "👑 <b>Админ панель</b>\n\n"
admin_text += "Выберите действие:"
await message.answer(
admin_text,
reply_markup=get_admin_keyboard(),
parse_mode="HTML"
)
@router.message(F.text == "📊 Статистика")
@require_permission("view_stats", "У вас нет прав для просмотра статистики.")
async def stats_button(message: Message, database: DatabaseService = None):
"""Обработчик кнопки 'Статистика'"""
await show_stats(message, database)
async def show_stats(message: Message, database: DatabaseService = None):
"""Показать статистику"""
try:
if not database:
from dependencies import get_database
database = get_database()
# Получаем статистику
users_stats = await database.get_users_stats()
questions_stats = await database.get_questions_stats()
# Объединяем статистику
all_stats = {**users_stats, **questions_stats}
# Форматируем и отправляем
stats_text = format_stats(all_stats)
await message.answer(
stats_text,
reply_markup=get_stats_keyboard(),
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Ошибка при получении статистики: {e}")
await message.answer(
"❌ Произошла ошибка при получении статистики. Попробуйте позже."
)
@router.message(F.text == "📢 Рассылка")
@require_permission("broadcast", "У вас нет прав для рассылки.")
async def broadcast_button(message: Message):
"""Обработчик кнопки 'Рассылка'"""
await message.answer(
"📢 <b>Рассылка</b>\n\n"
"Функция рассылки будет добавлена в следующих версиях.\n\n"
"💡 Планируемые возможности:\n"
"• Рассылка сообщений всем пользователям\n"
"• Рассылка по группам пользователей\n"
"• Планирование рассылок\n"
"• Статистика доставки",
reply_markup=get_admin_keyboard(),
parse_mode="HTML"
)
@router.message(F.text == "⚙️ Настройки")
@require_permission("admin_panel", "У вас нет прав для изменения настроек.")
async def settings_button(message: Message):
"""Обработчик кнопки 'Настройки'"""
settings_text = "⚙️ <b>Настройки бота</b>\n\n"
settings_text += f"🔧 <b>Текущие настройки:</b>\n"
settings_text += f"• Режим отладки: {'Включен' if config.DEBUG else 'Выключен'}\n"
settings_text += f"• Макс. длина вопроса: {config.MAX_QUESTION_LENGTH} символов\n"
settings_text += f"• Макс. длина ответа: {config.MAX_ANSWER_LENGTH} символов\n"
settings_text += f"• Путь к БД: {config.DATABASE_PATH}\n\n"
settings_text += f"👑 <b>Администраторы:</b>\n"
for admin_id in config.ADMINS:
settings_text += f"{admin_id}\n"
settings_text += "\n💡 Настройки изменяются в файле .env"
await message.answer(
settings_text,
reply_markup=get_admin_keyboard(),
parse_mode="HTML"
)
@router.callback_query(F.data == "admin_stats")
@require_permission("view_stats", "У вас нет прав")
async def admin_stats_callback(callback: CallbackQuery, database: DatabaseService = None):
"""Обработчик кнопки 'Статистика' в админ панели"""
await show_stats(callback.message, database)
await callback.answer()
@router.callback_query(F.data == "admin_broadcast")
@require_permission("broadcast", "У вас нет прав")
async def admin_broadcast_callback(callback: CallbackQuery):
"""Обработчик кнопки 'Рассылка' в админ панели"""
await callback.message.edit_text(
"📢 <b>Рассылка</b>\n\n"
"Функция рассылки будет добавлена в следующих версиях.\n\n"
"💡 Планируемые возможности:\n"
"• Рассылка сообщений всем пользователям\n"
"• Рассылка по группам пользователей\n"
"• Планирование рассылок\n"
"• Статистика доставки",
reply_markup=get_admin_keyboard(),
parse_mode="HTML"
)
await callback.answer()
@router.callback_query(F.data == "stats_general")
@require_permission("view_stats", "У вас нет прав")
async def stats_general_callback(callback: CallbackQuery, database: DatabaseService = None):
"""Обработчик кнопки 'Общая статистика'"""
await show_stats(callback.message, database)
await callback.answer()
@router.callback_query(F.data == "back_to_admin")
@require_permission("admin_panel", "У вас нет прав")
async def back_to_admin_callback(callback: CallbackQuery):
"""Обработчик кнопки 'Назад' в админ панель"""
admin_text = "👑 <b>Админ панель</b>\n\n"
admin_text += "Выберите действие:"
await callback.message.edit_text(
admin_text,
reply_markup=get_admin_keyboard(),
parse_mode="HTML"
)
await callback.answer()
@router.callback_query(F.data == "admin_assign_superuser")
@require_permission("manage_users", "У вас нет прав для управления пользователями.")
async def admin_assign_superuser_callback(
callback: CallbackQuery,
user_service: UserService,
message_service: MessageService
):
"""Обработчик кнопки 'Назначить суперпользователя'"""
try:
# Получаем пользователей с пагинацией (оптимизированный запрос)
users = await user_service.database.get_all_users(limit=MAX_PAGE_SIZE, offset=MIN_PAGE_NUMBER)
if not users:
await message_service.edit_message(
callback,
"👥 <b>Управление суперпользователями</b>\n\n"
"❌ Пользователи не найдены.",
get_admin_keyboard()
)
await message_service.send_callback_answer(callback)
return
# Показываем первую страницу
await show_superuser_assignment_page(
callback,
users,
page=MIN_PAGE_NUMBER,
message_service=message_service
)
await message_service.send_callback_answer(callback)
except Exception as e:
logger.error(f"Ошибка при получении списка пользователей: {e}")
await message_service.edit_message(
callback,
"❌ Произошла ошибка при загрузке списка пользователей.",
get_admin_keyboard()
)
await message_service.send_callback_answer(callback)
@router.callback_query(F.data.startswith("superuser_page_"))
@require_permission("manage_users", "У вас нет прав для управления пользователями.")
async def superuser_page_callback(callback: CallbackQuery, database: DatabaseService = None):
"""Обработчик пагинации списка пользователей для назначения суперпользователей"""
try:
# Извлекаем номер страницы
page = int(callback.data.split("_")[-1])
if not database:
from dependencies import get_database
database = get_database()
# Получаем всех пользователей
users = await database.get_all_users(limit=MAX_PAGE_SIZE, offset=MIN_PAGE_NUMBER)
if not users:
await callback.answer("❌ Пользователи не найдены", show_alert=True)
return
# Показываем нужную страницу
await show_superuser_assignment_page(callback, users, page)
await callback.answer()
except ValueError:
await callback.answer("❌ Неверный номер страницы", show_alert=True)
except Exception as e:
logger.error(f"Ошибка при пагинации пользователей: {e}")
await callback.answer("❌ Ошибка при загрузке страницы", show_alert=True)
@router.callback_query(F.data.startswith("assign_superuser_"))
@require_permission("manage_users", "У вас нет прав для управления пользователями.")
async def assign_superuser_callback(callback: CallbackQuery, database: DatabaseService = None, validator = None):
"""Обработчик выбора пользователя для назначения суперпользователем"""
try:
# Извлекаем ID пользователя
user_id_str = callback.data.split("_")[-1]
# Валидируем callback data
if validator:
validation_result = validator.validate_callback_data(callback.data)
if not validation_result:
logger.warning(f"⚠️ Невалидный callback data от пользователя {callback.from_user.id}: {callback.data}")
await callback.answer("❌ Неверные данные", show_alert=True)
return
# Валидируем user_id
try:
user_id = int(user_id_str)
if validator:
user_id_validation = validator.validate_telegram_id(user_id)
if not user_id_validation:
logger.warning(f"⚠️ Невалидный user_id в callback: {user_id}")
await callback.answer("❌ Неверный ID пользователя", show_alert=True)
return
except ValueError:
logger.warning(f"⚠️ Неверный формат user_id в callback: {user_id_str}")
await callback.answer("❌ Неверный формат ID", show_alert=True)
return
if not database:
from dependencies import get_database
database = get_database()
# Получаем информацию о пользователе
user = await database.get_user(user_id)
if not user:
await callback.answer("❌ Пользователь не найден", show_alert=True)
return
# Формируем текст с информацией о пользователе
user_text = f"👤 <b>Информация о пользователе</b>\n\n"
user_text += f"🆔 ID: {user.telegram_id}\n"
user_text += f"👤 Имя: {user.display_name}\n"
user_text += f"📝 Полное имя: {user.full_name}\n"
user_text += f"🔗 Ссылка: {user.profile_link}\n"
created_at_str = user.created_at.strftime('%d.%m.%Y %H:%M') if user.created_at else 'Неизвестно'
user_text += f"📅 Регистрация: {created_at_str}\n"
user_text += f"✅ Активен: {'Да' if user.is_active else 'Нет'}\n"
user_text += f"🔍 Суперпользователь: {'Да' if user.is_superuser else 'Нет'}\n\n"
if user.is_superuser:
user_text += "❓ <b>Хотите снять права суперпользователя?</b>"
else:
user_text += "❓ <b>Хотите назначить суперпользователем?</b>"
# Показываем информацию и кнопки подтверждения
await callback.message.edit_text(
user_text,
reply_markup=get_superuser_confirm_keyboard(user_id, user.is_superuser),
parse_mode="HTML"
)
await callback.answer()
except ValueError:
await callback.answer("❌ Неверный ID пользователя", show_alert=True)
except Exception as e:
logger.error(f"Ошибка при получении информации о пользователе: {e}")
await callback.answer("❌ Ошибка при загрузке информации", show_alert=True)
@router.callback_query(F.data.startswith("confirm_superuser_"))
@require_permission("manage_users", "У вас нет прав для управления пользователями.")
async def confirm_superuser_callback(callback: CallbackQuery, database: DatabaseService = None, validator = None):
"""Обработчик подтверждения назначения суперпользователем"""
try:
# Извлекаем ID пользователя
user_id_str = callback.data.split("_")[-1]
# Валидируем callback data
if validator:
validation_result = validator.validate_callback_data(callback.data)
if not validation_result:
logger.warning(f"⚠️ Невалидный callback data от пользователя {callback.from_user.id}: {callback.data}")
await callback.answer("❌ Неверные данные", show_alert=True)
return
# Валидируем user_id
try:
user_id = int(user_id_str)
if validator:
user_id_validation = validator.validate_telegram_id(user_id)
if not user_id_validation:
logger.warning(f"⚠️ Невалидный user_id в callback: {user_id}")
await callback.answer("❌ Неверный ID пользователя", show_alert=True)
return
except ValueError:
logger.warning(f"⚠️ Неверный формат user_id в callback: {user_id_str}")
await callback.answer("❌ Неверный формат ID", show_alert=True)
return
if not database:
from dependencies import get_database
database = get_database()
# Получаем пользователя
user = await database.get_user(user_id)
if not user:
await callback.answer("❌ Пользователь не найден", show_alert=True)
return
# Назначаем суперпользователем
user.is_superuser = True
await database.update_user(user)
await callback.message.edit_text(
f"✅ <b>Права назначены!</b>\n\n"
f"👤 Пользователь {user.display_name} теперь является суперпользователем.\n\n"
f"🔍 Суперпользователи могут видеть информацию об авторах вопросов.",
reply_markup=get_superuser_confirm_keyboard(user_id, True),
parse_mode="HTML"
)
await callback.answer("✅ Права суперпользователя назначены!")
except ValueError:
await callback.answer("❌ Неверный ID пользователя", show_alert=True)
except Exception as e:
logger.error(f"Ошибка при назначении суперпользователя: {e}")
await callback.answer("❌ Ошибка при назначении прав", show_alert=True)
@router.callback_query(F.data.startswith("remove_superuser_"))
@require_permission("manage_users", "У вас нет прав для управления пользователями.")
async def remove_superuser_callback(callback: CallbackQuery, database: DatabaseService = None, validator = None):
"""Обработчик снятия прав суперпользователя"""
try:
# Извлекаем ID пользователя
user_id_str = callback.data.split("_")[-1]
# Валидируем callback data
if validator:
validation_result = validator.validate_callback_data(callback.data)
if not validation_result:
logger.warning(f"⚠️ Невалидный callback data от пользователя {callback.from_user.id}: {callback.data}")
await callback.answer("❌ Неверные данные", show_alert=True)
return
# Валидируем user_id
try:
user_id = int(user_id_str)
if validator:
user_id_validation = validator.validate_telegram_id(user_id)
if not user_id_validation:
logger.warning(f"⚠️ Невалидный user_id в callback: {user_id}")
await callback.answer("❌ Неверный ID пользователя", show_alert=True)
return
except ValueError:
logger.warning(f"⚠️ Неверный формат user_id в callback: {user_id_str}")
await callback.answer("❌ Неверный формат ID", show_alert=True)
return
if not database:
from dependencies import get_database
database = get_database()
# Получаем пользователя
user = await database.get_user(user_id)
if not user:
await callback.answer("❌ Пользователь не найден", show_alert=True)
return
# Снимаем права суперпользователя
user.is_superuser = False
await database.update_user(user)
await callback.message.edit_text(
f"❌ <b>Права сняты!</b>\n\n"
f"👤 Пользователь {user.display_name} больше не является суперпользователем.\n\n"
f"👤 Теперь он видит анонимные вопросы без информации об авторах.",
reply_markup=get_superuser_confirm_keyboard(user_id, False),
parse_mode="HTML"
)
await callback.answer("❌ Права суперпользователя сняты!")
except ValueError:
await callback.answer("❌ Неверный ID пользователя", show_alert=True)
except Exception as e:
logger.error(f"Ошибка при снятии прав суперпользователя: {e}")
await callback.answer("❌ Ошибка при снятии прав", show_alert=True)
async def show_superuser_assignment_page(
callback: CallbackQuery,
users: list,
page: int = MIN_PAGE_NUMBER,
per_page: int = DEFAULT_PAGE_SIZE,
pagination_service: PaginationService = None,
message_service: MessageService = None
):
"""Показать страницу с пользователями для назначения суперпользователей"""
try:
# Используем сервисы если они переданы, иначе создаем временные
if not pagination_service:
from dependencies import get_pagination_service
pagination_service = get_pagination_service()
if not message_service:
from dependencies import get_message_service
message_service = get_message_service()
# Формируем текст сообщения
users_text = await _format_users_list(users, page, per_page, pagination_service)
# Создаем клавиатуру
keyboard = get_superuser_assignment_keyboard(users, page, per_page)
# Отправляем или редактируем сообщение
await message_service.edit_message(callback, users_text, keyboard)
except Exception as e:
logger.error(f"Ошибка при отображении страницы пользователей: {e}")
await message_service.edit_message(
callback,
"❌ Произошла ошибка при отображении списка пользователей.",
get_admin_keyboard()
)
# ===========================================
# Rate Limiting callback обработчики
# ===========================================
@router.callback_query(F.data == "admin_rate_limit")
async def admin_rate_limit_menu(
callback: CallbackQuery,
message_service: MessageService,
auth: AuthService
):
"""Показать меню rate limiting"""
try:
if not auth.is_admin(callback.from_user.id):
await callback.answer("❌ Доступ запрещен. Только для администраторов.", show_alert=True)
return
text = "🚦 <b>Управление Rate Limiting</b>\n\n"
text += "Выберите действие для управления системой ограничения скорости отправки сообщений:"
await message_service.edit_message(callback, text, get_rate_limit_keyboard())
except Exception as e:
logger.error(f"Ошибка при отображении меню rate limiting: {e}")
await callback.answer("❌ Произошла ошибка при отображении меню.", show_alert=True)
@router.callback_query(F.data == "rate_limit_stats")
@inject_admin_services
async def rate_limit_stats_callback(
callback: CallbackQuery,
rate_limit_service: RateLimitService,
message_service: MessageService,
auth: AuthService,
**kwargs
):
"""Показать статистику rate limiting"""
try:
if not auth.is_admin(callback.from_user.id):
await callback.answer("❌ Доступ запрещен. Только для администраторов.", show_alert=True)
return
stats = rate_limit_service.get_stats()
stats_text = "📊 <b>Статистика Rate Limiting</b>\n\n"
stats_text += "🔢 <b>Общая статистика:</b>\n"
stats_text += f"Всего запросов: {stats.get('total_requests', 0)}\n"
stats_text += f"• Успешных запросов: {stats.get('successful_requests', 0)}\n"
stats_text += f"• Неудачных запросов: {stats.get('failed_requests', 0)}\n"
stats_text += f"• Процент успеха: {stats.get('success_rate', 0.0):.{PERCENTAGE_DECIMAL_PLACES}%}\n"
stats_text += f"• Процент ошибок: {stats.get('error_rate', 0.0):.{PERCENTAGE_DECIMAL_PLACES}%}\n"
stats_text += f"• Среднее время ожидания: {stats.get('average_wait_time', 0.0):.{TIME_DECIMAL_PLACES}f}с\n\n"
stats_text += "🔍 <b>Детальная статистика:</b>\n"
stats_text += f"• RetryAfter ошибок: {stats.get('retry_after_errors', 0)}\n"
stats_text += f"• Других ошибок: {stats.get('other_errors', 0)}\n"
stats_text += f"• Процент RetryAfter: {stats.get('retry_after_rate', 0.0):.{PERCENTAGE_DECIMAL_PLACES}%}\n"
stats_text += f"• Процент других ошибок: {stats.get('other_error_rate', 0.0):.{PERCENTAGE_DECIMAL_PLACES}%}\n"
await message_service.edit_message(callback, stats_text, get_rate_limit_keyboard())
except Exception as e:
logger.error(f"Ошибка при получении статистики rate limiting: {e}")
await callback.answer("❌ Произошла ошибка при получении статистики.", show_alert=True)
@router.callback_query(F.data == "rate_limit_reset_stats")
@inject_admin_services
async def reset_rate_limit_stats_callback(
callback: CallbackQuery,
rate_limit_service: RateLimitService,
message_service: MessageService,
auth: AuthService,
**kwargs
):
"""Сбросить статистику rate limiting"""
try:
if not auth.is_admin(callback.from_user.id):
await callback.answer("❌ Доступ запрещен. Только для администраторов.", show_alert=True)
return
rate_limit_service.reset_stats()
await callback.answer("✅ Статистика rate limiting сброшена.", show_alert=True)
# Обновляем сообщение
text = "🚦 <b>Управление Rate Limiting</b>\n\n"
text += "Выберите действие для управления системой ограничения скорости отправки сообщений:"
await message_service.edit_message(callback, text, get_rate_limit_keyboard())
except Exception as e:
logger.error(f"Ошибка при сбросе статистики rate limiting: {e}")
await callback.answer("❌ Произошла ошибка при сбросе статистики.", show_alert=True)
@router.callback_query(F.data == "rate_limit_adapt")
@inject_admin_services
async def adapt_rate_limit_config_callback(
callback: CallbackQuery,
rate_limit_service: RateLimitService,
message_service: MessageService,
auth: AuthService,
**kwargs
):
"""Адаптировать конфигурацию rate limiting"""
try:
if not auth.is_admin(callback.from_user.id):
await callback.answer("❌ Доступ запрещен. Только для администраторов.", show_alert=True)
return
if rate_limit_service.should_adapt_config():
await rate_limit_service.adapt_config_if_needed()
await callback.answer("✅ Конфигурация rate limiting адаптирована на основе текущей производительности.", show_alert=True)
else:
await callback.answer(" Адаптация конфигурации не требуется. Недостаточно данных или производительность в норме.", show_alert=True)
# Обновляем сообщение
text = "🚦 <b>Управление Rate Limiting</b>\n\n"
text += "Выберите действие для управления системой ограничения скорости отправки сообщений:"
await message_service.edit_message(callback, text, get_rate_limit_keyboard())
except Exception as e:
logger.error(f"Ошибка при адаптации конфигурации rate limiting: {e}")
await callback.answer("❌ Произошла ошибка при адаптации конфигурации.", show_alert=True)

478
handlers/answers.py Normal file
View File

@@ -0,0 +1,478 @@
"""
Обработчики для работы с ответами на вопросы
"""
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)

217
handlers/errors.py Normal file
View File

@@ -0,0 +1,217 @@
"""
Глобальная обработка ошибок
"""
import traceback
from aiogram import Router
from aiogram.types import ErrorEvent, Message, CallbackQuery
from aiogram.exceptions import TelegramBadRequest, TelegramNetworkError, TelegramRetryAfter
from config import config
from services.infrastructure.logger import get_logger
from services.infrastructure.metrics import get_metrics_service
from keyboards.reply import get_main_keyboard_for_user
logger = get_logger(__name__)
router = Router()
@router.error()
async def error_handler(event: ErrorEvent):
"""Глобальный обработчик ошибок"""
error = event.exception
update = event.update
# Записываем метрику ошибки
metrics_service = get_metrics_service()
metrics_service.increment_errors(type(error).__name__, "global_handler")
# Логируем ошибку
logger.error(f"💥 Ошибка в обработчике: {error}")
logger.error(f"🔍 Тип ошибки: {type(error).__name__}")
logger.error(f"📋 Детали: {traceback.format_exc()}")
# Определяем тип обновления
if update.message:
await handle_message_error(update.message, error)
elif update.callback_query:
await handle_callback_error(update.callback_query, error)
else:
logger.error(f"❓ Неизвестный тип обновления: {update}")
async def handle_message_error(message: Message, error: Exception):
"""Обработка ошибок в сообщениях"""
try:
# Определяем тип ошибки и отправляем соответствующее сообщение
if isinstance(error, TelegramRetryAfter):
# Ошибка rate limiting
await message.answer(
f"⏳ Слишком много запросов. Попробуйте через {error.retry_after} секунд.",
reply_markup=get_main_keyboard_for_user(message.from_user.id)
)
elif isinstance(error, TelegramBadRequest):
# Некорректный запрос
if "message is not modified" in str(error):
# Сообщение не изменилось - это не критическая ошибка
logger.warning("Сообщение не изменилось")
elif "chat not found" in str(error):
# Чат не найден
logger.warning(f"Чат не найден: {message.chat.id}")
else:
await message.answer(
"❌ Произошла ошибка при обработке запроса. Попробуйте позже.",
reply_markup=get_main_keyboard_for_user(message.from_user.id)
)
elif isinstance(error, TelegramNetworkError):
# Сетевая ошибка
await message.answer(
"🌐 Проблемы с сетью. Проверьте подключение к интернету.",
reply_markup=get_main_keyboard_for_user(message.from_user.id)
)
elif isinstance(error, ValueError):
# Ошибка валидации
await message.answer(
"❌ Некорректные данные. Проверьте введенную информацию.",
reply_markup=get_main_keyboard_for_user(message.from_user.id)
)
elif isinstance(error, KeyError):
# Ошибка ключа (обычно в FSM)
await message.answer(
"❌ Ошибка состояния. Попробуйте начать заново с команды /start.",
reply_markup=get_main_keyboard_for_user(message.from_user.id)
)
else:
# Неизвестная ошибка
if config.DEBUG:
# В режиме отладки показываем детали ошибки
error_text = f"🐛 <b>Ошибка отладки:</b>\n\n"
error_text += f"<code>{type(error).__name__}: {str(error)}</code>"
await message.answer(
error_text,
reply_markup=get_main_keyboard_for_user(message.from_user.id),
parse_mode="HTML"
)
else:
# В продакшене показываем общее сообщение
await message.answer(
"❌ Произошла неожиданная ошибка. Попробуйте позже или обратитесь к администратору.",
reply_markup=get_main_keyboard_for_user(message.from_user.id)
)
# Уведомляем администраторов о критических ошибках
if not isinstance(error, (TelegramRetryAfter, TelegramBadRequest)):
await notify_admins_about_error(error, message)
except Exception as e:
# Если даже обработка ошибки не удалась
logger.critical(f"Критическая ошибка в обработчике ошибок: {e}")
try:
await message.answer(
"❌ Критическая ошибка. Бот временно недоступен.",
reply_markup=get_main_keyboard_for_user(message.from_user.id)
)
except:
pass # Если даже это не удалось, ничего не делаем
async def handle_callback_error(callback: CallbackQuery, error: Exception):
"""Обработка ошибок в callback запросах"""
try:
# Определяем тип ошибки и отправляем соответствующее сообщение
if isinstance(error, TelegramRetryAfter):
# Ошибка rate limiting
await callback.answer(
f"⏳ Слишком много запросов. Попробуйте через {error.retry_after} секунд.",
show_alert=True
)
elif isinstance(error, TelegramBadRequest):
# Некорректный запрос
if "message is not modified" in str(error):
# Сообщение не изменилось - это не критическая ошибка
logger.warning("Сообщение не изменилось в callback")
elif "query is too old" in str(error):
# Запрос слишком старый
await callback.answer(
"⏰ Запрос устарел. Обновите страницу и попробуйте снова.",
show_alert=True
)
else:
await callback.answer(
"❌ Произошла ошибка при обработке запроса.",
show_alert=True
)
elif isinstance(error, TelegramNetworkError):
# Сетевая ошибка
await callback.answer(
"🌐 Проблемы с сетью. Проверьте подключение.",
show_alert=True
)
else:
# Неизвестная ошибка
if config.DEBUG:
# В режиме отладки показываем детали ошибки
error_text = f"🐛 <b>Ошибка отладки:</b>\n\n"
error_text += f"<code>{type(error).__name__}: {str(error)}</code>"
await callback.message.edit_text(
error_text,
parse_mode="HTML"
)
else:
# В продакшене показываем общее сообщение
await callback.answer(
"❌ Произошла неожиданная ошибка.",
show_alert=True
)
# Уведомляем администраторов о критических ошибках
if not isinstance(error, (TelegramRetryAfter, TelegramBadRequest)):
await notify_admins_about_error(error, callback.message)
except Exception as e:
# Если даже обработка ошибки не удалась
logger.critical(f"Критическая ошибка в обработчике callback ошибок: {e}")
try:
await callback.answer(
"❌ Критическая ошибка.",
show_alert=True
)
except:
pass # Если даже это не удалось, ничего не делаем
async def notify_admins_about_error(error: Exception, message: Message):
"""Уведомление администраторов об ошибке"""
if not config.ADMINS:
return
try:
error_text = f"🚨 <b>Ошибка в боте</b>\n\n"
error_text += f"🐛 <b>Тип:</b> {type(error).__name__}\n"
error_text += f"📝 <b>Сообщение:</b> {str(error)}\n"
error_text += f"👤 <b>Пользователь:</b> {message.from_user.id}\n"
error_text += f"💬 <b>Чат:</b> {message.chat.id}\n"
error_text += f"📅 <b>Время:</b> {message.date.strftime('%d.%m.%Y %H:%M:%S')}\n\n"
if config.DEBUG:
error_text += f"🔍 <b>Трассировка:</b>\n<code>{traceback.format_exc()}</code>"
# Отправляем уведомление всем администраторам
from dependencies import get_message_service
message_service = get_message_service()
for admin_id in config.ADMINS:
try:
await message_service.send_bot_message(
message.bot,
admin_id,
error_text
)
except Exception as e:
logger.error(f"Не удалось отправить уведомление админу {admin_id}: {e}")
except Exception as e:
logger.error(f"Ошибка при уведомлении администраторов: {e}")

1272
handlers/questions.py Normal file

File diff suppressed because it is too large Load Diff

328
handlers/start.py Normal file
View File

@@ -0,0 +1,328 @@
"""
Обработчики команд /start и /help
"""
from datetime import datetime
from aiogram import Router, F
from aiogram.types import Message
from aiogram.filters import Command, CommandStart
from aiogram.fsm.context import FSMContext
from config import config
from models.user import User
from services.infrastructure.database import DatabaseService
from services.auth.auth_new import AuthService
from services.utils import UtilsService
from services.business.user_service import UserService
from services.business.message_service import MessageService
from services.infrastructure.logger import get_logger
from services.infrastructure.metrics import get_metrics_service, track_message_processing
from keyboards.reply import get_main_keyboard_for_user, get_admin_reply_keyboard
from keyboards.inline import get_admin_keyboard
from dependencies import inject_start_services, inject_link_services, inject_main_menu_services
logger = get_logger(__name__)
router = Router()
async def _create_welcome_message(user: User, referral_link: str) -> str:
"""Создание приветственного сообщения"""
welcome_text = f"👋 <b>Добро пожаловать, {user.display_name}!</b>\n\n"
welcome_text += "🤖 Я бот для анонимных вопросов.\n\n"
welcome_text += "📝 <b>Как это работает:</b>\n"
welcome_text += "• Поделитесь своей ссылкой с друзьями\n"
welcome_text += "• Они смогут задать вам анонимные вопросы\n"
welcome_text += "• Вы получите уведомления и сможете ответить\n\n"
welcome_text += f"🔗 <b>Ваша персональная ссылка:</b>\n"
welcome_text += f"<code>{referral_link}</code>\n\n"
welcome_text += "💡 <b>Совет:</b> Скопируйте ссылку и поделитесь ею в социальных сетях!"
return welcome_text
async def _process_start_command(
message: Message,
user_service: UserService,
auth: AuthService,
utils: UtilsService,
message_service: MessageService,
validator
) -> User:
"""Обработка команды /start без аргументов"""
# Валидируем Telegram ID пользователя
user_id_validation = validator.validate_telegram_id(message.from_user.id)
if not user_id_validation:
logger.error(f"❌ Невалидный Telegram ID: {message.from_user.id}")
await message_service.send_message(
message,
"❌ Ошибка: недопустимый ID пользователя.",
get_main_keyboard_for_user(message.from_user.id)
)
raise ValueError(f"Invalid Telegram ID: {message.from_user.id}")
# Создаем или обновляем пользователя
user = await user_service.create_or_update_user(message.from_user, message.chat.id)
# Проверяем, является ли пользователь админом
is_admin = auth.is_admin(user.telegram_id)
# Генерируем персональную ссылку
bot_info = await message.bot.get_me()
referral_link = user_service.generate_referral_link(bot_info.username, user)
# Создаем приветственное сообщение
welcome_text = await _create_welcome_message(user, referral_link)
# Выбираем клавиатуру в зависимости от роли
if is_admin:
keyboard = get_admin_reply_keyboard()
else:
keyboard = get_main_keyboard_for_user(user.telegram_id)
logger.info(f"⌨️ Создана клавиатура для пользователя {user.telegram_id}: {type(keyboard).__name__}")
# Отправляем сообщение
await message_service.send_message(message, welcome_text, keyboard)
logger.info(f"✅ Приветственное сообщение отправлено пользователю {user.telegram_id}")
return user
@router.message(CommandStart())
@track_message_processing("start_command")
@inject_start_services
async def cmd_start(
message: Message,
state: FSMContext,
user_service: UserService,
auth: AuthService,
utils: UtilsService,
message_service: MessageService,
validator,
**kwargs
):
"""Обработчик команды /start"""
try:
logger.info(f"🚀 Команда /start от пользователя {message.from_user.id} ({message.from_user.first_name})")
# Сбрасываем состояние FSM при команде /start
await state.clear()
logger.info(f"🔄 Состояние FSM сброшено для пользователя {message.from_user.id}")
# Получаем аргументы команды
args = message.text.split()[1:] if len(message.text.split()) > 1 else []
# Обрабатываем deep linking если есть аргументы
if args:
logger.info(f"🔗 Обработка deep link: {args[0]}")
await handle_deep_link(message, args[0], user_service, state, message_service, validator)
else:
# Обрабатываем обычную команду /start
await _process_start_command(message, user_service, auth, utils, message_service, validator)
except Exception as e:
logger.error(f"💥 Ошибка в обработчике /start: {e}")
await message_service.send_error_message(
message,
"❌ Произошла ошибка при запуске бота. Попробуйте позже."
)
async def handle_deep_link(
message: Message,
ref_code: str,
user_service: UserService,
state: FSMContext,
message_service: MessageService,
validator
):
"""Обработка deep linking для анонимных вопросов"""
try:
# Валидируем deep link
validation_result = validator.validate_deep_link(ref_code)
if not validation_result:
logger.warning(f"⚠️ Невалидный deep link от пользователя {message.from_user.id}: {ref_code}")
await message_service.send_message(
message,
f"{validation_result.error_message}",
get_main_keyboard_for_user(message.from_user.id)
)
return
# Используем санитизированное значение
ref_code = validation_result.sanitized_value
if not ref_code.startswith('ref_'):
await message_service.send_message(
message,
"❌ Неверная ссылка.",
get_main_keyboard_for_user(message.from_user.id)
)
return
# Извлекаем анонимный ID из реферального кода
anonymous_id = ref_code[4:] # Убираем 'ref_'
# Ищем пользователя по profile_link
target_user = await user_service.get_user_by_profile_link(anonymous_id)
if not target_user:
await message_service.send_message(
message,
"❌ Пользователь, на которого вы перешли, не найден.",
get_main_keyboard_for_user(message.from_user.id)
)
return
# Отправляем сообщение о переходе по ссылке
deep_link_text = (
f"👋 Вы перешли по ссылке пользователя {target_user.display_name}!\n\n"
f"📝 Теперь вы можете задать анонимный вопрос.\n"
f"Просто отправьте ваше сообщение, и оно будет передано получателю."
)
await message_service.send_message(
message,
deep_link_text,
get_main_keyboard_for_user(message.from_user.id)
)
# Устанавливаем состояние ожидания вопроса
from aiogram.fsm.state import State, StatesGroup
class QuestionStates(StatesGroup):
waiting_for_question = State()
await state.set_state(QuestionStates.waiting_for_question)
await state.update_data(target_user_id=target_user.telegram_id)
except Exception as e:
logger.error(f"Ошибка при обработке deep link: {e}")
await message_service.send_error_message(
message,
"❌ Произошла ошибка при обработке ссылки."
)
@router.message(Command("help"))
async def cmd_help(message: Message):
"""Обработчик команды /help"""
help_text = "📖 <b>Справка по боту</b>\n\n"
help_text += "🤖 <b>Основные команды:</b>\n"
help_text += "/start - Запуск бота и получение персональной ссылки\n"
help_text += "/help - Показать эту справку\n"
help_text += "/stats - Показать статистику (только для админов)\n\n"
help_text += "📝 <b>Как задать анонимный вопрос:</b>\n"
help_text += "1. Перейдите по персональной ссылке пользователя\n"
help_text += "2. Отправьте ваш вопрос боту\n"
help_text += "3. Вопрос будет передан получателю анонимно\n\n"
help_text += "💬 <b>Как ответить на вопрос:</b>\n"
help_text += "1. Получите уведомление о новом вопросе\n"
help_text += "2. Нажмите кнопку 'Ответить'\n"
help_text += "3. Введите ваш ответ\n"
help_text += "4. Ответ будет отправлен анонимно\n\n"
help_text += "🔗 <b>Ваша персональная ссылка:</b>\n"
help_text += "Используйте кнопку 'Моя ссылка' для получения ссылки\n\n"
help_text += "❓ <b>Нужна помощь?</b>\n"
help_text += "Обратитесь к администратору бота: @Kerrad1"
await message.answer(help_text, parse_mode="HTML")
@router.message(F.text == " Помощь")
async def help_button(message: Message):
"""Обработчик кнопки 'Помощь'"""
await cmd_help(message)
@router.message(F.text == "🔗 Моя ссылка")
@inject_link_services
async def my_link_button(
message: Message,
user_service: UserService,
message_service: MessageService,
**kwargs
):
"""Обработчик кнопки 'Моя ссылка'"""
try:
# Получаем пользователя из БД
user = await user_service.get_user_by_telegram_id(message.from_user.id)
if not user:
await message_service.send_message(
message,
"❌ Пользователь не найден. Используйте /start для регистрации."
)
return
# Получаем информацию о боте
bot_info = await message.bot.get_me()
referral_link = user_service.generate_referral_link(bot_info.username, user)
link_text = "🔗 <b>Ваша персональная ссылка:</b>\n\n"
link_text += f"<code>{referral_link}</code>\n\n"
link_text += "📝 <b>Как использовать:</b>\n"
link_text += "• Скопируйте ссылку\n"
link_text += "• Поделитесь ею в социальных сетях\n"
link_text += "• Друзья смогут задать вам анонимные вопросы\n\n"
link_text += "💡 <b>Совет:</b> Добавьте ссылку в описание профиля или поделитесь в Stories!"
await message_service.send_message(message, link_text)
except Exception as e:
logger.error(f"Ошибка при получении ссылки: {e}")
await message_service.send_error_message(
message,
"❌ Произошла ошибка при получении ссылки. Попробуйте позже."
)
@router.message(F.text == "⬅️ Главное меню")
@inject_main_menu_services
async def back_to_main(
message: Message,
state: FSMContext,
auth: AuthService,
message_service: MessageService,
**kwargs
):
"""Обработчик кнопки 'Главное меню'"""
# Сбрасываем состояние FSM при возврате в главное меню
await state.clear()
logger.info(f"🔄 Состояние FSM сброшено для пользователя {message.from_user.id} (кнопка 'Главное меню')")
is_admin = auth.is_admin(message.from_user.id)
if is_admin:
keyboard = get_admin_reply_keyboard()
else:
keyboard = get_main_keyboard_for_user(message.from_user.id)
await message_service.send_message(
message,
"🏠 <b>Главное меню</b>\n\nВыберите действие:",
keyboard
)
@router.message(F.text == "⚙️ Админ панель")
@inject_main_menu_services
async def admin_panel_button(
message: Message,
auth: AuthService,
message_service: MessageService,
**kwargs
):
"""Обработчик кнопки 'Админ панель'"""
# Проверяем, является ли пользователь админом
if not auth.is_admin(message.from_user.id):
await message_service.send_message(
message,
"У вас нет прав для доступа к админ панели."
)
return
# Получаем inline клавиатуру для админов
admin_keyboard = get_admin_keyboard()
await message_service.send_message(
message,
"👑 <b>Админ панель</b>\n\nВыберите действие:",
admin_keyboard
)