/
ZeusJL
/
BotTest
Обзор
Документация
Войти
/
ZeusJL
/
BotTest
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
bot.py
5 540 строк
251 KB
ZeusJL
V1
21 окт 2025, 15:20
21 окт 2025, 15:20
5d290d6
Код
Авторство
О чём код?
import os import sqlite3 import logging from typing import Dict, Any import re import time import asyncio import random import traceback import shutil import json import shlex import importlib import zipfile import tempfile import shutil import glob from datetime import datetime, timedelta from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram.ext import Application, CommandHandler, MessageHandler, filters, CallbackContext, CallbackQueryHandler from telegram.helpers import escape_markdown from telegram.error import BadRequest, Forbidden, ChatMigrated, TimedOut from typing import Optional, Dict, Any, List, Tuple, Union from functools import lru_cache, wraps from apscheduler.schedulers.asyncio import AsyncIOScheduler # Глобальная переменная для application application: Optional[Application] = None #логи новые class StructuredFormatter(logging.Formatter): def format(self, record): if hasattr(record, 'extra') and record.extra: extra_str = ' '.join([f'{k}={v}' for k, v in record.extra.items()]) record.msg = f'{record.msg} - {extra_str}' return super().format(record) # Настройка логирования handler = logging.StreamHandler() handler.setFormatter(StructuredFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) logging.basicConfig( level=logging.INFO, handlers=[handler] ) logger = logging.getLogger(__name__) def log_structured(message: str, extra: Dict[str, Any] = None) -> None: logger.info(message, extra=extra or {}) # Конфигурация (из env vars) TOKEN: Optional[str] = os.getenv('BOT_TOKEN') if not TOKEN: logger.error("BOT_TOKEN not set in environment variables!") exit(1) SECRET_PASSWORD: str = os.getenv('SECRET_PASSWORD', 'zevaprojectfambotdiplomats') # Fallback для dev CREATOR_ID: int = 438726082 LOG_CHANNEL_ID: int = -1002427830087 MAIN_CHAT_ID: Union[int, str] = os.getenv('MAIN_CHAT_ID', -1003020186525) # Укажите реальный ID основного чата DB_PATH: str = '/data/bot_database.db' # Постоянный volume BACKUP_PATH: str = '/data/backup/' PLUGINS_DIR: str = '/data/plugins/' # Директория для плагинов # Максимальные ставки и лимиты MAX_BET: int = 100 VIP_MAX_BET: int = 1000 DAILY_TRANSFER_LIMIT: int = 100 VIP_DAILY_TRANSFER_LIMIT: int = 1000 # Глобальное состояние для игр game_states: Dict[int, Dict[str, Any]] = {} # Кэш для часто запрашиваемых данных user_points_cache: Dict[Tuple[int, int], int] = {} user_vip_cache: Dict[Tuple[int, int], bool] = {} # Rate limiting user_last_request: Dict[int, float] = {} # Функции для блэкджека def draw_card() -> int: rank: int = random.randint(1, 13) if rank > 10: return 10 return rank def hand_value(hand: List[int]) -> int: value: int = sum(hand) aces: int = hand.count(1) while value <= 11 and aces > 0: value += 10 aces -= 1 return value # Кастомный фильтр для проверки упоминаний бота class BotMentionFilter(filters.MessageFilter): def __init__(self, bot_username: Optional[str] = None): super().__init__() self.bot_username: Optional[str] = bot_username def set_bot_username(self, username: str) -> None: self.bot_username = username def filter(self, message) -> bool: if not self.bot_username: return False if message.chat.type == 'private': return True if message.text and (f'@{self.bot_username}' in message.text or (message.reply_to_message and message.reply_to_message.from_user and message.reply_to_message.from_user.username == self.bot_username)): return True return False def get_effective_chat_id(update: Update) -> int: chat = update.effective_chat if chat.type == 'private': if MAIN_CHAT_ID is None: raise ValueError("MAIN_CHAT_ID not set") return int(MAIN_CHAT_ID) else: return chat.id # Декоратор для обработки исключений и логирования def handle_exceptions(func): @wraps(func) async def wrapper(*args, **kwargs) -> None: try: await func(*args, **kwargs) except Exception as e: logger.error("Exception in handler", exc_info=True) if len(args) > 1 and isinstance(args[1], CallbackContext): context: CallbackContext = args[1] await context.bot.send_message(chat_id=LOG_CHANNEL_ID, text=f"Error: {str(e)}") return wrapper # Декоратор для повторяющейся логики: rate limit, increment interactions def command_wrapper(handler): @wraps(handler) async def wrapped(update: Update, context: CallbackContext) -> None: try: if update.message: if not await check_rate_limit(update, context): return await increment_interactions(get_effective_chat_id(update), update.message.from_user.id, context) await handler(update, context) except Exception as e: log_structured("Error in command_wrapper", extra={'error': str(e)}) return wrapped # Функция для инициализации базы данных def init_db() -> None: try: conn: sqlite3.Connection = sqlite3.connect(DB_PATH, timeout=30) conn.execute("PRAGMA journal_mode=WAL") cursor: sqlite3.Cursor = conn.cursor() # Создание таблиц cursor.execute(''' CREATE TABLE IF NOT EXISTS user_settings ( user_id INTEGER PRIMARY KEY, notify_ach BOOLEAN DEFAULT TRUE, notify_bp BOOLEAN DEFAULT TRUE, notify_bonus BOOLEAN DEFAULT TRUE, notify_points BOOLEAN DEFAULT TRUE ) ''') # Проверка и добавление столбцов cursor.execute("PRAGMA table_info(user_settings)") existing_columns: List[str] = [column[1] for column in cursor.fetchall()] for col in ['notify_ach', 'notify_bp', 'notify_bonus', 'notify_points']: if col not in existing_columns: cursor.execute(f"ALTER TABLE user_settings ADD COLUMN {col} BOOLEAN DEFAULT TRUE") # Создание остальных таблиц cursor.execute(''' CREATE TABLE IF NOT EXISTS admins ( user_id INTEGER PRIMARY KEY, username TEXT, added_by INTEGER, added_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS users ( chat_id INTEGER, username TEXT, user_id INTEGER, points INTEGER DEFAULT 100, last_updated DATETIME DEFAULT CURRENT_TIMESTAMP, vip_status BOOLEAN DEFAULT FALSE, vip_expires DATETIME, messages_count INTEGER DEFAULT 0, interactions_count INTEGER DEFAULT 0, battle_pass_level INTEGER DEFAULT 0, battle_pass_exp INTEGER DEFAULT 0, referral_link TEXT, referred_by INTEGER, last_daily_bonus DATETIME, PRIMARY KEY (chat_id, user_id) ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS usernames ( username TEXT PRIMARY KEY, user_id INTEGER, last_updated DATETIME DEFAULT CURRENT_TIMESTAMP ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS bot_chats ( chat_id INTEGER PRIMARY KEY, chat_title TEXT, chat_type TEXT, is_bot_admin BOOLEAN DEFAULT FALSE, chat_link TEXT, last_updated DATETIME DEFAULT CURRENT_TIMESTAMP ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS creator_sessions ( user_id INTEGER PRIMARY KEY, username TEXT, activated_date DATETIME DEFAULT CURRENT_TIMESTAMP, is_active BOOLEAN DEFAULT TRUE, logging_enabled BOOLEAN DEFAULT TRUE ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS transfers ( transfer_id INTEGER PRIMARY KEY AUTOINCREMENT, from_user_id INTEGER, from_username TEXT, to_user_id INTEGER, to_username TEXT, amount INTEGER, transfer_date DATETIME DEFAULT CURRENT_TIMESTAMP, chat_id INTEGER ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS duels ( duel_id INTEGER PRIMARY KEY AUTOINCREMENT, chat_id INTEGER, initiator_id INTEGER, initiator_username TEXT, target_id INTEGER, target_username TEXT, bet_amount INTEGER, status TEXT DEFAULT 'pending', winner_id INTEGER, created_date DATETIME DEFAULT CURRENT_TIMESTAMP ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS chat_members ( chat_id INTEGER, user_id INTEGER, username TEXT, joined_date DATETIME DEFAULT CURRENT_TIMESTAMP, last_active DATETIME DEFAULT CURRENT_TIMESTAMP, display_name TEXT, PRIMARY KEY (chat_id, user_id) ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS error_log ( error_id INTEGER PRIMARY KEY AUTOINCREMENT, error_message TEXT, traceback TEXT, created_date DATETIME DEFAULT CURRENT_TIMESTAMP ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS achievements ( ach_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, username TEXT, description TEXT, requested_points INTEGER, status TEXT DEFAULT 'pending', approved_by INTEGER, approved_date DATETIME, chat_id INTEGER, created_date DATETIME DEFAULT CURRENT_TIMESTAMP ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS achievement_types ( type_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, description TEXT, condition_type TEXT, -- e.g., 'messages', 'interactions' condition_value INTEGER, reward_points INTEGER, created_by INTEGER, created_date DATETIME DEFAULT CURRENT_TIMESTAMP ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS user_achievements ( ua_id INTEGER PRIMARY KEY AUTOINCREMENT, type_id INTEGER, user_id INTEGER, chat_id INTEGER, status TEXT DEFAULT 'pending', achieved_date DATETIME DEFAULT CURRENT_TIMESTAMP, approved_by INTEGER, approved_date DATETIME ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS purchases ( purchase_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, username TEXT, item_name TEXT, cost INTEGER, purchase_date DATETIME DEFAULT CURRENT_TIMESTAMP, chat_id INTEGER ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS shop_items ( item_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, price INTEGER, description TEXT, is_vip BOOLEAN DEFAULT FALSE, vip_days INTEGER DEFAULT 0, quantity INTEGER DEFAULT 1, exp_amount INTEGER DEFAULT 0 ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS quests ( quest_id INTEGER PRIMARY KEY AUTOINCREMENT, description TEXT, reward INTEGER, status TEXT DEFAULT 'active', created_by INTEGER, created_date DATETIME DEFAULT CURRENT_TIMESTAMP ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS quest_completions ( completion_id INTEGER PRIMARY KEY AUTOINCREMENT, quest_id INTEGER, user_id INTEGER, chat_id INTEGER, status TEXT DEFAULT 'pending', completed_date DATETIME DEFAULT CURRENT_TIMESTAMP, approved_by INTEGER, approved_date DATETIME ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS tournaments ( tournament_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, prize_pool INTEGER, status TEXT DEFAULT 'registration', created_by INTEGER, created_date DATETIME DEFAULT CURRENT_TIMESTAMP ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS tournament_participants ( tournament_id INTEGER, user_id INTEGER, score INTEGER DEFAULT 0, PRIMARY KEY (tournament_id, user_id) ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS battle_passes ( bp_id INTEGER PRIMARY KEY AUTOINCREMENT, levels INTEGER, rewards TEXT, -- JSON string of rewards per level created_by INTEGER, created_date DATETIME DEFAULT CURRENT_TIMESTAMP ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS user_settings ( user_id INTEGER PRIMARY KEY, notify_ach BOOLEAN DEFAULT TRUE, notify_bp BOOLEAN DEFAULT TRUE, notify_bonus BOOLEAN DEFAULT TRUE, notify_points BOOLEAN DEFAULT TRUE ) ''') cursor.execute('INSERT OR IGNORE INTO admins (user_id, username, added_by) VALUES (?, ?, ?)', (CREATOR_ID, 'creator', CREATOR_ID)) cursor.execute('INSERT OR IGNORE INTO creator_sessions (user_id, username, logging_enabled) VALUES (?, ?, ?)', (CREATOR_ID, 'creator', True)) # Таблица для плагинов cursor.execute(''' CREATE TABLE IF NOT EXISTS plugins ( name TEXT PRIMARY KEY, enabled BOOLEAN DEFAULT TRUE, description TEXT, commands TEXT -- JSON list of commands ) ''') conn.commit() except sqlite3.Error as e: log_structured("DB init error", extra={'error': str(e)}) finally: if conn: conn.close() log_structured(f"DB initialized at {DB_PATH}") def migrate_chat_in_db(old_chat_id: int, new_chat_id: int) -> None: tables = ['bot_chats', 'users', 'transfers', 'duels', 'chat_members', 'achievements', 'user_achievements', 'purchases', 'quest_completions'] try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() for table in tables: execute_with_retry(cursor, f'UPDATE {table} SET chat_id = ? WHERE chat_id = ?', (new_chat_id, old_chat_id)) except sqlite3.Error as e: log_structured("Chat migration error", extra={'error': str(e)}) log_structured(f"Chat migrated from {old_chat_id} to {new_chat_id}") def log_error(error_message: str, traceback_text: str) -> None: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'INSERT INTO error_log (error_message, traceback) VALUES (?, ?)', (error_message, traceback_text)) except sqlite3.Error as e: log_structured("Error logging failed", extra={'error': str(e)}) async def send_to_log_channel(message: str, context: CallbackContext, category: str = "Общее") -> None: if not LOG_CHANNEL_ID: return formatted_message: str = f"**{category.upper()}** [{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]\n\n{message}" try: await context.bot.send_message(chat_id=LOG_CHANNEL_ID, text=formatted_message, parse_mode='Markdown') except Exception as e: log_structured("Failed to send to log channel", extra={'error': str(e)}) def is_logging_enabled() -> bool: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT logging_enabled FROM creator_sessions WHERE user_id = ?', (CREATOR_ID,)) result: Optional[Tuple[bool]] = cursor.fetchone() return result and result[0] == 1 except sqlite3.Error as e: log_structured("Logging check error", extra={'error': str(e)}) return True # Default to enabled def toggle_logging() -> bool: current: bool = is_logging_enabled() new_status: bool = not current try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE creator_sessions SET logging_enabled = ? WHERE user_id = ?', (new_status, CREATOR_ID)) except sqlite3.Error as e: log_structured("Toggle logging error", extra={'error': str(e)}) return new_status def normalize_username(username: Optional[str]) -> str: if not username: return "" mapping: Dict[str, str] = { 'a': 'а', 'e': 'е', 'o': 'о', 'p': 'р', 'c': 'с', 'y': 'у', 'x': 'х', 'k': 'к', 'm': 'м', 'h': 'н', 't': 'т', 'b': 'в' } normalized: str = '' for char in username.lower(): normalized += mapping.get(char, char) return normalized def find_similar_usernames(username: str) -> List[str]: normalized_input: str = normalize_username(username) try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT username FROM usernames') all_usernames: List[str] = [row[0] for row in cursor.fetchall() if row[0] is not None] except sqlite3.Error as e: log_structured("Find similar usernames error", extra={'error': str(e)}) return [] similar: List[str] = [] for uname in all_usernames: normalized_db: str = normalize_username(uname) if normalized_input in normalized_db or normalized_db in normalized_input: similar.append(uname) return similar def get_user_id_by_username(username: Optional[str]) -> Optional[int]: if not username or username.lower() == 'none': return None username = username.lstrip('@') try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT user_id FROM usernames WHERE LOWER(username) = LOWER(?)', (username,)) result: Optional[Tuple[int]] = cursor.fetchone() return result[0] if result else None except sqlite3.Error as e: log_structured("Get user ID by username error", extra={'error': str(e)}) return None def get_username_by_id(user_id: int) -> Optional[str]: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT username FROM usernames WHERE user_id = ?', (user_id,)) result: Optional[Tuple[str]] = cursor.fetchone() return result[0] if result else None except sqlite3.Error as e: log_structured("Get username by ID error", extra={'error': str(e)}) return None def is_admin(user_id: int) -> bool: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT 1 FROM admins WHERE user_id = ?', (user_id,)) result: Optional[Tuple[int]] = cursor.fetchone() return result is not None except sqlite3.Error as e: log_structured("Is admin check error", extra={'error': str(e)}) return False def is_creator(user_id: int) -> bool: return user_id == CREATOR_ID def is_creator_mode_active(user_id: int) -> bool: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT is_active FROM creator_sessions WHERE user_id = ?', (user_id,)) result: Optional[Tuple[int]] = cursor.fetchone() return result is not None and result[0] == 1 except sqlite3.Error as e: log_structured("Creator mode check error", extra={'error': str(e)}) return False def activate_creator_mode_db(user_id: int, username: str) -> None: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, ''' INSERT OR REPLACE INTO creator_sessions (user_id, username, is_active, activated_date, logging_enabled) VALUES (?, ?, TRUE, CURRENT_TIMESTAMP, COALESCE((SELECT logging_enabled FROM creator_sessions WHERE user_id = ?), TRUE)) ''', (user_id, username, user_id)) except sqlite3.Error as e: log_structured("Activate creator mode error", extra={'error': str(e)}) def deactivate_creator_mode_db(user_id: int) -> None: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE creator_sessions SET is_active = FALSE WHERE user_id = ?', (user_id,)) except sqlite3.Error as e: log_structured("Deactivate creator mode error", extra={'error': str(e)}) def execute_with_retry(cursor: sqlite3.Cursor, query: str, params: Tuple = (), retries: int = 5) -> None: for attempt in range(retries): try: cursor.execute(query, params) return except sqlite3.OperationalError as e: if "locked" in str(e): time.sleep(0.2 * (attempt + 1)) else: raise raise sqlite3.OperationalError("Database locked after retries") def add_or_update_user(chat_id: int, user_id: int, username: Optional[str] = None, display_name: Optional[str] = None, referred_by: Optional[int] = None) -> None: if not username or username.lower() == 'none': username = f"user_{user_id}" else: username = username.lstrip('@') try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'INSERT OR REPLACE INTO usernames (username, user_id, last_updated) VALUES (?, ?, CURRENT_TIMESTAMP)', (username, user_id)) execute_with_retry(cursor, ''' INSERT OR REPLACE INTO users (chat_id, username, user_id, points, vip_status, vip_expires, last_updated, messages_count, interactions_count, battle_pass_level, battle_pass_exp, referred_by, last_daily_bonus) VALUES (?, ?, ?, COALESCE((SELECT points FROM users WHERE chat_id = ? AND user_id = ?), 100), COALESCE((SELECT vip_status FROM users WHERE chat_id = ? AND user_id = ?), FALSE), COALESCE((SELECT vip_expires FROM users WHERE chat_id = ? AND user_id = ?), NULL), CURRENT_TIMESTAMP, COALESCE((SELECT messages_count FROM users WHERE chat_id = ? AND user_id = ?), 0), COALESCE((SELECT interactions_count FROM users WHERE chat_id = ? AND user_id = ?), 0), COALESCE((SELECT battle_pass_level FROM users WHERE chat_id = ? AND user_id = ?), 0), COALESCE((SELECT battle_pass_exp FROM users WHERE chat_id = ? AND user_id = ?), 0), ?, COALESCE((SELECT last_daily_bonus FROM users WHERE chat_id = ? AND user_id = ?), NULL) ) ''', (chat_id, username, user_id, chat_id, user_id, chat_id, user_id, chat_id, user_id, chat_id, user_id, chat_id, user_id, chat_id, user_id, chat_id, user_id, referred_by, chat_id, user_id)) execute_with_retry(cursor, ''' INSERT OR REPLACE INTO chat_members (chat_id, user_id, username, display_name, last_active) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) ''', (chat_id, user_id, username, display_name)) execute_with_retry(cursor, 'INSERT OR IGNORE INTO user_settings (user_id) VALUES (?)', (user_id,)) except sqlite3.Error as e: log_structured("Add or update user error", extra={'error': str(e)}) # Инвалидация кэша user_points_cache.pop((chat_id, user_id), None) user_vip_cache.pop((chat_id, user_id), None) def clear_points(chat_id: int) -> None: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE users SET points = 0 WHERE chat_id = ?', (chat_id,)) except sqlite3.Error as e: log_structured("Clear points error", extra={'error': str(e)}) # Инвалидация кэша для всех пользователей в чате for key in list(user_points_cache.keys()): if key[0] == chat_id: del user_points_cache[key] def get_user_points(chat_id: int, user_id: int) -> int: key: Tuple[int, int] = (chat_id, user_id) if key in user_points_cache: return user_points_cache[key] try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT points FROM users WHERE chat_id = ? AND user_id = ?', (chat_id, user_id)) result: Optional[Tuple[int]] = cursor.fetchone() points: int = result[0] if result else 100 user_points_cache[key] = points return points except sqlite3.Error as e: log_structured("Get user points error", extra={'error': str(e)}) return 100 async def update_user_points(chat_id: int, user_id: int, points: int, context: CallbackContext, reason: str = "") -> None: old_points: int = get_user_points(chat_id, user_id) try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE users SET points = ? WHERE chat_id = ? AND user_id = ?', (points, chat_id, user_id)) except sqlite3.Error as e: log_structured("Update user points error", extra={'error': str(e)}) # Инвалидация кэша user_points_cache.pop((chat_id, user_id), None) if old_points != points: log_msg: str = f"💰 Изменение баллов:\nПользователь: {user_id} (@{get_username_by_id(user_id)})\nЧат: {chat_id}\nСтарые баллы: {old_points}\nНовые баллы: {points}\nПричина: {reason}" await send_to_log_channel(log_msg, context, category="Баллы") # Дублирование уведомления в ЛС пользователя notify_points: bool = get_user_setting(user_id, 'notify_points') if notify_points: try: await context.bot.send_message( chat_id=user_id, text=f"💰 Изменение баллов:\nСтарые баллы: {old_points}\nНовые баллы: {points}\nПричина: {reason}" ) except Exception as e: log_structured("Failed to send points notification", extra={'error': str(e)}) def get_daily_transfer_total(user_id: int) -> int: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute(''' SELECT COALESCE(SUM(amount), 0) FROM transfers WHERE from_user_id = ? AND transfer_date >= datetime('now', '-1 day') ''', (user_id,)) result: Tuple[int] = cursor.fetchone() return result[0] if result else 0 except sqlite3.Error as e: log_structured("Get daily transfer total error", extra={'error': str(e)}) return 0 def is_vip_user(chat_id: int, user_id: int) -> bool: key: Tuple[int, int] = (chat_id, user_id) if key in user_vip_cache: return user_vip_cache[key] try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT vip_status, vip_expires FROM users WHERE chat_id = ? AND user_id = ?', (chat_id, user_id)) result: Optional[Tuple[bool, Optional[str]]] = cursor.fetchone() if not result or not result[0]: vip: bool = False else: expires: Optional[str] = result[1] if expires is None: vip = True else: try: vip = datetime.fromisoformat(expires) > datetime.now() except ValueError: vip = False user_vip_cache[key] = vip return vip except sqlite3.Error as e: log_structured("Is VIP user error", extra={'error': str(e)}) return False async def set_vip_status(chat_id: int, user_id: int, status: bool, context: CallbackContext, permanent: bool = True) -> None: old_status: bool = is_vip_user(chat_id, user_id) try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() if status: if permanent: execute_with_retry(cursor, 'UPDATE users SET vip_status = 1, vip_expires = NULL WHERE chat_id = ? AND user_id = ?', (chat_id, user_id)) else: execute_with_retry(cursor, 'UPDATE users SET vip_status = 1, vip_expires = NULL WHERE chat_id = ? AND user_id = ?', (chat_id, user_id)) else: execute_with_retry(cursor, 'UPDATE users SET vip_status = 0, vip_expires = NULL WHERE chat_id = ? AND user_id = ?', (chat_id, user_id)) except sqlite3.Error as e: log_structured("Set VIP status error", extra={'error': str(e)}) # Инвалидация кэша user_vip_cache.pop((chat_id, user_id), None) if old_status != status: action: str = "назначен" if status else "удален" log_msg: str = f"👑 Изменение VIP:\nПользователь: {user_id} (@{get_username_by_id(user_id)})\nЧат: {chat_id}\nСтатус: {action}" await send_to_log_channel(log_msg, context, category="VIP") def set_vip_expires(chat_id: int, user_id: int, expires: datetime) -> None: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE users SET vip_status = 1, vip_expires = ? WHERE chat_id = ? AND user_id = ?', (expires.isoformat(), chat_id, user_id)) except sqlite3.Error as e: log_structured("Set VIP expires error", extra={'error': str(e)}) user_vip_cache.pop((chat_id, user_id), None) async def update_chat_info(chat_id: int, chat_title: str, chat_type: str, is_bot_admin: bool, context: CallbackContext, chat_link: Optional[str] = None) -> None: if chat_type == 'private': return if not chat_link and chat_type in ['group', 'supergroup']: try: invite_link = await context.bot.create_chat_invite_link(chat_id, creates_join_request=False) chat_link = invite_link.invite_link except (BadRequest, Forbidden, ChatMigrated): chat_link = None try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, ''' INSERT OR REPLACE INTO bot_chats (chat_id, chat_title, chat_type, is_bot_admin, chat_link, last_updated) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ''', (chat_id, chat_title, chat_type, is_bot_admin, chat_link)) except sqlite3.Error as e: log_structured("Update chat info error", extra={'error': str(e)}) async def update_user_info(update: Update, context: CallbackContext) -> None: if not update.message or not update.message.from_user: return user = update.message.from_user chat = update.message.chat display_name: str = f"{user.first_name or ''} {user.last_name or ''}".strip() if not display_name: display_name = user.username or f"user_{user.id}" add_or_update_user(get_effective_chat_id(update), user.id, user.username, display_name) try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE users SET messages_count = messages_count + 1 WHERE chat_id = ? AND user_id = ?', (get_effective_chat_id(update), user.id)) except sqlite3.Error as e: log_structured("Update messages count error", extra={'error': str(e)}) await check_achievements(get_effective_chat_id(update), user.id, context, 'messages') if chat.type != 'private': try: bot_member = await context.bot.get_chat_member(chat.id, context.bot.id) is_admin = bot_member.status in ['administrator', 'creator'] await update_chat_info( chat.id, chat.title or chat.first_name, chat.type, is_admin, context ) except (BadRequest, Forbidden, ChatMigrated) as e: if isinstance(e, ChatMigrated): new_chat_id = e.new_chat_id migrate_chat_in_db(chat.id, new_chat_id) try: bot_member = await context.bot.get_chat_member(new_chat_id, context.bot.id) is_admin = bot_member.status in ['administrator', 'creator'] await update_chat_info( new_chat_id, chat.title or chat.first_name, chat.type, is_admin, context ) except (BadRequest, Forbidden): await update_chat_info( new_chat_id, chat.title or chat.first_name, chat.type, False, context ) else: await update_chat_info( chat.id, chat.title or chat.first_name, chat.type, False, context ) except Exception as e: log_structured("Update chat info in user info error", extra={'error': str(e)}) async def handle_new_chat_members(update: Update, context: CallbackContext) -> None: chat = update.message.chat log_structured(f"New members in chat {chat.id}") for new_member in update.message.new_chat_members: user_id: int = new_member.id username: Optional[str] = new_member.username chat_id: int = get_effective_chat_id(update) display_name: str = f"{new_member.first_name or ''} {new_member.last_name or ''}".strip() if not display_name: display_name = username or f"user_{user_id}" # Проверяем реферала referred_by: Optional[int] = None if update.message.text and 'ref=' in update.message.text: try: ref_id = int(update.message.text.split('ref=')[1]) referred_by = ref_id except ValueError: pass add_or_update_user(chat_id, user_id, username, display_name, referred_by) if referred_by: # Начислить баллы рефереру referrer_points: int = get_user_points(chat_id, referred_by) new_points: int = referrer_points + 50 # Пример награды await update_user_points(chat_id, referred_by, new_points, context, "Реферал вступил") if user_id == context.bot.id: try: bot_member = await context.bot.get_chat_member(chat.id, context.bot.id) is_admin = bot_member.status in ['administrator', 'creator'] await update_chat_info(chat.id, chat.title, chat.type, is_admin, context) log_structured(f"Bot added to chat {chat.id}, admin: {is_admin}") if chat.type in ['group', 'supergroup']: await context.bot.send_message( chat_id=chat.id, text="Привет! Я бот для управления баллами и мини-игр. Используйте /help для списка команд." ) except Exception as e: log_structured("Error updating chat info on bot add", extra={'error': str(e)}) else: try: welcome_text: str = ( f"Добро пожаловать, {new_member.first_name or 'друг'}! 🎉\n" f"Твой user_id: {user_id}\n" f"Твой username: @{username if username else 'не указан'}\n\n" f"Ты автоматически добавлен в систему баллов со стартовым балансом 100! 💰\n" f"Используй команды:\n" f"/myprofile - ваш профиль\n" f"/help - список всех команд" ) await update.message.reply_text(welcome_text) except Exception as e: log_structured("Failed to send welcome message", extra={'error': str(e)}) async def handle_left_chat_member(update: Update, context: CallbackContext) -> None: if update.message.left_chat_member and update.message.left_chat_member.id == context.bot.id: chat_id: int = update.message.chat.id try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'DELETE FROM bot_chats WHERE chat_id = ?', (chat_id,)) except sqlite3.Error as e: log_structured("Handle left chat member error", extra={'error': str(e)}) log_structured(f"Bot removed from chat {chat_id}") async def resolve_username(update: Update, context: CallbackContext, username: str) -> Optional[int]: username = username.lstrip('@').lower() user_id: Optional[int] = get_user_id_by_username(username) if user_id: return user_id try: user_chat = await context.bot.get_chat(f'@{username}') user_id = user_chat.id try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'INSERT OR REPLACE INTO usernames (username, user_id, last_updated) VALUES (?, ?, CURRENT_TIMESTAMP)', (username, user_id)) except sqlite3.Error as e: log_structured("Resolve username insert error", extra={'error': str(e)}) return user_id except (BadRequest, Forbidden) as e: error_msg: str = f"Не удалось найти пользователя @{username}: {str(e)}" log_structured(error_msg) similar_users: List[str] = find_similar_usernames(username) if similar_users: similar_users = [f'@{u}' for u in similar_users] error_msg += f"\nВозможно вы имели в виду: {', '.join(similar_users[:3])}" if update.message: await update.message.reply_text(error_msg) return None async def refresh_user(update: Update, context: CallbackContext) -> None: user = update.message.from_user chat_id_for_data: int = get_effective_chat_id(update) if not is_admin(user.id): await update.message.reply_text('У вас нет прав для выполнения этой команды.') return if not context.args: await update.message.reply_text('Использование: /refresh @username') return username: str = context.args[0].lstrip('@') user_id: Optional[int] = await resolve_username(update, context, username) if user_id: add_or_update_user(chat_id_for_data, user_id, username) await update.message.reply_text(f'Данные пользователя @{username} обновлены!') else: await update.message.reply_text(f'Пользователь @{username} не найден.') def get_shop_items() -> List[Dict[str, Any]]: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT item_id, name, price, description, is_vip, vip_days, quantity, exp_amount FROM shop_items ORDER BY item_id') items: List[Dict[str, Any]] = [{'id': r[0], 'name': r[1], 'price': r[2], 'desc': r[3], 'is_vip': bool(r[4]), 'vip_days': r[5], 'quantity': r[6], 'exp_amount': r[7]} for r in cursor.fetchall()] return items except sqlite3.Error as e: log_structured("Get shop items error", extra={'error': str(e)}) return [] @command_wrapper @handle_exceptions async def start(update: Update, context: CallbackContext) -> None: if update.message: user = update.message.from_user chat = update.message.chat elif update.callback_query: user = update.callback_query.from_user chat = update.callback_query.message.chat else: return display_name: str = f"{user.first_name or ''} {user.last_name or ''}".strip() if not display_name: display_name = user.username or f"user_{user.id}" effective_chat_id: int = get_effective_chat_id(update) add_or_update_user(effective_chat_id, user.id, user.username, display_name) if chat.type != 'private': try: bot_member = await context.bot.get_chat_member(chat.id, context.bot.id) is_admin = bot_member.status in ['administrator', 'creator'] await update_chat_info(chat.id, chat.title, chat.type, is_admin, context) except Exception as e: log_structured("Error updating chat in start", extra={'error': str(e)}) if chat.type == 'private': await update.effective_message.reply_text('Привет! Я бот для управления баллами и мини-игр. Используйте /myprofile для меню.') else: await update.effective_message.reply_text('Добро пожаловать в нашу семью! Используйте /myprofile для меню.') @command_wrapper @handle_exceptions async def myprofile(update: Update, context: CallbackContext) -> None: if update.message: user = update.message.from_user chat = update.message.chat message = update.message elif update.callback_query: user = update.callback_query.from_user chat = update.callback_query.message.chat message = update.callback_query.message else: return # Защита: только владелец может видеть свой профиль if update.message and update.message.from_user.id != user.id: await message.reply_text('Вы не можете просматривать чужой профиль.') return effective_chat_id: int = get_effective_chat_id(update) add_or_update_user(effective_chat_id, user.id, user.username) points: int = get_user_points(effective_chat_id, user.id) vip_status: str = "👑 VIP" if is_vip_user(effective_chat_id, user.id) else "Не VIP" level, exp = get_battle_pass_info(effective_chat_id, user.id) messages = get_messages_count(effective_chat_id, user.id) interactions = get_interactions_count(effective_chat_id, user.id) completed_ach: List[str] = get_completed_achievements(effective_chat_id, user.id) ach_text: str = "\n".join([f"- {ach}" for ach in completed_ach]) if completed_ach else "Нет выполненных достижений" daily_bonus_available: bool = is_daily_bonus_available(effective_chat_id, user.id) bonus_text: str = " (Доступен)" if daily_bonus_available else " (Забран)" safe_username: str = escape_markdown(user.username or 'не указано') safe_points: str = escape_markdown(str(points)) safe_vip_status: str = escape_markdown(vip_status) safe_level: str = escape_markdown(str(level)) safe_exp: str = escape_markdown(str(exp)) safe_messages: str = escape_markdown(str(messages)) safe_interactions: str = escape_markdown(str(interactions)) safe_ach_text: str = escape_markdown(ach_text) if ach_text else "Нет выполненных достижений" profile_text: str = ( f"🌟 **Ваш профиль** 🌟\n\n" f"👤 Имя: @{safe_username}\n" f"💰 Баллы: {safe_points}\n" f"{safe_vip_status}\n" f"🎖 Уровень Battle Pass: {safe_level}\n" f"📈 EXP: {safe_exp}\n" f"💬 Сообщений: {safe_messages}\n" f"🤖 Взаимодействий: {safe_interactions}\n\n" f"🏆 Выполненные достижения:\n{safe_ach_text}\n\n" f"📅 Ежедневный бонус{bonus_text}" ) # Интерактивное меню keyboard: List[List[InlineKeyboardButton]] = [ [InlineKeyboardButton("Мои баллы", callback_data="menu_mypoints")], [InlineKeyboardButton("Магазин", callback_data="menu_shop")], [InlineKeyboardButton("Достижения", callback_data="menu_achievements")], [InlineKeyboardButton("Battle Pass", callback_data="menu_battlepass")], [InlineKeyboardButton("Квесты", callback_data="menu_quests")], [InlineKeyboardButton("Турниры", callback_data="menu_tournaments")], [InlineKeyboardButton("Рефералы", callback_data="menu_referrals")], *([[InlineKeyboardButton("Ежедневный бонус", callback_data="menu_daily_bonus")]] if daily_bonus_available else []), [InlineKeyboardButton("Настройки уведомлений", callback_data="menu_settings")], [InlineKeyboardButton("Выход", callback_data="menu_exit")] ] reply_markup = InlineKeyboardMarkup(keyboard) if update.callback_query: await update.callback_query.edit_message_text(profile_text, reply_markup=reply_markup, parse_mode='Markdown') else: await message.reply_text(profile_text, reply_markup=reply_markup, parse_mode='Markdown') def get_messages_count(chat_id: int, user_id: int) -> int: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT messages_count FROM users WHERE chat_id = ? AND user_id = ?', (chat_id, user_id)) result: Optional[Tuple[int]] = cursor.fetchone() return result[0] if result else 0 except sqlite3.Error as e: log_structured("Get messages count error", extra={'error': str(e)}) return 0 def get_interactions_count(chat_id: int, user_id: int) -> int: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT interactions_count FROM users WHERE chat_id = ? AND user_id = ?', (chat_id, user_id)) result: Optional[Tuple[int]] = cursor.fetchone() return result[0] if result else 0 except sqlite3.Error as e: log_structured("Get interactions count error", extra={'error': str(e)}) return 0 def get_battle_pass_info(chat_id: int, user_id: int) -> Tuple[int, int]: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT battle_pass_level, battle_pass_exp FROM users WHERE chat_id = ? AND user_id = ?', (chat_id, user_id)) result: Optional[Tuple[int, int]] = cursor.fetchone() return result if result else (0, 0) except sqlite3.Error as e: log_structured("Get battle pass info error", extra={'error': str(e)}) return (0, 0) def get_completed_achievements(chat_id: int, user_id: int) -> List[str]: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute(''' SELECT at.description FROM user_achievements ua JOIN achievement_types at ON ua.type_id = at.type_id WHERE ua.chat_id = ? AND ua.user_id = ? AND ua.status = 'approved' ''', (chat_id, user_id)) results: List[str] = [row[0] for row in cursor.fetchall()] return results except sqlite3.Error as e: log_structured("Get completed achievements error", extra={'error': str(e)}) return [] @handle_exceptions async def menu_callback(update: Update, context: CallbackContext) -> None: query = update.callback_query await query.answer() data = query.data if data == "menu_back_profile": await myprofile(update, context) elif data == "menu_mypoints": await mypoints(update, context) elif data == "menu_shop": await shop(update, context) elif data == "menu_achievements": await show_achievements(update, context) elif data == "menu_battlepass": await battlepass(update, context) elif data == "menu_quests": await quests(update, context) elif data == "menu_tournaments": await tournaments(update, context) elif data == "menu_referrals": await referrals(update, context) elif data == "menu_daily_bonus": await claim_daily_bonus(update, context) elif data == "menu_settings": await settings_menu(update, context) elif data.startswith("history_"): await history_callback(update, context) elif data == "menu_exit": await query.edit_message_text("Профиль закрыт.") elif data.startswith("settings_"): await settings_callback(update, context) else: log_structured("Unknown menu callback", extra={'data': data}) await query.edit_message_text("Неизвестная команда.") @handle_exceptions async def show_achievements(update: Update, context: CallbackContext) -> None: user = update.callback_query.from_user chat_id_for_data: int = get_effective_chat_id(update) completed_ach: List[str] = get_completed_achievements(chat_id_for_data, user.id) ach_text: str = "\n".join([f"- {ach}" for ach in completed_ach]) if completed_ach else "Нет выполненных достижений" text: str = f"🏆 Ваши достижения:\n{ach_text}" keyboard: List[List[InlineKeyboardButton]] = [[InlineKeyboardButton("Назад", callback_data="menu_back_profile")]] reply_markup = InlineKeyboardMarkup(keyboard) await update.callback_query.edit_message_text(text, reply_markup=reply_markup) @command_wrapper @handle_exceptions async def adminpanel(update: Update, context: CallbackContext) -> None: if update.message: user = update.message.from_user elif update.callback_query: user = update.callback_query.from_user else: return if not is_admin(user.id): await update.effective_message.reply_text('У вас нет прав.') return keyboard: List[List[InlineKeyboardButton]] = [ [InlineKeyboardButton("Выдать баллы", callback_data="admin_givepoints")], [InlineKeyboardButton("Удалить баллы", callback_data="admin_removepoints")], [InlineKeyboardButton("Добавить VIP", callback_data="admin_addvip")], [InlineKeyboardButton("Удалить VIP", callback_data="admin_removevip")], [InlineKeyboardButton("Добавить предмет", callback_data="admin_additem")], [InlineKeyboardButton("Добавить EXP", callback_data="admin_addexp")], [InlineKeyboardButton("Создать квест", callback_data="admin_createquest")], [InlineKeyboardButton("Создать турнир", callback_data="admin_createtournament")], [InlineKeyboardButton("Создать Battle Pass", callback_data="admin_createbp")], [InlineKeyboardButton("Выдать всем баллы", callback_data="admin_giveall")], [InlineKeyboardButton("Добавить достижение", callback_data="admin_addach")], [InlineKeyboardButton("Плагины", callback_data="admin_plugins")], [InlineKeyboardButton("Назад", callback_data="admin_back")] ] reply_markup = InlineKeyboardMarkup(keyboard) if update.callback_query: await update.callback_query.edit_message_text('Админ панель:', reply_markup=reply_markup) else: await update.message.reply_text('Админ панель:', reply_markup=reply_markup) @handle_exceptions async def admin_callback(update: Update, context: CallbackContext) -> None: query = update.callback_query await query.answer() data = query.data if data == "admin_givepoints": await query.edit_message_text('Используйте /givepoints @username количество причина') elif data == "admin_removepoints": await query.edit_message_text('Используйте /removepoints @username количество причина') elif data == "admin_addvip": await query.edit_message_text('Используйте /addvip @username') elif data == "admin_removevip": await query.edit_message_text('Используйте /removevip @username') elif data == "admin_additem": await query.edit_message_text('Используйте /additem имя цена описание [vip_days] [количество]') elif data == "admin_addexp": await query.edit_message_text('Используйте /addexp цена exp_amount') elif data == "admin_createquest": await query.edit_message_text('Используйте /create_quest описание награда') elif data == "admin_createtournament": await query.edit_message_text('Используйте /create_tournament имя описание prize_pool') elif data == "admin_createbp": await query.edit_message_text('Используйте /create_battlepass уровни rewards_json') elif data == "admin_giveall": await query.edit_message_text('Используйте /giveallpoints количество причина') elif data == "admin_addach": await query.edit_message_text('Используйте /add_ach_type "имя" "описание" тип_условия значение_условия награда') elif data == "admin_plugins": text = '📦 Управление плагинами:\n' for name, info in plugins.items(): status = 'Вкл' if info['enabled'] else 'Выкл' text += f'{name}: {status} - {info["description"]}\n' keyboard: List[List[InlineKeyboardButton]] = [ [InlineKeyboardButton("Включить плагин", callback_data="admin_plugin_enable")], [InlineKeyboardButton("Выключить плагин", callback_data="admin_plugin_disable")], [InlineKeyboardButton("Назад", callback_data="admin_back")] ] reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text(text, reply_markup=reply_markup) elif data == "admin_plugin_enable": await query.edit_message_text('Используйте /plugin_enable имя') elif data == "admin_plugin_disable": await query.edit_message_text('Используйте /plugin_disable имя') elif data == "admin_back": await myprofile(update, context) else: log_structured("Unknown admin callback", extra={'data': data}) await query.edit_message_text("Неизвестная команда.") @command_wrapper @handle_exceptions async def help_command(update: Update, context: CallbackContext) -> None: if update.message: user = update.message.from_user chat = update.message.chat elif update.callback_query: user = update.callback_query.from_user chat = update.callback_query.message.chat else: return page: int = 1 if context.args: try: page = int(context.args[0]) except ValueError: page = 1 help_pages: List[str] = [] page1 = "📖 Доступные команды (Страница 1/4):\n\n" page1 += "🔹 Общие команды:\n" page1 += "/start - Запустить бота\n" page1 += "/help - Показать это сообщение\n" page1 += "/myprofile - Ваш профиль и меню\n" page1 += "/mypoints - Посмотреть свои баллы\n" page1 += "/pointstable - Показать таблицу баллов\n" page1 += "/transfer [@username] [количество] - Передать баллы другому пользователю\n" page1 += "/shop - Посмотреть магазин\n" page1 += "/buy [id или название] - Купить предмет\n" page1 += "/achievement [описание] [баллы] - Запросить награду за задание\n" page1 += "/clanstats - Статистика клана\n" page1 += "/quests - Посмотреть квесты\n" page1 += "/complete_quest [id квеста] - Завершить квест\n" page1 += "/tournaments - Посмотреть турниры\n" page1 += "/join_tournament [id турнира] - Присоединиться к турниру\n" page1 += "/battlepass - Посмотреть Battle Pass\n" page1 += "/referral - Получить реферальную ссылку\n" page1 += "/history - Просмотреть историю\n\n" help_pages.append(page1) page2 = "📖 Доступные команды (Страница 2/4):\n\n" page2 += "🎮 Мини-игры:\n" page2 += "/roulette [ставка] - Сыграть в рулетку. Ставка на красное, шанс 48% выигрыша 2x.\n" page2 += "/dice [ставка] - Бросить кости против бота. Больше число - победа.\n" page2 += "/coinflip [ставка] [орёл/решка] - Подбросить монетку. Угадайте сторону.\n" page2 += "/slots [ставка] - Игра в слот-машину. Три в ряд - джекпот.\n" page2 += "/blackjack [ставка] - Игра в блэкджек (21). Hit или Stand.\n" page2 += "/duel [@username] [ставка] - Вызвать пользователя на дуэль. Рандомный победитель.\n" page2 += "/highlow [@username] [ставка] - Выше/ниже карта. Угадайте относительно первой карты.\n" page2 += "/coinflip_pvp [@username] [ставка] - Монетка PvP. Выберите сторону.\n" page2 += "/dice_pvp [@username] [ставка] - Кости PvP. Бросьте кости, выше число - победа.\n" page2 += "/wheel [ставка] - Колесо фортуны. Крутите колесо для множителей.\n\n" help_pages.append(page2) page3 = "📖 Доступные команды (Страница 3/4):\n\n" if is_admin(user.id): page3 += "🔸 Команды для админов:\n" page3 += "/adminpanel - Админ панель\n" page3 += "/givepoints [@username] [количество] [причина] - Выдать баллы\n" page3 += "/removepoints [@username] [количество] [причина] - Удалить баллы\n" page3 += "/showpoints [@username] - Посмотреть баллы пользователя\n" page3 += "/clearpoints [chat_id] - Очистить все баллы в чате\n" page3 += "/addvip [@username] - Выдать VIP статус (постоянный)\n" page3 += "/removevip [@username] - Забрать VIP статус\n" page3 += "/refresh [@username] - Обновить данные пользователя\n" page3 += "/approve_ach [id достижения] - Одобрить достижение\n" page3 += "/reject_ach [id достижения] [причина] - Отклонить достижение\n" page3 += "/additem [имя] [цена] [описание] [vip_days=0] [количество=1] - Добавить предмет в магазин\n" page3 += "/addexp [цена] [exp_amount] - Добавить EXP в магазин\n" page3 += "/edititem [id] [цена] [описание] [vip_days] [количество] - Изменить предмет\n" page3 += "/delitem [id] - Удалить предмет\n" page3 += "/clearshop - Очистить магазин\n" page3 += "/create_quest [описание] [награда] - Создать квест\n" page3 += "/edit_quest [id] [описание] [награда] - Редактировать квест\n" page3 += "/del_quest [id] - Удалить квест\n" page3 += "/approve_quest [id завершения] - Одобрить завершение квеста\n" page3 += "/reject_quest [id завершения] - Отклонить завершение квеста\n" page3 += "/create_tournament [имя] [описание] [prize_pool] - Создать турнир\n" page3 += "/set_tournament_status [id] [status] - Изменить статус турнира\n" page3 += "/end_tournament [id турнира] - Завершить турнир\n" page3 += "/create_battlepass [уровни] [rewards_json] - Создать Battle Pass\n" page3 += "/edit_bp [id] [уровни] [rewards_json] - Редактировать BP\n" page3 += "/del_bp [id] - Удалить BP\n" page3 += "/giveallpoints [количество] [причина] - Выдать баллы всем\n" page3 += "/add_ach_type [имя] [описание] [condition_type] [condition_value] [награда] - Добавить тип достижения\n" page3 += "/del_ach_type [id] - Удалить тип достижения\n" page3 += "/approve_user_ach [id достижения] - Одобрить достижение пользователя\n" page3 += "/reject_user_ach [id достижения] - Отклонить достижение пользователя\n" page3 += "/plugins_list - Список плагинов\n" page3 += "/plugin_enable [имя] - Включить плагин\n" page3 += "/plugin_disable [имя] - Выключить плагин\n" page3 += "/seehistory [@username] [type] - Просмотреть историю пользователя\n\n" help_pages.append(page3) page4 = "📖 Доступные команды (Страница 4/4):\n\n" if is_creator_mode_active(user.id): page4 += "👑 Команды создателя:\n" page4 += "/zeushome [пароль] - Активировать режим создателя\n" page4 += "/addadmin [@username] - Добавить админа бота\n" page4 += "/deladmin [@username] - Удалить админа бота\n" page4 += "/mychannels - Показать все чаты где есть бот\n" page4 += "/toggle_logs - Включить/отключить логирование\n" page4 += "/listcreators - Список активных создателей\n" page4 += "/deactivatecreator [@username] - Деактивировать режим у пользователя\n" page4 += get_plugin_commands() help_pages.append(page4) total_pages: int = len(help_pages) if page > total_pages: page = total_pages if page < 1: page = 1 response: str = help_pages[page - 1] keyboard: List[InlineKeyboardButton] = [] if page > 1: keyboard.append(InlineKeyboardButton("⬅️ Назад", callback_data=f"help_page_{page-1}")) if page < total_pages: keyboard.append(InlineKeyboardButton("Вперед ➡️", callback_data=f"help_page_{page+1}")) reply_markup = InlineKeyboardMarkup([keyboard]) if keyboard else None if update.callback_query: await update.callback_query.edit_message_text(response, reply_markup=reply_markup) else: await update.message.reply_text(response, reply_markup=reply_markup) @handle_exceptions async def help_callback(update: Update, context: CallbackContext) -> None: query = update.callback_query await query.answer() data = query.data if data.startswith('help_page_'): page = int(data.split('_')[2]) user = query.from_user help_pages: List[str] = [] page1 = "📖 Доступные команды (Страница 1/4):\n\n" page1 += "🔹 Общие команды:\n" page1 += "/start - Запустить бота\n" page1 += "/help - Показать это сообщение\n" page1 += "/myprofile - Ваш профиль\n\n\n" help_pages.append(page1) page2 = "📖 Доступные команды (Страница 2/4):\n\n" page2 += "🎮 Мини-игры:\n" page2 += "/roulette [ставка] - Сыграть в рулетку. Ставка на красное, шанс 48% выигрыша 2x.\n" page2 += "/dice [ставка] - Бросить кости против бота. Больше число - победа.\n" page2 += "/coinflip [ставка] [орёл/решка] - Подбросить монетку. Угадайте сторону.\n" page2 += "/slots [ставка] - Игра в слот-машину. Три в ряд - джекпот.\n" page2 += "/blackjack [ставка] - Игра в блэкджек (21). Hit или Stand.\n" page2 += "/duel [@username] [ставка] - Вызвать пользователя на дуэль. Рандомный победитель.\n" page2 += "/highlow [@username] [ставка] - Выше/ниже карта. Угадайте относительно первой карты.\n" page2 += "/coinflip_pvp [@username] [ставка] - Монетка PvP. Выберите сторону.\n" page2 += "/dice_pvp [@username] [ставка] - Кости PvP. Бросьте кости, выше число - победа.\n" page2 += "/wheel [ставка] - Колесо фортуны. Крутите колесо для множителей.\n\n" help_pages.append(page2) page3 = "📖 Доступные команды (Страница 3/4):\n\n" if is_admin(user.id): page3 += "🔸 Команды для админов:\n" page3 += "/adminpanel - Админ панель\n" page3 += "/givepoints [@username] [количество] [причина] - Выдать баллы\n" page3 += "/removepoints [@username] [количество] [причина] - Удалить баллы\n" page3 += "/showpoints [@username] - Посмотреть баллы пользователя\n" page3 += "/clearpoints [chat_id] - Очистить все баллы в чате\n" page3 += "/addvip [@username] - Выдать VIP статус (постоянный)\n" page3 += "/removevip [@username] - Забрать VIP статус\n" page3 += "/refresh [@username] - Обновить данные пользователя\n" page3 += "/approve_ach [id достижения] - Одобрить достижение\n" page3 += "/reject_ach [id достижения] [причина] - Отклонить достижение\n" page3 += "/additem [имя] [цена] [описание] [vip_days=0] [количество=1] - Добавить предмет в магазин\n" page3 += "/addexp [цена] [exp_amount] - Добавить EXP в магазин\n" page3 += "/edititem [id] [цена] [описание] [vip_days] [количество] - Изменить предмет\n" page3 += "/delitem [id] - Удалить предмет\n" page3 += "/clearshop - Очистить магазин\n" page3 += "/create_quest [описание] [награда] - Создать квест\n" page3 += "/edit_quest [id] [описание] [награда] - Редактировать квест\n" page3 += "/del_quest [id] - Удалить квест\n" page3 += "/approve_quest [id завершения] - Одобрить завершение квеста\n" page3 += "/reject_quest [id завершения] - Отклонить завершение квеста\n" page3 += "/create_tournament [имя] [описание] [prize_pool] - Создать турнир\n" page3 += "/set_tournament_status [id] [status] - Изменить статус турнира\n" page3 += "/end_tournament [id турнира] - Завершить турнир\n" page3 += "/create_battlepass [уровни] [rewards_json] - Создать Battle Pass\n" page3 += "/edit_bp [id] [уровни] [rewards_json] - Редактировать BP\n" page3 += "/del_bp [id] - Удалить BP\n" page3 += "/giveallpoints [количество] [причина] - Выдать баллы всем\n" page3 += "/add_ach_type [имя] [описание] [condition_type] [condition_value] [награда] - Добавить тип достижения\n" page3 += "/del_ach_type [id] - Удалить тип достижения\n" page3 += "/approve_user_ach [id достижения] - Одобрить достижение пользователя\n" page3 += "/reject_user_ach [id достижения] - Отклонить достижение пользователя\n" page3 += "/plugins_list - Список плагинов\n" page3 += "/plugin_enable [имя] - Включить плагин\n" page3 += "/plugin_disable [имя] - Выключить плагин\n" page3 += "/seehistory [@username] [type] - Просмотреть историю пользователя\n\n" help_pages.append(page3) page4 = "📖 Доступные команды (Страница 4/4):\n\n" if is_creator_mode_active(user.id): page4 += "👑 Команды создателя:\n" page4 += "/zeushome [пароль] - Активировать режим создателя\n" page4 += "/addadmin [@username] - Добавить админа бота\n" page4 += "/deladmin [@username] - Удалить админа бота\n" page4 += "/mychannels - Показать все чаты где есть бот\n" page4 += "/toggle_logs - Включить/отключить логирование\n" page4 += "/listcreators - Список активных создателей\n" page4 += "/deactivatecreator [@username] - Деактивировать режим у пользователя\n" page4 += get_plugin_commands() help_pages.append(page4) total_pages: int = len(help_pages) response: str = help_pages[page - 1] keyboard: List[InlineKeyboardButton] = [] if page > 1: keyboard.append(InlineKeyboardButton("⬅️ Назад", callback_data=f"help_page_{page-1}")) if page < total_pages: keyboard.append(InlineKeyboardButton("Вперед ➡️", callback_data=f"help_page_{page+1}")) reply_markup = InlineKeyboardMarkup([keyboard]) if keyboard else None await query.edit_message_text(response, reply_markup=reply_markup) else: log_structured("Unknown help callback", extra={'data': data}) await query.edit_message_text("Неизвестная команда.") @command_wrapper @handle_exceptions async def mypoints(update: Update, context: CallbackContext) -> None: if update.message: user = update.message.from_user chat = update.message.chat message = update.message elif update.callback_query: user = update.callback_query.from_user chat = update.callback_query.message.chat message = update.callback_query.message else: return effective_chat_id: int = get_effective_chat_id(update) add_or_update_user(effective_chat_id, user.id, user.username) points: int = get_user_points(effective_chat_id, user.id) vip_status: str = "👑" if is_vip_user(effective_chat_id, user.id) else "" text: str = f'{vip_status}У вас {points} баллов.' keyboard: List[List[InlineKeyboardButton]] = [[InlineKeyboardButton("Назад", callback_data="menu_back_profile")]] reply_markup = InlineKeyboardMarkup(keyboard) if update.callback_query: await update.callback_query.edit_message_text(text, reply_markup=reply_markup) else: await message.reply_text(text, reply_markup=reply_markup) @command_wrapper @handle_exceptions async def showpoints(update: Update, context: CallbackContext) -> None: user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) if not is_admin(user.id): await update.message.reply_text('У вас нет прав для выполнения этой команды.') return if len(context.args) < 1: await update.message.reply_text('Использование: /showpoints @username') return target_username: str = context.args[0].lstrip('@') target_user_id: Optional[int] = await resolve_username(update, context, target_username) if not target_user_id: return points: int = get_user_points(effective_chat_id, target_user_id) vip_status: str = "👑" if is_vip_user(effective_chat_id, target_user_id) else "" await update.message.reply_text(f'{vip_status}Пользователь @{target_username} имеет {points} баллов.') @command_wrapper @handle_exceptions async def givepoints(update: Update, context: CallbackContext) -> None: if update.callback_query: await update.callback_query.edit_message_text('Используйте /givepoints @username количество причина') return user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) if not is_admin(user.id): await update.message.reply_text('У вас нет прав для выполнения этой команды.') return if len(context.args) < 3: await update.message.reply_text('Использование: /givepoints @username количество причина') return target_username: str = context.args[0].lstrip('@') try: amount: int = int(context.args[1]) reason: str = ' '.join(context.args[2:]) except ValueError: await update.message.reply_text('Количество должно быть числом.') return if amount <= 0: await update.message.reply_text('Количество должно быть положительным числом.') return target_user_id: Optional[int] = await resolve_username(update, context, target_username) if not target_user_id: return current_points: int = get_user_points(effective_chat_id, target_user_id) new_points: int = current_points + amount await update_user_points(effective_chat_id, target_user_id, new_points, context, f"Выдано админом @{user.username}: {reason}") vip_status: str = "👑" if is_vip_user(effective_chat_id, target_user_id) else "" await update.message.reply_text( f'{vip_status}✅ Выдано {amount} баллов пользователю @{target_username}\n' f'📝 Причина: {reason}\n' f'💰 Новый баланс: {new_points} баллов' ) @command_wrapper @handle_exceptions async def removepoints(update: Update, context: CallbackContext) -> None: if update.callback_query: await update.callback_query.edit_message_text('Используйте /removepoints @username количество причина') return user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) if not is_admin(user.id): await update.message.reply_text('У вас нет прав для выполнения этой команды.') return if len(context.args) < 3: await update.message.reply_text('Использование: /removepoints @username количество причина') return target_username: str = context.args[0].lstrip('@') try: amount: int = int(context.args[1]) reason: str = ' '.join(context.args[2:]) except ValueError: await update.message.reply_text('Количество должно быть числом.') return if amount <= 0: await update.message.reply_text('Количество должно быть положительным числом.') return target_user_id: Optional[int] = await resolve_username(update, context, target_username) if not target_user_id: return current_points: int = get_user_points(effective_chat_id, target_user_id) if current_points < amount: await update.message.reply_text( f'Недостаточно баллов у пользователя @{target_username}. ' f'Текущий баланс: {current_points}' ) return new_points: int = current_points - amount await update_user_points(effective_chat_id, target_user_id, new_points, context, f"Удалено админом @{user.username}: {reason}") vip_status: str = "👑" if is_vip_user(effective_chat_id, target_user_id) else "" await update.message.reply_text( f'{vip_status}❌ Удалено {amount} баллов у пользователя @{target_username}\n' f'📝 Причина: {reason}\n' f'💰 Новый баланс: {new_points} баллов' ) @command_wrapper @handle_exceptions async def pointstable(update: Update, context: CallbackContext) -> None: if update.message: user = update.message.from_user chat = update.message.chat elif update.callback_query: user = update.callback_query.from_user chat = update.callback_query.message.chat else: return effective_chat_id: int = get_effective_chat_id(update) add_or_update_user(effective_chat_id, user.id, user.username) page: int = 1 if context.args: try: page = int(context.args[0]) if page < 1: page = 1 except ValueError: page = 1 try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute(''' SELECT user_id, username, points, vip_status FROM users WHERE chat_id = ? ORDER BY points DESC, vip_status DESC ''', (effective_chat_id,)) top_users: List[Tuple[int, str, int, bool]] = cursor.fetchall() except sqlite3.Error as e: log_structured("Points table query error", extra={'error': str(e)}) top_users = [] if not top_users: await update.effective_message.reply_text('В этом чате еще нет пользователей с баллами.') return users_per_page: int = 10 total_pages: int = (len(top_users) + users_per_page - 1) // users_per_page if page > total_pages: page = total_pages start_idx: int = (page - 1) * users_per_page end_idx: int = min(start_idx + users_per_page, len(top_users)) response: str = f'🏆 Топ пользователей по баллам (Страница {page}/{total_pages}):\n\n' for i in range(start_idx, end_idx): user_id, username, points, vip_status = top_users[i] if not username or username.lower() == 'none': username = f"user_{user_id}" vip_icon: str = "👑 " if vip_status else "" response += f'{i+1}. {vip_icon}@{username}: {points} баллов\n' reply_markup = None if total_pages > 1: keyboard: List[InlineKeyboardButton] = [] if page > 1: keyboard.append(InlineKeyboardButton("⬅️ Назад", callback_data=f"points_page_{page-1}_{effective_chat_id}")) if page < total_pages: keyboard.append(InlineKeyboardButton("Вперед ➡️", callback_data=f"points_page_{page+1}_{effective_chat_id}")) if keyboard: reply_markup = InlineKeyboardMarkup([keyboard]) if update.callback_query: await update.callback_query.edit_message_text(response, reply_markup=reply_markup) else: await update.message.reply_text(response, reply_markup=reply_markup) @handle_exceptions async def points_table_callback(update: Update, context: CallbackContext) -> None: query = update.callback_query await query.answer() data = query.data if data.startswith('points_page_'): parts = data.split('_') page = int(parts[2]) chat_id = int(parts[3]) try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute(''' SELECT user_id, username, points, vip_status FROM users WHERE chat_id = ? ORDER BY points DESC, vip_status DESC ''', (chat_id,)) top_users: List[Tuple[int, str, int, bool]] = cursor.fetchall() except sqlite3.Error as e: log_structured("Points table callback query error", extra={'error': str(e)}) top_users = [] users_per_page: int = 10 total_pages: int = (len(top_users) + users_per_page - 1) // users_per_page if page > total_pages: page = total_pages start_idx: int = (page - 1) * users_per_page end_idx: int = min(start_idx + users_per_page, len(top_users)) response: str = f'🏆 Топ пользователей по баллам (Страница {page}/{total_pages}):\n\n' for i in range(start_idx, end_idx): user_id, username, points, vip_status = top_users[i] if not username or username.lower() == 'none': username = f"user_{user_id}" vip_icon: str = "👑 " if vip_status else "" response += f'{i+1}. {vip_icon}@{username}: {points} баллов\n' keyboard: List[InlineKeyboardButton] = [] if page > 1: keyboard.append(InlineKeyboardButton("⬅️ Назад", callback_data=f"points_page_{page-1}_{chat_id}")) if page < total_pages: keyboard.append(InlineKeyboardButton("Вперед ➡️", callback_data=f"points_page_{page+1}_{chat_id}")) reply_markup = InlineKeyboardMarkup([keyboard]) if keyboard else None await query.edit_message_text(response, reply_markup=reply_markup) else: log_structured("Unknown points table callback", extra={'data': data}) await query.edit_message_text("Неизвестная команда.") @command_wrapper @handle_exceptions async def clearpoints(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав для выполнения этой команды.') return if len(context.args) < 1: await update.message.reply_text('Использование: /clearpoints chat_id') return try: chat_id: int = int(context.args[0]) except ValueError: await update.message.reply_text('chat_id должен быть числом.') return clear_points(chat_id) await send_to_log_channel(f"🧹 Очистка баллов в чате {chat_id} админом @{user.username}", context, category="Админ") await update.message.reply_text(f'Все баллы в чате {chat_id} очищены.') @command_wrapper @handle_exceptions async def transfer(update: Update, context: CallbackContext) -> None: user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) add_or_update_user(effective_chat_id, user.id, user.username) if len(context.args) < 2: await update.message.reply_text('Использование: /transfer @username количество') return target_username: str = context.args[0].lstrip('@') try: amount: int = int(context.args[1]) except ValueError: await update.message.reply_text('Количество должно быть числом.') return if amount <= 0: await update.message.reply_text('Количество должно быть положительным числом.') return # Добавляем проверку на самоперевод if target_username.lower() == user.username.lower(): await update.message.reply_text('❌ Нельзя переводить баллы самому себе!') return sender_points: int = get_user_points(effective_chat_id, user.id) if sender_points < amount: await update.message.reply_text( f'❌ Недостаточно баллов для перевода!\n' f'💰 Ваш баланс: {sender_points} баллов\n' f'💸 Требуется: {amount} баллов' ) return target_user_id: Optional[int] = await resolve_username(update, context, target_username) if not target_user_id: return is_vip: bool = is_vip_user(effective_chat_id, user.id) daily_total: int = get_daily_transfer_total(user.id) daily_limit: int = VIP_DAILY_TRANSFER_LIMIT if is_vip else DAILY_TRANSFER_LIMIT if daily_total + amount > daily_limit: remaining: int = daily_limit - daily_total await update.message.reply_text( f'❌ Превышен дневной лимит переводов!\n' f'📊 Вы уже перевели {daily_total} баллов сегодня\\n' f'💰 Осталось доступно: {max(0, remaining)} баллов\n' f'⏰ Лимит сбрасывается через 24 часа' ) return # Транзакционная обработка try: with sqlite3.connect(DB_PATH, timeout=30) as conn: conn.isolation_level = None cursor: sqlite3.Cursor = conn.cursor() cursor.execute('BEGIN') sender_new_points: int = sender_points - amount target_points: int = get_user_points(effective_chat_id, target_user_id) target_new_points: int = target_points + amount execute_with_retry(cursor, 'UPDATE users SET points = ? WHERE chat_id = ? AND user_id = ?', (sender_new_points, effective_chat_id, user.id)) execute_with_retry(cursor, 'UPDATE users SET points = ? WHERE chat_id = ? AND user_id = ?', (target_new_points, effective_chat_id, target_user_id)) execute_with_retry(cursor, ''' INSERT INTO transfers (from_user_id, from_username, to_user_id, to_username, amount, chat_id) VALUES (?, ?, ?, ?, ?, ?) ''', (user.id, user.username, target_user_id, target_username, amount, effective_chat_id)) conn.commit() except sqlite3.Error as e: conn.rollback() log_structured("Transfer transaction error", extra={'error': str(e)}) await update.message.reply_text('Ошибка перевода. Попробуйте позже.') return # Инвалидация кэша user_points_cache.pop((effective_chat_id, user.id), None) user_points_cache.pop((effective_chat_id, target_user_id), None) await send_to_log_channel(f"💸 Перевод:\nОт: {user.id} (@{user.username})\nК: {target_user_id} (@{target_username})\nСумма: {amount}\nЧат: {effective_chat_id}", context, category="Перевод") vip_status: str = "👑" if is_vip else "" response: str = ( f'{vip_status}✅ Перевод выполнен успешно!\n' f'👤 Отправитель: @{user.username}\n' f'👥 Получатель: @{target_username}\n' f'💰 Сумма: {amount} баллов\n' f'📊 Ваш новый баланс: {sender_new_points} баллов\n' f'💳 Новый баланс получателя: {target_new_points} баллов\n' ) daily_total_after: int = daily_total + amount response += f'📅 Дневной лимит использован: {daily_total_after}/{daily_limit}' await update.message.reply_text(response) @command_wrapper @handle_exceptions async def add_vip(update: Update, context: CallbackContext) -> None: if update.callback_query: await update.callback_query.edit_message_text('Используйте /addvip @username') return user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) if not is_admin(user.id): await update.message.reply_text('У вас нет прав для выполнения этой команды.') return if len(context.args) < 1: await update.message.reply_text('Использование: /addvip @username') return target_username: str = context.args[0].lstrip('@') target_user_id: Optional[int] = await resolve_username(update, context, target_username) if not target_user_id: return await set_vip_status(effective_chat_id, target_user_id, True, context, permanent=True) await update.message.reply_text(f'✅ Пользователь @{target_username} получил постоянный VIP статус!') @command_wrapper @handle_exceptions async def remove_vip(update: Update, context: CallbackContext) -> None: if update.callback_query: await update.callback_query.edit_message_text('Используйте /removevip @username') return user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) if not is_admin(user.id): await update.message.reply_text('У вас нет прав для выполнения этой команды.') return if len(context.args) < 1: await update.message.reply_text('Использование: /removevip @username') return target_username: str = context.args[0].lstrip('@') target_user_id: Optional[int] = await resolve_username(update, context, target_username) if not target_user_id: return await set_vip_status(effective_chat_id, target_user_id, False, context) await update.message.reply_text(f'✅ VIP статус удален у пользователя @{target_username}.') @command_wrapper @handle_exceptions async def shop(update: Update, context: CallbackContext) -> None: if update.message: message = update.message elif update.callback_query: message = update.callback_query.message else: return items: List[Dict[str, Any]] = get_shop_items() if not items: text = '🛒 Магазин пуст. Обратитесь к админу.' else: text = "🛒 Магазин клана:\n\n" for item in items: if item['quantity'] <= 0: continue vip_info: str = f" (VIP на {item['vip_days']} дней)" if item['is_vip'] else "" exp_info: str = f" (EXP: {item['exp_amount']})" if item['exp_amount'] > 0 else "" quantity_info: str = f" [Осталось: {item['quantity']}]" if item['quantity'] > 0 else " [Нет в наличии]" text += f"ID {item['id']}. {item['name']}{vip_info}{exp_info}{quantity_info}: {item['price']} баллов - {item['desc']}\n" text += "\nИспользуйте /buy [ID] или /buy [название] для покупки." keyboard: List[List[InlineKeyboardButton]] = [[InlineKeyboardButton("Назад", callback_data="menu_back_profile")]] reply_markup = InlineKeyboardMarkup(keyboard) if update.callback_query: await update.callback_query.edit_message_text(text, reply_markup=reply_markup) else: await message.reply_text(text, reply_markup=reply_markup) @command_wrapper @handle_exceptions async def buy(update: Update, context: CallbackContext) -> None: user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) add_or_update_user(effective_chat_id, user.id, user.username) if len(context.args) < 1: await update.message.reply_text('Использование: /buy [ID] или /buy [название]') return arg: str = ' '.join(context.args).lower() items: List[Dict[str, Any]] = get_shop_items() item: Optional[Dict[str, Any]] = None if arg.isdigit(): item_id: int = int(arg) item = next((it for it in items if it['id'] == item_id), None) else: item = next((it for it in items if it['name'].lower() == arg), None) if not item: await update.message.reply_text('Предмет не найден. Используйте /shop.') return if item['quantity'] <= 0: await update.message.reply_text('Этот товар закончился.') return price: int = item['price'] points: int = get_user_points(effective_chat_id, user.id) if points < price: await update.message.reply_text(f'Недостаточно баллов. Нужно: {price}, у вас: {points}') return # Транзакционная обработка покупки try: with sqlite3.connect(DB_PATH, timeout=30) as conn: conn.isolation_level = None cursor: sqlite3.Cursor = conn.cursor() cursor.execute('BEGIN') new_points: int = points - price execute_with_retry(cursor, 'UPDATE users SET points = ? WHERE chat_id = ? AND user_id = ?', (new_points, effective_chat_id, user.id)) new_quantity: int = item['quantity'] - 1 if new_quantity <= 0: execute_with_retry(cursor, 'DELETE FROM shop_items WHERE item_id = ?', (item['id'],)) else: execute_with_retry(cursor, 'UPDATE shop_items SET quantity = ? WHERE item_id = ?', (new_quantity, item['id'])) if item['is_vip']: expires = datetime.now() + timedelta(days=item['vip_days']) execute_with_retry(cursor, 'UPDATE users SET vip_status = 1, vip_expires = ? WHERE chat_id = ? AND user_id = ?', (expires.isoformat(), effective_chat_id, user.id)) if item['exp_amount'] > 0: execute_with_retry(cursor, 'UPDATE users SET battle_pass_exp = battle_pass_exp + ? WHERE chat_id = ? AND user_id = ?', (item['exp_amount'], effective_chat_id, user.id)) execute_with_retry(cursor, ''' INSERT INTO purchases (user_id, username, item_name, cost, chat_id) VALUES (?, ?, ?, ?, ?) ''', (user.id, user.username, item['name'], price, effective_chat_id)) conn.commit() except sqlite3.Error as e: conn.rollback() log_structured("Buy transaction error", extra={'error': str(e)}) await update.message.reply_text('Ошибка покупки. Попробуйте позже.') return # Инвалидация кэша user_points_cache.pop((effective_chat_id, user.id), None) # Обработка EXP (если нужно, вызвать level up) if item['exp_amount'] > 0: await add_battle_pass_exp(effective_chat_id, user.id, item['exp_amount'], context) await send_to_log_channel(f"**Покупка из магазина**\nПользователь: {user.id} (@{user.username})\nПредмет: {item['name']}\nЦена: {price}\nЧат: {effective_chat_id}", context, category="Покупка") vip_msg: str = f"\n👑 VIP активирован на {item['vip_days']} дней!" if item['is_vip'] else "" exp_msg: str = f"\n📈 +{item['exp_amount']} EXP для Battle Pass!" if item['exp_amount'] > 0 else "" await update.message.reply_text(f'✅ Куплено: {item["name"].replace("_", " ").title()} за {price} баллов!{vip_msg}{exp_msg}\n{item["desc"]}\n💰 Новый баланс: {new_points}') async def add_battle_pass_exp(chat_id: int, user_id: int, amount: int, context: CallbackContext) -> None: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE users SET battle_pass_exp = battle_pass_exp + ? WHERE chat_id = ? AND user_id = ?', (amount, chat_id, user_id)) cursor.execute('SELECT battle_pass_level, battle_pass_exp FROM users WHERE chat_id = ? AND user_id = ?', (chat_id, user_id)) level, exp = cursor.fetchone() # Получаем текущий BP (предполагаем последний) cursor.execute('SELECT bp_id, levels, rewards FROM battle_passes ORDER BY created_date DESC LIMIT 1') bp = cursor.fetchone() if bp: bp_id, levels, rewards_str = bp try: rewards: Dict[str, Any] = json.loads(rewards_str) except json.JSONDecodeError: rewards = {} if not isinstance(rewards, dict): rewards = {} # Проверяем level up exp_per_level: int = 100 # Пример, можно сделать configurable level_up: bool = False while exp >= exp_per_level: level += 1 exp -= exp_per_level level_up = True if level <= levels: reward = rewards.get(str(level), 0) if isinstance(reward, int): await update_user_points(chat_id, user_id, get_user_points(chat_id, user_id) + reward, context, f"BP level {level} reward") elif reward == 'vip': await set_vip_status(chat_id, user_id, True, context, permanent=False) # Другие награды execute_with_retry(cursor, 'UPDATE users SET battle_pass_level = ?, battle_pass_exp = ? WHERE chat_id = ? AND user_id = ?', (level, exp, chat_id, user_id)) if level_up: # Уведомление notify_bp: bool = get_user_setting(user_id, 'notify_bp') if notify_bp: try: await context.bot.send_message(user_id, f"🎉 Новый уровень Battle Pass: {level}!") except Exception as e: log_structured("BP level up notification error", extra={'error': str(e)}) except sqlite3.Error as e: log_structured("Add battle pass exp error", extra={'error': str(e)}) def get_completed_quests(chat_id: int, user_id: int) -> List[str]: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT q.description FROM quest_completions qc JOIN quests q ON qc.quest_id = q.quest_id WHERE qc.chat_id = ? AND qc.user_id = ? AND qc.status = "approved"', (chat_id, user_id)) results: List[str] = [row[0] for row in cursor.fetchall()] return results except sqlite3.Error as e: log_structured("Get completed quests error", extra={'error': str(e)}) return [] @command_wrapper @handle_exceptions async def additem(update: Update, context: CallbackContext) -> None: if update.callback_query: await update.callback_query.edit_message_text('Используйте /additem имя цена описание [vip_days] [количество]') return user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 3: await update.message.reply_text('Использование: /additem [имя] [цена] [описание] [vip_days=0] [количество=1]') return name: str = context.args[0] try: price: int = int(context.args[1]) except ValueError: await update.message.reply_text('Цена должна быть числом.') return args: List[str] = context.args[2:] vip_days: int = 0 quantity: int = 1 if args and args[-1].isdigit(): quantity = int(args[-1]) args = args[:-1] if args and args[-1].isdigit(): vip_days = int(args[-1]) args = args[:-1] desc: str = ' '.join(args) if args else "" is_vip: int = 1 if vip_days > 0 else 0 try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'INSERT INTO shop_items (name, price, description, is_vip, vip_days, quantity) VALUES (?, ?, ?, ?, ?, ?)', (name, price, desc, is_vip, vip_days, quantity)) await update.message.reply_text(f'✅ Предмет "{name}" добавлен в магазин.') except sqlite3.IntegrityError: await update.message.reply_text('❌ Предмет с таким названием уже существует.') except sqlite3.Error as e: log_structured("Add item error", extra={'error': str(e)}) @command_wrapper @handle_exceptions async def addexp(update: Update, context: CallbackContext) -> None: if update.callback_query: await update.callback_query.edit_message_text('Используйте /addexp цена exp_amount') return user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 2: await update.message.reply_text('Использование: /addexp [цена] [exp_amount]') return try: price: int = int(context.args[0]) exp_amount: int = int(context.args[1]) except ValueError: await update.message.reply_text('Цена и EXP должны быть числами.') return name: str = f"EXP_{exp_amount}" desc: str = f"Дает {exp_amount} EXP для Battle Pass" try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'INSERT INTO shop_items (name, price, description, exp_amount) VALUES (?, ?, ?, ?)', (name, price, desc, exp_amount)) await update.message.reply_text(f'✅ EXP "{name}" добавлен в магазин.') except sqlite3.IntegrityError: await update.message.reply_text('❌ EXP с таким названием уже существует.') except sqlite3.Error as e: log_structured("Add exp error", extra={'error': str(e)}) @command_wrapper @handle_exceptions async def edititem(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 2: await update.message.reply_text('Использование: /edititem [id] [цена] [описание] [vip_days] [количество]') return try: item_id: int = int(context.args[0]) price: int = int(context.args[1]) except ValueError: await update.message.reply_text('ID и цена должны быть числами.') return args: List[str] = context.args[2:] vip_days: int = 0 quantity: int = 1 if args and args[-1].isdigit(): quantity = int(args[-1]) args = args[:-1] if args and args[-1].isdigit(): vip_days = int(args[-1]) args = args[:-1] desc: str = ' '.join(args) if args else "" is_vip: int = 1 if vip_days > 0 else 0 try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT 1 FROM shop_items WHERE item_id = ?', (item_id,)) if not cursor.fetchone(): await update.message.reply_text('❌ Предмет не найден.') return execute_with_retry(cursor, 'UPDATE shop_items SET price = ?, description = ?, is_vip = ?, vip_days = ?, quantity = ? WHERE item_id = ?', (price, desc, is_vip, vip_days, quantity, item_id)) except sqlite3.Error as e: log_structured("Edit item error", extra={'error': str(e)}) await update.message.reply_text(f'✅ Предмет {item_id} обновлен.') @command_wrapper @handle_exceptions async def delitem(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 1: await update.message.reply_text('Использование: /delitem [id]') return try: item_id: int = int(context.args[0]) except ValueError: await update.message.reply_text('ID должен быть числом.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'DELETE FROM shop_items WHERE item_id = ?', (item_id,)) if cursor.rowcount == 0: await update.message.reply_text('❌ Предмет не найден.') else: await update.message.reply_text(f'✅ Предмет {item_id} удален.') except sqlite3.Error as e: log_structured("Del item error", extra={'error': str(e)}) @command_wrapper @handle_exceptions async def clearshop(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'DELETE FROM shop_items') except sqlite3.Error as e: log_structured("Clear shop error", extra={'error': str(e)}) await update.message.reply_text('✅ Магазин полностью очищен.') @command_wrapper @handle_exceptions async def achievement(update: Update, context: CallbackContext) -> None: if update.callback_query: await show_achievements(update, context) return if update.message: user = update.message.from_user chat = update.message.chat message = update.message else: return effective_chat_id: int = get_effective_chat_id(update) add_or_update_user(effective_chat_id, user.id, user.username) reply_markup = None if len(context.args) < 2: text = 'Использование: /achievement [описание] [баллы]' else: description: str = ' '.join(context.args[:-1]) try: points: int = int(context.args[-1]) except ValueError: text = 'Баллы должны быть числом.' await message.reply_text(text) return if points <= 0: text = 'Баллы должны быть положительными.' await message.reply_text(text) return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, ''' INSERT INTO achievements (user_id, username, description, requested_points, chat_id) VALUES (?, ?, ?, ?, ?) ''', (user.id, user.username, description, points, effective_chat_id)) ach_id = cursor.lastrowid except sqlite3.Error as e: log_structured("Achievement insert error", extra={'error': str(e)}) text = 'Ошибка создания запроса.' await message.reply_text(text) return # Создаем инлайн-кнопки для админов keyboard: List[List[InlineKeyboardButton]] = [ [ InlineKeyboardButton("✅ Одобрить", callback_data=f"ach_approve_{ach_id}"), InlineKeyboardButton("❌ Отклонить", callback_data=f"ach_reject_{ach_id}") ] ] reply_markup = InlineKeyboardMarkup(keyboard) text = f'🏆 Запрос на достижение #{ach_id} от @{user.username}:\n' \ f'📝 Описание: {description}\n' \ f'💰 Запрошенные баллы: {points}\n\n' \ f'Админы, используйте кнопки ниже для решения:' await send_to_log_channel(f"🏆 Новый запрос достижения:\nID: {ach_id}\nОт: @{user.username} ({user.id})\nОписание: {description}\nБаллы: {points}\nЧат: {effective_chat_id}", context, category="Достижение") await message.reply_text(text, reply_markup=reply_markup if 'ach_id' in locals() else None) def get_all_admins() -> List[int]: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT user_id FROM admins') admins: List[int] = [row[0] for row in cursor.fetchall()] return admins except sqlite3.Error as e: log_structured("Get all admins error", extra={'error': str(e)}) return [] @handle_exceptions async def achievement_callback(update: Update, context: CallbackContext) -> None: query = update.callback_query await query.answer() data = query.data if not data.startswith('ach_'): return parts = data.split('_') action = parts[1] ach_id = int(parts[2]) user = query.from_user if not is_admin(user.id): await query.answer('Только админы могут одобрять достижения.', show_alert=True) return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT * FROM achievements WHERE ach_id = ?', (ach_id,)) ach: Optional[Tuple[Any, ...]] = cursor.fetchone() except sqlite3.Error as e: log_structured("Achievement callback query error", extra={'error': str(e)}) ach = None if not ach: await query.edit_message_text('Достижение не найдено.') return status: str = ach[5] # 5-й элемент - статус if status != 'pending': await query.edit_message_text('Это достижение уже было обработано.') return if action == 'approve': current_points: int = get_user_points(ach[8], ach[1]) # chat_id, user_id new_points: int = current_points + ach[4] # requested_points await update_user_points(ach[8], ach[1], new_points, context, f"Одобрено достижение {ach[3]}") try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE achievements SET status = "approved", approved_by = ?, approved_date = CURRENT_TIMESTAMP WHERE ach_id = ?', (user.id, ach_id)) except sqlite3.Error as e: log_structured("Approve achievement update error", extra={'error': str(e)}) await query.edit_message_text( f'✅ Задание {ach_id} одобрено! Начислено {ach[4]} баллов @{ach[2]}.') notify_ach: bool = get_user_setting(ach[1], 'notify_ach') if notify_ach: try: await context.bot.send_message(ach[1], f"🎉 Ваше Задание \"{ach[3]}\" одобрено! +{ach[4]} баллов.") except Exception as e: log_structured("Achievement notification error", extra={'error': str(e)}) elif action == 'reject': try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE achievements SET status = "rejected", approved_by = ?, approved_date = CURRENT_TIMESTAMP WHERE ach_id = ?', (user.id, ach_id)) except sqlite3.Error as e: log_structured("Reject achievement update error", extra={'error': str(e)}) await query.edit_message_text(f'❌ Достижение {ach_id} отклонено.') try: await context.bot.send_message(ach[1], f'😔 Ваш запрос достижения #{ach_id} отклонен.') except Exception as e: log_structured("Reject notification error", extra={'error': str(e)}) @command_wrapper @handle_exceptions async def approve_ach(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 1: await update.message.reply_text('Использование: /approve_ach [id достижения]') return try: ach_id: int = int(context.args[0]) except ValueError: await update.message.reply_text('ID должно быть числом.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT * FROM achievements WHERE ach_id = ? AND status = "pending"', (ach_id,)) ach: Optional[Tuple[Any, ...]] = cursor.fetchone() except sqlite3.Error as e: log_structured("Approve ach query error", extra={'error': str(e)}) ach = None if not ach: await update.message.reply_text('Запрос не найден или уже обработан.') return user_id, username, description, requested_points, chat_id = ach[1:6] current_points: int = get_user_points(chat_id, user_id) new_points: int = current_points + requested_points await update_user_points(chat_id, user_id, new_points, context, f"Одобрено достижение {description}") try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE achievements SET status = "approved", approved_by = ?, approved_date = CURRENT_TIMESTAMP WHERE ach_id = ?', (user.id, ach_id)) except sqlite3.Error as e: log_structured("Approve ach update error", extra={'error': str(e)}) await update.message.reply_text(f'✅ Задание {ach_id} одобрено! Начислено {requested_points} баллов @{username}.') notify_ach: bool = get_user_setting(user_id, 'notify_ach') if notify_ach: try: await context.bot.send_message(user_id, f'🎉 Ваше задание "{description}" одобрено! +{requested_points} баллов.') except Exception as e: log_structured("Approve ach notification error", extra={'error': str(e)}) @command_wrapper @handle_exceptions async def reject_ach(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 1: await update.message.reply_text('Использование: /reject_ach [id достижения] [причина]') return try: ach_id: int = int(context.args[0]) except ValueError: await update.message.reply_text('ID должно быть числом.') return reason: str = ' '.join(context.args[1:]) or "Без причины" try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT * FROM achievements WHERE ach_id = ? AND status = "pending"', (ach_id,)) ach: Optional[Tuple[Any, ...]] = cursor.fetchone() except sqlite3.Error as e: log_structured("Reject ach query error", extra={'error': str(e)}) ach = None if not ach: await update.message.reply_text('Запрос не найден или уже обработан.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE achievements SET status = "rejected", approved_by = ?, approved_date = CURRENT_TIMESTAMP WHERE ach_id = ?', (user.id, ach_id)) except sqlite3.Error as e: log_structured("Reject ach update error", extra={'error': str(e)}) await update.message.reply_text(f'✅ Достижение {ach_id} отклонено. Причина: {reason}') username = ach[2] user_id = ach[1] try: await context.bot.send_message(user_id, f'😔 Ваш запрос {ach_id} отклонен. Причина: {reason}') except Exception as e: log_structured("Reject ach notification error", extra={'error': str(e)}) @command_wrapper @handle_exceptions async def add_achievement_type(update: Update, context: CallbackContext) -> None: if update.callback_query: await update.callback_query.edit_message_text('Используйте /add_ach_type "имя" "описание" тип_условия значение_условия награда') return user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return # Объединяем аргументы с shlex для поддержки кавычек try: args: List[str] = shlex.split(update.message.text) except ValueError: args = update.message.text.split() if len(args) < 6: await update.message.reply_text('Использование: /add_ach_type "имя" "описание" тип_условия значение_условия награда\n\n' 'Доступные типы условий:\n' '- messages (количество сообщений)\n' '- interactions (количество взаимодействий)') return # Извлекаем аргументы name: str = args[1] description: str = args[2] condition_type: str = args[3] try: condition_value: int = int(args[4]) reward: int = int(args[5]) except ValueError: await update.message.reply_text('Значение условия и награда должны быть числами.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'INSERT INTO achievement_types (name, description, condition_type, condition_value, reward_points, created_by) VALUES (?, ?, ?, ?, ?, ?)', (name, description, condition_type, condition_value, reward, user.id)) except sqlite3.Error as e: log_structured("Add achievement type error", extra={'error': str(e)}) await update.message.reply_text(f'✅ Тип достижения "{name}" добавлен.') @command_wrapper @handle_exceptions async def del_achievement_type(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 1: await update.message.reply_text('Использование: /del_ach_type [id]') return try: type_id: int = int(context.args[0]) except ValueError: await update.message.reply_text('ID должен быть числом.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'DELETE FROM achievement_types WHERE type_id = ?', (type_id,)) except sqlite3.Error as e: log_structured("Del achievement type error", extra={'error': str(e)}) await update.message.reply_text(f'✅ Тип достижения {type_id} удален.') async def check_achievements(chat_id: int, user_id: int, context: CallbackContext, condition_type: str) -> None: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT type_id, condition_value, reward_points, description FROM achievement_types WHERE condition_type = ?', (condition_type,)) types: List[Tuple[int, int, int, str]] = cursor.fetchall() value: int = get_messages_count(chat_id, user_id) if condition_type == 'messages' else get_interactions_count(chat_id, user_id) for type_id, cond_val, reward, desc in types: if value >= cond_val: cursor.execute('SELECT 1 FROM user_achievements WHERE type_id = ? AND user_id = ? AND chat_id = ?', (type_id, user_id, chat_id)) if not cursor.fetchone(): execute_with_retry(cursor, 'INSERT INTO user_achievements (type_id, user_id, chat_id, status) VALUES (?, ?, ?, "approved")', (type_id, user_id, chat_id)) # Автоматически начислить награду current_points: int = get_user_points(chat_id, user_id) new_points: int = current_points + reward await update_user_points(chat_id, user_id, new_points, context, f"Автоматическое достижение: {desc}") await send_to_log_channel(f"🏆 Автоматическое достижение {desc} выдано user {user_id}", context, category="Достижение") notify_ach: bool = get_user_setting(user_id, 'notify_ach') if notify_ach: try: await context.bot.send_message(user_id, f"🎉 Новое достижение: {desc}! +{reward} баллов.") except Exception as e: log_structured("Achievement notification error", extra={'error': str(e)}) except sqlite3.Error as e: log_structured("Check achievements error", extra={'error': str(e)}) @command_wrapper @handle_exceptions async def approve_user_ach(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 1: await update.message.reply_text('Использование: /approve_user_ach [id достижения]') return try: ua_id: int = int(context.args[0]) except ValueError: await update.message.reply_text('ID должно быть числом.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT ua.user_id, ua.chat_id, at.reward_points FROM user_achievements ua JOIN achievement_types at ON ua.type_id = at.type_id WHERE ua.ua_id = ? AND ua.status = "pending"', (ua_id,)) result: Optional[Tuple[int, int, int]] = cursor.fetchone() except sqlite3.Error as e: log_structured("Approve user ach query error", extra={'error': str(e)}) result = None if not result: await update.message.reply_text('Запрос не найден или уже обработан.') return user_id, chat_id, reward = result current_points: int = get_user_points(chat_id, user_id) new_points: int = current_points + reward await update_user_points(chat_id, user_id, new_points, context, f"Достижение одобрено") try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE user_achievements SET status = "approved", approved_by = ?, approved_date = CURRENT_TIMESTAMP WHERE ua_id = ?', (user.id, ua_id)) except sqlite3.Error as e: log_structured("Approve user ach update error", extra={'error': str(e)}) await update.message.reply_text(f'✅ Достижение одобрено, начислено {reward} баллов.') notify_ach: bool = get_user_setting(user_id, 'notify_ach') if notify_ach: try: await context.bot.send_message(user_id, f"🎉 Ваше достижение одобрено! +{reward} баллов.") except Exception as e: log_structured("Approve user ach notification error", extra={'error': str(e)}) @command_wrapper @handle_exceptions async def reject_user_ach(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 1: await update.message.reply_text('Использование: /reject_user_ach [id достижения]') return try: ua_id: int = int(context.args[0]) except ValueError: await update.message.reply_text('ID должно быть числом.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE user_achievements SET status = "rejected", approved_by = ?, approved_date = CURRENT_TIMESTAMP WHERE ua_id = ?', (user.id, ua_id)) except sqlite3.Error as e: log_structured("Reject user ach update error", extra={'error': str(e)}) await update.message.reply_text(f'❌ Достижение отклонено.') @command_wrapper @handle_exceptions async def clanstats(update: Update, context: CallbackContext) -> None: effective_chat_id: int = get_effective_chat_id(update) try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT COUNT(*) FROM users WHERE chat_id = ?', (effective_chat_id,)) total_users: int = cursor.fetchone()[0] cursor.execute('SELECT SUM(points) FROM users WHERE chat_id = ?', (effective_chat_id,)) total_points: int = cursor.fetchone()[0] or 0 cursor.execute(''' SELECT username, points FROM users WHERE chat_id = ? ORDER BY points DESC LIMIT 3 ''', (effective_chat_id,)) top3: List[Tuple[str, int]] = cursor.fetchall() cursor.execute(''' SELECT COUNT(*) FROM transfers WHERE chat_id = ? AND transfer_date >= datetime('now', '-7 days') ''', (effective_chat_id,)) weekly_transfers: int = cursor.fetchone()[0] except sqlite3.Error as e: log_structured("Clan stats query error", extra={'error': str(e)}) total_users = 0 total_points = 0 top3 = [] weekly_transfers = 0 response: str = f"📊 Статистика клана ({update.effective_chat.title or 'Чат'}):\n\n" response += f"👥 Всего членов: {total_users}\n" response += f"💰 Общий банк баллов: {total_points}\n" response += f"💸 Переводов за неделю: {weekly_transfers}\n\n" response += "🏆 Топ-3:\n" for i, (username, points) in enumerate(top3, 1): username = username or "Неизвестно" response += f"{i}. @{username}: {points} баллов\n" await update.message.reply_text(response) @command_wrapper @handle_exceptions async def zeushome(update: Update, context: CallbackContext) -> None: user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) add_or_update_user(effective_chat_id, user.id, user.username) if len(context.args) < 1 or context.args[0] != SECRET_PASSWORD: await update.message.reply_text('Неверный пароль.') return activate_creator_mode_db(user.id, user.username) await update.message.reply_text( '🔓 Режим создателя активирован!\n' 'Теперь вам доступны команды управления ботом.' ) @command_wrapper @handle_exceptions async def addadmin(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_creator_mode_active(user.id): await update.message.reply_text('У вас нет прав для выполнения этой команды.') return if len(context.args) < 1: await update.message.reply_text('Использование: /addadmin @username') return target_username: str = context.args[0].lstrip('@') target_user_id: Optional[int] = await resolve_username(update, context, target_username) if not target_user_id: return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() was_admin = cursor.execute('SELECT 1 FROM admins WHERE user_id = ?', (target_user_id,)).fetchone() execute_with_retry(cursor, 'INSERT OR IGNORE INTO admins (user_id, username, added_by) VALUES (?, ?, ?)', (target_user_id, target_username, user.id)) except sqlite3.Error as e: log_structured("Add admin error", extra={'error': str(e)}) if not was_admin: await send_to_log_channel(f"🔧 Админ назначен:\nПользователь: {target_user_id} (@{target_username})\nНазначил: @{user.username}", context, category="Админ") await update.message.reply_text(f'✅ Пользователь @{target_username} добавлен в админы бота.') @command_wrapper @handle_exceptions async def deladmin(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_creator_mode_active(user.id): await update.message.reply_text('У вас нет прав для выполнения этой команды.') return if len(context.args) < 1: await update.message.reply_text('Использование: /deladmin @username') return target_username: str = context.args[0].lstrip('@') try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() was_admin = cursor.execute('SELECT 1 FROM admins WHERE LOWER(username) = LOWER(?)', (target_username,)).fetchone() execute_with_retry(cursor, 'DELETE FROM admins WHERE LOWER(username) = LOWER(?)', (target_username,)) except sqlite3.Error as e: log_structured("Del admin error", extra={'error': str(e)}) if was_admin: await send_to_log_channel(f"🔧 Админ удален:\nПользователь: @{target_username}\nУдалил: @{user.username}", context, category="Админ") await update.message.reply_text(f'✅ Пользователь @{target_username} удален из админов бота.') @command_wrapper @handle_exceptions async def toggle_logs(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_creator(user.id): await update.message.reply_text('Только создатель может использовать эту команду.') return new_status: bool = toggle_logging() status_text: str = "включено" if new_status else "отключено" await update.message.reply_text(f'Логирование {status_text}.') @command_wrapper @handle_exceptions async def mychannels(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_creator_mode_active(user.id): await update.message.reply_text('У вас нет прав для выполнения этой команды.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT chat_id, chat_title, chat_type, is_bot_admin, chat_link FROM bot_chats WHERE chat_type != "private"') chats: List[Tuple[int, str, str, bool, Optional[str]]] = cursor.fetchall() except sqlite3.Error as e: log_structured("My channels query error", extra={'error': str(e)}) chats = [] if not chats: await update.message.reply_text('Бот еще не добавлен ни в один чат.') return response: str = '📊 Чаты, где есть бот:\n\n' for chat_id, chat_title, chat_type, is_bot_admin, chat_link in chats: admin_status: str = '✅' if is_bot_admin else '❌' response += f'{admin_status} {chat_title} ({chat_type})\n' if chat_link: response += f'🔗 {chat_link}\n' response += f'🆔 {chat_id}\n\n' if len(response) > 4096: for i in range(0, len(response), 4096): await update.message.reply_text(response[i:i+4096]) else: await update.message.reply_text(response) @command_wrapper @handle_exceptions async def listcreators(update: Update, context: CallbackContext) -> None: user = update.message.from_user if user.id != CREATOR_ID: await update.message.reply_text('Только главный создатель может использовать эту команду.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT user_id, username, activated_date FROM creator_sessions WHERE is_active = TRUE') creators: List[Tuple[int, str, str]] = cursor.fetchall() except sqlite3.Error as e: log_structured("List creators query error", extra={'error': str(e)}) creators = [] if not creators: await update.message.reply_text('Нет активных режимов создателя.') return response: str = '👑 Активные создатели:\n\n' for user_id, username, activated_date in creators: response += f"@{username or user_id} (ID: {user_id}) - Активировано: {activated_date}\n" await update.message.reply_text(response) @command_wrapper @handle_exceptions async def deactivatecreator(update: Update, context: CallbackContext) -> None: user = update.message.from_user if user.id != CREATOR_ID: await update.message.reply_text('Только главный создатель может использовать эту команду.') return if len(context.args) < 1: await update.message.reply_text('Использование: /deactivatecreator @username') return target_username: str = context.args[0].lstrip('@') target_user_id: Optional[int] = await resolve_username(update, context, target_username) if not target_user_id: return deactivate_creator_mode_db(target_user_id) await update.message.reply_text(f'✅ Режим создателя деактивирован для @{target_username}.') @command_wrapper @handle_exceptions async def roulette(update: Update, context: CallbackContext) -> None: user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) add_or_update_user(effective_chat_id, user.id, user.username) if len(context.args) < 1: await update.message.reply_text('Использование: /roulette [ставка]') return try: bet: int = int(context.args[0]) except ValueError: await update.message.reply_text('Ставка должна быть числом.') return if bet <= 0: await update.message.reply_text('Ставка должна быть положительным числом.') return is_vip: bool = is_vip_user(effective_chat_id, user.id) max_bet: int = VIP_MAX_BET if is_vip else MAX_BET if bet > max_bet: await update.message.reply_text(f'Максимальная ставка: {max_bet} баллов.') return user_points: int = get_user_points(effective_chat_id, user.id) if user_points < bet: await update.message.reply_text(f'Недостаточно баллов. Ваш баланс: {user_points}') return # Предполагаем ставку на красное # Чтобы сделать больше проигрышей, делаем вероятность выигрыша ниже, например, win only on specific numbers result: int = random.randint(0, 36) win_conditions: bool = random.choice([True, False, False]) # 33% win chance for more losses if win_conditions: # Выигрыш: 2x ставка (поскольку bet снята, + bet*2) win_amount: int = bet * 2 points_change: int = bet # чистый выигрыш color: str = "красное" result_text: str = f"🎯 Выпало {color} {result}! Вы выиграли {win_amount} баллов!" await award_tournament_score(user.id, effective_chat_id) else: # Проигрыш: теряем ставку points_change = -bet if result == 0: color = "зеленое" else: color = "черное" result_text = f"🎯 Выпало {color} {result}! Вы проиграли {bet} баллов." win_amount = 0 new_points: int = user_points + points_change await update_user_points(effective_chat_id, user.id, new_points, context, f"Рулетка: {result} (изменение {points_change})") await update.message.reply_text( f'🎰 Результаты рулетки (ставка на красное):\n' f'{result_text}\n' f'💰 Ваш новый баланс: {new_points} баллов' ) @command_wrapper @handle_exceptions async def dice(update: Update, context: CallbackContext) -> None: user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) add_or_update_user(effective_chat_id, user.id, user.username) if len(context.args) < 1: await update.message.reply_text('Использование: /dice [ставка]') return try: bet: int = int(context.args[0]) except ValueError: await update.message.reply_text('Ставка должна быть числом.') return if bet <= 0: await update.message.reply_text('Ставка должна быть положительным числом.') return is_vip: bool = is_vip_user(effective_chat_id, user.id) max_bet: int = VIP_MAX_BET if is_vip else MAX_BET if bet > max_bet: await update.message.reply_text(f'Максимальная ставка: {max_bet} баллов.') return user_points: int = get_user_points(effective_chat_id, user.id) if user_points < bet: await update.message.reply_text(f'Недостаточно баллов. Ваш баланс: {user_points}') return user_roll: int = random.randint(1, 6) bot_roll: int = random.randint(1, 6) if user_roll > bot_roll: result = "победа" points_change: int = bet result_text = "Победа!" await award_tournament_score(user.id, effective_chat_id) elif user_roll < bot_roll: result = "проигрыш" points_change = -bet result_text = "Проигрыш!" else: result = "ничья" points_change = 0 result_text = "Ничья!" new_points: int = user_points + points_change await update_user_points(effective_chat_id, user.id, new_points, context, f"Кости: {user_roll} vs {bot_roll} ({result}, изменение {points_change})") await update.message.reply_text( f'🎲 Результаты броска:\n' f'Вы: {user_roll}\n' f'Бот: {bot_roll}\n\n' f'Результат: {result}\n' f'{result_text}\n' f'Изменение баллов: {points_change}\n' f'Новый баланс: {new_points}' ) @command_wrapper @handle_exceptions async def coinflip(update: Update, context: CallbackContext) -> None: user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) add_or_update_user(effective_chat_id, user.id, user.username) if len(context.args) < 2: await update.message.reply_text('Использование: /coinflip [ставка] [орёл/решка]') return try: bet: int = int(context.args[0]) except ValueError: await update.message.reply_text('Ставка должна быть числом.') return if bet <= 0: await update.message.reply_text('Ставка должна быть положительным числом.') return is_vip: bool = is_vip_user(effective_chat_id, user.id) max_bet: int = VIP_MAX_BET if is_vip else MAX_BET if bet > max_bet: await update.message.reply_text(f'Максимальная ставка: {max_bet} баллов.') return choice: str = context.args[1].lower() if choice not in ['орёл', 'орел', 'орл', 'решка']: await update.message.reply_text('Выберите "орёл" или "решка".') return if choice in ['орёл', 'орел', 'орл']: choice = 'орёл' else: choice = 'решка' user_points: int = get_user_points(effective_chat_id, user.id) if user_points < bet: await update.message.reply_text(f'Недостаточно баллов. Ваш баланс: {user_points}') return result: str = random.choice(['орёл', 'решка']) if choice == result: points_change: int = bet result_text: str = f"✅ Вы угадали! Вы выиграли {bet * 2} баллов!" await award_tournament_score(user.id, effective_chat_id) else: points_change = -bet result_text = "❌ Вы не угадали!" new_points: int = user_points + points_change await update_user_points(effective_chat_id, user.id, new_points, context, f"Монетка: {result} (выбор {choice}, изменение {points_change})") await update.message.reply_text( f'🪙 Результат подбрасывания монетки:\n' f'Выпало: {result}\n' f'Ваш выбор: {choice}\n\n' f'{result_text}\n' f'💰 Ваш новый баланс: {new_points} баллов' ) @command_wrapper @handle_exceptions async def slots(update: Update, context: CallbackContext) -> None: user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) add_or_update_user(effective_chat_id, user.id, user.username) if len(context.args) < 1: await update.message.reply_text('Использование: /slots [ставка]') return try: bet: int = int(context.args[0]) except ValueError: await update.message.reply_text('Ставка должна быть числом.') return if bet <= 0: await update.message.reply_text('Ставка должна быть положительным числом.') return is_vip: bool = is_vip_user(effective_chat_id, user.id) max_bet: int = VIP_MAX_BET if is_vip else MAX_BET if bet > max_bet: await update.message.reply_text(f'Максимальная ставка: {max_bet} баллов.') return user_points: int = get_user_points(effective_chat_id, user.id) if user_points < bet: await update.message.reply_text(f'Недостаточно баллов. Ваш баланс: {user_points}') return symbols: List[str] = ['🍒', '🍋', '🍊', '🍇', '🍉', '💎', '7️⃣'] reels: List[str] = [random.choice(symbols) for _ in range(3)] # Джекпот шанс 0.1% if random.random() < 0.001: reels = ['7️⃣', '7️⃣', '7️⃣'] win_amount: int = 5000 points_change: int = win_amount - bet result_text: str = f"🎉 СУПЕР ДЖЕКПОТ! Вы выиграли {win_amount} баллов!" elif reels[0] == reels[1] == reels[2]: if reels[0] == '💎': multiplier: int = 10 elif reels[0] == '7️⃣': multiplier = 5 else: multiplier = 3 win_amount = bet * multiplier points_change = win_amount - bet result_text = f"🎉 ДЖЕКПОТ! Вы выиграли {win_amount} баллов!" elif reels[0] == reels[1] or reels[1] == reels[2] or reels[0] == reels[2]: win_amount = bet * 2 points_change = win_amount - bet result_text = f"✅ Два в ряд! Вы выиграли {win_amount} баллов!" else: points_change = -bet result_text = "❌ Проигрыш!" if points_change > 0: await award_tournament_score(user.id, effective_chat_id) new_points: int = user_points + points_change await update_user_points(effective_chat_id, user.id, new_points, context, f"Слоты: {reels} (изменение {points_change})") await update.message.reply_text( f'🎰 Результаты слотов:\n' f'{" | ".join(reels)}\n\n' f'{result_text}\n' f'💰 Ваш новый баланс: {new_points} баллов' ) @command_wrapper @handle_exceptions async def wheel(update: Update, context: CallbackContext) -> None: user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) add_or_update_user(effective_chat_id, user.id, user.username) if len(context.args) < 1: await update.message.reply_text('Использование: /wheel [ставка]') return try: bet: int = int(context.args[0]) except ValueError: await update.message.reply_text('Ставка должна быть числом.') return if bet <= 0: await update.message.reply_text('Ставка должна быть положительным числом.') return is_vip: bool = is_vip_user(effective_chat_id, user.id) max_bet: int = VIP_MAX_BET if is_vip else MAX_BET if bet > max_bet: await update.message.reply_text(f'Максимальная ставка: {max_bet} баллов.') return user_points: int = get_user_points(effective_chat_id, user.id) if user_points < bet: await update.message.reply_text(f'Недостаточно баллов. Ваш баланс: {user_points}') return # Сектора колеса: multipliers or loss sectors: List[int] = [0, 1, 2, 3] # 0 - loss, others - multipliers result: int = random.choice(sectors) # Джекпот шанс 0.1% if random.random() < 0.001: result = 5000 // bet # to make win 5000 win_amount: int = int(bet * result) points_change: int = win_amount - bet result_text: str = f"Результат: x{result}! Вы выиграли {win_amount} баллов!" if result > 0 else "Проигрыш!" if points_change > 0: await award_tournament_score(user.id, effective_chat_id) new_points: int = user_points + points_change await update_user_points(effective_chat_id, user.id, new_points, context, f"Колесо: x{result} (изменение {points_change})") await update.message.reply_text( f'🎡 Колесо фортуны:\n' f'{result_text}\n' f'💰 Ваш новый баланс: {new_points} баллов' ) @command_wrapper @handle_exceptions async def blackjack(update: Update, context: CallbackContext) -> None: user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) add_or_update_user(effective_chat_id, user.id, user.username) if len(context.args) < 1: await update.message.reply_text('Использование: /blackjack [ставка]') return try: bet: int = int(context.args[0]) except ValueError: await update.message.reply_text('Ставка должна быть числом.') return if bet <= 0: await update.message.reply_text('Ставка должна быть положительным числом.') return is_vip: bool = is_vip_user(effective_chat_id, user.id) max_bet: int = VIP_MAX_BET if is_vip else MAX_BET if bet > max_bet: await update.message.reply_text(f'Максимальная ставка: {max_bet} баллов.') return user_points: int = get_user_points(effective_chat_id, user.id) if user_points < bet: await update.message.reply_text(f'Недостаточно баллов. Ваш баланс: {user_points}') return # Снимаем ставку сразу new_points: int = user_points - bet await update_user_points(effective_chat_id, user.id, new_points, context, f"Ставка в блэкджек {bet}") player_hand: List[int] = [draw_card(), draw_card()] dealer_hand: List[int] = [draw_card(), draw_card()] player_value: int = hand_value(player_hand) dealer_value: int = hand_value(dealer_hand) state: Dict[str, Any] = { 'player_hand': player_hand, 'dealer_hand': dealer_hand, 'bet': bet, 'chat_id': effective_chat_id, 'user_id': user.id, 'stage': 'player_turn' if player_value < 21 else 'check_bust', 'created': time.time() # Добавлено для таймаута } game_states[user.id] = state cards_text = lambda h: ' '.join([str(c) if c < 11 else ['J', 'Q', 'K', 'A'][c-11] for c in h]) if player_value > 21: await update.message.reply_text(f'🃏 Блэкджек: Вы bust! Проигрыш.\nВаша рука: {cards_text(player_hand)} ({player_value})\nНовый баланс: {new_points}') del game_states[user.id] return keyboard: List[List[InlineKeyboardButton]] = [[InlineKeyboardButton("Hit (Взять)", callback_data=f"bj_hit_{user.id}"), InlineKeyboardButton("Stand (Стоп)", callback_data=f"bj_stand_{user.id}")]] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text( f'🃏 Блэкджек начат! Ставка: {bet}\n' f'Ваша рука: {cards_text(player_hand)} ({player_value})\n' f'Карта дилера: {cards_text([dealer_hand[0]])} (скрытая: ?)\n' f'Ваш ход:', reply_markup=reply_markup ) @handle_exceptions async def blackjack_callback(update: Update, context: CallbackContext) -> None: query = update.callback_query await query.answer() user_id: int = query.from_user.id if user_id not in game_states: await query.edit_message_text('Игра не найдена.') return state: Dict[str, Any] = game_states[user_id] action: str = query.data.split('_')[1] cards_text = lambda h: ' '.join([str(c) if c < 11 else ['J', 'Q', 'K', 'A'][c-11] for c in h]) if action == 'hit': state['player_hand'].append(draw_card()) player_value: int = hand_value(state['player_hand']) if player_value > 21: state['stage'] = 'bust' await query.edit_message_text( f'🃏 Bust! Проигрыш.\n' f'Ваша рука: {cards_text(state["player_hand"])} ({player_value})\n' f'Дилер: {cards_text(state["dealer_hand"])} ({hand_value(state["dealer_hand"])})\n' f'Новый баланс: {get_user_points(state["chat_id"], user_id)}' ) del game_states[user_id] return state['player_value'] = player_value keyboard: List[List[InlineKeyboardButton]] = [[InlineKeyboardButton("Hit", callback_data=f"bj_hit_{user_id}"), InlineKeyboardButton("Stand", callback_data=f"bj_stand_{user_id}")]] reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text( f'🃏 Ваша рука: {cards_text(state["player_hand"])} ({player_value})\n' f'Карта дилера: {cards_text([state["dealer_hand"][0]])} (скрытая: ?)\n' f'Ваш ход:', reply_markup=reply_markup ) elif action == 'stand': while hand_value(state['dealer_hand']) < 17: state['dealer_hand'].append(draw_card()) player_value: int = hand_value(state['player_hand']) dealer_value: int = hand_value(state['dealer_hand']) # Исправленная логика начисления выигрыша if dealer_value > 21 or player_value > dealer_value: # Выигрыш: начисляем 2*ставка (поскольку bet снята, + bet*2) win_amount: int = state['bet'] * 2 points_change: int = win_amount await award_tournament_score(user_id, state['chat_id']) elif player_value < dealer_value: # Проигрыш: ничего не начисляем (bet уже снята) win_amount = 0 points_change = 0 else: # Ничья: возвращаем ставку (+ bet) win_amount = state['bet'] points_change = win_amount new_points: int = get_user_points(state['chat_id'], user_id) + points_change await update_user_points(state['chat_id'], user_id, new_points, context, f"Блэкджек: {player_value} vs {dealer_value} (изменение {points_change})") # Определяем результат для сообщения if dealer_value > 21 or player_value > dealer_value: result_text = f"Победа! Вы выиграли {state['bet'] * 2} баллов!" elif player_value < dealer_value: result_text = f"Проигрыш! Вы проиграли {state['bet']} баллов." else: result_text = "Ничья! Ставка возвращена." await query.edit_message_text( f'🃏 Результат:\n' f'Ваша рука: {cards_text(state["player_hand"])} ({player_value})\n' f'Дилер: {cards_text(state["dealer_hand"])} ({dealer_value})\n' f'{result_text}\n' f'Новый баланс: {new_points}' ) del game_states[user_id] else: log_structured("Unknown blackjack callback", extra={'data': data}) await query.edit_message_text("Неизвестная команда.") @command_wrapper @handle_exceptions async def duel(update: Update, context: CallbackContext) -> None: user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) add_or_update_user(effective_chat_id, user.id, user.username) if len(context.args) < 2: await update.message.reply_text('Использование: /duel [@username] [ставка]') return target_username: str = context.args[0].lstrip('@') try: bet: int = int(context.args[1]) except ValueError: await update.message.reply_text('Ставка должна быть числом.') return if bet <= 0: await update.message.reply_text('Ставка должна быть положительным числом.') return is_vip: bool = is_vip_user(effective_chat_id, user.id) max_bet: int = VIP_MAX_BET if is_vip else MAX_BET if bet > max_bet: await update.message.reply_text(f'Максимальная ставка: {max_bet} баллов.') return initiator_points: int = get_user_points(effective_chat_id, user.id) if initiator_points < bet: await update.message.reply_text(f'Недостаточно баллов. Ваш баланс: {initiator_points}') return target_user_id: Optional[int] = await resolve_username(update, context, target_username) if not target_user_id: return if user.id == target_user_id: await update.message.reply_text('Нельзя вызвать на дуэль самого себя!') return target_points: int = get_user_points(effective_chat_id, target_user_id) if target_points < bet: await update.message.reply_text(f'У пользователя @{target_username} недостаточно баллов для дуэли.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, ''' INSERT INTO duels (chat_id, initiator_id, initiator_username, target_id, target_username, bet_amount, status) VALUES (?, ?, ?, ?, ?, ?, 'pending') ''', (effective_chat_id, user.id, user.username, target_user_id, target_username, bet)) duel_id = cursor.lastrowid except sqlite3.Error as e: log_structured("Duel insert error", extra={'error': str(e)}) await update.message.reply_text('Ошибка создания дуэли.') return keyboard: List[List[InlineKeyboardButton]] = [ [ InlineKeyboardButton("Принять дуэль", callback_data=f"duel_accept_{duel_id}"), InlineKeyboardButton("Отклонить дуэль", callback_data=f"duel_decline_{duel_id}") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text( f'⚔️ @{user.username} вызывает @{target_username} на дуэль!\n' f'🏆 Ставка: {bet} баллов\n' f'❓ Принять вызов?', reply_markup=reply_markup ) @handle_exceptions async def duel_callback(update: Update, context: CallbackContext) -> None: query = update.callback_query await query.answer() data = query.data if data.startswith('duel_'): parts = data.split('_') action = parts[1] duel_id = int(parts[2]) try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT * FROM duels WHERE duel_id = ?', (duel_id,)) duel: Optional[Tuple[Any, ...]] = cursor.fetchone() except sqlite3.Error as e: log_structured("Duel callback query error", extra={'error': str(e)}) duel = None if not duel: await query.edit_message_text('Дуэль не найдена или уже завершена.') return chat_id, initiator_id, initiator_username, target_id, target_username, bet_amount, status, winner_id, created_date = duel[1:] if query.from_user.id != target_id: await query.answer('Только целевой пользователь может ответить на вызов дуэли.', show_alert=True) return if action == 'accept': initiator_points: int = get_user_points(chat_id, initiator_id) target_points: int = get_user_points(chat_id, target_id) if initiator_points < bet_amount or target_points < bet_amount: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE duels SET status = "cancelled" WHERE duel_id = ?', (duel_id,)) except sqlite3.Error as e: log_structured("Duel accept update error", extra={'error': str(e)}) await query.edit_message_text('❌ У одного из игроков недостаточно баллов. Дуэль отменена.') return initiator_new: int = initiator_points - bet_amount target_new: int = target_points - bet_amount await update_user_points(chat_id, initiator_id, initiator_new, context, f"Ставка в дуэли {duel_id}") await update_user_points(chat_id, target_id, target_new, context, f"Ставка в дуэли {duel_id}") winner_id: int = random.choice([initiator_id, target_id]) loser_id: int = target_id if winner_id == initiator_id else initiator_id winner_new: int = get_user_points(chat_id, winner_id) + (bet_amount * 2) loser_new: int = get_user_points(chat_id, loser_id) await update_user_points(chat_id, winner_id, winner_new, context, f"Выигрыш в дуэли {duel_id}") await award_tournament_score(winner_id, chat_id) try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE duels SET status = "completed", winner_id = ? WHERE duel_id = ?', (winner_id, duel_id)) except sqlite3.Error as e: log_structured("Duel complete update error", extra={'error': str(e)}) winner_username: str = initiator_username if winner_id == initiator_id else target_username await query.edit_message_text( f'⚔️ Дуэль завершена!\n' f'🏆 Победитель: @{winner_username}\n' f'💰 Выигрыш: {bet_amount} баллов' ) await send_to_log_channel(f"⚔️ Дуэль завершена:\nID: {duel_id}\nПобедитель: @{winner_username} ({winner_id})\nПроигравший: @{initiator_username if winner_id == target_id else target_username} ({loser_id})\nСтавка: {bet_amount}\nЧат: {chat_id}", context, category="Игра") elif action == 'decline': try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE duels SET status = "declined" WHERE duel_id = ?', (duel_id,)) except sqlite3.Error as e: log_structured("Duel decline update error", extra={'error': str(e)}) await query.edit_message_text(f'❌ @{target_username} отклонил вызов на дуэль.') await send_to_log_channel(f"⚔️ Дуэль отклонена:\nID: {duel_id}\nОтклонил: @{target_username}\nИнициатор: @{initiator_username}\nЧат: {chat_id}", context, category="Игра") else: log_structured("Unknown duel callback", extra={'data': data}) await query.edit_message_text("Неизвестная команда.") @command_wrapper @handle_exceptions async def highlow(update: Update, context: CallbackContext) -> None: user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) add_or_update_user(effective_chat_id, user.id, user.username) if len(context.args) < 2: await update.message.reply_text('Использование: /highlow [@username] [ставка]') return target_username: str = context.args[0].lstrip('@') try: bet: int = int(context.args[1]) except ValueError: await update.message.reply_text('Ставка должна быть числом.') return if bet <= 0: await update.message.reply_text('Ставка должна быть положительным числом.') return is_vip: bool = is_vip_user(effective_chat_id, user.id) max_bet: int = VIP_MAX_BET if is_vip else MAX_BET if bet > max_bet: await update.message.reply_text(f'Максимальная ставка: {max_bet} баллов.') return user_points: int = get_user_points(effective_chat_id, user.id) if user_points < bet: await update.message.reply_text(f'Недостаточно баллов. Ваш баланс: {user_points}') return target_user_id: Optional[int] = await resolve_username(update, context, target_username) if not target_user_id: return if user.id == target_user_id: await update.message.reply_text('Нельзя играть с самим собой!') return target_points: int = get_user_points(effective_chat_id, target_user_id) if target_points < bet: await update.message.reply_text(f'У @{target_username} недостаточно баллов.') return # Создаем игру first_card: int = random.randint(1, 13) game_id: str = f"highlow_{user.id}_{target_user_id}_{int(time.time())}" game_states[game_id] = { 'first_card': first_card, 'bet': bet, 'chat_id': effective_chat_id, 'initiator_id': user.id, 'target_id': target_user_id, 'initiator_username': user.username, 'target_username': target_username, 'status': 'waiting_choice', 'created': time.time() # Добавлено для таймаута } # Отправляем сообщение с кнопками выбора и отменой для инициатора keyboard: List[List[InlineKeyboardButton]] = [ [ InlineKeyboardButton("Выше", callback_data=f"highlow_higher_{game_id}"), InlineKeyboardButton("Ниже", callback_data=f"highlow_lower_{game_id}") ], [ InlineKeyboardButton("❌ Отменить", callback_data=f"highlow_cancel_{game_id}") ] ] reply_markup = InlineKeyboardMarkup(keyboard) message = await update.message.reply_text( f'🎴 Игра Выше/Ниже\n' f'@{user.username} вызывает @{target_username}\n' f'Ставка: {bet} баллов\n' f'Текущая карта: {first_card}\n' f'Следующая карта будет выше или ниже?\n\n' f'@{target_username}, ваш ход:', reply_markup=reply_markup ) # Сохраняем ID сообщения для возможного редактирования game_states[game_id]['message_id'] = message.message_id @handle_exceptions async def highlow_callback(update: Update, context: CallbackContext) -> None: query = update.callback_query await query.answer() data = query.data if data.startswith('highlow_'): parts = data.split('_') action = parts[1] game_id = '_'.join(parts[2:]) if game_id not in game_states: await query.edit_message_text('Игра не найдена или завершена.') return state = game_states[game_id] # Проверяем, что нажал целевой пользователь или инициатор для отмены if query.from_user.id != state['target_id'] and (action != 'cancel' or query.from_user.id != state['initiator_id']): await query.answer('Только приглашенный игрок может сделать выбор или инициатор отменить.', show_alert=True) return if action == 'cancel': # Отмена инициатором del game_states[game_id] await query.edit_message_text('❌ Игра отменена инициатором.') return first_card: int = state['first_card'] second_card: int = random.randint(1, 13) # Определяем результат if (action == 'higher' and second_card > first_card) or (action == 'lower' and second_card < first_card): winner_id = state['target_id'] winner_username = state['target_username'] result_text = "Победа!" points_change = state['bet'] await award_tournament_score(winner_id, state['chat_id']) else: winner_id = state['initiator_id'] winner_username = state['initiator_username'] result_text = "Проигрыш!" points_change = -state['bet'] await award_tournament_score(winner_id, state['chat_id']) # Обновляем баллы target_points: int = get_user_points(state['chat_id'], state['target_id']) initiator_points: int = get_user_points(state['chat_id'], state['initiator_id']) # Снимаем ставку с обоих игроков await update_user_points(state['chat_id'], state['target_id'], target_points - state['bet'], context, f"Ставка в High/Low") await update_user_points(state['chat_id'], state['initiator_id'], initiator_points - state['bet'], context, f"Ставка в High/Low") # Начисляем выигрыш победителю winner_new_points: int = get_user_points(state['chat_id'], winner_id) + (state['bet'] * 2) await update_user_points(state['chat_id'], winner_id, winner_new_points, context, f"Выигрыш в High/Low: {first_card} -> {second_card}") # Удаляем состояние игры del game_states[game_id] await query.edit_message_text( f'🎴 Результат High/Low:\n' f'Первая карта: {first_card}\n' f'Вторая карта: {second_card}\n' f'Выбор: {"Выше" if action == "higher" else "Ниже"}\n\n' f'Победитель: @{winner_username}\n' f'{result_text}\n' f'Выигрыш: {state["bet"] * 2} баллов' ) else: log_structured("Unknown highlow callback", extra={'data': data}) await query.edit_message_text("Неизвестная команда.") @command_wrapper @handle_exceptions async def coinflip_pvp(update: Update, context: CallbackContext) -> None: user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) add_or_update_user(effective_chat_id, user.id, user.username) if len(context.args) < 2: await update.message.reply_text('Использование: /coinflip_pvp [@username] [ставка]') return target_username: str = context.args[0].lstrip('@') try: bet: int = int(context.args[1]) except ValueError: await update.message.reply_text('Ставка должна быть числом.') return if bet <= 0: await update.message.reply_text('Ставка должна быть положительным числом.') return is_vip: bool = is_vip_user(effective_chat_id, user.id) max_bet: int = VIP_MAX_BET if is_vip else MAX_BET if bet > max_bet: await update.message.reply_text(f'Максимальная ставка: {max_bet} баллов.') return user_points: int = get_user_points(effective_chat_id, user.id) if user_points < bet: await update.message.reply_text(f'Недостаточно баллов. Ваш баланс: {user_points}') return target_user_id: Optional[int] = await resolve_username(update, context, target_username) if not target_user_id: return if user.id == target_user_id: await update.message.reply_text('Нельзя играть с самим собой!') return target_points: int = get_user_points(effective_chat_id, target_user_id) if target_points < bet: await update.message.reply_text(f'У @{target_username} недостаточно баллов.') return # Создаем игру game_id: str = f"coinflip_{user.id}_{target_user_id}_{int(time.time())}" game_states[game_id] = { 'bet': bet, 'chat_id': effective_chat_id, 'initiator_id': user.id, 'target_id': target_user_id, 'initiator_username': user.username, 'target_username': target_username, 'status': 'waiting_choice', 'created': time.time() # Добавлено для таймаута } # Отправляем сообщение с кнопками выбора и отменой для инициатора keyboard: List[List[InlineKeyboardButton]] = [ [ InlineKeyboardButton("Орёл", callback_data=f"coinflip_heads_{game_id}"), InlineKeyboardButton("Решка", callback_data=f"coinflip_tails_{game_id}") ], [ InlineKeyboardButton("❌ Отменить", callback_data=f"coinflip_cancel_{game_id}") ] ] reply_markup = InlineKeyboardMarkup(keyboard) message = await update.message.reply_text( f'🪙 Монетка PvP\n' f'@{user.username} вызывает @{target_username}\n' f'Ставка: {bet} баллов\n\n' f'@{target_username}, выберите сторону:', reply_markup=reply_markup ) # Сохраняем ID сообщения для возможного редактирования game_states[game_id]['message_id'] = message.message_id @handle_exceptions async def coinflip_pvp_callback(update: Update, context: CallbackContext) -> None: query = update.callback_query await query.answer() data = query.data if data.startswith('coinflip_'): parts = data.split('_') choice = parts[1] game_id = '_'.join(parts[2:]) if game_id not in game_states: await query.edit_message_text('Игра не найдена или завершена.') return state = game_states[game_id] # Проверяем, что нажал целевой пользователь или инициатор для отмены if query.from_user.id != state['target_id'] and (choice != 'cancel' or query.from_user.id != state['initiator_id']): await query.answer('Только приглашенный игрок может сделать выбор или инициатор отменить.', show_alert=True) return if choice == 'cancel': # Отмена инициатором del game_states[game_id] await query.edit_message_text('❌ Игра отменена инициатором.') return result: str = random.choice(['heads', 'tails']) # Определяем результат if choice == result: winner_id = state['target_id'] winner_username = state['target_username'] result_text = "Победа!" points_change = state['bet'] await award_tournament_score(winner_id, state['chat_id']) else: winner_id = state['initiator_id'] winner_username = state['initiator_username'] result_text = "Проигрыш!" points_change = -state['bet'] await award_tournament_score(winner_id, state['chat_id']) # Обновляем баллы target_points: int = get_user_points(state['chat_id'], state['target_id']) initiator_points: int = get_user_points(state['chat_id'], state['initiator_id']) # Снимаем ставку с обоих игроков await update_user_points(state['chat_id'], state['target_id'], target_points - state['bet'], context, f"Ставка в CoinFlip PvP") await update_user_points(state['chat_id'], state['initiator_id'], initiator_points - state['bet'], context, f"Ставка в CoinFlip PvP") # Начисляем выигрыш победителю winner_new_points: int = get_user_points(state['chat_id'], winner_id) + (state['bet'] * 2) await update_user_points(state['chat_id'], winner_id, winner_new_points, context, f"Выигрыш в CoinFlip PvP: {result}") # Удаляем состояние игры del game_states[game_id] await query.edit_message_text( f'🪙 Результат монетки:\n' f'Выпало: {"Орёл" if result == "heads" else "Решка"}\n' f'Выбор: {"Орёл" if choice == "heads" else "Решка"}\n\n' f'Победитель: @{winner_username}\n' f'{result_text}\n' f'Выигрыш: {state["bet"] * 2} баллов' ) else: log_structured("Unknown coinflip pvp callback", extra={'data': data}) await query.edit_message_text("Неизвестная команда.") @command_wrapper @handle_exceptions async def dice_pvp(update: Update, context: CallbackContext) -> None: user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) add_or_update_user(effective_chat_id, user.id, user.username) if len(context.args) < 2: await update.message.reply_text('Использование: /dice_pvp [@username] [ставка]') return target_username: str = context.args[0].lstrip('@') try: bet: int = int(context.args[1]) except ValueError: await update.message.reply_text('Ставка должна быть числом.') return if bet <= 0: await update.message.reply_text('Ставка должна быть положительным числом.') return is_vip: bool = is_vip_user(effective_chat_id, user.id) max_bet: int = VIP_MAX_BET if is_vip else MAX_BET if bet > max_bet: await update.message.reply_text(f'Максимальная ставка: {max_bet} баллов.') return user_points: int = get_user_points(effective_chat_id, user.id) if user_points < bet: await update.message.reply_text(f'Недостаточно баллов. Ваш баланс: {user_points}') return target_user_id: Optional[int] = await resolve_username(update, context, target_username) if not target_user_id: return if user.id == target_user_id: await update.message.reply_text('Нельзя играть с самим собой!') return target_points: int = get_user_points(effective_chat_id, target_user_id) if target_points < bet: await update.message.reply_text(f'У @{target_username} недостаточно баллов.') return # Создаем игру game_id: str = f"dice_{user.id}_{target_user_id}_{int(time.time())}" game_states[game_id] = { 'bet': bet, 'chat_id': effective_chat_id, 'initiator_id': user.id, 'target_id': target_user_id, 'initiator_username': user.username, 'target_username': target_username, 'initiator_roll': None, 'target_roll': None, 'status': 'waiting_rolls', 'created': time.time() # Добавлено для таймаута } # Отправляем сообщение с кнопкой броска для инициатора и отменой keyboard: List[List[InlineKeyboardButton]] = [ [InlineKeyboardButton("Бросить кости", callback_data=f"dice_roll_{game_id}")], [InlineKeyboardButton("❌ Отменить", callback_data=f"dice_cancel_{game_id}")] ] reply_markup = InlineKeyboardMarkup(keyboard) message = await update.message.reply_text( f'🎲 Кости PvP\n' f'@{user.username} vs @{target_username}\n' f'Ставка: {bet} баллов\n\n' f'@{user.username}, нажмите чтобы бросить кости:', reply_markup=reply_markup ) # Сохраняем ID сообщения для возможного редактирования game_states[game_id]['message_id'] = message.message_id @handle_exceptions async def dice_pvp_callback(update: Update, context: CallbackContext) -> None: query = update.callback_query await query.answer() data = query.data if data.startswith('dice_'): parts = data.split('_') action = parts[1] game_id = '_'.join(parts[2:]) if game_id not in game_states: await query.edit_message_text('Игра не найдена или завершена.') return state = game_states[game_id] result_text = "Результат не определён" # Проверяем, что игрок участвует в игре или инициатор для отмены if query.from_user.id not in [state['initiator_id'], state['target_id']] and (action != 'cancel' or query.from_user.id != state['initiator_id']): await query.answer('Вы не участник этой игры или не можете отменить.', show_alert=True) return if action == 'cancel': # Отмена инициатором del game_states[game_id] await query.edit_message_text('❌ Игра отменена инициатором.') return # Бросаем кости player_roll: int = random.randint(1, 6) # Сохраняем результат броска if query.from_user.id == state['initiator_id']: state['initiator_roll'] = player_roll roll_type = 'initiator' else: state['target_roll'] = player_roll roll_type = 'target' # Проверяем, все ли игроки бросили кости if state['initiator_roll'] is not None and state['target_roll'] is not None: # Оба игрока бросили кости, определяем победителя if state['initiator_roll'] > state['target_roll']: winner_id = state['initiator_id'] winner_username = state['initiator_username'] elif state['target_roll'] > state['initiator_roll']: winner_id = state['target_id'] winner_username = state['target_username'] else: winner_id = None winner_username = None # Ничья # Обновляем баллы initiator_points: int = get_user_points(state['chat_id'], state['initiator_id']) target_points: int = get_user_points(state['chat_id'], state['target_id']) # Снимаем ставку с обоих игроков await update_user_points(state['chat_id'], state['initiator_id'], initiator_points - state['bet'], context, f"Ставка в Dice PvP") await update_user_points(state['chat_id'], state['target_id'], target_points - state['bet'], context, f"Ставка в Dice PvP") if winner_id: if winner_username is None: winner_username = get_username_by_id(winner_id) or "Неизвестный" winner_new_points: int = get_user_points(state['chat_id'], winner_id) + (state['bet'] * 2) await update_user_points(state['chat_id'], winner_id, winner_new_points, context, f"Выигрыш в Dice PvP") await award_tournament_score(winner_id, state['chat_id']) result_text = f'Победитель: @{winner_username}\nВыигрыш: {state["bet"] * 2} баллов' else: # Ничья - возвращаем ставки await update_user_points(state['chat_id'], state['initiator_id'], initiator_points, context, f"Ничья в Dice PvP") await update_user_points(state['chat_id'], state['target_id'], target_points, context, f"Ничья в Dice PvP") result_text = 'Ничья! Ставки возвращены.' # Удаляем состояние игры del game_states[game_id] await query.edit_message_text( f'🎲 Результат костей:\n' f'@{state["initiator_username"]}: {state["initiator_roll"]}\n' f'@{state["target_username"]}: {state["target_roll"]}\n\n' f'{result_text}' ) else: # Еще не все бросили кости, обновляем сообщение if roll_type == 'initiator': next_player = state['target_username'] else: next_player = state['initiator_username'] next_text: str = f'@{next_player}, ваш ход:' keyboard: List[List[InlineKeyboardButton]] = [ [InlineKeyboardButton("Бросить кости", callback_data=f"dice_roll_{game_id}")], [InlineKeyboardButton("❌ Отменить", callback_data=f"dice_cancel_{game_id}")] if query.from_user.id == state['initiator_id'] else [] ] reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text( f'🎲 Кости PvP\n' f'@{state["initiator_username"]} vs @{state["target_username"]}\n' f'Ставка: {state["bet"]} баллов\n\n' f'@{query.from_user.username} бросил: {player_roll}\n' f'{next_text}', reply_markup=reply_markup ) else: log_structured("Unknown dice pvp callback", extra={'data': data}) await query.edit_message_text("Неизвестная команда.") @command_wrapper @handle_exceptions async def create_quest(update: Update, context: CallbackContext) -> None: if update.callback_query: await update.callback_query.edit_message_text('Используйте /create_quest описание награда') return user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 2: await update.message.reply_text('Использование: /create_quest [описание] [награда]') return description: str = ' '.join(context.args[:-1]) try: reward: int = int(context.args[-1]) except ValueError: await update.message.reply_text('Награда должна быть числом.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'INSERT INTO quests (description, reward, created_by) VALUES (?, ?, ?)', (description, reward, user.id)) except sqlite3.Error as e: log_structured("Create quest error", extra={'error': str(e)}) await update.message.reply_text(f'✅ Квест создан: {description} (награда {reward})') @command_wrapper @handle_exceptions async def edit_quest(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 3: await update.message.reply_text('Использование: /edit_quest [id] [описание] [награда]') return try: quest_id: int = int(context.args[0]) reward: int = int(context.args[-1]) except ValueError: await update.message.reply_text('ID и награда должны быть числами.') return description: str = ' '.join(context.args[1:-1]) try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE quests SET description = ?, reward = ? WHERE quest_id = ?', (description, reward, quest_id)) except sqlite3.Error as e: log_structured("Edit quest error", extra={'error': str(e)}) await update.message.reply_text(f'✅ Квест {quest_id} обновлен.') @command_wrapper @handle_exceptions async def del_quest(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 1: await update.message.reply_text('Использование: /del_quest [id]') return try: quest_id: int = int(context.args[0]) except ValueError: await update.message.reply_text('ID должен быть числом.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'DELETE FROM quests WHERE quest_id = ?', (quest_id,)) except sqlite3.Error as e: log_structured("Del quest error", extra={'error': str(e)}) await update.message.reply_text(f'✅ Квест {quest_id} удален.') @command_wrapper @handle_exceptions async def quests(update: Update, context: CallbackContext) -> None: if update.message: message = update.message elif update.callback_query: message = update.callback_query.message else: return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT quest_id, description, reward FROM quests WHERE status = "active"') quests_list: List[Tuple[int, str, int]] = cursor.fetchall() except sqlite3.Error as e: log_structured("Quests query error", extra={'error': str(e)}) quests_list = [] if not quests_list: text = 'Нет активных квестов.' else: text = '📜 Активные квесты:\n\n' for q_id, desc, reward in quests_list: text += f'ID {q_id}. {desc} - Награда: {reward} баллов\n' keyboard: List[List[InlineKeyboardButton]] = [[InlineKeyboardButton("Назад", callback_data="menu_back_profile")]] reply_markup = InlineKeyboardMarkup(keyboard) if update.callback_query: await update.callback_query.edit_message_text(text, reply_markup=reply_markup) else: await message.reply_text(text, reply_markup=reply_markup) @command_wrapper @handle_exceptions async def complete_quest(update: Update, context: CallbackContext) -> None: user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) if len(context.args) < 1: await update.message.reply_text('Использование: /complete_quest [id квеста]') return try: quest_id: int = int(context.args[0]) except ValueError: await update.message.reply_text('ID должно быть числом.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'INSERT INTO quest_completions (quest_id, user_id, chat_id) VALUES (?, ?, ?)', (quest_id, user.id, effective_chat_id)) completion_id = cursor.lastrowid except sqlite3.Error as e: log_structured("Complete quest insert error", extra={'error': str(e)}) await update.message.reply_text('Ошибка завершения квеста.') return # Создаем кнопки для одобрения keyboard: List[List[InlineKeyboardButton]] = [ [ InlineKeyboardButton("✅ Одобрить", callback_data=f"quest_approve_{completion_id}"), InlineKeyboardButton("❌ Отклонить", callback_data=f"quest_reject_{completion_id}") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text( f'📜 Заявка на завершение квеста #{quest_id} от @{user.username}\n' f'Админы, используйте кнопки:', reply_markup=reply_markup ) await send_to_log_channel(f"📜 Заявка на квест #{quest_id} от @{user.username}", context, category="Квест") @handle_exceptions async def quest_callback(update: Update, context: CallbackContext) -> None: query = update.callback_query await query.answer() data = query.data if not data.startswith('quest_'): return parts = data.split('_') action = parts[1] completion_id = int(parts[2]) user = query.from_user if not is_admin(user.id): await query.answer('Только админы могут одобрять.', show_alert=True) return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT qc.user_id, qc.chat_id, q.reward FROM quest_completions qc JOIN quests q ON qc.quest_id = q.quest_id WHERE qc.completion_id = ? AND qc.status = "pending"', (completion_id,)) result: Optional[Tuple[int, int, int]] = cursor.fetchone() except sqlite3.Error as e: log_structured("Quest callback query error", extra={'error': str(e)}) result = None if not result: await query.edit_message_text('Заявка не найдена или уже обработана.') return user_id, chat_id, reward = result if action == 'approve': current_points: int = get_user_points(chat_id, user_id) new_points: int = current_points + reward await update_user_points(chat_id, user_id, new_points, context, f"Квест одобрен") try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE quest_completions SET status = "approved", approved_by = ?, approved_date = CURRENT_TIMESTAMP WHERE completion_id = ?', (user.id, completion_id)) except sqlite3.Error as e: log_structured("Approve quest update error", extra={'error': str(e)}) await query.edit_message_text(f'✅ Квест одобрен, начислено {reward} баллов.') elif action == 'reject': try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE quest_completions SET status = "rejected", approved_by = ?, approved_date = CURRENT_TIMESTAMP WHERE completion_id = ?', (user.id, completion_id)) except sqlite3.Error as e: log_structured("Reject quest update error", extra={'error': str(e)}) await query.edit_message_text(f'❌ Квест отклонен.') else: log_structured("Unknown quest callback", extra={'data': data}) await query.edit_message_text("Неизвестная команда.") @command_wrapper @handle_exceptions async def create_tournament(update: Update, context: CallbackContext) -> None: if update.callback_query: await update.callback_query.edit_message_text('Используйте /create_tournament имя описание prize_pool') return user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 3: await update.message.reply_text('Использование: /create_tournament [имя] [описание] [prize_pool]') return name: str = context.args[0] description: str = ' '.join(context.args[1:-1]) try: prize_pool: int = int(context.args[-1]) except ValueError: await update.message.reply_text('Призовой фонд должен быть числом.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'INSERT INTO tournaments (name, description, prize_pool, created_by) VALUES (?, ?, ?, ?)', (name, description, prize_pool, user.id)) except sqlite3.Error as e: log_structured("Create tournament error", extra={'error': str(e)}) await update.message.reply_text(f'✅ Турнир "{name}" создан.') @command_wrapper @handle_exceptions async def set_tournament_status(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 2: await update.message.reply_text('Использование: /set_tournament_status [id] [status]') return try: tournament_id: int = int(context.args[0]) except ValueError: await update.message.reply_text('ID должно быть числом.') return status: str = context.args[1].lower() if status not in ['registration', 'ongoing', 'completed']: await update.message.reply_text('Статус должен быть registration, ongoing или completed.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE tournaments SET status = ? WHERE tournament_id = ?', (status, tournament_id)) except sqlite3.Error as e: log_structured("Set tournament status error", extra={'error': str(e)}) await update.message.reply_text(f'✅ Статус турнира {tournament_id} изменен на {status}.') @command_wrapper @handle_exceptions async def tournaments(update: Update, context: CallbackContext) -> None: if update.message: message = update.message elif update.callback_query: message = update.callback_query.message else: return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT tournament_id, name, description, prize_pool, status, created_date FROM tournaments') tournaments_list: List[Tuple[int, str, str, int, str, str]] = cursor.fetchall() text = '🏆 Турниры:\n\n' for t_id, name, desc, prize, status, created in tournaments_list: cursor.execute('SELECT COUNT(*) FROM tournament_participants WHERE tournament_id = ?', (t_id,)) participants = cursor.fetchone()[0] text += f'ID {t_id}. {name}\nОписание: {desc}\nПриз: {prize}\nСтатус: {status}\nУчастников: {participants}\nСоздан: {created}\n\n' except sqlite3.Error as e: log_structured("Tournaments query error", extra={'error': str(e)}) text = 'Нет турниров.' keyboard: List[List[InlineKeyboardButton]] = [[InlineKeyboardButton("Назад", callback_data="menu_back_profile")]] reply_markup = InlineKeyboardMarkup(keyboard) if update.callback_query: await update.callback_query.edit_message_text(text, reply_markup=reply_markup) else: await message.reply_text(text, reply_markup=reply_markup) @command_wrapper @handle_exceptions async def join_tournament(update: Update, context: CallbackContext) -> None: user = update.message.from_user if len(context.args) < 1: await update.message.reply_text('Использование: /join_tournament [id турнира]') return try: tournament_id: int = int(context.args[0]) except ValueError: await update.message.reply_text('ID должно быть числом.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT status FROM tournaments WHERE tournament_id = ?', (tournament_id,)) status = cursor.fetchone() except sqlite3.Error as e: log_structured("Join tournament query error", extra={'error': str(e)}) status = None if not status: await update.message.reply_text('Турнир не найден.') return status = status[0] if status != 'registration': await update.message.reply_text('Присоединение возможно только во время регистрации.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT 1 FROM tournament_participants WHERE tournament_id = ? AND user_id = ?', (tournament_id, user.id)) if cursor.fetchone(): await update.message.reply_text('Вы уже участник этого турнира.') return execute_with_retry(cursor, 'INSERT INTO tournament_participants (tournament_id, user_id) VALUES (?, ?)', (tournament_id, user.id)) except sqlite3.Error as e: log_structured("Join tournament insert error", extra={'error': str(e)}) await update.message.reply_text(f'✅ Вы присоединились к турниру #{tournament_id}.') @command_wrapper @handle_exceptions async def end_tournament(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 1: await update.message.reply_text('Использование: /end_tournament [id турнира]') return try: tournament_id: int = int(context.args[0]) except ValueError: await update.message.reply_text('ID должно быть числом.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT prize_pool, status FROM tournaments WHERE tournament_id = ?', (tournament_id,)) result: Optional[Tuple[int, str]] = cursor.fetchone() except sqlite3.Error as e: log_structured("End tournament query error", extra={'error': str(e)}) result = None if not result: await update.message.reply_text('Турнир не найден.') return prize, status = result if status == 'completed': await update.message.reply_text('Турнир уже завершен.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT COUNT(*) FROM tournament_participants WHERE tournament_id = ?', (tournament_id,)) participants = cursor.fetchone()[0] except sqlite3.Error as e: log_structured("End tournament participants count error", extra={'error': str(e)}) participants = 0 if participants < 3: await update.message.reply_text('Минимальное количество участников - 3.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT user_id, score FROM tournament_participants WHERE tournament_id = ? ORDER BY score DESC LIMIT 3', (tournament_id,)) top3: List[Tuple[int, int]] = cursor.fetchall() except sqlite3.Error as e: log_structured("End tournament top3 query error", extra={'error': str(e)}) top3 = [] distributions: List[float] = [0.5, 0.3, 0.2] for i, (user_id, score) in enumerate(top3): reward: int = int(prize * distributions[i]) current_points: int = get_user_points(update.message.chat.id, user_id) new_points: int = current_points + reward await update_user_points(update.message.chat.id, user_id, new_points, context, f"Приз в турнире {tournament_id}") try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE tournaments SET status = "completed" WHERE tournament_id = ?', (tournament_id,)) except sqlite3.Error as e: log_structured("End tournament update error", extra={'error': str(e)}) await update.message.reply_text(f'✅ Турнир завершен, награды выданы.') @command_wrapper @handle_exceptions async def create_battlepass(update: Update, context: CallbackContext) -> None: if update.callback_query: await update.callback_query.edit_message_text('Используйте /create_battlepass уровни rewards_json') return user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 2: await update.message.reply_text('Использование: /create_battlepass [уровни] [rewards_json]') return try: levels: int = int(context.args[0]) except ValueError: await update.message.reply_text('Уровни должны быть числом.') return rewards: str = ' '.join(context.args[1:]) # JSON string try: json.loads(rewards) # Validate JSON except json.JSONDecodeError: await update.message.reply_text('Rewards должны быть валидным JSON.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'INSERT INTO battle_passes (levels, rewards, created_by) VALUES (?, ?, ?)', (levels, rewards, user.id)) except sqlite3.Error as e: log_structured("Create battlepass error", extra={'error': str(e)}) await update.message.reply_text(f'✅ Battle Pass создан с {levels} уровнями.') @command_wrapper @handle_exceptions async def edit_bp(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 3: await update.message.reply_text('Использование: /edit_bp [id] [уровни] [rewards_json]') return try: bp_id: int = int(context.args[0]) levels: int = int(context.args[1]) except ValueError: await update.message.reply_text('ID и уровни должны быть числами.') return rewards: str = ' '.join(context.args[2:]) try: json.loads(rewards) except json.JSONDecodeError: await update.message.reply_text('Rewards должны быть валидным JSON.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE battle_passes SET levels = ?, rewards = ? WHERE bp_id = ?', (levels, rewards, bp_id)) except sqlite3.Error as e: log_structured("Edit bp error", extra={'error': str(e)}) await update.message.reply_text(f'✅ Battle Pass {bp_id} обновлен.') @command_wrapper @handle_exceptions async def del_bp(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 1: await update.message.reply_text('Использование: /del_bp [id]') return try: bp_id: int = int(context.args[0]) except ValueError: await update.message.reply_text('ID должен быть числом.') return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'DELETE FROM battle_passes WHERE bp_id = ?', (bp_id,)) except sqlite3.Error as e: log_structured("Del bp error", extra={'error': str(e)}) await update.message.reply_text(f'✅ Battle Pass {bp_id} удален.') @command_wrapper @handle_exceptions async def battlepass(update: Update, context: CallbackContext) -> None: if update.message: user = update.message.from_user chat = update.message.chat message = update.message elif update.callback_query: user = update.callback_query.from_user chat = update.callback_query.message.chat message = update.callback_query.message else: return effective_chat_id: int = get_effective_chat_id(update) level, exp = get_battle_pass_info(effective_chat_id, user.id) try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT bp_id, levels, rewards FROM battle_passes ORDER BY created_date DESC LIMIT 1') bp: Optional[Tuple[int, int, str]] = cursor.fetchone() except sqlite3.Error as e: log_structured("Battlepass query error", extra={'error': str(e)}) bp = None if not bp: text = 'Нет активного Battle Pass.' else: bp_id, levels, rewards_str = bp try: rewards: Dict[str, Any] = json.loads(rewards_str) except json.JSONDecodeError: rewards = {} if not isinstance(rewards, dict): rewards = {} text = f'🎖 Battle Pass:\nУровень: {level}/{levels}\nEXP: {exp}\n\nНаграды:\n' for lvl in range(1, levels + 1): reward = rewards.get(str(lvl), 'Нет') text += f'Уровень {lvl}: {reward}\n' keyboard: List[List[InlineKeyboardButton]] = [[InlineKeyboardButton("Назад", callback_data="menu_back_profile")]] reply_markup = InlineKeyboardMarkup(keyboard) if update.callback_query: await update.callback_query.edit_message_text(text, reply_markup=reply_markup) else: await message.reply_text(text, reply_markup=reply_markup) @command_wrapper @handle_exceptions async def referrals(update: Update, context: CallbackContext) -> None: if update.message: user = update.message.from_user chat = update.message.chat message = update.message elif update.callback_query: user = update.callback_query.from_user chat = update.callback_query.message.chat message = update.callback_query.message else: return effective_chat_id: int = get_effective_chat_id(update) try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT referral_link FROM users WHERE chat_id = ? AND user_id = ?', (effective_chat_id, user.id)) link: Optional[str] = cursor.fetchone()[0] if not link: link = f"t.me/{context.bot.username}?start=ref{user.id}" execute_with_retry(cursor, 'UPDATE users SET referral_link = ? WHERE chat_id = ? AND user_id = ?', (link, effective_chat_id, user.id)) except sqlite3.Error as e: log_structured("Referrals query error", extra={'error': str(e)}) link = "" text: str = f'Ваша реферальная ссылка: {link}\nЗа каждого приглашенного - 50 баллов.' keyboard: List[List[InlineKeyboardButton]] = [[InlineKeyboardButton("Назад", callback_data="menu_back_profile")]] reply_markup = InlineKeyboardMarkup(keyboard) if update.callback_query: await update.callback_query.edit_message_text(text, reply_markup=reply_markup) else: await message.reply_text(text, reply_markup=reply_markup) @command_wrapper @handle_exceptions async def giveallpoints(update: Update, context: CallbackContext) -> None: if update.callback_query: await update.callback_query.edit_message_text('Используйте /giveallpoints количество причина') return user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 2: await update.message.reply_text('Использование: /giveallpoints [количество] [причина]') return try: amount: int = int(context.args[0]) except ValueError: await update.message.reply_text('Количество должно быть числом.') return reason: str = ' '.join(context.args[1:]) try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT user_id FROM users WHERE chat_id = ?', (effective_chat_id,)) users: List[int] = [row[0] for row in cursor.fetchall()] except sqlite3.Error as e: log_structured("Give all points query error", extra={'error': str(e)}) users = [] for user_id in users: current_points: int = get_user_points(effective_chat_id, user_id) new_points: int = current_points + amount await update_user_points(effective_chat_id, user_id, new_points, context, f"Массовое начисление: {reason}") await update.message.reply_text(f'✅ {amount} баллов выдано всем пользователям в чате.') async def award_tournament_score(user_id: int, chat_id: int, points: int = 1) -> None: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT tournament_id FROM tournaments WHERE status = "ongoing"') ongoing: List[int] = [row[0] for row in cursor.fetchall()] for tid in ongoing: cursor.execute('SELECT 1 FROM tournament_participants WHERE tournament_id = ? AND user_id = ?', (tid, user_id)) if cursor.fetchone(): execute_with_retry(cursor, 'UPDATE tournament_participants SET score = score + ? WHERE tournament_id = ? AND user_id = ?', (points, tid, user_id)) except sqlite3.Error as e: log_structured("Award tournament score error", extra={'error': str(e)}) @command_wrapper @handle_exceptions async def history(update: Update, context: CallbackContext) -> None: user = update.message.from_user effective_chat_id: int = get_effective_chat_id(update) keyboard: List[List[InlineKeyboardButton]] = [ [InlineKeyboardButton("Переводы", callback_data="history_transfers")], [InlineKeyboardButton("Покупки", callback_data="history_purchases")], [InlineKeyboardButton("Выигрыши", callback_data="history_wins")], [InlineKeyboardButton("Назад", callback_data="history_back")] ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text('Выберите, что посмотреть:', reply_markup=reply_markup) @handle_exceptions async def history_callback(update: Update, context: CallbackContext) -> None: query = update.callback_query await query.answer() data = query.data if data == "history_back": await myprofile(update, context) return user_id = query.from_user.id effective_chat_id: int = get_effective_chat_id(update) if data == "history_transfers": try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT transfer_date, to_username, amount FROM transfers WHERE from_user_id = ? ORDER BY transfer_date DESC LIMIT 10', (user_id,)) transfers: List[Tuple[str, str, int]] = cursor.fetchall() except sqlite3.Error as e: log_structured("History transfers query error", extra={'error': str(e)}) transfers = [] if not transfers: text = 'Нет истории переводов.' else: text = 'История переводов:\n' for date, to_user, amount in transfers: text += f'{date}: @{to_user} +{amount}\n' elif data == "history_purchases": try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT purchase_date, item_name, cost FROM purchases WHERE user_id = ? ORDER BY purchase_date DESC LIMIT 10', (user_id,)) purchases: List[Tuple[str, str, int]] = cursor.fetchall() except sqlite3.Error as e: log_structured("History purchases query error", extra={'error': str(e)}) purchases = [] if not purchases: text = 'Нет истории покупок.' else: text = 'История покупок:\n' for date, item, cost in purchases: text += f'{date}: {item} -{cost}\n' elif data == "history_wins": # Предполагаем, что выигрыши записываются в transfers или отдельно, здесь симуляция text = 'Нет истории выигрышей.' # Добавить логику else: log_structured("Unknown history callback", extra={'data': data}) text = 'Неизвестная команда.' keyboard: List[List[InlineKeyboardButton]] = [[InlineKeyboardButton("Назад", callback_data="menu_back_profile")]] reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text(text, reply_markup=reply_markup) @command_wrapper @handle_exceptions async def seehistory(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 2: await update.message.reply_text('Использование: /seehistory @username [type]') return target_username: str = context.args[0].lstrip('@') type_: str = context.args[1].lower() target_user_id: Optional[int] = await resolve_username(update, context, target_username) if not target_user_id: return if type_ == "transfers": try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT transfer_date, to_username, amount FROM transfers WHERE from_user_id = ? ORDER BY transfer_date DESC LIMIT 10', (target_user_id,)) transfers: List[Tuple[str, str, int]] = cursor.fetchall() except sqlite3.Error as e: log_structured("See history transfers query error", extra={'error': str(e)}) transfers = [] if not transfers: text = 'Нет истории переводов.' else: text = f'История переводов @{target_username}:\n' for date, to_user, amount in transfers: text += f'{date}: @{to_user} +{amount}\n' elif type_ == "purchases": try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT purchase_date, item_name, cost FROM purchases WHERE user_id = ? ORDER BY purchase_date DESC LIMIT 10', (target_user_id,)) purchases: List[Tuple[str, str, int]] = cursor.fetchall() except sqlite3.Error as e: log_structured("See history purchases query error", extra={'error': str(e)}) purchases = [] if not purchases: text = 'Нет истории покупок.' else: text = f'История покупок @{target_username}:\n' for date, item, cost in purchases: text += f'{date}: {item} -{cost}\n' elif type_ == "wins": text = 'Нет истории выигрышей.' # Добавить логику else: text = 'Неверный тип. Используйте transfers, purchases или wins.' await update.message.reply_text(text) async def backup_db() -> None: if not os.path.exists(BACKUP_PATH): os.makedirs(BACKUP_PATH) backup_file: str = os.path.join(BACKUP_PATH, f"backup_{datetime.now().strftime('%Y%m%d')}.db") try: shutil.copy(DB_PATH, backup_file) log_structured(f"Backup created: {backup_file}") except Exception as e: log_structured("Backup DB error", extra={'error': str(e)}) async def clean_old_games() -> None: current_time: float = time.time() to_delete: List[int] = [] for game_id, state in list(game_states.items()): if 'created' in state and current_time - state['created'] > 3600: # 1 час таймаут to_delete.append(game_id) for game_id in to_delete: del game_states[game_id] if to_delete: log_structured(f"Cleared {len(to_delete)} old games") async def error_handler(update: Update, context: CallbackContext) -> None: log_structured("Exception while handling an update", extra={'exc_info': context.error}) error_message: str = str(context.error) traceback_text: str = ''.join(traceback.format_exception(type(context.error), context.error, context.error.__traceback__)) log_error(error_message, traceback_text) try: if LOG_CHANNEL_ID: await context.bot.send_message( chat_id=LOG_CHANNEL_ID, text=f'❌ Произошла ошибка в боте:\n\n{error_message}\n\n{traceback_text[:1000]}...' ) except Exception as e: log_structured("Error handler send message error", extra={'error': str(e)}) async def check_rate_limit(update: Update, context: CallbackContext) -> bool: user_id: int = update.effective_user.id current_time: float = time.time() if user_id in user_last_request and current_time - user_last_request[user_id] < 5: await update.message.reply_text('Подождите 5 секунд перед следующим запросом.') return False user_last_request[user_id] = current_time return True async def increment_interactions(chat_id: int, user_id: int, context: CallbackContext) -> None: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE users SET interactions_count = interactions_count + 1 WHERE chat_id = ? AND user_id = ?', (chat_id, user_id)) except sqlite3.Error as e: log_structured("Increment interactions error", extra={'error': str(e)}) await check_achievements(chat_id, user_id, context, 'interactions') def is_daily_bonus_available(chat_id: int, user_id: int) -> bool: try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute('SELECT last_daily_bonus FROM users WHERE chat_id = ? AND user_id = ?', (chat_id, user_id)) last_bonus: Optional[str] = cursor.fetchone()[0] if not last_bonus: return True last_bonus_dt: datetime = datetime.fromisoformat(last_bonus) return (datetime.now() - last_bonus_dt) > timedelta(days=1) except (sqlite3.Error, ValueError) as e: log_structured("Is daily bonus available error", extra={'error': str(e)}) return True @handle_exceptions async def claim_daily_bonus(update: Update, context: CallbackContext) -> None: query = update.callback_query await query.answer() user_id: int = query.from_user.id effective_chat_id: int = get_effective_chat_id(update) if not is_daily_bonus_available(effective_chat_id, user_id): await query.edit_message_text('Ежедневный бонус уже забран сегодня.') return current_points: int = get_user_points(effective_chat_id, user_id) new_points: int = current_points + 10 await update_user_points(effective_chat_id, user_id, new_points, context, "Ежедневный бонус") try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE users SET last_daily_bonus = ? WHERE chat_id = ? AND user_id = ?', (datetime.now().isoformat(), effective_chat_id, user_id)) except sqlite3.Error as e: log_structured("Claim daily bonus update error", extra={'error': str(e)}) notify_bonus: bool = get_user_setting(user_id, 'notify_bonus') if notify_bonus: try: await context.bot.send_message(user_id, "🎁 Вы получили ежедневный бонус +10 баллов!") except Exception as e: log_structured("Daily bonus notification error", extra={'error': str(e)}) await query.edit_message_text('✅ Ежедневный бонус забран! +10 баллов.') def get_user_setting(user_id: int, setting: str) -> bool: allowed_settings: List[str] = ['notify_ach', 'notify_bp', 'notify_bonus', 'notify_points'] if setting not in allowed_settings: return True try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() cursor.execute(f'SELECT {setting} FROM user_settings WHERE user_id = ?', (user_id,)) result: Optional[Tuple[bool]] = cursor.fetchone() return result[0] if result else True except sqlite3.Error as e: log_structured("Get user setting error", extra={'error': str(e)}) return True def set_user_setting(user_id: int, setting: str, value: bool) -> None: allowed_settings: List[str] = ['notify_ach', 'notify_bp', 'notify_bonus', 'notify_points'] if setting not in allowed_settings: return try: with sqlite3.connect(DB_PATH, timeout=30) as conn: cursor: sqlite3.Cursor = conn.cursor() execute_with_retry(cursor, f'UPDATE user_settings SET {setting} = ? WHERE user_id = ?', (value, user_id)) except sqlite3.Error as e: log_structured("Set user setting error", extra={'error': str(e)}) @handle_exceptions async def settings_menu(update: Update, context: CallbackContext) -> None: query = update.callback_query await query.answer() user_id: int = query.from_user.id notify_ach: bool = get_user_setting(user_id, 'notify_ach') notify_bp: bool = get_user_setting(user_id, 'notify_bp') notify_bonus: bool = get_user_setting(user_id, 'notify_bonus') notify_points: bool = get_user_setting(user_id, 'notify_points') text: str = 'Настройки уведомлений:\n' text += f'Достижения: {"Вкл" if notify_ach else "Выкл"}\n' text += f'Battle Pass: {"Вкл" if notify_bp else "Выкл"}\n' text += f'Бонус: {"Вкл" if notify_bonus else "Выкл"}\n' text += f'Изменения баллов: {"Вкл" if notify_points else "Выкл"}\n' keyboard: List[List[InlineKeyboardButton]] = [ [InlineKeyboardButton(f"Достижения {'Выкл' if notify_ach else 'Вкл'}", callback_data="settings_toggle_notify_ach")], [InlineKeyboardButton(f"Battle Pass {'Выкл' if notify_bp else 'Вкл'}", callback_data="settings_toggle_notify_bp")], [InlineKeyboardButton(f"Бонус {'Выкл' if notify_bonus else 'Вкл'}", callback_data="settings_toggle_notify_bonus")], [InlineKeyboardButton(f"Изменения баллов {'Выкл' if notify_points else 'Вкл'}", callback_data="settings_toggle_notify_points")], [InlineKeyboardButton("Назад", callback_data="menu_back_profile")] ] reply_markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text(text, reply_markup=reply_markup) @handle_exceptions async def settings_callback(update: Update, context: CallbackContext) -> None: query = update.callback_query await query.answer() data = query.data user_id: int = query.from_user.id if data.startswith('settings_toggle_'): setting: str = data.replace('settings_toggle_', '') current: bool = get_user_setting(user_id, setting) new_value: bool = not current set_user_setting(user_id, setting, new_value) await settings_menu(update, context) else: log_structured("Unknown settings callback", extra={'data': data}) await query.edit_message_text("Неизвестная команда.") # Плагины plugins: Dict[str, Dict[str, Any]] = {} # name: {'module': module, 'enabled': bool, 'description': str, 'commands': list of dicts {'command': str, 'description': str}} def get_plugin_commands() -> str: """Возвращает строку с командами плагинов для раздела помощи.""" if not plugins: return "" text = "\n🔌 Команды плагинов:\n" for plugin_name, plugin_info in plugins.items(): if plugin_info.get('enabled', False): for cmd in plugin_info.get('commands', []): text += f"{cmd['command']} - {cmd['description']}\n" return text def load_plugins(application: Application) -> None: # Создаем папку для плагинов, если её нет if not os.path.exists(PLUGINS_DIR): os.makedirs(PLUGINS_DIR) # Проверяем наличие plugins.zip в корневой папке /data root_plugins_zip = '/data/plugins.zip' if os.path.exists(root_plugins_zip): try: log_structured(f"Found plugins.zip in root directory, extracting to {PLUGINS_DIR}") # Создаем временную директорию для распаковки temp_extract_dir = tempfile.mkdtemp() try: # Распаковываем архив во временную директорию with zipfile.ZipFile(root_plugins_zip, 'r') as zip_ref: zip_ref.extractall(temp_extract_dir) # Проверяем структуру распакованного архива extracted_items = os.listdir(temp_extract_dir) # Если в архиве есть папка plugins, берем её содержимое if 'plugins' in extracted_items and os.path.isdir(os.path.join(temp_extract_dir, 'plugins')): plugins_dir_path = os.path.join(temp_extract_dir, 'plugins') # Копируем содержимое папки plugins в целевую директорию for item in os.listdir(plugins_dir_path): src_path = os.path.join(plugins_dir_path, item) dst_path = os.path.join(PLUGINS_DIR, item) if os.path.isdir(src_path): if os.path.exists(dst_path): shutil.rmtree(dst_path) shutil.copytree(src_path, dst_path) else: if os.path.exists(dst_path): os.remove(dst_path) shutil.copy2(src_path, dst_path) else: # Если нет папки plugins, копируем все содержимое архива for item in extracted_items: src_path = os.path.join(temp_extract_dir, item) dst_path = os.path.join(PLUGINS_DIR, item) if os.path.isdir(src_path): if os.path.exists(dst_path): shutil.rmtree(dst_path) shutil.copytree(src_path, dst_path) else: if os.path.exists(dst_path): os.remove(dst_path) shutil.copy2(src_path, dst_path) log_structured("Successfully extracted plugins.zip from root directory") finally: # Удаляем временную директорию shutil.rmtree(temp_extract_dir) except Exception as e: log_structured("Error extracting plugins.zip from root directory", extra={'error': str(e)}) # Создаем временную директорию для обработки ZIP-архивов плагинов temp_dir = tempfile.mkdtemp() try: # Обрабатываем ZIP-архивы в папке плагинов for file in glob.glob(os.path.join(PLUGINS_DIR, '*.zip')): try: log_structured(f"Processing ZIP plugin: {file}") # Распаковываем архив во временную директорию with zipfile.ZipFile(file, 'r') as zip_ref: zip_ref.extractall(temp_dir) # Ищем Python файлы в распакованном архиве for py_file in glob.glob(os.path.join(temp_dir, '**', '*.py'), recursive=True): name = os.path.splitext(os.path.basename(py_file))[0] if name == '__init__': continue try: # Загружаем модуль из распакованного файла spec = importlib.util.spec_from_file_location(name, py_file) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) # Регистрируем плагин if hasattr(module, 'register_handlers'): with sqlite3.connect(DB_PATH) as conn: cursor = conn.cursor() cursor.execute('SELECT enabled, description, commands FROM plugins WHERE name = ?', (name,)) result = cursor.fetchone() if not result: description = getattr(module, 'DESCRIPTION', 'No description') commands = getattr(module, 'COMMANDS', []) commands_json = json.dumps(commands) cursor.execute('INSERT INTO plugins (name, description, commands) VALUES (?, ?, ?)', (name, description, commands_json)) enabled = True else: enabled, description, commands_json = result commands = json.loads(commands_json) if enabled: module.register_handlers(application) plugins[name] = { 'module': module, 'enabled': enabled, 'description': description, 'commands': commands } log_structured(f"ZIP plugin loaded: {name}") except Exception as e: log_structured(f"ZIP plugin {name} load error", extra={'error': str(e)}) # Очищаем временную директорию для следующего архива for item in os.listdir(temp_dir): item_path = os.path.join(temp_dir, item) if os.path.isdir(item_path): shutil.rmtree(item_path) else: os.remove(item_path) except Exception as e: log_structured(f"ZIP archive {file} processing error", extra={'error': str(e)}) # Обрабатываем обычные .py файлы (для обратной совместимости) for file in glob.glob(os.path.join(PLUGINS_DIR, '*.py')): name = os.path.basename(file)[:-3] if name == '__init__': continue try: spec = importlib.util.spec_from_file_location(name, file) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) if hasattr(module, 'register_handlers'): with sqlite3.connect(DB_PATH) as conn: cursor = conn.cursor() cursor.execute('SELECT enabled, description, commands FROM plugins WHERE name = ?', (name,)) result = cursor.fetchone() if not result: description = getattr(module, 'DESCRIPTION', 'No description') commands = getattr(module, 'COMMANDS', []) commands_json = json.dumps(commands) cursor.execute('INSERT INTO plugins (name, description, commands) VALUES (?, ?, ?)', (name, description, commands_json)) enabled = True else: enabled, description, commands_json = result commands = json.loads(commands_json) if enabled: module.register_handlers(application) plugins[name] = { 'module': module, 'enabled': enabled, 'description': description, 'commands': commands } log_structured(f"Plugin loaded: {name}") except Exception as e: log_structured(f"Plugin {name} load error", extra={'error': str(e)}) finally: # Убираем временную директорию if os.path.exists(temp_dir): shutil.rmtree(temp_dir) @command_wrapper @handle_exceptions async def plugins_list(update: Update, context: CallbackContext) -> None: user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return text = '📦 Список плагинов:\n' for name, info in plugins.items(): status = 'Вкл' if info['enabled'] else 'Выкл' text += f'{name}: {status} - {info["description"]}\n' await update.message.reply_text(text) @command_wrapper @handle_exceptions async def plugin_enable(update: Update, context: CallbackContext) -> None: global application # В начале этих функций user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 1: await update.message.reply_text('Использование: /plugin_enable [name]') return name = context.args[0] if name not in plugins: await update.message.reply_text('Плагин не найден.') return if plugins[name]['enabled']: await update.message.reply_text('Плагин уже включен.') return plugins[name]['enabled'] = True try: with sqlite3.connect(DB_PATH) as conn: cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE plugins SET enabled = TRUE WHERE name = ?', (name,)) except sqlite3.Error as e: log_structured("Plugin enable error", extra={'error': str(e)}) # Регистрируем handlers module = plugins[name]['module'] if hasattr(module, 'register_handlers'): module.register_handlers(application) await update.message.reply_text(f'✅ Плагин {name} включен.') @command_wrapper @handle_exceptions async def plugin_disable(update: Update, context: CallbackContext) -> None: global application # В начале этих функций user = update.message.from_user if not is_admin(user.id): await update.message.reply_text('У вас нет прав.') return if len(context.args) < 1: await update.message.reply_text('Использование: /plugin_disable [name]') return name = context.args[0] if name not in plugins: await update.message.reply_text('Плагин не найден.') return if not plugins[name]['enabled']: await update.message.reply_text('Плагин уже выключен.') return plugins[name]['enabled'] = False try: with sqlite3.connect(DB_PATH) as conn: cursor = conn.cursor() execute_with_retry(cursor, 'UPDATE plugins SET enabled = FALSE WHERE name = ?', (name,)) except sqlite3.Error as e: log_structured("Plugin disable error", extra={'error': str(e)}) # Handlers не удаляем, но плагин должен проверять enabled внутри функций, если нужно await update.message.reply_text(f'✅ Плагин {name} выключен.') def main() -> None: init_db() global application application = Application.builder().token(TOKEN).build() # ЗАГРУЗКА ПЛАГИНОВ - ДОБАВЛЕН ВЫЗОВ load_plugins(application) bot_mention_filter = BotMentionFilter() async def post_init(app: Application) -> None: me = await app.bot.get_me() bot_mention_filter.set_bot_username(me.username) log_structured(f"Bot started as @{me.username}") application.post_init = post_init application.add_handler(MessageHandler(filters.ALL & ~filters.COMMAND, update_user_info)) application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("help", help_command)) application.add_handler(CommandHandler("myprofile", myprofile)) application.add_handler(CommandHandler("mypoints", mypoints)) application.add_handler(CommandHandler("showpoints", showpoints)) application.add_handler(CommandHandler("givepoints", givepoints)) application.add_handler(CommandHandler("removepoints", removepoints)) application.add_handler(CommandHandler("pointstable", pointstable)) application.add_handler(CommandHandler("clearpoints", clearpoints)) application.add_handler(CommandHandler("transfer", transfer)) application.add_handler(CommandHandler("addvip", add_vip)) application.add_handler(CommandHandler("removevip", remove_vip)) application.add_handler(CommandHandler("zeushome", zeushome)) application.add_handler(CommandHandler("addadmin", addadmin)) application.add_handler(CommandHandler("deladmin", deladmin)) application.add_handler(CommandHandler("mychannels", mychannels)) application.add_handler(CommandHandler("refresh", refresh_user)) application.add_handler(CommandHandler("shop", shop)) application.add_handler(CommandHandler("buy", buy)) application.add_handler(CommandHandler("additem", additem)) application.add_handler(CommandHandler("addexp", addexp)) application.add_handler(CommandHandler("edititem", edititem)) application.add_handler(CommandHandler("delitem", delitem)) application.add_handler(CommandHandler("clearshop", clearshop)) application.add_handler(CommandHandler("achievement", achievement)) application.add_handler(CommandHandler("approve_ach", approve_ach)) application.add_handler(CommandHandler("reject_ach", reject_ach)) application.add_handler(CommandHandler("clanstats", clanstats)) application.add_handler(CommandHandler("toggle_logs", toggle_logs)) application.add_handler(CommandHandler("listcreators", listcreators)) application.add_handler(CommandHandler("deactivatecreator", deactivatecreator)) application.add_handler(CommandHandler("create_quest", create_quest)) application.add_handler(CommandHandler("edit_quest", edit_quest)) application.add_handler(CommandHandler("del_quest", del_quest)) application.add_handler(CommandHandler("quests", quests)) application.add_handler(CommandHandler("complete_quest", complete_quest)) application.add_handler(CommandHandler("create_tournament", create_tournament)) application.add_handler(CommandHandler("set_tournament_status", set_tournament_status)) application.add_handler(CommandHandler("tournaments", tournaments)) application.add_handler(CommandHandler("join_tournament", join_tournament)) application.add_handler(CommandHandler("end_tournament", end_tournament)) application.add_handler(CommandHandler("create_battlepass", create_battlepass)) application.add_handler(CommandHandler("edit_bp", edit_bp)) application.add_handler(CommandHandler("del_bp", del_bp)) application.add_handler(CommandHandler("battlepass", battlepass)) application.add_handler(CommandHandler("referral", referrals)) application.add_handler(CommandHandler("adminpanel", adminpanel)) application.add_handler(CommandHandler("giveallpoints", giveallpoints)) application.add_handler(CommandHandler("add_ach_type", add_achievement_type)) application.add_handler(CommandHandler("del_ach_type", del_achievement_type)) application.add_handler(CommandHandler("approve_user_ach", approve_user_ach)) application.add_handler(CommandHandler("reject_user_ach", reject_user_ach)) application.add_handler(CommandHandler("history", history)) application.add_handler(CommandHandler("seehistory", seehistory)) application.add_handler(CommandHandler("plugins_list", plugins_list)) application.add_handler(CommandHandler("plugin_enable", plugin_enable)) application.add_handler(CommandHandler("plugin_disable", plugin_disable)) application.add_handler(CommandHandler("roulette", roulette)) application.add_handler(CommandHandler("dice", dice)) application.add_handler(CommandHandler("coinflip", coinflip)) application.add_handler(CommandHandler("slots", slots)) application.add_handler(CommandHandler("wheel", wheel)) application.add_handler(CommandHandler("blackjack", blackjack)) application.add_handler(CommandHandler("duel", duel)) application.add_handler(CommandHandler("highlow", highlow)) application.add_handler(CommandHandler("coinflip_pvp", coinflip_pvp)) application.add_handler(CommandHandler("dice_pvp", dice_pvp)) application.add_handler(CallbackQueryHandler(menu_callback, pattern="^menu_")) application.add_handler(CallbackQueryHandler(admin_callback, pattern="^admin_")) application.add_handler(CallbackQueryHandler(help_callback, pattern="^help_page_")) application.add_handler(CallbackQueryHandler(achievement_callback, pattern="^ach_")) application.add_handler(CallbackQueryHandler(duel_callback, pattern="^duel_")) application.add_handler(CallbackQueryHandler(highlow_callback, pattern="^highlow_")) application.add_handler(CallbackQueryHandler(coinflip_pvp_callback, pattern="^coinflip_")) application.add_handler(CallbackQueryHandler(dice_pvp_callback, pattern="^dice_")) application.add_handler(CallbackQueryHandler(quest_callback, pattern="^quest_")) application.add_handler(CallbackQueryHandler(history_callback, pattern="^history_")) application.add_handler(CallbackQueryHandler(settings_callback, pattern="^settings_")) application.add_handler(CallbackQueryHandler(blackjack_callback, pattern="^bj_")) application.add_handler(CallbackQueryHandler(points_table_callback, pattern="^points_page_")) application.add_handler(MessageHandler(filters.StatusUpdate.NEW_CHAT_MEMBERS, handle_new_chat_members)) application.add_handler(MessageHandler(filters.StatusUpdate.LEFT_CHAT_MEMBER, handle_left_chat_member)) application.add_error_handler(error_handler) # Scheduler initialization moved after application creation global scheduler scheduler = AsyncIOScheduler() scheduler.add_job(backup_db, 'cron', hour=0) scheduler.add_job(clean_old_games, 'interval', minutes=5) # Добавлена очистка старых игр scheduler.start() log_structured("Starting polling...") application.run_polling(drop_pending_updates=True) # drop_pending_updates для production if __name__ == '__main__': main()