import logging from aiogram import Dispatcher, types, F from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.filters import StateFilter from sqlalchemy.ext.asyncio import AsyncSession from app.database.crud.user_message import ( create_user_message, get_all_user_messages, get_user_message_by_id, update_user_message, delete_user_message, toggle_user_message_status, get_user_messages_stats ) from app.database.models import User from app.keyboards.admin import get_admin_main_keyboard from app.utils.validators import ( get_html_help_text, sanitize_html, validate_html_tags, ) from app.utils.decorators import admin_required, error_handler from app.localization.texts import get_texts logger = logging.getLogger(__name__) class UserMessageStates(StatesGroup): waiting_for_message_text = State() waiting_for_edit_text = State() def get_user_messages_keyboard(language: str = "ru"): from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton return InlineKeyboardMarkup(inline_keyboard=[ [ InlineKeyboardButton( text="📝 Добавить сообщение", callback_data="add_user_message" ) ], [ InlineKeyboardButton( text="📋 Список сообщений", callback_data="list_user_messages:0" ) ], [ InlineKeyboardButton( text="📊 Статистика", callback_data="user_messages_stats" ) ], [ InlineKeyboardButton( text="🔙 Назад в админку", callback_data="admin_panel" ) ] ]) def get_message_actions_keyboard(message_id: int, is_active: bool, language: str = "ru"): from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton status_text = "🔴 Деактивировать" if is_active else "🟢 Активировать" return InlineKeyboardMarkup(inline_keyboard=[ [ InlineKeyboardButton( text="✏️ Редактировать", callback_data=f"edit_user_message:{message_id}" ) ], [ InlineKeyboardButton( text=status_text, callback_data=f"toggle_user_message:{message_id}" ) ], [ InlineKeyboardButton( text="🗑️ Удалить", callback_data=f"delete_user_message:{message_id}" ) ], [ InlineKeyboardButton( text="🔙 К списку", callback_data="list_user_messages:0" ) ] ]) @admin_required @error_handler async def show_user_messages_panel( callback: types.CallbackQuery, db_user: User, db: AsyncSession ): texts = get_texts(db_user.language) text = ( "📢 Управление сообщениями в главном меню\n\n" "Здесь вы можете добавлять сообщения, которые будут показываться пользователям " "в главном меню между информацией о подписке и кнопками действий.\n\n" "• Сообщения поддерживают HTML теги\n" "• Можно создать несколько сообщений\n" "• Активные сообщения показываются случайно\n" "• Неактивные сообщения не показываются" ) await callback.message.edit_text( text, reply_markup=get_user_messages_keyboard(db_user.language), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def add_user_message_start( callback: types.CallbackQuery, state: FSMContext, db_user: User, db: AsyncSession ): await callback.message.edit_text( f"📝 Добавление нового сообщения\n\n" f"Введите текст сообщения, которое будет показываться в главном меню.\n\n" f"{get_html_help_text()}\n\n" f"Отправьте /cancel для отмены.", parse_mode="HTML" ) await state.set_state(UserMessageStates.waiting_for_message_text) await callback.answer() @admin_required @error_handler async def process_new_message_text( message: types.Message, state: FSMContext, db_user: User, db: AsyncSession ): if message.text == "/cancel": await state.clear() await message.answer( "❌ Добавление сообщения отменено.", reply_markup=get_user_messages_keyboard(db_user.language) ) return message_text = message.text.strip() if len(message_text) > 4000: await message.answer( "❌ Сообщение слишком длинное. Максимум 4000 символов.\n" "Попробуйте еще раз или отправьте /cancel для отмены." ) return is_valid, error_msg = validate_html_tags(message_text) if not is_valid: await message.answer( f"❌ Ошибка в HTML разметке: {error_msg}\n\n" f"Исправьте ошибку и попробуйте еще раз, или отправьте /cancel для отмены.", parse_mode=None ) return try: new_message = await create_user_message( db=db, message_text=message_text, created_by=db_user.id, is_active=True ) await state.clear() await message.answer( f"✅ Сообщение добавлено!\n\n" f"ID: {new_message.id}\n" f"Статус: {'🟢 Активно' if new_message.is_active else '🔴 Неактивно'}\n" f"Создано: {new_message.created_at.strftime('%d.%m.%Y %H:%M')}\n\n" f"Предварительный просмотр:\n" f"
{message_text}
", reply_markup=get_user_messages_keyboard(db_user.language), parse_mode="HTML" ) except Exception as e: logger.error(f"Ошибка создания сообщения: {e}") await state.clear() await message.answer( "❌ Произошла ошибка при создании сообщения. Попробуйте еще раз.", reply_markup=get_user_messages_keyboard(db_user.language) ) @admin_required @error_handler async def list_user_messages( callback: types.CallbackQuery, db_user: User, db: AsyncSession ): page = 0 if ":" in callback.data: try: page = int(callback.data.split(":")[1]) except (ValueError, IndexError): page = 0 limit = 5 offset = page * limit messages = await get_all_user_messages(db, offset=offset, limit=limit) if not messages: await callback.message.edit_text( "📋 Список сообщений\n\n" "Сообщений пока нет. Добавьте первое сообщение!", reply_markup=get_user_messages_keyboard(db_user.language), parse_mode="HTML" ) await callback.answer() return text = "📋 Список сообщений\n\n" for msg in messages: status_emoji = "🟢" if msg.is_active else "🔴" preview = msg.message_text[:100] + "..." if len(msg.message_text) > 100 else msg.message_text preview = preview.replace('<', '<').replace('>', '>') text += ( f"{status_emoji} ID {msg.id}\n" f"{preview}\n" f"📅 {msg.created_at.strftime('%d.%m.%Y %H:%M')}\n\n" ) from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton keyboard = [] for msg in messages: status_emoji = "🟢" if msg.is_active else "🔴" keyboard.append([ InlineKeyboardButton( text=f"{status_emoji} ID {msg.id}", callback_data=f"view_user_message:{msg.id}" ) ]) nav_buttons = [] if page > 0: nav_buttons.append( InlineKeyboardButton( text="⬅️ Назад", callback_data=f"list_user_messages:{page-1}" ) ) nav_buttons.append( InlineKeyboardButton( text="➕ Добавить", callback_data="add_user_message" ) ) if len(messages) == limit: nav_buttons.append( InlineKeyboardButton( text="Вперед ➡️", callback_data=f"list_user_messages:{page+1}" ) ) if nav_buttons: keyboard.append(nav_buttons) keyboard.append([ InlineKeyboardButton( text="🔙 Назад", callback_data="user_messages_panel" ) ]) await callback.message.edit_text( text, reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def view_user_message( callback: types.CallbackQuery, db_user: User, db: AsyncSession ): try: message_id = int(callback.data.split(":")[1]) except (ValueError, IndexError): await callback.answer("❌ Неверный ID сообщения", show_alert=True) return message = await get_user_message_by_id(db, message_id) if not message: await callback.answer("❌ Сообщение не найдено", show_alert=True) return safe_content = sanitize_html(message.message_text) status_text = "🟢 Активно" if message.is_active else "🔴 Неактивно" text = ( f"📋 Сообщение ID {message.id}\n\n" f"Статус: {status_text}\n" f"Создано: {message.created_at.strftime('%d.%m.%Y %H:%M')}\n" f"Обновлено: {message.updated_at.strftime('%d.%m.%Y %H:%M')}\n\n" f"Содержимое:\n" f"
{safe_content}
" ) await callback.message.edit_text( text, reply_markup=get_message_actions_keyboard( message_id, message.is_active, db_user.language ), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def toggle_message_status( callback: types.CallbackQuery, db_user: User, db: AsyncSession ): try: message_id = int(callback.data.split(":")[1]) except (ValueError, IndexError): await callback.answer("❌ Неверный ID сообщения", show_alert=True) return message = await toggle_user_message_status(db, message_id) if not message: await callback.answer("❌ Сообщение не найдено", show_alert=True) return status_text = "активировано" if message.is_active else "деактивировано" await callback.answer(f"✅ Сообщение {status_text}") await view_user_message(callback, db_user, db) @admin_required @error_handler async def delete_message_confirm( callback: types.CallbackQuery, db_user: User, db: AsyncSession ): """Подтвердить удаление сообщения""" try: message_id = int(callback.data.split(":")[1]) except (ValueError, IndexError): await callback.answer("❌ Неверный ID сообщения", show_alert=True) return success = await delete_user_message(db, message_id) if success: await callback.answer("✅ Сообщение удалено") await list_user_messages( types.CallbackQuery( id=callback.id, from_user=callback.from_user, chat_instance=callback.chat_instance, data="list_user_messages:0", message=callback.message ), db_user, db ) else: await callback.answer("❌ Ошибка удаления сообщения", show_alert=True) @admin_required @error_handler async def show_messages_stats( callback: types.CallbackQuery, db_user: User, db: AsyncSession ): stats = await get_user_messages_stats(db) text = ( "📊 Статистика сообщений\n\n" f"📝 Всего сообщений: {stats['total_messages']}\n" f"🟢 Активных: {stats['active_messages']}\n" f"🔴 Неактивных: {stats['inactive_messages']}\n\n" "Активные сообщения показываются пользователям случайным образом " "в главном меню между информацией о подписке и кнопками действий." ) from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton keyboard = InlineKeyboardMarkup(inline_keyboard=[ [ InlineKeyboardButton( text="🔙 Назад", callback_data="user_messages_panel" ) ] ]) await callback.message.edit_text( text, reply_markup=keyboard, parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def edit_user_message_start( callback: types.CallbackQuery, state: FSMContext, db_user: User, db: AsyncSession ): try: message_id = int(callback.data.split(":")[1]) except (ValueError, IndexError): await callback.answer("❌ Неверный ID сообщения", show_alert=True) return message = await get_user_message_by_id(db, message_id) if not message: await callback.answer("❌ Сообщение не найдено", show_alert=True) return await callback.message.edit_text( f"✏️ Редактирование сообщения ID {message.id}\n\n" f"Текущий текст:\n" f"
{sanitize_html(message.message_text)}
\n\n" f"Введите новый текст сообщения или отправьте /cancel для отмены:", parse_mode="HTML" ) await state.set_data({"editing_message_id": message_id}) await state.set_state(UserMessageStates.waiting_for_edit_text) await callback.answer() @admin_required @error_handler async def process_edit_message_text( message: types.Message, state: FSMContext, db_user: User, db: AsyncSession ): if message.text == "/cancel": await state.clear() await message.answer( "❌ Редактирование отменено.", reply_markup=get_user_messages_keyboard(db_user.language) ) return data = await state.get_data() message_id = data.get("editing_message_id") if not message_id: await state.clear() await message.answer("❌ Ошибка: ID сообщения не найден") return new_text = message.text.strip() if len(new_text) > 4000: await message.answer( "❌ Сообщение слишком длинное. Максимум 4000 символов.\n" "Попробуйте еще раз или отправьте /cancel для отмены." ) return is_valid, error_msg = validate_html_tags(new_text) if not is_valid: await message.answer( f"❌ Ошибка в HTML разметке: {error_msg}\n\n" f"Исправьте ошибку и попробуйте еще раз, или отправьте /cancel для отмены.", parse_mode=None ) return try: updated_message = await update_user_message( db=db, message_id=message_id, message_text=new_text ) if updated_message: await state.clear() await message.answer( f"✅ Сообщение обновлено!\n\n" f"ID: {updated_message.id}\n" f"Обновлено: {updated_message.updated_at.strftime('%d.%m.%Y %H:%M')}\n\n" f"Новый текст:\n" f"
{sanitize_html(new_text)}
", reply_markup=get_user_messages_keyboard(db_user.language), parse_mode="HTML" ) else: await state.clear() await message.answer( "❌ Сообщение не найдено или ошибка обновления.", reply_markup=get_user_messages_keyboard(db_user.language) ) except Exception as e: logger.error(f"Ошибка обновления сообщения: {e}") await state.clear() await message.answer( "❌ Произошла ошибка при обновлении сообщения.", reply_markup=get_user_messages_keyboard(db_user.language) ) def register_handlers(dp: Dispatcher): dp.callback_query.register( show_user_messages_panel, F.data == "user_messages_panel" ) dp.callback_query.register( add_user_message_start, F.data == "add_user_message" ) dp.message.register( process_new_message_text, StateFilter(UserMessageStates.waiting_for_message_text) ) dp.callback_query.register( edit_user_message_start, F.data.startswith("edit_user_message:") ) dp.message.register( process_edit_message_text, StateFilter(UserMessageStates.waiting_for_edit_text) ) dp.callback_query.register( list_user_messages, F.data.startswith("list_user_messages") ) dp.callback_query.register( view_user_message, F.data.startswith("view_user_message:") ) dp.callback_query.register( toggle_message_status, F.data.startswith("toggle_user_message:") ) dp.callback_query.register( delete_message_confirm, F.data.startswith("delete_user_message:") ) dp.callback_query.register( show_messages_stats, F.data == "user_messages_stats" )