import logging import re import html import contextlib from aiogram import Dispatcher, types, F from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from sqlalchemy.ext.asyncio import AsyncSession from app.database.models import User from app.config import settings from app.localization.texts import get_texts from app.utils.decorators import admin_required, error_handler from app.services.support_settings_service import SupportSettingsService from app.states import SupportSettingsStates logger = logging.getLogger(__name__) def _get_support_settings_keyboard(language: str) -> types.InlineKeyboardMarkup: texts = get_texts(language) mode = SupportSettingsService.get_system_mode() menu_enabled = SupportSettingsService.is_support_menu_enabled() admin_notif = SupportSettingsService.get_admin_ticket_notifications_enabled() user_notif = SupportSettingsService.get_user_ticket_notifications_enabled() sla_enabled = SupportSettingsService.get_sla_enabled() sla_minutes = SupportSettingsService.get_sla_minutes() rows: list[list[types.InlineKeyboardButton]] = [] status_enabled = texts.t("ADMIN_SUPPORT_SETTINGS_STATUS_ENABLED", "Включены") status_disabled = texts.t("ADMIN_SUPPORT_SETTINGS_STATUS_DISABLED", "Отключены") def mode_button(label_key: str, default: str, active: bool) -> str: prefix = "🔘" if active else "⚪" return f"{prefix} {texts.t(label_key, default)}" rows.append([ types.InlineKeyboardButton( text=( f"{'✅' if menu_enabled else '🚫'} " f"{texts.t('ADMIN_SUPPORT_SETTINGS_MENU_LABEL', 'Пункт «Техподдержка» в меню')}" ), callback_data="admin_support_toggle_menu" ) ]) rows.append([ types.InlineKeyboardButton( text=mode_button("ADMIN_SUPPORT_SETTINGS_MODE_TICKETS", "Тикеты", mode == "tickets"), callback_data="admin_support_mode_tickets" ), types.InlineKeyboardButton( text=mode_button("ADMIN_SUPPORT_SETTINGS_MODE_CONTACT", "Контакт", mode == "contact"), callback_data="admin_support_mode_contact" ), types.InlineKeyboardButton( text=mode_button("ADMIN_SUPPORT_SETTINGS_MODE_BOTH", "Оба", mode == "both"), callback_data="admin_support_mode_both" ), ]) rows.append([ types.InlineKeyboardButton( text=texts.t("ADMIN_SUPPORT_SETTINGS_EDIT_DESCRIPTION", "📝 Изменить описание"), callback_data="admin_support_edit_desc" ) ]) # Notifications block rows.append([ types.InlineKeyboardButton( text=( f"{'🔔' if admin_notif else '🔕'} " f"{texts.t('ADMIN_SUPPORT_SETTINGS_ADMIN_NOTIFICATIONS', 'Админ-уведомления')}: " f"{status_enabled if admin_notif else status_disabled}" ), callback_data="admin_support_toggle_admin_notifications" ) ]) rows.append([ types.InlineKeyboardButton( text=( f"{'🔔' if user_notif else '🔕'} " f"{texts.t('ADMIN_SUPPORT_SETTINGS_USER_NOTIFICATIONS', 'Пользовательские уведомления')}: " f"{status_enabled if user_notif else status_disabled}" ), callback_data="admin_support_toggle_user_notifications" ) ]) # SLA block rows.append([ types.InlineKeyboardButton( text=( f"{'⏰' if sla_enabled else '⏹️'} " f"{texts.t('ADMIN_SUPPORT_SETTINGS_SLA_LABEL', 'SLA')}: " f"{status_enabled if sla_enabled else status_disabled}" ), callback_data="admin_support_toggle_sla" ) ]) rows.append([ types.InlineKeyboardButton( text=texts.t("ADMIN_SUPPORT_SETTINGS_SLA_TIME", "⏳ Время SLA: {minutes} мин").format(minutes=sla_minutes), callback_data="admin_support_set_sla_minutes" ) ]) # Moderators moderators = SupportSettingsService.get_moderators() mod_count = len(moderators) rows.append([ types.InlineKeyboardButton( text=texts.t("ADMIN_SUPPORT_SETTINGS_MODERATORS_COUNT", "🧑‍⚖️ Модераторы: {count}").format(count=mod_count), callback_data="admin_support_list_moderators" ) ]) rows.append([ types.InlineKeyboardButton( text=texts.t("ADMIN_SUPPORT_SETTINGS_ADD_MODERATOR", "➕ Назначить модератора"), callback_data="admin_support_add_moderator" ), types.InlineKeyboardButton( text=texts.t("ADMIN_SUPPORT_SETTINGS_REMOVE_MODERATOR", "➖ Удалить модератора"), callback_data="admin_support_remove_moderator" ) ]) rows.append([ types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_submenu_support") ]) return types.InlineKeyboardMarkup(inline_keyboard=rows) @admin_required @error_handler async def show_support_settings( callback: types.CallbackQuery, db_user: User, db: AsyncSession ): texts = get_texts(db_user.language) desc = SupportSettingsService.get_support_info_text(db_user.language) await callback.message.edit_text( texts.t("ADMIN_SUPPORT_SETTINGS_TITLE", "🛟 Настройки поддержки") + "\n\n" + texts.t("ADMIN_SUPPORT_SETTINGS_DESCRIPTION", "Режим работы и видимость в меню. Ниже текущее описание меню поддержки:") + "\n\n" + desc, reply_markup=_get_support_settings_keyboard(db_user.language), parse_mode="HTML" ) await callback.answer() @admin_required @error_handler async def toggle_support_menu( callback: types.CallbackQuery, db_user: User, db: AsyncSession ): current = SupportSettingsService.is_support_menu_enabled() SupportSettingsService.set_support_menu_enabled(not current) await show_support_settings(callback, db_user, db) @admin_required @error_handler async def toggle_admin_notifications(callback: types.CallbackQuery, db_user: User, db: AsyncSession): current = SupportSettingsService.get_admin_ticket_notifications_enabled() SupportSettingsService.set_admin_ticket_notifications_enabled(not current) await show_support_settings(callback, db_user, db) @admin_required @error_handler async def toggle_user_notifications(callback: types.CallbackQuery, db_user: User, db: AsyncSession): current = SupportSettingsService.get_user_ticket_notifications_enabled() SupportSettingsService.set_user_ticket_notifications_enabled(not current) await show_support_settings(callback, db_user, db) @admin_required @error_handler async def toggle_sla(callback: types.CallbackQuery, db_user: User, db: AsyncSession): current = SupportSettingsService.get_sla_enabled() SupportSettingsService.set_sla_enabled(not current) await show_support_settings(callback, db_user, db) class SupportAdvancedStates(StatesGroup): waiting_for_sla_minutes = State() waiting_for_moderator_id = State() @admin_required @error_handler async def start_set_sla_minutes(callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext): texts = get_texts(db_user.language) await callback.message.edit_text( texts.t( "ADMIN_SUPPORT_SLA_SETUP_PROMPT", "⏳ Настройка SLA\n\nВведите количество минут ожидания ответа (целое число > 0):" ), parse_mode="HTML", reply_markup=types.InlineKeyboardMarkup( inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_support_settings")]] ) ) await state.set_state(SupportAdvancedStates.waiting_for_sla_minutes) await callback.answer() @admin_required @error_handler async def handle_sla_minutes(message: types.Message, db_user: User, db: AsyncSession, state: FSMContext): texts = get_texts(db_user.language) text = (message.text or "").strip() try: minutes = int(text) if minutes <= 0 or minutes > 1440: raise ValueError() except Exception: await message.answer(texts.t("ADMIN_SUPPORT_SLA_INVALID", "❌ Введите корректное число минут (1-1440)")) return SupportSettingsService.set_sla_minutes(minutes) await state.clear() markup = types.InlineKeyboardMarkup( inline_keyboard=[[types.InlineKeyboardButton(text=texts.t("DELETE_MESSAGE", "🗑 Удалить"), callback_data="admin_support_delete_msg")]] ) await message.answer(texts.t("ADMIN_SUPPORT_SLA_SAVED", "✅ Значение SLA сохранено"), reply_markup=markup) @admin_required @error_handler async def start_add_moderator(callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext): texts = get_texts(db_user.language) await callback.message.edit_text( texts.t( "ADMIN_SUPPORT_ASSIGN_MODERATOR_PROMPT", "🧑‍⚖️ Назначение модератора\n\nОтправьте Telegram ID пользователя (число)" ), parse_mode="HTML", reply_markup=types.InlineKeyboardMarkup( inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_support_settings")]] ) ) await state.set_state(SupportAdvancedStates.waiting_for_moderator_id) await callback.answer() @admin_required @error_handler async def start_remove_moderator(callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext): texts = get_texts(db_user.language) await callback.message.edit_text( texts.t( "ADMIN_SUPPORT_REMOVE_MODERATOR_PROMPT", "🧑‍⚖️ Удаление модератора\n\nОтправьте Telegram ID пользователя (число)" ), parse_mode="HTML", reply_markup=types.InlineKeyboardMarkup( inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_support_settings")]] ) ) await state.set_state(SupportAdvancedStates.waiting_for_moderator_id) # We'll reuse the same state; next message will decide action via flag await state.update_data(action="remove_moderator") await callback.answer() @admin_required @error_handler async def handle_moderator_id(message: types.Message, db_user: User, db: AsyncSession, state: FSMContext): texts = get_texts(db_user.language) data = await state.get_data() action = data.get("action", "add") text = (message.text or "").strip() try: tid = int(text) except Exception: await message.answer(texts.t("ADMIN_SUPPORT_INVALID_TELEGRAM_ID", "❌ Введите корректный Telegram ID (число)")) return if action == "remove_moderator": ok = SupportSettingsService.remove_moderator(tid) msg = ( texts.t("ADMIN_SUPPORT_MODERATOR_REMOVED_SUCCESS", "✅ Модератор {tid} удалён").format(tid=tid) if ok else texts.t("ADMIN_SUPPORT_MODERATOR_REMOVED_FAIL", "❌ Не удалось удалить модератора") ) else: ok = SupportSettingsService.add_moderator(tid) msg = ( texts.t("ADMIN_SUPPORT_MODERATOR_ADDED_SUCCESS", "✅ Пользователь {tid} назначен модератором").format(tid=tid) if ok else texts.t("ADMIN_SUPPORT_MODERATOR_ADDED_FAIL", "❌ Не удалось назначить модератора") ) await state.clear() markup = types.InlineKeyboardMarkup( inline_keyboard=[[types.InlineKeyboardButton(text=texts.t("DELETE_MESSAGE", "🗑 Удалить"), callback_data="admin_support_delete_msg")]] ) await message.answer(msg, reply_markup=markup) @admin_required @error_handler async def list_moderators(callback: types.CallbackQuery, db_user: User, db: AsyncSession): texts = get_texts(db_user.language) moderators = SupportSettingsService.get_moderators() if not moderators: await callback.answer(texts.t("ADMIN_SUPPORT_MODERATORS_EMPTY", "Список пуст"), show_alert=True) return text = ( texts.t("ADMIN_SUPPORT_MODERATORS_TITLE", "🧑‍⚖️ Модераторы") + "\n\n" + "\n".join([f"• {tid}" for tid in moderators]) ) markup = types.InlineKeyboardMarkup( inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_support_settings")]] ) await callback.message.edit_text(text, parse_mode="HTML", reply_markup=markup) await callback.answer() @admin_required @error_handler async def set_mode_tickets(callback: types.CallbackQuery, db_user: User, db: AsyncSession): SupportSettingsService.set_system_mode("tickets") await show_support_settings(callback, db_user, db) @admin_required @error_handler async def set_mode_contact(callback: types.CallbackQuery, db_user: User, db: AsyncSession): SupportSettingsService.set_system_mode("contact") await show_support_settings(callback, db_user, db) @admin_required @error_handler async def set_mode_both(callback: types.CallbackQuery, db_user: User, db: AsyncSession): SupportSettingsService.set_system_mode("both") await show_support_settings(callback, db_user, db) @admin_required @error_handler async def start_edit_desc(callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext): texts = get_texts(db_user.language) current_desc_html = SupportSettingsService.get_support_info_text(db_user.language) # plain text for display-only code block current_desc_plain = re.sub(r"<[^>]+>", "", current_desc_html) kb_rows: list[list[types.InlineKeyboardButton]] = [] kb_rows.append([ types.InlineKeyboardButton( text=texts.t("ADMIN_SUPPORT_SEND_DESCRIPTION", "📨 Прислать текст"), callback_data="admin_support_send_desc" ) ]) # Подготовим блок контакта (отдельным инлайном) from app.config import settings support_contact_display = settings.get_support_contact_display() kb_rows.append([ types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_support_settings") ]) text_parts = [ texts.t("ADMIN_SUPPORT_EDIT_DESCRIPTION_TITLE", "📝 Редактирование описания поддержки"), "", texts.t("ADMIN_SUPPORT_EDIT_DESCRIPTION_CURRENT", "Текущее описание:"), "", f"{html.escape(current_desc_plain)}", ] if support_contact_display: text_parts += [ "", texts.t("ADMIN_SUPPORT_EDIT_DESCRIPTION_CONTACT_TITLE", "Контакт для режима «Контакт»"), f"{html.escape(support_contact_display)}", "", texts.t("ADMIN_SUPPORT_EDIT_DESCRIPTION_CONTACT_HINT", "Добавьте в описание при необходимости."), ] await callback.message.edit_text( "\n".join(text_parts), reply_markup=types.InlineKeyboardMarkup(inline_keyboard=kb_rows), parse_mode="HTML" ) await state.set_state(SupportSettingsStates.waiting_for_desc) await callback.answer() @admin_required @error_handler async def handle_new_desc(message: types.Message, db_user: User, db: AsyncSession, state: FSMContext): texts = get_texts(db_user.language) new_text = message.html_text or message.text SupportSettingsService.set_support_info_text(db_user.language, new_text) await state.clear() markup = types.InlineKeyboardMarkup( inline_keyboard=[[types.InlineKeyboardButton(text=texts.t("DELETE_MESSAGE", "🗑 Удалить"), callback_data="admin_support_delete_msg")]] ) await message.answer(texts.t("ADMIN_SUPPORT_DESCRIPTION_UPDATED", "✅ Описание обновлено."), reply_markup=markup) @admin_required @error_handler async def send_desc_copy(callback: types.CallbackQuery, db_user: User, db: AsyncSession): # send plain text for easy copying texts = get_texts(db_user.language) current_desc_html = SupportSettingsService.get_support_info_text(db_user.language) current_desc_plain = re.sub(r"<[^>]+>", "", current_desc_html) # attach delete button to the sent message markup = types.InlineKeyboardMarkup( inline_keyboard=[[types.InlineKeyboardButton(text=texts.t("DELETE_MESSAGE", "🗑 Удалить"), callback_data="admin_support_delete_msg")]] ) if len(current_desc_plain) <= 4000: await callback.message.answer(current_desc_plain, reply_markup=markup) else: # split long messages (attach delete only to the last chunk) chunk = 0 while chunk < len(current_desc_plain): next_chunk = current_desc_plain[chunk:chunk+4000] is_last = (chunk + 4000) >= len(current_desc_plain) await callback.message.answer(next_chunk, reply_markup=(markup if is_last else None)) chunk += 4000 await callback.answer(texts.t("ADMIN_SUPPORT_DESCRIPTION_SENT", "Текст отправлен ниже")) @error_handler async def delete_sent_message(callback: types.CallbackQuery, db_user: User, db: AsyncSession): # Allow admins and moderators to delete informational notifications try: may_delete = (settings.is_admin(callback.from_user.id) or SupportSettingsService.is_moderator(callback.from_user.id)) except Exception: may_delete = False texts = get_texts(db_user.language if db_user else 'ru') if not may_delete: await callback.answer(texts.ACCESS_DENIED, show_alert=True) return try: await callback.message.delete() finally: with contextlib.suppress(Exception): await callback.answer(texts.t("ADMIN_SUPPORT_MESSAGE_DELETED", "Сообщение удалено")) def register_handlers(dp: Dispatcher): dp.callback_query.register(show_support_settings, F.data == "admin_support_settings") dp.callback_query.register(toggle_support_menu, F.data == "admin_support_toggle_menu") dp.callback_query.register(set_mode_tickets, F.data == "admin_support_mode_tickets") dp.callback_query.register(set_mode_contact, F.data == "admin_support_mode_contact") dp.callback_query.register(set_mode_both, F.data == "admin_support_mode_both") dp.callback_query.register(start_edit_desc, F.data == "admin_support_edit_desc") dp.callback_query.register(send_desc_copy, F.data == "admin_support_send_desc") dp.callback_query.register(delete_sent_message, F.data == "admin_support_delete_msg") dp.callback_query.register(toggle_admin_notifications, F.data == "admin_support_toggle_admin_notifications") dp.callback_query.register(toggle_user_notifications, F.data == "admin_support_toggle_user_notifications") dp.callback_query.register(toggle_sla, F.data == "admin_support_toggle_sla") dp.callback_query.register(start_set_sla_minutes, F.data == "admin_support_set_sla_minutes") dp.callback_query.register(start_add_moderator, F.data == "admin_support_add_moderator") dp.callback_query.register(start_remove_moderator, F.data == "admin_support_remove_moderator") dp.callback_query.register(list_moderators, F.data == "admin_support_list_moderators") dp.message.register(handle_new_desc, SupportSettingsStates.waiting_for_desc) dp.message.register(handle_sla_minutes, SupportAdvancedStates.waiting_for_sla_minutes) dp.message.register(handle_moderator_id, SupportAdvancedStates.waiting_for_moderator_id)