Починен краш 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']
|
pairings = pairings_data['pairings']
|
||||||
rnd = pairings_data['round']
|
rnd = pairings_data['round']
|
||||||
|
|
||||||
# Find the longest player name + 3 spaces padding
|
|
||||||
max_name_w = 20
|
max_name_w = 20
|
||||||
for pairing in pairings:
|
for pairing in pairings:
|
||||||
if len(pairing) == 3:
|
if len(pairing) == 3:
|
||||||
|
|
@ -81,31 +80,38 @@ def format_pairings(pairings_data: dict, tournament_name: str) -> str:
|
||||||
w_board, w_pts = 4, 5
|
w_board, w_pts = 4, 5
|
||||||
border = '─┼─'.join('─' * w for w in [w_board, max_name_w, w_pts])
|
border = '─┼─'.join('─' * w for w in [w_board, max_name_w, w_pts])
|
||||||
|
|
||||||
lines = [
|
header = f'{_pad_col("#", w_board)} │ {_pad_col("Игрок", max_name_w)} │ {_pad_col("О", w_pts)}'
|
||||||
f'*{_md_escape(tournament_name)}*',
|
table_header = f'```\n{header}\n{border}\n'
|
||||||
f'📋 *Тур {rnd}* — пары',
|
|
||||||
'',
|
|
||||||
'```',
|
|
||||||
f'{_pad_col("#", w_board)} │ {_pad_col("Игрок", max_name_w)} │ '
|
|
||||||
f'{_pad_col("О", w_pts)}',
|
|
||||||
border,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
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
|
board = 1
|
||||||
for pi, pairing in enumerate(pairings):
|
for pairing in pairings:
|
||||||
if len(pairing) != 3:
|
if len(pairing) != 3:
|
||||||
continue
|
continue
|
||||||
p1, p2, color = pairing
|
p1, p2, color = pairing
|
||||||
|
|
||||||
if color == 'bye':
|
if color == 'bye':
|
||||||
lines.append(
|
rows.append(
|
||||||
f'{_pad_col(str(board), w_board)} │ '
|
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)}'
|
f'{_pad_col(str(p1.points), w_pts)}'
|
||||||
)
|
)
|
||||||
lines.append(
|
rows.append(
|
||||||
f'{_pad_col("", w_board)} │ '
|
f'{_pad_col("", w_board)} │ '
|
||||||
f'{_pad_col("— BYE —", max_name_w)} │ '
|
f'{_pad_col("— BYE —", w_name)} │ '
|
||||||
f'{_pad_col("", w_pts)}'
|
f'{_pad_col("", w_pts)}'
|
||||||
)
|
)
|
||||||
board += 1
|
board += 1
|
||||||
|
|
@ -116,33 +122,48 @@ def format_pairings(pairings_data: dict, tournament_name: str) -> str:
|
||||||
else:
|
else:
|
||||||
white, black = p2, p1
|
white, black = p2, p1
|
||||||
|
|
||||||
lines.append(
|
rows.append(
|
||||||
f'{_pad_col(str(board), w_board)} │ '
|
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)}'
|
f'{_pad_col(f"{white.points:.1f}", w_pts)}'
|
||||||
)
|
)
|
||||||
lines.append(
|
rows.append(
|
||||||
f'{_pad_col("", w_board)} │ '
|
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)}'
|
f'{_pad_col(f"{black.points:.1f}", w_pts)}'
|
||||||
)
|
)
|
||||||
board += 1
|
board += 1
|
||||||
|
|
||||||
# Separator between pairs
|
return rows
|
||||||
if pi < len(pairings) - 1:
|
|
||||||
next_p = pairings[pi + 1]
|
|
||||||
if len(next_p) == 3 and next_p[2] != 'bye':
|
|
||||||
lines.append(border)
|
|
||||||
|
|
||||||
lines.append('```')
|
|
||||||
|
|
||||||
source = pairings_data.get('source', 'algorithm')
|
def _render_chunks(fmt: dict) -> list:
|
||||||
if source == 'bbp_pairings_fide_2025':
|
header = fmt['table_header']
|
||||||
lines.append('\n_FIDE 2025 \(bbpPairings\)_')
|
footer = fmt['footer']
|
||||||
elif source == 'swiss_algorithm_simplified':
|
rows = fmt['rows']
|
||||||
lines.append('\n_Упрощённый Swiss \(bbpPairings недоступен\)_')
|
|
||||||
|
|
||||||
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):
|
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))
|
record_request(user.id, url, tournament.get('name', ''), False, str(e))
|
||||||
return
|
return
|
||||||
|
|
||||||
output = format_pairings(result, tournament['name'])
|
fmt = format_pairings(result, tournament['name'])
|
||||||
|
chunks = _render_chunks(fmt)
|
||||||
|
|
||||||
# Telegram has 4096 char limit
|
|
||||||
try:
|
try:
|
||||||
if len(output) > 4000:
|
|
||||||
chunks = _chunk_output(output, 3800)
|
|
||||||
await calc_msg.edit_text(chunks[0], parse_mode=ParseMode.MARKDOWN_V2,
|
await calc_msg.edit_text(chunks[0], parse_mode=ParseMode.MARKDOWN_V2,
|
||||||
disable_web_page_preview=True)
|
disable_web_page_preview=True)
|
||||||
for chunk in chunks[1:]:
|
for chunk in chunks[1:]:
|
||||||
await update.message.reply_text(chunk, parse_mode=ParseMode.MARKDOWN_V2,
|
await update.message.reply_text(chunk, parse_mode=ParseMode.MARKDOWN_V2,
|
||||||
disable_web_page_preview=True)
|
disable_web_page_preview=True)
|
||||||
else:
|
except Exception as e:
|
||||||
await calc_msg.edit_text(output, parse_mode=ParseMode.MARKDOWN_V2,
|
# Fallback: plain text without MarkdownV2
|
||||||
disable_web_page_preview=True)
|
plain_chunks = [c.replace('*', '').replace('_', '').replace('\\', '') for c in chunks]
|
||||||
except Exception:
|
await calc_msg.edit_text(plain_chunks[0])
|
||||||
# MarkdownV2 parse failed — fall back to plain text
|
for chunk in plain_chunks[1:]:
|
||||||
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:]:
|
|
||||||
await update.message.reply_text(chunk)
|
await update.message.reply_text(chunk)
|
||||||
else:
|
|
||||||
await calc_msg.edit_text(plain)
|
|
||||||
|
|
||||||
record_request(user.id, url, tournament.get('name', ''), True)
|
record_request(user.id, url, tournament.get('name', ''), True)
|
||||||
|
|
||||||
|
|
||||||
def _chunk_output(text: str, size: int) -> list:
|
def _chunk_output(text: str, size: int) -> list:
|
||||||
"""Split output at ``` boundaries: keep code blocks intact."""
|
lines = text.split('\n')
|
||||||
chunks = []
|
chunks = []
|
||||||
in_code = False
|
|
||||||
current = []
|
current = []
|
||||||
current_len = 0
|
current_len = 0
|
||||||
for line in text.split('\n'):
|
for line in lines:
|
||||||
is_fence = line.strip().startswith('```')
|
line_len = len(line) + 1 # +1 for newline
|
||||||
new_len = current_len + len(line) + 1
|
if current_len + line_len > size and current:
|
||||||
if new_len > size and current and not in_code:
|
|
||||||
chunks.append('\n'.join(current))
|
chunks.append('\n'.join(current))
|
||||||
current = [line]
|
current = []
|
||||||
current_len = len(line)
|
current_len = 0
|
||||||
else:
|
|
||||||
current.append(line)
|
current.append(line)
|
||||||
current_len += len(line) + 1
|
current_len += line_len
|
||||||
if is_fence:
|
|
||||||
in_code = not in_code
|
|
||||||
if current:
|
if current:
|
||||||
chunks.append('\n'.join(current))
|
chunks.append('\n'.join(current))
|
||||||
return chunks
|
return chunks
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue