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