admin panel

This commit is contained in:
vrubelroman 2025-11-13 01:00:48 +03:00
parent 3362bf89e2
commit 23de80f94d
6 changed files with 424 additions and 20 deletions

View file

@ -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"]

View file

@ -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"🆕 <b>Новый пользователь Telegram</b>\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"🎮 <b>Добавлен новый игрок для отслеживания</b>\n\n"
f"Игрок: <a href=\"{lichess_url}\">{player_username}</a>\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"📊 <b>Статистика базы данных</b>\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()

View file

@ -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()

View file

@ -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"]

View file

@ -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}")