EN language only
This commit is contained in:
parent
3ec1fe614d
commit
3362bf89e2
4 changed files with 353 additions and 94 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import sqlite3
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
|
@ -17,6 +18,7 @@ from config import (
|
|||
from database import Database
|
||||
from lichess_api import LichessAPI
|
||||
from formatters import StatsFormatter
|
||||
from i18n import t
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
|
|
@ -35,6 +37,20 @@ class LichessBot:
|
|||
self.periodic_tasks = {} # Store periodic tasks
|
||||
self.period_start_times = {} # Store start times for each gamer
|
||||
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):
|
||||
"""Start periodic tasks for all user-gamer pairs that have periods set"""
|
||||
|
|
@ -56,41 +72,17 @@ class LichessBot:
|
|||
"""Start command handler"""
|
||||
# Register user in database
|
||||
user = update.effective_user
|
||||
lang_code = user.language_code if user else None
|
||||
self.db.add_or_get_telegram_user(
|
||||
user_id=user.id,
|
||||
username=user.username,
|
||||
first_name=user.first_name,
|
||||
last_name=user.last_name
|
||||
last_name=user.last_name,
|
||||
language_code=lang_code
|
||||
)
|
||||
|
||||
await update.message.reply_text(
|
||||
"Бот отслеживает игру одного или нескольких игроков на Lichess.\n"
|
||||
"Он показывает рейтинги, дневную, вчерашнюю и недельную активность.\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)"
|
||||
)
|
||||
lang = self.get_user_language_from_update(update)
|
||||
await update.message.reply_text(t('start_message', lang))
|
||||
|
||||
async def start_and_addgamer(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Start command that automatically launches addgamer"""
|
||||
|
|
@ -101,16 +93,14 @@ class LichessBot:
|
|||
|
||||
async def addgamer_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""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
|
||||
|
||||
async def addtoken_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Start addtoken command - token required"""
|
||||
await update.message.reply_text(
|
||||
"🔑 Введите Lichess API токен, чтобы получать данные по задачам.\n"
|
||||
"Токен создаётся в настройках профиля — дайте ему только право puzzle:read.\n"
|
||||
"После этого будет добавлен игрок, соответствующий этому токену."
|
||||
)
|
||||
lang = self.get_user_language_from_update(update)
|
||||
await update.message.reply_text(t('addtoken_prompt', lang))
|
||||
return WAITING_FOR_TOKEN
|
||||
|
||||
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)
|
||||
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:
|
||||
# Update token for existing gamer
|
||||
self.db.add_user_gamer(user_id, existing_gamer['id'], token)
|
||||
await update.message.reply_text(
|
||||
f"✅ Токен добавлен для игрока {username}!"
|
||||
t('token_added', lang, username=username)
|
||||
)
|
||||
else:
|
||||
# Add new gamer and link with token
|
||||
|
|
@ -147,17 +138,19 @@ class LichessBot:
|
|||
self.db.set_user_active_gamer(user_id, gamer_id)
|
||||
|
||||
await update.message.reply_text(
|
||||
f"✅ Игрок {username} добавлен с токеном!"
|
||||
t('gamer_added_with_token', lang, username=username)
|
||||
)
|
||||
return ConversationHandler.END
|
||||
else:
|
||||
lang = self.get_user_language_from_update(update)
|
||||
await update.message.reply_text(
|
||||
"❌ Не удалось получить username из токена. Попробуйте еще раз."
|
||||
t('token_username_error', lang)
|
||||
)
|
||||
return WAITING_FOR_TOKEN
|
||||
else:
|
||||
lang = self.get_user_language_from_update(update)
|
||||
await update.message.reply_text(
|
||||
"❌ Неверный токен. Попробуйте еще раз."
|
||||
t('invalid_token', lang)
|
||||
)
|
||||
return WAITING_FOR_TOKEN
|
||||
|
||||
|
|
@ -166,9 +159,10 @@ class LichessBot:
|
|||
username = update.message.text.strip()
|
||||
user_id = update.effective_user.id
|
||||
|
||||
lang = self.get_user_language_from_update(update)
|
||||
if not username:
|
||||
await update.message.reply_text(
|
||||
"❌ Username не может быть пустым. Попробуйте еще раз."
|
||||
t('empty_username', lang)
|
||||
)
|
||||
return WAITING_FOR_USERNAME
|
||||
|
||||
|
|
@ -176,7 +170,7 @@ class LichessBot:
|
|||
user_exists = await self.lichess_api.check_user_exists(username)
|
||||
if not user_exists:
|
||||
await update.message.reply_text(
|
||||
f"❌ Игрок {username} не найден на Lichess. Проверьте правильность написания имени."
|
||||
t('user_not_found', lang, username=username)
|
||||
)
|
||||
return WAITING_FOR_USERNAME
|
||||
|
||||
|
|
@ -193,8 +187,9 @@ class LichessBot:
|
|||
if len(user_gamers) == 1:
|
||||
self.db.set_user_active_gamer(user_id, gamer_id)
|
||||
|
||||
lang = self.get_user_language_from_update(update)
|
||||
await update.message.reply_text(
|
||||
f"✅ Игрок {username} успешно добавлен!"
|
||||
t('gamer_added', lang, username=username)
|
||||
)
|
||||
|
||||
return ConversationHandler.END
|
||||
|
|
@ -206,12 +201,13 @@ class LichessBot:
|
|||
|
||||
logger.info(f"getgamers: user_id={user_id}, found {len(gamers)} gamers")
|
||||
|
||||
lang = self.get_user_language_from_update(update)
|
||||
if not gamers:
|
||||
await update.message.reply_text("📭 В базе нет игроков. Используйте /adduser для добавления.")
|
||||
await update.message.reply_text(t('no_gamers', lang))
|
||||
return
|
||||
|
||||
# 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
|
||||
gamers_data = []
|
||||
|
|
@ -240,7 +236,8 @@ class LichessBot:
|
|||
|
||||
# Add period information if period > 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({
|
||||
'id': gamer['id'],
|
||||
|
|
@ -257,6 +254,9 @@ class LichessBot:
|
|||
import traceback
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
# 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({
|
||||
'id': gamer['id'],
|
||||
'status': "🟢" if gamer['is_active'] else "⚪",
|
||||
|
|
@ -264,7 +264,7 @@ class LichessBot:
|
|||
'bullet': 'N/A',
|
||||
'blitz': '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")
|
||||
|
||||
|
|
@ -277,7 +277,7 @@ class LichessBot:
|
|||
)
|
||||
|
||||
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")
|
||||
|
||||
|
|
@ -325,26 +325,37 @@ class LichessBot:
|
|||
gamers = self.db.get_user_gamers(user_id)
|
||||
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:
|
||||
# Set active gamer for this user
|
||||
self.db.set_user_active_gamer(user_id, gamer_id)
|
||||
await query.edit_message_text(
|
||||
f"✅ Активный игрок: {selected_gamer['username']}"
|
||||
t('active_gamer_set', lang, username=selected_gamer['username'])
|
||||
)
|
||||
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):
|
||||
"""Show gamers list for deletion"""
|
||||
user_id = update.effective_user.id
|
||||
gamers = self.db.get_user_gamers(user_id)
|
||||
|
||||
lang = self.get_user_language_from_update(update)
|
||||
if not gamers:
|
||||
await update.message.reply_text("📭 У вас нет отслеживаемых игроков.")
|
||||
await update.message.reply_text(t('no_gamers_to_delete', lang))
|
||||
return
|
||||
|
||||
# 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
|
||||
text_lines = []
|
||||
|
|
@ -370,7 +381,8 @@ class LichessBot:
|
|||
bullet_rating = blitz_rating = rapid_rating = 'N/A'
|
||||
|
||||
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(
|
||||
f"{status} <b>{username}</b> "
|
||||
|
|
@ -383,7 +395,7 @@ class LichessBot:
|
|||
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)
|
||||
|
||||
|
|
@ -418,19 +430,29 @@ class LichessBot:
|
|||
gamers = self.db.get_user_gamers(user_id)
|
||||
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:
|
||||
username = gamer_to_delete['username']
|
||||
deleted = self.db.remove_user_gamer(user_id, gamer_id)
|
||||
|
||||
if deleted:
|
||||
await query.edit_message_text(
|
||||
f"✅ Игрок <b>{username}</b> удален из списка отслеживаемых.",
|
||||
t('gamer_deleted', lang, username=username),
|
||||
parse_mode='HTML'
|
||||
)
|
||||
else:
|
||||
await query.edit_message_text("❌ Не удалось удалить игрока")
|
||||
await query.edit_message_text(t('delete_failed', lang))
|
||||
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):
|
||||
"""Get statistics for a period"""
|
||||
|
|
@ -439,9 +461,10 @@ class LichessBot:
|
|||
# Get active gamer for this user
|
||||
active_gamer = self.db.get_user_active_gamer(user_id)
|
||||
|
||||
lang = self.get_user_language_from_update(update)
|
||||
if not active_gamer:
|
||||
await update.message.reply_text(
|
||||
"❌ Нет активного игрока. Используйте /getgamers для выбора."
|
||||
t('no_active_gamer', lang)
|
||||
)
|
||||
return
|
||||
|
||||
|
|
@ -455,11 +478,11 @@ class LichessBot:
|
|||
elif period == "week":
|
||||
data = await self.lichess_api.get_week_stats(username)
|
||||
else:
|
||||
await update.message.reply_text("❌ Неизвестный период")
|
||||
await update.message.reply_text(t('unknown_period', lang))
|
||||
return
|
||||
|
||||
# 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)
|
||||
|
||||
async def today(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
|
|
@ -481,24 +504,24 @@ class LichessBot:
|
|||
# Get active gamer for this user
|
||||
active_gamer = self.db.get_user_active_gamer(user_id)
|
||||
|
||||
lang = self.get_user_language_from_update(update)
|
||||
if not active_gamer:
|
||||
await update.message.reply_text(
|
||||
"❌ Нет активного игрока. Используйте /getgamers для выбора."
|
||||
t('no_active_gamer', lang)
|
||||
)
|
||||
return
|
||||
|
||||
keyboard = []
|
||||
for period in PERIOD_OPTIONS:
|
||||
if period == 0:
|
||||
button_text = "❌ Отключить уведомления"
|
||||
button_text = t('disable_notifications', lang)
|
||||
else:
|
||||
button_text = f"⏰ {period} минут"
|
||||
button_text = t('period_minutes', lang, period=period)
|
||||
keyboard.append([InlineKeyboardButton(button_text, callback_data=f"period_{period}")])
|
||||
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
await update.message.reply_text(
|
||||
f"⏱️ Выберите период для игрока {active_gamer['username']}:\n"
|
||||
f"📱 Уведомления будут приходить в личные сообщения",
|
||||
t('select_period', lang, username=active_gamer['username']),
|
||||
reply_markup=reply_markup
|
||||
)
|
||||
|
||||
|
|
@ -513,23 +536,83 @@ class LichessBot:
|
|||
# Get active gamer for this user
|
||||
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:
|
||||
# Set period for this user-gamer pair
|
||||
self.db.set_user_gamer_period(user_id, active_gamer['id'], period)
|
||||
|
||||
if period == 0:
|
||||
await query.edit_message_text(
|
||||
f"✅ Уведомления для {active_gamer['username']} отключены"
|
||||
t('notifications_disabled', lang, username=active_gamer['username'])
|
||||
)
|
||||
else:
|
||||
await query.edit_message_text(
|
||||
f"✅ Период {period} минут установлен для {active_gamer['username']}\n"
|
||||
f"📱 Уведомления будут приходить в личные сообщения"
|
||||
t('period_set', lang, period=period, username=active_gamer['username'])
|
||||
)
|
||||
|
||||
# Start periodic task for this gamer (send to user's personal messages)
|
||||
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):
|
||||
"""Start periodic task for a gamer"""
|
||||
task_key = f"{gamer['id']}_{user_id}"
|
||||
|
|
@ -622,8 +705,10 @@ class LichessBot:
|
|||
# Отправляем уведомление только если есть реальная активность
|
||||
if has_games or has_puzzles:
|
||||
try:
|
||||
# Get user language from database
|
||||
user_lang = self.db.get_user_language(user_id)
|
||||
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:
|
||||
|
|
@ -690,6 +775,8 @@ class LichessBot:
|
|||
application.add_handler(CommandHandler("yesterday", self.yesterday))
|
||||
application.add_handler(CommandHandler("week", self.week))
|
||||
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
|
||||
application.add_handler(CallbackQueryHandler(self.select_gamer, pattern="^select_"))
|
||||
|
|
|
|||
|
|
@ -22,10 +22,18 @@ class Database:
|
|||
username TEXT,
|
||||
first_name TEXT,
|
||||
last_name TEXT,
|
||||
language_code TEXT,
|
||||
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)
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS gamers (
|
||||
|
|
@ -104,7 +112,8 @@ class Database:
|
|||
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,
|
||||
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"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
|
|
@ -114,13 +123,24 @@ class Database:
|
|||
|
||||
if not existing:
|
||||
cursor.execute(
|
||||
"INSERT INTO telegram_users (user_id, username, first_name, last_name) VALUES (?, ?, ?, ?)",
|
||||
(user_id, username, first_name, last_name)
|
||||
"INSERT INTO telegram_users (user_id, username, first_name, last_name, language_code) VALUES (?, ?, ?, ?, ?)",
|
||||
(user_id, username, first_name, last_name, language_code)
|
||||
)
|
||||
conn.commit()
|
||||
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
|
||||
|
||||
def get_user_language(self, user_id: int) -> str:
|
||||
"""Always return English language"""
|
||||
return 'en'
|
||||
|
||||
def add_gamer(self, username: str) -> int:
|
||||
"""Add a new gamer to the database (return gamer_id)"""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
from i18n import t
|
||||
|
||||
class StatsFormatter:
|
||||
@staticmethod
|
||||
|
|
@ -13,10 +14,10 @@ class StatsFormatter:
|
|||
return f"0"
|
||||
|
||||
@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"""
|
||||
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}"
|
||||
|
||||
# Extract data from API response
|
||||
|
|
@ -25,7 +26,7 @@ class StatsFormatter:
|
|||
games = api_data.get('games', {})
|
||||
|
||||
# Format date range
|
||||
date_range = StatsFormatter._get_date_range(period)
|
||||
date_range = StatsFormatter._get_date_range(period, lang)
|
||||
|
||||
# Format tasks section
|
||||
task_text = ""
|
||||
|
|
@ -33,7 +34,7 @@ class StatsFormatter:
|
|||
total_tasks = tasks.get('total', 0)
|
||||
solved = tasks.get('solved', 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
|
||||
games_text = ""
|
||||
|
|
@ -55,21 +56,29 @@ class StatsFormatter:
|
|||
# Format rating change
|
||||
rating_change_str = StatsFormatter._format_rating_change(rating_change)
|
||||
|
||||
games_text += f"{emoji} {game_type.title()} — {games_count} игр • {rating_change_str}\n"
|
||||
games_text += f"Рейтинг: {rating}\n"
|
||||
games_text += f"✅ Победы: {wins}\n"
|
||||
games_text += f"❌ Поражения: {losses}\n"
|
||||
games_text += f"🤝 Ничьи: {draws}\n\n"
|
||||
# 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 = f"📊 Статистика {username} • {date_range}\n\n"
|
||||
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) -> str:
|
||||
def _get_date_range(period: str, lang: str = 'en') -> str:
|
||||
"""Get date range string for the period"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
|
@ -99,19 +108,19 @@ class StatsFormatter:
|
|||
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) -> 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"""
|
||||
from datetime import datetime
|
||||
|
||||
# Format period text
|
||||
if period_minutes == 1:
|
||||
period_text = "за 1 минуту"
|
||||
period_text = t('period_1_minute', lang)
|
||||
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:
|
||||
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)
|
||||
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)
|
||||
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
|
||||
if games_data and games_data.get('data'):
|
||||
|
|
@ -142,14 +151,21 @@ class StatsFormatter:
|
|||
draws = game_data.get('draws', 0)
|
||||
|
||||
rating_change_str = StatsFormatter._format_rating_change(rating_change)
|
||||
result += f"{emoji} {game_type.title()} — {games_count} игр • {rating_change_str}\n"
|
||||
result += f"Рейтинг: {rating}\n"
|
||||
result += f"✅ Победы: {wins}\n"
|
||||
result += f"❌ Поражения: {losses}\n"
|
||||
result += f"🤝 Ничьи: {draws}\n\n"
|
||||
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
|
||||
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()
|
||||
|
|
|
|||
136
LichessClientTG_bot/i18n.py
Normal file
136
LichessClientTG_bot/i18n.py
Normal 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'
|
||||
Loading…
Add table
Add a link
Reference in a new issue