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('final_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}"