Починен краш Message_too_long: умная разбивка на чанки с code-блоками
- format_pairings возвращает структуру (title/rows/header/footer) - _render_chunks разбивает пары на группы по ~30 пар - Каждый чанк — свой code-блок с заголовком - Убраны разделители между парами (экономия ~50% размера) - Fallback plain text при любой ошибке MarkdownV2
This commit is contained in:
parent
6bb1d238aa
commit
bd8cb7d9fc
1 changed files with 73 additions and 66 deletions
|
|
@ -68,7 +68,6 @@ def format_pairings(pairings_data: dict, tournament_name: str) -> str:
|
|||
pairings = pairings_data['pairings']
|
||||
rnd = pairings_data['round']
|
||||
|
||||
# Find the longest player name + 3 spaces padding
|
||||
max_name_w = 20
|
||||
for pairing in pairings:
|
||||
if len(pairing) == 3:
|
||||
|
|
@ -81,31 +80,38 @@ def format_pairings(pairings_data: dict, tournament_name: str) -> str:
|
|||
w_board, w_pts = 4, 5
|
||||
border = '─┼─'.join('─' * w for w in [w_board, max_name_w, w_pts])
|
||||
|
||||
lines = [
|
||||
f'*{_md_escape(tournament_name)}*',
|
||||
f'📋 *Тур {rnd}* — пары',
|
||||
'',
|
||||
'```',
|
||||
f'{_pad_col("#", w_board)} │ {_pad_col("Игрок", max_name_w)} │ '
|
||||
f'{_pad_col("О", w_pts)}',
|
||||
border,
|
||||
]
|
||||
header = f'{_pad_col("#", w_board)} │ {_pad_col("Игрок", max_name_w)} │ {_pad_col("О", w_pts)}'
|
||||
table_header = f'```\n{header}\n{border}\n'
|
||||
|
||||
source = pairings_data.get('source', 'algorithm')
|
||||
footer = '\n_FIDE 2025 \(bbpPairings\)_' if source == 'bbp_pairings_fide_2025' else \
|
||||
'\n_Упрощённый Swiss \(bbpPairings недоступен\)_' if source == 'swiss_algorithm_simplified' else ''
|
||||
|
||||
return {
|
||||
'title': f'*{_md_escape(tournament_name)}*\n📋 *Тур {rnd}* — пары',
|
||||
'rows': _build_table_rows(pairings, w_board, max_name_w, w_pts),
|
||||
'table_header': table_header,
|
||||
'footer': footer,
|
||||
}
|
||||
|
||||
|
||||
def _build_table_rows(pairings: list, w_board: int, w_name: int, w_pts: int) -> list:
|
||||
rows = []
|
||||
board = 1
|
||||
for pi, pairing in enumerate(pairings):
|
||||
for pairing in pairings:
|
||||
if len(pairing) != 3:
|
||||
continue
|
||||
p1, p2, color = pairing
|
||||
|
||||
if color == 'bye':
|
||||
lines.append(
|
||||
rows.append(
|
||||
f'{_pad_col(str(board), w_board)} │ '
|
||||
f'{_pad_col(p1.name, max_name_w)} │ '
|
||||
f'{_pad_col(p1.name, w_name)} │ '
|
||||
f'{_pad_col(str(p1.points), w_pts)}'
|
||||
)
|
||||
lines.append(
|
||||
rows.append(
|
||||
f'{_pad_col("", w_board)} │ '
|
||||
f'{_pad_col("— BYE —", max_name_w)} │ '
|
||||
f'{_pad_col("— BYE —", w_name)} │ '
|
||||
f'{_pad_col("", w_pts)}'
|
||||
)
|
||||
board += 1
|
||||
|
|
@ -116,33 +122,48 @@ def format_pairings(pairings_data: dict, tournament_name: str) -> str:
|
|||
else:
|
||||
white, black = p2, p1
|
||||
|
||||
lines.append(
|
||||
rows.append(
|
||||
f'{_pad_col(str(board), w_board)} │ '
|
||||
f'{_pad_col(white.name, max_name_w)} │ '
|
||||
f'{_pad_col(white.name, w_name)} │ '
|
||||
f'{_pad_col(f"{white.points:.1f}", w_pts)}'
|
||||
)
|
||||
lines.append(
|
||||
rows.append(
|
||||
f'{_pad_col("", w_board)} │ '
|
||||
f'{_pad_col(black.name, max_name_w)} │ '
|
||||
f'{_pad_col(black.name, w_name)} │ '
|
||||
f'{_pad_col(f"{black.points:.1f}", w_pts)}'
|
||||
)
|
||||
board += 1
|
||||
|
||||
# Separator between pairs
|
||||
if pi < len(pairings) - 1:
|
||||
next_p = pairings[pi + 1]
|
||||
if len(next_p) == 3 and next_p[2] != 'bye':
|
||||
lines.append(border)
|
||||
return rows
|
||||
|
||||
lines.append('```')
|
||||
|
||||
source = pairings_data.get('source', 'algorithm')
|
||||
if source == 'bbp_pairings_fide_2025':
|
||||
lines.append('\n_FIDE 2025 \(bbpPairings\)_')
|
||||
elif source == 'swiss_algorithm_simplified':
|
||||
lines.append('\n_Упрощённый Swiss \(bbpPairings недоступен\)_')
|
||||
def _render_chunks(fmt: dict) -> list:
|
||||
header = fmt['table_header']
|
||||
footer = fmt['footer']
|
||||
rows = fmt['rows']
|
||||
|
||||
return '\n'.join(lines)
|
||||
chunks = []
|
||||
title_line = fmt['title'] + '\n'
|
||||
|
||||
# Estimate overhead per chunk: title (if first) + header + footer + ```
|
||||
overhead = len(title_line) + len(header) + len(footer) + 10
|
||||
available = 3800 - overhead
|
||||
rows_per_chunk = max(10, available // 60) # ~60 chars per row
|
||||
rows_per_chunk = (rows_per_chunk // 2) * 2 # keep pairs together
|
||||
|
||||
for i in range(0, len(rows), rows_per_chunk):
|
||||
batch = rows[i:i + rows_per_chunk]
|
||||
parts = []
|
||||
if i == 0:
|
||||
parts.append(title_line)
|
||||
parts.append(header)
|
||||
parts.extend(batch)
|
||||
parts.append('```')
|
||||
if i + rows_per_chunk >= len(rows):
|
||||
parts.append(footer)
|
||||
chunks.append('\n'.join(parts))
|
||||
|
||||
return chunks
|
||||
|
||||
|
||||
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
|
|
@ -234,52 +255,38 @@ async def handle_url(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||
record_request(user.id, url, tournament.get('name', ''), False, str(e))
|
||||
return
|
||||
|
||||
output = format_pairings(result, tournament['name'])
|
||||
fmt = format_pairings(result, tournament['name'])
|
||||
chunks = _render_chunks(fmt)
|
||||
|
||||
# Telegram has 4096 char limit
|
||||
try:
|
||||
if len(output) > 4000:
|
||||
chunks = _chunk_output(output, 3800)
|
||||
await calc_msg.edit_text(chunks[0], parse_mode=ParseMode.MARKDOWN_V2,
|
||||
disable_web_page_preview=True)
|
||||
for chunk in chunks[1:]:
|
||||
await update.message.reply_text(chunk, parse_mode=ParseMode.MARKDOWN_V2,
|
||||
disable_web_page_preview=True)
|
||||
else:
|
||||
await calc_msg.edit_text(output, parse_mode=ParseMode.MARKDOWN_V2,
|
||||
disable_web_page_preview=True)
|
||||
except Exception:
|
||||
# MarkdownV2 parse failed — fall back to plain text
|
||||
plain = output.replace('*', '').replace('_', '').replace('\\', '')
|
||||
if len(plain) > 4000:
|
||||
chunks = _chunk_output(plain, 3800)
|
||||
await calc_msg.edit_text(chunks[0])
|
||||
for chunk in chunks[1:]:
|
||||
except Exception as e:
|
||||
# Fallback: plain text without MarkdownV2
|
||||
plain_chunks = [c.replace('*', '').replace('_', '').replace('\\', '') for c in chunks]
|
||||
await calc_msg.edit_text(plain_chunks[0])
|
||||
for chunk in plain_chunks[1:]:
|
||||
await update.message.reply_text(chunk)
|
||||
else:
|
||||
await calc_msg.edit_text(plain)
|
||||
|
||||
record_request(user.id, url, tournament.get('name', ''), True)
|
||||
|
||||
|
||||
def _chunk_output(text: str, size: int) -> list:
|
||||
"""Split output at ``` boundaries: keep code blocks intact."""
|
||||
lines = text.split('\n')
|
||||
chunks = []
|
||||
in_code = False
|
||||
current = []
|
||||
current_len = 0
|
||||
for line in text.split('\n'):
|
||||
is_fence = line.strip().startswith('```')
|
||||
new_len = current_len + len(line) + 1
|
||||
if new_len > size and current and not in_code:
|
||||
for line in lines:
|
||||
line_len = len(line) + 1 # +1 for newline
|
||||
if current_len + line_len > size and current:
|
||||
chunks.append('\n'.join(current))
|
||||
current = [line]
|
||||
current_len = len(line)
|
||||
else:
|
||||
current = []
|
||||
current_len = 0
|
||||
current.append(line)
|
||||
current_len += len(line) + 1
|
||||
if is_fence:
|
||||
in_code = not in_code
|
||||
current_len += line_len
|
||||
if current:
|
||||
chunks.append('\n'.join(current))
|
||||
return chunks
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue