diff --git a/LichessClientTG_bot/bot.py b/LichessClientTG_bot/bot.py index f8d1884..ed8054a 100644 --- a/LichessClientTG_bot/bot.py +++ b/LichessClientTG_bot/bot.py @@ -133,17 +133,19 @@ class LichessBot: logger.error(f"test_admin_notify failed: {e}") await update.message.reply_text(f"❌ Failed to trigger admin notification: {e}") def get_user_language_from_update(self, update: Update) -> str: - """Always return English language""" - # Update user info in database + """Get user's selected bot language from database""" user = update.effective_user if user: + # Update user info in database (this will auto-detect language for new users) 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 + language_code=user.language_code ) + # Get user's selected bot language from database + return self.db.get_user_language(user.id) return 'en' async def start_existing_periodic_tasks(self): @@ -1121,58 +1123,61 @@ class LichessBot: # Start periodic task for this gamer (send to user's personal messages) await self.start_periodic_task(selected_gamer, user_id, period) - async def check_language(self, update: Update, context: ContextTypes.DEFAULT_TYPE): - """Check and display current language settings""" + async def set_lang(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """Show language selection menu""" 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) - self.counters.increment('lang') - else: + if not user: await update.message.reply_text("❌ Failed to get user information") + return + + # Get current language to show menu in user's language + lang = self.get_user_language_from_update(update) + + # Create keyboard with language buttons + keyboard = [ + [ + InlineKeyboardButton("🇬🇧 English", callback_data="lang_en"), + InlineKeyboardButton("🇷🇺 Русский", callback_data="lang_ru") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + t('select_language', lang), + reply_markup=reply_markup, + parse_mode='HTML' + ) - 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." - ) - self.counters.increment('resetlang') + async def handle_language_selection(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """Handle language selection callback""" + query = update.callback_query + await query.answer() + + user = query.from_user + user_id = user.id + selected_lang = query.data.split('_')[1] # 'en' or 'ru' + + # Update user info in database + 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=user.language_code + ) + + # Save selected language to database + success = self.db.set_user_language(user_id, selected_lang) + + if success: + # Get message in selected language + message = t('language_set_ru', selected_lang) if selected_lang == 'ru' else t('language_set_en', selected_lang) + await query.edit_message_text(message) else: - await update.message.reply_text("❌ Failed to get user information") + # Use current language for error message + lang = self.db.get_user_language(user_id) + error_msg = "❌ Не удалось установить язык" if lang == 'ru' else "❌ Failed to set language" + await query.edit_message_text(error_msg) async def start_periodic_task(self, gamer: Dict[str, Any], user_id: int, period_minutes: int): """Start periodic task for a gamer""" @@ -1408,11 +1413,11 @@ class LichessBot: application.add_handler(CommandHandler("lastYear_or_1000games", self.last_year_or_1000games)) application.add_handler(CommandHandler("support", self.support)) application.add_handler(CommandHandler("setperiod", self.setperiod)) - application.add_handler(CommandHandler("lang", self.check_language)) - application.add_handler(CommandHandler("resetlang", self.reset_language)) + application.add_handler(CommandHandler("set_lang", self.set_lang)) application.add_handler(CommandHandler("test_admin_notify", self.test_admin_notify)) # Callback handlers (order matters - more specific patterns first) + application.add_handler(CallbackQueryHandler(self.handle_language_selection, pattern="^lang_")) application.add_handler(CallbackQueryHandler(self.select_gamer_for_period, pattern="^select_gamer_period_")) application.add_handler(CallbackQueryHandler(self.select_gamer, pattern="^select_")) application.add_handler(CallbackQueryHandler(self.handle_delete_gamer, pattern="^delete_")) diff --git a/LichessClientTG_bot/config.py b/LichessClientTG_bot/config.py index 45678e4..ae4f780 100644 --- a/LichessClientTG_bot/config.py +++ b/LichessClientTG_bot/config.py @@ -49,7 +49,7 @@ DATABASE_PATH = _resolve_database_path() PERIOD_OPTIONS = [0, 15, 30, 60, 120, 180, 360, 720, 1440] # minutes (0=disable, then: 15min, 30min, 1h, 2h, 3h, 6h, 12h, 24h) # Bot Version -BOT_VERSION = "1.2.0" +BOT_VERSION = "1.3.0" # Telegram Bot Long Polling Configuration POLL_INTERVAL = 1.0 # seconds diff --git a/LichessClientTG_bot/database.py b/LichessClientTG_bot/database.py index 9317b2a..34f2ef5 100644 --- a/LichessClientTG_bot/database.py +++ b/LichessClientTG_bot/database.py @@ -28,13 +28,23 @@ class Database: ) ''') - # Add language_code column if it doesn't exist + # Add language_code column if it doesn't exist (Telegram language) try: cursor.execute("ALTER TABLE telegram_users ADD COLUMN language_code TEXT") except sqlite3.OperationalError: # Column already exists pass + # Add bot_language column if it doesn't exist (user-selected bot language) + try: + cursor.execute("ALTER TABLE telegram_users ADD COLUMN bot_language TEXT DEFAULT 'en'") + # Set default value for existing users + cursor.execute("UPDATE telegram_users SET bot_language = 'en' WHERE bot_language IS NULL") + conn.commit() + except sqlite3.OperationalError: + # Column already exists + pass + # Create gamers table (Lichess players only) cursor.execute(''' CREATE TABLE IF NOT EXISTS gamers ( @@ -144,22 +154,30 @@ class Database: def add_or_get_telegram_user(self, user_id: int, username: Optional[str] = None, 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. + For new users, automatically sets bot_language based on Telegram language_code: + - 'ru' -> 'ru' (Russian) + - anything else -> 'en' (English) + """ with sqlite3.connect(self.db_path) as conn: cursor = conn.cursor() - cursor.execute("SELECT user_id FROM telegram_users WHERE user_id = ?", (user_id,)) + cursor.execute("SELECT user_id, bot_language FROM telegram_users WHERE user_id = ?", (user_id,)) existing = cursor.fetchone() if not existing: + # New user - determine bot language from Telegram language_code + bot_language = 'ru' if language_code and language_code.lower().startswith('ru') else 'en' cursor.execute( - "INSERT INTO telegram_users (user_id, username, first_name, last_name, language_code) VALUES (?, ?, ?, ?, ?)", - (user_id, username, first_name, last_name, language_code) + "INSERT INTO telegram_users (user_id, username, first_name, last_name, language_code, bot_language) VALUES (?, ?, ?, ?, ?, ?)", + (user_id, username, first_name, last_name, language_code, bot_language) ) conn.commit() + logger.info(f"New user {user_id} added with bot_language={bot_language} (from Telegram language_code={language_code})") return True else: - # Update language_code if provided (always update, even if None to clear old value) + # Existing user - update language_code but keep bot_language unchanged cursor.execute( "UPDATE telegram_users SET language_code = ? WHERE user_id = ?", (language_code, user_id) @@ -168,8 +186,28 @@ class Database: return False def get_user_language(self, user_id: int) -> str: - """Always return English language""" - return 'en' + """Get user's selected bot language from database""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT bot_language FROM telegram_users WHERE user_id = ?", (user_id,)) + result = cursor.fetchone() + if result and result[0]: + return result[0] + # Default to English if not set + return 'en' + + def set_user_language(self, user_id: int, language: str) -> bool: + """Set user's selected bot language""" + if language not in ['en', 'ru']: + return False + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + "UPDATE telegram_users SET bot_language = ? WHERE user_id = ?", + (language, user_id) + ) + conn.commit() + return cursor.rowcount > 0 def add_gamer(self, username: str) -> int: """Add a new gamer to the database (return gamer_id)""" diff --git a/LichessClientTG_bot/i18n.py b/LichessClientTG_bot/i18n.py index 960bd8f..c7f2988 100644 --- a/LichessClientTG_bot/i18n.py +++ b/LichessClientTG_bot/i18n.py @@ -1,11 +1,11 @@ """ Internationalization module for the Lichess Telegram Bot -English language only +Supports English and Russian languages """ from typing import Optional -# Translation dictionary (English only) +# Translation dictionary (English and Russian) TRANSLATIONS = { 'en': { # Start command @@ -37,6 +37,7 @@ TRANSLATIONS = { "/lastYear_or_1000games - Statistics for the last year or last 1000 rated games (whichever comes first)\n" "/setperiod - Set up periodic notifications for the active player\n" "(active player changes in the /getgamers menu)\n" + "/set_lang - Select bot language (English / Russian)\n" "/support - Contact the developer for support and feedback" ), @@ -117,30 +118,153 @@ TRANSLATIONS = { # Common 'period_minutes_suffix': "m", + + # Language selection + 'select_language': "🌐 Select language / Выберите язык:", + 'language_set_en': "✅ Language set to English", + 'language_set_ru': "✅ Язык установлен: Русский", + }, + 'ru': { + # Start command + 'start_message': ( + "Бот отслеживает одного или нескольких игроков на Lichess.\n" + "Показывает рейтинги, активность за сегодня, вчера и за неделю.\n" + "При указании user_token (с разрешением на чтение пазлов) можно также получать данные по пазлам.\n" + "Бот поддерживает автоматические проверки с заданными интервалами и отправляет отчет, если была активность за это время.\n\n" + "Пример за неделю:\n" + "🧩 Пазлы: 114 (✅ 81 - ❌ 33)\n\n" + "🔥 Блиц — 5 игр • 🔴 -10\n" + "Рейтинг: 2245\n" + "✅ Побед: 1\n" + "❌ Поражений: 3\n" + "🤝 Ничьих: 1\n\n" + "🐇 Рапид — 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" + "/lastYear_or_1000games - Статистика за последний год или последние 1000 рейтинговых игр (что наступит раньше)\n" + "/setperiod - Настроить периодические уведомления для активного игрока\n" + "(активный игрок меняется в меню /getgamers)\n" + "/set_lang - Выбрать язык бота\n" + "/support - Связаться с разработчиком для поддержки и обратной связи" + ), + + # Add gamer commands + 'addgamer_prompt': "👤 Введите username игрока Lichess для отслеживания:", + 'addtoken_prompt': ( + "🔑 Введите токен API Lichess для получения данных по пазлам.\n" + "Токен создается в настройках профиля — дайте ему только разрешение puzzle:read.\n" + "После этого будет добавлен игрок, соответствующий этому токену." + ), + 'token_added': "✅ Токен добавлен для игрока {username}!", + 'gamer_added_with_token': "✅ Игрок {username} добавлен с токеном!", + 'gamer_added': "✅ Игрок {username} успешно добавлен!\n\nДля добавления следующего игрока воспользуйтесь /addgamer", + 'invalid_token': "❌ Неверный токен. Попробуйте еще раз.", + 'token_username_error': "❌ Не удалось получить username из токена. Попробуйте еще раз.", + 'empty_username': "❌ Username не может быть пустым. Попробуйте еще раз.", + 'user_not_found': "❌ Игрок {username} не найден на Lichess. Проверьте правильность написания имени.", + + # Get gamers + 'no_gamers': "📭 Нет игроков в базе данных. Используйте /addgamer для добавления.", + 'loading_ratings': "🔄 Загрузка рейтингов игроков...", + 'select_active_gamer': "👥 Выберите активного игрока:\n\n", + 'active_gamer_set': "✅ Активный игрок: {username}", + 'gamer_not_found': "❌ Игрок не найден", + + # Delete gamer + 'no_gamers_to_delete': "📭 У вас нет отслеживаемых игроков.", + 'loading_gamers': "🔄 Загрузка списка игроков...", + 'select_gamer_to_delete': "🗑️ Выберите игрока для удаления:\n\n", + 'gamer_deleted': "✅ Игрок {username} удален из списка отслеживаемых.", + 'active_gamer_deleted': "✅ Игрок {username} удален из списка отслеживаемых.\n\n⚠️ Вы удалили активного игрока. Используйте команду /getgamers для выбора игрока, для которого будут работать команды /today, /yesterday, /week.", + 'last_gamer_deleted': "✅ Игрок {username} удален из списка отслеживаемых.\n\n⚠️ Вы удалили последнего игрока. Используйте команду /addgamer для добавления нового отслеживаемого игрока.", + 'delete_failed': "❌ Не удалось удалить игрока", + + # Stats commands + 'no_active_gamer': "❌ Нет активного игрока. Используйте /getgamers для выбора.", + 'unknown_period': "❌ Неизвестный период", + 'no_data': "📭 Нет данных", + 'stats_title': "📊 Статистика {username} • {date_range}\n\n", + 'puzzles_section': "🧩 Пазлы: {total} (✅ {solved} - ❌ {unsolved})\n\n", + 'games_section': "{emoji} {game_type} — {games_count} игр • {rating_change}\nРейтинг: {rating}\n✅ Побед: {wins}\n❌ Поражений: {losses}\n🤝 Ничьих: {draws}\n\n", + + # Set period + 'select_gamer_for_period': "⏱️ Выберите игрока для установки периода уведомлений:\n\n", + 'select_period': "⏱️ Выберите период для игрока {username}:\n📱 Уведомления будут отправляться в личные сообщения", + 'notifications_disabled': "✅ Уведомления отключены для {username}", + 'period_set': "✅ Период {period} минут установлен для {username}\n📱 Уведомления будут отправляться в личные сообщения", + 'disable_notifications': "❌ Отключить уведомления", + 'period_minutes': "⏰ {period} минут", + + # Period notification + 'period_1_minute': "за 1 минуту", + 'period_2_3_4_minutes': "за {period} минуты", + 'period_minutes_text': "за {period} минут", + 'period_notification_title': "📊 Статистика {username} • {period_text}\n\n", + 'period_puzzles_section': "🧩 Пазлы: {total} (✅ {solved} - ❌ {failed})\n\n", + 'period_games_section': "{emoji} {game_type} — {games_count} игр • {rating_change}\nРейтинг: {rating}\n✅ Побед: {wins}\n❌ Поражений: {losses}\n🤝 Ничьих: {draws}\n\n", + 'no_activity': "📭 Нет активности за этот период", + + # Last year or 1000 games + 'last_year_1000_processing': "⏳ Обработка запроса... Это может занять некоторое время, так как запросы очень медленные.", + 'last_year_1000_player_processing': "🔄 Запрос данных для игрока {username}...", + + # Stats commands (today/yesterday/week) + 'stats_processing': "⏳ Обработка запроса...", + 'stats_player_processing': "🔄 Запрос данных для игрока {username}...", + 'stats_all_done': "✅ Это всё", + + # Support + 'support_message': ( + "💬 Поддержка и обратная связь\n\n" + "Вы можете написать разработчику сервиса напрямую и сообщить о проблеме, предложить новые функции или задать вопрос.\n\n" + "Я буду рад рассмотреть каждое сообщение и ответить на него.\n\n" + "Разработчик принимает сообщения на английском и русском языках.\n\n" + "📧 Контакт: @vrubelr\n\n" + "📦 Версия бота: {version}" + ), + + # Common + 'period_minutes_suffix': "м", + + # Language selection + 'select_language': "🌐 Выберите язык / Select language:", + 'language_set_en': "✅ Language set to English", + 'language_set_ru': "✅ Язык установлен: Русский", } } -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). + Translate a key to the specified language. Args: key: Translation key - lang: Language code (ignored, always uses English) + lang: Language code ('en' or 'ru') **kwargs: Format arguments for the translation string Returns: Translated string with formatted arguments """ - translation = TRANSLATIONS['en'].get(key, key) + # Default to English if language not supported + if lang not in TRANSLATIONS: + lang = 'en' + + translation = TRANSLATIONS[lang].get(key) + + # Fallback to English if translation not found + if translation is None: + translation = TRANSLATIONS['en'].get(key, key) # Format the string if kwargs provided if kwargs: @@ -151,10 +275,3 @@ def t(key: str, lang: str = 'en', **kwargs) -> str: return translation return translation - - -def get_lang_from_update(update) -> str: - """ - Always return English. - """ - return 'en'