From 0a8801afecdb120530aaa447c089d4181761b352 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Fri, 27 Feb 2026 18:11:46 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=BE=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B1=D0=BB?= =?UTF-8?q?=D0=BE=D0=BA=D0=BE=D0=B2=20=D0=BA=D0=BE=D0=B4=D0=B0=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=20=D1=80=D0=B0=D0=B7=D0=B1=D0=B8=D0=B2=D0=BA=D0=B5?= =?UTF-8?q?=20=D0=B4=D0=BB=D0=B8=D0=BD=D0=BD=D1=8B=D1=85=20=D1=81=D0=BE?= =?UTF-8?q?=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Qwen-Coder --- bot/utils/formatters.py | 94 +++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 37 deletions(-) diff --git a/bot/utils/formatters.py b/bot/utils/formatters.py index 0d96397..cf16efb 100644 --- a/bot/utils/formatters.py +++ b/bot/utils/formatters.py @@ -58,87 +58,110 @@ def find_code_blocks(text: str) -> List[Tuple[int, int]]: return blocks -def split_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> List[Tuple[str, bool]]: +def split_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> List[Tuple[str, bool, bool, bool]]: """ Умно разбить длинный текст на сообщения <= max_length символов. - - Возвращает список кортежей (text, has_markdown): + + Возвращает список кортежей (text, has_code, code_opened, code_closed): - text: текст сообщения - - has_markdown: True если сообщение содержит блоки кода - + - has_code: True если сообщение содержит часть блока кода + - code_opened: True если блок кода ОТКРЫТ в этом сообщении (есть открывающий ```) + - code_closed: True если блок кода ЗАКРЫТ в этом сообщении (есть закрывающий ```) + Алгоритм: 1. Находим все блоки кода 2. Стараемся не разрывать блоки кода 3. Если блок кода не влезает — отправляем отдельным сообщением без Markdown """ if len(text) <= max_length: - return [(text, '```' in text)] - + has_code = '```' in text + # Считаем количество ``` для определения открыт/закрыт + backtick_count = text.count('```') + # Если 1 или нечётное количество — блок открыт но не закрыт (или наоборот) + code_opened = backtick_count >= 1 and (backtick_count % 2 == 1) + code_closed = backtick_count >= 2 and (backtick_count % 2 == 0) + return [(text, has_code, code_opened, code_closed)] + parts = [] code_blocks = find_code_blocks(text) - + # Текущая позиция в тексте pos = 0 current = "" current_has_code = False - + current_code_opened = False + current_code_closed = False + for block_start, block_end in code_blocks: # Текст до блока кода before_code = text[pos:block_start] - + # Сам блок кода (включая ```) code_block = text[block_start:block_end] - + # Обрабатываем текст до блока for line in before_code.split('\n'): if len(current) + len(line) + 1 > max_length - RESERVED_FOR_HEADER: if current: - parts.append((current, current_has_code)) + parts.append((current, current_has_code, current_code_opened, current_code_closed)) current = line current_has_code = False + current_code_opened = False + current_code_closed = False else: current += ('\n' if current else '') + line - + # Проверяем влезет ли блок кода if len(current) + len(code_block) + 1 > max_length - RESERVED_FOR_HEADER: # Отправляем что накопилось if current: - parts.append((current, current_has_code)) - + parts.append((current, current_has_code, current_code_opened, current_code_closed)) + # Если блок кода слишком длинный — режем его на части if len(code_block) > max_length - RESERVED_FOR_HEADER: # Отправляем блок кода частями без Markdown for i in range(0, len(code_block), max_length - RESERVED_FOR_HEADER): chunk = code_block[i:i + max_length - RESERVED_FOR_HEADER] - parts.append((chunk, False)) # Без Markdown! + # Первая часть имеет открывающий ```, последняя — закрывающий + first_chunk = (i == 0) + last_chunk = (i + max_length - RESERVED_FOR_HEADER >= len(code_block)) + parts.append((chunk, True, first_chunk, last_chunk)) current = "" current_has_code = False + current_code_opened = False + current_code_closed = False else: # Блок влезает в следующее сообщение current = code_block current_has_code = True + current_code_opened = True # Блок начинается с ``` + current_code_closed = True # Блок заканчивается на ``` else: # Блок кода влезает в текущее сообщение current += ('\n' if current else '') + code_block current_has_code = True - + current_code_opened = True # Блок содержит открывающий ``` + current_code_closed = True # Блок содержит закрывающий ``` + pos = block_end - + # Обрабатываем остаток текста после последнего блока if pos < len(text): remaining = text[pos:] for line in remaining.split('\n'): if len(current) + len(line) + 1 > max_length - RESERVED_FOR_HEADER: if current: - parts.append((current, current_has_code)) + parts.append((current, current_has_code, current_code_opened, current_code_closed)) current = line current_has_code = False + current_code_opened = False + current_code_closed = False else: current += ('\n' if current else '') + line - + if current: - parts.append((current, current_has_code)) - + parts.append((current, current_has_code, current_code_opened, current_code_closed)) + return parts @@ -150,9 +173,7 @@ async def send_long_message(update: Update, text: str, parse_mode: str = None, p - Блоки кода не разрываются между сообщениями - Если блок кода не влезает — отправляется без Markdown - Нумерация (1/N), (2/N) только если сообщений > 1 - - Если сообщение содержит блок кода, он закрывается в конце и открывается в начале следующего - - КАЖДЫЕ pause_after сообщений — пауза с кнопками "Продолжить" / "Отменить" - - После нажатия кнопки — они удаляются + - КАЖДЫЕ pause_every сообщений — пауза с кнопками "Продолжить" / "Отменить" Args: update: Telegram update @@ -168,7 +189,7 @@ async def send_long_message(update: Update, text: str, parse_mode: str = None, p messages_sent = 0 wait_msg = None - for i, (part, has_code) in enumerate(parts): + for i, (part, has_code, code_opened, code_closed) in enumerate(parts): # Добавляем номер части если их несколько if total > 1: header = f"({i+1}/{total}) " @@ -176,20 +197,19 @@ async def send_long_message(update: Update, text: str, parse_mode: str = None, p part = header + part # Определяем parse_mode для этого сообщения - if parse_mode and has_code: - actual_parse_mode = parse_mode - elif parse_mode and not has_code: - actual_parse_mode = parse_mode - else: - actual_parse_mode = None + actual_parse_mode = parse_mode if parse_mode and has_code else None - # Если это не первое сообщение и предыдущее имело блок кода — открываем блок - # Если это не последнее сообщение и текущее имеет блок кода — закрываем блок - if total > 1 and actual_parse_mode: - if i > 0 and parts[i-1][1]: # Предыдущее имело блок кода + # Логика работы с блоками кода между сообщениями: + # - Если предыдущее сообщение не закрыло блок (code_closed=False) и имело код — открываем в этом + # - Если текущее сообщение не закрывает блок (code_closed=False) и следующее имеет код — не закрываем + if total > 1 and actual_parse_mode and has_code: + # Проверяем нужно ли открыть блок в начале этого сообщения + if i > 0 and not parts[i-1][3]: # предыдущее не закрыло блок (parts[i-1][3] = code_closed) part = "```\n" + part - if i < total - 1 and has_code: # Следующее будет иметь блок кода + # Проверяем нужно ли закрыть блок в конце этого сообщения + # Закрываем только если следующее сообщение НЕ имеет кода + if i < total - 1 and not code_closed and not parts[i+1][1]: # следующее не имеет кода part = part + "\n```" try: