LichessStatTgWeb/LichessClientTG_bot/formatters.py

251 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from typing import Dict, Any, Optional
from datetime import datetime
from i18n import t
class StatsFormatter:
@staticmethod
def _format_rating_change(rating_change: int) -> str:
"""Format rating change with colored circles"""
if rating_change > 0:
return f"🟢 +{rating_change}"
elif rating_change < 0:
return f"🔴 {rating_change}"
else:
return "⚪ 0"
@staticmethod
def format_stats_response(data: Dict[str, Any], username: str, period: str, lang: str = 'en') -> str:
"""Format statistics response according to the template"""
if not data or data.get('data') is None:
message = data.get('message', t('no_data', lang)) if data else t('no_data', lang)
# Filter out old "No active player" messages - this functionality is deprecated
if 'No active player' in message or 'Нет активного игрока' in message or 'active player' in message.lower() or 'активного игрока' in message.lower():
return t('no_data', lang)
return f"📭 {message}"
# Extract data from API response
api_data = data.get('data', {})
tasks = api_data.get('tasks', {})
games = api_data.get('games', {})
# Format date range
date_range = StatsFormatter._get_date_range(period, lang)
# Format tasks section
task_text = ""
if tasks and tasks.get('total', 0) > 0:
total_tasks = tasks.get('total', 0)
solved = tasks.get('solved', 0)
unsolved = tasks.get('unsolved', 0)
task_text = t('puzzles_section', lang, total=total_tasks, solved=solved, unsolved=unsolved)
# Format games section
games_text = ""
if games:
for game_type, game_data in games.items():
if not game_data or game_data.get('games_played', 0) == 0:
continue
# Get game type emoji
emoji = StatsFormatter._get_game_type_emoji(game_type)
games_count = game_data.get('games_played', 0)
rating_change = game_data.get('rating_change', 0)
rating = game_data.get('final_rating', 0)
wins = game_data.get('wins', 0)
losses = game_data.get('losses', 0)
draws = game_data.get('draws', 0)
# Format rating change
rating_change_str = StatsFormatter._format_rating_change(rating_change)
# Get game type name (capitalize first letter)
game_type_name = game_type.title()
games_text += t('games_section', lang,
emoji=emoji,
game_type=game_type_name,
games_count=games_count,
rating_change=rating_change_str,
rating=rating,
wins=wins,
losses=losses,
draws=draws
)
# Combine all parts
result = t('stats_title', lang, username=username, date_range=date_range)
result += task_text
result += games_text.rstrip()
return result
@staticmethod
def _get_date_range(period: str, lang: str = 'en') -> str:
"""Get date range string for the period"""
from datetime import datetime, timedelta
today = datetime.now()
if period == "today":
return today.strftime("%d.%m.%Y")
elif period == "yesterday":
yesterday = today - timedelta(days=1)
return yesterday.strftime("%d.%m.%Y")
elif period == "week":
week_ago = today - timedelta(days=7)
return f"{week_ago.strftime('%d.%m.%Y')}{today.strftime('%d.%m.%Y')}"
else:
return today.strftime("%d.%m.%Y")
@staticmethod
def _get_game_type_emoji(game_type: str) -> str:
"""Get emoji for game type"""
emoji_map = {
'bullet': '⚡️',
'blitz': '🔥',
'rapid': '🐇',
'classical': '♟️',
'correspondence': '📮'
}
return emoji_map.get(game_type.lower(), '🎯')
@staticmethod
def format_period_notification(username: str, games_data: Optional[Dict], puzzles_data: Optional[Dict], period_minutes: int, lang: str = 'en') -> str:
"""Format notification for periodic checks"""
from datetime import datetime
# Format period text
if period_minutes == 1:
period_text = t('period_1_minute', lang)
elif period_minutes in [2, 3, 4]:
period_text = t('period_2_3_4_minutes', lang, period=period_minutes)
else:
period_text = t('period_minutes_text', lang, period=period_minutes)
result = t('period_notification_title', lang, username=username, period_text=period_text)
# Format puzzles first (if available and there's actual activity)
has_puzzles_data = False
if puzzles_data:
# Check puzzles_in_period on top level first (priority)
top_level_puzzles = puzzles_data.get('puzzles_in_period', 0)
# Also check data.total_attempts
if puzzles_data.get('data'):
puzzles_info = puzzles_data['data']
total_puzzles = puzzles_info.get('total_attempts', 0)
solved = puzzles_info.get('solved', 0)
failed = puzzles_info.get('failed', 0)
effective_puzzles = top_level_puzzles if top_level_puzzles > 0 else total_puzzles
# Only show tasks section if there's actual activity (not all zeros)
if effective_puzzles > 0 or solved > 0 or failed > 0:
has_puzzles_data = True
result += t('period_puzzles_section', lang, total=effective_puzzles, solved=solved, failed=failed)
# Format games
has_games_data = False
if games_data and games_data.get('data'):
games_info = games_data['data']
# Check games_count on top level first (priority)
top_level_games_count = games_data.get('games_count', 0)
# Also check data.total.games_played
total_games = games_info.get('total', {}).get('games_played', 0)
# Use top-level games_count if available, otherwise use total.games_played
effective_games_count = top_level_games_count if top_level_games_count > 0 else total_games
# Show details for each game type if there were games
if effective_games_count > 0:
for game_type, game_data in games_info.items():
if game_type != 'total' and game_data and game_data.get('games_played', 0) > 0:
has_games_data = True # Only set to True if we actually add game data
emoji = StatsFormatter._get_game_type_emoji(game_type)
games_count = game_data.get('games_played', 0)
rating_change = game_data.get('rating_change', 0)
rating = game_data.get('rating', 0)
wins = game_data.get('wins', 0)
losses = game_data.get('losses', 0)
draws = game_data.get('draws', 0)
rating_change_str = StatsFormatter._format_rating_change(rating_change)
game_type_name = game_type.title()
result += t('period_games_section', lang,
emoji=emoji,
game_type=game_type_name,
games_count=games_count,
rating_change=rating_change_str,
rating=rating,
wins=wins,
losses=losses,
draws=draws
)
# If no activity at all
if not has_games_data and not has_puzzles_data:
result += t('no_activity', lang)
return result.rstrip()
@staticmethod
def format_last_year_or_1000(data: Dict[str, Any], username: str, lang: str = 'en') -> str:
"""
Format response for last year or last 1000 games.
Expects GamesOfPeriodResponse-like payload.
"""
if not data:
return "📭 No data"
games_count = data.get('games_count', 0)
period_start = data.get('period_start')
period_end = data.get('period_end')
stats = (data.get('data') or {})
# Title and subheader
if games_count >= 1000:
header = f"📈 {username}: last 1000 rated games"
earliest_ts = data.get('earliest_game_ts')
if isinstance(earliest_ts, int):
earliest = datetime.fromtimestamp(earliest_ts).strftime("%d.%m.%Y")
header += f"\n\n\nStart of these 1000 games: {earliest}"
else:
header = f"📈 {username}: last year (rated), games: {games_count}"
# Use earliest actual game date instead of naive 'year ago'
earliest_ts = data.get('earliest_game_ts', period_start)
if isinstance(earliest_ts, int) and isinstance(period_end, int):
start_str = datetime.fromtimestamp(earliest_ts).strftime("%d.%m.%Y")
end_str = datetime.fromtimestamp(period_end).strftime("%d.%m.%Y")
header += f"\n\n\nPeriod: {start_str}{end_str}"
# Body per mode
lines = []
for mode in ["bullet", "blitz", "rapid", "classical", "correspondence"]:
mode_stats = stats.get(mode)
if not mode_stats:
continue
games_played = mode_stats.get('games_played', 0)
if games_played == 0:
continue
emoji = StatsFormatter._get_game_type_emoji(mode)
wins = mode_stats.get('wins', 0)
losses = mode_stats.get('losses', 0)
draws = mode_stats.get('draws', 0)
rating_change = mode_stats.get('rating_change', 0)
rating_change_str = StatsFormatter._format_rating_change(rating_change)
rating = mode_stats.get('rating')
rating_str = rating if rating is not None else ""
lines.append(
f"{emoji} {mode.title()}: {games_played} Δ {rating_change_str} R {rating_str}{wins}{losses} 🤝 {draws}"
)
# Join lines with newlines between each mode
# Between regular modes: one empty line (\n\n)
# Before last mode: two empty lines (\n\n\n)
if len(lines) == 0:
body = ""
elif len(lines) == 1:
body = lines[0]
else:
# All modes except last joined with one empty line
body = "\n\n".join(lines[:-1])
# Add two empty lines before last mode
body += "\n\n\n" + lines[-1]
return f"{header}\n\n{body}"