From d20092730e6fb94065ab9a7f2b8c16484a4b2fc4 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Fri, 27 Feb 2026 20:01:28 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20polling=20=D0=BF=D0=BE=D0=B4=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D0=B6=D0=B8=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BE=D0=BA=20?= =?UTF-8?q?=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20asyncio.Event?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Qwen-Coder --- bot/handlers/callbacks.py | 12 ----- bot/utils/formatters.py | 97 ++++++++++++++------------------------- 2 files changed, 34 insertions(+), 75 deletions(-) diff --git a/bot/handlers/callbacks.py b/bot/handlers/callbacks.py index 8ca419d..34c0b23 100644 --- a/bot/handlers/callbacks.py +++ b/bot/handlers/callbacks.py @@ -62,12 +62,6 @@ async def menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): await query.delete_message() except Exception as e: logger.warning(f"Не удалось удалить сообщение с кнопками: {e}") - # Устанавливаем event чтобы разблокировать send_long_message - if state.output_continue_event: - logger.info("callback: устанавливаем continue_event") - state.output_continue_event.set() - else: - logger.warning("callback: output_continue_event не найден!") await query.answer() return @@ -82,12 +76,6 @@ async def menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): await query.delete_message() except Exception as e: logger.warning(f"Не удалось удалить сообщение с кнопками: {e}") - # Устанавливаем event чтобы разблокировать send_long_message - if state.output_continue_event: - logger.info("callback: устанавливаем continue_event (отмена)") - state.output_continue_event.set() - else: - logger.warning("callback: output_continue_event не найден!") await query.answer() return diff --git a/bot/utils/formatters.py b/bot/utils/formatters.py index 632d2e9..aa2012a 100644 --- a/bot/utils/formatters.py +++ b/bot/utils/formatters.py @@ -17,13 +17,13 @@ RESERVED_FOR_HEADER = 20 # Резервируем место для "(N/N) " def escape_markdown(text: str) -> str: """ Экранирование специальных символов Markdown для Telegram API. - + Telegram Markdown v1 использует: * _ ` [ ] ( ) Эти символы нужно экранировать обратным слэшем. """ if not text: return text - + # Экранируем специальные символы Markdown # Порядок важен: сначала экранируем обратные слэши text = text.replace('\\', '\\\\') @@ -34,7 +34,7 @@ def escape_markdown(text: str) -> str: text = text.replace(']', '\\]') text = text.replace('(', '\\(') text = text.replace(')', '\\)') - + return text @@ -46,7 +46,7 @@ def find_code_blocks(text: str) -> List[Tuple[int, int]]: blocks = [] pattern = re.compile(r'```') matches = list(pattern.finditer(text)) - + # Пары start-end для каждого блока i = 0 while i < len(matches) - 1: @@ -54,7 +54,7 @@ def find_code_blocks(text: str) -> List[Tuple[int, int]]: end = matches[i + 1].end() blocks.append((start, end)) i += 2 - + return blocks @@ -89,12 +89,11 @@ def split_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> List[Tuple lines = text.split('\n') current = "" in_code_block = False # Состояние: внутри блока кода или нет - code_block_started_in_current = False # Блок кода был открыт в текущей части for line in lines: # Проверяем, содержит ли строка ``` backticks_in_line = line.count('```') - + # Если строка содержит нечётное количество ```, она меняет состояние if backticks_in_line % 2 == 1: # Эта строка содержит ``` который меняет состояние @@ -108,10 +107,8 @@ def split_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> List[Tuple # code_closed=False потому что блок продолжится parts.append((current, has_code, code_opened, False)) current = line - code_block_started_in_current = False else: current = test_line - code_block_started_in_current = True in_code_block = False else: # Были снаружи — эта строка открывает блок @@ -121,10 +118,8 @@ def split_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> List[Tuple has_code, code_opened, code_closed = calc_code_flags(current) parts.append((current, has_code, code_opened, code_closed)) current = line - code_block_started_in_current = True else: current = test_line - code_block_started_in_current = True in_code_block = True else: # Строка не меняет состояние @@ -136,7 +131,6 @@ def split_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> List[Tuple code_closed = not in_code_block parts.append((current, has_code, code_opened, code_closed)) current = line - code_block_started_in_current = in_code_block else: current = test_line @@ -151,22 +145,9 @@ def split_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> List[Tuple async def send_long_message(update: Update, text: str, parse_mode: str = None, pause_every: int = 3): """ Отправить длинный текст, разбив на несколько сообщений. - - Умная разбивка: - - Блоки кода не разрываются между сообщениями - - Если блок кода не влезает — отправляется без Markdown - - Нумерация (1/N), (2/N) только если сообщений > 1 - - КАЖДЫЕ pause_every сообщений — пауза с кнопками "Продолжить" / "Отменить" - - Ждём нажатия кнопки бесконечно, не продолжаем автоматически - - Args: - update: Telegram update - text: Текст для отправки - parse_mode: Режим парсинга (Markdown) - pause_every: Каждые сколько сообщений делать паузу (0 = без паузы) + Использует polling для ожидания нажатия кнопки (не блокирует event loop). """ from telegram import InlineKeyboardButton, InlineKeyboardMarkup - import asyncio parts = split_message(text) total = len(parts) @@ -175,27 +156,18 @@ async def send_long_message(update: Update, text: str, parse_mode: str = None, p for i, (part, has_code, code_opened, code_closed) in enumerate(parts): # Определяем parse_mode для этого сообщения - # Используем parse_mode если сообщение имеет код ИЛИ если мы внутри блока кода - # Мы внутри блока кода если: предыдущее не закрыло ИЛИ (текущее открыто и не закрыто) prev_closed = parts[i-1][3] if i > 0 else True in_code_block = not prev_closed or (code_opened and not code_closed) actual_parse_mode = parse_mode if parse_mode and (has_code or in_code_block) else None - # Логика работы с блоками кода между сообщениями: - # Если предыдущее сообщение не закрыло блок — нужно открыть в этом - # Если текущее сообщение не закрыло блок — нужно закрыть в следующем + # Логика работы с блоками кода между сообщениями if total > 1 and actual_parse_mode: - # Проверяем нужно ли открыть блок в начале этого сообщения if i > 0 and not parts[i-1][3]: # предыдущее не закрыло блок part = "```\n" + part + if i < total - 1 and not code_closed and not parts[i+1][2]: # следующее не открывает блок + part = part + "\n```" - # Проверяем нужно ли закрыть блок в конце этого сообщения - if i < total - 1 and not code_closed: - # Следующее сообщение не начинается с ``` — закрываем блок - if not parts[i+1][2]: # следующее не открывает блок - part = part + "\n```" - - # Добавляем номер части ПОСЛЕ работы с блоками кода + # Добавляем номер части if total > 1: header = f"({i+1}/{total}) " if len(header) + len(part) <= MAX_MESSAGE_LENGTH: @@ -204,16 +176,13 @@ async def send_long_message(update: Update, text: str, parse_mode: str = None, p try: await update.message.reply_text(part, parse_mode=actual_parse_mode) except Exception as e: - # Фоллбэк: отправляем без разметки logger.debug(f"Ошибка Markdown, отправляем без разметки: {e}") await update.message.reply_text(part) messages_sent += 1 - - # Небольшая пауза между сообщениями await asyncio.sleep(0.1) - # КАЖДЫЕ pause_every сообщений — спрашиваем продолжать ли + # КАЖДЫЕ pause_every сообщений — спрашивать продолжать ли if pause_every > 0 and messages_sent % pause_every == 0 and i < total - 1: remaining = total - (i + 1) keyboard = InlineKeyboardMarkup([ @@ -231,47 +200,49 @@ async def send_long_message(update: Update, text: str, parse_mode: str = None, p reply_markup=keyboard ) - # Ждём ответа пользователя через asyncio.Event (не блокируем event loop) + # Ждём через polling (короткие паузы дают event loop обработать callback) from bot.config import state_manager - import asyncio user_id = update.effective_user.id - - # Создаём Event для этого пользователя - continue_event = asyncio.Event() state = state_manager.get(user_id) state.waiting_for_output_control = True state.output_remaining = remaining state.output_wait_message = wait_msg - state.output_continue_event = continue_event + state.continue_output = None # None = ещё не решил logger.info(f"send_long_message: ждём нажатия кнопки (user_id={user_id}, remaining={remaining})") - # Ждём пока callback handler не установит event - await continue_event.wait() + # Polling с короткими паузами (даём event loop обработать callback) + for _ in range(600): # Максимум 600 * 0.5 = 300 секунд = 5 минут + await asyncio.sleep(0.5) + state = state_manager.get(user_id) + if state.continue_output is not None: + # Пользователь нажал кнопку + break logger.info(f"send_long_message: кнопка нажата, continue_output={state.continue_output}") - # Пользователь ответил - if state.continue_output: - # Продолжаем - удаляем кнопки + # Проверяем решение пользователя + if not state.continue_output: + # Отменил try: await wait_msg.delete() except: pass - else: - # Отменил - удаляем сообщение с кнопками - try: - await wait_msg.delete() - except: - pass - return # Прерываем вывод + state.waiting_for_output_control = False + state.output_remaining = None + state.output_wait_message = None + return + + # Продолжаем + try: + await wait_msg.delete() + except: + pass - # Очищаем состояние state.waiting_for_output_control = False state.output_remaining = None state.output_wait_message = None - state.output_continue_event = None def format_long_output(text: str, max_lines: int = 100, head_lines: int = 50, tail_lines: int = 50) -> str: