fix: player tracking subscription flow and /myplayers display
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 4s

- scan_for_player: remove hardcoded TNR range (1434k-1450k), search full cache
- discover_max_tnr: start from cache MAX(tnr) instead of stuck-at-500k value
- addplayer: if no tournament found, add fide_only watch instead of asking for URL
- rescan_new_tournaments: deactivate fide_only entry when real tournament found
- /myplayers: card layout grouped by player, removal by FIDE ID
- /removeplayer: now accepts FIDE ID, removes all subscriptions for that player
- fix MarkdownV2 escaping: dates (hyphens), <> in messages, title='None'
- check_all_subscriptions: skip fide_only entries; interval 5min → 1min

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
vrubel 2026-06-20 22:54:01 +00:00
parent 282c651821
commit 293c1d90c8
2 changed files with 131 additions and 106 deletions

View file

@ -291,8 +291,7 @@ async def add_player(update: Update, context: ContextTypes.DEFAULT_TYPE):
async def cancel_sub(update: Update, context: ContextTypes.DEFAULT_TYPE):
had_fide = context.user_data.pop('awaiting_fide_id', None)
had_pending = context.user_data.pop('pending_fide', None)
if had_fide or had_pending:
if had_fide:
await update.message.reply_text('❎ Подписка отменена\.')
else:
await update.message.reply_text('Нет активного процесса подписки\.')
@ -300,20 +299,42 @@ async def cancel_sub(update: Update, context: ContextTypes.DEFAULT_TYPE):
async def my_players(update: Update, context: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
subs = tracker.get_user_subs(user.id)
if not subs:
players = tracker.get_user_subs(user.id)
if not players:
await update.message.reply_text('У вас нет активных подписок\.\n'
'/addplayer — добавить игрока')
'/addplayer — добавить игрока',
parse_mode=ParseMode.MARKDOWN_V2)
return
lines = ['*Ваши подписки:*']
for s in subs:
safe_name = _md_escape(s['player_name'])
safe_tour = _md_escape(s['tournament_name'] or '')
last_pts_str = _md_escape(f'{s["last_points"]:.1f}')
lines.append(
f' {s["id"]}\\. {safe_name} \({s["rating"]}, {s["fed"]}\) '
f'{safe_tour}{last_pts_str} очк\\.')
lines.append('\n/removeplayer <номер> — удалить подписку')
sep = '' * 22
lines = [f'*Ваши игроки \\({len(players)}\\):*']
for p in players:
safe_name = _md_escape(p['player_name'])
fed = p['fed'] or ''
rating = p['rating'] or ''
fide_id = p['fide_id']
lines.append(sep)
lines.append(f'👤 *{safe_name}*')
lines.append(f'🏳 {_md_escape(fed)} · {rating}')
tours = p['tournaments']
waiting = all(t['name'] is None for t in tours)
if waiting:
lines.append('⏳ _Ожидаю турнир_')
else:
for t in tours:
if t['name'] is None:
continue
safe_tour = _md_escape(t['name'])
pts = _md_escape(f'{t["points"]:.1f}')
rd = t['round_done']
lines.append(f'📌 {safe_tour}')
lines.append(f' {pts} очк\\., тур {rd}')
lines.append(f'🗑 /removeplayer {fide_id}')
lines.append(sep)
await update.message.reply_text('\n'.join(lines),
parse_mode=ParseMode.MARKDOWN_V2)
@ -323,16 +344,18 @@ async def remove_player(update: Update, context: ContextTypes.DEFAULT_TYPE):
parts = update.message.text.strip().split()
if len(parts) < 2 or not parts[1].isdigit():
await update.message.reply_text(
'Использование: `/removeplayer <номер>`\n'
'/myplayers — посмотреть список с номерами',
'Использование: `/removeplayer FIDE_ID`\n'
'/myplayers — посмотреть список',
parse_mode=ParseMode.MARKDOWN_V2)
return
sub_id = int(parts[1])
ok = tracker.remove_subscription(sub_id, user.id)
fide_id = int(parts[1])
ok = tracker.remove_player_subscriptions(fide_id, user.id)
if ok:
await update.message.reply_text('✅ Подписка удалена\.')
await update.message.reply_text('✅ Игрок удалён из подписок\.',
parse_mode=ParseMode.MARKDOWN_V2)
else:
await update.message.reply_text('Подписка не найдена\. /myplayers — список')
await update.message.reply_text('Игрок не найден\. /myplayers — список',
parse_mode=ParseMode.MARKDOWN_V2)
def _extract_fide_id(text: str) -> int | None:
@ -362,10 +385,12 @@ async def _lookup_and_confirm(update: Update, context, fide_id: int):
return
safe_name = _md_escape(player['name'])
title_str = f', {player["title"]}' if player.get('title') else ''
title = player.get('title') or ''
title_str = f', {_md_escape(title)}' if title and title != 'None' else ''
fed = player.get('fed') or ''
await update.message.reply_text(
f'✅ *{safe_name}*{title_str}\n'
f'{"🇫 " if player["fed"] else ""}{player["fed"]} \\| Рейтинг: {player["rating"]}\n\n'
f'{"🇫 " if fed else ""}{_md_escape(fed)} \\| Рейтинг: {player["rating"]}\n\n'
'🔍 Ищу турниры на chess\\-results\\.com\\.\\.',
parse_mode=ParseMode.MARKDOWN_V2)
@ -385,7 +410,7 @@ async def _lookup_and_confirm(update: Update, context, fide_id: int):
0, 0.0, lang_code, t['start_date'], t['end_date'])
dates = ''
if t['start_date'] and t['end_date']:
dates = f' \({t["start_date"]}{t["end_date"]}\)'
dates = f' \\({_md_escape(t["start_date"])}{_md_escape(t["end_date"])}\\)'
safe_tour = _md_escape(t['name'])
lines.append(f'{safe_tour}{dates}')
lines.append('\nЯ буду присылать результаты и жеребьёвку\.\n'
@ -393,16 +418,14 @@ async def _lookup_and_confirm(update: Update, context, fide_id: int):
await update.message.reply_text('\n'.join(lines),
parse_mode=ParseMode.MARKDOWN_V2)
else:
# No tournaments found — fallback to manual URL
context.user_data['pending_fide'] = {
'player': player,
'lang': lang_code,
}
# No active tournament found — watch for future ones
tracker.add_player_watch(user.id, player, lang_code)
safe_name = _md_escape(player['name'])
await update.message.reply_text(
'❌ Турниров не найдено автоматически\\.\n\n'
'Отправь ссылку на турнир chess\\-results\\.com, '
'где играет этот игрок\\.\n'
'/cancel — отмена',
f'👁 *{safe_name}* поставлен на отслеживание\.\n\n'
'Активного турнира сейчас не найдено\\. '
'Как только игрок появится в новом турнире — сразу подпишу и пришлю жеребьёвку\\.\n\n'
'/myplayers \\- список подписок',
parse_mode=ParseMode.MARKDOWN_V2)
@ -479,50 +502,6 @@ def _apply_round_results(tournament, games, rd):
tournament['current_round'] = rd
async def _subscribe_to_tournament(update: Update, context, url: str, pending: dict):
user = update.effective_user
player = pending['player']
lang_code = pending['lang']
msg = await update.message.reply_text('⏳ Проверяю турнир\.\.\.',
parse_mode=ParseMode.MARKDOWN_V2)
try:
tournament = fetch_tournament(url)
except Exception as e:
await msg.edit_text(f'❌ Ошибка загрузки турнира: {_md_escape(str(e))}',
parse_mode=ParseMode.MARKDOWN_V2)
context.user_data['pending_fide'] = pending
return
p = tracker.find_player(tournament, player['name'])
if p is None:
safe_name = _md_escape(player['name'])
await msg.edit_text(
f'❌ *{safe_name}* не найден в этом турнире\\.\n'
'Отправь другой URL или /cancel',
parse_mode=ParseMode.MARKDOWN_V2)
context.user_data['pending_fide'] = pending
return
p_sno = p.get('starting_sno', 0)
results_count = len(p.get('results', []))
points = p.get('points', 0.0)
tracker.add_subscription(
user.id, player, url,
tournament.get('name', ''),
p_sno, results_count, points, lang_code)
safe_name = _md_escape(player['name'])
safe_tour = _md_escape(tournament.get('name', ''))
await msg.edit_text(
f'✅ *{safe_name}* найден в турнире\\.\n'
f'📅 *{safe_tour}*\n'
f'Ст\\.№ {p_sno}, {_md_escape(f"{points:.1f}")} очк\\. — отслеживаю\!\n\n'
'Я буду присылать результаты игрока и жеребьёвку следующих туров\.\n'
'/myplayers — список подписок',
parse_mode=ParseMode.MARKDOWN_V2)
async def handle_url(update: Update, context: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
@ -530,8 +509,6 @@ async def handle_url(update: Update, context: ContextTypes.DEFAULT_TYPE):
record_user(user.id, user.username or '', user.first_name or '',
user.last_name or '')
# State machine: waiting for FIDE ID → tournament URL → regular pairings
# Step 1: user just called /addplayer — waiting for FIDE ID
if context.user_data.pop('awaiting_fide_id', None):
fide_id = _extract_fide_id(text)
@ -546,24 +523,7 @@ async def handle_url(update: Update, context: ContextTypes.DEFAULT_TYPE):
await _lookup_and_confirm(update, context, fide_id)
return
# Step 2: player confirmed — waiting for tournament URL
pending = context.user_data.get('pending_fide')
if pending:
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`\n\n'
'/cancel — отмена',
parse_mode=ParseMode.MARKDOWN_V2)
return
url = match.group(0)
context.user_data.pop('pending_fide', None)
await _subscribe_to_tournament(update, context, url, pending)
return
# Step 3: regular pairings request
# Step 2: regular pairings request
match = URL_PATTERN.search(text)
if not match:
await update.message.reply_text(
@ -718,7 +678,7 @@ def main():
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_url))
app.job_queue.run_repeating(
tracker.check_all_subscriptions, interval=300, first=10)
tracker.check_all_subscriptions, interval=60, first=10)
app.job_queue.run_repeating(
tracker.rescan_new_tournaments, interval=3600, first=60)
app.job_queue.run_repeating(