From 23de80f94d16a0660c62df632d19d436eaa26166 Mon Sep 17 00:00:00 2001 From: vrubelroman Date: Thu, 13 Nov 2025 01:00:48 +0300 Subject: [PATCH] admin panel --- LichessClientTG_bot/Dockerfile.admin | 30 ++++ LichessClientTG_bot/admin_bot.py | 223 +++++++++++++++++++++++++++ LichessClientTG_bot/bot.py | 135 +++++++++++++--- LichessClientTG_bot/config.py | 5 +- LichessClientTG_bot/database.py | 33 ++++ docker-compose.yml | 18 +++ 6 files changed, 424 insertions(+), 20 deletions(-) create mode 100644 LichessClientTG_bot/Dockerfile.admin create mode 100644 LichessClientTG_bot/admin_bot.py diff --git a/LichessClientTG_bot/Dockerfile.admin b/LichessClientTG_bot/Dockerfile.admin new file mode 100644 index 0000000..cd6da65 --- /dev/null +++ b/LichessClientTG_bot/Dockerfile.admin @@ -0,0 +1,30 @@ +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first for better caching +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Create directory for database +RUN mkdir -p /app/data + +# Set environment variables +ENV PYTHONPATH=/app +ENV PYTHONUNBUFFERED=1 + +# Run the admin bot +CMD ["python", "admin_bot.py"] + + diff --git a/LichessClientTG_bot/admin_bot.py b/LichessClientTG_bot/admin_bot.py new file mode 100644 index 0000000..9e9514b --- /dev/null +++ b/LichessClientTG_bot/admin_bot.py @@ -0,0 +1,223 @@ +""" +Admin Panel Telegram Bot +Sends notifications about new users and new tracked players +""" + +import logging +from typing import Optional + +from telegram import Update +from telegram.ext import Application, CommandHandler, ContextTypes + +from config import ADMINPANEL_TELEGRAM_BOT_TOKEN, DATABASE_PATH +from database import Database + +# Configure logging +logging.basicConfig( + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO +) +logger = logging.getLogger(__name__) + + +class AdminBot: + def __init__(self): + self.db = Database() + self.application = None + + async def send_notification(self, message: str): + """Send notification to admin chat""" + admin_chat_id = self.get_admin_chat_id() + if not admin_chat_id: + logger.warning("Admin chat ID not set, cannot send notification") + return + + # Try to use application if available (when running as separate service) + if self.application: + try: + await self.application.bot.send_message( + chat_id=admin_chat_id, + text=message, + parse_mode='HTML' + ) + logger.debug(f"Notification sent via application to chat {admin_chat_id}") + return + except Exception as e: + logger.warning(f"Failed to send via application: {e}, trying direct API call") + + # Fallback: use direct Telegram API call + try: + import aiohttp + url = f"https://api.telegram.org/bot{ADMINPANEL_TELEGRAM_BOT_TOKEN}/sendMessage" + 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.debug(f"Notification sent via direct API to chat {admin_chat_id}") + else: + error_text = await response.text() + logger.error(f"Failed to send 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()) + + def get_admin_chat_id(self) -> Optional[int]: + """Get admin chat ID from database""" + return self.db.get_admin_chat_id() + + def set_admin_chat_id(self, chat_id: int): + """Set admin chat ID in database""" + self.db.set_admin_chat_id(chat_id) + + async def notify_new_user(self, user_id: int, username: Optional[str], first_name: Optional[str]): + """Send notification about new Telegram user""" + username_text = f"@{username}" if username else "без username" + name_text = first_name if first_name else "без имени" + + message = ( + f"🆕 Новый пользователь Telegram\n\n" + f"ID: {user_id}\n" + f"Username: {username_text}\n" + f"Имя: {name_text}" + ) + await self.send_notification(message) + + async def notify_new_player(self, player_username: str, added_by_user_id: int, added_by_username: Optional[str]): + """Send notification about new tracked player""" + 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"🎮 Добавлен новый игрок для отслеживания\n\n" + f"Игрок: {player_username}\n" + f"Добавил: {added_by_text}" + ) + await self.send_notification(message) + + async def status(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """Status command - show statistics""" + try: + import sqlite3 + conn = sqlite3.connect(self.db.db_path) + cursor = conn.cursor() + + # Count users + cursor.execute("SELECT COUNT(*) FROM telegram_users") + users_count = cursor.fetchone()[0] + + # Count unique gamers + cursor.execute("SELECT COUNT(DISTINCT username) FROM gamers") + gamers_count = cursor.fetchone()[0] + + conn.close() + + message = ( + f"📊 Статистика базы данных\n\n" + f"👥 Пользователей Telegram: {users_count}\n" + f"🎮 Отслеживаемых игроков: {gamers_count}" + ) + await update.message.reply_text(message, parse_mode='HTML') + except Exception as e: + logger.error(f"Error getting status: {e}") + await update.message.reply_text(f"❌ Ошибка получения статистики: {e}") + + async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """Start command - register admin chat""" + try: + chat_id = update.effective_chat.id + self.set_admin_chat_id(chat_id) + + await update.message.reply_text( + "✅ Админ-панель активирована!\n\n" + "Доступные команды:\n" + "/status - Показать статистику" + ) + except Exception as e: + logger.error(f"Error in start command: {e}") + import traceback + logger.error(traceback.format_exc()) + try: + await update.message.reply_text(f"Ошибка: {e}") + except: + pass + + def setup_handlers(self, application: Application): + """Setup all handlers""" + self.application = application # Store application reference + + # Add start command handler + application.add_handler(CommandHandler("start", self.start)) + + # Add status command handler + application.add_handler(CommandHandler("status", self.status)) + + +# Global admin bot instance +_admin_bot_instance: Optional[AdminBot] = None + + +def get_admin_bot() -> Optional[AdminBot]: + """Get global admin bot instance""" + return _admin_bot_instance + + +def init_admin_bot(): + """Initialize admin bot""" + global _admin_bot_instance + if not _admin_bot_instance: + _admin_bot_instance = AdminBot() + return _admin_bot_instance + + +def main(): + """Main function""" + admin_bot = init_admin_bot() + + # Create application with Long Polling configuration + application = Application.builder().token(ADMINPANEL_TELEGRAM_BOT_TOKEN).build() + + # Setup handlers + admin_bot.setup_handlers(application) + + # Set application reference + admin_bot.application = application + + # 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) + + # Add post_init hook to log bot info + async def post_init(app: Application) -> None: + bot_info = await app.bot.get_me() + logger.info(f"Admin bot initialized: @{bot_info.username} (ID: {bot_info.id})") + + application.post_init = post_init + + # Start the bot with Long Polling + logger.info("Starting Admin Panel Bot with Long Polling...") + try: + application.run_polling( + poll_interval=1.0, + timeout=30, + drop_pending_updates=True, + allowed_updates=["message", "callback_query"] + ) + except Exception as e: + logger.error(f"Fatal error in run_polling: {e}") + import traceback + logger.error(traceback.format_exc()) + raise + + +if __name__ == '__main__': + main() + diff --git a/LichessClientTG_bot/bot.py b/LichessClientTG_bot/bot.py index 8f5872c..70f697d 100644 --- a/LichessClientTG_bot/bot.py +++ b/LichessClientTG_bot/bot.py @@ -13,12 +13,13 @@ from telegram.ext import ( from config import ( TELEGRAM_BOT_TOKEN, PERIOD_OPTIONS, POLL_INTERVAL, POLL_TIMEOUT, DROP_PENDING_UPDATES, ALLOWED_UPDATES, - LICHESS_STATS_API_BASE_URL + LICHESS_STATS_API_BASE_URL, ADMINPANEL_TELEGRAM_BOT_TOKEN ) from database import Database from lichess_api import LichessAPI from formatters import StatsFormatter from i18n import t +from admin_bot import get_admin_bot, init_admin_bot # Configure logging logging.basicConfig( @@ -70,31 +71,60 @@ class LichessBot: async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """Start command handler""" + logger.info(f"📝 start() method called for user {update.effective_user.id}") # Register user in database user = update.effective_user lang_code = user.language_code if user else None - self.db.add_or_get_telegram_user( + 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( user_id=user.id, username=user.username, first_name=user.first_name, last_name=user.last_name, language_code=lang_code ) + # Notify admin bot about new user + if is_new_user: + 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 + ) lang = self.get_user_language_from_update(update) - await update.message.reply_text(t('start_message', lang)) + start_msg = t('start_message', lang) + await update.message.reply_text(start_msg) async def start_and_addgamer(self, update: Update, context: ContextTypes.DEFAULT_TYPE): - """Start command that automatically launches addgamer""" - # First run the regular start command - await self.start(update, context) - # Then start addgamer conversation - return await self.addgamer_start(update, context) + """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 async def addgamer_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """Start addgamer command - simple username only""" + logger.info(f"addgamer_start called for user {update.effective_user.id}") lang = self.get_user_language_from_update(update) - await update.message.reply_text(t('addgamer_prompt', lang)) + try: + await update.message.reply_text(t('addgamer_prompt', lang)) + logger.info(f"Addgamer prompt sent to user {update.effective_user.id}") + except Exception as e: + logger.error(f"Error sending addgamer prompt: {e}") + import traceback + logger.error(traceback.format_exc()) return WAITING_FOR_USERNAME async def addtoken_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -126,6 +156,14 @@ class LichessBot: ) else: # Add new gamer and link with token + # 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 + gamer_id = self.db.add_gamer(username) self.db.add_user_gamer(user_id, gamer_id, token) @@ -137,6 +175,17 @@ class LichessBot: if len(user_gamers) == 1: self.db.set_user_active_gamer(user_id, gamer_id) + # Notify admin bot about new player (only if it's a new gamer) + if is_new_gamer: + admin_bot = get_admin_bot() + if admin_bot: + user_obj = update.effective_user + await admin_bot.notify_new_player( + player_username=username, + added_by_user_id=user_id, + added_by_username=user_obj.username if user_obj else None + ) + await update.message.reply_text( t('gamer_added_with_token', lang, username=username) ) @@ -175,6 +224,14 @@ class LichessBot: return WAITING_FOR_USERNAME # Add gamer to database (without token) + # 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 + gamer_id = self.db.add_gamer(username) # Link user to gamer (without token) self.db.add_user_gamer(user_id, gamer_id, None) @@ -187,6 +244,28 @@ class LichessBot: if len(user_gamers) == 1: self.db.set_user_active_gamer(user_id, gamer_id) + # Notify admin bot about new player (only if it's a new gamer) + if is_new_gamer: + logger.info(f"New gamer detected: {username}, notifying admin bot...") + admin_bot = get_admin_bot() + if admin_bot: + user_obj = update.effective_user + try: + await admin_bot.notify_new_player( + player_username=username, + added_by_user_id=user_id, + added_by_username=user_obj.username if user_obj else None + ) + logger.info(f"Admin bot notification sent for player {username}") + except Exception as e: + logger.error(f"Failed to notify admin bot: {e}") + import traceback + logger.error(traceback.format_exc()) + else: + logger.warning("Admin bot not available for notification") + else: + logger.info(f"Gamer {username} already exists, skipping admin notification") + lang = self.get_user_language_from_update(update) await update.message.reply_text( t('gamer_added', lang, username=username) @@ -748,12 +827,13 @@ class LichessBot: addgamer_conv = ConversationHandler( entry_points=[ CommandHandler("addgamer", self.addgamer_start), - CommandHandler("start", self.start_and_addgamer) # Custom entry point that calls start and addgamer + CommandHandler("start", self.start_and_addgamer) # Also handle /start to start addgamer flow ], states={ WAITING_FOR_USERNAME: [MessageHandler(filters.TEXT & ~filters.COMMAND, self.handle_username)], }, - fallbacks=[CommandHandler("cancel", lambda u, c: ConversationHandler.END)] + fallbacks=[CommandHandler("cancel", lambda u, c: ConversationHandler.END)], + name="addgamer_conversation" ) # Conversation handler for addtoken (token required) @@ -765,8 +845,7 @@ class LichessBot: fallbacks=[CommandHandler("cancel", lambda u, c: ConversationHandler.END)] ) - # Add all handlers - # Note: start command is handled by addgamer_conv entry_points + # Add conversation handlers application.add_handler(addgamer_conv) application.add_handler(addtoken_conv) application.add_handler(CommandHandler("getgamers", self.getgamers)) @@ -785,6 +864,9 @@ class LichessBot: def main(): """Main function""" + # Initialize admin bot for notifications (admin bot runs as separate service) + init_admin_bot() + bot = LichessBot() # Create application with Long Polling configuration @@ -802,14 +884,29 @@ def main(): application.post_init = post_init + # 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) + # Start the bot with Long Polling logger.info("Starting Lichess Statistics Bot with Long Polling...") - application.run_polling( - poll_interval=POLL_INTERVAL, - timeout=POLL_TIMEOUT, - drop_pending_updates=DROP_PENDING_UPDATES, - allowed_updates=ALLOWED_UPDATES - ) + 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 if __name__ == '__main__': main() diff --git a/LichessClientTG_bot/config.py b/LichessClientTG_bot/config.py index be8d3f7..e32207b 100644 --- a/LichessClientTG_bot/config.py +++ b/LichessClientTG_bot/config.py @@ -6,6 +6,9 @@ load_dotenv() # Telegram Bot Configuration TELEGRAM_BOT_TOKEN = "7903295042:AAGBO2k8pfBDy4RoLRFsknwE7z0N-thAPI8" +# Admin Panel Bot Configuration +ADMINPANEL_TELEGRAM_BOT_TOKEN = "8588876086:AAHoZncfhTCbul1BblpvnZMzvz7jAYVFmcw" + # Lichess API Configuration LICHESS_API_BASE_URL = "https://lichess.org/api" LICHESS_STATS_API_BASE_URL = "http://localhost:8001" # For Docker container access @@ -19,5 +22,5 @@ PERIOD_OPTIONS = [0, 15, 30, 60, 120, 180] # minutes # Telegram Bot Long Polling Configuration POLL_INTERVAL = 1.0 # seconds POLL_TIMEOUT = 30 # seconds -DROP_PENDING_UPDATES = True +DROP_PENDING_UPDATES = True # Drop pending updates on startup ALLOWED_UPDATES = ["message", "callback_query"] diff --git a/LichessClientTG_bot/database.py b/LichessClientTG_bot/database.py index af966cf..cde266b 100644 --- a/LichessClientTG_bot/database.py +++ b/LichessClientTG_bot/database.py @@ -67,6 +67,15 @@ class Database: # Column already exists pass + # Create admin_settings table for admin bot configuration + cursor.execute(''' + CREATE TABLE IF NOT EXISTS admin_settings ( + key TEXT PRIMARY KEY, + value TEXT, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + conn.commit() # Migrate tokens from gamers to user_gamers if needed @@ -329,3 +338,27 @@ class Database: }) return gamers + + def get_admin_chat_id(self) -> Optional[int]: + """Get admin chat ID from database""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT value FROM admin_settings WHERE key = 'admin_chat_id'") + result = cursor.fetchone() + if result: + try: + return int(result[0]) + except (ValueError, TypeError): + return None + return None + + def set_admin_chat_id(self, chat_id: int): + """Set admin chat ID in database""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(''' + INSERT OR REPLACE INTO admin_settings (key, value, updated_at) + VALUES ('admin_chat_id', ?, CURRENT_TIMESTAMP) + ''', (str(chat_id),)) + conn.commit() + logger.info(f"Admin chat ID saved to database: {chat_id}") diff --git a/docker-compose.yml b/docker-compose.yml index f2705cf..c698cac 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,7 @@ services: container_name: lichess-telegram-bot volumes: - ./LichessClientTG_bot/data:/app/data + - ./LichessClientTG_bot:/app environment: - PYTHONPATH=/app - PYTHONUNBUFFERED=1 @@ -36,6 +37,23 @@ services: retries: 3 start_period: 40s + # Admin Panel Telegram Bot + admin-bot: + build: + context: ./LichessClientTG_bot + dockerfile: Dockerfile.admin + container_name: lichess-admin-bot + volumes: + - ./LichessClientTG_bot/data:/app/data + - ./LichessClientTG_bot:/app + environment: + - PYTHONPATH=/app + - PYTHONUNBUFFERED=1 + network_mode: "host" + restart: always + depends_on: + - lichess-api + # Web View Interface web-view: build: ./LichessWebView