Fix text dialog routing and club card registration recovery
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-09-13 21:39:40 +09:00
parent 3c1a6a02a5
commit f3f8d0f737
28 changed files with 788 additions and 334 deletions

View File

@@ -1,6 +1,10 @@
"""
Расширенная админ-панель для управления розыгрышами
"""
from src.utils.input_validation import lottery_field, numeric_id
from src.utils.text_output import answer_plain_pages
from html import escape
from src.filters.dialog_input import NotCommand
from src.utils.errors import public_error
from src.core.access import is_admin
from src.core.access import is_admin as check_admin_access
@@ -180,11 +184,12 @@ def get_winner_management_keyboard() -> InlineKeyboardMarkup:
@admin_router.callback_query(F.data == "admin_panel")
async def show_admin_panel(callback: CallbackQuery):
async def show_admin_panel(callback: CallbackQuery, state: FSMContext):
"""Показать админ-панель"""
if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True)
return
await state.clear()
async with async_session_maker() as session:
# Быстрая статистика
@@ -215,11 +220,12 @@ async def show_admin_panel(callback: CallbackQuery):
# ======================
@admin_router.callback_query(F.data.in_({"admin_lotteries", "lottery_management"}))
async def show_lottery_management(callback: CallbackQuery):
async def show_lottery_management(callback: CallbackQuery, state: FSMContext):
"""Управление розыгрышами"""
if not await check_admin_access(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True)
return
await state.clear()
text = "🎲 Управление розыгрышами\n\n"
text += "Здесь вы можете создавать, редактировать и управлять розыгрышами.\n\n"
@@ -243,6 +249,7 @@ async def start_create_lottery(callback: CallbackQuery, state: FSMContext):
logging.info(f"✅ Админ {callback.from_user.id} начинает создание розыгрыша")
await state.clear()
text = "📝 Создание нового розыгрыша\n\n"
text += "Шаг 1 из 4\n\n"
text += "Введите название розыгрыша:"
@@ -261,13 +268,19 @@ async def start_create_lottery(callback: CallbackQuery, state: FSMContext):
await callback.message.answer(f"❌ Ошибка: {public_error(e)}")
@admin_router.message(StateFilter(AdminStates.lottery_title))
@admin_router.message(StateFilter(AdminStates.lottery_title), F.text, NotCommand())
async def process_lottery_title(message: Message, state: FSMContext):
"""Обработка названия розыгрыша (создание или редактирование)"""
if not await check_admin_access(message.from_user.id):
await message.answer("❌ Недостаточно прав")
return
try:
value = lottery_field("title", message.text)
except ValueError as error:
await message.answer(str(error))
return
data = await state.get_data()
edit_lottery_id = data.get('edit_lottery_id')
@@ -277,51 +290,49 @@ async def process_lottery_title(message: Message, state: FSMContext):
success = await LotteryService.update_lottery(
session,
edit_lottery_id,
title=message.text
title=value
)
if success:
await message.answer(f"✅ Название изменено на: {message.text}")
await message.answer(f"✅ Название изменено на: {value}")
await state.clear()
# Возвращаемся к выбору полей
from aiogram.types import CallbackQuery
fake_callback = CallbackQuery(
id="fake",
from_user=message.from_user,
chat_instance="fake",
data=f"admin_edit_lottery_select_{edit_lottery_id}",
message=message
)
await choose_edit_field(fake_callback, state)
await show_edit_fields(message, edit_lottery_id)
else:
await message.answer("❌ Ошибка при изменении названия")
return
# Если это создание нового розыгрыша
await state.update_data(title=message.text)
await state.update_data(title=value)
text = f"📝 Создание нового розыгрыша\n\n"
text += f"Шаг 2 из 4\n\n"
text += f"✅ Название: {message.text}\n\n"
text += f"✅ Название: {value}\n\n"
text += f"Введите описание розыгрыша (или '-' для пропуска):"
await message.answer(text)
await answer_plain_pages(message, text)
await state.set_state(AdminStates.lottery_description)
@admin_router.message(StateFilter(AdminStates.lottery_description))
@admin_router.message(StateFilter(AdminStates.lottery_description), F.text, NotCommand())
async def process_lottery_description(message: Message, state: FSMContext):
"""Обработка описания розыгрыша (создание или редактирование)"""
if not await check_admin_access(message.from_user.id):
await message.answer("❌ Недостаточно прав")
return
try:
value = lottery_field("description", message.text)
except ValueError as error:
await message.answer(str(error))
return
data = await state.get_data()
edit_lottery_id = data.get('edit_lottery_id')
# Если это редактирование существующего розыгрыша
if edit_lottery_id:
description = None if message.text == "-" else message.text
description = value
async with async_session_maker() as session:
success = await LotteryService.update_lottery(
session,
@@ -333,21 +344,13 @@ async def process_lottery_description(message: Message, state: FSMContext):
await message.answer(f"✅ Описание изменено")
await state.clear()
# Возвращаемся к выбору полей
from aiogram.types import CallbackQuery
fake_callback = CallbackQuery(
id="fake",
from_user=message.from_user,
chat_instance="fake",
data=f"admin_edit_lottery_select_{edit_lottery_id}",
message=message
)
await choose_edit_field(fake_callback, state)
await show_edit_fields(message, edit_lottery_id)
else:
await message.answer("❌ Ошибка при изменении описания")
return
# Если это создание нового розыгрыша
description = None if message.text == "-" else message.text
description = value
await state.update_data(description=description)
data = await state.get_data()
@@ -363,21 +366,27 @@ async def process_lottery_description(message: Message, state: FSMContext):
text += f"🥉 AirPods Pro\n"
text += f"🏆 10,000 рублей"
await message.answer(text)
await answer_plain_pages(message, text)
await state.set_state(AdminStates.lottery_prizes)
@admin_router.message(StateFilter(AdminStates.lottery_prizes))
@admin_router.message(StateFilter(AdminStates.lottery_prizes), F.text, NotCommand())
async def process_lottery_prizes(message: Message, state: FSMContext):
"""Обработка призов розыгрыша (создание или редактирование)"""
if not await check_admin_access(message.from_user.id):
await message.answer("❌ Недостаточно прав")
return
try:
value = lottery_field("prizes", message.text)
except ValueError as error:
await message.answer(str(error))
return
data = await state.get_data()
edit_lottery_id = data.get('edit_lottery_id')
prizes = [prize.strip() for prize in message.text.split('\n') if prize.strip()]
prizes = value
# Если это редактирование существующего розыгрыша
if edit_lottery_id:
@@ -392,15 +401,7 @@ async def process_lottery_prizes(message: Message, state: FSMContext):
await message.answer(f"✅ Призы изменены")
await state.clear()
# Возвращаемся к выбору полей
from aiogram.types import CallbackQuery
fake_callback = CallbackQuery(
id="fake",
from_user=message.from_user,
chat_instance="fake",
data=f"admin_edit_lottery_select_{edit_lottery_id}",
message=message
)
await choose_edit_field(fake_callback, state)
await show_edit_fields(message, edit_lottery_id)
else:
await message.answer("❌ Ошибка при изменении призов")
return
@@ -420,7 +421,7 @@ async def process_lottery_prizes(message: Message, state: FSMContext):
text += f"\n✅ Подтвердите создание розыгрыша:"
await message.answer(
await answer_plain_pages(message,
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="✅ Создать", callback_data="confirm_create_lottery")],
@@ -765,7 +766,7 @@ async def choose_user_to_add(callback: CallbackQuery, state: FSMContext):
await state.set_state(AdminStates.add_participant_user)
@admin_router.message(StateFilter(AdminStates.add_participant_user), F.text)
@admin_router.message(StateFilter(AdminStates.add_participant_user), F.text, NotCommand())
async def process_add_participant(message: Message, state: FSMContext):
"""Обработка добавления участника"""
if not await check_admin_access(message.from_user.id):
@@ -886,7 +887,7 @@ async def remove_participant_select_lottery(callback: CallbackQuery, state: FSMC
)
@admin_router.message(StateFilter(AdminStates.remove_participant_user), F.text)
@admin_router.message(StateFilter(AdminStates.remove_participant_user), F.text, NotCommand())
async def process_remove_participant(message: Message, state: FSMContext):
"""Обработка удаления участника"""
if not await check_admin_access(message.from_user.id):
@@ -1161,7 +1162,7 @@ async def start_search_participants(callback: CallbackQuery, state: FSMContext):
await state.set_state(AdminStates.participant_search)
@admin_router.message(StateFilter(AdminStates.participant_search), F.text)
@admin_router.message(StateFilter(AdminStates.participant_search), F.text, NotCommand())
async def process_search_participants(message: Message, state: FSMContext):
"""Обработка поиска участников"""
if not await check_admin_access(message.from_user.id):
@@ -1169,6 +1170,9 @@ async def process_search_participants(message: Message, state: FSMContext):
return
search_term = message.text.strip()
if not 1 <= len(search_term) <= 100:
await message.answer("Поисковый запрос должен содержать от 1 до 100 символов. /cancel — отмена.")
return
async with async_session_maker() as session:
users = await UserService.search_users(session, search_term)
@@ -1279,7 +1283,7 @@ async def choose_users_bulk_add(callback: CallbackQuery, state: FSMContext):
await state.set_state(AdminStates.add_participant_bulk)
@admin_router.message(StateFilter(AdminStates.add_participant_bulk))
@admin_router.message(StateFilter(AdminStates.add_participant_bulk), F.text, NotCommand())
async def process_bulk_add_participant(message: Message, state: FSMContext):
"""Обработка массового добавления участников"""
if not await check_admin_access(message.from_user.id):
@@ -1290,7 +1294,10 @@ async def process_bulk_add_participant(message: Message, state: FSMContext):
lottery_id = data['bulk_add_lottery_id']
# Парсим входные данные
user_inputs = [x.strip() for x in message.text.split(',') if x.strip()]
user_inputs = [x.strip() for x in message.text.replace('\n', ',').split(',') if x.strip()]
if not user_inputs:
await message.answer('Введите хотя бы один Telegram ID или @username, либо /cancel.')
return
telegram_ids = []
async with async_session_maker() as session:
@@ -1302,11 +1309,14 @@ async def process_bulk_add_participant(message: Message, state: FSMContext):
if user:
telegram_ids.append(user.telegram_id)
elif user_input.isdigit():
telegram_ids.append(int(user_input))
telegram_ids.append(numeric_id(user_input))
except:
continue
# Массовое добавление
if not telegram_ids or len(telegram_ids) != len(user_inputs):
await message.answer("Часть ID или username неверна либо не найдена. Исправьте список и повторите ввод. Изменения не внесены.")
return
results = await ParticipationService.add_participants_bulk(session, lottery_id, telegram_ids)
lottery = await LotteryService.get_lottery(session, lottery_id)
@@ -1410,7 +1420,7 @@ async def choose_users_bulk_remove(callback: CallbackQuery, state: FSMContext):
await state.set_state(AdminStates.remove_participant_bulk)
@admin_router.message(StateFilter(AdminStates.remove_participant_bulk))
@admin_router.message(StateFilter(AdminStates.remove_participant_bulk), F.text, NotCommand())
async def process_bulk_remove_participant(message: Message, state: FSMContext):
"""Обработка массового удаления участников"""
if not await check_admin_access(message.from_user.id):
@@ -1421,7 +1431,10 @@ async def process_bulk_remove_participant(message: Message, state: FSMContext):
lottery_id = data['bulk_remove_lottery_id']
# Парсим входные данные
user_inputs = [x.strip() for x in message.text.split(',') if x.strip()]
user_inputs = [x.strip() for x in message.text.replace('\n', ',').split(',') if x.strip()]
if not user_inputs:
await message.answer('Введите хотя бы один Telegram ID или @username, либо /cancel.')
return
telegram_ids = []
async with async_session_maker() as session:
@@ -1433,11 +1446,14 @@ async def process_bulk_remove_participant(message: Message, state: FSMContext):
if user:
telegram_ids.append(user.telegram_id)
elif user_input.isdigit():
telegram_ids.append(int(user_input))
telegram_ids.append(numeric_id(user_input))
except:
continue
# Массовое удаление
if not telegram_ids or len(telegram_ids) != len(user_inputs):
await message.answer("Часть ID или username неверна либо не найдена. Исправьте список и повторите ввод. Изменения не внесены.")
return
results = await ParticipationService.remove_participants_bulk(session, lottery_id, telegram_ids)
lottery = await LotteryService.get_lottery(session, lottery_id)
@@ -1501,7 +1517,7 @@ async def add_participant_to_lottery(callback: CallbackQuery, state: FSMContext)
await state.set_state(AdminStates.add_to_lottery_user)
@admin_router.message(StateFilter(AdminStates.add_to_lottery_user), F.text)
@admin_router.message(StateFilter(AdminStates.add_to_lottery_user), F.text, NotCommand())
async def process_add_to_lottery(message: Message, state: FSMContext):
"""Обработка добавления участника в конкретный розыгрыш"""
if not await is_admin(message.from_user.id):
@@ -1637,7 +1653,7 @@ async def remove_participant_from_lottery(callback: CallbackQuery, state: FSMCon
await state.set_state(AdminStates.remove_from_lottery_user)
@admin_router.message(StateFilter(AdminStates.remove_from_lottery_user), F.text)
@admin_router.message(StateFilter(AdminStates.remove_from_lottery_user), F.text, NotCommand())
async def process_remove_from_lottery(message: Message, state: FSMContext):
"""Обработка удаления участника из конкретного розыгрыша"""
if not await is_admin(message.from_user.id):
@@ -2002,7 +2018,7 @@ async def choose_accounts_bulk_add(callback: CallbackQuery, state: FSMContext):
await state.set_state(AdminStates.add_participant_bulk_accounts)
@admin_router.message(StateFilter(AdminStates.add_participant_bulk_accounts))
@admin_router.message(StateFilter(AdminStates.add_participant_bulk_accounts), F.text, NotCommand())
async def process_bulk_add_accounts(message: Message, state: FSMContext):
"""Обработка массового добавления участников по номерам счетов"""
if not await check_admin_access(message.from_user.id):
@@ -2015,6 +2031,9 @@ async def process_bulk_add_accounts(message: Message, state: FSMContext):
# Используем функцию парсинга из account_utils для корректной обработки формата "КАРТА СЧЕТ"
from ..utils.account_utils import parse_accounts_from_message
account_inputs = parse_accounts_from_message(message.text)
if not account_inputs:
await message.answer("Счета не распознаны. Используйте формат 11-22-33-44-55-66-77 или /cancel.")
return
async with async_session_maker() as session:
# Массовое добавление по номерам счетов
@@ -2127,7 +2146,7 @@ async def choose_accounts_bulk_remove(callback: CallbackQuery, state: FSMContext
await state.set_state(AdminStates.remove_participant_bulk_accounts)
@admin_router.message(StateFilter(AdminStates.remove_participant_bulk_accounts))
@admin_router.message(StateFilter(AdminStates.remove_participant_bulk_accounts), F.text, NotCommand())
async def process_bulk_remove_accounts(message: Message, state: FSMContext):
"""Обработка массового удаления участников по номерам счетов"""
if not await check_admin_access(message.from_user.id):
@@ -2140,6 +2159,9 @@ async def process_bulk_remove_accounts(message: Message, state: FSMContext):
# Используем функцию парсинга из account_utils для корректной обработки формата "КАРТА СЧЕТ"
from ..utils.account_utils import parse_accounts_from_message
account_inputs = parse_accounts_from_message(message.text)
if not account_inputs:
await message.answer("Счета не распознаны. Используйте формат 11-22-33-44-55-66-77 или /cancel.")
return
async with async_session_maker() as session:
# Массовое удаление по номерам счетов
@@ -2408,13 +2430,23 @@ async def choose_edit_field(callback: CallbackQuery, state: FSMContext):
lottery_id = int(callback.data.split("_")[-1])
await state.update_data(edit_lottery_id=lottery_id)
await state.set_state(None)
await callback.answer()
await show_edit_fields(callback.message, lottery_id, edit=True)
async def show_edit_fields(message: Message, lottery_id: int, edit=False):
async with async_session_maker() as session:
lottery = await LotteryService.get_lottery(session, lottery_id)
if lottery is None:
await message.answer("Розыгрыш не найден. Откройте /admin.")
return
text = f"📝 Редактирование: {lottery.title}\n\n"
text += "Выберите, что хотите изменить:\n\n"
text += f"📝 Название: {lottery.title}\n"
text += f"📄 Описание: {lottery.description[:50]}{'...' if len(lottery.description) > 50 else ''}\n"
text += f"📄 Описание: {(lottery.description or "")[:50]}{'...' if len(lottery.description or "") > 50 else ''}\n"
text += f"🎁 Призы: {len(getattr(lottery, 'prizes', []))} шт.\n"
text += f"🎭 Отображение: {getattr(lottery, 'winner_display_type', 'username')}\n"
text += f"🟢 Активен: {'Да' if getattr(lottery, 'is_active', True) else 'Нет'}"
@@ -2431,7 +2463,7 @@ async def choose_edit_field(callback: CallbackQuery, state: FSMContext):
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_edit_lottery")]
]
await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
await (message.edit_text if edit else message.answer)(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
@admin_router.callback_query(F.data.startswith("admin_toggle_active_"))
@@ -2776,7 +2808,7 @@ async def choose_winner_place(callback: CallbackQuery, state: FSMContext):
await state.set_state(AdminStates.set_winner_place)
@admin_router.message(StateFilter(AdminStates.set_winner_place))
@admin_router.message(StateFilter(AdminStates.set_winner_place), F.text, NotCommand())
async def process_winner_place(message: Message, state: FSMContext):
"""Обработка места победителя"""
if not await check_admin_access(message.from_user.id):
@@ -2784,7 +2816,7 @@ async def process_winner_place(message: Message, state: FSMContext):
return
try:
place = int(message.text)
place = numeric_id(message.text, maximum=2**31 - 1)
if place < 1:
raise ValueError
except ValueError:
@@ -2798,6 +2830,9 @@ async def process_winner_place(message: Message, state: FSMContext):
async with async_session_maker() as session:
lottery = await LotteryService.get_lottery(session, lottery_id)
if not lottery or place > len(lottery.prizes or []):
await message.answer("Номер места должен соответствовать одному из призов розыгрыша.")
return
if lottery.manual_winners and str(place) in lottery.manual_winners:
existing_id = lottery.manual_winners[str(place)]
existing_user = await UserService.get_user_by_telegram_id(session, existing_id)
@@ -2824,7 +2859,7 @@ async def process_winner_place(message: Message, state: FSMContext):
await state.set_state(AdminStates.set_winner_user)
@admin_router.message(StateFilter(AdminStates.set_winner_user), F.text)
@admin_router.message(StateFilter(AdminStates.set_winner_user), F.text, NotCommand())
async def process_winner_user(message: Message, state: FSMContext):
"""Обработка пользователя-победителя (по ID, username или номеру счета)"""
if not await check_admin_access(message.from_user.id):
@@ -4397,7 +4432,7 @@ async def admin_import_users_start(callback: CallbackQuery, state: FSMContext):
await state.set_state(AdminStates.import_users_json)
@admin_router.message(StateFilter(AdminStates.import_users_json), F.document)
@admin_router.message(StateFilter(AdminStates.import_users_json), F.document, NotCommand())
async def admin_import_users_process(message: Message, state: FSMContext):
if not await check_admin_access(message.from_user.id):
return
@@ -4644,7 +4679,7 @@ BROADCAST_FAILURE_TEXT = "❌ Рассылка прервана. Часть со
BROADCAST_BUSY_TEXT = "⏳ Ваша предыдущая рассылка ещё выполняется или заняты оба места отправки. Повторите запуск позже через /admin → Рассылки."
@admin_router.message(StateFilter(AdminStates.broadcast_message), F.text | F.photo | F.video | F.document | F.animation | F.audio | F.voice | F.sticker)
@admin_router.message(StateFilter(AdminStates.broadcast_message), F.text | F.photo | F.video | F.document | F.animation | F.audio | F.voice | F.sticker, NotCommand())
async def admin_broadcast_send(message: Message, state: FSMContext):
"""Обработка и отправка рассылки"""
if not await check_admin_access(message.from_user.id):
@@ -4876,14 +4911,14 @@ async def admin_broadcast_add_channel_start(callback: CallbackQuery, state: FSMC
await state.set_state(AdminStates.broadcast_add_channel_id)
@admin_router.message(StateFilter(AdminStates.broadcast_add_channel_id), F.text)
@admin_router.message(StateFilter(AdminStates.broadcast_add_channel_id), F.text, NotCommand())
async def admin_broadcast_add_channel_id(message: Message, state: FSMContext):
"""Обработка ID канала"""
if not await check_admin_access(message.from_user.id):
return
try:
chat_id = int(message.text.strip())
chat_id = numeric_id(message.text, signed=True)
except ValueError:
await message.answer(
"❌ Неверный формат ID. Отправьте число, например: -1001234567890"
@@ -4903,7 +4938,6 @@ async def admin_broadcast_add_channel_id(message: Message, state: FSMContext):
await message.answer(
"❌ Неверный тип чата. Поддерживаются только каналы и группы."
)
await state.clear()
return
# Сохраняем данные
@@ -4917,7 +4951,7 @@ async def admin_broadcast_add_channel_id(message: Message, state: FSMContext):
# Запрашиваем описание
text = (
f"✅ <b>Канал найден!</b>\n\n"
f"📱 <b>Название:</b> {chat.title}\n"
f"📱 <b>Название:</b> {escape(chat.title or "")}\n"
f"🆔 <b>ID:</b> <code>{chat_id}</code>\n"
f"📝 <b>Тип:</b> {'Канал' if chat_type == 'channel' else 'Группа'}\n"
)
@@ -4939,10 +4973,9 @@ async def admin_broadcast_add_channel_id(message: Message, state: FSMContext):
f"Детали: {public_error(e)}",
parse_mode="HTML"
)
await state.clear()
@admin_router.message(StateFilter(AdminStates.broadcast_add_channel_title), F.text)
@admin_router.message(StateFilter(AdminStates.broadcast_add_channel_title), F.text, NotCommand("skip"))
async def admin_broadcast_add_channel_description(message: Message, state: FSMContext):
"""Обработка описания канала"""
if not await check_admin_access(message.from_user.id):
@@ -4950,7 +4983,7 @@ async def admin_broadcast_add_channel_description(message: Message, state: FSMCo
data = await state.get_data()
description = None if message.text.strip() == '/skip' else message.text.strip()
description = None if message.text.strip().split('@', 1)[0].lower() == '/skip' else message.text.strip()
# Сохраняем в БД
async with async_session_maker() as session:
@@ -4977,10 +5010,11 @@ async def admin_broadcast_add_channel_description(message: Message, state: FSMCo
existing.description = description
existing.chat_type = data['chat_type']
await session.commit()
await state.clear()
await message.answer(
"✅ <b>Канал обновлен!</b>\n\n"
f"📱 {data['title']}",
f"📱 {escape(data['title'])}",
parse_mode="HTML"
)
else:
@@ -4995,10 +5029,11 @@ async def admin_broadcast_add_channel_description(message: Message, state: FSMCo
)
session.add(channel)
await session.commit()
await state.clear()
await message.answer(
"✅ <b>Канал добавлен!</b>\n\n"
f"📱 {data['title']}\n"
f"📱 {escape(data['title'])}\n"
f"Теперь вы можете использовать его для рассылок.",
parse_mode="HTML"
)
@@ -5209,13 +5244,16 @@ async def admin_users_search_prompt(callback: CallbackQuery, state: FSMContext):
await state.set_state(AdminStates.user_management_search)
@admin_router.message(AdminStates.user_management_search, F.text)
@admin_router.message(AdminStates.user_management_search, F.text, NotCommand())
async def admin_users_search_process(message: Message, state: FSMContext):
"""Обработка поискового запроса"""
if not await check_admin_access(message.from_user.id):
return
query = message.text.strip()
if not 1 <= len(query) <= 100:
await message.answer("Поисковый запрос должен содержать от 1 до 100 символов. /cancel — отмена.")
return
if query == "/cancel":
await message.answer("❌ Поиск отменен")
@@ -5233,12 +5271,12 @@ async def admin_users_search_process(message: Message, state: FSMContext):
)
if not users:
text = f"❌ По запросу «{query}» ничего не найдено"
text = f"❌ По запросу «{escape(query)}» ничего не найдено"
buttons = [
[InlineKeyboardButton(text="◀️ В управление пользователями", callback_data="admin_users")]
]
else:
text = f"🔍 <b>Результаты поиска:</b> «{query}»\n"
text = f"🔍 <b>Результаты поиска:</b> «{escape(query)}»\n"
text += f"Найдено: {total} пользователей\n\n"
buttons = []
@@ -5262,7 +5300,7 @@ async def admin_users_search_process(message: Message, state: FSMContext):
if total > 15:
nav_buttons.append(InlineKeyboardButton(
text="➡️ Далее",
callback_data=f"admin_users_search_page:{query}:2"
callback_data=f"admin_users_search_page:{escape(query)}:2"
))
buttons.append(nav_buttons)