add statistics

This commit is contained in:
vrubelroman 2025-11-13 13:32:46 +03:00
parent 23de80f94d
commit ceb62b408a
6 changed files with 318 additions and 3 deletions

View file

@ -11,6 +11,7 @@ from telegram.ext import Application, CommandHandler, ContextTypes
from config import ADMINPANEL_TELEGRAM_BOT_TOKEN, DATABASE_PATH from config import ADMINPANEL_TELEGRAM_BOT_TOKEN, DATABASE_PATH
from database import Database from database import Database
from message_counters import MessageCounters
# Configure logging # Configure logging
logging.basicConfig( logging.basicConfig(
@ -24,6 +25,7 @@ class AdminBot:
def __init__(self): def __init__(self):
self.db = Database() self.db = Database()
self.application = None self.application = None
self.counters = MessageCounters()
async def send_notification(self, message: str): async def send_notification(self, message: str):
"""Send notification to admin chat""" """Send notification to admin chat"""
@ -115,10 +117,30 @@ class AdminBot:
conn.close() 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 = ( message = (
f"📊 <b>Статистика базы данных</b>\n\n" f"📊 <b>Статистика базы данных</b>\n\n"
f"👥 Пользователей Telegram: {users_count}\n" f"👥 Пользователей Telegram: {users_count}\n"
f"🎮 Отслеживаемых игроков: {gamers_count}" f"🎮 Отслеживаемых игроков: {gamers_count}\n\n"
f"📨 <b>Счетчики сообщений</b>\n\n"
f"Всего отправлено: <b>{stats['total_all_time']}</b>\n"
f"Сегодня отправлено: <b>{stats['total_today']}</b>\n\n"
f"<b>По командам:</b>\n{counters_text}"
) )
await update.message.reply_text(message, parse_mode='HTML') await update.message.reply_text(message, parse_mode='HTML')
except Exception as e: except Exception as e:

View file

@ -20,6 +20,7 @@ from lichess_api import LichessAPI
from formatters import StatsFormatter from formatters import StatsFormatter
from i18n import t from i18n import t
from admin_bot import get_admin_bot, init_admin_bot from admin_bot import get_admin_bot, init_admin_bot
from message_counters import MessageCounters
# Configure logging # Configure logging
logging.basicConfig( logging.basicConfig(
@ -38,6 +39,7 @@ class LichessBot:
self.periodic_tasks = {} # Store periodic tasks self.periodic_tasks = {} # Store periodic tasks
self.period_start_times = {} # Store start times for each gamer self.period_start_times = {} # Store start times for each gamer
self.application = None # Will be set when application is created 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: def get_user_language_from_update(self, update: Update) -> str:
"""Always return English language""" """Always return English language"""
@ -65,9 +67,38 @@ class LichessBot:
# Start periodic task with user_id and gamer # Start periodic task with user_id and gamer
await self.start_periodic_task(gamer, user_id, gamer['period_minutes']) 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") 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: except Exception as e:
logger.error(f"Error starting existing periodic tasks: {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): async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Start command handler""" """Start command handler"""
@ -96,6 +127,7 @@ class LichessBot:
lang = self.get_user_language_from_update(update) lang = self.get_user_language_from_update(update)
start_msg = t('start_message', lang) start_msg = t('start_message', lang)
await update.message.reply_text(start_msg) await update.message.reply_text(start_msg)
self.counters.increment('start')
async def start_and_addgamer(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def start_and_addgamer(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Start command that shows welcome message and starts addgamer conversation""" """Start command that shows welcome message and starts addgamer conversation"""
@ -121,6 +153,7 @@ class LichessBot:
try: try:
await update.message.reply_text(t('addgamer_prompt', lang)) await update.message.reply_text(t('addgamer_prompt', lang))
logger.info(f"Addgamer prompt sent to user {update.effective_user.id}") logger.info(f"Addgamer prompt sent to user {update.effective_user.id}")
self.counters.increment('addgamer')
except Exception as e: except Exception as e:
logger.error(f"Error sending addgamer prompt: {e}") logger.error(f"Error sending addgamer prompt: {e}")
import traceback import traceback
@ -131,6 +164,7 @@ class LichessBot:
"""Start addtoken command - token required""" """Start addtoken command - token required"""
lang = self.get_user_language_from_update(update) lang = self.get_user_language_from_update(update)
await update.message.reply_text(t('addtoken_prompt', lang)) await update.message.reply_text(t('addtoken_prompt', lang))
self.counters.increment('addtoken')
return WAITING_FOR_TOKEN return WAITING_FOR_TOKEN
async def handle_token(self, update: Update, context: ContextTypes.DEFAULT_TYPE): 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) lang = self.get_user_language_from_update(update)
if not gamers: if not gamers:
await update.message.reply_text(t('no_gamers', lang)) await update.message.reply_text(t('no_gamers', lang))
self.counters.increment('getgamers')
return return
self.counters.increment('getgamers')
# Show loading message # Show loading message
loading_msg = await update.message.reply_text(t('loading_ratings', lang)) 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) lang = self.get_user_language_from_update(update)
if not gamers: if not gamers:
await update.message.reply_text(t('no_gamers_to_delete', lang)) await update.message.reply_text(t('no_gamers_to_delete', lang))
self.counters.increment('delgamer')
return return
self.counters.increment('delgamer')
# Show loading message # Show loading message
loading_msg = await update.message.reply_text(t('loading_gamers', lang)) loading_msg = await update.message.reply_text(t('loading_gamers', lang))
@ -563,6 +603,14 @@ class LichessBot:
# Format and send response # Format and send response
formatted_response = StatsFormatter.format_stats_response(data, username, period, lang) formatted_response = StatsFormatter.format_stats_response(data, username, period, lang)
await update.message.reply_text(formatted_response) 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): async def today(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Today command""" """Today command"""
@ -603,6 +651,7 @@ class LichessBot:
t('select_period', lang, username=active_gamer['username']), t('select_period', lang, username=active_gamer['username']),
reply_markup=reply_markup reply_markup=reply_markup
) )
self.counters.increment('setperiod')
async def select_period(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def select_period(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle period selection""" """Handle period selection"""
@ -668,6 +717,7 @@ class LichessBot:
f"Bot uses English language only." f"Bot uses English language only."
) )
await update.message.reply_text(message) await update.message.reply_text(message)
self.counters.increment('lang')
else: else:
await update.message.reply_text("❌ Failed to get user information") await update.message.reply_text("❌ Failed to get user information")
@ -689,6 +739,7 @@ class LichessBot:
await update.message.reply_text( await update.message.reply_text(
"✅ Language reset! Bot uses English language only." "✅ Language reset! Bot uses English language only."
) )
self.counters.increment('resetlang')
else: else:
await update.message.reply_text("❌ Failed to get user information") 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}") logger.info(f"Sent periodic notification for {gamer['username']} to user {user_id}")
# Обновляем время начала только после успешной отправки уведомления # Обновляем время начала только после успешной отправки уведомления
self.period_start_times[task_key] = now self.period_start_times[task_key] = now
# Increment periodic notification counter
self.counters.increment('periodic_notification')
except Exception as e: 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}")
# Не обновляем время начала при ошибке отправки # Не обновляем время начала при ошибке отправки

View file

@ -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() conn.commit()
# Migrate tokens from gamers to user_gamers if needed # Migrate tokens from gamers to user_gamers if needed

View file

@ -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']
}

View file

@ -1,7 +1,9 @@
from flask import Flask, jsonify, render_template from flask import Flask, jsonify, render_template
from flask_cors import CORS from flask_cors import CORS
import sqlite3 import sqlite3
from datetime import datetime import sys
import os
from datetime import datetime, date
app = Flask(__name__) app = Flask(__name__)
CORS(app) CORS(app)
@ -9,6 +11,14 @@ CORS(app)
# Путь к базе данных бота # Путь к базе данных бота
DB_PATH = "/app/data/lichess_bot.db" 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('/') @app.route('/')
def index(): def index():
"""Главная страница""" """Главная страница"""
@ -60,11 +70,32 @@ def get_users():
''') ''')
total_gamers = cursor.fetchone()[0] 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({ return jsonify({
'success': True, 'success': True,
'users': users, 'users': users,
'total_users': len(users), 'total_users': len(users),
'total_gamers': total_gamers 'total_gamers': total_gamers,
'message_stats': message_stats
}) })
except Exception as e: except Exception as e:

View file

@ -250,6 +250,20 @@
Кол-во игроков: <strong id="total-gamers">0</strong> Кол-во игроков: <strong id="total-gamers">0</strong>
</div> </div>
<div class="stats" style="margin-top: 15px; padding: 15px; background: #f5f5f5; border-radius: 8px;">
<h3 style="margin-top: 0; margin-bottom: 10px; font-size: 16px;">📨 Счетчики сообщений</h3>
<div style="margin-bottom: 8px;">
Всего отправлено: <strong id="total-messages-all">0</strong>
</div>
<div style="margin-bottom: 8px;">
Сегодня отправлено: <strong id="total-messages-today">0</strong>
</div>
<div id="message-stats-by-command" style="margin-top: 10px; font-size: 12px; color: #666;">
<div style="margin-bottom: 5px;"><strong>По командам:</strong></div>
<div id="command-stats-list"></div>
</div>
</div>
<div class="search-box"> <div class="search-box">
<input type="text" id="search-input" placeholder="Поиск по имени или никнейму..."> <input type="text" id="search-input" placeholder="Поиск по имени или никнейму...">
</div> </div>
@ -292,6 +306,45 @@
users = data.users; users = data.users;
document.getElementById('total-users').textContent = data.total_users; document.getElementById('total-users').textContent = data.total_users;
document.getElementById('total-gamers').textContent = data.total_gamers; 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 `<div style="margin-bottom: 3px;">
${cmdName}: <strong>${stats.total}</strong> (сегодня: <strong>${stats.today}</strong>)
</div>`;
}).join('');
} else {
commandStatsList.innerHTML = '<div style="color: #999;">Нет данных</div>';
}
}
filteredUsers = users; filteredUsers = users;
renderUsers(); renderUsers();