2025-10-26 20:23:26 +03:00
|
|
|
|
import asyncio
|
|
|
|
|
|
import logging
|
2025-11-12 23:20:01 +03:00
|
|
|
|
import sqlite3
|
2025-10-26 20:23:26 +03:00
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
from typing import Dict, Any, Optional
|
|
|
|
|
|
|
|
|
|
|
|
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-16 21:36:48 +03:00
|
|
|
|
LICHESS_STATS_API_BASE_URL, ADMINPANEL_TELEGRAM_BOT_TOKEN, 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-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:
|
|
|
|
|
|
# Run the regular start command
|
|
|
|
|
|
await self.start(update, context)
|
|
|
|
|
|
# Start addgamer conversation and return state
|
|
|
|
|
|
return await self.addgamer_start(update, context)
|
|
|
|
|
|
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
|
|
|
|
|
|
return ConversationHandler.END
|
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):
|
|
|
|
|
|
"""Start addgamer command - simple username only"""
|
2025-11-23 01:46:12 +03:00
|
|
|
|
user_id = update.effective_user.id
|
|
|
|
|
|
logger.info(f"addgamer_start called for user {user_id}")
|
|
|
|
|
|
|
|
|
|
|
|
# Check if we're already awaiting a username (prevent duplicate messages)
|
|
|
|
|
|
if context and hasattr(context, "user_data") and context.user_data.get('awaiting_addgamer_username'):
|
|
|
|
|
|
logger.info(f"addgamer_start: Already awaiting username for user {user_id}, skipping duplicate call")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2025-11-12 23:20:01 +03:00
|
|
|
|
lang = self.get_user_language_from_update(update)
|
2025-11-13 01:00:48 +03:00
|
|
|
|
try:
|
2025-11-16 15:23:54 +03:00
|
|
|
|
# Mark that we are awaiting a username reply
|
|
|
|
|
|
if context and hasattr(context, "user_data"):
|
|
|
|
|
|
context.user_data['awaiting_addgamer_username'] = True
|
2025-11-13 01:00:48 +03:00
|
|
|
|
await update.message.reply_text(t('addgamer_prompt', lang))
|
2025-11-23 01:46:12 +03:00
|
|
|
|
logger.info(f"Addgamer prompt sent to user {user_id}")
|
2025-11-13 13:32:46 +03:00
|
|
|
|
self.counters.increment('addgamer')
|
2025-11-13 01:00:48 +03:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Error sending addgamer prompt: {e}")
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
logger.error(traceback.format_exc())
|
2025-11-23 01:46:12 +03:00
|
|
|
|
# Clear flag on error
|
|
|
|
|
|
if context and hasattr(context, "user_data"):
|
|
|
|
|
|
context.user_data['awaiting_addgamer_username'] = False
|
2025-11-16 15:23:54 +03:00
|
|
|
|
# No conversation state returned; handler-based flow
|
|
|
|
|
|
return
|
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-11-12 23:20:01 +03:00
|
|
|
|
t('user_not_found', lang, username=username)
|
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(
|
|
|
|
|
|
t('gamer_already_added', lang, username=username)
|
|
|
|
|
|
)
|
|
|
|
|
|
# Clear awaiting flag
|
|
|
|
|
|
try:
|
|
|
|
|
|
context.user_data['awaiting_addgamer_username'] = False
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
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(
|
|
|
|
|
|
t('gamer_already_added', lang, username=username)
|
|
|
|
|
|
)
|
|
|
|
|
|
# Clear awaiting flag
|
|
|
|
|
|
try:
|
|
|
|
|
|
context.user_data['awaiting_addgamer_username'] = False
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
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-12 23:20:01 +03:00
|
|
|
|
t('gamer_added', lang, username=username)
|
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-11-20 03:23:38 +03:00
|
|
|
|
# Инициализируем время начала отслеживания как текущее время
|
|
|
|
|
|
# Первая проверка произойдет через period_minutes минут
|
|
|
|
|
|
start_time = datetime.now()
|
2025-10-26 20:23:26 +03:00
|
|
|
|
self.period_start_times[task_key] = start_time
|
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-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-11-20 03:23:38 +03:00
|
|
|
|
# Ждем заданное количество минут перед следующей проверкой
|
|
|
|
|
|
logger.info(f"⏳ Waiting {period_minutes} minutes before next check for {username}")
|
2025-10-26 20:23:26 +03:00
|
|
|
|
await asyncio.sleep(period_minutes * 60)
|
|
|
|
|
|
|
2025-11-20 03:23:38 +03:00
|
|
|
|
# Получаем текущее время
|
2025-10-26 20:23:26 +03:00
|
|
|
|
now = datetime.now()
|
|
|
|
|
|
|
2025-11-18 16:28:07 +03:00
|
|
|
|
# Рассчитываем период: от (текущее время - период) до текущего времени
|
2025-11-20 03:23:38 +03:00
|
|
|
|
# Это гарантирует, что мы проверяем последний период активности
|
2025-11-18 16:28:07 +03:00
|
|
|
|
since_time = now - timedelta(minutes=period_minutes)
|
|
|
|
|
|
since_timestamp = int(since_time.timestamp() * 1000)
|
2025-10-26 20:23:26 +03:00
|
|
|
|
until_timestamp = int(now.timestamp() * 1000)
|
|
|
|
|
|
|
2025-11-20 03:23:38 +03:00
|
|
|
|
logger.info(f"🔍 Checking activity for {username} (user {user_id}): period from {since_time} to {now} (last {period_minutes} minutes)")
|
|
|
|
|
|
logger.info(f"📅 Unix timestamps: since={since_timestamp}, until={until_timestamp}")
|
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-11-16 23:36:57 +03:00
|
|
|
|
gamer['username'], since_timestamp, until_timestamp
|
2025-10-26 20:23:26 +03:00
|
|
|
|
)
|
2025-11-20 03:23:38 +03:00
|
|
|
|
logger.info(f"✅ Games API response received for {gamer['username']}")
|
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
|
|
|
|
|
|
# Продолжаем с обновлением времени начала, чтобы не зацикливаться
|
|
|
|
|
|
now = datetime.now()
|
|
|
|
|
|
self.period_start_times[task_key] = now
|
|
|
|
|
|
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,
|
|
|
|
|
|
gamer['token'], since_timestamp, until_timestamp, 150
|
2025-11-16 23:36:57 +03:00
|
|
|
|
)
|
2025-11-20 03:23:38 +03:00
|
|
|
|
logger.info(f"✅ Puzzles API response received for {gamer['username']}")
|
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 на верхнем уровне (приоритет)
|
|
|
|
|
|
if games_data.get('games_count', 0) > 0:
|
2025-11-20 01:22:52 +03:00
|
|
|
|
total_games = games_data.get('games_count', 0)
|
|
|
|
|
|
has_games = True
|
2025-11-20 03:23:38 +03:00
|
|
|
|
logger.info(f"✅ Found {total_games} games via games_count field")
|
|
|
|
|
|
# Также проверяем data.total.games_played
|
|
|
|
|
|
elif games_data.get('data') and games_data.get('data', {}).get('total', {}).get('games_played', 0) > 0:
|
|
|
|
|
|
total_games = games_data.get('data', {}).get('total', {}).get('games_played', 0)
|
|
|
|
|
|
has_games = True
|
|
|
|
|
|
logger.info(f"✅ Found {total_games} games via data.total.games_played field")
|
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")
|
|
|
|
|
|
|
|
|
|
|
|
# Всегда обновляем время начала на текущее время после проверки (независимо от наличия активности)
|
|
|
|
|
|
self.period_start_times[task_key] = now
|
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
|
|
|
|
|
|
|
|
|
|
|
|
# Ждем перед повторной попыткой при ошибке
|
|
|
|
|
|
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-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-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()
|