""" Клиентский Telegram-бот chessCalc. Принимает ссылку на турнир chess-results.com, рассчитывает жеребьёвку следующего тура и выводит пары. """ import os import re import sys import asyncio from telegram import Update from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes from telegram.constants import ParseMode sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from swiss_calc.parser import fetch_tournament from swiss_calc.swiss import calculate_next_round from swiss_calc.display import format_pairings_table from bots.stats import record_user, record_request TOKEN = os.environ.get('CLIENT_BOT_TOKEN', '') URL_PATTERN = re.compile(r'https?://(?:[\w-]+\.)?chess-results\.com/\S+') START_MSG = ( '♟️ chessCalc — жеребьёвка следующего тура\n\n' 'Я рассчитываю пары следующего тура шахматного турнира ' 'до официальной публикации на chess-results.com.\n\n' 'Просто пришли мне ссылку на турнир — и я покажу, ' 'кто с кем играет в следующем туре.\n\n' 'Пример ссылки:\n' 'https://chess-results.com/tnr1393124.aspx?lan=11\n\n' 'Работает для швейцарских турниров. Движок: FIDE Dutch System (bbpPairings).' ) async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): user = update.effective_user record_user(user.id, user.username or '', user.first_name or '', user.last_name or '') await update.message.reply_text(START_MSG, parse_mode=ParseMode.HTML) async def handle_url(update: Update, context: ContextTypes.DEFAULT_TYPE): user = update.effective_user text = update.message.text.strip() record_user(user.id, user.username or '', user.first_name or '', user.last_name or '') match = URL_PATTERN.search(text) if not match: await update.message.reply_text( '❌ Не нашёл ссылку на chess-results.com в сообщении.\n\n' 'Пришли ссылку вида:\n' 'https://chess-results.com/tnr1393124.aspx?lan=11', parse_mode=ParseMode.HTML) return url = match.group(0) msg = await update.message.reply_text('⏳ Загружаю турнир...') try: tournament = fetch_tournament(url) except Exception as e: await msg.edit_text(f'❌ Ошибка загрузки: {e}') record_request(user.id, url, '', False, str(e)) return if tournament['current_round'] >= tournament['num_rounds']: await msg.edit_text('🏁 Турнир уже завершён. Жеребьёвки не будет.') record_request(user.id, url, tournament.get('name', ''), True) return status_parts = [ f'✅ {tournament["name"]}', f'📅 Сыграно туров: {tournament["current_round"]} из {tournament["num_rounds"]}', f'👥 Участников: {len(tournament["standings"])}', ] await msg.edit_text('\n'.join(status_parts), parse_mode=ParseMode.HTML) await asyncio.sleep(0.5) # дать прочитать # Check if next round pairings already published on site base_url = re.sub(r'[&?]art=\d+', '', url) base_url = re.sub(r'[&?]rd=\d+', '', base_url) base_url = re.sub(r'[&?]turdet=\w+', '', base_url) base_url = re.sub(r'[&?]SNode=\w+', '', base_url) next_rd = tournament['current_round'] + 1 from swiss_calc.parser import fetch_url as _fetch_url, parse_round_pairings try: rd_html = _fetch_url(f'{base_url}&art=2&rd={next_rd}&turdet=YES') rd_games = parse_round_pairings(rd_html, next_rd) # Check if any game has a result (means round already played) played = any(g.get('white_score') is not None for g in rd_games if g) if played: await update.message.reply_text( f'⚠️ Тур {next_rd} уже сыгран — на сайте есть результаты.', parse_mode=ParseMode.HTML) record_request(user.id, url, tournament.get('name', ''), True) return # Check if pairings are published (unplayed games exist) if rd_games: await update.message.reply_text( f'⚠️ Жеребьёвка тура {next_rd} уже опубликована на chess-results.com.\n' 'Я всё равно посчитаю, но пары могут незначительно отличаться ' '(разные редакции правил FIDE).', parse_mode=ParseMode.HTML) except Exception: pass # art=2 page not accessible — pairings likely not published yet # Calculate next round calc_msg = await update.message.reply_text( f'🧮 Считаю жеребьёвку тура {next_rd}...') try: result = calculate_next_round(tournament) except Exception as e: await calc_msg.edit_text(f'❌ Ошибка расчёта: {e}') record_request(user.id, url, tournament.get('name', ''), False, str(e)) return output = format_pairings(result, tournament['name']) # Telegram has 4096 char limit if len(output) > 4000: chunks = _chunk_output(output, 3800) await calc_msg.edit_text(chunks[0], parse_mode=ParseMode.HTML, disable_web_page_preview=True) for chunk in chunks[1:]: await update.message.reply_text(chunk, parse_mode=ParseMode.HTML, disable_web_page_preview=True) else: await calc_msg.edit_text(output, parse_mode=ParseMode.HTML, disable_web_page_preview=True) record_request(user.id, url, tournament.get('name', ''), True) def format_pairings(pairings_data: dict, tournament_name: str) -> str: pairings = pairings_data['pairings'] rnd = pairings_data['round'] lines = [ f'{_escape_html(tournament_name)}', f'📋 Тур {rnd} — пары', '', ] board = 1 for pairing in pairings: if len(pairing) != 3: continue p1, p2, color = pairing if color == 'bye': lines.append(f' {board}. {_escape_html(p1.name)} — BYE') board += 1 continue if color == 'w': white, black = p1, p2 else: white, black = p2, p1 w_name = _escape_html(white.name) b_name = _escape_html(black.name) w_pts = white.points b_pts = black.points lines.append( f' {board}. {w_name} — {b_name} ' f'({w_pts}/{b_pts})' ) board += 1 lines.append('') source = pairings_data.get('source', 'algorithm') if source == 'bbp_pairings_fide_2025': lines.append('FIDE 2025 (bbpPairings)') elif source == 'swiss_algorithm_simplified': lines.append('Упрощённый Swiss (bbpPairings недоступен)') return '\n'.join(lines) def _escape_html(text: str) -> str: return text.replace('&', '&').replace('<', '<').replace('>', '>') def _chunk_output(text: str, size: int) -> list: lines = text.split('\n') chunks = [] current = [] current_len = 0 for line in lines: if current_len + len(line) > size and current: chunks.append('\n'.join(current)) current = [line] current_len = len(line) else: current.append(line) current_len += len(line) if current: chunks.append('\n'.join(current)) return chunks def main(): if not TOKEN: print('CLIENT_BOT_TOKEN not set', file=sys.stderr) sys.exit(1) app = Application.builder().token(TOKEN).build() app.add_handler(CommandHandler('start', start)) app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_url)) print('Client bot started', file=sys.stderr) app.run_polling() if __name__ == '__main__': main()