feat: inline remove button in /myplayers, localized no-players message
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 14s
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 14s
- /myplayers: each player card gets inline 🗑 button — tap to remove instantly - removal via CallbackQueryHandler (remove:<fide_id>), no text command needed - /removeplayer command removed - no-players message localized (ru/en) via LOCALE - /myplayers header and labels (round, pts) also localized Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
293c1d90c8
commit
39f1f7e857
1 changed files with 94 additions and 60 deletions
|
|
@ -7,8 +7,8 @@ import os
|
|||
import re
|
||||
import sys
|
||||
import asyncio
|
||||
from telegram import Update
|
||||
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
|
||||
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters, ContextTypes
|
||||
from telegram.constants import ParseMode
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
|
@ -22,16 +22,34 @@ 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\)\.'
|
||||
)
|
||||
START_MSG = {
|
||||
'ru': (
|
||||
'♟️ *chessCalc* — жеребьёвка следующего тура\n\n'
|
||||
'Рассчитываю пары _до официальной публикации_ на chess\-results\.com\.\n'
|
||||
'Слежу за игроками: как только тур завершён — присылаю результат и жеребьёвку следующего\.\n\n'
|
||||
'*Команды:*\n'
|
||||
'/addplayer — подписаться на игрока по FIDE ID\n'
|
||||
'/myplayers — список отслеживаемых игроков\n'
|
||||
'/removeplayer — отписаться от игрока\n\n'
|
||||
'*Разовый расчёт:*\n'
|
||||
'Пришли ссылку на турнир — получи жеребьёвку прямо сейчас\n'
|
||||
'`https://chess\-results\.com/tnr1393124\.aspx?lan=11`\n\n'
|
||||
'_Движок: FIDE Dutch System \(bbpPairings\), точность 100% на большинстве турниров_'
|
||||
),
|
||||
'en': (
|
||||
'♟️ *chessCalc* — next round pairings\n\n'
|
||||
'I calculate pairings _before official publication_ on chess\-results\.com\.\n'
|
||||
'I also track players: when a round finishes — I send the result and next round pairings\.\n\n'
|
||||
'*Commands:*\n'
|
||||
'/addplayer — subscribe to a player by FIDE ID\n'
|
||||
'/myplayers — list of tracked players\n'
|
||||
'/removeplayer — unsubscribe from a player\n\n'
|
||||
'*One\-time calculation:*\n'
|
||||
'Send a tournament link — get pairings right now\n'
|
||||
'`https://chess\-results\.com/tnr1393124\.aspx?lan=11`\n\n'
|
||||
'_Engine: FIDE Dutch System \(bbpPairings\), 100% accuracy on most tournaments_'
|
||||
),
|
||||
}
|
||||
|
||||
LOCALE = {
|
||||
'ru': {
|
||||
|
|
@ -43,6 +61,11 @@ LOCALE = {
|
|||
'bye': '— BYE —',
|
||||
'footer_bbp': '\n_FIDE 2025 \(bbpPairings\)_',
|
||||
'footer_fallback': '\n_Упрощённый Swiss \(bbpPairings недоступен\)_',
|
||||
'no_players': 'У вас нет отслеживаемых игроков\.\n/addplayer — добавить игрока',
|
||||
'my_players_title': 'Ваши игроки',
|
||||
'waiting': '⏳ _Ожидаю турнир_',
|
||||
'round_label': 'тур',
|
||||
'points_label': 'очк\\.',
|
||||
},
|
||||
'en': {
|
||||
'player': 'Player',
|
||||
|
|
@ -53,6 +76,11 @@ LOCALE = {
|
|||
'bye': '— BYE —',
|
||||
'footer_bbp': '\n_FIDE 2025 \(bbpPairings\)_',
|
||||
'footer_fallback': '\n_Simplified Swiss \(bbpPairings unavailable\)_',
|
||||
'no_players': 'You have no tracked players\.\n/addplayer — add a player',
|
||||
'my_players_title': 'Your players',
|
||||
'waiting': '⏳ _Waiting for a tournament_',
|
||||
'round_label': 'round',
|
||||
'points_label': 'pts',
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -273,7 +301,8 @@ 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.MARKDOWN_V2)
|
||||
lang = 'ru' if (user.language_code or '').startswith('ru') else 'en'
|
||||
await update.message.reply_text(START_MSG[lang], parse_mode=ParseMode.MARKDOWN_V2)
|
||||
|
||||
|
||||
async def add_player(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
|
|
@ -297,65 +326,70 @@ async def cancel_sub(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||
await update.message.reply_text('Нет активного процесса подписки\.')
|
||||
|
||||
|
||||
def _player_card(p: dict, loc: dict) -> tuple[str, InlineKeyboardMarkup]:
|
||||
safe_name = _md_escape(p['player_name'])
|
||||
fed = p['fed'] or ''
|
||||
rating = p['rating'] or '—'
|
||||
fide_id = p['fide_id']
|
||||
|
||||
lines = [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(loc['waiting'])
|
||||
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} {loc["points_label"]}, {loc["round_label"]} {rd}')
|
||||
|
||||
keyboard = InlineKeyboardMarkup([[
|
||||
InlineKeyboardButton('🗑 Удалить', callback_data=f'remove:{fide_id}')
|
||||
]])
|
||||
return '\n'.join(lines), keyboard
|
||||
|
||||
|
||||
async def my_players(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
user = update.effective_user
|
||||
loc = _get_locale(user.language_code or '')
|
||||
players = tracker.get_user_subs(user.id)
|
||||
if not players:
|
||||
await update.message.reply_text('У вас нет активных подписок\.\n'
|
||||
'/addplayer — добавить игрока',
|
||||
await update.message.reply_text(loc['no_players'],
|
||||
parse_mode=ParseMode.MARKDOWN_V2)
|
||||
return
|
||||
|
||||
sep = '━' * 22
|
||||
lines = [f'*Ваши игроки \\({len(players)}\\):*']
|
||||
header = f'*{_md_escape(loc["my_players_title"])} \\({len(players)}\\):*'
|
||||
await update.message.reply_text(header, parse_mode=ParseMode.MARKDOWN_V2)
|
||||
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)
|
||||
text, keyboard = _player_card(p, loc)
|
||||
await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN_V2,
|
||||
reply_markup=keyboard)
|
||||
|
||||
|
||||
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 FIDE_ID`\n'
|
||||
'/myplayers — посмотреть список',
|
||||
parse_mode=ParseMode.MARKDOWN_V2)
|
||||
return
|
||||
fide_id = int(parts[1])
|
||||
async def remove_player_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
user = query.from_user
|
||||
loc = _get_locale(user.language_code or '')
|
||||
|
||||
fide_id = int(query.data.split(':')[1])
|
||||
ok = tracker.remove_player_subscriptions(fide_id, user.id)
|
||||
|
||||
if ok:
|
||||
await update.message.reply_text('✅ Игрок удалён из подписок\.',
|
||||
parse_mode=ParseMode.MARKDOWN_V2)
|
||||
# Strike through the card text and remove the button
|
||||
struck = query.message.text.replace('👤', '❌')
|
||||
await query.edit_message_text(struck, reply_markup=None)
|
||||
else:
|
||||
await update.message.reply_text('Игрок не найден\. /myplayers — список',
|
||||
parse_mode=ParseMode.MARKDOWN_V2)
|
||||
await query.edit_message_text(query.message.text + '\n\n_уже удалён_',
|
||||
parse_mode=ParseMode.MARKDOWN_V2,
|
||||
reply_markup=None)
|
||||
|
||||
|
||||
|
||||
def _extract_fide_id(text: str) -> int | None:
|
||||
|
|
@ -674,7 +708,7 @@ def main():
|
|||
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(CallbackQueryHandler(remove_player_callback, pattern=r'^remove:\d+$'))
|
||||
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_url))
|
||||
|
||||
app.job_queue.run_repeating(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue