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 re
|
||||||
import sys
|
import sys
|
||||||
import asyncio
|
import asyncio
|
||||||
from telegram import Update
|
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
|
from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters, ContextTypes
|
||||||
from telegram.constants import ParseMode
|
from telegram.constants import ParseMode
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
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+')
|
URL_PATTERN = re.compile(r'https?://(?:[\w-]+\.)?chess-results\.com/\S+')
|
||||||
|
|
||||||
START_MSG = (
|
START_MSG = {
|
||||||
'*♟️ chessCalc — жеребьёвка следующего тура*\n\n'
|
'ru': (
|
||||||
'Я рассчитываю пары следующего тура шахматного турнира '
|
'♟️ *chessCalc* — жеребьёвка следующего тура\n\n'
|
||||||
'_до официальной публикации_ на chess\-results\.com\.\n\n'
|
'Рассчитываю пары _до официальной публикации_ на chess\-results\.com\.\n'
|
||||||
'Просто пришли мне ссылку на турнир — и я покажу, '
|
'Слежу за игроками: как только тур завершён — присылаю результат и жеребьёвку следующего\.\n\n'
|
||||||
'кто с кем играет в следующем туре\.\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'
|
'`https://chess\-results\.com/tnr1393124\.aspx?lan=11`\n\n'
|
||||||
'Работает для швейцарских турниров\. Движок: FIDE Dutch System \(bbpPairings\)\.'
|
'_Движок: 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 = {
|
LOCALE = {
|
||||||
'ru': {
|
'ru': {
|
||||||
|
|
@ -43,6 +61,11 @@ LOCALE = {
|
||||||
'bye': '— BYE —',
|
'bye': '— BYE —',
|
||||||
'footer_bbp': '\n_FIDE 2025 \(bbpPairings\)_',
|
'footer_bbp': '\n_FIDE 2025 \(bbpPairings\)_',
|
||||||
'footer_fallback': '\n_Упрощённый Swiss \(bbpPairings недоступен\)_',
|
'footer_fallback': '\n_Упрощённый Swiss \(bbpPairings недоступен\)_',
|
||||||
|
'no_players': 'У вас нет отслеживаемых игроков\.\n/addplayer — добавить игрока',
|
||||||
|
'my_players_title': 'Ваши игроки',
|
||||||
|
'waiting': '⏳ _Ожидаю турнир_',
|
||||||
|
'round_label': 'тур',
|
||||||
|
'points_label': 'очк\\.',
|
||||||
},
|
},
|
||||||
'en': {
|
'en': {
|
||||||
'player': 'Player',
|
'player': 'Player',
|
||||||
|
|
@ -53,6 +76,11 @@ LOCALE = {
|
||||||
'bye': '— BYE —',
|
'bye': '— BYE —',
|
||||||
'footer_bbp': '\n_FIDE 2025 \(bbpPairings\)_',
|
'footer_bbp': '\n_FIDE 2025 \(bbpPairings\)_',
|
||||||
'footer_fallback': '\n_Simplified Swiss \(bbpPairings unavailable\)_',
|
'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
|
user = update.effective_user
|
||||||
record_user(user.id, user.username or '', user.first_name or '',
|
record_user(user.id, user.username or '', user.first_name or '',
|
||||||
user.last_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):
|
async def add_player(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
|
@ -297,31 +326,19 @@ async def cancel_sub(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
await update.message.reply_text('Нет активного процесса подписки\.')
|
await update.message.reply_text('Нет активного процесса подписки\.')
|
||||||
|
|
||||||
|
|
||||||
async def my_players(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
def _player_card(p: dict, loc: dict) -> tuple[str, InlineKeyboardMarkup]:
|
||||||
user = update.effective_user
|
|
||||||
players = tracker.get_user_subs(user.id)
|
|
||||||
if not players:
|
|
||||||
await update.message.reply_text('У вас нет активных подписок\.\n'
|
|
||||||
'/addplayer — добавить игрока',
|
|
||||||
parse_mode=ParseMode.MARKDOWN_V2)
|
|
||||||
return
|
|
||||||
|
|
||||||
sep = '━' * 22
|
|
||||||
lines = [f'*Ваши игроки \\({len(players)}\\):*']
|
|
||||||
for p in players:
|
|
||||||
safe_name = _md_escape(p['player_name'])
|
safe_name = _md_escape(p['player_name'])
|
||||||
fed = p['fed'] or ''
|
fed = p['fed'] or ''
|
||||||
rating = p['rating'] or '—'
|
rating = p['rating'] or '—'
|
||||||
fide_id = p['fide_id']
|
fide_id = p['fide_id']
|
||||||
|
|
||||||
lines.append(sep)
|
lines = [f'👤 *{safe_name}*']
|
||||||
lines.append(f'👤 *{safe_name}*')
|
|
||||||
lines.append(f'🏳 {_md_escape(fed)} · {rating}')
|
lines.append(f'🏳 {_md_escape(fed)} · {rating}')
|
||||||
|
|
||||||
tours = p['tournaments']
|
tours = p['tournaments']
|
||||||
waiting = all(t['name'] is None for t in tours)
|
waiting = all(t['name'] is None for t in tours)
|
||||||
if waiting:
|
if waiting:
|
||||||
lines.append('⏳ _Ожидаю турнир_')
|
lines.append(loc['waiting'])
|
||||||
else:
|
else:
|
||||||
for t in tours:
|
for t in tours:
|
||||||
if t['name'] is None:
|
if t['name'] is None:
|
||||||
|
|
@ -330,32 +347,49 @@ async def my_players(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
pts = _md_escape(f'{t["points"]:.1f}')
|
pts = _md_escape(f'{t["points"]:.1f}')
|
||||||
rd = t['round_done']
|
rd = t['round_done']
|
||||||
lines.append(f'📌 {safe_tour}')
|
lines.append(f'📌 {safe_tour}')
|
||||||
lines.append(f' {pts} очк\\., тур {rd}')
|
lines.append(f' {pts} {loc["points_label"]}, {loc["round_label"]} {rd}')
|
||||||
|
|
||||||
lines.append(f'🗑 /removeplayer {fide_id}')
|
keyboard = InlineKeyboardMarkup([[
|
||||||
|
InlineKeyboardButton('🗑 Удалить', callback_data=f'remove:{fide_id}')
|
||||||
lines.append(sep)
|
]])
|
||||||
await update.message.reply_text('\n'.join(lines),
|
return '\n'.join(lines), keyboard
|
||||||
parse_mode=ParseMode.MARKDOWN_V2)
|
|
||||||
|
|
||||||
|
|
||||||
async def remove_player(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def my_players(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
user = update.effective_user
|
user = update.effective_user
|
||||||
parts = update.message.text.strip().split()
|
loc = _get_locale(user.language_code or '')
|
||||||
if len(parts) < 2 or not parts[1].isdigit():
|
players = tracker.get_user_subs(user.id)
|
||||||
await update.message.reply_text(
|
if not players:
|
||||||
'Использование: `/removeplayer FIDE_ID`\n'
|
await update.message.reply_text(loc['no_players'],
|
||||||
'/myplayers — посмотреть список',
|
|
||||||
parse_mode=ParseMode.MARKDOWN_V2)
|
parse_mode=ParseMode.MARKDOWN_V2)
|
||||||
return
|
return
|
||||||
fide_id = int(parts[1])
|
|
||||||
|
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:
|
||||||
|
text, keyboard = _player_card(p, loc)
|
||||||
|
await update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN_V2,
|
||||||
|
reply_markup=keyboard)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
ok = tracker.remove_player_subscriptions(fide_id, user.id)
|
||||||
|
|
||||||
if ok:
|
if ok:
|
||||||
await update.message.reply_text('✅ Игрок удалён из подписок\.',
|
# Strike through the card text and remove the button
|
||||||
parse_mode=ParseMode.MARKDOWN_V2)
|
struck = query.message.text.replace('👤', '❌')
|
||||||
|
await query.edit_message_text(struck, reply_markup=None)
|
||||||
else:
|
else:
|
||||||
await update.message.reply_text('Игрок не найден\. /myplayers — список',
|
await query.edit_message_text(query.message.text + '\n\n_уже удалён_',
|
||||||
parse_mode=ParseMode.MARKDOWN_V2)
|
parse_mode=ParseMode.MARKDOWN_V2,
|
||||||
|
reply_markup=None)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_fide_id(text: str) -> int | 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('addplayer', add_player))
|
||||||
app.add_handler(CommandHandler('cancel', cancel_sub))
|
app.add_handler(CommandHandler('cancel', cancel_sub))
|
||||||
app.add_handler(CommandHandler('myplayers', my_players))
|
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.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_url))
|
||||||
|
|
||||||
app.job_queue.run_repeating(
|
app.job_queue.run_repeating(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue