Подписка на игроков: /addplayer, /myplayers, /removeplayer, фоновая проверка результатов и жеребьёвки
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 11s
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 11s
This commit is contained in:
parent
cfd410d5fe
commit
7ebe1aa66e
2 changed files with 519 additions and 0 deletions
|
|
@ -16,6 +16,7 @@ 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 bots.stats import record_user, record_request
|
||||
from bots import tracker
|
||||
|
||||
TOKEN = os.environ.get('CLIENT_BOT_TOKEN', '')
|
||||
|
||||
|
|
@ -228,6 +229,107 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||
await update.message.reply_text(START_MSG, parse_mode=ParseMode.MARKDOWN_V2)
|
||||
|
||||
|
||||
async def add_player(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 '')
|
||||
context.user_data['awaiting_fide_id'] = True
|
||||
await update.message.reply_text(
|
||||
'Пришли мне FIDE ID игрока \(число\) или ссылку на профиль FIDE:\n'
|
||||
r'`https://ratings\.fide\.com/profile/1503014`\n'
|
||||
'\n'
|
||||
'/cancel — отмена',
|
||||
parse_mode=ParseMode.MARKDOWN_V2)
|
||||
|
||||
|
||||
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:
|
||||
await update.message.reply_text('❎ Подписка отменена\.')
|
||||
else:
|
||||
await update.message.reply_text('Нет активного процесса подписки\.')
|
||||
|
||||
|
||||
async def my_players(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
user = update.effective_user
|
||||
subs = tracker.get_user_subs(user.id)
|
||||
if not subs:
|
||||
await update.message.reply_text('У вас нет активных подписок\.\n'
|
||||
'/addplayer — добавить игрока')
|
||||
return
|
||||
lines = ['*Ваши подписки:*']
|
||||
for s in subs:
|
||||
safe_name = _md_escape(s['player_name'])
|
||||
safe_tour = _md_escape(s['tournament_name'] or '—')
|
||||
lines.append(
|
||||
f' {s["id"]}\\. {safe_name} \({s["rating"]}, {s["fed"]}\) '
|
||||
f'— {safe_tour} — {s["last_points"]:.1f} очк\\.')
|
||||
lines.append('\n/removeplayer <номер> — удалить подписку')
|
||||
await update.message.reply_text('\n'.join(lines),
|
||||
parse_mode=ParseMode.MARKDOWN_V2)
|
||||
|
||||
|
||||
async def remove_player(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
user = update.effective_user
|
||||
parts = update.message.text.strip().split()
|
||||
if len(parts) < 2 or not parts[1].isdigit():
|
||||
await update.message.reply_text(
|
||||
'Использование: `/removeplayer <номер>`\n'
|
||||
'/myplayers — посмотреть список с номерами',
|
||||
parse_mode=ParseMode.MARKDOWN_V2)
|
||||
return
|
||||
sub_id = int(parts[1])
|
||||
ok = tracker.remove_subscription(sub_id, user.id)
|
||||
if ok:
|
||||
await update.message.reply_text('✅ Подписка удалена\.')
|
||||
else:
|
||||
await update.message.reply_text('Подписка не найдена\. /myplayers — список')
|
||||
|
||||
|
||||
def _extract_fide_id(text: str) -> int | None:
|
||||
"""Extract FIDE ID from plain number or profile URL."""
|
||||
m = re.search(r'profile/(\d+)', text)
|
||||
if m:
|
||||
return int(m.group(1))
|
||||
# Try plain number (between 6-8 digits)
|
||||
for token in text.split():
|
||||
if token.isdigit() and 100000 <= int(token) <= 9999999999:
|
||||
return int(token)
|
||||
return None
|
||||
|
||||
|
||||
async def _lookup_and_confirm(update: Update, context, fide_id: int):
|
||||
user = update.effective_user
|
||||
lang_code = user.language_code or ''
|
||||
|
||||
try:
|
||||
player = tracker.fetch_fide_player(fide_id)
|
||||
except Exception as e:
|
||||
await update.message.reply_text(
|
||||
f'❌ Не удалось найти игрока: {_md_escape(str(e))}\n'
|
||||
'Попробуй другой ID или /cancel',
|
||||
parse_mode=ParseMode.MARKDOWN_V2)
|
||||
context.user_data['awaiting_fide_id'] = True
|
||||
return
|
||||
|
||||
safe_name = _md_escape(player['name'])
|
||||
title_str = f', {player["title"]}' if player.get('title') else ''
|
||||
|
||||
msg = (
|
||||
f'✅ *{safe_name}*{title_str}\n'
|
||||
f'{"🇫 " if player["fed"] else ""}{player["fed"]} \\| Рейтинг: {player["rating"]}\n\n'
|
||||
'Теперь отправь ссылку на турнир chess\\-results\\.com, '
|
||||
'где играет этот игрок\.\n'
|
||||
'/cancel — отмена'
|
||||
)
|
||||
context.user_data['pending_fide'] = {
|
||||
'player': player,
|
||||
'lang': lang_code,
|
||||
}
|
||||
await update.message.reply_text(msg, parse_mode=ParseMode.MARKDOWN_V2)
|
||||
|
||||
|
||||
def _apply_round_results(tournament, games, rd):
|
||||
standings = tournament['standings']
|
||||
sno_to_standing = {s['starting_sno']: s for s in standings if s.get('starting_sno')}
|
||||
|
|
@ -301,12 +403,91 @@ 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}, {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
|
||||
text = update.message.text.strip()
|
||||
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)
|
||||
if fide_id is None:
|
||||
await update.message.reply_text(
|
||||
'Не удалось распознать FIDE ID\. Отправь число или ссылку вида\n'
|
||||
r'`https://ratings\.fide\.com/profile/1503014`' '\n'
|
||||
'/cancel — отмена',
|
||||
parse_mode=ParseMode.MARKDOWN_V2)
|
||||
context.user_data['awaiting_fide_id'] = True
|
||||
return
|
||||
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
|
||||
match = URL_PATTERN.search(text)
|
||||
if not match:
|
||||
await update.message.reply_text(
|
||||
|
|
@ -441,8 +622,15 @@ def main():
|
|||
|
||||
app = Application.builder().token(TOKEN).build()
|
||||
app.add_handler(CommandHandler('start', start))
|
||||
app.add_handler(CommandHandler('addplayer', add_player))
|
||||
app.add_handler(CommandHandler('cancel', cancel_sub))
|
||||
app.add_handler(CommandHandler('myplayers', my_players))
|
||||
app.add_handler(CommandHandler('removeplayer', remove_player))
|
||||
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_url))
|
||||
|
||||
app.job_queue.run_repeating(
|
||||
tracker.check_all_subscriptions, interval=300, first=10)
|
||||
|
||||
print('Client bot started', file=sys.stderr)
|
||||
app.run_polling()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue