diff --git a/LichessClientTG_bot/admin_bot.py b/LichessClientTG_bot/admin_bot.py index 9e9514b..6fe69b9 100644 --- a/LichessClientTG_bot/admin_bot.py +++ b/LichessClientTG_bot/admin_bot.py @@ -11,6 +11,7 @@ from telegram.ext import Application, CommandHandler, ContextTypes from config import ADMINPANEL_TELEGRAM_BOT_TOKEN, DATABASE_PATH from database import Database +from message_counters import MessageCounters # Configure logging logging.basicConfig( @@ -24,6 +25,7 @@ class AdminBot: def __init__(self): self.db = Database() self.application = None + self.counters = MessageCounters() async def send_notification(self, message: str): """Send notification to admin chat""" @@ -115,10 +117,30 @@ class AdminBot: conn.close() + # Get message counters statistics + stats = self.counters.get_stats_summary() + + # Filter out commands that should not be displayed + excluded_commands = {'lang', 'resetlang', 'start'} + filtered_stats = { + cmd: data for cmd, data in stats['by_command'].items() + if cmd not in excluded_commands + } + + # Format message counters + counters_text = "\n".join([ + f" • {cmd}: {data['total']} (сегодня: {data['today']})" + for cmd, data in sorted(filtered_stats.items()) + ]) + message = ( f"📊 Статистика базы данных\n\n" f"👥 Пользователей Telegram: {users_count}\n" - f"🎮 Отслеживаемых игроков: {gamers_count}" + f"🎮 Отслеживаемых игроков: {gamers_count}\n\n" + f"📨 Счетчики сообщений\n\n" + f"Всего отправлено: {stats['total_all_time']}\n" + f"Сегодня отправлено: {stats['total_today']}\n\n" + f"По командам:\n{counters_text}" ) await update.message.reply_text(message, parse_mode='HTML') except Exception as e: diff --git a/LichessClientTG_bot/bot.py b/LichessClientTG_bot/bot.py index 70f697d..80d6cf0 100644 --- a/LichessClientTG_bot/bot.py +++ b/LichessClientTG_bot/bot.py @@ -20,6 +20,7 @@ from lichess_api import LichessAPI from formatters import StatsFormatter from i18n import t from admin_bot import get_admin_bot, init_admin_bot +from message_counters import MessageCounters # Configure logging logging.basicConfig( @@ -38,6 +39,7 @@ class LichessBot: self.periodic_tasks = {} # Store periodic tasks self.period_start_times = {} # Store start times for each gamer self.application = None # Will be set when application is created + self.counters = MessageCounters() # Message counters def get_user_language_from_update(self, update: Update) -> str: """Always return English language""" @@ -65,9 +67,38 @@ class LichessBot: # Start periodic task with user_id and gamer await self.start_periodic_task(gamer, user_id, gamer['period_minutes']) logger.info(f"Started periodic task for {gamer['username']} (user {user_id}) with period {gamer['period_minutes']} minutes") + + # Start daily counter reset task + asyncio.create_task(self.daily_counter_reset_task()) + logger.info("Started daily counter reset task") except Exception as e: logger.error(f"Error starting existing periodic tasks: {e}") + + 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) async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """Start command handler""" @@ -96,6 +127,7 @@ class LichessBot: lang = self.get_user_language_from_update(update) start_msg = t('start_message', lang) await update.message.reply_text(start_msg) + self.counters.increment('start') async def start_and_addgamer(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """Start command that shows welcome message and starts addgamer conversation""" @@ -121,6 +153,7 @@ class LichessBot: try: await update.message.reply_text(t('addgamer_prompt', lang)) logger.info(f"Addgamer prompt sent to user {update.effective_user.id}") + self.counters.increment('addgamer') except Exception as e: logger.error(f"Error sending addgamer prompt: {e}") import traceback @@ -131,6 +164,7 @@ class LichessBot: """Start addtoken command - token required""" lang = self.get_user_language_from_update(update) await update.message.reply_text(t('addtoken_prompt', lang)) + self.counters.increment('addtoken') return WAITING_FOR_TOKEN async def handle_token(self, update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -283,8 +317,11 @@ class LichessBot: lang = self.get_user_language_from_update(update) if not gamers: await update.message.reply_text(t('no_gamers', lang)) + self.counters.increment('getgamers') return + self.counters.increment('getgamers') + # Show loading message loading_msg = await update.message.reply_text(t('loading_ratings', lang)) @@ -431,8 +468,11 @@ class LichessBot: lang = self.get_user_language_from_update(update) if not gamers: await update.message.reply_text(t('no_gamers_to_delete', lang)) + self.counters.increment('delgamer') return + self.counters.increment('delgamer') + # Show loading message loading_msg = await update.message.reply_text(t('loading_gamers', lang)) @@ -563,6 +603,14 @@ class LichessBot: # Format and send response formatted_response = StatsFormatter.format_stats_response(data, username, period, lang) await update.message.reply_text(formatted_response) + + # 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') async def today(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """Today command""" @@ -603,6 +651,7 @@ class LichessBot: t('select_period', lang, username=active_gamer['username']), reply_markup=reply_markup ) + self.counters.increment('setperiod') async def select_period(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """Handle period selection""" @@ -668,6 +717,7 @@ class LichessBot: f"Bot uses English language only." ) await update.message.reply_text(message) + self.counters.increment('lang') else: await update.message.reply_text("❌ Failed to get user information") @@ -689,6 +739,7 @@ class LichessBot: await update.message.reply_text( "✅ Language reset! Bot uses English language only." ) + self.counters.increment('resetlang') else: await update.message.reply_text("❌ Failed to get user information") @@ -799,6 +850,8 @@ class LichessBot: 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}") # Не обновляем время начала при ошибке отправки diff --git a/LichessClientTG_bot/database.py b/LichessClientTG_bot/database.py index cde266b..40f8d54 100644 --- a/LichessClientTG_bot/database.py +++ b/LichessClientTG_bot/database.py @@ -76,6 +76,26 @@ class Database: ) ''') + # Create message_counters table for tracking sent messages + cursor.execute(''' + CREATE TABLE IF NOT EXISTS message_counters ( + command TEXT PRIMARY KEY, + total_count INTEGER DEFAULT 0, + today_count INTEGER DEFAULT 0, + last_reset_date DATE DEFAULT CURRENT_DATE + ) + ''') + + # Initialize counters for all commands + commands = ['start', 'addgamer', 'addtoken', 'getgamers', 'delgamer', + 'today', 'yesterday', 'week', 'setperiod', 'lang', 'resetlang', + 'periodic_notification'] + for cmd in commands: + cursor.execute(''' + INSERT OR IGNORE INTO message_counters (command, total_count, today_count, last_reset_date) + VALUES (?, 0, 0, CURRENT_DATE) + ''', (cmd,)) + conn.commit() # Migrate tokens from gamers to user_gamers if needed diff --git a/LichessClientTG_bot/message_counters.py b/LichessClientTG_bot/message_counters.py new file mode 100644 index 0000000..303e976 --- /dev/null +++ b/LichessClientTG_bot/message_counters.py @@ -0,0 +1,136 @@ +""" +Module for tracking message counters in Telegram bot +""" +import sqlite3 +import logging +from datetime import datetime, date +from typing import Dict, Any +from config import DATABASE_PATH + +logger = logging.getLogger(__name__) + + +class MessageCounters: + """Class for managing message counters""" + + def __init__(self, db_path: str = DATABASE_PATH): + self.db_path = db_path + self._ensure_counters_exist() + self._reset_daily_counters_if_needed() + + def _ensure_counters_exist(self): + """Ensure all command counters exist in database""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + commands = ['start', 'addgamer', 'addtoken', 'getgamers', 'delgamer', + 'today', 'yesterday', 'week', 'setperiod', 'lang', 'resetlang', + 'periodic_notification'] + for cmd in commands: + cursor.execute(''' + INSERT OR IGNORE INTO message_counters (command, total_count, today_count, last_reset_date) + VALUES (?, 0, 0, CURRENT_DATE) + ''', (cmd,)) + conn.commit() + + def _reset_daily_counters_if_needed(self): + """Reset daily counters if it's a new day""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + today = date.today() + + # Check all counters and reset if needed + cursor.execute(''' + SELECT command, last_reset_date FROM message_counters + ''') + + for row in cursor.fetchall(): + cmd, last_reset = row[0], row[1] + if last_reset: + try: + last_reset_date = datetime.strptime(last_reset, '%Y-%m-%d').date() + if last_reset_date < today: + cursor.execute(''' + UPDATE message_counters + SET today_count = 0, last_reset_date = ? + WHERE command = ? + ''', (today.isoformat(), cmd)) + except (ValueError, TypeError): + # Invalid date format, reset it + cursor.execute(''' + UPDATE message_counters + SET today_count = 0, last_reset_date = ? + WHERE command = ? + ''', (today.isoformat(), cmd)) + else: + # No reset date, set it to today + cursor.execute(''' + UPDATE message_counters + SET last_reset_date = ? + WHERE command = ? + ''', (today.isoformat(), cmd)) + + conn.commit() + + def increment(self, command: str): + """Increment counter for a command""" + self._reset_daily_counters_if_needed() + + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + + # Ensure counter exists + cursor.execute(''' + INSERT OR IGNORE INTO message_counters (command, total_count, today_count, last_reset_date) + VALUES (?, 0, 0, CURRENT_DATE) + ''', (command,)) + + # Increment both counters + cursor.execute(''' + UPDATE message_counters + SET total_count = total_count + 1, + today_count = today_count + 1 + WHERE command = ? + ''', (command,)) + + conn.commit() + + def get_all_stats(self) -> Dict[str, Any]: + """Get all statistics""" + self._reset_daily_counters_if_needed() + + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + + cursor.execute(''' + SELECT command, total_count, today_count + FROM message_counters + ORDER BY command + ''') + + stats = { + 'by_command': {}, + 'total_all_time': 0, + 'total_today': 0 + } + + for row in cursor.fetchall(): + cmd, total, today = row[0], row[1], row[2] + stats['by_command'][cmd] = { + 'total': total, + 'today': today + } + stats['total_all_time'] += total + stats['total_today'] += today + + return stats + + def get_stats_summary(self) -> Dict[str, Any]: + """Get summary statistics for display""" + stats = self.get_all_stats() + + return { + 'total_all_time': stats['total_all_time'], + 'total_today': stats['total_today'], + 'by_command': stats['by_command'] + } + diff --git a/LichessWebView/app.py b/LichessWebView/app.py index 3e73d85..2d21522 100644 --- a/LichessWebView/app.py +++ b/LichessWebView/app.py @@ -1,7 +1,9 @@ from flask import Flask, jsonify, render_template from flask_cors import CORS import sqlite3 -from datetime import datetime +import sys +import os +from datetime import datetime, date app = Flask(__name__) CORS(app) @@ -9,6 +11,14 @@ CORS(app) # Путь к базе данных бота DB_PATH = "/app/data/lichess_bot.db" +# Add parent directory to path to import message_counters +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'LichessClientTG_bot')) +try: + from message_counters import MessageCounters +except ImportError: + # Fallback if import fails + MessageCounters = None + @app.route('/') def index(): """Главная страница""" @@ -60,11 +70,32 @@ def get_users(): ''') total_gamers = cursor.fetchone()[0] + # Get message counters statistics + message_stats = {} + if MessageCounters: + try: + counters = MessageCounters(db_path=DB_PATH) + message_stats = counters.get_stats_summary() + except Exception as e: + print(f"Error getting message counters: {e}") + message_stats = { + 'total_all_time': 0, + 'total_today': 0, + 'by_command': {} + } + else: + message_stats = { + 'total_all_time': 0, + 'total_today': 0, + 'by_command': {} + } + return jsonify({ 'success': True, 'users': users, 'total_users': len(users), - 'total_gamers': total_gamers + 'total_gamers': total_gamers, + 'message_stats': message_stats }) except Exception as e: diff --git a/LichessWebView/templates/index.html b/LichessWebView/templates/index.html index 30032f1..b651e5a 100644 --- a/LichessWebView/templates/index.html +++ b/LichessWebView/templates/index.html @@ -250,6 +250,20 @@ Кол-во игроков: 0 +
+

📨 Счетчики сообщений

+
+ Всего отправлено: 0 +
+
+ Сегодня отправлено: 0 +
+
+
По командам:
+
+
+
+ @@ -292,6 +306,45 @@ users = data.users; document.getElementById('total-users').textContent = data.total_users; document.getElementById('total-gamers').textContent = data.total_gamers; + + // Update message counters + if (data.message_stats) { + document.getElementById('total-messages-all').textContent = data.message_stats.total_all_time || 0; + document.getElementById('total-messages-today').textContent = data.message_stats.total_today || 0; + + // Render command stats + const commandStatsList = document.getElementById('command-stats-list'); + if (data.message_stats.by_command && Object.keys(data.message_stats.by_command).length > 0) { + const commandNames = { + 'addgamer': 'Add Gamer', + 'addtoken': 'Add Token', + 'getgamers': 'Get Gamers', + 'delgamer': 'Del Gamer', + 'today': 'Today', + 'yesterday': 'Yesterday', + 'week': 'Week', + 'setperiod': 'Set Period', + 'periodic_notification': 'Periodic Notifications' + }; + + // Filter out excluded commands + const excludedCommands = ['start', 'lang', 'resetlang']; + const filteredCommands = Object.entries(data.message_stats.by_command) + .filter(([cmd]) => !excludedCommands.includes(cmd)); + + commandStatsList.innerHTML = filteredCommands + .sort((a, b) => b[1].total - a[1].total) + .map(([cmd, stats]) => { + const cmdName = commandNames[cmd] || cmd; + return `
+ ${cmdName}: ${stats.total} (сегодня: ${stats.today}) +
`; + }).join(''); + } else { + commandStatsList.innerHTML = '
Нет данных
'; + } + } + filteredUsers = users; renderUsers();