2025-10-26 20:23:26 +03:00
import asyncio
import logging
2025-11-12 23:20:01 +03:00
import sqlite3
2025-12-03 02:33:38 +03:00
import os
2025-10-26 20:23:26 +03:00
from datetime import datetime , timedelta
from typing import Dict , Any , Optional
2025-12-03 02:33:38 +03:00
from pathlib import Path
2025-10-26 20:23:26 +03:00
from telegram import Update , InlineKeyboardButton , InlineKeyboardMarkup
from telegram . ext import (
Application , CommandHandler , CallbackQueryHandler ,
MessageHandler , filters , ContextTypes , ConversationHandler
)
from config import (
TELEGRAM_BOT_TOKEN , PERIOD_OPTIONS , POLL_INTERVAL ,
POLL_TIMEOUT , DROP_PENDING_UPDATES , ALLOWED_UPDATES ,
2025-11-23 13:11:58 +03:00
LICHESS_STATS_API_BASE_URL , ADMINPANEL_TELEGRAM_BOT_TOKEN
2025-10-26 20:23:26 +03:00
)
2025-11-23 13:11:58 +03:00
from version import BOT_VERSION
2025-10-26 20:23:26 +03:00
from database import Database
from lichess_api import LichessAPI
from formatters import StatsFormatter
2025-11-12 23:20:01 +03:00
from i18n import t
2025-11-13 01:00:48 +03:00
from admin_bot import get_admin_bot , init_admin_bot
2025-11-13 13:32:46 +03:00
from message_counters import MessageCounters
2025-11-20 03:23:38 +03:00
from request_queue import get_request_queue
2025-11-16 12:48:23 +03:00
import time
2025-11-16 13:38:25 +03:00
import aiohttp
2025-10-26 20:23:26 +03:00
# Configure logging
logging . basicConfig (
format = ' %(asctime)s - %(name)s - %(levelname)s - %(message)s ' ,
2025-10-29 00:29:53 +03:00
level = logging . DEBUG
2025-10-26 20:23:26 +03:00
)
logger = logging . getLogger ( __name__ )
# Conversation states
WAITING_FOR_TOKEN , WAITING_FOR_USERNAME = range ( 2 )
class LichessBot :
def __init__ ( self ) :
self . db = Database ( )
self . lichess_api = LichessAPI ( )
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
2025-11-13 13:32:46 +03:00
self . counters = MessageCounters ( ) # Message counters
2025-11-20 03:23:38 +03:00
self . request_queue = get_request_queue ( ) # Request queue for rate limiting
2025-11-12 23:20:01 +03:00
2025-11-18 19:39:52 +03:00
async def _notify_admin_new_player ( self , player_username : str , added_by_user_id : int , added_by_username : Optional [ str ] , is_new_gamer : bool = False ) :
2025-11-16 13:38:25 +03:00
""" Notify admin about newly linked player (always try to send). """
try :
admin_bot = get_admin_bot ( )
if admin_bot :
logger . info ( " Sending admin notification via admin_bot instance " )
await admin_bot . notify_new_player (
player_username = player_username ,
added_by_user_id = added_by_user_id ,
2025-11-18 19:39:52 +03:00
added_by_username = added_by_username ,
is_new_gamer = is_new_gamer
2025-11-16 13:38:25 +03:00
)
return
except Exception as e :
logger . warning ( f " notify_new_player via admin_bot failed: { e } " )
# Fallback: direct API call using admin bot token and chat id from DB
try :
admin_chat_id = self . db . get_admin_chat_id ( )
if not admin_chat_id :
logger . warning ( " Admin chat id is not set; cannot send admin notification. " )
return
url = f " https://api.telegram.org/bot { ADMINPANEL_TELEGRAM_BOT_TOKEN } /sendMessage "
added_by_text = f " @ { added_by_username } " if added_by_username else f " ID: { added_by_user_id } "
lichess_url = f " https://lichess.org/@/ { player_username } "
message = (
f " 🎮 <b>Добавлен новый игрок для отслеживания</b> \n \n "
f " Игрок: <a href= \" { lichess_url } \" > { player_username } </a> \n "
f " Добавил: { added_by_text } "
)
logger . info ( f " Sending admin notification via direct API to chat_id= { admin_chat_id } " )
async with aiohttp . ClientSession ( ) as session :
async with session . post ( url , json = {
" chat_id " : admin_chat_id ,
" text " : message ,
" parse_mode " : " HTML "
} ) as response :
if response . status != 200 :
error_text = await response . text ( )
logger . error ( f " Failed to send admin notification (fallback): { response . status } - { error_text } " )
else :
logger . info ( " Admin notification sent successfully via direct API " )
except Exception as e :
logger . error ( f " Fallback admin notification failed: { e } " )
2025-11-18 14:03:06 +03:00
async def _notify_admin_new_user ( self , user_id : int , username : Optional [ str ] , first_name : Optional [ str ] ) :
""" Notify admin about new Telegram user (fallback method). """
try :
admin_chat_id = self . db . get_admin_chat_id ( )
if not admin_chat_id :
logger . warning ( " Admin chat id is not set; cannot send admin notification. " )
return
url = f " https://api.telegram.org/bot { ADMINPANEL_TELEGRAM_BOT_TOKEN } /sendMessage "
username_text = f " @ { username } " if username else " без username "
name_text = first_name if first_name else " без имени "
message = (
f " 🆕 <b>Новый пользователь Telegram</b> \n \n "
f " ID: { user_id } \n "
f " Username: { username_text } \n "
f " Имя: { name_text } "
)
logger . info ( f " Sending admin notification via direct API to chat_id= { admin_chat_id } " )
import aiohttp
async with aiohttp . ClientSession ( ) as session :
async with session . post ( url , json = {
" chat_id " : admin_chat_id ,
" text " : message ,
" parse_mode " : " HTML "
} ) as response :
if response . status == 200 :
logger . info ( f " Admin notification sent successfully via direct API " )
else :
error_text = await response . text ( )
logger . error ( f " Failed to send admin notification: { response . status } - { error_text } " )
except Exception as e :
logger . error ( f " Failed to send admin notification via API: { e } " )
import traceback
logger . error ( traceback . format_exc ( ) )
2025-11-16 13:38:25 +03:00
async def test_admin_notify ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Manual test to verify admin notifications delivery path. """
user = update . effective_user
try :
await self . _notify_admin_new_player ( " test_player_notify " , user . id , user . username if user else None )
await update . message . reply_text ( " ✅ Admin notification test triggered. " )
except Exception as e :
logger . error ( f " test_admin_notify failed: { e } " )
await update . message . reply_text ( f " ❌ Failed to trigger admin notification: { e } " )
2025-11-12 23:20:01 +03:00
def get_user_language_from_update ( self , update : Update ) - > str :
2025-11-20 12:43:00 +03:00
""" Get user ' s selected bot language from database """
2025-11-12 23:20:01 +03:00
user = update . effective_user
if user :
2025-11-20 12:43:00 +03:00
# Update user info in database (this will auto-detect language for new users)
2025-11-12 23:20:01 +03:00
self . db . add_or_get_telegram_user (
user_id = user . id ,
username = user . username ,
first_name = user . first_name ,
last_name = user . last_name ,
2025-11-20 12:43:00 +03:00
language_code = user . language_code
2025-11-12 23:20:01 +03:00
)
2025-11-20 12:43:00 +03:00
# Get user's selected bot language from database
return self . db . get_user_language ( user . id )
2025-11-12 23:20:01 +03:00
return ' en '
2025-10-26 20:23:26 +03:00
async def start_existing_periodic_tasks ( self ) :
""" Start periodic tasks for all user-gamer pairs that have periods set """
try :
gamers_with_periods = self . db . get_all_gamers_with_periods ( )
2025-11-18 14:03:06 +03:00
# Get statistics
import sqlite3
with sqlite3 . connect ( self . db . db_path ) as conn :
cursor = conn . cursor ( )
cursor . execute ( " SELECT COUNT(*) FROM telegram_users " )
total_users = cursor . fetchone ( ) [ 0 ]
cursor . execute ( " SELECT COUNT(DISTINCT username) FROM gamers " )
total_gamers = cursor . fetchone ( ) [ 0 ]
logger . info ( f " 📊 Statistics: { total_users } users, { total_gamers } tracked gamers " )
logger . info ( f " 🔔 Found { len ( gamers_with_periods ) } user-gamer pairs with periodic notifications enabled " )
if len ( gamers_with_periods ) == 0 :
logger . warning ( " ⚠️ No periodic notifications configured! Users need to set periods using /setperiod " )
2025-10-26 20:23:26 +03:00
2025-11-20 03:23:38 +03:00
# Start request queue processor
self . request_queue . _start_processor ( )
logger . info ( " ✅ Request queue processor started " )
2025-10-26 20:23:26 +03:00
for gamer in gamers_with_periods :
if gamer [ ' period_minutes ' ] > 0 :
user_id = gamer [ ' user_id ' ]
2025-11-20 03:23:38 +03:00
username = gamer [ ' username ' ]
period = gamer [ ' period_minutes ' ]
2025-10-26 20:23:26 +03:00
# Start periodic task with user_id and gamer
2025-11-20 03:23:38 +03:00
await self . start_periodic_task ( gamer , user_id , period )
logger . info ( f " ✅ Started periodic task for { username } (user { user_id } ) with period { period } minutes " )
logger . info ( f " ✅ All periodic tasks started. Total: { len ( [ g for g in gamers_with_periods if g [ ' period_minutes ' ] > 0 ] ) } " )
2025-11-13 13:32:46 +03:00
# Start daily counter reset task
asyncio . create_task ( self . daily_counter_reset_task ( ) )
2025-11-18 14:03:06 +03:00
logger . info ( " ✅ Started daily counter reset task " )
2025-10-26 20:23:26 +03:00
except Exception as e :
2025-11-18 14:03:06 +03:00
logger . error ( f " ❌ Error starting existing periodic tasks: { e } " )
import traceback
logger . error ( traceback . format_exc ( ) )
2025-11-13 13:32:46 +03:00
async def daily_counter_reset_task ( self ) :
""" Background task to reset daily counters at midnight """
while True :
try :
# Calculate seconds until next midnight
now = datetime . now ( )
next_midnight = ( now + timedelta ( days = 1 ) ) . replace ( hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
seconds_until_midnight = ( next_midnight - now ) . total_seconds ( )
logger . info ( f " Daily counter reset task: waiting { seconds_until_midnight } seconds until next midnight " )
await asyncio . sleep ( seconds_until_midnight )
# Reset daily counters
self . counters . _reset_daily_counters_if_needed ( )
logger . info ( " Daily counters reset at midnight " )
except asyncio . CancelledError :
break
except Exception as e :
logger . error ( f " Error in daily counter reset task: { e } " )
import traceback
logger . error ( traceback . format_exc ( ) )
# Wait 1 hour before retrying
await asyncio . sleep ( 3600 )
2025-10-26 20:23:26 +03:00
async def start ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Start command handler """
2025-11-13 01:00:48 +03:00
logger . info ( f " 📝 start() method called for user { update . effective_user . id } " )
2025-10-26 20:23:26 +03:00
# Register user in database
user = update . effective_user
2025-11-12 23:20:01 +03:00
lang_code = user . language_code if user else None
2025-11-13 01:00:48 +03:00
logger . info ( f " User info: id= { user . id } , username= { user . username } , lang_code= { lang_code } " )
is_new_user = self . db . add_or_get_telegram_user (
2025-10-26 20:23:26 +03:00
user_id = user . id ,
username = user . username ,
first_name = user . first_name ,
2025-11-12 23:20:01 +03:00
last_name = user . last_name ,
language_code = lang_code
2025-10-26 20:23:26 +03:00
)
2025-11-13 01:00:48 +03:00
# Notify admin bot about new user
if is_new_user :
2025-11-18 14:03:06 +03:00
try :
admin_bot = get_admin_bot ( )
if admin_bot :
await admin_bot . notify_new_user (
user_id = user . id ,
username = user . username ,
first_name = user . first_name
)
else :
# Fallback: direct API call
await self . _notify_admin_new_user ( user . id , user . username , user . first_name )
except Exception as e :
logger . error ( f " Failed to notify admin about new user: { e } " )
import traceback
logger . error ( traceback . format_exc ( ) )
# Try fallback
try :
await self . _notify_admin_new_user ( user . id , user . username , user . first_name )
except Exception as e2 :
logger . error ( f " Fallback notification also failed: { e2 } " )
2025-10-26 20:23:26 +03:00
2025-11-12 23:20:01 +03:00
lang = self . get_user_language_from_update ( update )
2025-11-13 01:00:48 +03:00
start_msg = t ( ' start_message ' , lang )
await update . message . reply_text ( start_msg )
2025-11-13 13:32:46 +03:00
self . counters . increment ( ' start ' )
2025-10-26 20:23:26 +03:00
2025-12-01 13:52:26 +03:00
async def help_command ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Help command handler """
lang = self . get_user_language_from_update ( update )
help_msg = t ( ' help_message ' , lang )
await update . message . reply_text ( help_msg )
2025-10-29 11:32:45 +03:00
async def start_and_addgamer ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
2025-11-13 01:00:48 +03:00
""" Start command that shows welcome message and starts addgamer conversation """
try :
2025-11-28 18:11:13 +03:00
# Clear any existing conversation state
if context and hasattr ( context , " user_data " ) :
context . user_data . clear ( )
2025-11-13 01:00:48 +03:00
# Run the regular start command
await self . start ( update , context )
2025-11-28 18:11:13 +03:00
# Start addgamer conversation
await self . addgamer_start ( update , context )
2025-11-13 01:00:48 +03:00
except Exception as e :
logger . error ( f " Error in start_and_addgamer: { e } " )
import traceback
logger . error ( traceback . format_exc ( ) )
try :
await update . message . reply_text ( f " Error: { e } " )
except :
pass
2025-10-29 11:32:45 +03:00
2025-10-28 23:09:00 +03:00
async def addgamer_start ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
2025-12-03 02:33:38 +03:00
""" Start addgamer command - show menu with options """
2025-11-23 01:46:12 +03:00
user_id = update . effective_user . id
logger . info ( f " addgamer_start called for user { user_id } " )
2025-12-03 02:33:38 +03:00
lang = self . get_user_language_from_update ( update )
try :
keyboard = [
[
InlineKeyboardButton (
text = t ( ' addgamer_btn_add ' , lang ) ,
callback_data = " addgamer_add "
)
] ,
[
InlineKeyboardButton (
text = t ( ' addgamer_btn_how ' , lang ) ,
callback_data = " addgamer_how "
)
] ,
]
reply_markup = InlineKeyboardMarkup ( keyboard )
await update . message . reply_text (
t ( ' addgamer_menu ' , lang ) ,
reply_markup = reply_markup
)
logger . info ( f " Addgamer menu sent to user { user_id } " )
except Exception as e :
logger . error ( f " Error sending addgamer menu: { e } " )
import traceback
logger . error ( traceback . format_exc ( ) )
# No conversation state returned; handler-based flow
return
async def addgamer_show_prompt ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Callback: show username prompt after user presses ' Add player ' """
query = update . callback_query
await query . answer ( )
user_id = query . from_user . id
logger . info ( f " addgamer_show_prompt called for user { user_id } " )
# Clear previous state and mark that we're waiting for username
if context and hasattr ( context , " user_data " ) :
context . user_data . clear ( )
context . user_data [ ' awaiting_addgamer_username ' ] = True
# Language from DB (for callbacks)
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 )
await query . message . reply_text (
t ( ' addgamer_prompt ' , lang ) ,
parse_mode = ' HTML '
)
logger . info ( f " Addgamer prompt (from button) sent to user { user_id } " )
# Count real start of username input flow
self . counters . increment ( ' addgamer ' )
async def addgamer_show_help ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Callback: show how to find username on Lichess (with images) """
query = update . callback_query
if not query or not query . message :
logger . error ( " addgamer_show_help: Invalid query or message " )
return
await query . answer ( )
user_id = query . from_user . id
logger . info ( f " addgamer_show_help called for user { user_id } " )
# Language from DB (for callbacks)
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 )
# Определяем имя файла картинки в зависимости от языка
# Для русской локализации - helpRU.jpeg, для английской - helpEN.jpeg
image_filename = " helpRU.jpeg " if lang == ' ru ' else " helpEN.jpeg "
# Определяем путь к картинке (она находится в той же папке, что и bot.py)
bot_dir = Path ( __file__ ) . resolve ( ) . parent
image_path = bot_dir / image_filename
# Пробуем альтернативные пути, если основной не найден
possible_paths = [
image_path , # В папке бота
bot_dir . parent / image_filename , # В корне проекта
Path ( " /home/vrubel/PROJECTS/LichessStatTgWeb/LichessClientTG_bot " ) / image_filename , # Абсолютный путь
]
# Ищем существующий файл
found_path = None
for candidate in possible_paths :
if candidate and candidate . exists ( ) :
found_path = candidate
logger . info ( f " ✅ Found { image_filename } at: { found_path } " )
break
if not found_path or not found_path . exists ( ) :
error_msg = f " ❌ Error: Could not find { image_filename } \n \n Checked paths: \n "
for path in possible_paths :
exists = path . exists ( ) if path else False
error_msg + = f " • { path } (exists: { exists } ) \n "
logger . error ( error_msg )
await query . message . reply_text ( f " ❌ Error: Could not find image file. Please check bot logs. " )
2025-11-23 01:46:12 +03:00
return
2025-11-13 01:00:48 +03:00
try :
2025-12-03 02:33:38 +03:00
logger . info ( f " Sending help image: { found_path } (language: { lang } ) " )
with open ( found_path , " rb " ) as img :
await query . message . reply_photo ( photo = img )
logger . info ( f " Successfully sent help image to user { user_id } " )
# Устанавливаем флаг ожидания username, чтобы пользователь мог ввести е г о после просмотра картинки
2025-11-16 15:23:54 +03:00
if context and hasattr ( context , " user_data " ) :
context . user_data [ ' awaiting_addgamer_username ' ] = True
2025-12-03 02:33:38 +03:00
# Отправляем текст с запросом username
await query . message . reply_text (
t ( ' addgamer_prompt ' , lang ) ,
parse_mode = ' HTML '
)
# Считаем это началом процесса добавления игрока
2025-11-13 13:32:46 +03:00
self . counters . increment ( ' addgamer ' )
2025-11-13 01:00:48 +03:00
except Exception as e :
2025-12-03 02:33:38 +03:00
logger . error ( f " Error sending lichess help images: { e } " )
2025-11-13 01:00:48 +03:00
import traceback
logger . error ( traceback . format_exc ( ) )
2025-12-03 02:33:38 +03:00
try :
await query . message . reply_text ( f " ❌ Error sending images: { e } " )
except Exception as e2 :
logger . error ( f " Failed to send error message: { e2 } " )
2025-10-28 23:09:00 +03:00
async def addtoken_start ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Start addtoken command - token required """
2025-11-21 23:49:26 +03:00
# Reset any existing conversation state
if context and hasattr ( context , " user_data " ) :
context . user_data . clear ( )
2025-11-12 23:20:01 +03:00
lang = self . get_user_language_from_update ( update )
await update . message . reply_text ( t ( ' addtoken_prompt ' , lang ) )
2025-11-13 13:32:46 +03:00
self . counters . increment ( ' addtoken ' )
2025-10-26 20:23:26 +03:00
return WAITING_FOR_TOKEN
async def handle_token ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
2025-10-28 23:09:00 +03:00
""" Handle token input for /addtoken """
2025-10-26 20:23:26 +03:00
token = update . message . text . strip ( )
user_id = update . effective_user . id
2025-11-19 12:02:54 +03:00
logger . info ( f " Processing token for user { user_id } , token prefix: { token [ : 10 ] } ... " )
2025-10-28 23:09:00 +03:00
# Get username from token
profile = await self . lichess_api . get_user_profile ( token )
2025-11-19 12:02:54 +03:00
logger . info ( f " Profile response: { profile is not None } " )
2025-10-28 23:09:00 +03:00
if profile :
username = profile . get ( ' username ' )
if username :
# Check if this gamer is already tracked by this user
user_gamers = self . db . get_user_gamers ( user_id )
existing_gamer = next ( ( g for g in user_gamers if g [ ' username ' ] == username ) , None )
2025-11-12 23:20:01 +03:00
lang = self . get_user_language_from_update ( update )
2025-10-28 23:09:00 +03:00
if existing_gamer :
# Update token for existing gamer
self . db . add_user_gamer ( user_id , existing_gamer [ ' id ' ] , token )
await update . message . reply_text (
2025-11-12 23:20:01 +03:00
t ( ' token_added ' , lang , username = username )
2025-10-28 23:09:00 +03:00
)
2025-11-16 13:38:25 +03:00
# Always notify admin about link/added player
try :
user_obj = update . effective_user
await self . _notify_admin_new_player ( username , user_id , user_obj . username if user_obj else None )
except Exception as e :
logger . error ( f " Admin notify failed after token update: { e } " )
2025-10-28 23:09:00 +03:00
else :
# Add new gamer and link with token
2025-11-13 01:00:48 +03:00
# Check if gamer already exists
import sqlite3
with sqlite3 . connect ( self . db . db_path ) as conn :
cursor = conn . cursor ( )
cursor . execute ( " SELECT id FROM gamers WHERE username = ? " , ( username , ) )
existing_gamer = cursor . fetchone ( )
is_new_gamer = existing_gamer is None
2025-10-26 20:35:23 +03:00
gamer_id = self . db . add_gamer ( username )
self . db . add_user_gamer ( user_id , gamer_id , token )
2025-10-26 20:23:26 +03:00
2025-10-31 19:12:39 +03:00
# Set default period to 1 hour (60 minutes) for new gamer
self . db . set_user_gamer_period ( user_id , gamer_id , 60 )
2025-10-26 20:23:26 +03:00
# If this is the first gamer for this user, make it active
user_gamers = self . db . get_user_gamers ( user_id )
if len ( user_gamers ) == 1 :
self . db . set_user_active_gamer ( user_id , gamer_id )
2025-11-18 14:41:13 +03:00
# Start periodic task for this gamer (60 minutes period)
try :
gamer_data = {
' id ' : gamer_id ,
' username ' : username ,
' token ' : token ,
' period_minutes ' : 60
}
await self . start_periodic_task ( gamer_data , user_id , 60 )
logger . info ( f " Started periodic task for { username } (user { user_id } ) with period 60 minutes " )
except Exception as e :
logger . error ( f " Failed to start periodic task for { username } : { e } " )
import traceback
logger . error ( traceback . format_exc ( ) )
2025-11-16 13:38:25 +03:00
# Notify admin bot about new player (always notify on link)
try :
user_obj = update . effective_user
await self . _notify_admin_new_player (
2025-11-18 19:39:52 +03:00
username , user_id , user_obj . username if user_obj else None , is_new_gamer
2025-11-16 13:38:25 +03:00
)
except Exception as e :
logger . error ( f " Admin notify failed after adding gamer with token: { e } " )
2025-11-13 01:00:48 +03:00
2025-10-26 20:23:26 +03:00
await update . message . reply_text (
2025-11-12 23:20:01 +03:00
t ( ' gamer_added_with_token ' , lang , username = username )
2025-10-26 20:23:26 +03:00
)
2025-10-28 23:09:00 +03:00
return ConversationHandler . END
2025-10-26 20:23:26 +03:00
else :
2025-11-12 23:20:01 +03:00
lang = self . get_user_language_from_update ( update )
2025-10-26 20:23:26 +03:00
await update . message . reply_text (
2025-11-12 23:20:01 +03:00
t ( ' token_username_error ' , lang )
2025-10-26 20:23:26 +03:00
)
return WAITING_FOR_TOKEN
2025-10-28 23:09:00 +03:00
else :
2025-11-12 23:20:01 +03:00
lang = self . get_user_language_from_update ( update )
2025-10-28 23:09:00 +03:00
await update . message . reply_text (
2025-11-12 23:20:01 +03:00
t ( ' invalid_token ' , lang )
2025-10-28 23:09:00 +03:00
)
return WAITING_FOR_TOKEN
2025-10-26 20:23:26 +03:00
async def handle_username ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
2025-10-28 23:09:00 +03:00
""" Handle username input for /addgamer """
2025-11-16 15:23:54 +03:00
# Only handle if we are awaiting an addgamer username
if not ( context and hasattr ( context , " user_data " ) and context . user_data . get ( ' awaiting_addgamer_username ' ) ) :
return
2025-10-26 20:23:26 +03:00
username = update . message . text . strip ( )
user_id = update . effective_user . id
2025-11-12 23:20:01 +03:00
lang = self . get_user_language_from_update ( update )
2025-11-07 22:54:49 +03:00
if not username :
2025-10-26 20:23:26 +03:00
await update . message . reply_text (
2025-11-12 23:20:01 +03:00
t ( ' empty_username ' , lang )
2025-10-26 20:23:26 +03:00
)
2025-11-16 15:23:54 +03:00
return
2025-11-07 22:54:49 +03:00
# Check if user exists on Lichess
user_exists = await self . lichess_api . check_user_exists ( username )
if not user_exists :
2025-10-26 20:23:26 +03:00
await update . message . reply_text (
2025-12-03 13:09:40 +03:00
t ( ' user_not_found ' , lang , username = username ) + ' \n \n ' + t ( ' addgamer_prompt ' , lang ) ,
parse_mode = ' HTML '
2025-10-26 20:23:26 +03:00
)
return WAITING_FOR_USERNAME
2025-11-23 01:46:12 +03:00
# Check if this gamer is already tracked by this user
user_gamers = self . db . get_user_gamers ( user_id )
existing_gamer = next ( ( g for g in user_gamers if g [ ' username ' ] . lower ( ) == username . lower ( ) ) , None )
if existing_gamer :
# Player is already being tracked by this user
await update . message . reply_text (
2025-12-03 13:09:40 +03:00
t ( ' gamer_already_added ' , lang , username = username ) + ' \n \n ' + t ( ' addgamer_prompt ' , lang ) ,
parse_mode = ' HTML '
2025-11-23 01:46:12 +03:00
)
2025-12-03 13:09:40 +03:00
# Keep awaiting flag - don't clear it, so user can try again
2025-11-23 01:46:12 +03:00
return
2025-11-07 22:54:49 +03:00
# Add gamer to database (without token)
2025-11-23 01:46:12 +03:00
# Check if gamer already exists in global gamers table
2025-11-13 01:00:48 +03:00
import sqlite3
with sqlite3 . connect ( self . db . db_path ) as conn :
cursor = conn . cursor ( )
cursor . execute ( " SELECT id FROM gamers WHERE username = ? " , ( username , ) )
2025-11-23 01:46:12 +03:00
existing_gamer_row = cursor . fetchone ( )
is_new_gamer = existing_gamer_row is None
2025-11-13 01:00:48 +03:00
2025-11-07 22:54:49 +03:00
gamer_id = self . db . add_gamer ( username )
# Link user to gamer (without token)
2025-11-23 01:46:12 +03:00
added = self . db . add_user_gamer ( user_id , gamer_id , None )
# If add_user_gamer returned False, it means the pair already exists (shouldn't happen after our check, but just in case)
if not added :
await update . message . reply_text (
2025-12-03 13:09:40 +03:00
t ( ' gamer_already_added ' , lang , username = username ) + ' \n \n ' + t ( ' addgamer_prompt ' , lang ) ,
parse_mode = ' HTML '
2025-11-23 01:46:12 +03:00
)
2025-12-03 13:09:40 +03:00
# Keep awaiting flag - don't clear it, so user can try again
2025-11-23 01:46:12 +03:00
return
2025-11-07 22:54:49 +03:00
# Set default period to 1 hour (60 minutes) for new gamer
self . db . set_user_gamer_period ( user_id , gamer_id , 60 )
# If this is the first gamer for this user, make it active
user_gamers = self . db . get_user_gamers ( user_id )
if len ( user_gamers ) == 1 :
self . db . set_user_active_gamer ( user_id , gamer_id )
2025-11-18 14:41:13 +03:00
# Start periodic task for this gamer (60 minutes period)
try :
gamer_data = {
' id ' : gamer_id ,
' username ' : username ,
' token ' : None ,
' period_minutes ' : 60
}
await self . start_periodic_task ( gamer_data , user_id , 60 )
logger . info ( f " Started periodic task for { username } (user { user_id } ) with period 60 minutes " )
except Exception as e :
logger . error ( f " Failed to start periodic task for { username } : { e } " )
import traceback
logger . error ( traceback . format_exc ( ) )
2025-11-16 13:38:25 +03:00
# Notify admin bot about player link (always notify)
try :
user_obj = update . effective_user
await self . _notify_admin_new_player (
2025-11-18 19:39:52 +03:00
username , user_id , user_obj . username if user_obj else None , is_new_gamer
2025-11-16 13:38:25 +03:00
)
logger . info ( f " Admin notification processed for player { username } " )
except Exception as e :
logger . error ( f " Failed to notify admin about new player link: { e } " )
2025-11-13 01:00:48 +03:00
2025-11-12 23:20:01 +03:00
lang = self . get_user_language_from_update ( update )
2025-11-07 22:54:49 +03:00
await update . message . reply_text (
2025-11-23 16:53:12 +03:00
t ( ' gamer_added ' , lang , username = username ) ,
parse_mode = ' HTML '
2025-11-07 22:54:49 +03:00
)
2025-11-16 15:23:54 +03:00
# Clear awaiting flag
try :
context . user_data [ ' awaiting_addgamer_username ' ] = False
except Exception :
pass
return
2025-10-26 20:23:26 +03:00
async def getgamers ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Get all gamers for the current user and allow selection """
user_id = update . effective_user . id
gamers = self . db . get_user_gamers ( user_id )
2025-10-29 00:29:53 +03:00
logger . info ( f " getgamers: user_id= { user_id } , found { len ( gamers ) } gamers " )
2025-11-12 23:20:01 +03:00
lang = self . get_user_language_from_update ( update )
2025-10-26 20:23:26 +03:00
if not gamers :
2025-11-23 01:46:12 +03:00
logger . info ( f " getgamers: No gamers found for user { user_id } , sending no_gamers message " )
2025-11-12 23:20:01 +03:00
await update . message . reply_text ( t ( ' no_gamers ' , lang ) )
2025-11-13 13:32:46 +03:00
self . counters . increment ( ' getgamers ' )
2025-10-26 20:23:26 +03:00
return
2025-11-23 01:46:12 +03:00
logger . info ( f " getgamers: Proceeding with { len ( gamers ) } gamers for user { user_id } " )
2025-11-13 13:32:46 +03:00
self . counters . increment ( ' getgamers ' )
2025-10-26 20:23:26 +03:00
# Show loading message
2025-11-12 23:20:01 +03:00
loading_msg = await update . message . reply_text ( t ( ' loading_ratings ' , lang ) )
2025-10-26 20:23:26 +03:00
# Prepare data for each gamer
gamers_data = [ ]
2025-10-29 00:29:53 +03:00
for i , gamer in enumerate ( gamers ) :
try :
logger . info ( f " Processing gamer { i + 1 } / { len ( gamers ) } : { gamer [ ' username ' ] } (ID: { gamer [ ' id ' ] } ) " )
username = gamer [ ' username ' ]
# Get user ratings from Lichess API
logger . debug ( f " Requesting ratings for { username } " )
ratings_data = await self . lichess_api . get_user_ratings ( username )
logger . debug ( f " Received ratings for { username } : { ratings_data is not None } " )
2025-10-29 00:59:36 +03:00
# Add delay between requests to avoid rate limiting
if i < len ( gamers ) - 1 : # Don't sleep after the last one
await asyncio . sleep ( 0.5 ) # 500ms delay between requests
2025-10-29 00:29:53 +03:00
if ratings_data and ' perfs ' in ratings_data :
perfs = ratings_data [ ' perfs ' ]
bullet_rating = perfs . get ( ' bullet ' , { } ) . get ( ' rating ' , ' N/A ' )
blitz_rating = perfs . get ( ' blitz ' , { } ) . get ( ' rating ' , ' N/A ' )
rapid_rating = perfs . get ( ' rapid ' , { } ) . get ( ' rating ' , ' N/A ' )
else :
bullet_rating = blitz_rating = rapid_rating = ' N/A '
# Add period information if period > 0
period_minutes = gamer . get ( ' period_minutes ' , 0 )
2025-11-12 23:20:01 +03:00
period_suffix = t ( ' period_minutes_suffix ' , lang )
period_text = f " · { period_minutes } { period_suffix } " if period_minutes > 0 else " "
2025-10-29 00:29:53 +03:00
gamers_data . append ( {
' id ' : gamer [ ' id ' ] ,
' username ' : username ,
' bullet ' : bullet_rating ,
' blitz ' : blitz_rating ,
' rapid ' : rapid_rating ,
' period ' : period_text
} )
logger . info ( f " Successfully added gamer { username } to gamers_data (total: { len ( gamers_data ) } ) " )
except Exception as e :
logger . error ( f " Error processing gamer { gamer . get ( ' username ' , ' unknown ' ) } : { e } " )
import traceback
logger . error ( f " Traceback: { traceback . format_exc ( ) } " )
# Still add the gamer with N/A ratings
2025-11-12 23:20:01 +03:00
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 " "
2025-10-29 00:29:53 +03:00
gamers_data . append ( {
' id ' : gamer [ ' id ' ] ,
' username ' : gamer [ ' username ' ] ,
' bullet ' : ' N/A ' ,
' blitz ' : ' N/A ' ,
' rapid ' : ' N/A ' ,
2025-11-12 23:20:01 +03:00
' period ' : period_text
2025-10-29 00:29:53 +03:00
} )
logger . info ( f " Added gamer { gamer [ ' username ' ] } with N/A ratings due to error " )
2025-10-26 20:23:26 +03:00
# Create text message with stats
text_lines = [ ]
for gamer in gamers_data :
text_lines . append (
2025-11-16 23:36:57 +03:00
f " <b> { gamer [ ' username ' ] } </b> "
2025-10-26 20:23:26 +03:00
f " ⚡ { gamer [ ' bullet ' ] } 🔥 { gamer [ ' blitz ' ] } 🐇 { gamer [ ' rapid ' ] } { gamer [ ' period ' ] } "
)
2025-10-29 00:29:53 +03:00
logger . info ( f " getgamers: prepared { len ( gamers_data ) } gamers for display " )
2025-11-23 01:46:12 +03:00
# Check if we have any gamers to display
if not gamers_data :
logger . warning ( f " getgamers: No gamers data prepared, but gamers list was not empty. This should not happen. " )
try :
await loading_msg . delete ( )
except :
pass
await update . message . reply_text ( t ( ' no_gamers ' , lang ) )
return
2025-11-12 23:20:01 +03:00
gamers_text = t ( ' select_active_gamer ' , lang ) + " \n " . join ( text_lines )
2025-10-26 20:23:26 +03:00
2025-10-29 00:29:53 +03:00
logger . info ( f " getgamers: message length: { len ( gamers_text ) } characters " )
2025-11-16 23:36:57 +03:00
# Edit the loading message with the results (no keyboard)
2025-10-28 21:34:35 +03:00
try :
await loading_msg . edit_text (
gamers_text ,
2025-11-16 23:36:57 +03:00
parse_mode = ' HTML '
2025-10-28 21:34:35 +03:00
)
except Exception as e :
logger . error ( f " Error editing message: { e } " )
# If edit fails, delete the loading message and send a new one
try :
await loading_msg . delete ( )
except :
pass
await update . message . reply_text (
gamers_text ,
2025-11-16 23:36:57 +03:00
parse_mode = ' HTML '
2025-10-28 21:34:35 +03:00
)
2025-11-23 01:46:12 +03:00
logger . info ( f " getgamers: Completed successfully for user { user_id } , displayed { len ( gamers_data ) } gamers " )
2025-10-26 20:23:26 +03:00
async def select_gamer ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Handle gamer selection """
query = update . callback_query
await query . answer ( )
user_id = query . from_user . id
gamer_id = int ( query . data . split ( ' _ ' ) [ 1 ] )
# Get user gamers to find the selected one
gamers = self . db . get_user_gamers ( user_id )
selected_gamer = next ( ( g for g in gamers if g [ ' id ' ] == gamer_id ) , None )
2025-11-12 23:20:01 +03:00
# Для 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 )
2025-10-26 20:23:26 +03:00
if selected_gamer :
# Set active gamer for this user
self . db . set_user_active_gamer ( user_id , gamer_id )
await query . edit_message_text (
2025-11-12 23:20:01 +03:00
t ( ' active_gamer_set ' , lang , username = selected_gamer [ ' username ' ] )
2025-10-26 20:23:26 +03:00
)
else :
2025-11-12 23:20:01 +03:00
await query . edit_message_text ( t ( ' gamer_not_found ' , lang ) )
2025-10-26 20:23:26 +03:00
2025-10-28 21:59:16 +03:00
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 )
2025-11-12 23:20:01 +03:00
lang = self . get_user_language_from_update ( update )
2025-10-28 21:59:16 +03:00
if not gamers :
2025-11-12 23:20:01 +03:00
await update . message . reply_text ( t ( ' no_gamers_to_delete ' , lang ) )
2025-11-13 13:32:46 +03:00
self . counters . increment ( ' delgamer ' )
2025-10-28 21:59:16 +03:00
return
2025-11-13 13:32:46 +03:00
self . counters . increment ( ' delgamer ' )
2025-10-28 21:59:16 +03:00
# Show loading message
2025-11-12 23:20:01 +03:00
loading_msg = await update . message . reply_text ( t ( ' loading_gamers ' , lang ) )
2025-10-28 21:59:16 +03:00
# Create text message with stats
text_lines = [ ]
keyboard = [ ]
2025-10-29 00:59:36 +03:00
for i , gamer in enumerate ( gamers ) :
2025-10-28 21:59:16 +03:00
status = " 🟢 " if gamer [ ' is_active ' ] else " ⚪ "
username = gamer [ ' username ' ]
# Get user ratings from Lichess API
ratings_data = await self . lichess_api . get_user_ratings ( username )
2025-10-29 00:59:36 +03:00
# Add delay between requests to avoid rate limiting
if i < len ( gamers ) - 1 : # Don't sleep after the last one
await asyncio . sleep ( 0.5 ) # 500ms delay between requests
2025-10-28 21:59:16 +03:00
if ratings_data and ' perfs ' in ratings_data :
perfs = ratings_data [ ' perfs ' ]
bullet_rating = perfs . get ( ' bullet ' , { } ) . get ( ' rating ' , ' N/A ' )
blitz_rating = perfs . get ( ' blitz ' , { } ) . get ( ' rating ' , ' N/A ' )
rapid_rating = perfs . get ( ' rapid ' , { } ) . get ( ' rating ' , ' N/A ' )
else :
bullet_rating = blitz_rating = rapid_rating = ' N/A '
period_minutes = gamer . get ( ' period_minutes ' , 0 )
2025-11-12 23:20:01 +03:00
period_suffix = t ( ' period_minutes_suffix ' , lang )
period_text = f " · { period_minutes } { period_suffix } " if period_minutes > 0 else " "
2025-10-28 21:59:16 +03:00
text_lines . append (
f " { status } <b> { username } </b> "
f " ⚡ { bullet_rating } 🔥 { blitz_rating } 🐇 { rapid_rating } { period_text } "
)
# Add delete button
keyboard . append ( [ InlineKeyboardButton (
text = f " 🗑️ { username } " ,
callback_data = f " delete_ { gamer [ ' id ' ] } "
) ] )
2025-11-12 23:20:01 +03:00
gamers_text = t ( ' select_gamer_to_delete ' , lang ) + " \n " . join ( text_lines )
2025-10-28 21:59:16 +03:00
reply_markup = InlineKeyboardMarkup ( keyboard )
# Edit the loading message with the results
try :
await loading_msg . edit_text (
gamers_text ,
parse_mode = ' HTML ' ,
reply_markup = reply_markup
)
except Exception as e :
logger . error ( f " Error editing message: { e } " )
try :
await loading_msg . delete ( )
except :
pass
await update . message . reply_text (
gamers_text ,
parse_mode = ' HTML ' ,
reply_markup = reply_markup
)
async def handle_delete_gamer ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Handle gamer deletion """
query = update . callback_query
await query . answer ( )
user_id = query . from_user . id
gamer_id = int ( query . data . split ( ' _ ' ) [ 1 ] )
# Get gamer info before deletion
gamers = self . db . get_user_gamers ( user_id )
gamer_to_delete = next ( ( g for g in gamers if g [ ' id ' ] == gamer_id ) , None )
2025-11-12 23:20:01 +03:00
# Для 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 )
2025-10-28 21:59:16 +03:00
if gamer_to_delete :
username = gamer_to_delete [ ' username ' ]
2025-11-16 20:44:02 +03:00
was_active = gamer_to_delete . get ( ' is_active ' , False )
total_gamers_before = len ( gamers )
2025-10-28 21:59:16 +03:00
deleted = self . db . remove_user_gamer ( user_id , gamer_id )
if deleted :
2025-11-16 20:44:02 +03:00
# Check how many gamers remain after deletion
remaining_gamers = self . db . get_user_gamers ( user_id )
remaining_count = len ( remaining_gamers )
# Determine which message to show
if remaining_count == 0 :
# Last gamer deleted
message = t ( ' last_gamer_deleted ' , lang , username = username )
elif was_active :
# Active gamer deleted but there are other gamers
message = t ( ' active_gamer_deleted ' , lang , username = username )
else :
# Regular deletion
message = t ( ' gamer_deleted ' , lang , username = username )
2025-10-28 21:59:16 +03:00
await query . edit_message_text (
2025-11-16 20:44:02 +03:00
message ,
2025-10-28 21:59:16 +03:00
parse_mode = ' HTML '
)
else :
2025-11-12 23:20:01 +03:00
await query . edit_message_text ( t ( ' delete_failed ' , lang ) )
2025-10-28 21:59:16 +03:00
else :
2025-11-12 23:20:01 +03:00
await query . edit_message_text ( t ( ' gamer_not_found ' , lang ) )
2025-10-28 21:59:16 +03:00
2025-10-26 20:23:26 +03:00
async def get_stats ( self , update : Update , context : ContextTypes . DEFAULT_TYPE , period : str ) :
2025-11-16 22:49:56 +03:00
""" Get statistics for a period - shows stats for all players with activity """
2025-10-26 20:23:26 +03:00
user_id = update . effective_user . id
2025-11-20 03:14:06 +03:00
logger . info ( f " 🔍 get_stats called: user_id= { user_id } , period= { period } " )
2025-11-16 22:49:56 +03:00
# Get all gamers for this user
gamers = self . db . get_user_gamers ( user_id )
2025-11-20 03:14:06 +03:00
logger . info ( f " 🔍 Found { len ( gamers ) } gamers for user { user_id } : { [ g [ ' username ' ] for g in gamers ] } " )
2025-10-26 20:23:26 +03:00
2025-11-12 23:20:01 +03:00
lang = self . get_user_language_from_update ( update )
2025-11-16 22:49:56 +03:00
if not gamers :
2025-10-26 20:23:26 +03:00
await update . message . reply_text (
2025-11-16 22:49:56 +03:00
t ( ' no_gamers ' , lang )
2025-10-26 20:23:26 +03:00
)
return
2025-11-16 23:10:08 +03:00
# Send initial message about processing
try :
await update . message . reply_text ( t ( ' stats_processing ' , lang ) , parse_mode = ' HTML ' )
except Exception :
pass
2025-11-16 22:49:56 +03:00
# Process each gamer
has_any_activity = False
for i , gamer in enumerate ( gamers ) :
username = gamer [ ' username ' ]
2025-11-20 03:14:06 +03:00
logger . info ( f " 🔍 Processing gamer { i + 1 } / { len ( gamers ) } : { username } for period { period } " )
2025-11-16 22:49:56 +03:00
2025-11-16 23:10:08 +03:00
# Send message about processing this player
processing_msg = None
try :
processing_msg = await update . message . reply_text ( t ( ' stats_player_processing ' , lang , username = username ) , parse_mode = ' HTML ' )
except Exception :
pass
2025-11-16 22:49:56 +03:00
# Get stats based on period
2025-11-20 03:14:06 +03:00
try :
logger . info ( f " 🔍 Making API request for { username } , period= { period } " )
if period == " today " :
data = await self . lichess_api . get_today_stats ( username )
elif period == " yesterday " :
data = await self . lichess_api . get_yesterday_stats ( username )
elif period == " week " :
data = await self . lichess_api . get_week_stats ( username )
else :
await update . message . reply_text ( t ( ' unknown_period ' , lang ) )
return
logger . info ( f " 🔍 API response for { username } : data= { data is not None } , type= { type ( data ) } " )
if data :
logger . info ( f " 🔍 API response keys: { data . keys ( ) if isinstance ( data , dict ) else ' not a dict ' } " )
if isinstance ( data , dict ) and ' message ' in data :
logger . info ( f " 🔍 API message: { data . get ( ' message ' ) } " )
except Exception as e :
logger . error ( f " ❌ Error getting stats for { username } : { e } " )
import traceback
logger . error ( traceback . format_exc ( ) )
data = None
2025-11-16 22:49:56 +03:00
2025-11-16 23:10:08 +03:00
# Delete processing message
if processing_msg :
try :
await processing_msg . delete ( )
except Exception :
pass
2025-11-16 22:49:56 +03:00
# Check if there's activity
has_activity = False
2025-11-20 03:14:06 +03:00
if data :
if data . get ( ' data ' ) :
api_data = data . get ( ' data ' , { } )
tasks = api_data . get ( ' tasks ' , { } )
games = api_data . get ( ' games ' , { } )
logger . info ( f " 🔍 Activity check for { username } : tasks= { tasks } , games= { games } " )
# Check for puzzles activity
if tasks and tasks . get ( ' total ' , 0 ) > 0 :
has_activity = True
logger . info ( f " ✅ { username } has puzzles activity: { tasks . get ( ' total ' ) } " )
# Check for games activity
if games :
for game_type , game_data in games . items ( ) :
if game_data and game_data . get ( ' games_played ' , 0 ) > 0 :
has_activity = True
logger . info ( f " ✅ { username } has { game_type } activity: { game_data . get ( ' games_played ' ) } games " )
break
else :
# API вернул ответ, но без данных (нет активности)
message = data . get ( ' message ' , ' No message ' )
2025-11-23 01:46:12 +03:00
# Filter out old "No active player" messages - this functionality is deprecated
if ' No active player ' in message or ' Нет активного игрока ' in message or ' active player ' in message . lower ( ) or ' активного игрока ' in message . lower ( ) :
logger . info ( f " ℹ ️ API response for { username } : filtered out deprecated ' No active player ' message " )
message = None
else :
logger . info ( f " ℹ ️ API response for { username } : { message } (no activity data) " )
2025-11-20 03:14:06 +03:00
else :
logger . warning ( f " ⚠️ No response data for { username } : data is None " )
2025-11-16 22:49:56 +03:00
# Only send response if there's activity
if has_activity :
formatted_response = StatsFormatter . format_stats_response ( data , username , period , lang )
await update . message . reply_text ( formatted_response )
has_any_activity = True
2025-11-20 03:14:06 +03:00
else :
logger . info ( f " ℹ ️ No activity found for { username } , skipping response " )
2025-11-16 22:49:56 +03:00
# Add delay between requests to avoid rate limiting
if i < len ( gamers ) - 1 :
2025-11-16 23:10:08 +03:00
await asyncio . sleep ( 1.0 )
2025-10-26 20:23:26 +03:00
2025-11-16 22:49:56 +03:00
# If no activity for any player
if not has_any_activity :
await update . message . reply_text ( t ( ' no_activity ' , lang ) )
2025-11-16 23:10:08 +03:00
else :
# Send final message that all is done
await update . message . reply_text ( t ( ' stats_all_done ' , lang ) )
2025-11-13 13:32:46 +03:00
# Increment counter for the period command
if period == " today " :
self . counters . increment ( ' today ' )
elif period == " yesterday " :
self . counters . increment ( ' yesterday ' )
elif period == " week " :
self . counters . increment ( ' week ' )
2025-10-26 20:23:26 +03:00
async def today ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Today command """
await self . get_stats ( update , context , " today " )
async def yesterday ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Yesterday command """
await self . get_stats ( update , context , " yesterday " )
async def week ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Week command """
await self . get_stats ( update , context , " week " )
2025-11-16 20:23:01 +03:00
async def support ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Support command - show contact information """
lang = self . get_user_language_from_update ( update )
2025-11-16 21:36:48 +03:00
support_msg = t ( ' support_message ' , lang , version = BOT_VERSION )
2025-11-16 20:23:01 +03:00
await update . message . reply_text ( support_msg , parse_mode = ' HTML ' )
self . counters . increment ( ' support ' )
2025-11-16 12:48:23 +03:00
async def last_year_or_1000games ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
2025-11-16 22:49:56 +03:00
""" Get last year stats or last 1000 rated games for all players with activity """
2025-11-16 12:48:23 +03:00
user_id = update . effective_user . id
2025-11-16 22:49:56 +03:00
# Get all gamers for this user
gamers = self . db . get_user_gamers ( user_id )
2025-11-16 12:48:23 +03:00
lang = self . get_user_language_from_update ( update )
2025-11-16 22:49:56 +03:00
if not gamers :
2025-11-16 12:48:23 +03:00
await update . message . reply_text (
2025-11-16 22:49:56 +03:00
t ( ' no_gamers ' , lang )
2025-11-16 12:48:23 +03:00
)
return
2025-11-16 22:49:56 +03:00
# Send initial message about processing
try :
await update . message . reply_text ( t ( ' last_year_1000_processing ' , lang ) , parse_mode = ' HTML ' )
except Exception :
pass
2025-11-16 12:48:23 +03:00
now_ms = int ( time . time ( ) * 1000 )
year_ms = 365 * 24 * 3600 * 1000
since_ms = now_ms - year_ms
2025-11-16 22:49:56 +03:00
has_any_activity = False
# Process each gamer sequentially
for i , gamer in enumerate ( gamers ) :
username = gamer [ ' username ' ]
2025-11-16 13:24:39 +03:00
try :
2025-11-16 22:49:56 +03:00
# Send message about processing this player
2025-11-16 23:10:08 +03:00
processing_msg = None
2025-11-16 22:49:56 +03:00
try :
2025-11-16 23:10:08 +03:00
processing_msg = await update . message . reply_text ( t ( ' last_year_1000_player_processing ' , lang , username = username ) , parse_mode = ' HTML ' )
2025-11-16 22:49:56 +03:00
except Exception :
pass
# Get data for this player
data = await self . lichess_api . get_games_period ( username , since_ms , now_ms , rated_only = True )
2025-11-16 23:10:08 +03:00
# Delete processing message
if processing_msg :
try :
await processing_msg . delete ( )
except Exception :
pass
2025-11-16 22:49:56 +03:00
if data :
# Check if there's activity (games_count > 0)
games_count = data . get ( ' games_count ' , 0 )
if games_count > 0 :
# Format and send immediately
text = StatsFormatter . format_last_year_or_1000 ( data , username , lang )
await update . message . reply_text ( text )
has_any_activity = True
2025-11-16 22:52:45 +03:00
# Wait 3 seconds before next request (except after the last one)
2025-11-16 22:49:56 +03:00
if i < len ( gamers ) - 1 :
2025-11-16 22:52:45 +03:00
await asyncio . sleep ( 3.0 )
2025-11-16 22:49:56 +03:00
except Exception as e :
logger . error ( f " /lastYear_or_1000games error for { username } : { e } " )
await update . message . reply_text ( f " Error for { username } : { e } " )
# If no activity for any player
if not has_any_activity :
await update . message . reply_text ( t ( ' no_activity ' , lang ) )
2025-11-16 23:10:08 +03:00
else :
# Send final message that all is done
await update . message . reply_text ( t ( ' stats_all_done ' , lang ) )
2025-11-16 22:49:56 +03:00
self . counters . increment ( ' last_year_1000 ' )
2025-11-16 12:48:23 +03:00
2025-10-26 20:23:26 +03:00
async def setperiod ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
2025-11-16 23:36:57 +03:00
""" Set period command - first select gamer, then select period """
2025-10-26 20:23:26 +03:00
user_id = update . effective_user . id
2025-11-16 23:36:57 +03:00
# Get all gamers for this user
gamers = self . db . get_user_gamers ( user_id )
2025-10-26 20:23:26 +03:00
2025-11-12 23:20:01 +03:00
lang = self . get_user_language_from_update ( update )
2025-11-16 23:36:57 +03:00
if not gamers :
await update . message . reply_text ( t ( ' no_gamers ' , lang ) )
self . counters . increment ( ' setperiod ' )
2025-10-26 20:23:26 +03:00
return
2025-11-16 23:36:57 +03:00
# Create keyboard with gamers and their periods
keyboard = [ ]
for gamer in gamers :
username = gamer [ ' username ' ]
period_minutes = gamer . get ( ' period_minutes ' , 0 )
# Format period text
if period_minutes == 0 :
period_text = " — "
elif period_minutes < 60 :
period_text = f " { period_minutes } m "
elif period_minutes == 60 :
period_text = " 1h "
else :
hours = period_minutes / / 60
period_text = f " { hours } h "
keyboard . append ( [ InlineKeyboardButton (
text = f " { username } · { period_text } " ,
callback_data = f " select_gamer_period_ { gamer [ ' id ' ] } "
) ] )
reply_markup = InlineKeyboardMarkup ( keyboard )
await update . message . reply_text (
" ⏱️ Select player to set notification period: " ,
reply_markup = reply_markup
)
self . counters . increment ( ' setperiod ' )
async def select_gamer_for_period ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Handle gamer selection for period setting """
query = update . callback_query
await query . answer ( )
logger . info ( f " select_gamer_for_period called with callback_data: { query . data } " )
user_id = query . from_user . id
# Parse callback_data: select_gamer_period_{gamer_id}
try :
gamer_id = int ( query . data . split ( ' _ ' ) [ - 1 ] )
logger . info ( f " Parsed gamer_id: { gamer_id } " )
except ( ValueError , IndexError ) as e :
logger . error ( f " Error parsing gamer_id from callback_data ' { query . data } ' : { e } " )
await query . edit_message_text ( " ❌ Error: Invalid player selection " )
return
# Get gamer info
gamers = self . db . get_user_gamers ( user_id )
selected_gamer = None
for gamer in gamers :
if gamer [ ' id ' ] == gamer_id :
selected_gamer = gamer
break
if not selected_gamer :
await query . edit_message_text ( " ❌ Player not found " )
return
# Для 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 )
# Show period options for selected gamer
2025-10-26 20:23:26 +03:00
keyboard = [ ]
for period in PERIOD_OPTIONS :
if period == 0 :
2025-11-12 23:20:01 +03:00
button_text = t ( ' disable_notifications ' , lang )
2025-10-26 20:23:26 +03:00
else :
2025-11-16 21:01:14 +03:00
# Format period text: minutes for < 60, hours for >= 60
if period < 60 :
button_text = f " ⏰ { period } minutes "
elif period == 60 :
button_text = " ⏰ 1 hour "
else :
hours = period / / 60
button_text = f " ⏰ { hours } hours "
2025-11-16 23:36:57 +03:00
keyboard . append ( [ InlineKeyboardButton (
button_text ,
callback_data = f " period_ { gamer_id } _ { period } "
) ] )
2025-10-26 20:23:26 +03:00
reply_markup = InlineKeyboardMarkup ( keyboard )
2025-11-16 23:36:57 +03:00
await query . edit_message_text (
t ( ' select_period ' , lang , username = selected_gamer [ ' username ' ] ) ,
reply_markup = reply_markup ,
parse_mode = ' HTML '
2025-10-26 20:23:26 +03:00
)
async def select_period ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Handle period selection """
query = update . callback_query
await query . answer ( )
user_id = query . from_user . id
2025-11-16 23:36:57 +03:00
# Parse callback data: period_{gamer_id}_{period}
parts = query . data . split ( ' _ ' )
gamer_id = int ( parts [ 1 ] )
period = int ( parts [ 2 ] )
2025-10-26 20:23:26 +03:00
2025-11-16 23:36:57 +03:00
# Get gamer info
gamers = self . db . get_user_gamers ( user_id )
selected_gamer = None
for gamer in gamers :
if gamer [ ' id ' ] == gamer_id :
selected_gamer = gamer
break
if not selected_gamer :
await query . edit_message_text ( " ❌ Player not found " )
return
2025-10-26 20:23:26 +03:00
2025-11-12 23:20:01 +03:00
# Для 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 )
2025-11-16 23:36:57 +03:00
# Set period for this user-gamer pair
self . db . set_user_gamer_period ( user_id , gamer_id , period )
if period == 0 :
await query . edit_message_text (
t ( ' notifications_disabled ' , lang , username = selected_gamer [ ' username ' ] )
)
else :
# Format period text for confirmation message
if period < 60 :
period_text = f " { period } minutes "
elif period == 60 :
period_text = " 1 hour "
2025-10-26 20:23:26 +03:00
else :
2025-11-16 23:36:57 +03:00
hours = period / / 60
period_text = f " { hours } hours "
await query . edit_message_text (
f " ✅ Period { period_text } set for { selected_gamer [ ' username ' ] } \n 📱 Notifications will be sent to personal messages "
)
# Start periodic task for this gamer (send to user's personal messages)
await self . start_periodic_task ( selected_gamer , user_id , period )
2025-10-26 20:23:26 +03:00
2025-11-20 12:43:00 +03:00
async def set_lang ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Show language selection menu """
2025-11-12 23:20:01 +03:00
user = update . effective_user
2025-11-20 12:43:00 +03:00
if not user :
2025-11-12 23:20:01 +03:00
await update . message . reply_text ( " ❌ Failed to get user information " )
2025-11-20 12:43:00 +03:00
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 '
)
2025-11-12 23:20:01 +03:00
2025-11-20 12:43:00 +03:00
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 )
2025-11-12 23:20:01 +03:00
else :
2025-11-20 12:43:00 +03:00
# 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 )
2025-11-12 23:20:01 +03:00
2025-11-21 23:16:35 +03:00
async def profile ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Show list of players to view their profiles """
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 ( t ( ' no_gamers ' , lang ) )
return
# Create keyboard with player buttons
keyboard = [ ]
for gamer in gamers :
username = gamer [ ' username ' ]
keyboard . append ( [
InlineKeyboardButton (
username ,
callback_data = f " profile_ { gamer [ ' id ' ] } "
)
] )
reply_markup = InlineKeyboardMarkup ( keyboard )
await update . message . reply_text (
t ( ' select_player_profile ' , lang ) ,
reply_markup = reply_markup ,
parse_mode = ' HTML '
)
async def handle_profile_selection ( self , update : Update , context : ContextTypes . DEFAULT_TYPE ) :
""" Handle profile selection callback - send profile link """
query = update . callback_query
await query . answer ( )
user_id = query . from_user . id
gamer_id = int ( query . data . split ( ' _ ' ) [ 1 ] )
# Get gamer info
gamers = self . db . get_user_gamers ( user_id )
selected_gamer = next ( ( g for g in gamers if g [ ' id ' ] == gamer_id ) , None )
if not selected_gamer :
lang = self . db . get_user_language ( user_id )
await query . edit_message_text ( t ( ' gamer_not_found ' , lang ) )
return
username = selected_gamer [ ' username ' ]
profile_url = f " https://lichess.org/@/ { username } "
lang = self . db . get_user_language ( user_id )
# Send profile link
await query . message . reply_text (
f " 🔗 <a href= \" { profile_url } \" > { profile_url } </a> " ,
parse_mode = ' HTML '
)
# Edit original message to show confirmation
await query . edit_message_text (
t ( ' profile_link_sent ' , lang ) ,
parse_mode = ' HTML '
)
2025-10-26 20:23:26 +03:00
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 } "
# Cancel existing task if any
if task_key in self . periodic_tasks :
self . periodic_tasks [ task_key ] . cancel ( )
logger . info ( f " Cancelled existing periodic task for { gamer [ ' username ' ] } " )
# Remove old start time
if task_key in self . period_start_times :
del self . period_start_times [ task_key ]
# Create new periodic task
task = asyncio . create_task (
self . periodic_check ( gamer , user_id , period_minutes )
)
self . periodic_tasks [ task_key ] = task
async def periodic_check ( self , gamer : Dict [ str , Any ] , user_id : int , period_minutes : int ) :
""" Periodic check for gamer activity """
task_key = f " { gamer [ ' id ' ] } _ { user_id } "
2025-11-20 03:23:38 +03:00
username = gamer [ ' username ' ]
2025-10-26 20:23:26 +03:00
2025-12-06 00:28:53 +03:00
# Н Е устанавливаем period_start_times при инициализации
# Это позволит использовать логику первой проверки (else блок)
2025-11-20 03:23:38 +03:00
logger . info ( f " 🔄 Started periodic monitoring for { username } (user { user_id } ) with { period_minutes } minute intervals " )
2025-10-26 20:23:26 +03:00
2025-11-16 23:36:57 +03:00
consecutive_errors = 0
max_consecutive_errors = 5
2025-12-06 00:28:53 +03:00
is_first_check = True # Флаг для первой проверки
2025-11-16 23:36:57 +03:00
2025-10-26 20:23:26 +03:00
while True :
try :
2025-11-16 23:36:57 +03:00
# Проверяем, что период все еще установлен в БД
current_gamers = self . db . get_user_gamers ( user_id )
gamer_still_exists = False
current_period = 0
for g in current_gamers :
if g [ ' id ' ] == gamer [ ' id ' ] :
gamer_still_exists = True
current_period = g . get ( ' period_minutes ' , 0 )
break
# Если игрок удален или период отключен, прекращаем мониторинг
if not gamer_still_exists or current_period == 0 :
logger . info ( f " Periodic monitoring stopped for { gamer [ ' username ' ] } : gamer removed or period disabled " )
if task_key in self . periodic_tasks :
del self . periodic_tasks [ task_key ]
if task_key in self . period_start_times :
del self . period_start_times [ task_key ]
break
# Обновляем период на случай, если он был изменен
if current_period != period_minutes :
logger . info ( f " Period changed for { gamer [ ' username ' ] } from { period_minutes } to { current_period } minutes " )
period_minutes = current_period
2025-12-06 00:28:53 +03:00
# Получаем сохраненное время последней проверки для расчета следующего периода
# Используем флаг is_first_check для первой проверки вместо проверки наличия ключа
last_check_time = self . period_start_times . get ( task_key )
if is_first_check :
last_check_time = None # Принудительно делаем первую проверку
is_first_check = False
2025-10-26 20:23:26 +03:00
2025-12-06 00:28:53 +03:00
if last_check_time :
# Уже была хотя бы одна проверка
# Рассчитываем, когда должен начаться следующий период
next_period_start = last_check_time + timedelta ( minutes = period_minutes )
now = datetime . now ( )
# Если следующий период еще не наступил, ждем
if next_period_start > now :
wait_seconds = ( next_period_start - now ) . total_seconds ( )
logger . info ( f " ⏳ Waiting { wait_seconds : .1f } seconds until next period start ( { next_period_start } ) for { username } " )
await asyncio . sleep ( wait_seconds )
# Используем сохраненное время как начало периода
since_time = last_check_time
# Конец периода - это момент, когда должен был начаться следующий период
period_end_approx = next_period_start
logger . info ( f " 📌 Using saved period: from { since_time } to { period_end_approx } " )
else :
# Первая проверка - ждем period_minutes минут от момента запуска
logger . info ( f " ⏳ First check: waiting { period_minutes } minutes before first check for { username } " )
await asyncio . sleep ( period_minutes * 60 )
# Получаем текущее время
period_end_approx = datetime . now ( )
# Начало периода - текущее время минус period_minutes
since_time = period_end_approx - timedelta ( minutes = period_minutes )
logger . info ( f " 📌 First check: period from { since_time } to { period_end_approx } " )
2025-10-26 20:23:26 +03:00
2025-11-18 16:28:07 +03:00
since_timestamp = int ( since_time . timestamp ( ) * 1000 )
2025-12-06 00:28:53 +03:00
# Используем приблизительное время как until_timestamp
# После получения ответа пересчитаем фактическое время
until_timestamp_approx = int ( period_end_approx . timestamp ( ) * 1000 )
2025-10-26 20:23:26 +03:00
2025-12-06 00:28:53 +03:00
logger . info ( f " 🔍 Checking activity for { username } (user { user_id } ): period from { since_time } to { period_end_approx } (approx, last { period_minutes } minutes) " )
logger . info ( f " 📅 Unix timestamps: since= { since_timestamp } , until_approx= { until_timestamp_approx } " )
2025-10-26 20:23:26 +03:00
2025-11-20 03:23:38 +03:00
# Делаем запросы к API через очередь с обработкой ошибок
2025-11-16 23:36:57 +03:00
games_data = None
2025-10-26 20:23:26 +03:00
puzzles_data = None
2025-11-16 23:36:57 +03:00
try :
2025-11-20 03:23:38 +03:00
# Добавляем запрос в очередь (будет выполнен с задержкой 7 секунд)
logger . info ( f " 📥 Adding games request to queue for { gamer [ ' username ' ] } " )
games_data = await self . request_queue . add_request (
self . lichess_api . get_games_period ,
2025-12-06 00:28:53 +03:00
gamer [ ' username ' ] , since_timestamp , until_timestamp_approx
2025-10-26 20:23:26 +03:00
)
2025-12-06 00:28:53 +03:00
# Фиксируем фактическое время получения ответа
request_end_time = datetime . now ( )
logger . info ( f " ✅ Games API response received for { gamer [ ' username ' ] } at { request_end_time } " )
2025-11-16 23:36:57 +03:00
except Exception as e :
2025-11-20 03:23:38 +03:00
logger . error ( f " ❌ Error getting games data for { gamer [ ' username ' ] } : { e } " )
2025-11-16 23:36:57 +03:00
consecutive_errors + = 1
if consecutive_errors > = max_consecutive_errors :
logger . error ( f " Too many consecutive errors for { gamer [ ' username ' ] } , stopping periodic check " )
break
2025-12-06 00:28:53 +03:00
# Продолжаем с обновлением времени начала на планируемое время окончания периода
# чтобы не создавать пропусков в следующих проверках
self . period_start_times [ task_key ] = period_end_approx
logger . warning ( f " ⚠️ Error occurred, updated period_start_time to { period_end_approx } (planned period end) " )
2025-11-16 23:36:57 +03:00
continue
if gamer . get ( ' token ' ) :
try :
2025-11-20 03:23:38 +03:00
# Добавляем запрос в очередь (будет выполнен с задержкой 7 секунд)
logger . info ( f " 📥 Adding puzzles request to queue for { gamer [ ' username ' ] } " )
puzzles_data = await self . request_queue . add_request (
self . lichess_api . get_puzzles_period ,
2025-12-06 00:28:53 +03:00
gamer [ ' token ' ] , since_timestamp , until_timestamp_approx , 150
2025-11-16 23:36:57 +03:00
)
2025-12-06 00:28:53 +03:00
# Обновляем фактическое время после получения ответа по пазлам
request_end_time = datetime . now ( )
logger . info ( f " ✅ Puzzles API response received for { gamer [ ' username ' ] } at { request_end_time } " )
2025-11-16 23:36:57 +03:00
except Exception as e :
2025-11-20 03:23:38 +03:00
logger . warning ( f " ⚠️ Error getting puzzles data for { gamer [ ' username ' ] } : { e } " )
2025-11-16 23:36:57 +03:00
# Продолжаем без данных по пазлам
# Сбрасываем счетчик ошибок при успешном запросе
consecutive_errors = 0
2025-10-26 20:23:26 +03:00
# Проверяем наличие реальной активности
has_games = False
total_games = 0
2025-11-20 01:22:52 +03:00
if games_data :
# Логируем структуру ответа для отладки
2025-11-20 03:23:38 +03:00
logger . info ( f " 📊 Games data structure for { username } : { games_data } " )
# Проверяем games_count на верхнем уровне (приоритет)
2025-12-06 00:28:53 +03:00
top_level_count = games_data . get ( ' games_count ' , 0 )
logger . debug ( f " 🔍 Top-level games_count: { top_level_count } " )
if top_level_count > 0 :
total_games = top_level_count
2025-11-20 01:22:52 +03:00
has_games = True
2025-11-20 03:23:38 +03:00
logger . info ( f " ✅ Found { total_games } games via games_count field " )
2025-12-06 00:28:53 +03:00
else :
# Также проверяем data.total.games_played
games_data_dict = games_data . get ( ' data ' )
if games_data_dict :
data_total = games_data_dict . get ( ' total ' , { } )
total_games_played = data_total . get ( ' games_played ' , 0 ) if data_total else 0
else :
total_games_played = 0
logger . debug ( f " 🔍 data.total.games_played: { total_games_played } " )
if total_games_played > 0 :
total_games = total_games_played
has_games = True
logger . info ( f " ✅ Found { total_games } games via data.total.games_played field " )
else :
logger . warning ( f " ⚠️ No games found: games_count= { top_level_count } , data.total.games_played= { total_games_played } " )
2025-11-20 01:22:52 +03:00
else :
2025-11-20 03:23:38 +03:00
logger . warning ( f " ⚠️ No games_data returned for { username } " )
2025-10-26 20:23:26 +03:00
has_puzzles = False
2025-11-20 01:22:52 +03:00
total_puzzles = 0
if puzzles_data :
2025-11-20 03:23:38 +03:00
logger . info ( f " 📊 Puzzles data structure for { username } : { puzzles_data } " )
# Проверяем puzzles_in_period на верхнем уровне (приоритет)
if puzzles_data . get ( ' puzzles_in_period ' , 0 ) > 0 :
2025-11-20 01:22:52 +03:00
total_puzzles = puzzles_data . get ( ' puzzles_in_period ' , 0 )
has_puzzles = True
2025-11-20 03:23:38 +03:00
logger . info ( f " ✅ Found { total_puzzles } puzzles via puzzles_in_period field " )
# Также проверяем data.total_attempts
elif puzzles_data . get ( ' data ' ) and puzzles_data . get ( ' data ' , { } ) . get ( ' total_attempts ' , 0 ) > 0 :
total_puzzles = puzzles_data . get ( ' data ' , { } ) . get ( ' total_attempts ' , 0 )
has_puzzles = True
logger . info ( f " ✅ Found { total_puzzles } puzzles via data.total_attempts field " )
2025-10-26 20:23:26 +03:00
2025-11-20 03:23:38 +03:00
logger . info ( f " 🔍 Activity check result for { username } : has_games= { has_games } (total= { total_games } ), has_puzzles= { has_puzzles } (total= { total_puzzles } ) " )
2025-10-26 20:23:26 +03:00
# Отправляем уведомление только если есть реальная активность
if has_games or has_puzzles :
2025-11-18 14:03:06 +03:00
logger . info ( f " 📊 Activity detected for { gamer [ ' username ' ] } , preparing notification... " )
2025-10-26 20:23:26 +03:00
try :
2025-11-12 23:20:01 +03:00
# Get user language from database
user_lang = self . db . get_user_language ( user_id )
2025-10-26 20:23:26 +03:00
notification = StatsFormatter . format_period_notification (
2025-11-12 23:20:01 +03:00
gamer [ ' username ' ] , games_data , puzzles_data , period_minutes , lang = user_lang
2025-10-26 20:23:26 +03:00
)
if self . application :
try :
await self . application . bot . send_message (
chat_id = user_id ,
text = notification
)
2025-11-16 23:36:57 +03:00
logger . info ( f " ✅ Sent periodic notification for { gamer [ ' username ' ] } to user { user_id } " )
2025-11-13 13:32:46 +03:00
# Increment periodic notification counter
self . counters . increment ( ' periodic_notification ' )
2025-10-26 20:23:26 +03:00
except Exception as e :
2025-11-16 23:36:57 +03:00
logger . error ( f " ❌ Failed to send notification to user { user_id } : { e } " )
import traceback
logger . error ( f " Traceback: { traceback . format_exc ( ) } " )
else :
2025-11-18 14:03:06 +03:00
logger . error ( f " ❌ Application not initialized, cannot send notification for { gamer [ ' username ' ] } to user { user_id } " )
2025-10-26 20:23:26 +03:00
except Exception as e :
logger . error ( f " Error formatting notification for { gamer [ ' username ' ] } : { e } " )
import traceback
logger . error ( f " Traceback: { traceback . format_exc ( ) } " )
2025-11-20 01:22:52 +03:00
else :
logger . debug ( f " ⏭️ No activity found for { gamer [ ' username ' ] } in the last { period_minutes } minutes " )
2025-12-06 00:28:53 +03:00
# Обновляем время начала следующего периода на ПЛАНИРУЕМОЕ время окончания текущего периода
# (period_end_approx), а не на фактическое время завершения запроса (request_end_time).
# Это гарантирует непрерывность периодов без пропусков:
# - Проверяем период A-B
# - Следующая проверка будет периода B-C
# - Без пропусков между A-B и B-C
#
# Использование request_end_time приведет к пропуску диапазона между period_end_approx и request_end_time
#
# period_end_approx уже установлено в начале итерации
self . period_start_times [ task_key ] = period_end_approx
logger . info ( f " 📌 Updated period_start_time for { username } to { period_end_approx } (planned period end, next period will start from here) " )
if ' request_end_time ' in locals ( ) :
delay = ( request_end_time - period_end_approx ) . total_seconds ( )
if delay > 0 :
logger . info ( f " ⏱️ Request completed with { delay : .1f } s delay after planned period end " )
2025-10-26 20:23:26 +03:00
except asyncio . CancelledError :
2025-11-16 23:36:57 +03:00
logger . info ( f " Periodic check cancelled for { gamer [ ' username ' ] } " )
2025-10-26 20:23:26 +03:00
break
except Exception as e :
2025-11-16 23:36:57 +03:00
consecutive_errors + = 1
logger . error ( f " Error in periodic check for { gamer [ ' username ' ] } : { e } " )
2025-10-26 20:23:26 +03:00
import traceback
logger . error ( f " Full traceback: { traceback . format_exc ( ) } " )
2025-11-16 23:36:57 +03:00
if consecutive_errors > = max_consecutive_errors :
logger . error ( f " Too many consecutive errors for { gamer [ ' username ' ] } , stopping periodic check " )
if task_key in self . periodic_tasks :
del self . periodic_tasks [ task_key ]
if task_key in self . period_start_times :
del self . period_start_times [ task_key ]
break
2025-12-06 00:28:53 +03:00
# Важно: обновляем period_start_times даже при ошибке, чтобы не зациклиться на одном периоде
# Используем period_end_approx, если он был установлен, иначе используем текущее время
if ' period_end_approx ' in locals ( ) :
self . period_start_times [ task_key ] = period_end_approx
logger . warning ( f " ⚠️ Error occurred, updated period_start_time to { period_end_approx } to prevent loop " )
else :
# Если period_end_approx не был установлен (например, ошибка в начале), используем текущее время
now = datetime . now ( )
self . period_start_times [ task_key ] = now
logger . warning ( f " ⚠️ Error occurred early, updated period_start_time to { now } to prevent loop " )
2025-11-16 23:36:57 +03:00
# Ждем перед повторной попыткой при ошибке
await asyncio . sleep ( 60 ) # 1 minute delay before retry
2025-10-26 20:23:26 +03:00
def setup_handlers ( self , application : Application ) :
""" Setup all handlers """
self . application = application # Store application reference
2025-10-28 23:09:00 +03:00
# Conversation handler for addtoken (token required)
2025-11-19 12:02:54 +03:00
# Must be added BEFORE general MessageHandler to avoid conflicts
2025-10-28 23:09:00 +03:00
addtoken_conv = ConversationHandler (
entry_points = [ CommandHandler ( " addtoken " , self . addtoken_start ) ] ,
states = {
WAITING_FOR_TOKEN : [ MessageHandler ( filters . TEXT & ~ filters . COMMAND , self . handle_token ) ] ,
} ,
2025-11-21 23:49:26 +03:00
fallbacks = [
CommandHandler ( " cancel " , lambda u , c : ConversationHandler . END ) ,
CommandHandler ( " addtoken " , self . addtoken_start ) # Allow restarting conversation
] ,
per_chat = True ,
per_user = True
2025-10-28 23:09:00 +03:00
)
2025-11-19 12:02:54 +03:00
# Add handlers - ConversationHandler must be before general MessageHandler
2025-11-16 21:32:33 +03:00
application . add_handler ( CommandHandler ( " start " , self . start_and_addgamer ) )
2025-12-01 13:52:26 +03:00
application . add_handler ( CommandHandler ( " help " , self . help_command ) )
2025-11-16 15:23:54 +03:00
application . add_handler ( CommandHandler ( " addgamer " , self . addgamer_start ) )
2025-11-19 12:02:54 +03:00
application . add_handler ( addtoken_conv ) # Add before general MessageHandler
2025-11-16 15:23:54 +03:00
application . add_handler ( MessageHandler ( filters . TEXT & ~ filters . COMMAND , self . handle_username ) )
2025-10-26 20:23:26 +03:00
application . add_handler ( CommandHandler ( " getgamers " , self . getgamers ) )
2025-10-28 21:59:16 +03:00
application . add_handler ( CommandHandler ( " delgamer " , self . delgamer ) )
2025-10-26 20:23:26 +03:00
application . add_handler ( CommandHandler ( " today " , self . today ) )
application . add_handler ( CommandHandler ( " yesterday " , self . yesterday ) )
application . add_handler ( CommandHandler ( " week " , self . week ) )
2025-11-16 12:48:23 +03:00
application . add_handler ( CommandHandler ( " lastYear_or_1000games " , self . last_year_or_1000games ) )
2025-11-16 20:23:01 +03:00
application . add_handler ( CommandHandler ( " support " , self . support ) )
2025-10-26 20:23:26 +03:00
application . add_handler ( CommandHandler ( " setperiod " , self . setperiod ) )
2025-11-20 12:43:00 +03:00
application . add_handler ( CommandHandler ( " set_lang " , self . set_lang ) )
2025-11-21 23:16:35 +03:00
application . add_handler ( CommandHandler ( " profile " , self . profile ) )
2025-11-16 13:38:25 +03:00
application . add_handler ( CommandHandler ( " test_admin_notify " , self . test_admin_notify ) )
2025-10-26 20:23:26 +03:00
2025-11-16 23:36:57 +03:00
# Callback handlers (order matters - more specific patterns first)
2025-12-03 02:33:38 +03:00
application . add_handler ( CallbackQueryHandler ( self . addgamer_show_prompt , pattern = " ^addgamer_add$ " ) )
application . add_handler ( CallbackQueryHandler ( self . addgamer_show_help , pattern = " ^addgamer_how$ " ) )
2025-11-20 12:43:00 +03:00
application . add_handler ( CallbackQueryHandler ( self . handle_language_selection , pattern = " ^lang_ " ) )
2025-11-21 23:16:35 +03:00
application . add_handler ( CallbackQueryHandler ( self . handle_profile_selection , pattern = " ^profile_ " ) )
2025-11-16 23:36:57 +03:00
application . add_handler ( CallbackQueryHandler ( self . select_gamer_for_period , pattern = " ^select_gamer_period_ " ) )
2025-10-26 20:23:26 +03:00
application . add_handler ( CallbackQueryHandler ( self . select_gamer , pattern = " ^select_ " ) )
2025-10-28 21:59:16 +03:00
application . add_handler ( CallbackQueryHandler ( self . handle_delete_gamer , pattern = " ^delete_ " ) )
2025-10-26 20:23:26 +03:00
application . add_handler ( CallbackQueryHandler ( self . select_period , pattern = " ^period_ " ) )
def main ( ) :
""" Main function """
2025-11-13 01:00:48 +03:00
# Initialize admin bot for notifications (admin bot runs as separate service)
init_admin_bot ( )
2025-10-26 20:23:26 +03:00
bot = LichessBot ( )
# Create application with Long Polling configuration
application = Application . builder ( ) . token ( TELEGRAM_BOT_TOKEN ) . build ( )
# Setup handlers
bot . setup_handlers ( application )
# Set application reference for periodic tasks
bot . application = application
# Start periodic tasks for existing gamers (will be called after application starts)
async def post_init ( app ) :
await bot . start_existing_periodic_tasks ( )
application . post_init = post_init
2025-11-13 01:00:48 +03:00
# Add error handler
async def error_handler ( update : object , context : ContextTypes . DEFAULT_TYPE ) - > None :
""" Log the error and send a telegram message to notify the developer. """
logger . error ( f " Exception while handling an update: { context . error } " )
import traceback
logger . error ( traceback . format_exc ( ) )
application . add_error_handler ( error_handler )
2025-10-26 20:23:26 +03:00
# Start the bot with Long Polling
logger . info ( " Starting Lichess Statistics Bot with Long Polling... " )
2025-11-13 01:00:48 +03:00
try :
application . run_polling (
poll_interval = POLL_INTERVAL ,
timeout = POLL_TIMEOUT ,
drop_pending_updates = DROP_PENDING_UPDATES ,
allowed_updates = ALLOWED_UPDATES
)
except Exception as e :
logger . error ( f " Fatal error in run_polling: { e } " )
import traceback
logger . error ( traceback . format_exc ( ) )
raise
2025-10-26 20:23:26 +03:00
if __name__ == ' __main__ ' :
main ( )