2025-10-26 20:23:26 +03:00
|
|
|
|
from typing import Dict, Any, Optional
|
|
|
|
|
|
from datetime import datetime
|
2025-11-12 23:20:01 +03:00
|
|
|
|
from i18n import t
|
2025-10-26 20:23:26 +03:00
|
|
|
|
|
|
|
|
|
|
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:
|
2025-11-16 13:24:39 +03:00
|
|
|
|
return "⚪ 0"
|
2025-10-26 20:23:26 +03:00
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
2025-11-12 23:20:01 +03:00
|
|
|
|
def format_stats_response(data: Dict[str, Any], username: str, period: str, lang: str = 'en') -> str:
|
2025-10-26 20:23:26 +03:00
|
|
|
|
"""Format statistics response according to the template"""
|
|
|
|
|
|
if not data or data.get('data') is None:
|
2025-11-12 23:20:01 +03:00
|
|
|
|
message = data.get('message', t('no_data', lang)) if data else t('no_data', lang)
|
2025-10-26 20:23:26 +03:00
|
|
|
|
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
|
2025-11-12 23:20:01 +03:00
|
|
|
|
date_range = StatsFormatter._get_date_range(period, lang)
|
2025-10-26 20:23:26 +03:00
|
|
|
|
|
|
|
|
|
|
# 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)
|
2025-11-12 23:20:01 +03:00
|
|
|
|
task_text = t('puzzles_section', lang, total=total_tasks, solved=solved, unsolved=unsolved)
|
2025-10-26 20:23:26 +03:00
|
|
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
2025-11-12 23:20:01 +03:00
|
|
|
|
# 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
|
|
|
|
|
|
)
|
2025-10-26 20:23:26 +03:00
|
|
|
|
|
|
|
|
|
|
# Combine all parts
|
2025-11-12 23:20:01 +03:00
|
|
|
|
result = t('stats_title', lang, username=username, date_range=date_range)
|
2025-10-26 20:23:26 +03:00
|
|
|
|
result += task_text
|
|
|
|
|
|
result += games_text.rstrip()
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
2025-11-12 23:20:01 +03:00
|
|
|
|
def _get_date_range(period: str, lang: str = 'en') -> str:
|
2025-10-26 20:23:26 +03:00
|
|
|
|
"""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
|
2025-11-12 23:20:01 +03:00
|
|
|
|
def format_period_notification(username: str, games_data: Optional[Dict], puzzles_data: Optional[Dict], period_minutes: int, lang: str = 'en') -> str:
|
2025-10-26 20:23:26 +03:00
|
|
|
|
"""Format notification for periodic checks"""
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
|
|
# Format period text
|
|
|
|
|
|
if period_minutes == 1:
|
2025-11-12 23:20:01 +03:00
|
|
|
|
period_text = t('period_1_minute', lang)
|
2025-10-26 20:23:26 +03:00
|
|
|
|
elif period_minutes in [2, 3, 4]:
|
2025-11-12 23:20:01 +03:00
|
|
|
|
period_text = t('period_2_3_4_minutes', lang, period=period_minutes)
|
2025-10-26 20:23:26 +03:00
|
|
|
|
else:
|
2025-11-12 23:20:01 +03:00
|
|
|
|
period_text = t('period_minutes_text', lang, period=period_minutes)
|
2025-10-26 20:23:26 +03:00
|
|
|
|
|
2025-11-12 23:20:01 +03:00
|
|
|
|
result = t('period_notification_title', lang, username=username, period_text=period_text)
|
2025-10-26 20:23:26 +03:00
|
|
|
|
|
|
|
|
|
|
# Format puzzles first (if available and there's actual activity)
|
|
|
|
|
|
if puzzles_data and 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)
|
|
|
|
|
|
|
|
|
|
|
|
# Only show tasks section if there's actual activity (not all zeros)
|
|
|
|
|
|
if total_puzzles > 0 or solved > 0 or failed > 0:
|
2025-11-12 23:20:01 +03:00
|
|
|
|
result += t('period_puzzles_section', lang, total=total_puzzles, solved=solved, failed=failed)
|
2025-10-26 20:23:26 +03:00
|
|
|
|
|
|
|
|
|
|
# Format games
|
|
|
|
|
|
if games_data and games_data.get('data'):
|
|
|
|
|
|
games_info = games_data['data']
|
|
|
|
|
|
total_games = games_info.get('total', {}).get('games_played', 0)
|
|
|
|
|
|
|
|
|
|
|
|
# Show details for each game type if there were games
|
|
|
|
|
|
if total_games > 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:
|
|
|
|
|
|
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)
|
2025-11-12 23:20:01 +03:00
|
|
|
|
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
|
|
|
|
|
|
)
|
2025-10-26 20:23:26 +03:00
|
|
|
|
|
|
|
|
|
|
# If no activity
|
|
|
|
|
|
if not (games_data and games_data.get('data')) and not (puzzles_data and puzzles_data.get('data')):
|
2025-11-12 23:20:01 +03:00
|
|
|
|
result += t('no_activity', lang)
|
2025-10-26 20:23:26 +03:00
|
|
|
|
|
|
|
|
|
|
return result.rstrip()
|
2025-11-16 12:48:23 +03:00
|
|
|
|
|
|
|
|
|
|
@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"
|
2025-11-16 13:24:39 +03:00
|
|
|
|
earliest_ts = data.get('earliest_game_ts')
|
|
|
|
|
|
if isinstance(earliest_ts, int):
|
|
|
|
|
|
earliest = datetime.fromtimestamp(earliest_ts).strftime("%d.%m.%Y")
|
2025-11-16 20:07:52 +03:00
|
|
|
|
header += f"\n\n\nStart of these 1000 games: {earliest}"
|
2025-11-16 12:48:23 +03:00
|
|
|
|
else:
|
|
|
|
|
|
header = f"📈 {username}: last year (rated), games: {games_count}"
|
2025-11-16 13:24:39 +03:00
|
|
|
|
# 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")
|
2025-11-16 12:48:23 +03:00
|
|
|
|
end_str = datetime.fromtimestamp(period_end).strftime("%d.%m.%Y")
|
2025-11-16 20:07:52 +03:00
|
|
|
|
header += f"\n\n\nPeriod: {start_str}–{end_str}"
|
2025-11-16 12:48:23 +03:00
|
|
|
|
# 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(
|
2025-11-16 13:24:39 +03:00
|
|
|
|
f"{emoji} {mode.title()}: {games_played} Δ {rating_change_str} R {rating_str} ✅ {wins} ❌ {losses} 🤝 {draws}"
|
2025-11-16 12:48:23 +03:00
|
|
|
|
)
|
2025-11-16 20:07:52 +03:00
|
|
|
|
# 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}"
|