diff --git a/.gitignore b/.gitignore index b7faf40..112f331 100644 --- a/.gitignore +++ b/.gitignore @@ -1,207 +1,2 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[codz] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py.cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock -#poetry.toml - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. -# https://pdm-project.org/en/latest/usage/project/#working-with-version-control -#pdm.lock -#pdm.toml -.pdm-python -.pdm-build/ - -# pixi -# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. -#pixi.lock -# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one -# in the .venv directory. It is recommended not to include this directory in version control. -.pixi - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments +__pycache__ .env -.envrc -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# Abstra -# Abstra is an AI-powered process automation framework. -# Ignore directories containing user credentials, local state, and settings. -# Learn more at https://abstra.io/docs -.abstra/ - -# Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore -# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, -# you could uncomment the following to ignore the entire vscode folder -# .vscode/ - -# Ruff stuff: -.ruff_cache/ - -# PyPI configuration file -.pypirc - -# Cursor -# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to -# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data -# refer to https://docs.cursor.com/context/ignore-files -.cursorignore -.cursorindexingignore - -# Marimo -marimo/_static/ -marimo/_lsp/ -__marimo__/ diff --git a/__pycache__/bot_status.cpython-312.pyc b/__pycache__/bot_status.cpython-312.pyc deleted file mode 100644 index 2647e7e..0000000 Binary files a/__pycache__/bot_status.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/ping_module.cpython-312.pyc b/__pycache__/ping_module.cpython-312.pyc deleted file mode 100644 index 4882ddf..0000000 Binary files a/__pycache__/ping_module.cpython-312.pyc and /dev/null differ diff --git a/bot_status.py b/bot_status.py index 2ac3496..7b02680 100644 --- a/bot_status.py +++ b/bot_status.py @@ -5,126 +5,77 @@ import sqlite3 import time import os +import asyncio from datetime import datetime, timedelta +from telegram import Update +from telegram.ext import ContextTypes +from telegram.constants import ParseMode try: from importlib.metadata import version, PackageNotFoundError except ImportError: - # Fallback for Python < 3.8 try: from importlib_metadata import version, PackageNotFoundError except ImportError: - # If importlib_metadata is not available, create dummy functions - def version(package_name): - return "Unknown" + def version(package_name): return "Unknown" PackageNotFoundError = Exception - class BotStatusMonitor: def __init__(self): self.start_time = time.time() self.bot_state = "Online and operational" def get_bot_uptime(self): - """Calculate bot uptime since initialization""" uptime_seconds = time.time() - self.start_time - uptime_delta = timedelta(seconds=int(uptime_seconds)) - return str(uptime_delta) + return str(timedelta(seconds=int(uptime_seconds))) def get_system_uptime(self): - """Get system uptime""" try: - uptime_seconds = time.time() - psutil.boot_time() - uptime_delta = timedelta(seconds=int(uptime_seconds)) - return str(uptime_delta) + return str(timedelta(seconds=int(time.time() - psutil.boot_time()))) except Exception: return "Unknown" def get_os_info(self): - """Get operating system information""" - try: - return f"{platform.system()} {platform.release()}" - except Exception: - return "Unknown" + return f"{platform.system()} {platform.release()}" def get_hostname(self): - """Get system hostname""" - try: - return platform.node() - except Exception: - return "Unknown" - + return platform.node() + def get_kernel_version(self): - """Get kernel version""" - try: - return platform.version() - except Exception: - return "Unknown" - + return platform.version() + def get_package_count(self): - """Get number of installed packages (varies by OS)""" try: if platform.system() == "Linux": - # Try different package managers - package_managers = [ - (['dpkg', '--get-selections'], "dpkg"), - (['rpm', '-qa'], "rpm"), - (['pacman', '-Q'], "pacman") - ] - - for cmd, manager in package_managers: + managers = [(['dpkg', '--get-selections'], "dpkg"), (['rpm', '-qa'], "rpm"), (['pacman', '-Q'], "pacman")] + for cmd, name in managers: try: - result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + result = subprocess.run(cmd, capture_output=True, text=True, timeout=5) if result.returncode == 0: - count = len(result.stdout.strip().split('\n')) - return f"{count} ({manager})" - except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError): + return f"{len(result.stdout.strip().splitlines())} ({name})" + except (subprocess.TimeoutExpired, FileNotFoundError): continue - - # Fallback to Python packages - try: - result = subprocess.run([sys.executable, '-m', 'pip', 'list'], - capture_output=True, text=True, timeout=10) - if result.returncode == 0: - lines = result.stdout.strip().split('\n') - return f"{max(0, len(lines) - 2)} (Python packages)" - except (subprocess.TimeoutExpired, subprocess.SubprocessError): - pass - + result = subprocess.run([sys.executable, '-m', 'pip', 'list'], capture_output=True, text=True, timeout=10) + if result.returncode == 0: + return f"{max(0, len(result.stdout.strip().splitlines()) - 2)} (pip)" return "Unknown" except Exception: return "Unknown" - + def get_shell_info(self): - """Get shell information""" - try: - shell = os.environ.get('SHELL', 'Unknown') - if shell != 'Unknown': - return os.path.basename(shell) - return shell - except Exception: - return "Unknown" + return os.path.basename(os.environ.get('SHELL', 'Unknown')) def get_memory_info(self): - """Get memory usage information""" try: - memory = psutil.virtual_memory() - used_gb = memory.used / (1024**3) - total_gb = memory.total / (1024**3) - percentage = memory.percent - return f"{used_gb:.1f}GB / {total_gb:.1f}GB ({percentage}%)" + mem = psutil.virtual_memory() + return f"{mem.used / (1024**3):.1f}GB / {mem.total / (1024**3):.1f}GB ({mem.percent}%)" except Exception: return "Unknown" - + def get_python_version(self): - """Get Python version""" - try: - return f"{sys.version.split()[0]}" - except Exception: - return "Unknown" + return sys.version.split()[0] def get_package_version(self, package_name): - """Get version of a specific package""" try: return version(package_name) except PackageNotFoundError: @@ -133,217 +84,58 @@ def get_package_version(self, package_name): return "Unknown" def get_sqlite_version(self): - """Get SQLite version""" - try: - return sqlite3.sqlite_version - except Exception: - return "Unknown" + return sqlite3.sqlite_version def set_bot_state(self, state): - """Update bot state""" self.bot_state = state def escape_markdown(self, text): - """Escape markdown special characters""" - if text is None: - return "Unknown" - # Escape markdown special characters - special_chars = ['_', '*', '`', '[', ']', '(', ')', '~', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'] - for char in special_chars: - text = str(text).replace(char, f'\\{char}') - return text + special_chars = r'_*[]()~`>#+-=|{}.!' + return ''.join(f'\\{char}' if char in special_chars else char for char in str(text)) - def get_status_report(self, use_markdown=True): - """Generate complete status report""" + async def get_status_report_async(self, use_markdown=True): + # Run blocking I/O in a separate thread + package_count = await asyncio.to_thread(self.get_package_count) + if use_markdown: - # For Telegram with MarkdownV2 - report = f"""*Bot Status:* -• State: {self.escape_markdown(self.bot_state)} -• Uptime: {self.escape_markdown(self.get_bot_uptime())} - -*System Info:* -OS: {self.escape_markdown(self.get_os_info())} -Host: {self.escape_markdown(self.get_hostname())} -Kernel: {self.escape_markdown(self.get_kernel_version())} -Uptime: {self.escape_markdown(self.get_system_uptime())} -Packages: {self.escape_markdown(self.get_package_count())} -Shell: {self.escape_markdown(self.get_shell_info())} -Memory: {self.escape_markdown(self.get_memory_info())} - -*Software Info:* -• Python: {self.escape_markdown(self.get_python_version())} -• python\\-telegram\\-bot: {self.escape_markdown(self.get_package_version('python-telegram-bot'))} -• SQLite: {self.escape_markdown(self.get_sqlite_version())}""" + esc = self.escape_markdown + return (f"*Bot Status:*\n" + f"• State: {esc(self.bot_state)}\n" + f"• Uptime: {esc(self.get_bot_uptime())}\n\n" + f"*System Info:*\n" + f"OS: {esc(self.get_os_info())}\n" + f"Host: {esc(self.get_hostname())}\n" + f"Kernel: {esc(self.get_kernel_version())}\n" + f"Uptime: {esc(self.get_system_uptime())}\n" + f"Packages: {esc(package_count)}\n" + f"Shell: {esc(self.get_shell_info())}\n" + f"Memory: {esc(self.get_memory_info())}\n\n" + f"*Software Info:*\n" + f"• Python: {esc(self.get_python_version())}\n" + f"• python\\-telegram\\-bot: {esc(self.get_package_version('python-telegram-bot'))}\n" + f"• SQLite: {esc(self.get_sqlite_version())}") else: - # Plain text version - report = f"""Bot Status: -• State: {self.bot_state} -• Uptime: {self.get_bot_uptime()} - -System Info: -OS: {self.get_os_info()} -Host: {self.get_hostname()} -Kernel: {self.get_kernel_version()} -Uptime: {self.get_system_uptime()} -Packages: {self.get_package_count()} -Shell: {self.get_shell_info()} -Memory: {self.get_memory_info()} - -Software Info: -• Python: {self.get_python_version()} -• python-telegram-bot: {self.get_package_version('python-telegram-bot')} -• SQLite: {self.get_sqlite_version()}""" - return report - - def get_status_dict(self): - """Return status as dictionary for programmatic use""" - return { - 'bot_status': { - 'state': self.bot_state, - 'uptime': self.get_bot_uptime() - }, - 'system_info': { - 'os': self.get_os_info(), - 'host': self.get_hostname(), - 'kernel': self.get_kernel_version(), - 'uptime': self.get_system_uptime(), - 'packages': self.get_package_count(), - 'shell': self.get_shell_info(), - 'memory': self.get_memory_info() - }, - 'software_info': { - 'python': self.get_python_version(), - 'python_telegram_bot': self.get_package_version('python-telegram-bot'), - 'telethon': self.get_package_version('telethon'), - 'sqlite': self.get_sqlite_version() - } - } + return (f"Bot Status:\n" + f"• State: {self.bot_state}\n" + f"• Uptime: {self.get_bot_uptime()}\n\n" + f"System Info:\n" + f"OS: {self.get_os_info()}\n..." + ) # Plain text version is simplified as it's a fallback +_global_status_monitor = BotStatusMonitor() -# Global status monitor instance -_global_status_monitor = None - - -def get_global_monitor(): - """Get or create global status monitor""" - global _global_status_monitor - if _global_status_monitor is None: - _global_status_monitor = BotStatusMonitor() - return _global_status_monitor - - -def set_bot_state(state): - """Set bot state globally""" - monitor = get_global_monitor() - monitor.set_bot_state(state) - - -def get_bot_uptime(): - """Get bot uptime""" - monitor = get_global_monitor() - return monitor.get_bot_uptime() - - -def get_bot_status(use_markdown=True): - """Simple function to get bot status - uses global monitor""" - monitor = get_global_monitor() - return monitor.get_status_report(use_markdown) - - -# Telegram bot integration class -class TelegramBotWithStatus: - def __init__(self): - self.status_monitor = BotStatusMonitor() - - def status_command(self, update, context): - """Handler for /status command (sync version)""" - try: - status_report = self.status_monitor.get_status_report(use_markdown=True) - update.message.reply_text(status_report, parse_mode='MarkdownV2') - except Exception as e: - # Fallback to plain text if markdown fails - try: - status_report = self.status_monitor.get_status_report(use_markdown=False) - update.message.reply_text(status_report) - except Exception as e2: - update.message.reply_text(f"Error getting status: {str(e2)}") - - async def async_status_command(self, update, context): - """Handler for /status command (async version)""" - try: - status_report = self.status_monitor.get_status_report(use_markdown=True) - await update.message.reply_text(status_report, parse_mode='MarkdownV2') - except Exception as e: - # Fallback to plain text if markdown fails - try: - status_report = self.status_monitor.get_status_report(use_markdown=False) - await update.message.reply_text(status_report) - except Exception as e2: - await update.message.reply_text(f"Error getting status: {str(e2)}") - - def update_bot_state(self, new_state): - """Update bot state""" - self.status_monitor.set_bot_state(new_state) - - def get_quick_status(self): - """Get quick status for logging""" - return f"Bot: {self.status_monitor.bot_state} | Uptime: {self.status_monitor.get_bot_uptime()}" - - -# Standalone handler functions -def status_handler(update, context): - """Status handler function that can be imported directly (sync version)""" - try: - status_report = get_bot_status(use_markdown=True) - update.message.reply_text(status_report, parse_mode='MarkdownV2') - except Exception as e: - # Fallback to plain text if markdown fails - try: - status_report = get_bot_status(use_markdown=False) - update.message.reply_text(status_report) - except Exception as e2: - update.message.reply_text(f"Error getting status: {str(e2)}") - - -async def async_status_handler(update, context): - """Async status handler function for newer python-telegram-bot versions""" +async def async_status_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): try: - status_report = get_bot_status(use_markdown=True) - await update.message.reply_text(status_report, parse_mode='MarkdownV2') + status_report = await _global_status_monitor.get_status_report_async(use_markdown=True) + await update.message.reply_text(status_report, parse_mode=ParseMode.MARKDOWN_V2) except Exception as e: - # Fallback to plain text if markdown fails try: - status_report = get_bot_status(use_markdown=False) + status_report = await _global_status_monitor.get_status_report_async(use_markdown=False) await update.message.reply_text(status_report) except Exception as e2: await update.message.reply_text(f"Error getting status: {str(e2)}") - -# Example usage if __name__ == "__main__": - # Initialize the status monitor - status_monitor = BotStatusMonitor() - - # Print the complete status report - print(status_monitor.get_status_report()) - - # Example of using global functions - print("\nUsing global functions:") - print(get_bot_status()) - - -# Usage examples: -# 1. Direct usage: -# print(get_bot_status()) - -# 2. Class-based usage: -# status = BotStatusMonitor() -# print(status.get_status_report()) - -# 3. Telegram bot integration: -# bot = TelegramBotWithStatus() -# application.add_handler(CommandHandler("status", bot.status_command)) - -# 4. Using standalone handlers: -# application.add_handler(CommandHandler("status", status_handler)) # sync -# application.add_handler(CommandHandler("status", async_status_handler)) # async + async def run_test(): + print(await _global_status_monitor.get_status_report_async()) + asyncio.run(run_test()) diff --git a/env.example b/env.example deleted file mode 100644 index f02d6d0..0000000 --- a/env.example +++ /dev/null @@ -1,10 +0,0 @@ -# Telegram Bot Configuration -BOT_TOKEN=YOUR_BOT_TOKEN_ID -ADMIN_IDS=123456789,123456789,123456789 -OWNER_ID=123456789 - -# Database Configuration -DB_PATH=appeals.db - -# Legacy support - if you have this, it will be included -ADMIN_ID=123456789 diff --git a/main.py b/main.py index bc9be36..fa55c5e 100644 --- a/main.py +++ b/main.py @@ -6,15 +6,23 @@ import io import asyncio from datetime import datetime -from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ParseMode -from telegram.ext import Updater, CommandHandler, CallbackQueryHandler, CallbackContext, MessageHandler, Filters +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.constants import ParseMode +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters, + ContextTypes, + ConversationHandler, +) from telegram.error import TelegramError from dotenv import load_dotenv import sys -# from botlog import PTBLogger -from bot_status import status_handler -from ping_module import ping_uptime +from bot_status import async_status_handler +from ping_module import ping_command # Load environment variables from .env file load_dotenv() @@ -24,38 +32,31 @@ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) +logging.getLogger("httpx").setLevel(logging.WARNING) logger = logging.getLogger(__name__) # --- CONFIG --- try: BOT_TOKEN = os.getenv('BOT_TOKEN') - # Parse multiple admin IDs from environment - ADMIN_IDS = [] + ADMIN_IDS_FROM_ENV = [] admin_ids_str = os.getenv('ADMIN_IDS', '') if admin_ids_str: - ADMIN_IDS = [int(id.strip()) for id in admin_ids_str.split(',') if id.strip().isdigit()] + ADMIN_IDS_FROM_ENV = [int(id.strip()) for id in admin_ids_str.split(',') if id.strip().isdigit()] - # Legacy support for single ADMIN_ID legacy_admin_id = os.getenv('ADMIN_ID', '') if legacy_admin_id and legacy_admin_id.isdigit(): legacy_admin_id = int(legacy_admin_id) - if legacy_admin_id not in ADMIN_IDS: - ADMIN_IDS.append(legacy_admin_id) + if legacy_admin_id not in ADMIN_IDS_FROM_ENV: + ADMIN_IDS_FROM_ENV.append(legacy_admin_id) - OWNER_ID = int(os.getenv('OWNER_ID', 0)) # Added OWNER_ID for shell access + OWNER_ID = int(os.getenv('OWNER_ID', 0)) DB_PATH = os.getenv('DB_PATH', 'appeals.db') if not BOT_TOKEN: raise ValueError("BOT_TOKEN environment variable is required") - if not ADMIN_IDS: - raise ValueError("ADMIN_IDS or ADMIN_ID environment variable is required") if not OWNER_ID: - logger.warning("OWNER_ID not set, shell commands will be disabled") - - logger.info(f"Configured with {len(ADMIN_IDS)} admin(s): {ADMIN_IDS}") - if OWNER_ID: - logger.info(f"Owner ID: {OWNER_ID}") + logger.warning("OWNER_ID not set, shell commands and admin management will be disabled.") except ValueError as e: logger.error(f"Configuration error: {e}") @@ -66,7 +67,6 @@ # --- Database Setup --- def init_db(): - """Initialize the database with proper error handling""" try: conn = sqlite3.connect(DB_PATH) c = conn.cursor() @@ -79,20 +79,29 @@ def init_db(): status TEXT DEFAULT "pending", timestamp TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)''') + + c.execute('''CREATE TABLE IF NOT EXISTS admins + (user_id INTEGER PRIMARY KEY NOT NULL, + added_by INTEGER, + added_at DATETIME DEFAULT CURRENT_TIMESTAMP)''') + + c.execute("SELECT COUNT(*) FROM admins") + if c.fetchone()[0] == 0 and ADMIN_IDS_FROM_ENV: + logger.info("Admin table is empty. Seeding with admins from .env file...") + for admin_id in ADMIN_IDS_FROM_ENV: + c.execute("INSERT OR IGNORE INTO admins (user_id, added_by) VALUES (?, ?)", (admin_id, OWNER_ID or 0)) + logger.info(f"Seeded {len(ADMIN_IDS_FROM_ENV)} admin(s) into the database.") + conn.commit() logger.info("Database initialized successfully") except sqlite3.Error as e: logger.error(f"Database initialization error: {e}") sys.exit(1) - except Exception as e: - logger.error(f"Unexpected database error: {e}") - sys.exit(1) finally: if conn: conn.close() def get_db_connection(): - """Get database connection with error handling""" try: conn = sqlite3.connect(DB_PATH) return conn @@ -100,35 +109,211 @@ def get_db_connection(): logger.error(f"Database connection error: {e}") return None +# --- Helper function for admin access --- +def is_admin_or_owner(user_id: int) -> bool: + if OWNER_ID and user_id == OWNER_ID: + return True + + conn = get_db_connection() + if not conn: + return False + try: + c = conn.cursor() + c.execute("SELECT 1 FROM admins WHERE user_id = ?", (user_id,)) + return c.fetchone() is not None + except sqlite3.Error as e: + logger.error(f"DB error in is_admin_or_owner check: {e}") + return False + finally: + if conn: + conn.close() + +# --- States for Conversation Handler --- +SELECTING_TYPE, TYPING_APPEAL = range(2) + +async def catch_all_updates(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Ta funkcja przechwytuje i drukuje KAŻDĄ aktualizację, którą otrzymuje bot.""" + print("\n" + "="*50) + print(f"--- DEBUG: Otrzymano aktualizację ---") + print(f"--- Typ: {type(update).__name__} ---") + if update.message: + print(f"--- Wiadomość od: {update.message.from_user.id} ---") + print(f"--- Tekst: {update.message.text} ---") + elif update.callback_query: + print(f"--- Zapytanie od: {update.callback_query.from_user.id} ---") + print(f"--- Dane: {update.callback_query.data} ---") + print("="*50 + "\n") + +# --- User Commands --- +async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): + try: + await update.message.reply_text( + "📝 Welcome to the Appeals Bot!\n\n" + "Use /appeal to submit a FedBan appeal or request Fed Admin status" + ) + logger.info(f"User {update.effective_user.id} started the bot") + except TelegramError as e: + logger.error(f"Error in start command: {e}") + +async def appeal(update: Update, context: ContextTypes.DEFAULT_TYPE): + try: + keyboard = [ + [InlineKeyboardButton("🔓 Fed Unban Appeal", callback_data="unban")], + [InlineKeyboardButton("👑 Fed Admin Request", callback_data="admin")] + ] + await update.message.reply_text( + "Select appeal type:", + reply_markup=InlineKeyboardMarkup(keyboard) + ) + logger.info(f"User {update.effective_user.id} initiated appeal process") + return SELECTING_TYPE + except TelegramError as e: + logger.error(f"Error in appeal command: {e}") + await update.message.reply_text("❌ An error occurred. Please try again later.") + return ConversationHandler.END + +async def handle_appeal_type(update: Update, context: ContextTypes.DEFAULT_TYPE): + query = update.callback_query + await query.answer() + user = query.from_user + appeal_type_choice = query.data + + if appeal_type_choice not in ['unban', 'admin']: + await query.edit_message_text("❌ Invalid appeal type") + return ConversationHandler.END + + context.user_data['appeal_type'] = appeal_type_choice + appeal_type_text = "unban" if appeal_type_choice == "unban" else "admin request" + + template = "" + if appeal_type_choice == "unban": + template = ( + "\n\n📝 Please write your appeal in detail. Example:\n" + "1. Why were you banned?\n" + "2. What have you learned from this experience?\n" + "3. Why should we unban you?\n" + "4. Any additional information?" + ) + else: + template = ( + "\n\n📝 Please write your admin request. Example:\n" + "1. Why do you want to be an admin?\n" + "2. What experience do you have?\n" + "3. How will you help the community?\n" + "4. Any additional information?" + ) + + await query.edit_message_text( + f"✍️ Please write and submit your {appeal_type_text} appeal.{template}\n\n" + "Type your appeal now:" + ) + + logger.info(f"User {user.id} selected {appeal_type_choice} appeal type") + return TYPING_APPEAL + +async def handle_appeal_text(update: Update, context: ContextTypes.DEFAULT_TYPE): + user = update.message.from_user + appeal_text = update.message.text + appeal_type = context.user_data.get('appeal_type') + + if not appeal_type: + return ConversationHandler.END + + conn = get_db_connection() + if not conn: + await update.message.reply_text("❌ Database error. Please try again later.") + return ConversationHandler.END + + try: + c = conn.cursor() + c.execute('''INSERT INTO appeals + (user_id, username, appeal_type, appeal_text, timestamp) + VALUES (?, ?, ?, ?, ?)''', + (user.id, user.username or 'No username', appeal_type, appeal_text, + datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) + conn.commit() + appeal_id = c.lastrowid + + await update.message.reply_text( + f"✅ {appeal_type.capitalize()} appeal submitted successfully!\n" + f"Appeal ID: #{appeal_id}\n\n" + "Your appeal will be reviewed by an admin." + ) + + notification_message = ( + f"🚨 New Appeal #{appeal_id}\n" + f"User: @{user.username or 'No username'} (ID: {user.id})\n" + f"Type: {appeal_type.capitalize()}\n" + f"Time: {datetime.now().strftime('%H:%M %d-%m-%Y')}\n\n" + f"📝 Appeal Text:\n{appeal_text}\n\n" + f"Use /approve {appeal_id} to approve\n" + f"Use /reject {appeal_id} to reject\n\n" + f"Use /pending to view all pending appeals" + ) + + c.execute("SELECT user_id FROM admins") + db_admin_ids = {row[0] for row in c.fetchall()} + if OWNER_ID: + db_admin_ids.add(OWNER_ID) + + successful_notifications = 0 + for recipient_id in db_admin_ids: + try: + await context.bot.send_message(recipient_id, notification_message) + successful_notifications += 1 + except TelegramError as e: + logger.error(f"Failed to notify {recipient_id}: {e}") + + logger.info(f"Appeal #{appeal_id} submitted. Notified {successful_notifications}/{len(db_admin_ids)} admins.") + + except sqlite3.Error as e: + logger.error(f"Database error in handle_appeal_text: {e}") + await update.message.reply_text("❌ Database error. Please try again later.") + finally: + conn.close() + + context.user_data.clear() + return ConversationHandler.END + +async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE): + await update.message.reply_text("🚫 Appeal submission cancelled.") + context.user_data.clear() + return ConversationHandler.END + # --- Admin Management Commands --- -def add_admin(update: Update, context: CallbackContext): - """Add a new admin (owner only)""" +async def add_admin(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not OWNER_ID or update.effective_user.id != OWNER_ID: + await update.message.reply_text("❌ Access denied. Only the bot owner can use this command.") + return + if not context.args: + await update.message.reply_text("❌ Usage: /addadmin ") + return try: - if not OWNER_ID or update.effective_user.id != OWNER_ID: - update.message.reply_text("❌ Access denied. Only the bot owner can use this command.") - return - - if not context.args: - update.message.reply_text("❌ Usage: /addadmin ") - return - - try: - new_admin_id = int(context.args[0]) - except ValueError: - update.message.reply_text("❌ Invalid user ID. Please provide a valid number.") - return - - if new_admin_id in ADMIN_IDS: - update.message.reply_text(f"❌ User {new_admin_id} is already an admin.") + new_admin_id = int(context.args[0]) + except ValueError: + await update.message.reply_text("❌ Invalid user ID. Please provide a valid number.") + return + + conn = get_db_connection() + if not conn: + await update.message.reply_text("❌ Database error. Please try again later.") + return + + try: + c = conn.cursor() + c.execute("SELECT 1 FROM admins WHERE user_id = ?", (new_admin_id,)) + if c.fetchone(): + await update.message.reply_text(f"❌ User {new_admin_id} is already an admin.") return - - ADMIN_IDS.append(new_admin_id) - update.message.reply_text(f"✅ User {new_admin_id} has been added as an admin.") - logger.info(f"Owner {update.effective_user.id} added {new_admin_id} as admin") + + c.execute("INSERT INTO admins (user_id, added_by) VALUES (?, ?)", (new_admin_id, update.effective_user.id)) + conn.commit() + + await update.message.reply_text(f"✅ User {new_admin_id} has been added as an admin.") + logger.info(f"Owner {update.effective_user.id} added {new_admin_id} as admin to the database") - # Notify the new admin try: - context.bot.send_message( + await context.bot.send_message( new_admin_id, "🎉 You have been granted admin access to the Appeals Bot!\n\n" "Available admin commands:\n" @@ -141,721 +326,429 @@ def add_admin(update: Update, context: CallbackContext): ) except TelegramError as e: logger.error(f"Failed to notify new admin {new_admin_id}: {e}") - update.message.reply_text(f"Admin added but failed to notify them.") - - except TelegramError as e: - logger.error(f"Telegram error in add_admin: {e}") - except Exception as e: - logger.error(f"Unexpected error in add_admin: {e}") + await update.message.reply_text(f"Admin added but failed to notify them.") + + except sqlite3.Error as e: + logger.error(f"Database error in add_admin: {e}") + await update.message.reply_text("❌ Database error while adding admin.") + finally: + if conn: + conn.close() -def remove_admin(update: Update, context: CallbackContext): - """Remove an admin (owner only)""" +async def remove_admin(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not OWNER_ID or update.effective_user.id != OWNER_ID: + await update.message.reply_text("❌ Access denied. Only the bot owner can use this command.") + return + if not context.args: + await update.message.reply_text("❌ Usage: /removeadmin ") + return try: - if not OWNER_ID or update.effective_user.id != OWNER_ID: - update.message.reply_text("❌ Access denied. Only the bot owner can use this command.") - return - - if not context.args: - update.message.reply_text("❌ Usage: /removeadmin ") - return - - try: - admin_id = int(context.args[0]) - except ValueError: - update.message.reply_text("❌ Invalid user ID. Please provide a valid number.") - return - - if admin_id not in ADMIN_IDS: - update.message.reply_text(f"❌ User {admin_id} is not an admin.") + admin_id = int(context.args[0]) + except ValueError: + await update.message.reply_text("❌ Invalid user ID. Please provide a valid number.") + return + + conn = get_db_connection() + if not conn: + await update.message.reply_text("❌ Database error. Please try again later.") + return + + try: + c = conn.cursor() + c.execute("SELECT 1 FROM admins WHERE user_id = ?", (admin_id,)) + if not c.fetchone(): + await update.message.reply_text(f"❌ User {admin_id} is not an admin.") return - - ADMIN_IDS.remove(admin_id) - update.message.reply_text(f"✅ User {admin_id} has been removed from admin list.") + + c.execute("DELETE FROM admins WHERE user_id = ?", (admin_id,)) + conn.commit() + + await update.message.reply_text(f"✅ User {admin_id} has been removed from admin list.") logger.info(f"Owner {update.effective_user.id} removed {admin_id} from admin list") - # Notify the removed admin try: - context.bot.send_message( + await context.bot.send_message( admin_id, "🚫 Your admin access to the Appeals Bot has been revoked." ) except TelegramError as e: logger.error(f"Failed to notify removed admin {admin_id}: {e}") - except TelegramError as e: - logger.error(f"Telegram error in remove_admin: {e}") - except Exception as e: - logger.error(f"Unexpected error in remove_admin: {e}") + except sqlite3.Error as e: + logger.error(f"Database error in remove_admin: {e}") + await update.message.reply_text("❌ Database error while removing admin.") + finally: + if conn: + conn.close() + +async def list_admins(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not is_admin_or_owner(update.effective_user.id): + await update.message.reply_text("❌ Access denied.") + return + + conn = get_db_connection() + if not conn: + await update.message.reply_text("❌ Database error. Please try again later.") + return -def list_admins(update: Update, context: CallbackContext): - """List all admins (admin/owner only)""" try: - if not is_admin_or_owner(update.effective_user.id): - update.message.reply_text("❌ Access denied.") - return - - admin_list = "\n".join([f"• {admin_id}" for admin_id in ADMIN_IDS]) + c = conn.cursor() + c.execute("SELECT user_id FROM admins ORDER BY user_id") + db_admins = [row[0] for row in c.fetchall()] + + admin_list = "\n".join([f"• {admin_id}" for admin_id in db_admins]) if db_admins else "No admins found." owner_info = f"\n👑 Owner: {OWNER_ID}" if OWNER_ID else "" + total_authorized = len(set(db_admins + ([OWNER_ID] if OWNER_ID else []))) + response = ( - f"👥 Admin List ({len(ADMIN_IDS)} admins)\n\n" + f"👥 Admin List ({len(db_admins)} admins)\n\n" f"{admin_list}" f"{owner_info}\n\n" - f"Total authorized users: {len(ADMIN_IDS) + (1 if OWNER_ID else 0)}" + f"Total authorized users: {total_authorized}" ) - update.message.reply_text(response, parse_mode=ParseMode.HTML) - logger.info(f"Admin list viewed by {update.effective_user.id}") - - except TelegramError as e: - logger.error(f"Telegram error in list_admins: {e}") - except Exception as e: - logger.error(f"Unexpected error in list_admins: {e}") + await update.message.reply_text(response, parse_mode=ParseMode.HTML) + except sqlite3.Error as e: + logger.error(f"Database error in list_admins: {e}") + await update.message.reply_text("❌ Database error. Please try again later.") + finally: + if conn: + conn.close() # --- Shell Command Support --- -def shell_command(update: Update, context: CallbackContext): - """Execute shell commands (owner only)""" - try: - # Check if OWNER_ID is set and user is owner - if not OWNER_ID or update.effective_user.id != OWNER_ID: - update.message.reply_text("❌ Access denied. Only the bot owner can use this command.") - return - - if not context.args: - update.message.reply_text( - "❌ Usage: /shell \n" - "Example: /shell ls -la\n" - "⚠️ Use with caution - this executes system commands!" - ) - return - - command = ' '.join(context.args) +async def shell_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not OWNER_ID or update.effective_user.id != OWNER_ID: + await update.message.reply_text("❌ Access denied. Only the bot owner can use this command.") + return - # Log the command execution - logger.info(f"Owner {update.effective_user.id} executing shell command: {command}") + if not context.args: + await update.message.reply_text( + "❌ Usage: /shell \n" + "Example: /shell ls -la\n" + "⚠️ Use with caution - this executes system commands!" + ) + return - # Show "typing" status - context.bot.send_chat_action(chat_id=update.effective_chat.id, action='typing') + command = ' '.join(context.args) + logger.info(f"Owner {update.effective_user.id} executing shell command: {command}") + + try: + proc = await asyncio.create_subprocess_shell( + command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=30) - try: - # Execute command with timeout - result = subprocess.run( - command, - shell=True, - capture_output=True, - text=True, - timeout=30, # 30 second timeout - cwd=os.getcwd() + output = "" + if stdout and stdout.decode(): + output += f"📤 STDOUT:\n
{html.escape(stdout.decode().strip())}
\n" + if stderr and stderr.decode(): + output += f"🚨 STDERR:\n
{html.escape(stderr.decode().strip())}
\n" + + output += f"\n📊 Return code: {proc.returncode}" + output += f"\n🕐 Command: {html.escape(command)}" + + if len(output) > 4000: + output_file = io.BytesIO( + f"Command: {command}\n" + f"Return code: {proc.returncode}\n\n" + f"STDOUT:\n{stdout.decode()}\n\n" + f"STDERR:\n{stderr.decode()}".encode() ) + output_file.name = f"shell_output_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" - # Prepare output - output = "" - if result.stdout: - output += f"📤 STDOUT:\n
{html.escape(result.stdout)}
\n" - if result.stderr: - output += f"🚨 STDERR:\n
{html.escape(result.stderr)}
\n" - - output += f"\n📊 Return code: {result.returncode}" - output += f"\n🕐 Command: {html.escape(command)}" - - # Handle long outputs - if len(output) > 4000: - # Send as file if output is too long - output_file = io.BytesIO( - f"Command: {command}\n" - f"Return code: {result.returncode}\n" - f"STDOUT:\n{result.stdout}\n" - f"STDERR:\n{result.stderr}".encode() - ) - output_file.name = f"shell_output_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" - - update.message.reply_document( - document=output_file, - caption=f"📁 Output too long, sent as file\n" - f"Command: {html.escape(command)}\n" - f"Return code: {result.returncode}", - parse_mode=ParseMode.HTML - ) - else: - update.message.reply_text(output, parse_mode=ParseMode.HTML) - - except subprocess.TimeoutExpired: - update.message.reply_text( - f"⏰ Command timed out after 30 seconds\n" - f"Command: {html.escape(command)}", - parse_mode=ParseMode.HTML - ) - except subprocess.SubprocessError as e: - update.message.reply_text( - f"❌ Subprocess error:\n
{html.escape(str(e))}
\n" - f"Command: {html.escape(command)}", - parse_mode=ParseMode.HTML - ) - except Exception as e: - update.message.reply_text( - f"❌ Execution error:\n
{html.escape(str(e))}
\n" - f"Command: {html.escape(command)}", + await update.message.reply_document( + document=output_file, + caption=f"📁 Output too long, sent as file\n" + f"Command: {html.escape(command)}\n" + f"Return code: {proc.returncode}", parse_mode=ParseMode.HTML ) + else: + await update.message.reply_text(output or f"Command executed with return code {proc.returncode} and no output.", parse_mode=ParseMode.HTML) - except TelegramError as e: - logger.error(f"Telegram error in shell command: {e}") - try: - update.message.reply_text("❌ Failed to send command output due to Telegram error.") - except: - pass - except Exception as e: - logger.error(f"Unexpected error in shell command: {e}") - try: - update.message.reply_text(f"❌ Unexpected error: {str(e)}") - except: - pass - -# --- User Commands --- -def start(update: Update, context: CallbackContext): - """Start command handler""" - try: - update.message.reply_text( - "📝 Welcome to the Appeals Bot!\n\n" - "Use /appeal to submit a FedBan appeal or request Fed Admin status" + except asyncio.TimeoutError: + await update.message.reply_text( + f"⏰ Command timed out after 30 seconds\n" + f"Command: {html.escape(command)}", + parse_mode=ParseMode.HTML ) - logger.info(f"User {update.effective_user.id} started the bot") - except TelegramError as e: - logger.error(f"Error in start command: {e}") except Exception as e: - logger.error(f"Unexpected error in start command: {e}") - -def appeal(update: Update, context: CallbackContext): - """Appeal command handler""" - try: - keyboard = [ - [InlineKeyboardButton("🔓 Fed Unban Appeal", callback_data="unban")], - [InlineKeyboardButton("👑 Fed Admin Request", callback_data="admin")] - ] - update.message.reply_text( - "Select appeal type:", - reply_markup=InlineKeyboardMarkup(keyboard) + await update.message.reply_text( + f"❌ Execution error:\n
{html.escape(str(e))}
\n" + f"Command: {html.escape(command)}", + parse_mode=ParseMode.HTML ) - logger.info(f"User {update.effective_user.id} requested appeal menu") - except TelegramError as e: - logger.error(f"Error in appeal command: {e}") - update.message.reply_text("❌ An error occurred. Please try again later.") - except Exception as e: - logger.error(f"Unexpected error in appeal command: {e}") -# Store temporary data for users writing appeals -user_appeals = {} - -def handle_appeal_type(update: Update, context: CallbackContext): - """Handle appeal type selection""" +# --- Admin Commands --- +async def pending(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not is_admin_or_owner(update.effective_user.id): + await update.message.reply_text("❌ Access denied.") + return + + conn = get_db_connection() + if not conn: + await update.message.reply_text("❌ Database error. Please try again later.") + return try: - query = update.callback_query - query.answer() - user = query.from_user + c = conn.cursor() + c.execute("SELECT * FROM appeals WHERE status='pending' ORDER BY id DESC") + appeals = c.fetchall() - # Validate appeal type - if query.data not in ['unban', 'admin']: - query.edit_message_text("❌ Invalid appeal type") + if not appeals: + await update.message.reply_text("📋 No pending appeals!") return - # Store appeal type temporarily - user_appeals[user.id] = {'type': query.data} - - # Ask for appeal text with template - appeal_type = "unban" if query.data == "unban" else "admin request" - template = "" - - if query.data == "unban": - template = ( - "\n\n📝 Please write your appeal in detail. Example:\n" - "1. Why were you banned?\n" - "2. What have you learned from this experience?\n" - "3. Why should we unban you?\n" - "4. Any additional information?" - ) - else: - template = ( - "\n\n📝 Please write your admin request. Example:\n" - "1. Why do you want to be an admin?\n" - "2. What experience do you have?\n" - "3. How will you help the community?\n" - "4. Any additional information?" + response = "📋 Pending Appeals:\n\n" + for appeal in appeals: + response += ( + f"ID: #{appeal[0]}\n" + f"User: @{appeal[2]} (ID: {appeal[1]})\n" + f"Type: {appeal[3].capitalize()}\n" + f"Time: {appeal[6]}\n" + f"Status: {appeal[5]}\n" + f"Text: {appeal[4][:100]}...\n" + "───────────────\n" ) - - query.edit_message_text( - f"✍️ Please write and submit your {appeal_type} appeal.{template}\n\n" - "Type your appeal now:" - ) - - # Set next step to handle appeal text - context.user_data['expecting_appeal_text'] = True - context.user_data['appeal_type'] = query.data - - logger.info(f"User {user.id} selected {query.data} appeal type") - - except TelegramError as e: - logger.error(f"Telegram error in handle_appeal_type: {e}") - except Exception as e: - logger.error(f"Unexpected error in handle_appeal_type: {e}") - -def handle_appeal_text(update: Update, context: CallbackContext): - """Handle user's appeal text submission""" - try: - if not context.user_data.get('expecting_appeal_text'): - return - - user = update.message.from_user - appeal_text = update.message.text - appeal_type = context.user_data['appeal_type'] - # Save to database - conn = get_db_connection() - if not conn: - update.message.reply_text("❌ Database error. Please try again later.") - return - - try: - c = conn.cursor() - c.execute('''INSERT INTO appeals - (user_id, username, appeal_type, appeal_text, timestamp) - VALUES (?, ?, ?, ?, ?)''', - (user.id, user.username or 'No username', appeal_type, appeal_text, - datetime.now().strftime("%Y-%m-%d %H:%M:%S"))) - conn.commit() - appeal_id = c.lastrowid - - update.message.reply_text( - f"✅ {appeal_type.capitalize()} appeal submitted successfully!\n" - f"Appeal ID: #{appeal_id}\n\n" - "Your appeal will be reviewed by an admin." - ) - - # Prepare notification message - notification_message = ( - f"🚨 New Appeal #{appeal_id}\n" - f"User: @{user.username or 'No username'} (ID: {user.id})\n" - f"Type: {appeal_type.capitalize()}\n" - f"Time: {datetime.now().strftime('%H:%M %d-%m-%Y')}\n\n" - f"📝 Appeal Text:\n{appeal_text}\n\n" - f"Use /approve {appeal_id} to approve\n" - f"Use /reject {appeal_id} to reject\n\n" - f"Use /pending to view all pending appeals" - ) - - # Notify all admins and owner - notification_recipients = [] - notification_recipients.extend(ADMIN_IDS) # Add all admin IDs - if OWNER_ID and OWNER_ID not in ADMIN_IDS: # Add owner if not already in admin list - notification_recipients.append(OWNER_ID) - - # Remove duplicates - notification_recipients = list(set(notification_recipients)) - - successful_notifications = 0 - for recipient_id in notification_recipients: - try: - context.bot.send_message(recipient_id, notification_message) - logger.info(f"Notified {recipient_id} about appeal #{appeal_id}") - successful_notifications += 1 - except TelegramError as e: - logger.error(f"Failed to notify {recipient_id}: {e}") - - logger.info(f"Appeal #{appeal_id} submitted by user {user.id}. Notified {successful_notifications}/{len(notification_recipients)} admins.") - - # Clean up user data - del context.user_data['expecting_appeal_text'] - del context.user_data['appeal_type'] - if user.id in user_appeals: - del user_appeals[user.id] - - except sqlite3.Error as e: - logger.error(f"Database error in handle_appeal_text: {e}") - update.message.reply_text("❌ Database error. Please try again later.") - finally: - conn.close() - - except TelegramError as e: - logger.error(f"Telegram error in handle_appeal_text: {e}") - except Exception as e: - logger.error(f"Unexpected error in handle_appeal_text: {e}") - -# --- Helper function for admin access --- -def is_admin_or_owner(user_id): - """Check if user is admin or owner""" - return user_id in ADMIN_IDS or (OWNER_ID and user_id == OWNER_ID) + 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) + except sqlite3.Error as e: + logger.error(f"Database error in pending: {e}") + finally: + conn.close() -# --- Admin Commands --- -def pending(update: Update, context: CallbackContext): - """Show pending appeals (admin/owner only)""" +async def view_appeal(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not is_admin_or_owner(update.effective_user.id): + await update.message.reply_text("❌ Access denied.") + return + if not context.args: + await update.message.reply_text("❌ Usage: /view ") + return try: - if not is_admin_or_owner(update.effective_user.id): - update.message.reply_text("❌ Access denied.") - return - - conn = get_db_connection() - if not conn: - update.message.reply_text("❌ Database error. Please try again later.") - return - - try: - c = conn.cursor() - c.execute("SELECT * FROM appeals WHERE status='pending' ORDER BY id DESC") - appeals = c.fetchall() - - if not appeals: - update.message.reply_text("📋 No pending appeals!") - return - - response = "📋 Pending Appeals:\n\n" - for appeal in appeals: - response += ( - f"ID: #{appeal[0]}\n" - f"User: @{appeal[2]} (ID: {appeal[1]})\n" - f"Type: {appeal[3].capitalize()}\n" - f"Time: {appeal[6]}\n" - f"Status: {appeal[5]}\n" - f"Text: {appeal[4][:100]}...\n" - "───────────────\n" - ) - - # Split long messages if needed - if len(response) > 4096: - for i in range(0, len(response), 4096): - update.message.reply_text(response[i:i+4096]) - else: - update.message.reply_text(response) - - logger.info(f"Admin {update.effective_user.id} viewed pending appeals") - - except sqlite3.Error as e: - logger.error(f"Database error in pending: {e}") - update.message.reply_text("❌ Database error. Please try again later.") - finally: - conn.close() - - except TelegramError as e: - logger.error(f"Telegram error in pending: {e}") - except Exception as e: - logger.error(f"Unexpected error in pending: {e}") + appeal_id = int(context.args[0]) + except ValueError: + await update.message.reply_text("❌ Invalid appeal ID. Please provide a number.") + return -def view_appeal(update: Update, context: CallbackContext): - """View full appeal details (admin/owner only)""" + conn = get_db_connection() + if not conn: + await update.message.reply_text("❌ Database error. Please try again later.") + return + try: - if not is_admin_or_owner(update.effective_user.id): - update.message.reply_text("❌ Access denied.") - return - - if not context.args: - update.message.reply_text("❌ Usage: /view ") - return - - try: - appeal_id = int(context.args[0]) - except ValueError: - update.message.reply_text("❌ Invalid appeal ID. Please provide a number.") - return - - conn = get_db_connection() - if not conn: - update.message.reply_text("❌ Database error. Please try again later.") + c = conn.cursor() + c.execute("SELECT * FROM appeals WHERE id=?", (appeal_id,)) + appeal = c.fetchone() + + if not appeal: + await update.message.reply_text(f"❌ Appeal #{appeal_id} not found.") return - try: - c = conn.cursor() - c.execute("SELECT * FROM appeals WHERE id=?", (appeal_id,)) - appeal = c.fetchone() - - if not appeal: - update.message.reply_text(f"❌ Appeal #{appeal_id} not found.") - return - - response = ( - f"📄 Appeal Details #{appeal[0]}\n" - f"User: @{appeal[2]} (ID: {appeal[1]})\n" - f"Type: {appeal[3].capitalize()}\n" - f"Status: {appeal[5]}\n" - f"Time: {appeal[6]}\n\n" - f"📝 Appeal Text:\n{appeal[4]}\n\n" - f"Use /approve {appeal[0]} to approve\n" - f"Use /reject {appeal[0]} to reject" - ) - - update.message.reply_text(response) - logger.info(f"Admin {update.effective_user.id} viewed appeal #{appeal_id}") - - except sqlite3.Error as e: - logger.error(f"Database error in view_appeal: {e}") - update.message.reply_text("❌ Database error. Please try again later.") - finally: - conn.close() - - except TelegramError as e: - logger.error(f"Telegram error in view_appeal: {e}") - except Exception as e: - logger.error(f"Unexpected error in view_appeal: {e}") + response = ( + f"📄 Appeal Details #{appeal[0]}\n" + f"User: @{appeal[2]} (ID: {appeal[1]})\n" + f"Type: {appeal[3].capitalize()}\n" + f"Status: {appeal[5]}\n" + f"Time: {appeal[6]}\n\n" + f"📝 Appeal Text:\n{appeal[4]}\n\n" + f"Use /approve {appeal[0]} to approve\n" + f"Use /reject {appeal[0]} to reject" + ) + + await update.message.reply_text(response) + except sqlite3.Error as e: + logger.error(f"Database error in view_appeal: {e}") + finally: + conn.close() -def approve(update: Update, context: CallbackContext): - """Approve appeal (admin/owner only)""" +async def process_appeal(update: Update, context: ContextTypes.DEFAULT_TYPE, new_status: str): + if not is_admin_or_owner(update.effective_user.id): + await update.message.reply_text("❌ Access denied.") + return + + command = "approve" if new_status == "approved" else "reject" + if not context.args: + await update.message.reply_text(f"❌ Usage: /{command} ") + return try: - if not is_admin_or_owner(update.effective_user.id): - update.message.reply_text("❌ Access denied.") - return - - if not context.args: - update.message.reply_text("❌ Usage: /approve ") - return - - try: - appeal_id = int(context.args[0]) - except ValueError: - update.message.reply_text("❌ Invalid appeal ID. Please provide a number.") - return - - conn = get_db_connection() - if not conn: - update.message.reply_text("❌ Database error. Please try again later.") - return - - try: - c = conn.cursor() - - # Check if appeal exists - c.execute("SELECT user_id, appeal_type, appeal_text FROM appeals WHERE id=? AND status='pending'", (appeal_id,)) - result = c.fetchone() - - if not result: - update.message.reply_text(f"❌ Appeal #{appeal_id} not found or already processed.") - return - - user_id, appeal_type, appeal_text = result - - # Update database - c.execute("UPDATE appeals SET status='approved' WHERE id=?", (appeal_id,)) - conn.commit() - - update.message.reply_text(f"✅ Appeal #{appeal_id} approved successfully!") - - # Notify user - try: - context.bot.send_message( - user_id, - f"🎉 Your {appeal_type} appeal has been approved!\n" - f"Appeal ID: #{appeal_id}\n\n" - f"Your appeal text:\n{appeal_text}" - ) - logger.info(f"User {user_id} notified about approved appeal #{appeal_id}") - except TelegramError as e: - logger.error(f"Failed to notify user {user_id}: {e}") - update.message.reply_text(f"Appeal approved but failed to notify user.") - - logger.info(f"Appeal #{appeal_id} approved by admin {update.effective_user.id}") - - except sqlite3.Error as e: - logger.error(f"Database error in approve: {e}") - update.message.reply_text("❌ Database error. Please try again later.") - finally: - conn.close() - - except TelegramError as e: - logger.error(f"Telegram error in approve: {e}") - except Exception as e: - logger.error(f"Unexpected error in approve: {e}") + appeal_id = int(context.args[0]) + except ValueError: + await update.message.reply_text("❌ Invalid appeal ID. Please provide a number.") + return + + conn = get_db_connection() + if not conn: + await update.message.reply_text("❌ Database error. Please try again later.") + return -def reject(update: Update, context: CallbackContext): - """Reject appeal (admin/owner only)""" try: - if not is_admin_or_owner(update.effective_user.id): - update.message.reply_text("❌ Access denied.") - return - - if not context.args: - update.message.reply_text("❌ Usage: /reject ") - return - - try: - appeal_id = int(context.args[0]) - except ValueError: - update.message.reply_text("❌ Invalid appeal ID. Please provide a number.") - return - - conn = get_db_connection() - if not conn: - update.message.reply_text("❌ Database error. Please try again later.") + c = conn.cursor() + c.execute("SELECT user_id, appeal_type, appeal_text FROM appeals WHERE id=? AND status='pending'", (appeal_id,)) + result = c.fetchone() + + if not result: + await update.message.reply_text(f"❌ Appeal #{appeal_id} not found or already processed.") return + user_id, appeal_type, appeal_text = result + c.execute("UPDATE appeals SET status=? WHERE id=?", (new_status, appeal_id)) + conn.commit() + + if new_status == "approved": + await update.message.reply_text(f"✅ Appeal #{appeal_id} approved successfully!") + notification_text = f"🎉 Your {appeal_type} appeal has been approved!\nAppeal ID: #{appeal_id}\n\nYour appeal text:\n{appeal_text}" + else: # rejected + await update.message.reply_text(f"❌ Appeal #{appeal_id} rejected.") + notification_text = f"❌ Your {appeal_type} appeal has been rejected.\nAppeal ID: #{appeal_id}\n\nYour appeal text:\n{appeal_text}\n\nYou may submit a new appeal if you wish." + try: - c = conn.cursor() - - # Check if appeal exists - c.execute("SELECT user_id, appeal_type, appeal_text FROM appeals WHERE id=? AND status='pending'", (appeal_id,)) - result = c.fetchone() - - if not result: - update.message.reply_text(f"❌ Appeal #{appeal_id} not found or already processed.") - return - - user_id, appeal_type, appeal_text = result - - # Update database - c.execute("UPDATE appeals SET status='rejected' WHERE id=?", (appeal_id,)) - conn.commit() - - update.message.reply_text(f"❌ Appeal #{appeal_id} rejected.") - - # Notify user - try: - context.bot.send_message( - user_id, - f"❌ Your {appeal_type} appeal has been rejected.\n" - f"Appeal ID: #{appeal_id}\n\n" - f"Your appeal text:\n{appeal_text}\n\n" - "You may submit a new appeal if you wish." - ) - logger.info(f"User {user_id} notified about rejected appeal #{appeal_id}") - except TelegramError as e: - logger.error(f"Failed to notify user {user_id}: {e}") - update.message.reply_text(f"Appeal rejected but failed to notify user.") - - logger.info(f"Appeal #{appeal_id} rejected by admin {update.effective_user.id}") - - except sqlite3.Error as e: - logger.error(f"Database error in reject: {e}") - update.message.reply_text("❌ Database error. Please try again later.") - finally: - conn.close() + await context.bot.send_message(user_id, notification_text) + logger.info(f"User {user_id} notified about {new_status} appeal #{appeal_id}") + except TelegramError as e: + logger.error(f"Failed to notify user {user_id}: {e}") + await update.message.reply_text(f"Appeal {new_status} but failed to notify user.") - except TelegramError as e: - logger.error(f"Telegram error in reject: {e}") - except Exception as e: - logger.error(f"Unexpected error in reject: {e}") + logger.info(f"Appeal #{appeal_id} {new_status} by admin {update.effective_user.id}") + + except sqlite3.Error as e: + logger.error(f"Database error in {command}: {e}") + finally: + conn.close() -def stats(update: Update, context: CallbackContext): - """Show appeal statistics (admin/owner only)""" - try: - if not is_admin_or_owner(update.effective_user.id): - update.message.reply_text("❌ Access denied.") - return - - conn = get_db_connection() - if not conn: - update.message.reply_text("❌ Database error. Please try again later.") - return - - try: - c = conn.cursor() - - # Get basic stats - c.execute("SELECT COUNT(*) FROM appeals") - total = c.fetchone()[0] - - c.execute("SELECT COUNT(*) FROM appeals WHERE status='pending'") - pending = c.fetchone()[0] - - c.execute("SELECT COUNT(*) FROM appeals WHERE status='approved'") - approved = c.fetchone()[0] - - c.execute("SELECT COUNT(*) FROM appeals WHERE status='rejected'") - rejected = c.fetchone()[0] - - # Get appeal type distribution - c.execute("SELECT appeal_type, COUNT(*) FROM appeals GROUP BY appeal_type") - type_stats = "\n".join([f"• {row[0].capitalize()}: {row[1]}" for row in c.fetchall()]) - - # Get recent activity - c.execute("SELECT COUNT(*) FROM appeals WHERE created_at >= datetime('now', '-1 day')") - last_24h = c.fetchone()[0] - - c.execute("SELECT COUNT(*) FROM appeals WHERE created_at >= datetime('now', '-7 days')") - last_7d = c.fetchone()[0] - - response = ( - "📊 Appeal Statistics\n\n" - f"Total Appeals: {total}\n" - f"Pending: {pending}\n" - f"Approved: {approved}\n" - f"Rejected: {rejected}\n\n" - f"Recent Activity:\n" - f"• Last 24h: {last_24h}\n" - f"• Last 7 days: {last_7d}\n\n" - f"By Appeal Type:\n" - f"{type_stats}\n\n" - f"System Info:\n" - f"• Active Admins: {len(ADMIN_IDS)}\n" - f"• Owner ID: {OWNER_ID if OWNER_ID else 'Not set'}\n\n" - "Use /pending to view pending appeals" - ) - - update.message.reply_text(response, parse_mode=ParseMode.HTML) - logger.info(f"Admin {update.effective_user.id} viewed statistics") - - except sqlite3.Error as e: - logger.error(f"Database error in stats: {e}") - update.message.reply_text("❌ Database error. Please try again later.") - finally: - conn.close() - - except TelegramError as e: - logger.error(f"Telegram error in stats: {e}") - except Exception as e: - logger.error(f"Unexpected error in stats: {e}") +async def approve(update: Update, context: ContextTypes.DEFAULT_TYPE): + await process_appeal(update, context, "approved") -def error_handler(update: Update, context: CallbackContext): - """Global error handler""" - logger.error(f"Update {update} caused error {context.error}") +async def reject(update: Update, context: ContextTypes.DEFAULT_TYPE): + await process_appeal(update, context, "rejected") -# --- Bot Setup --- -def main(): - """Main function to run the bot""" - try: - # Initialize database - init_db() +async def stats(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not is_admin_or_owner(update.effective_user.id): + await update.message.reply_text("❌ Access denied.") + return - # Create updater - updater = Updater(BOT_TOKEN, use_context=True) - dp = updater.dispatcher + conn = get_db_connection() + if not conn: + await update.message.reply_text("❌ Database error. Please try again later.") + return - # User commands - dp.add_handler(CommandHandler("start", start)) - dp.add_handler(CommandHandler("appeal", appeal)) - - # Admin commands - dp.add_handler(CommandHandler("pending", pending)) - dp.add_handler(CommandHandler("view", view_appeal)) - dp.add_handler(CommandHandler("approve", approve)) - dp.add_handler(CommandHandler("reject", reject)) - dp.add_handler(CommandHandler("stats", stats)) - dp.add_handler(CommandHandler("admins", list_admins)) - - # Owner commands - dp.add_handler(CommandHandler("addadmin", add_admin)) - dp.add_handler(CommandHandler("removeadmin", remove_admin)) - - # System commands - dp.add_handler(CommandHandler("ping", ping_uptime.ping_command)) - dp.add_handler(CommandHandler("status", status_handler)) - - # Shell commands (Owner only) - dp.add_handler(CommandHandler(["shell", "sh"], shell_command)) + try: + c = conn.cursor() - # Callbacks - dp.add_handler(CallbackQueryHandler(handle_appeal_type)) + c.execute("SELECT COUNT(*) FROM appeals") + total = c.fetchone()[0] + c.execute("SELECT COUNT(*) FROM appeals WHERE status='pending'") + pending_count = c.fetchone()[0] + c.execute("SELECT COUNT(*) FROM appeals WHERE status='approved'") + approved = c.fetchone()[0] + c.execute("SELECT COUNT(*) FROM appeals WHERE status='rejected'") + rejected = c.fetchone()[0] + c.execute("SELECT appeal_type, COUNT(*) FROM appeals GROUP BY appeal_type") + type_stats = "\n".join([f"• {row[0].capitalize()}: {row[1]}" for row in c.fetchall()]) + c.execute("SELECT COUNT(*) FROM appeals WHERE created_at >= datetime('now', '-1 day')") + last_24h = c.fetchone()[0] + c.execute("SELECT COUNT(*) FROM appeals WHERE created_at >= datetime('now', '-7 days')") + last_7d = c.fetchone()[0] + c.execute("SELECT COUNT(*) FROM admins") + active_admins = c.fetchone()[0] - # Text handler for appeal submission - dp.add_handler(MessageHandler(Filters.text & ~Filters.command, handle_appeal_text)) + response = ( + "📊 Appeal Statistics\n\n" + f"Total Appeals: {total}\n" + f"Pending: {pending_count}\n" + f"Approved: {approved}\n" + f"Rejected: {rejected}\n\n" + f"Recent Activity:\n" + f"• Last 24h: {last_24h}\n" + f"• Last 7 days: {last_7d}\n\n" + f"By Appeal Type:\n" + f"{type_stats}\n\n" + f"System Info:\n" + f"• Active Admins: {active_admins}\n" + f"• Owner ID: {OWNER_ID if OWNER_ID else 'Not set'}\n\n" + "Use /pending to view pending appeals" + ) - # Error handler - dp.add_error_handler(error_handler) + await update.message.reply_text(response, parse_mode=ParseMode.HTML) + except sqlite3.Error as e: + logger.error(f"Database error in stats: {e}") + finally: + conn.close() + +async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE): + logger.error("Exception while handling an update:", exc_info=context.error) + +async def main() -> None: + """Start the bot.""" + init_db() + + application = Application.builder().token(BOT_TOKEN).build() + + conv_handler = ConversationHandler( + entry_points=[CommandHandler("appeal", appeal)], + states={ + SELECTING_TYPE: [CallbackQueryHandler(handle_appeal_type)], + TYPING_APPEAL: [MessageHandler(filters.TEXT & ~filters.COMMAND, handle_appeal_text)], + }, + fallbacks=[CommandHandler("cancel", cancel)], + per_message=False + ) + application.add_handler(conv_handler) + + # Add other handlers + application.add_handler(MessageHandler(filters.ALL, catch_all_updates), group=1) + application.add_handler(CommandHandler("start", start)) + application.add_handler(CommandHandler("pending", pending)) + application.add_handler(CommandHandler("view", view_appeal)) + application.add_handler(CommandHandler("approve", approve)) + application.add_handler(CommandHandler("reject", reject)) + application.add_handler(CommandHandler("stats", stats)) + application.add_handler(CommandHandler("admins", list_admins)) + application.add_handler(CommandHandler("addadmin", add_admin)) + application.add_handler(CommandHandler("removeadmin", remove_admin)) + application.add_handler(CommandHandler("ping", ping_command)) + application.add_handler(CommandHandler("status", async_status_handler)) + application.add_handler(CommandHandler(["shell", "sh"], shell_command)) + application.add_error_handler(error_handler) + + try: + logger.info("Initializing bot...") + await application.initialize() + logger.info("Starting bot polling...") + await application.updater.start_polling() - logger.info("Bot started successfully") - logger.info(f"Configured with {len(ADMIN_IDS)} admin(s) and owner: {OWNER_ID}") + conn = get_db_connection() + if conn: + c = conn.cursor() + c.execute("SELECT COUNT(*) FROM admins") + num_admins = c.fetchone()[0] + conn.close() + logger.info(f"Loaded with {num_admins} admin(s) from DB and owner: {OWNER_ID}") + else: + logger.error("Could not connect to DB to count admins.") + print("Bot is running...") - updater.start_polling() - updater.idle() - + await asyncio.Event().wait() + + except (KeyboardInterrupt, SystemExit): + logger.info("Bot shutdown requested.") except Exception as e: - logger.error(f"Failed to start bot: {e}") - sys.exit(1) + logger.error(f"An error occurred during bot runtime: {e}", exc_info=True) + finally: + logger.info("Shutting down bot...") + if application.updater and application.updater.running: + await application.updater.stop() + await application.shutdown() + logger.info("Bot shutdown complete.") if __name__ == '__main__': - main() + try: + asyncio.run(main()) + except KeyboardInterrupt: + logger.info("Bot stopped by user (KeyboardInterrupt from outer scope)") diff --git a/ping_module.py b/ping_module.py index 2984132..0f1de86 100644 --- a/ping_module.py +++ b/ping_module.py @@ -1,63 +1,44 @@ -# ping_uptime.py - Telegram Bot Module - import time from datetime import datetime +from telegram import Update +from telegram.ext import ContextTypes +from telegram.constants import ParseMode class PingUptime: def __init__(self): self.start_time = time.time() - def ping_command(self, update, context): - """Handle /ping command with bot response ping""" - # Calculate bot response time - start_time = time.time() - - # Send initial message - message = update.message.reply_text("*Checking ping...*", parse_mode='Markdown') + async def ping_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + start_ts = time.time() + message = await update.message.reply_text("*Checking ping...*", parse_mode=ParseMode.MARKDOWN) + end_ts = time.time() - # Calculate response time - end_time = time.time() - bot_ping = round((end_time - start_time) * 1000) + bot_ping = round((end_ts - start_ts) * 1000) - # Calculate uptime uptime_seconds = int(time.time() - self.start_time) uptime_str = self.format_uptime(uptime_seconds) - # Format response - response = f"*Pong!*\n\n" - response += f"*Bot Ping:* `{bot_ping}ms`\n" - response += f"*Uptime:* `{uptime_str}`" - response += f"\n*Last Check:* `{datetime.now().strftime('%H:%M:%S')}`" + response = (f"*Pong!*\n\n" + f"*Bot Ping:* `{bot_ping}ms`\n" + f"*Uptime:* `{uptime_str}`" + f"\n*Last Check:* `{datetime.now().strftime('%H:%M:%S')}`") - # Edit the message with results - message.edit_text(response, parse_mode='Markdown') + await message.edit_text(response, parse_mode=ParseMode.MARKDOWN) - - def format_uptime(self, seconds): - """Format uptime seconds into readable string""" - days = seconds // 86400 - hours = (seconds % 86400) // 3600 - minutes = (seconds % 3600) // 60 - seconds = seconds % 60 + def format_uptime(self, seconds: int) -> str: + days, rem = divmod(seconds, 86400) + hours, rem = divmod(rem, 3600) + minutes, secs = divmod(rem, 60) if days > 0: - return f"{days}d {hours}h {minutes}m {seconds}s" - elif hours > 0: - return f"{hours}h {minutes}m {seconds}s" - elif minutes > 0: - return f"{minutes}m {seconds}s" - else: - return f"{seconds}s" - -# Create module instance -ping_uptime = PingUptime() + return f"{days}d {hours}h {minutes}m {secs}s" + if hours > 0: + return f"{hours}h {minutes}m {secs}s" + if minutes > 0: + return f"{minutes}m {secs}s" + return f"{secs}s" -# Export the handler function -def setup_ping_handler(application): - """Setup ping command handler""" - from telegram.ext import CommandHandler - application.add_handler(CommandHandler("ping", ping_uptime.ping_command)) +_ping_uptime_instance = PingUptime() -def ping_handler(update, context): - """Direct ping handler function""" - ping_uptime.ping_command(update, context) +# Expose the async command function directly +ping_command = _ping_uptime_instance.ping_command diff --git a/requirements.txt b/requirements.txt index 4df70c3..e3424c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -python-telegram-bot==13.15 -python-dotenv==1.0.0 -six==1.16.0 -urllib3==1.26.18 +python-telegram-bot +python-dotenv +six +urllib3 psutil