From 0e66a05b90576db24c9f55ccc1b31d255405cac8 Mon Sep 17 00:00:00 2001 From: vrubelroman Date: Sun, 16 Nov 2025 23:36:57 +0300 Subject: [PATCH] delete activ status --- LichessClientTG_bot/bot.py | 303 +++++++++++++++++++--------- LichessClientTG_bot/config.py | 2 +- LichessClientTG_bot/i18n.py | 1 + LichessWebView/app.py | 10 +- LichessWebView/templates/index.html | 9 +- docker-compose.yml | 1 + 6 files changed, 218 insertions(+), 108 deletions(-) diff --git a/LichessClientTG_bot/bot.py b/LichessClientTG_bot/bot.py index 208e896..cf52ac1 100644 --- a/LichessClientTG_bot/bot.py +++ b/LichessClientTG_bot/bot.py @@ -387,7 +387,6 @@ class LichessBot: for i, gamer in enumerate(gamers): try: logger.info(f"Processing gamer {i+1}/{len(gamers)}: {gamer['username']} (ID: {gamer['id']})") - status = "🟢" if gamer['is_active'] else "⚪" username = gamer['username'] # Get user ratings from Lichess API @@ -414,7 +413,6 @@ class LichessBot: gamers_data.append({ 'id': gamer['id'], - 'status': status, 'username': username, 'bullet': bullet_rating, 'blitz': blitz_rating, @@ -432,7 +430,6 @@ class LichessBot: period_text = f" · {period_minutes}{period_suffix}" if period_minutes > 0 else "" gamers_data.append({ 'id': gamer['id'], - 'status': "🟢" if gamer['is_active'] else "⚪", 'username': gamer['username'], 'bullet': 'N/A', 'blitz': 'N/A', @@ -445,7 +442,7 @@ class LichessBot: text_lines = [] for gamer in gamers_data: text_lines.append( - f"{gamer['status']} {gamer['username']} " + f"{gamer['username']} " f"⚡ {gamer['bullet']} 🔥 {gamer['blitz']} 🐇 {gamer['rapid']}{gamer['period']}" ) @@ -454,24 +451,11 @@ class LichessBot: logger.info(f"getgamers: message length: {len(gamers_text)} characters") - # Create simple keyboard with just usernames - keyboard = [] - for gamer in gamers_data: - keyboard.append([InlineKeyboardButton( - text=f"{gamer['status']} {gamer['username']}", - callback_data=f"select_{gamer['id']}" - )]) - logger.debug(f"Added button for {gamer['username']} (ID: {gamer['id']})") - - logger.info(f"getgamers: created {len(keyboard)} buttons in keyboard") - reply_markup = InlineKeyboardMarkup(keyboard) - - # Edit the loading message with the results + # Edit the loading message with the results (no keyboard) try: await loading_msg.edit_text( gamers_text, - parse_mode='HTML', - reply_markup=reply_markup + parse_mode='HTML' ) except Exception as e: logger.error(f"Error editing message: {e}") @@ -482,8 +466,7 @@ class LichessBot: pass await update.message.reply_text( gamers_text, - parse_mode='HTML', - reply_markup=reply_markup + parse_mode='HTML' ) async def select_gamer(self, update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -835,19 +818,90 @@ class LichessBot: self.counters.increment('last_year_1000') async def setperiod(self, update: Update, context: ContextTypes.DEFAULT_TYPE): - """Set period command""" + """Set period command - first select gamer, then select period""" user_id = update.effective_user.id - # Get active gamer for this user - active_gamer = self.db.get_user_active_gamer(user_id) + # Get all gamers for this user + gamers = self.db.get_user_gamers(user_id) lang = self.get_user_language_from_update(update) - if not active_gamer: - await update.message.reply_text( - t('no_active_gamer', lang) - ) + if not gamers: + await update.message.reply_text(t('no_gamers', lang)) + self.counters.increment('setperiod') return + # 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 keyboard = [] for period in PERIOD_OPTIONS: if period == 0: @@ -861,14 +915,17 @@ class LichessBot: else: hours = period // 60 button_text = f"⏰ {hours} hours" - keyboard.append([InlineKeyboardButton(button_text, callback_data=f"period_{period}")]) + keyboard.append([InlineKeyboardButton( + button_text, + callback_data=f"period_{gamer_id}_{period}" + )]) reply_markup = InlineKeyboardMarkup(keyboard) - await update.message.reply_text( - t('select_period', lang, username=active_gamer['username']), - reply_markup=reply_markup + await query.edit_message_text( + t('select_period', lang, username=selected_gamer['username']), + reply_markup=reply_markup, + parse_mode='HTML' ) - self.counters.increment('setperiod') async def select_period(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """Handle period selection""" @@ -876,10 +933,22 @@ class LichessBot: await query.answer() user_id = query.from_user.id - period = int(query.data.split('_')[1]) + # Parse callback data: period_{gamer_id}_{period} + parts = query.data.split('_') + gamer_id = int(parts[1]) + period = int(parts[2]) - # Get active gamer for this user - active_gamer = self.db.get_user_active_gamer(user_id) + # 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: @@ -891,29 +960,29 @@ class LichessBot: language_code=update.effective_user.language_code ) lang = self.db.get_user_language(user_id) - if active_gamer: - # Set period for this user-gamer pair - self.db.set_user_gamer_period(user_id, active_gamer['id'], period) - - if period == 0: - await query.edit_message_text( - t('notifications_disabled', lang, username=active_gamer['username']) - ) + + # 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" else: - # Format period text for confirmation message - if period < 60: - period_text = f"{period} minutes" - elif period == 60: - period_text = "1 hour" - else: - hours = period // 60 - period_text = f"{hours} hours" - await query.edit_message_text( - f"✅ Period {period_text} set for {active_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(active_gamer, user_id, period) + 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) async def check_language(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """Check and display current language settings""" @@ -996,8 +1065,35 @@ class LichessBot: self.period_start_times[task_key] = start_time logger.info(f"Started periodic monitoring for {gamer['username']} with {period_minutes} minute intervals") + consecutive_errors = 0 + max_consecutive_errors = 5 + while True: try: + # Проверяем, что период все еще установлен в БД + 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 + # Ждем заданное количество минут await asyncio.sleep(period_minutes * 60) @@ -1012,34 +1108,44 @@ class LichessBot: logger.info(f"Checking period for {gamer['username']}: {period_start} to {now}") logger.info(f"Unix timestamps: since={since_timestamp}, until={until_timestamp}") - # Делаем запросы к API - games_url = f"{LICHESS_STATS_API_BASE_URL}/games/{gamer['username']}/period?since={since_timestamp}&until={until_timestamp}" - logger.info(f"🎮 GAMES API REQUEST: {games_url}") - - games_data = await self.lichess_api.get_games_period( - gamer['username'], since_timestamp, until_timestamp - ) - logger.info(f"Games API response: {games_data}") - + # Делаем запросы к API с обработкой ошибок + games_data = None puzzles_data = None - if gamer['token']: - puzzles_url = f"{LICHESS_STATS_API_BASE_URL}/puzzle/period?since={since_timestamp}&until={until_timestamp}&max=150" - logger.info(f"🧩 PUZZLES API REQUEST: {puzzles_url}") - - puzzles_data = await self.lichess_api.get_puzzles_period( - gamer['token'], since_timestamp, until_timestamp, max_puzzles=150 + + try: + games_data = await self.lichess_api.get_games_period( + gamer['username'], since_timestamp, until_timestamp ) - logger.info(f"Puzzles API response: {puzzles_data}") - else: - logger.info(f"No token for {gamer['username']}, skipping puzzles API call") + logger.info(f"Games API response received for {gamer['username']}") + except Exception as e: + logger.error(f"Error getting games data for {gamer['username']}: {e}") + 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: + puzzles_data = await self.lichess_api.get_puzzles_period( + gamer['token'], since_timestamp, until_timestamp, max_puzzles=150 + ) + logger.info(f"Puzzles API response received for {gamer['username']}") + except Exception as e: + logger.warning(f"Error getting puzzles data for {gamer['username']}: {e}") + # Продолжаем без данных по пазлам + + # Сбрасываем счетчик ошибок при успешном запросе + consecutive_errors = 0 # Проверяем наличие реальной активности has_games = False total_games = 0 - total_losses = 0 if games_data and games_data.get('data'): total_games = games_data.get('data', {}).get('total', {}).get('games_played', 0) - total_losses = games_data.get('data', {}).get('total', {}).get('losses', 0) has_games = total_games > 0 has_puzzles = False @@ -1047,15 +1153,7 @@ class LichessBot: total_puzzles = puzzles_data.get('data', {}).get('total_attempts', 0) has_puzzles = total_puzzles > 0 - # Детальное логирование для отладки logger.info(f"Activity check for {gamer['username']}: has_games={has_games}, has_puzzles={has_puzzles}") - if games_data and games_data.get('data'): - total_games = games_data.get('data', {}).get('total', {}).get('games_played', 0) - total_losses = games_data.get('data', {}).get('total', {}).get('losses', 0) - logger.info(f"Games data: total_games={total_games}, total_losses={total_losses}") - if puzzles_data and puzzles_data.get('data'): - total_puzzles = puzzles_data.get('data', {}).get('total_attempts', 0) - logger.info(f"Puzzles data: total_attempts={total_puzzles}") # Отправляем уведомление только если есть реальная активность if has_games or has_puzzles: @@ -1072,30 +1170,50 @@ class LichessBot: chat_id=user_id, text=notification ) - logger.info(f"Sent periodic notification for {gamer['username']} to user {user_id}") + logger.info(f"✅ Sent periodic notification for {gamer['username']} to user {user_id}") # Обновляем время начала только после успешной отправки уведомления self.period_start_times[task_key] = now # Increment periodic notification counter self.counters.increment('periodic_notification') except Exception as e: - logger.error(f"Failed to send notification to user {user_id}: {e}") - # Не обновляем время начала при ошибке отправки + logger.error(f"❌ Failed to send notification to user {user_id}: {e}") + import traceback + logger.error(f"Traceback: {traceback.format_exc()}") + # Обновляем время начала даже при ошибке отправки, чтобы не зацикливаться + self.period_start_times[task_key] = now + else: + logger.error(f"Application not initialized, cannot send notification for {gamer['username']}") + self.period_start_times[task_key] = now except Exception as e: logger.error(f"Error formatting notification for {gamer['username']}: {e}") import traceback logger.error(f"Traceback: {traceback.format_exc()}") - # Не обновляем время начала при ошибке форматирования + # Обновляем время начала даже при ошибке форматирования + self.period_start_times[task_key] = now else: logger.info(f"No activity found for {gamer['username']} in the last {period_minutes} minutes") # Обновляем время начала даже если нет активности, чтобы не зацикливаться self.period_start_times[task_key] = now except asyncio.CancelledError: + logger.info(f"Periodic check cancelled for {gamer['username']}") break except Exception as e: - logger.error(f"Error in periodic check: {e}") + consecutive_errors += 1 + logger.error(f"Error in periodic check for {gamer['username']}: {e}") import traceback logger.error(f"Full traceback: {traceback.format_exc()}") + + 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 def setup_handlers(self, application: Application): """Setup all handlers""" @@ -1127,7 +1245,8 @@ class LichessBot: application.add_handler(CommandHandler("resetlang", self.reset_language)) application.add_handler(CommandHandler("test_admin_notify", self.test_admin_notify)) - # Callback handlers + # Callback handlers (order matters - more specific patterns first) + application.add_handler(CallbackQueryHandler(self.select_gamer_for_period, pattern="^select_gamer_period_")) application.add_handler(CallbackQueryHandler(self.select_gamer, pattern="^select_")) application.add_handler(CallbackQueryHandler(self.handle_delete_gamer, pattern="^delete_")) application.add_handler(CallbackQueryHandler(self.select_period, pattern="^period_")) diff --git a/LichessClientTG_bot/config.py b/LichessClientTG_bot/config.py index c2a254b..45678e4 100644 --- a/LichessClientTG_bot/config.py +++ b/LichessClientTG_bot/config.py @@ -49,7 +49,7 @@ DATABASE_PATH = _resolve_database_path() PERIOD_OPTIONS = [0, 15, 30, 60, 120, 180, 360, 720, 1440] # minutes (0=disable, then: 15min, 30min, 1h, 2h, 3h, 6h, 12h, 24h) # Bot Version -BOT_VERSION = "1.1.1" +BOT_VERSION = "1.2.0" # Telegram Bot Long Polling Configuration POLL_INTERVAL = 1.0 # seconds diff --git a/LichessClientTG_bot/i18n.py b/LichessClientTG_bot/i18n.py index e103122..3f3f870 100644 --- a/LichessClientTG_bot/i18n.py +++ b/LichessClientTG_bot/i18n.py @@ -80,6 +80,7 @@ TRANSLATIONS = { 'games_section': "{emoji} {game_type} — {games_count} games • {rating_change}\nRating: {rating}\n✅ Wins: {wins}\n❌ Losses: {losses}\n🤝 Draws: {draws}\n\n", # Set period + 'select_gamer_for_period': "⏱️ Select player to set notification period:\n\n", 'select_period': "⏱️ Select period for player {username}:\n📱 Notifications will be sent to personal messages", 'notifications_disabled': "✅ Notifications disabled for {username}", 'period_set': "✅ Period {period} minutes set for {username}\n📱 Notifications will be sent to personal messages", diff --git a/LichessWebView/app.py b/LichessWebView/app.py index a1200bc..dc2e138 100644 --- a/LichessWebView/app.py +++ b/LichessWebView/app.py @@ -118,7 +118,6 @@ def get_users(): tu.last_name, tu.created_at, COUNT(ug.id) as gamer_count, - SUM(CASE WHEN ug.is_active = 1 THEN 1 ELSE 0 END) as active_gamers, SUM(CASE WHEN ug.period_minutes > 0 THEN 1 ELSE 0 END) as monitored_gamers FROM telegram_users tu LEFT JOIN user_gamers ug ON tu.user_id = ug.user_id @@ -137,8 +136,7 @@ def get_users(): 'last_name': row[3], 'created_at': row[4], 'gamer_count': row[5], - 'active_gamers': row[6], - 'monitored_gamers': row[7] + 'monitored_gamers': row[6] }) # Получаем общее количество игроков (уникальных) @@ -187,7 +185,6 @@ def get_user_gamers(user_id): g.id, g.username, ug.token, - ug.is_active, ug.period_minutes, ug.created_at FROM user_gamers ug @@ -204,9 +201,8 @@ def get_user_gamers(user_id): 'id': row[0], 'username': row[1], 'has_token': bool(row[2]), # Token from user_gamers, not gamers - 'is_active': bool(row[3]), - 'period_minutes': row[4], - 'created_at': row[5] + 'period_minutes': row[3], + 'created_at': row[4] }) return jsonify({ diff --git a/LichessWebView/templates/index.html b/LichessWebView/templates/index.html index ce79d6b..a5b8469 100644 --- a/LichessWebView/templates/index.html +++ b/LichessWebView/templates/index.html @@ -387,8 +387,7 @@
@${escapeHtml(user.username)} • ID: ${user.user_id}
Добавлен: ${formatDate(user.created_at)}
- 📊 ${user.gamer_count} игроков - ✅ ${user.active_gamers} активен +📊 ${user.gamer_count} игроков ⏰ ${user.monitored_gamers} с уведомл.
@@ -447,7 +446,6 @@ Игрок - Статус Токен Период уведомлений Добавлен @@ -457,11 +455,6 @@ ${gamers.map(gamer => ` ${escapeHtml(gamer.username)} (ID: ${gamer.id}) - - - ${gamer.is_active ? '✅ АКТИВЕН' : '⚪'} - - ${gamer.has_token ? '🔑 Есть' : '❌ Нет'} diff --git a/docker-compose.yml b/docker-compose.yml index c698cac..fedca0c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -62,6 +62,7 @@ services: - "5000:5000" volumes: - ./LichessClientTG_bot/data:/app/data:ro + - ./LichessWebView:/app restart: always depends_on: - lichess-bot