fix: player tracking subscription flow and /myplayers display
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 4s
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:
parent
282c651821
commit
293c1d90c8
2 changed files with 131 additions and 106 deletions
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue