EN language only

This commit is contained in:
vrubelroman 2025-11-12 23:20:01 +03:00
parent 3ec1fe614d
commit 3362bf89e2
4 changed files with 353 additions and 94 deletions

View file

@ -1,5 +1,6 @@
import asyncio import asyncio
import logging import logging
import sqlite3
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
@ -17,6 +18,7 @@ from config import (
from database import Database from database import Database
from lichess_api import LichessAPI from lichess_api import LichessAPI
from formatters import StatsFormatter from formatters import StatsFormatter
from i18n import t
# Configure logging # Configure logging
logging.basicConfig( logging.basicConfig(
@ -35,6 +37,20 @@ class LichessBot:
self.periodic_tasks = {} # Store periodic tasks self.periodic_tasks = {} # Store periodic tasks
self.period_start_times = {} # Store start times for each gamer self.period_start_times = {} # Store start times for each gamer
self.application = None # Will be set when application is created self.application = None # Will be set when application is created
def get_user_language_from_update(self, update: Update) -> str:
"""Always return English language"""
# Update user info in database
user = update.effective_user
if user:
self.db.add_or_get_telegram_user(
user_id=user.id,
username=user.username,
first_name=user.first_name,
last_name=user.last_name,
language_code=None
)
return 'en'
async def start_existing_periodic_tasks(self): async def start_existing_periodic_tasks(self):
"""Start periodic tasks for all user-gamer pairs that have periods set""" """Start periodic tasks for all user-gamer pairs that have periods set"""
@ -56,41 +72,17 @@ class LichessBot:
"""Start command handler""" """Start command handler"""
# Register user in database # Register user in database
user = update.effective_user user = update.effective_user
lang_code = user.language_code if user else None
self.db.add_or_get_telegram_user( self.db.add_or_get_telegram_user(
user_id=user.id, user_id=user.id,
username=user.username, username=user.username,
first_name=user.first_name, first_name=user.first_name,
last_name=user.last_name last_name=user.last_name,
language_code=lang_code
) )
await update.message.reply_text( lang = self.get_user_language_from_update(update)
"Бот отслеживает игру одного или нескольких игроков на Lichess.\n" await update.message.reply_text(t('start_message', lang))
"Он показывает рейтинги, дневную, вчерашнюю и недельную активность.\n"
"При указании user_token (с правом чтения задач) можно также получать данные по задачам.\n"
"Бот поддерживает автоматические проверки с заданным интервалом и отправляет отчёт, если за это время была активность.\n\n"
"Пример за неделю:\n"
"🧩 Задачи: 114 (✅ 81 - ❌ 33)\n\n"
"🔥 Blitz — 5 игр • 🔴 -10\n"
"Рейтинг: 2245\n"
"✅ Победы: 1\n"
"❌ Поражения: 3\n"
"🤝 Ничьи: 1\n\n"
"🐇 Rapid — 19 игр • 🟢 +20\n"
"Рейтинг: 2248\n"
"✅ Победы: 8\n"
"❌ Поражения: 4\n"
"🤝 Ничьи: 7\n\n"
"📋 Доступные команды:\n"
"/addgamer - Добавить игрока Lichess для отслеживания (только username)\n"
"/addtoken - Добавить игрока с токеном для получения данных по задачам\n"
"/getgamers - Выбрать активного игрока (для которого будут работать команды /today и тд)\n"
"/delgamer - Удалить игрока из списка отслеживаемых\n"
"/today - Статистика за сегодня\n"
"/yesterday - Статистика за вчера\n"
"/week - Статистика за неделю\n"
"/setperiod - Настроить периодические уведомления активного игрока\n"
"(активный игрок меняется в меню команды /getgamers)"
)
async def start_and_addgamer(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def start_and_addgamer(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Start command that automatically launches addgamer""" """Start command that automatically launches addgamer"""
@ -101,16 +93,14 @@ class LichessBot:
async def addgamer_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def addgamer_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Start addgamer command - simple username only""" """Start addgamer command - simple username only"""
await update.message.reply_text("👤 Введите Lichess username игрока для отслеживания:") lang = self.get_user_language_from_update(update)
await update.message.reply_text(t('addgamer_prompt', lang))
return WAITING_FOR_USERNAME return WAITING_FOR_USERNAME
async def addtoken_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def addtoken_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Start addtoken command - token required""" """Start addtoken command - token required"""
await update.message.reply_text( lang = self.get_user_language_from_update(update)
"🔑 Введите Lichess API токен, чтобы получать данные по задачам.\n" await update.message.reply_text(t('addtoken_prompt', lang))
"Токен создаётся в настройках профиля — дайте ему только право puzzle:read.\n"
"После этого будет добавлен игрок, соответствующий этому токену."
)
return WAITING_FOR_TOKEN return WAITING_FOR_TOKEN
async def handle_token(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def handle_token(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
@ -127,11 +117,12 @@ class LichessBot:
user_gamers = self.db.get_user_gamers(user_id) user_gamers = self.db.get_user_gamers(user_id)
existing_gamer = next((g for g in user_gamers if g['username'] == username), None) existing_gamer = next((g for g in user_gamers if g['username'] == username), None)
lang = self.get_user_language_from_update(update)
if existing_gamer: if existing_gamer:
# Update token for existing gamer # Update token for existing gamer
self.db.add_user_gamer(user_id, existing_gamer['id'], token) self.db.add_user_gamer(user_id, existing_gamer['id'], token)
await update.message.reply_text( await update.message.reply_text(
f"✅ Токен добавлен для игрока {username}!" t('token_added', lang, username=username)
) )
else: else:
# Add new gamer and link with token # Add new gamer and link with token
@ -147,17 +138,19 @@ class LichessBot:
self.db.set_user_active_gamer(user_id, gamer_id) self.db.set_user_active_gamer(user_id, gamer_id)
await update.message.reply_text( await update.message.reply_text(
f"✅ Игрок {username} добавлен с токеном!" t('gamer_added_with_token', lang, username=username)
) )
return ConversationHandler.END return ConversationHandler.END
else: else:
lang = self.get_user_language_from_update(update)
await update.message.reply_text( await update.message.reply_text(
"Не удалось получить username из токена. Попробуйте еще раз." t('token_username_error', lang)
) )
return WAITING_FOR_TOKEN return WAITING_FOR_TOKEN
else: else:
lang = self.get_user_language_from_update(update)
await update.message.reply_text( await update.message.reply_text(
"❌ Неверный токен. Попробуйте еще раз." t('invalid_token', lang)
) )
return WAITING_FOR_TOKEN return WAITING_FOR_TOKEN
@ -166,9 +159,10 @@ class LichessBot:
username = update.message.text.strip() username = update.message.text.strip()
user_id = update.effective_user.id user_id = update.effective_user.id
lang = self.get_user_language_from_update(update)
if not username: if not username:
await update.message.reply_text( await update.message.reply_text(
"❌ Username не может быть пустым. Попробуйте еще раз." t('empty_username', lang)
) )
return WAITING_FOR_USERNAME return WAITING_FOR_USERNAME
@ -176,7 +170,7 @@ class LichessBot:
user_exists = await self.lichess_api.check_user_exists(username) user_exists = await self.lichess_api.check_user_exists(username)
if not user_exists: if not user_exists:
await update.message.reply_text( await update.message.reply_text(
f"❌ Игрок {username} не найден на Lichess. Проверьте правильность написания имени." t('user_not_found', lang, username=username)
) )
return WAITING_FOR_USERNAME return WAITING_FOR_USERNAME
@ -193,8 +187,9 @@ class LichessBot:
if len(user_gamers) == 1: if len(user_gamers) == 1:
self.db.set_user_active_gamer(user_id, gamer_id) self.db.set_user_active_gamer(user_id, gamer_id)
lang = self.get_user_language_from_update(update)
await update.message.reply_text( await update.message.reply_text(
f"✅ Игрок {username} успешно добавлен!" t('gamer_added', lang, username=username)
) )
return ConversationHandler.END return ConversationHandler.END
@ -206,12 +201,13 @@ class LichessBot:
logger.info(f"getgamers: user_id={user_id}, found {len(gamers)} gamers") logger.info(f"getgamers: user_id={user_id}, found {len(gamers)} gamers")
lang = self.get_user_language_from_update(update)
if not gamers: if not gamers:
await update.message.reply_text("📭 В базе нет игроков. Используйте /adduser для добавления.") await update.message.reply_text(t('no_gamers', lang))
return return
# Show loading message # Show loading message
loading_msg = await update.message.reply_text("🔄 Загружаем рейтинги игроков...") loading_msg = await update.message.reply_text(t('loading_ratings', lang))
# Prepare data for each gamer # Prepare data for each gamer
gamers_data = [] gamers_data = []
@ -240,7 +236,8 @@ class LichessBot:
# Add period information if period > 0 # Add period information if period > 0
period_minutes = gamer.get('period_minutes', 0) period_minutes = gamer.get('period_minutes', 0)
period_text = f" · {period_minutes}м" if period_minutes > 0 else "" period_suffix = t('period_minutes_suffix', lang)
period_text = f" · {period_minutes}{period_suffix}" if period_minutes > 0 else ""
gamers_data.append({ gamers_data.append({
'id': gamer['id'], 'id': gamer['id'],
@ -257,6 +254,9 @@ class LichessBot:
import traceback import traceback
logger.error(f"Traceback: {traceback.format_exc()}") logger.error(f"Traceback: {traceback.format_exc()}")
# Still add the gamer with N/A ratings # Still add the gamer with N/A ratings
period_minutes = gamer.get('period_minutes', 0)
period_suffix = t('period_minutes_suffix', lang)
period_text = f" · {period_minutes}{period_suffix}" if period_minutes > 0 else ""
gamers_data.append({ gamers_data.append({
'id': gamer['id'], 'id': gamer['id'],
'status': "🟢" if gamer['is_active'] else "", 'status': "🟢" if gamer['is_active'] else "",
@ -264,7 +264,7 @@ class LichessBot:
'bullet': 'N/A', 'bullet': 'N/A',
'blitz': 'N/A', 'blitz': 'N/A',
'rapid': 'N/A', 'rapid': 'N/A',
'period': f" · {gamer.get('period_minutes', 0)}м" if gamer.get('period_minutes', 0) > 0 else "" 'period': period_text
}) })
logger.info(f"Added gamer {gamer['username']} with N/A ratings due to error") logger.info(f"Added gamer {gamer['username']} with N/A ratings due to error")
@ -277,7 +277,7 @@ class LichessBot:
) )
logger.info(f"getgamers: prepared {len(gamers_data)} gamers for display") logger.info(f"getgamers: prepared {len(gamers_data)} gamers for display")
gamers_text = "👥 <b>Выберите активного игрока:</b>\n\n" + "\n".join(text_lines) gamers_text = t('select_active_gamer', lang) + "\n".join(text_lines)
logger.info(f"getgamers: message length: {len(gamers_text)} characters") logger.info(f"getgamers: message length: {len(gamers_text)} characters")
@ -325,26 +325,37 @@ class LichessBot:
gamers = self.db.get_user_gamers(user_id) gamers = self.db.get_user_gamers(user_id)
selected_gamer = next((g for g in gamers if g['id'] == gamer_id), None) selected_gamer = next((g for g in gamers if g['id'] == gamer_id), None)
# Для callback query обновляем язык пользователя если есть в update
if update.effective_user:
self.db.add_or_get_telegram_user(
user_id=update.effective_user.id,
username=update.effective_user.username,
first_name=update.effective_user.first_name,
last_name=update.effective_user.last_name,
language_code=update.effective_user.language_code
)
lang = self.db.get_user_language(user_id)
if selected_gamer: if selected_gamer:
# Set active gamer for this user # Set active gamer for this user
self.db.set_user_active_gamer(user_id, gamer_id) self.db.set_user_active_gamer(user_id, gamer_id)
await query.edit_message_text( await query.edit_message_text(
f"✅ Активный игрок: {selected_gamer['username']}" t('active_gamer_set', lang, username=selected_gamer['username'])
) )
else: else:
await query.edit_message_text("❌ Игрок не найден") await query.edit_message_text(t('gamer_not_found', lang))
async def delgamer(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def delgamer(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Show gamers list for deletion""" """Show gamers list for deletion"""
user_id = update.effective_user.id user_id = update.effective_user.id
gamers = self.db.get_user_gamers(user_id) gamers = self.db.get_user_gamers(user_id)
lang = self.get_user_language_from_update(update)
if not gamers: if not gamers:
await update.message.reply_text("📭 У вас нет отслеживаемых игроков.") await update.message.reply_text(t('no_gamers_to_delete', lang))
return return
# Show loading message # Show loading message
loading_msg = await update.message.reply_text("🔄 Загружаем список игроков...") loading_msg = await update.message.reply_text(t('loading_gamers', lang))
# Create text message with stats # Create text message with stats
text_lines = [] text_lines = []
@ -370,7 +381,8 @@ class LichessBot:
bullet_rating = blitz_rating = rapid_rating = 'N/A' bullet_rating = blitz_rating = rapid_rating = 'N/A'
period_minutes = gamer.get('period_minutes', 0) period_minutes = gamer.get('period_minutes', 0)
period_text = f" · {period_minutes}м" if period_minutes > 0 else "" period_suffix = t('period_minutes_suffix', lang)
period_text = f" · {period_minutes}{period_suffix}" if period_minutes > 0 else ""
text_lines.append( text_lines.append(
f"{status} <b>{username}</b> " f"{status} <b>{username}</b> "
@ -383,7 +395,7 @@ class LichessBot:
callback_data=f"delete_{gamer['id']}" callback_data=f"delete_{gamer['id']}"
)]) )])
gamers_text = "🗑️ <b>Выберите игрока для удаления:</b>\n\n" + "\n".join(text_lines) gamers_text = t('select_gamer_to_delete', lang) + "\n".join(text_lines)
reply_markup = InlineKeyboardMarkup(keyboard) reply_markup = InlineKeyboardMarkup(keyboard)
@ -418,19 +430,29 @@ class LichessBot:
gamers = self.db.get_user_gamers(user_id) gamers = self.db.get_user_gamers(user_id)
gamer_to_delete = next((g for g in gamers if g['id'] == gamer_id), None) gamer_to_delete = next((g for g in gamers if g['id'] == gamer_id), None)
# Для callback query получаем язык из БД
if update.effective_user:
self.db.add_or_get_telegram_user(
user_id=update.effective_user.id,
username=update.effective_user.username,
first_name=update.effective_user.first_name,
last_name=update.effective_user.last_name,
language_code=update.effective_user.language_code
)
lang = self.db.get_user_language(user_id)
if gamer_to_delete: if gamer_to_delete:
username = gamer_to_delete['username'] username = gamer_to_delete['username']
deleted = self.db.remove_user_gamer(user_id, gamer_id) deleted = self.db.remove_user_gamer(user_id, gamer_id)
if deleted: if deleted:
await query.edit_message_text( await query.edit_message_text(
f"✅ Игрок <b>{username}</b> удален из списка отслеживаемых.", t('gamer_deleted', lang, username=username),
parse_mode='HTML' parse_mode='HTML'
) )
else: else:
await query.edit_message_text("Не удалось удалить игрока") await query.edit_message_text(t('delete_failed', lang))
else: else:
await query.edit_message_text("❌ Игрок не найден") await query.edit_message_text(t('gamer_not_found', lang))
async def get_stats(self, update: Update, context: ContextTypes.DEFAULT_TYPE, period: str): async def get_stats(self, update: Update, context: ContextTypes.DEFAULT_TYPE, period: str):
"""Get statistics for a period""" """Get statistics for a period"""
@ -439,9 +461,10 @@ class LichessBot:
# Get active gamer for this user # Get active gamer for this user
active_gamer = self.db.get_user_active_gamer(user_id) active_gamer = self.db.get_user_active_gamer(user_id)
lang = self.get_user_language_from_update(update)
if not active_gamer: if not active_gamer:
await update.message.reply_text( await update.message.reply_text(
"❌ Нет активного игрока. Используйте /getgamers для выбора." t('no_active_gamer', lang)
) )
return return
@ -455,11 +478,11 @@ class LichessBot:
elif period == "week": elif period == "week":
data = await self.lichess_api.get_week_stats(username) data = await self.lichess_api.get_week_stats(username)
else: else:
await update.message.reply_text("❌ Неизвестный период") await update.message.reply_text(t('unknown_period', lang))
return return
# Format and send response # Format and send response
formatted_response = StatsFormatter.format_stats_response(data, username, period) formatted_response = StatsFormatter.format_stats_response(data, username, period, lang)
await update.message.reply_text(formatted_response) await update.message.reply_text(formatted_response)
async def today(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def today(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
@ -481,24 +504,24 @@ class LichessBot:
# Get active gamer for this user # Get active gamer for this user
active_gamer = self.db.get_user_active_gamer(user_id) active_gamer = self.db.get_user_active_gamer(user_id)
lang = self.get_user_language_from_update(update)
if not active_gamer: if not active_gamer:
await update.message.reply_text( await update.message.reply_text(
"❌ Нет активного игрока. Используйте /getgamers для выбора." t('no_active_gamer', lang)
) )
return return
keyboard = [] keyboard = []
for period in PERIOD_OPTIONS: for period in PERIOD_OPTIONS:
if period == 0: if period == 0:
button_text = "❌ Отключить уведомления" button_text = t('disable_notifications', lang)
else: else:
button_text = f"{period} минут" button_text = t('period_minutes', lang, period=period)
keyboard.append([InlineKeyboardButton(button_text, callback_data=f"period_{period}")]) keyboard.append([InlineKeyboardButton(button_text, callback_data=f"period_{period}")])
reply_markup = InlineKeyboardMarkup(keyboard) reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text( await update.message.reply_text(
f"⏱️ Выберите период для игрока {active_gamer['username']}:\n" t('select_period', lang, username=active_gamer['username']),
f"📱 Уведомления будут приходить в личные сообщения",
reply_markup=reply_markup reply_markup=reply_markup
) )
@ -513,23 +536,83 @@ class LichessBot:
# Get active gamer for this user # Get active gamer for this user
active_gamer = self.db.get_user_active_gamer(user_id) active_gamer = self.db.get_user_active_gamer(user_id)
# Для callback query получаем язык из БД
if update.effective_user:
self.db.add_or_get_telegram_user(
user_id=update.effective_user.id,
username=update.effective_user.username,
first_name=update.effective_user.first_name,
last_name=update.effective_user.last_name,
language_code=update.effective_user.language_code
)
lang = self.db.get_user_language(user_id)
if active_gamer: if active_gamer:
# Set period for this user-gamer pair # Set period for this user-gamer pair
self.db.set_user_gamer_period(user_id, active_gamer['id'], period) self.db.set_user_gamer_period(user_id, active_gamer['id'], period)
if period == 0: if period == 0:
await query.edit_message_text( await query.edit_message_text(
f"✅ Уведомления для {active_gamer['username']} отключены" t('notifications_disabled', lang, username=active_gamer['username'])
) )
else: else:
await query.edit_message_text( await query.edit_message_text(
f"✅ Период {period} минут установлен для {active_gamer['username']}\n" t('period_set', lang, period=period, username=active_gamer['username'])
f"📱 Уведомления будут приходить в личные сообщения"
) )
# Start periodic task for this gamer (send to user's personal messages) # Start periodic task for this gamer (send to user's personal messages)
await self.start_periodic_task(active_gamer, user_id, period) await self.start_periodic_task(active_gamer, user_id, period)
async def check_language(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Check and display current language settings"""
user = update.effective_user
if user:
# Обновляем язык из update
lang = self.get_user_language_from_update(update)
# Получаем язык из БД для отображения
db_lang = self.db.get_user_language(user.id)
# Получаем language_code из БД напрямую
with sqlite3.connect(self.db.db_path) as conn:
cursor = conn.cursor()
cursor.execute("SELECT language_code FROM telegram_users WHERE user_id = ?", (user.id,))
row = cursor.fetchone()
db_language_code = row[0] if row and row[0] else None
message = (
f"🌐 Language Info:\n"
f"Current language: {lang}\n"
f"DB language: {db_lang}\n"
f"DB language_code: {db_language_code}\n"
f"Update language_code: {user.language_code}\n\n"
f"Language used: {lang}\n\n"
f"Bot uses English language only."
)
await update.message.reply_text(message)
else:
await update.message.reply_text("❌ Failed to get user information")
async def reset_language(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Reset user language in database - will be detected from /start"""
user = update.effective_user
if user:
# Сбрасываем язык в БД (устанавливаем NULL)
with sqlite3.connect(self.db.db_path) as conn:
cursor = conn.cursor()
cursor.execute(
"UPDATE telegram_users SET language_code = NULL WHERE user_id = ?",
(user.id,)
)
conn.commit()
# Language is always English
lang = 'en'
await update.message.reply_text(
"✅ Language reset! Bot uses English language only."
)
else:
await update.message.reply_text("❌ Failed to get user information")
async def start_periodic_task(self, gamer: Dict[str, Any], user_id: int, period_minutes: int): async def start_periodic_task(self, gamer: Dict[str, Any], user_id: int, period_minutes: int):
"""Start periodic task for a gamer""" """Start periodic task for a gamer"""
task_key = f"{gamer['id']}_{user_id}" task_key = f"{gamer['id']}_{user_id}"
@ -622,8 +705,10 @@ class LichessBot:
# Отправляем уведомление только если есть реальная активность # Отправляем уведомление только если есть реальная активность
if has_games or has_puzzles: if has_games or has_puzzles:
try: try:
# Get user language from database
user_lang = self.db.get_user_language(user_id)
notification = StatsFormatter.format_period_notification( notification = StatsFormatter.format_period_notification(
gamer['username'], games_data, puzzles_data, period_minutes gamer['username'], games_data, puzzles_data, period_minutes, lang=user_lang
) )
if self.application: if self.application:
@ -690,6 +775,8 @@ class LichessBot:
application.add_handler(CommandHandler("yesterday", self.yesterday)) application.add_handler(CommandHandler("yesterday", self.yesterday))
application.add_handler(CommandHandler("week", self.week)) application.add_handler(CommandHandler("week", self.week))
application.add_handler(CommandHandler("setperiod", self.setperiod)) application.add_handler(CommandHandler("setperiod", self.setperiod))
application.add_handler(CommandHandler("lang", self.check_language))
application.add_handler(CommandHandler("resetlang", self.reset_language))
# Callback handlers # Callback handlers
application.add_handler(CallbackQueryHandler(self.select_gamer, pattern="^select_")) application.add_handler(CallbackQueryHandler(self.select_gamer, pattern="^select_"))

View file

@ -22,10 +22,18 @@ class Database:
username TEXT, username TEXT,
first_name TEXT, first_name TEXT,
last_name TEXT, last_name TEXT,
language_code TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) )
''') ''')
# Add language_code column if it doesn't exist
try:
cursor.execute("ALTER TABLE telegram_users ADD COLUMN language_code TEXT")
except sqlite3.OperationalError:
# Column already exists
pass
# Create gamers table (Lichess players only) # Create gamers table (Lichess players only)
cursor.execute(''' cursor.execute('''
CREATE TABLE IF NOT EXISTS gamers ( CREATE TABLE IF NOT EXISTS gamers (
@ -104,7 +112,8 @@ class Database:
logger.info(f"Migrated {migrated} tokens from gamers to user_gamers") logger.info(f"Migrated {migrated} tokens from gamers to user_gamers")
def add_or_get_telegram_user(self, user_id: int, username: Optional[str] = None, def add_or_get_telegram_user(self, user_id: int, username: Optional[str] = None,
first_name: Optional[str] = None, last_name: Optional[str] = None) -> bool: first_name: Optional[str] = None, last_name: Optional[str] = None,
language_code: Optional[str] = None) -> bool:
"""Add or update Telegram user""" """Add or update Telegram user"""
with sqlite3.connect(self.db_path) as conn: with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor() cursor = conn.cursor()
@ -114,13 +123,24 @@ class Database:
if not existing: if not existing:
cursor.execute( cursor.execute(
"INSERT INTO telegram_users (user_id, username, first_name, last_name) VALUES (?, ?, ?, ?)", "INSERT INTO telegram_users (user_id, username, first_name, last_name, language_code) VALUES (?, ?, ?, ?, ?)",
(user_id, username, first_name, last_name) (user_id, username, first_name, last_name, language_code)
) )
conn.commit() conn.commit()
return True return True
else:
# Update language_code if provided (always update, even if None to clear old value)
cursor.execute(
"UPDATE telegram_users SET language_code = ? WHERE user_id = ?",
(language_code, user_id)
)
conn.commit()
return False return False
def get_user_language(self, user_id: int) -> str:
"""Always return English language"""
return 'en'
def add_gamer(self, username: str) -> int: def add_gamer(self, username: str) -> int:
"""Add a new gamer to the database (return gamer_id)""" """Add a new gamer to the database (return gamer_id)"""
with sqlite3.connect(self.db_path) as conn: with sqlite3.connect(self.db_path) as conn:

View file

@ -1,5 +1,6 @@
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from datetime import datetime from datetime import datetime
from i18n import t
class StatsFormatter: class StatsFormatter:
@staticmethod @staticmethod
@ -13,10 +14,10 @@ class StatsFormatter:
return f"0" return f"0"
@staticmethod @staticmethod
def format_stats_response(data: Dict[str, Any], username: str, period: str) -> str: def format_stats_response(data: Dict[str, Any], username: str, period: str, lang: str = 'en') -> str:
"""Format statistics response according to the template""" """Format statistics response according to the template"""
if not data or data.get('data') is None: if not data or data.get('data') is None:
message = data.get('message', 'Нет данных') if data else 'Нет данных' message = data.get('message', t('no_data', lang)) if data else t('no_data', lang)
return f"📭 {message}" return f"📭 {message}"
# Extract data from API response # Extract data from API response
@ -25,7 +26,7 @@ class StatsFormatter:
games = api_data.get('games', {}) games = api_data.get('games', {})
# Format date range # Format date range
date_range = StatsFormatter._get_date_range(period) date_range = StatsFormatter._get_date_range(period, lang)
# Format tasks section # Format tasks section
task_text = "" task_text = ""
@ -33,7 +34,7 @@ class StatsFormatter:
total_tasks = tasks.get('total', 0) total_tasks = tasks.get('total', 0)
solved = tasks.get('solved', 0) solved = tasks.get('solved', 0)
unsolved = tasks.get('unsolved', 0) unsolved = tasks.get('unsolved', 0)
task_text = f"🧩 Задачи: {total_tasks} (✅ {solved} - ❌ {unsolved})\n\n" task_text = t('puzzles_section', lang, total=total_tasks, solved=solved, unsolved=unsolved)
# Format games section # Format games section
games_text = "" games_text = ""
@ -55,21 +56,29 @@ class StatsFormatter:
# Format rating change # Format rating change
rating_change_str = StatsFormatter._format_rating_change(rating_change) rating_change_str = StatsFormatter._format_rating_change(rating_change)
games_text += f"{emoji} {game_type.title()}{games_count} игр • {rating_change_str}\n" # Get game type name (capitalize first letter)
games_text += f"Рейтинг: {rating}\n" game_type_name = game_type.title()
games_text += f"✅ Победы: {wins}\n"
games_text += f"❌ Поражения: {losses}\n" games_text += t('games_section', lang,
games_text += f"🤝 Ничьи: {draws}\n\n" 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 # Combine all parts
result = f"📊 Статистика {username}{date_range}\n\n" result = t('stats_title', lang, username=username, date_range=date_range)
result += task_text result += task_text
result += games_text.rstrip() result += games_text.rstrip()
return result return result
@staticmethod @staticmethod
def _get_date_range(period: str) -> str: def _get_date_range(period: str, lang: str = 'en') -> str:
"""Get date range string for the period""" """Get date range string for the period"""
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -99,19 +108,19 @@ class StatsFormatter:
return emoji_map.get(game_type.lower(), '🎯') return emoji_map.get(game_type.lower(), '🎯')
@staticmethod @staticmethod
def format_period_notification(username: str, games_data: Optional[Dict], puzzles_data: Optional[Dict], period_minutes: int) -> str: 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""" """Format notification for periodic checks"""
from datetime import datetime from datetime import datetime
# Format period text # Format period text
if period_minutes == 1: if period_minutes == 1:
period_text = "за 1 минуту" period_text = t('period_1_minute', lang)
elif period_minutes in [2, 3, 4]: elif period_minutes in [2, 3, 4]:
period_text = f"за {period_minutes} минуты" period_text = t('period_2_3_4_minutes', lang, period=period_minutes)
else: else:
period_text = f"за {period_minutes} минут" period_text = t('period_minutes_text', lang, period=period_minutes)
result = f"📊 Статистика {username}{period_text}\n\n" result = t('period_notification_title', lang, username=username, period_text=period_text)
# Format puzzles first (if available and there's actual activity) # Format puzzles first (if available and there's actual activity)
if puzzles_data and puzzles_data.get('data'): if puzzles_data and puzzles_data.get('data'):
@ -122,7 +131,7 @@ class StatsFormatter:
# Only show tasks section if there's actual activity (not all zeros) # Only show tasks section if there's actual activity (not all zeros)
if total_puzzles > 0 or solved > 0 or failed > 0: if total_puzzles > 0 or solved > 0 or failed > 0:
result += f"🧩 Задачи: {total_puzzles} (✅ {solved} - ❌ {failed})\n\n" result += t('period_puzzles_section', lang, total=total_puzzles, solved=solved, failed=failed)
# Format games # Format games
if games_data and games_data.get('data'): if games_data and games_data.get('data'):
@ -142,14 +151,21 @@ class StatsFormatter:
draws = game_data.get('draws', 0) draws = game_data.get('draws', 0)
rating_change_str = StatsFormatter._format_rating_change(rating_change) rating_change_str = StatsFormatter._format_rating_change(rating_change)
result += f"{emoji} {game_type.title()}{games_count} игр • {rating_change_str}\n" game_type_name = game_type.title()
result += f"Рейтинг: {rating}\n"
result += f"✅ Победы: {wins}\n" result += t('period_games_section', lang,
result += f"❌ Поражения: {losses}\n" emoji=emoji,
result += f"🤝 Ничьи: {draws}\n\n" 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 # If no activity
if not (games_data and games_data.get('data')) and not (puzzles_data and puzzles_data.get('data')): if not (games_data and games_data.get('data')) and not (puzzles_data and puzzles_data.get('data')):
result += "📭 Нет активности за этот период" result += t('no_activity', lang)
return result.rstrip() return result.rstrip()

136
LichessClientTG_bot/i18n.py Normal file
View file

@ -0,0 +1,136 @@
"""
Internationalization module for the Lichess Telegram Bot
English language only
"""
from typing import Optional
# Translation dictionary (English only)
TRANSLATIONS = {
'en': {
# Start command
'start_message': (
"The bot tracks one or more players on Lichess.\n"
"It shows ratings, daily, yesterday and weekly activity.\n"
"When specifying a user_token (with puzzle read permission), you can also get puzzle data.\n"
"The bot supports automatic checks at specified intervals and sends a report if there was activity during that time.\n\n"
"Example for a week:\n"
"🧩 Puzzles: 114 (✅ 81 - ❌ 33)\n\n"
"🔥 Blitz — 5 games • 🔴 -10\n"
"Rating: 2245\n"
"✅ Wins: 1\n"
"❌ Losses: 3\n"
"🤝 Draws: 1\n\n"
"🐇 Rapid — 19 games • 🟢 +20\n"
"Rating: 2248\n"
"✅ Wins: 8\n"
"❌ Losses: 4\n"
"🤝 Draws: 7\n\n"
"📋 Available commands:\n"
"/addgamer - Add a Lichess player to track (username only)\n"
"/addtoken - Add a player with a token to get puzzle data\n"
"/getgamers - Select active player (for which /today and other commands will work)\n"
"/delgamer - Remove a player from the tracked list\n"
"/today - Statistics for today\n"
"/yesterday - Statistics for yesterday\n"
"/week - Statistics for the week\n"
"/setperiod - Set up periodic notifications for the active player\n"
"(active player changes in the /getgamers menu)"
),
# Add gamer commands
'addgamer_prompt': "👤 Enter the Lichess username of the player to track:",
'addtoken_prompt': (
"🔑 Enter the Lichess API token to get puzzle data.\n"
"The token is created in profile settings — give it only puzzle:read permission.\n"
"After that, the player corresponding to this token will be added."
),
'token_added': "✅ Token added for player {username}!",
'gamer_added_with_token': "✅ Player {username} added with token!",
'gamer_added': "✅ Player {username} successfully added!",
'invalid_token': "❌ Invalid token. Please try again.",
'token_username_error': "❌ Failed to get username from token. Please try again.",
'empty_username': "❌ Username cannot be empty. Please try again.",
'user_not_found': "❌ Player {username} not found on Lichess. Check the spelling of the name.",
# Get gamers
'no_gamers': "📭 No players in database. Use /addgamer to add.",
'loading_ratings': "🔄 Loading player ratings...",
'select_active_gamer': "👥 <b>Select active player:</b>\n\n",
'active_gamer_set': "✅ Active player: {username}",
'gamer_not_found': "❌ Player not found",
# Delete gamer
'no_gamers_to_delete': "📭 You have no tracked players.",
'loading_gamers': "🔄 Loading player list...",
'select_gamer_to_delete': "🗑️ <b>Select player to delete:</b>\n\n",
'gamer_deleted': "✅ Player <b>{username}</b> removed from tracked list.",
'delete_failed': "❌ Failed to delete player",
# Stats commands
'no_active_gamer': "❌ No active player. Use /getgamers to select.",
'unknown_period': "❌ Unknown period",
'no_data': "📭 No data",
'stats_title': "📊 Statistics {username}{date_range}\n\n",
'puzzles_section': "🧩 Puzzles: {total} (✅ {solved} - ❌ {unsolved})\n\n",
'games_section': "{emoji} {game_type}{games_count} games • {rating_change}\nRating: {rating}\n✅ Wins: {wins}\n❌ Losses: {losses}\n🤝 Draws: {draws}\n\n",
# Set period
'select_period': "⏱️ Select period for player {username}:\n📱 Notifications will be sent to personal messages",
'notifications_disabled': "✅ Notifications disabled for {username}",
'period_set': "✅ Period {period} minutes set for {username}\n📱 Notifications will be sent to personal messages",
'disable_notifications': "❌ Disable notifications",
'period_minutes': "{period} minutes",
# Period notification
'period_1_minute': "for 1 minute",
'period_2_3_4_minutes': "for {period} minutes",
'period_minutes_text': "for {period} minutes",
'period_notification_title': "📊 Statistics {username}{period_text}\n\n",
'period_puzzles_section': "🧩 Puzzles: {total} (✅ {solved} - ❌ {failed})\n\n",
'period_games_section': "{emoji} {game_type}{games_count} games • {rating_change}\nRating: {rating}\n✅ Wins: {wins}\n❌ Losses: {losses}\n🤝 Draws: {draws}\n\n",
'no_activity': "📭 No activity for this period",
# Common
'period_minutes_suffix': "m",
}
}
def get_user_language(update) -> str:
"""
Always return English language.
"""
return 'en'
def t(key: str, lang: str = 'en', **kwargs) -> str:
"""
Translate a key (always English).
Args:
key: Translation key
lang: Language code (ignored, always uses English)
**kwargs: Format arguments for the translation string
Returns:
Translated string with formatted arguments
"""
translation = TRANSLATIONS['en'].get(key, key)
# Format the string if kwargs provided
if kwargs:
try:
return translation.format(**kwargs)
except KeyError:
# If formatting fails, return the translation as-is
return translation
return translation
def get_lang_from_update(update) -> str:
"""
Always return English.
"""
return 'en'