# -*- coding: utf-8 -*- import telebot from telebot import apihelper apihelper.CONNECT_TIMEOUT = 60 apihelper.READ_TIMEOUT = 60 # Tor SOCKS5 proxy โ€” bypasses HuggingFace block on api.telegram.org apihelper.proxy = {'https': 'socks5h://127.0.0.1:9050', 'http': 'socks5h://127.0.0.1:9050'} import subprocess import os import zipfile import tempfile import shutil from telebot import types import time from datetime import datetime, timedelta # Removed unused telegram.* imports as we are using telebot consistently # from telegram import Update # from telegram.ext import Updater, CommandHandler, CallbackContext import psutil import sqlite3 import json # Kept in case needed elsewhere, but not used in provided logic import logging # Kept in case needed elsewhere import signal # Kept in case needed elsewhere import threading import re # Added for regex matching in auto-install import sys # Added for sys.executable import atexit import requests # For polling exceptions # --- Flask Keep Alive --- from flask import Flask from threading import Thread app = Flask('') @app.route('/') def home(): return "I am alive" def run_flask(): port = int(os.environ.get("PORT", 7860)) app.run(host='0.0.0.0', port=port) def keep_alive(): t = Thread(target=run_flask) t.daemon = True t.start() print("Flask Keep-Alive server started.") # --- End Flask Keep Alive --- # --- Auto Ping for Hugging Face Spaces --- def auto_ping(): while True: try: # Ping the bot\'s own Flask server to keep it alive requests.get("http://localhost:7860") # Assuming Flask runs on 7860 as per run_flask logger.info("Self-ping successful.") except requests.exceptions.ConnectionError as e: logger.error(f"Self-ping failed: {e}") time.sleep(300) # Ping every 5 minutes def start_auto_ping(): t = Thread(target=auto_ping) t.daemon = True t.start() logger.info("Auto-ping thread started.") # --- End Auto Ping --- # --- Configuration --- TOKEN = os.environ.get('BOT_TOKEN', 'YOUR_DEFAULT_BOT_TOKEN') # Replace with your bot token from BotFather OWNER_ID = int(os.environ.get('OWNER_ID', '0')) # Replace with your Owner ID ADMIN_ID = int(os.environ.get('ADMIN_ID', '0')) # Replace with your Admin ID (can be same as Owner) YOUR_USERNAME = os.environ.get('YOUR_USERNAME', '@your_telegram_username') # Replace with your Telegram username (without the @) UPDATE_CHANNEL = os.environ.get('UPDATE_CHANNEL', '@your_update_channel_link') # Replace with your update channel link # Folder setup - using absolute paths BASE_DIR = os.path.abspath(os.path.dirname(__file__)) # Get script's directory UPLOAD_BOTS_DIR = os.path.join(BASE_DIR, 'upload_bots') IROTECH_DIR = os.path.join(BASE_DIR, 'inf') # Assuming this name is intentional DATABASE_PATH = os.path.join(IROTECH_DIR, 'bot_data.db') # File upload limits FREE_USER_LIMIT = 0 SUBSCRIBED_USER_LIMIT = 15 # Changed from 10 to 15 ADMIN_LIMIT = 999 # Changed from 50 to 999 OWNER_LIMIT = float('inf') # Changed from 999 to infinity # FREE_MODE_LIMIT = 3 # Removed as free_mode is removed # Create necessary directories os.makedirs(UPLOAD_BOTS_DIR, exist_ok=True) os.makedirs(IROTECH_DIR, exist_ok=True) # Initialize bot bot = telebot.TeleBot(TOKEN) # --- Data structures --- bot_scripts = {} # Stores info about running scripts {script_key: info_dict} user_subscriptions = {} # {user_id: {'expiry': datetime_object}} user_files = {} # {user_id: [(file_name, file_type), ...]} active_users = set() # Set of all user IDs that have interacted with the bot admin_ids = {ADMIN_ID, OWNER_ID} # Set of admin IDs bot_locked = False # free_mode = False # Removed free_mode # --- Logging Setup --- # Configure basic logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # --- Command Button Layouts (ReplyKeyboardMarkup) --- COMMAND_BUTTONS_LAYOUT_USER_SPEC = [ ["๐Ÿ“ข Updates Channel"], ["๐Ÿ“ค Upload File", "๐Ÿ“‚ Check Files"], ["โšก Bot Speed", "๐Ÿ“Š Statistics"], # Statistics button kept for users, logic will restrict if not admin ["๐Ÿ“ž Contact Owner"] ] ADMIN_COMMAND_BUTTONS_LAYOUT_USER_SPEC = [ ["๐Ÿ“ข Updates Channel"], ["๐Ÿ“ค Upload File", "๐Ÿ“‚ Check Files"], ["โšก Bot Speed", "๐Ÿ“Š Statistics"], ["๐Ÿ’ณ Subscriptions", "๐Ÿ“ข Broadcast"], ["๐Ÿ”’ Lock Bot", "๐ŸŸข Running All Code"], # Changed "Free Mode" to "Running All Code" ["๐Ÿ‘‘ Admin Panel", "๐Ÿ“ž Contact Owner"] ] # --- Database Setup --- def init_db(): """Initialize the database with required tables""" logger.info(f"Initializing database at: {DATABASE_PATH}") try: conn = sqlite3.connect(DATABASE_PATH, check_same_thread=False) # Allow access from multiple threads c = conn.cursor() c.execute('''CREATE TABLE IF NOT EXISTS subscriptions (user_id INTEGER PRIMARY KEY, expiry TEXT)''') c.execute('''CREATE TABLE IF NOT EXISTS user_files (user_id INTEGER, file_name TEXT, file_type TEXT, PRIMARY KEY (user_id, file_name))''') c.execute('''CREATE TABLE IF NOT EXISTS active_users (user_id INTEGER PRIMARY KEY)''') c.execute('''CREATE TABLE IF NOT EXISTS admins (user_id INTEGER PRIMARY KEY)''') # Added admins table # Ensure owner and initial admin are in admins table c.execute('INSERT OR IGNORE INTO admins (user_id) VALUES (?)', (OWNER_ID,)) if ADMIN_ID != OWNER_ID: c.execute('INSERT OR IGNORE INTO admins (user_id) VALUES (?)', (ADMIN_ID,)) conn.commit() conn.close() logger.info("Database initialized successfully.") except Exception as e: logger.error(f"โŒ Database initialization error: {e}", exc_info=True) def load_data(): """Load data from database into memory""" logger.info("Loading data from database...") try: conn = sqlite3.connect(DATABASE_PATH, check_same_thread=False) c = conn.cursor() # Load subscriptions c.execute('SELECT user_id, expiry FROM subscriptions') for user_id, expiry in c.fetchall(): try: user_subscriptions[user_id] = {'expiry': datetime.fromisoformat(expiry)} except ValueError: logger.warning(f"โš ๏ธ Invalid expiry date format for user {user_id}: {expiry}. Skipping.") # Load user files c.execute('SELECT user_id, file_name, file_type FROM user_files') for user_id, file_name, file_type in c.fetchall(): if user_id not in user_files: user_files[user_id] = [] user_files[user_id].append((file_name, file_type)) # Load active users c.execute('SELECT user_id FROM active_users') active_users.update(user_id for (user_id,) in c.fetchall()) # Load admins c.execute('SELECT user_id FROM admins') admin_ids.update(user_id for (user_id,) in c.fetchall()) # Load admins into the set conn.close() logger.info(f"Data loaded: {len(active_users)} users, {len(user_subscriptions)} subscriptions, {len(admin_ids)} admins.") except Exception as e: logger.error(f"โŒ Error loading data: {e}", exc_info=True) # Initialize DB and Load Data at startup init_db() load_data() # --- End Database Setup --- # --- Helper Functions --- def get_user_folder(user_id): """Get or create user's folder for storing files""" user_folder = os.path.join(UPLOAD_BOTS_DIR, str(user_id)) os.makedirs(user_folder, exist_ok=True) return user_folder def get_user_file_limit(user_id): """Get the file upload limit for a user""" # if free_mode: return FREE_MODE_LIMIT # Removed free_mode check if user_id == OWNER_ID: return OWNER_LIMIT if user_id in admin_ids: return ADMIN_LIMIT if user_id in user_subscriptions and user_subscriptions[user_id]['expiry'] > datetime.now(): return SUBSCRIBED_USER_LIMIT return FREE_USER_LIMIT def get_user_file_count(user_id): """Get the number of files uploaded by a user""" return len(user_files.get(user_id, [])) def is_bot_running(script_owner_id, file_name): # Parameter renamed for clarity """Check if a bot script is currently running for a specific user""" script_key = f"{script_owner_id}_{file_name}" # Key uses script_owner_id script_info = bot_scripts.get(script_key) if script_info and script_info.get('process'): try: proc = psutil.Process(script_info['process'].pid) is_running = proc.is_running() and proc.status() != psutil.STATUS_ZOMBIE if not is_running: logger.warning(f"Process {script_info['process'].pid} for {script_key} found in memory but not running/zombie. Cleaning up.") if 'log_file' in script_info and hasattr(script_info['log_file'], 'close') and not script_info['log_file'].closed: try: script_info['log_file'].close() except Exception as log_e: logger.error(f"Error closing log file during zombie cleanup {script_key}: {log_e}") if script_key in bot_scripts: del bot_scripts[script_key] return is_running except psutil.NoSuchProcess: logger.warning(f"Process for {script_key} not found (NoSuchProcess). Cleaning up.") if 'log_file' in script_info and hasattr(script_info['log_file'], 'close') and not script_info['log_file'].closed: try: script_info['log_file'].close() except Exception as log_e: logger.error(f"Error closing log file during cleanup of non-existent process {script_key}: {log_e}") if script_key in bot_scripts: del bot_scripts[script_key] return False except Exception as e: logger.error(f"Error checking process status for {script_key}: {e}", exc_info=True) return False return False def kill_process_tree(process_info): """Kill a process and all its children, ensuring log file is closed.""" pid = None log_file_closed = False script_key = process_info.get('script_key', 'N/A') try: if 'log_file' in process_info and hasattr(process_info['log_file'], 'close') and not process_info['log_file'].closed: try: process_info['log_file'].close() log_file_closed = True logger.info(f"Closed log file for {script_key} (PID: {process_info.get('process', {}).get('pid', 'N/A')})") except Exception as log_e: logger.error(f"Error closing log file during kill for {script_key}: {log_e}") process = process_info.get('process') if process and hasattr(process, 'pid'): pid = process.pid if pid: try: parent = psutil.Process(pid) children = parent.children(recursive=True) logger.info(f"Attempting to kill process tree for {script_key} (PID: {pid}, Children: {[c.pid for c in children]})") for child in children: try: child.terminate() logger.info(f"Terminated child process {child.pid} for {script_key}") except psutil.NoSuchProcess: logger.warning(f"Child process {child.pid} for {script_key} already gone.") except Exception as e: logger.error(f"Error terminating child {child.pid} for {script_key}: {e}. Trying kill...") try: child.kill(); logger.info(f"Killed child process {child.pid} for {script_key}") except Exception as e2: logger.error(f"Failed to kill child {child.pid} for {script_key}: {e2}") gone, alive = psutil.wait_procs(children, timeout=1) for p in alive: logger.warning(f"Child process {p.pid} for {script_key} still alive. Killing.") try: p.kill() except Exception as e: logger.error(f"Failed to kill child {p.pid} for {script_key} after wait: {e}") try: parent.terminate() logger.info(f"Terminated parent process {pid} for {script_key}") try: parent.wait(timeout=1) except psutil.TimeoutExpired: logger.warning(f"Parent process {pid} for {script_key} did not terminate. Killing.") parent.kill() logger.info(f"Killed parent process {pid} for {script_key}") except psutil.NoSuchProcess: logger.warning(f"Parent process {pid} for {script_key} already gone.") except Exception as e: logger.error(f"Error terminating parent {pid} for {script_key}: {e}. Trying kill...") try: parent.kill(); logger.info(f"Killed parent process {pid} for {script_key}") except Exception as e2: logger.error(f"Failed to kill parent {pid} for {script_key}: {e2}") except psutil.NoSuchProcess: logger.warning(f"Process {pid or 'N/A'} for {script_key} not found during kill. Already terminated?") else: logger.error(f"Process PID is None for {script_key}.") elif log_file_closed: logger.warning(f"Process object missing for {script_key}, but log file closed.") else: logger.error(f"Process object missing for {script_key}, and no log file. Cannot kill.") except Exception as e: logger.error(f"โŒ Unexpected error killing process tree for PID {pid or 'N/A'} ({script_key}): {e}", exc_info=True) # --- Automatic Package Installation & Script Running --- def attempt_install_pip(module_name, message): package_name = TELEGRAM_MODULES.get(module_name.lower(), module_name) if package_name is None: logger.info(f"Module '{module_name}' is core. Skipping pip install.") return False try: bot.reply_to(message, f"๐Ÿ Module `{module_name}` not found. Installing `{package_name}`...", parse_mode=None) command = [sys.executable, '-m', 'pip', 'install', package_name] logger.info(f"Running install: {' '.join(command)}") result = subprocess.run(command, capture_output=True, text=True, check=False, encoding='utf-8', errors='ignore') if result.returncode == 0: logger.info(f"Installed {package_name}. Output:\n{result.stdout}") bot.reply_to(message, f"โœ… Package `{package_name}` (for `{module_name}`) installed.", parse_mode=None) return True else: error_msg = f"โŒ Failed to install `{package_name}` for `{module_name}`.\nLog:\n```\n{result.stderr or result.stdout}\n```" logger.error(error_msg) if len(error_msg) > 4000: error_msg = error_msg[:4000] + "\n... (Log truncated)" bot.reply_to(message, error_msg, parse_mode=None) return False except Exception as e: error_msg = f"โŒ Error installing `{package_name}`: {str(e)}" logger.error(error_msg, exc_info=True) bot.reply_to(message, error_msg) return False def attempt_install_npm(module_name, user_folder, message): try: bot.reply_to(message, f"๐ŸŸ  Node package `{module_name}` not found. Installing locally...", parse_mode=None) command = ['npm', 'install', module_name] logger.info(f"Running npm install: {' '.join(command)} in {user_folder}") result = subprocess.run(command, capture_output=True, text=True, check=False, cwd=user_folder, encoding='utf-8', errors='ignore') if result.returncode == 0: logger.info(f"Installed {module_name}. Output:\n{result.stdout}") bot.reply_to(message, f"โœ… Node package `{module_name}` installed locally.", parse_mode=None) return True else: error_msg = f"โŒ Failed to install Node package `{module_name}`.\nLog:\n```\n{result.stderr or result.stdout}\n```" logger.error(error_msg) if len(error_msg) > 4000: error_msg = error_msg[:4000] + "\n... (Log truncated)" bot.reply_to(message, error_msg, parse_mode=None) return False except FileNotFoundError: error_msg = "โŒ Error: 'npm' not found. Ensure Node.js/npm are installed and in PATH." logger.error(error_msg) bot.reply_to(message, error_msg) return False except Exception as e: error_msg = f"โŒ Error installing Node package `{module_name}`: {str(e)}" logger.error(error_msg, exc_info=True) bot.reply_to(message, error_msg) return False def run_script(script_path, script_owner_id, user_folder, file_name, message_obj_for_reply, attempt=1): """Run Python script. script_owner_id is used for the script_key. message_obj_for_reply is for sending feedback.""" max_attempts = 2 if attempt > max_attempts: bot.reply_to(message_obj_for_reply, f"โŒ Failed to run '{file_name}' after {max_attempts} attempts. Check logs.") return script_key = f"{script_owner_id}_{file_name}" logger.info(f"Attempt {attempt} to run Python script: {script_path} (Key: {script_key}) for user {script_owner_id}") try: if not os.path.exists(script_path): bot.reply_to(message_obj_for_reply, f"โŒ Error: Script '{file_name}' not found at '{script_path}'!") logger.error(f"Script not found: {script_path} for user {script_owner_id}") if script_owner_id in user_files: user_files[script_owner_id] = [f for f in user_files.get(script_owner_id, []) if f[0] != file_name] remove_user_file_db(script_owner_id, file_name) return if attempt == 1: check_command = [sys.executable, script_path] logger.info(f"Running Python pre-check: {' '.join(check_command)}") check_proc = None try: check_proc = subprocess.Popen(check_command, cwd=user_folder, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8', errors='ignore') stdout, stderr = check_proc.communicate(timeout=15) return_code = check_proc.returncode logger.info(f"Python Pre-check early. RC: {return_code}. Stderr: {stderr[:200]}...") if return_code != 0 and stderr: match_py = re.search(r"ModuleNotFoundError: No module named '(.+?)'", stderr) if match_py: module_name = match_py.group(1).strip().strip("'\"") logger.info(f"Detected missing Python module: {module_name}") if attempt_install_pip(module_name, message_obj_for_reply): logger.info(f"Install OK for {module_name}. Retrying run_script...") bot.reply_to(message_obj_for_reply, f"๐Ÿ”„ Install successful. Retrying '{file_name}'...") time.sleep(2) threading.Thread(target=run_script, args=(script_path, script_owner_id, user_folder, file_name, message_obj_for_reply, attempt + 1)).start() return else: bot.reply_to(message_obj_for_reply, f"โŒ Install failed. Cannot run '{file_name}'.") return else: error_summary = stderr[:500] bot.reply_to(message_obj_for_reply, f"โŒ Error in script pre-check for '{file_name}':\n```\n{error_summary}\n```\nFix the script.", parse_mode=None) return except subprocess.TimeoutExpired: logger.info("Python Pre-check timed out (>5s), imports likely OK. Killing check process.") if check_proc and check_proc.poll() is None: check_proc.kill(); check_proc.communicate() logger.info("Python Check process killed. Proceeding to long run.") except FileNotFoundError: logger.error(f"Python interpreter not found: {sys.executable}") bot.reply_to(message_obj_for_reply, f"โŒ Error: Python interpreter '{sys.executable}' not found.") return except Exception as e: logger.error(f"Error in Python pre-check for {script_key}: {e}", exc_info=True) bot.reply_to(message_obj_for_reply, f"โŒ Unexpected error in script pre-check for '{file_name}': {e}") return finally: if check_proc and check_proc.poll() is None: logger.warning(f"Python Check process {check_proc.pid} still running. Killing.") check_proc.kill(); check_proc.communicate() logger.info(f"Starting long-running Python process for {script_key}") log_file_path = os.path.join(user_folder, f"{os.path.splitext(file_name)[0]}.log") log_file = None; process = None try: log_file = open(log_file_path, 'w', encoding='utf-8', errors='ignore') except Exception as e: logger.error(f"Failed to open log file '{log_file_path}' for {script_key}: {e}", exc_info=True) bot.reply_to(message_obj_for_reply, f"โŒ Failed to open log file '{log_file_path}': {e}") return try: startupinfo = None; creationflags = 0 if os.name == 'nt': startupinfo = subprocess.STARTUPINFO(); startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.wShowWindow = subprocess.SW_HIDE process = subprocess.Popen( [sys.executable, script_path], cwd=user_folder, stdout=log_file, stderr=log_file, stdin=subprocess.PIPE, startupinfo=startupinfo, creationflags=creationflags, encoding='utf-8', errors='ignore' ) logger.info(f"Started Python process {process.pid} for {script_key}") bot_scripts[script_key] = { 'process': process, 'log_file': log_file, 'file_name': file_name, 'chat_id': message_obj_for_reply.chat.id, # Chat ID for potential future direct replies from script, defaults to admin/triggering user 'script_owner_id': script_owner_id, # Actual owner of the script 'start_time': datetime.now(), 'user_folder': user_folder, 'type': 'py', 'script_key': script_key } bot.reply_to(message_obj_for_reply, f"โœ… Python script '{file_name}' started! (PID: {process.pid}) (For User: {script_owner_id})") except FileNotFoundError: logger.error(f"Python interpreter {sys.executable} not found for long run {script_key}") bot.reply_to(message_obj_for_reply, f"โŒ Error: Python interpreter '{sys.executable}' not found.") if log_file and not log_file.closed: log_file.close() if script_key in bot_scripts: del bot_scripts[script_key] except Exception as e: if log_file and not log_file.closed: log_file.close() error_msg = f"โŒ Error starting Python script '{file_name}': {str(e)}" logger.error(error_msg, exc_info=True) bot.reply_to(message_obj_for_reply, error_msg) if process and process.poll() is None: logger.warning(f"Killing potentially started Python process {process.pid} for {script_key}") kill_process_tree({'process': process, 'log_file': log_file, 'script_key': script_key}) if script_key in bot_scripts: del bot_scripts[script_key] except Exception as e: error_msg = f"โŒ Unexpected error running Python script '{file_name}': {str(e)}" logger.error(error_msg, exc_info=True) bot.reply_to(message_obj_for_reply, error_msg) if script_key in bot_scripts: logger.warning(f"Cleaning up {script_key} due to error in run_script.") kill_process_tree(bot_scripts[script_key]) del bot_scripts[script_key] def run_js_script(script_path, script_owner_id, user_folder, file_name, message_obj_for_reply, attempt=1): """Run JS script. script_owner_id is used for the script_key. message_obj_for_reply is for sending feedback.""" max_attempts = 2 if attempt > max_attempts: bot.reply_to(message_obj_for_reply, f"โŒ Failed to run '{file_name}' after {max_attempts} attempts. Check logs.") return script_key = f"{script_owner_id}_{file_name}" logger.info(f"Attempt {attempt} to run JS script: {script_path} (Key: {script_key}) for user {script_owner_id}") try: if not os.path.exists(script_path): bot.reply_to(message_obj_for_reply, f"โŒ Error: Script '{file_name}' not found at '{script_path}'!") logger.error(f"JS Script not found: {script_path} for user {script_owner_id}") if script_owner_id in user_files: user_files[script_owner_id] = [f for f in user_files.get(script_owner_id, []) if f[0] != file_name] remove_user_file_db(script_owner_id, file_name) return if attempt == 1: check_command = ['node', script_path] logger.info(f"Running JS pre-check: {' '.join(check_command)}") check_proc = None try: check_proc = subprocess.Popen(check_command, cwd=user_folder, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8', errors='ignore') stdout, stderr = check_proc.communicate(timeout=15) return_code = check_proc.returncode logger.info(f"JS Pre-check early. RC: {return_code}. Stderr: {stderr[:200]}...") if return_code != 0 and stderr: match_js = re.search(r"Cannot find module '(.+?)'", stderr) if match_js: module_name = match_js.group(1).strip().strip("'\"") if not module_name.startswith('.') and not module_name.startswith('/'): logger.info(f"Detected missing Node module: {module_name}") if attempt_install_npm(module_name, user_folder, message_obj_for_reply): logger.info(f"NPM Install OK for {module_name}. Retrying run_js_script...") bot.reply_to(message_obj_for_reply, f"๐Ÿ”„ NPM Install successful. Retrying '{file_name}'...") time.sleep(2) threading.Thread(target=run_js_script, args=(script_path, script_owner_id, user_folder, file_name, message_obj_for_reply, attempt + 1)).start() return else: bot.reply_to(message_obj_for_reply, f"โŒ NPM Install failed. Cannot run '{file_name}'.") return else: logger.info(f"Skipping npm install for relative/core: {module_name}") error_summary = stderr[:500] bot.reply_to(message_obj_for_reply, f"โŒ Error in JS script pre-check for '{file_name}':\n```\n{error_summary}\n```\nFix script or install manually.", parse_mode=None) return except subprocess.TimeoutExpired: logger.info("JS Pre-check timed out (>5s), imports likely OK. Killing check process.") if check_proc and check_proc.poll() is None: check_proc.kill(); check_proc.communicate() logger.info("JS Check process killed. Proceeding to long run.") except FileNotFoundError: error_msg = "โŒ Error: 'node' not found. Ensure Node.js is installed for JS files." logger.error(error_msg) bot.reply_to(message_obj_for_reply, error_msg) return except Exception as e: logger.error(f"Error in JS pre-check for {script_key}: {e}", exc_info=True) bot.reply_to(message_obj_for_reply, f"โŒ Unexpected error in JS pre-check for '{file_name}': {e}") return finally: if check_proc and check_proc.poll() is None: logger.warning(f"JS Check process {check_proc.pid} still running. Killing.") check_proc.kill(); check_proc.communicate() logger.info(f"Starting long-running JS process for {script_key}") log_file_path = os.path.join(user_folder, f"{os.path.splitext(file_name)[0]}.log") log_file = None; process = None try: log_file = open(log_file_path, 'w', encoding='utf-8', errors='ignore') except Exception as e: logger.error(f"Failed to open log file '{log_file_path}' for JS script {script_key}: {e}", exc_info=True) bot.reply_to(message_obj_for_reply, f"โŒ Failed to open log file '{log_file_path}': {e}") return try: startupinfo = None; creationflags = 0 if os.name == 'nt': startupinfo = subprocess.STARTUPINFO(); startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.wShowWindow = subprocess.SW_HIDE process = subprocess.Popen( ['node', script_path], cwd=user_folder, stdout=log_file, stderr=log_file, stdin=subprocess.PIPE, startupinfo=startupinfo, creationflags=creationflags, encoding='utf-8', errors='ignore' ) logger.info(f"Started JS process {process.pid} for {script_key}") bot_scripts[script_key] = { 'process': process, 'log_file': log_file, 'file_name': file_name, 'chat_id': message_obj_for_reply.chat.id, # Chat ID for potential future direct replies 'script_owner_id': script_owner_id, # Actual owner of the script 'start_time': datetime.now(), 'user_folder': user_folder, 'type': 'js', 'script_key': script_key } bot.reply_to(message_obj_for_reply, f"โœ… JS script '{file_name}' started! (PID: {process.pid}) (For User: {script_owner_id})") except FileNotFoundError: error_msg = "โŒ Error: 'node' not found for long run. Ensure Node.js is installed." logger.error(error_msg) if log_file and not log_file.closed: log_file.close() bot.reply_to(message_obj_for_reply, error_msg) if script_key in bot_scripts: del bot_scripts[script_key] except Exception as e: if log_file and not log_file.closed: log_file.close() error_msg = f"โŒ Error starting JS script '{file_name}': {str(e)}" logger.error(error_msg, exc_info=True) bot.reply_to(message_obj_for_reply, error_msg) if process and process.poll() is None: logger.warning(f"Killing potentially started JS process {process.pid} for {script_key}") kill_process_tree({'process': process, 'log_file': log_file, 'script_key': script_key}) if script_key in bot_scripts: del bot_scripts[script_key] except Exception as e: error_msg = f"โŒ Unexpected error running JS script '{file_name}': {str(e)}" logger.error(error_msg, exc_info=True) bot.reply_to(message_obj_for_reply, error_msg) if script_key in bot_scripts: logger.warning(f"Cleaning up {script_key} due to error in run_js_script.") kill_process_tree(bot_scripts[script_key]) del bot_scripts[script_key] # --- Map Telegram import names to actual PyPI package names --- TELEGRAM_MODULES = { # Main Bot Frameworks 'telebot': 'pyTelegramBotAPI', 'telegram': 'python-telegram-bot', 'python_telegram_bot': 'python-telegram-bot', 'aiogram': 'aiogram', 'pyrogram': 'pyrogram', 'telethon': 'telethon', 'telethon.sync': 'telethon', # Handle specific imports 'from telethon.sync import telegramclient': 'telethon', # Example # Additional Libraries (add more specific mappings if import name differs) 'telepot': 'telepot', 'pytg': 'pytg', 'tgcrypto': 'tgcrypto', 'telegram_upload': 'telegram-upload', 'telegram_send': 'telegram-send', 'telegram_text': 'telegram-text', # MTProto & Low-Level 'mtproto': 'telegram-mtproto', # Example, check actual package name 'tl': 'telethon', # Part of Telethon, install 'telethon' # Utilities & Helpers (examples, verify package names) 'telegram_utils': 'telegram-utils', 'telegram_logger': 'telegram-logger', 'telegram_handlers': 'python-telegram-handlers', # Database Integrations (examples) 'telegram_redis': 'telegram-redis', 'telegram_sqlalchemy': 'telegram-sqlalchemy', # Payment & E-commerce (examples) 'telegram_payment': 'telegram-payment', 'telegram_shop': 'telegram-shop-sdk', # Testing & Debugging (examples) 'pytest_telegram': 'pytest-telegram', 'telegram_debug': 'telegram-debug', # Scraping & Analytics (examples) 'telegram_scraper': 'telegram-scraper', 'telegram_analytics': 'telegram-analytics', # NLP & AI (examples) 'telegram_nlp': 'telegram-nlp-toolkit', 'telegram_ai': 'telegram-ai', # Assuming this exists # Web & API Integration (examples) 'telegram_api': 'telegram-api-client', 'telegram_web': 'telegram-web-integration', # Gaming & Interactive (examples) 'telegram_games': 'telegram-games', 'telegram_quiz': 'telegram-quiz-bot', # File & Media Handling (examples) 'telegram_ffmpeg': 'telegram-ffmpeg', 'telegram_media': 'telegram-media-utils', # Security & Encryption (examples) 'telegram_2fa': 'telegram-twofa', 'telegram_crypto': 'telegram-crypto-bot', # Localization & i18n (examples) 'telegram_i18n': 'telegram-i18n', 'telegram_translate': 'telegram-translate', # Common non-telegram examples 'bs4': 'beautifulsoup4', 'requests': 'requests', 'pillow': 'Pillow', # Note the capitalization difference 'cv2': 'opencv-python', # Common import name for OpenCV 'yaml': 'PyYAML', 'dotenv': 'python-dotenv', 'dateutil': 'python-dateutil', 'pandas': 'pandas', 'numpy': 'numpy', 'flask': 'Flask', 'django': 'Django', 'sqlalchemy': 'SQLAlchemy', 'asyncio': None, # Core module, should not be installed 'json': None, # Core module 'datetime': None,# Core module 'os': None, # Core module 'sys': None, # Core module 're': None, # Core module 'time': None, # Core module 'math': None, # Core module 'random': None, # Core module 'logging': None, # Core module 'threading': None,# Core module 'subprocess':None,# Core module 'zipfile':None, # Core module 'tempfile':None, # Core module 'shutil':None, # Core module 'sqlite3':None, # Core module 'psutil': 'psutil', 'atexit': None # Core module } # --- End Automatic Package Installation & Script Running --- # --- Database Operations --- DB_LOCK = threading.Lock() def save_user_file(user_id, file_name, file_type='py'): with DB_LOCK: conn = sqlite3.connect(DATABASE_PATH, check_same_thread=False) c = conn.cursor() try: c.execute('INSERT OR REPLACE INTO user_files (user_id, file_name, file_type) VALUES (?, ?, ?)', (user_id, file_name, file_type)) conn.commit() if user_id not in user_files: user_files[user_id] = [] user_files[user_id] = [(fn, ft) for fn, ft in user_files[user_id] if fn != file_name] user_files[user_id].append((file_name, file_type)) logger.info(f"Saved file '{file_name}' ({file_type}) for user {user_id}") except sqlite3.Error as e: logger.error(f"โŒ SQLite error saving file for user {user_id}, {file_name}: {e}") except Exception as e: logger.error(f"โŒ Unexpected error saving file for {user_id}, {file_name}: {e}", exc_info=True) finally: conn.close() def remove_user_file_db(user_id, file_name): with DB_LOCK: conn = sqlite3.connect(DATABASE_PATH, check_same_thread=False) c = conn.cursor() try: c.execute('DELETE FROM user_files WHERE user_id = ? AND file_name = ?', (user_id, file_name)) conn.commit() if user_id in user_files: user_files[user_id] = [f for f in user_files[user_id] if f[0] != file_name] if not user_files[user_id]: del user_files[user_id] logger.info(f"Removed file '{file_name}' for user {user_id} from DB") except sqlite3.Error as e: logger.error(f"โŒ SQLite error removing file for {user_id}, {file_name}: {e}") except Exception as e: logger.error(f"โŒ Unexpected error removing file for {user_id}, {file_name}: {e}", exc_info=True) finally: conn.close() def add_active_user(user_id): active_users.add(user_id) with DB_LOCK: conn = sqlite3.connect(DATABASE_PATH, check_same_thread=False) c = conn.cursor() try: c.execute('INSERT OR IGNORE INTO active_users (user_id) VALUES (?)', (user_id,)) conn.commit() logger.info(f"Added/Confirmed active user {user_id} in DB") except sqlite3.Error as e: logger.error(f"โŒ SQLite error adding active user {user_id}: {e}") except Exception as e: logger.error(f"โŒ Unexpected error adding active user {user_id}: {e}", exc_info=True) finally: conn.close() def save_subscription(user_id, expiry): with DB_LOCK: conn = sqlite3.connect(DATABASE_PATH, check_same_thread=False) c = conn.cursor() try: expiry_str = expiry.isoformat() c.execute('INSERT OR REPLACE INTO subscriptions (user_id, expiry) VALUES (?, ?)', (user_id, expiry_str)) conn.commit() user_subscriptions[user_id] = {'expiry': expiry} logger.info(f"Saved subscription for {user_id}, expiry {expiry_str}") except sqlite3.Error as e: logger.error(f"โŒ SQLite error saving subscription for {user_id}: {e}") except Exception as e: logger.error(f"โŒ Unexpected error saving subscription for {user_id}: {e}", exc_info=True) finally: conn.close() def remove_subscription_db(user_id): with DB_LOCK: conn = sqlite3.connect(DATABASE_PATH, check_same_thread=False) c = conn.cursor() try: c.execute('DELETE FROM subscriptions WHERE user_id = ?', (user_id,)) conn.commit() if user_id in user_subscriptions: del user_subscriptions[user_id] logger.info(f"Removed subscription for {user_id} from DB") except sqlite3.Error as e: logger.error(f"โŒ SQLite error removing subscription for {user_id}: {e}") except Exception as e: logger.error(f"โŒ Unexpected error removing subscription for {user_id}: {e}", exc_info=True) finally: conn.close() def add_admin_db(admin_id): with DB_LOCK: conn = sqlite3.connect(DATABASE_PATH, check_same_thread=False) c = conn.cursor() try: c.execute('INSERT OR IGNORE INTO admins (user_id) VALUES (?)', (admin_id,)) conn.commit() admin_ids.add(admin_id) logger.info(f"Added admin {admin_id} to DB") except sqlite3.Error as e: logger.error(f"โŒ SQLite error adding admin {admin_id}: {e}") except Exception as e: logger.error(f"โŒ Unexpected error adding admin {admin_id}: {e}", exc_info=True) finally: conn.close() def remove_admin_db(admin_id): if admin_id == OWNER_ID: logger.warning("Attempted to remove OWNER_ID from admins.") return False with DB_LOCK: conn = sqlite3.connect(DATABASE_PATH, check_same_thread=False) c = conn.cursor() removed = False try: c.execute('SELECT 1 FROM admins WHERE user_id = ?', (admin_id,)) if c.fetchone(): c.execute('DELETE FROM admins WHERE user_id = ?', (admin_id,)) conn.commit() removed = c.rowcount > 0 if removed: admin_ids.discard(admin_id); logger.info(f"Removed admin {admin_id} from DB") else: logger.warning(f"Admin {admin_id} found but delete affected 0 rows.") else: logger.warning(f"Admin {admin_id} not found in DB.") admin_ids.discard(admin_id) return removed except sqlite3.Error as e: logger.error(f"โŒ SQLite error removing admin {admin_id}: {e}"); return False except Exception as e: logger.error(f"โŒ Unexpected error removing admin {admin_id}: {e}", exc_info=True); return False finally: conn.close() # --- End Database Operations --- # --- Menu creation (Inline and ReplyKeyboards) --- def create_main_menu_inline(user_id): markup = types.InlineKeyboardMarkup(row_width=2) buttons = [ types.InlineKeyboardButton('๐Ÿ“ข Updates Channel', url=f"https://t.me/{UPDATE_CHANNEL.lstrip('@')}"), types.InlineKeyboardButton('๐Ÿ“ค Upload File', callback_data='upload'), types.InlineKeyboardButton('๐Ÿ“‚ Check Files', callback_data='check_files'), types.InlineKeyboardButton('โšก Bot Speed', callback_data='speed'), types.InlineKeyboardButton('๐Ÿ“ž Contact Owner', url=f'https://t.me/{YOUR_USERNAME.replace("@", "")}') ] if user_id in admin_ids: admin_buttons = [ types.InlineKeyboardButton('๐Ÿ’ณ Subscriptions', callback_data='subscription'), #0 types.InlineKeyboardButton('๐Ÿ“Š Statistics', callback_data='stats'), #1 types.InlineKeyboardButton('๐Ÿ”’ Lock Bot' if not bot_locked else '๐Ÿ”“ Unlock Bot', #2 callback_data='lock_bot' if not bot_locked else 'unlock_bot'), types.InlineKeyboardButton('๐Ÿ“ข Broadcast', callback_data='broadcast'), #3 types.InlineKeyboardButton('๐Ÿ‘‘ Admin Panel', callback_data='admin_panel'), #4 types.InlineKeyboardButton('๐ŸŸข Run All User Scripts', callback_data='run_all_scripts') #5 ] markup.add(buttons[0]) # Updates markup.add(buttons[1], buttons[2]) # Upload, Check Files markup.add(buttons[3], admin_buttons[0]) # Speed, Subscriptions markup.add(admin_buttons[1], admin_buttons[3]) # Stats, Broadcast markup.add(admin_buttons[2], admin_buttons[5]) # Lock Bot, Run All Scripts markup.add(admin_buttons[4]) # Admin Panel markup.add(buttons[4]) # Contact else: markup.add(buttons[0]) markup.add(buttons[1], buttons[2]) markup.add(buttons[3]) markup.add(types.InlineKeyboardButton('๐Ÿ“Š Statistics', callback_data='stats')) # Allow non-admins to see stats too markup.add(buttons[4]) return markup def create_reply_keyboard_main_menu(user_id): markup = types.ReplyKeyboardMarkup(resize_keyboard=True, row_width=2) layout_to_use = ADMIN_COMMAND_BUTTONS_LAYOUT_USER_SPEC if user_id in admin_ids else COMMAND_BUTTONS_LAYOUT_USER_SPEC for row_buttons_text in layout_to_use: markup.add(*[types.KeyboardButton(text) for text in row_buttons_text]) return markup def create_control_buttons(script_owner_id, file_name, is_running=True): # Parameter renamed markup = types.InlineKeyboardMarkup(row_width=2) # Callbacks use script_owner_id if is_running: markup.row( types.InlineKeyboardButton("๐Ÿ”ด Stop", callback_data=f'stop_{script_owner_id}_{file_name}'), types.InlineKeyboardButton("๐Ÿ”„ Restart", callback_data=f'restart_{script_owner_id}_{file_name}') ) markup.row( types.InlineKeyboardButton("๐Ÿ—‘๏ธ Delete", callback_data=f'delete_{script_owner_id}_{file_name}'), types.InlineKeyboardButton("๐Ÿ“œ Logs", callback_data=f'logs_{script_owner_id}_{file_name}') ) else: markup.row( types.InlineKeyboardButton("๐ŸŸข Start", callback_data=f'start_{script_owner_id}_{file_name}'), types.InlineKeyboardButton("๐Ÿ—‘๏ธ Delete", callback_data=f'delete_{script_owner_id}_{file_name}') ) markup.row( types.InlineKeyboardButton("๐Ÿ“œ View Logs", callback_data=f'logs_{script_owner_id}_{file_name}') ) markup.add(types.InlineKeyboardButton("๐Ÿ”™ Back to Files", callback_data='check_files')) return markup def create_admin_panel(): markup = types.InlineKeyboardMarkup(row_width=2) markup.row( types.InlineKeyboardButton('โž• Add Admin', callback_data='add_admin'), types.InlineKeyboardButton('โž– Remove Admin', callback_data='remove_admin') ) markup.row(types.InlineKeyboardButton('๐Ÿ“‹ List Admins', callback_data='list_admins')) markup.row(types.InlineKeyboardButton('๐Ÿ”™ Back to Main', callback_data='back_to_main')) return markup def create_subscription_menu(): markup = types.InlineKeyboardMarkup(row_width=2) markup.row( types.InlineKeyboardButton('โž• Add Subscription', callback_data='add_subscription'), types.InlineKeyboardButton('โž– Remove Subscription', callback_data='remove_subscription') ) markup.row(types.InlineKeyboardButton('๐Ÿ” Check Subscription', callback_data='check_subscription')) markup.row(types.InlineKeyboardButton('๐Ÿ”™ Back to Main', callback_data='back_to_main')) return markup # --- End Menu Creation --- # --- File Handling --- def handle_zip_file(downloaded_file_content, file_name_zip, message): user_id = message.from_user.id # chat_id = message.chat.id # script_owner_id (user_id here) will be used for script key context user_folder = get_user_folder(user_id) temp_dir = None try: temp_dir = tempfile.mkdtemp(prefix=f"user_{user_id}_zip_") logger.info(f"Temp dir for zip: {temp_dir}") zip_path = os.path.join(temp_dir, file_name_zip) with open(zip_path, 'wb') as new_file: new_file.write(downloaded_file_content) with zipfile.ZipFile(zip_path, 'r') as zip_ref: for member in zip_ref.infolist(): member_path = os.path.abspath(os.path.join(temp_dir, member.filename)) if not member_path.startswith(os.path.abspath(temp_dir)): raise zipfile.BadZipFile(f"Zip has unsafe path: {member.filename}") zip_ref.extractall(temp_dir) logger.info(f"Extracted zip to {temp_dir}") extracted_items = os.listdir(temp_dir) py_files = [f for f in extracted_items if f.endswith('.py')] js_files = [f for f in extracted_items if f.endswith('.js')] req_file = 'requirements.txt' if 'requirements.txt' in extracted_items else None pkg_json = 'package.json' if 'package.json' in extracted_items else None if req_file: req_path = os.path.join(temp_dir, req_file) logger.info(f"requirements.txt found, installing: {req_path}") bot.reply_to(message, f"๐Ÿ”„ Installing Python deps from `{req_file}`...") try: command = [sys.executable, '-m', 'pip', 'install', '-r', req_path] result = subprocess.run(command, capture_output=True, text=True, check=True, encoding='utf-8', errors='ignore') logger.info(f"pip install from requirements.txt OK. Output:\n{result.stdout}") bot.reply_to(message, f"โœ… Python deps from `{req_file}` installed.") except subprocess.CalledProcessError as e: error_msg = f"โŒ Failed to install Python deps from `{req_file}`.\nLog:\n```\n{e.stderr or e.stdout}\n```" logger.error(error_msg) if len(error_msg) > 4000: error_msg = error_msg[:4000] + "\n... (Log truncated)" bot.reply_to(message, error_msg, parse_mode=None); return except Exception as e: error_msg = f"โŒ Unexpected error installing Python deps: {e}" logger.error(error_msg, exc_info=True); bot.reply_to(message, error_msg); return if pkg_json: logger.info(f"package.json found, npm install in: {temp_dir}") bot.reply_to(message, f"๐Ÿ”„ Installing Node deps from `{pkg_json}`...") try: command = ['npm', 'install'] result = subprocess.run(command, capture_output=True, text=True, check=True, cwd=temp_dir, encoding='utf-8', errors='ignore') logger.info(f"npm install OK. Output:\n{result.stdout}") bot.reply_to(message, f"โœ… Node deps from `{pkg_json}` installed.") except FileNotFoundError: bot.reply_to(message, "โŒ 'npm' not found. Cannot install Node deps."); return except subprocess.CalledProcessError as e: error_msg = f"โŒ Failed to install Node deps from `{pkg_json}`.\nLog:\n```\n{e.stderr or e.stdout}\n```" logger.error(error_msg) if len(error_msg) > 4000: error_msg = error_msg[:4000] + "\n... (Log truncated)" bot.reply_to(message, error_msg, parse_mode=None); return except Exception as e: error_msg = f"โŒ Unexpected error installing Node deps: {e}" logger.error(error_msg, exc_info=True); bot.reply_to(message, error_msg); return main_script_name = None; file_type = None preferred_py = ['main.py', 'bot.py', 'app.py']; preferred_js = ['index.js', 'main.js', 'bot.js', 'app.js'] for p in preferred_py: if p in py_files: main_script_name = p; file_type = 'py'; break if not main_script_name: for p in preferred_js: if p in js_files: main_script_name = p; file_type = 'js'; break if not main_script_name: if py_files: main_script_name = py_files[0]; file_type = 'py' elif js_files: main_script_name = js_files[0]; file_type = 'js' if not main_script_name: bot.reply_to(message, "โŒ No `.py` or `.js` script found in archive!"); return logger.info(f"Moving extracted files from {temp_dir} to {user_folder}") moved_count = 0 for item_name in os.listdir(temp_dir): src_path = os.path.join(temp_dir, item_name) dest_path = os.path.join(user_folder, item_name) if os.path.isdir(dest_path): shutil.rmtree(dest_path) elif os.path.exists(dest_path): os.remove(dest_path) shutil.move(src_path, dest_path); moved_count +=1 logger.info(f"Moved {moved_count} items to {user_folder}") save_user_file(user_id, main_script_name, file_type) logger.info(f"Saved main script '{main_script_name}' ({file_type}) for {user_id} from zip.") main_script_path = os.path.join(user_folder, main_script_name) bot.reply_to(message, f"โœ… Files extracted. Starting main script: `{main_script_name}`...", parse_mode=None) # Use user_id as script_owner_id for script key context if file_type == 'py': threading.Thread(target=run_script, args=(main_script_path, user_id, user_folder, main_script_name, message)).start() elif file_type == 'js': threading.Thread(target=run_js_script, args=(main_script_path, user_id, user_folder, main_script_name, message)).start() except zipfile.BadZipFile as e: logger.error(f"Bad zip file from {user_id}: {e}") bot.reply_to(message, f"โŒ Error: Invalid/corrupted ZIP. {e}") except Exception as e: logger.error(f"โŒ Error processing zip for {user_id}: {e}", exc_info=True) bot.reply_to(message, f"โŒ Error processing zip: {str(e)}") finally: if temp_dir and os.path.exists(temp_dir): try: shutil.rmtree(temp_dir); logger.info(f"Cleaned temp dir: {temp_dir}") except Exception as e: logger.error(f"Failed to clean temp dir {temp_dir}: {e}", exc_info=True) def handle_js_file(file_path, script_owner_id, user_folder, file_name, message): try: save_user_file(script_owner_id, file_name, 'js') threading.Thread(target=run_js_script, args=(file_path, script_owner_id, user_folder, file_name, message)).start() except Exception as e: logger.error(f"โŒ Error processing JS file {file_name} for {script_owner_id}: {e}", exc_info=True) bot.reply_to(message, f"โŒ Error processing JS file: {str(e)}") def handle_py_file(file_path, script_owner_id, user_folder, file_name, message): try: save_user_file(script_owner_id, file_name, 'py') threading.Thread(target=run_script, args=(file_path, script_owner_id, user_folder, file_name, message)).start() except Exception as e: logger.error(f"โŒ Error processing Python file {file_name} for {script_owner_id}: {e}", exc_info=True) bot.reply_to(message, f"โŒ Error processing Python file: {str(e)}") # --- End File Handling --- # --- Logic Functions (called by commands and text handlers) --- def _logic_send_welcome(message): user_id = message.from_user.id chat_id = message.chat.id user_name = message.from_user.first_name user_username = message.from_user.username logger.info(f"Welcome request from user_id: {user_id}, username: @{user_username}") if bot_locked and user_id not in admin_ids: bot.send_message(chat_id, "โš ๏ธ Bot locked by admin. Try later.") return # Send welcome message immediately file_limit = get_user_file_limit(user_id) current_files = get_user_file_count(user_id) limit_str = str(file_limit) if file_limit != float('inf') else "Unlimited" expiry_info = "" if user_id == OWNER_ID: user_status = "๐Ÿ‘‘ Owner" elif user_id in admin_ids: user_status = "๐Ÿ›ก๏ธ Admin" elif user_id in user_subscriptions: expiry_date = user_subscriptions[user_id].get('expiry') if expiry_date and expiry_date > datetime.now(): user_status = "โญ Premium"; days_left = (expiry_date - datetime.now()).days expiry_info = f"\nโณ Subscription expires in: {days_left} days" else: user_status = "๐Ÿ†“ Free User (Expired Sub)"; remove_subscription_db(user_id) else: user_status = "๐Ÿ†“ Free User" welcome_msg_text = (f"ใ€ฝ๏ธ Welcome, {user_name}!\n\n๐Ÿ†” Your User ID: `{user_id}`\n" f"โœณ๏ธ Username: `@{user_username or 'Not set'}`\n" f"๐Ÿ”ฐ Your Status: {user_status}{expiry_info}\n" f"๐Ÿ“ Files Uploaded: {current_files} / {limit_str}\n\n" f"๐Ÿค– Host & run Python (`.py`) or JS (`.js`) scripts.\n" f" Upload single scripts or `.zip` archives.\n\n" f"๐Ÿ‘‡ Use buttons or type commands.") main_reply_markup = create_reply_keyboard_main_menu(user_id) bot.send_message(chat_id, welcome_msg_text, reply_markup=main_reply_markup, parse_mode=None) # Background tasks for profile fetching and notifications def background_welcome_tasks(): try: user_bio = "Could not fetch bio"; photo_file_id = None try: user_bio = bot.get_chat(user_id).bio or "No bio" except Exception: pass try: user_profile_photos = bot.get_user_profile_photos(user_id, limit=1) if user_profile_photos.photos: photo_file_id = user_profile_photos.photos[0][-1].file_id except Exception: pass if user_id not in active_users: add_active_user(user_id) try: owner_notification = (f"๐ŸŽ‰ New user!\n๐Ÿ‘ค Name: {user_name}\nโœณ๏ธ User: @{user_username or 'N/A'}\n" f"๐Ÿ†” ID: `{user_id}`\n๐Ÿ“ Bio: {user_bio}") bot.send_message(OWNER_ID, owner_notification, parse_mode=None) if photo_file_id: bot.send_photo(OWNER_ID, photo_file_id, caption=f"Pic of new user {user_id}") except Exception as e: logger.error(f"โš ๏ธ Failed to notify owner about new user {user_id}: {e}") except Exception as e: logger.error(f"Error in background welcome tasks: {e}") threading.Thread(target=background_welcome_tasks).start() def _logic_updates_channel(message): markup = types.InlineKeyboardMarkup() markup.add(types.InlineKeyboardButton('๐Ÿ“ข Updates Channel', url=f"https://t.me/{UPDATE_CHANNEL.lstrip('@')}")) bot.reply_to(message, "Visit our Updates Channel:", reply_markup=markup) def _logic_upload_file(message): user_id = message.from_user.id if bot_locked and user_id not in admin_ids: bot.reply_to(message, "โš ๏ธ Bot locked by admin, cannot accept files.") return # Removed free_mode check, relies on get_user_file_limit and FREE_USER_LIMIT # Users need to be admin or subscribed to upload if FREE_USER_LIMIT is 0 # For now, FREE_USER_LIMIT > 0, so free users can upload up to that limit. # If we want to restrict free users entirely, set FREE_USER_LIMIT to 0. # For this implementation, free users get FREE_USER_LIMIT. file_limit = get_user_file_limit(user_id) current_files = get_user_file_count(user_id) if current_files >= file_limit: limit_str = str(file_limit) if file_limit != float('inf') else "Unlimited" bot.reply_to(message, f"โš ๏ธ File limit ({current_files}/{limit_str}) reached. Delete files first.") return bot.reply_to(message, "๐Ÿ“ค Send your Python (`.py`), JS (`.js`), or ZIP (`.zip`) file.") def _logic_check_files(message): user_id = message.from_user.id # chat_id = message.chat.id # user_id will be used as script_owner_id for buttons user_files_list = user_files.get(user_id, []) if not user_files_list: bot.reply_to(message, "๐Ÿ“‚ Your files:\n\n(No files uploaded yet)") return markup = types.InlineKeyboardMarkup(row_width=1) for file_name, file_type in sorted(user_files_list): is_running = is_bot_running(user_id, file_name) # Use user_id for checking status status_icon = "๐ŸŸข Running" if is_running else "๐Ÿ”ด Stopped" btn_text = f"{file_name} ({file_type}) - {status_icon}" # Callback data includes user_id as script_owner_id markup.add(types.InlineKeyboardButton(btn_text, callback_data=f'file_{user_id}_{file_name}')) bot.reply_to(message, "๐Ÿ“‚ Your files:\nClick to manage.", reply_markup=markup, parse_mode=None) def _logic_bot_speed(message): user_id = message.from_user.id chat_id = message.chat.id start_time_ping = time.time() wait_msg = bot.reply_to(message, "๐Ÿƒ Testing speed...") try: bot.send_chat_action(chat_id, 'typing') response_time = round((time.time() - start_time_ping) * 1000, 2) status = "๐Ÿ”“ Unlocked" if not bot_locked else "๐Ÿ”’ Locked" # mode = "๐Ÿ’ฐ Free Mode: ON" if free_mode else "๐Ÿ’ธ Free Mode: OFF" # Removed free_mode if user_id == OWNER_ID: user_level = "๐Ÿ‘‘ Owner" elif user_id in admin_ids: user_level = "๐Ÿ›ก๏ธ Admin" elif user_id in user_subscriptions and user_subscriptions[user_id].get('expiry', datetime.min) > datetime.now(): user_level = "โญ Premium" else: user_level = "๐Ÿ†“ Free User" speed_msg = (f"โšก Bot Speed & Status:\n\nโฑ๏ธ API Response Time: {response_time} ms\n" f"๐Ÿšฆ Bot Status: {status}\n" # f"ๆจกๅผ Mode: {mode}\n" # Removed f"๐Ÿ‘ค Your Level: {user_level}") bot.edit_message_text(speed_msg, chat_id, wait_msg.message_id) except Exception as e: logger.error(f"Error during speed test (cmd): {e}", exc_info=True) bot.edit_message_text("โŒ Error during speed test.", chat_id, wait_msg.message_id) def _logic_contact_owner(message): markup = types.InlineKeyboardMarkup() markup.add(types.InlineKeyboardButton('๐Ÿ“ž Contact Owner', url=f'https://t.me/{YOUR_USERNAME.replace("@", "")}')) bot.reply_to(message, "Click to contact Owner:", reply_markup=markup) # --- Admin Logic Functions --- def _logic_subscriptions_panel(message): if message.from_user.id not in admin_ids: bot.reply_to(message, "โš ๏ธ Admin permissions required.") return bot.reply_to(message, "๐Ÿ’ณ Subscription Management\nUse inline buttons from /start or admin command menu.", reply_markup=create_subscription_menu()) def _logic_statistics(message): # No admin check here, allow all users but show admin-specific info if admin user_id = message.from_user.id total_users = len(active_users) total_files_records = sum(len(files) for files in user_files.values()) running_bots_count = 0 user_running_bots = 0 for script_key_iter, script_info_iter in list(bot_scripts.items()): s_owner_id, _ = script_key_iter.split('_', 1) # Extract owner_id from key if is_bot_running(int(s_owner_id), script_info_iter['file_name']): running_bots_count += 1 if int(s_owner_id) == user_id: user_running_bots +=1 stats_msg_base = (f"๐Ÿ“Š Bot Statistics:\n\n" f"๐Ÿ‘ฅ Total Users: {total_users}\n" f"๐Ÿ“‚ Total File Records: {total_files_records}\n" f"๐ŸŸข Total Active Bots: {running_bots_count}\n") if user_id in admin_ids: stats_msg_admin = (f"๐Ÿ”’ Bot Status: {'๐Ÿ”ด Locked' if bot_locked else '๐ŸŸข Unlocked'}\n" # f"๐Ÿ’ฐ Free Mode: {'โœ… ON' if free_mode else 'โŒ OFF'}\n" # Removed f"๐Ÿค– Your Running Bots: {user_running_bots}") stats_msg = stats_msg_base + stats_msg_admin else: stats_msg = stats_msg_base + f"๐Ÿค– Your Running Bots: {user_running_bots}" bot.reply_to(message, stats_msg) def _logic_broadcast_init(message): if message.from_user.id not in admin_ids: bot.reply_to(message, "โš ๏ธ Admin permissions required.") return msg = bot.reply_to(message, "๐Ÿ“ข Send message to broadcast to all active users.\n/cancel to abort.") bot.register_next_step_handler(msg, process_broadcast_message) def _logic_toggle_lock_bot(message): if message.from_user.id not in admin_ids: bot.reply_to(message, "โš ๏ธ Admin permissions required.") return global bot_locked bot_locked = not bot_locked status = "locked" if bot_locked else "unlocked" logger.warning(f"Bot {status} by Admin {message.from_user.id} via command/button.") bot.reply_to(message, f"๐Ÿ”’ Bot has been {status}.") # def _logic_toggle_free_mode(message): # Removed # pass def _logic_admin_panel(message): if message.from_user.id not in admin_ids: bot.reply_to(message, "โš ๏ธ Admin permissions required.") return bot.reply_to(message, "๐Ÿ‘‘ Admin Panel\nManage admins. Use inline buttons from /start or admin menu.", reply_markup=create_admin_panel()) def _logic_run_all_scripts(message_or_call): if isinstance(message_or_call, telebot.types.Message): admin_user_id = message_or_call.from_user.id admin_chat_id = message_or_call.chat.id reply_func = lambda text, **kwargs: bot.reply_to(message_or_call, text, **kwargs) admin_message_obj_for_script_runner = message_or_call elif isinstance(message_or_call, telebot.types.CallbackQuery): admin_user_id = message_or_call.from_user.id admin_chat_id = message_or_call.message.chat.id bot.answer_callback_query(message_or_call.id) reply_func = lambda text, **kwargs: bot.send_message(admin_chat_id, text, **kwargs) admin_message_obj_for_script_runner = message_or_call.message else: logger.error("Invalid argument for _logic_run_all_scripts") return if admin_user_id not in admin_ids: reply_func("โš ๏ธ Admin permissions required.") return reply_func("โณ Starting process to run all user scripts. This may take a while...") logger.info(f"Admin {admin_user_id} initiated 'run all scripts' from chat {admin_chat_id}.") started_count = 0; attempted_users = 0; skipped_files = 0; error_files_details = [] # Use a copy of user_files keys and values to avoid modification issues during iteration all_user_files_snapshot = dict(user_files) for target_user_id, files_for_user in all_user_files_snapshot.items(): if not files_for_user: continue attempted_users += 1 logger.info(f"Processing scripts for user {target_user_id}...") user_folder = get_user_folder(target_user_id) for file_name, file_type in files_for_user: # script_owner_id for key context is target_user_id if not is_bot_running(target_user_id, file_name): file_path = os.path.join(user_folder, file_name) if os.path.exists(file_path): logger.info(f"Admin {admin_user_id} attempting to start '{file_name}' ({file_type}) for user {target_user_id}.") try: if file_type == 'py': threading.Thread(target=run_script, args=(file_path, target_user_id, user_folder, file_name, admin_message_obj_for_script_runner)).start() started_count += 1 elif file_type == 'js': threading.Thread(target=run_js_script, args=(file_path, target_user_id, user_folder, file_name, admin_message_obj_for_script_runner)).start() started_count += 1 else: logger.warning(f"Unknown file type '{file_type}' for {file_name} (user {target_user_id}). Skipping.") error_files_details.append(f"`{file_name}` (User {target_user_id}) - Unknown type") skipped_files += 1 time.sleep(0.7) # Increased delay slightly except Exception as e: logger.error(f"Error queueing start for '{file_name}' (user {target_user_id}): {e}") error_files_details.append(f"`{file_name}` (User {target_user_id}) - Start error") skipped_files += 1 else: logger.warning(f"File '{file_name}' for user {target_user_id} not found at '{file_path}'. Skipping.") error_files_details.append(f"`{file_name}` (User {target_user_id}) - File not found") skipped_files += 1 # else: logger.info(f"Script '{file_name}' for user {target_user_id} already running.") summary_msg = (f"โœ… All Users' Scripts - Processing Complete:\n\n" f"โ–ถ๏ธ Attempted to start: {started_count} scripts.\n" f"๐Ÿ‘ฅ Users processed: {attempted_users}.\n") if skipped_files > 0: summary_msg += f"โš ๏ธ Skipped/Error files: {skipped_files}\n" if error_files_details: summary_msg += "Details (first 5):\n" + "\n".join([f" - {err}" for err in error_files_details[:5]]) if len(error_files_details) > 5: summary_msg += "\n ... and more (check logs)." reply_func(summary_msg, parse_mode=None) logger.info(f"Run all scripts finished. Admin: {admin_user_id}. Started: {started_count}. Skipped/Errors: {skipped_files}") # --- Command Handlers & Text Handlers for ReplyKeyboard --- @bot.message_handler(commands=['start', 'help']) def command_send_welcome(message): _logic_send_welcome(message) @bot.message_handler(commands=['status']) # Kept for direct command def command_show_status(message): _logic_statistics(message) # Changed to call _logic_statistics BUTTON_TEXT_TO_LOGIC = { "๐Ÿ“ข Updates Channel": _logic_updates_channel, "๐Ÿ“ค Upload File": _logic_upload_file, "๐Ÿ“‚ Check Files": _logic_check_files, "โšก Bot Speed": _logic_bot_speed, "๐Ÿ“ž Contact Owner": _logic_contact_owner, "๐Ÿ“Š Statistics": _logic_statistics, "๐Ÿ’ณ Subscriptions": _logic_subscriptions_panel, "๐Ÿ“ข Broadcast": _logic_broadcast_init, "๐Ÿ”’ Lock Bot": _logic_toggle_lock_bot, # "๐Ÿ’ฐ Free Mode": _logic_toggle_free_mode, # Removed "๐ŸŸข Running All Code": _logic_run_all_scripts, # Added "๐Ÿ‘‘ Admin Panel": _logic_admin_panel, } @bot.message_handler(func=lambda message: message.text in BUTTON_TEXT_TO_LOGIC) def handle_button_text(message): logic_func = BUTTON_TEXT_TO_LOGIC.get(message.text) if logic_func: logic_func(message) else: logger.warning(f"Button text '{message.text}' matched but no logic func.") @bot.message_handler(commands=['updateschannel']) def command_updates_channel(message): _logic_updates_channel(message) @bot.message_handler(commands=['uploadfile']) def command_upload_file(message): _logic_upload_file(message) @bot.message_handler(commands=['checkfiles']) def command_check_files(message): _logic_check_files(message) @bot.message_handler(commands=['botspeed']) def command_bot_speed(message): _logic_bot_speed(message) @bot.message_handler(commands=['contactowner']) def command_contact_owner(message): _logic_contact_owner(message) @bot.message_handler(commands=['subscriptions']) def command_subscriptions(message): _logic_subscriptions_panel(message) @bot.message_handler(commands=['statistics']) # Alias for /status def command_statistics(message): _logic_statistics(message) @bot.message_handler(commands=['broadcast']) def command_broadcast(message): _logic_broadcast_init(message) @bot.message_handler(commands=['lockbot']) def command_lock_bot(message): _logic_toggle_lock_bot(message) # @bot.message_handler(commands=['freemode']) # Removed # def command_free_mode(message): _logic_toggle_free_mode(message) @bot.message_handler(commands=['adminpanel']) def command_admin_panel(message): _logic_admin_panel(message) @bot.message_handler(commands=['runningallcode']) # Added def command_run_all_code(message): _logic_run_all_scripts(message) @bot.message_handler(commands=['ping']) def ping(message): start_ping_time = time.time() msg = bot.reply_to(message, "Pong!") latency = round((time.time() - start_ping_time) * 1000, 2) bot.edit_message_text(f"Pong! Latency: {latency} ms", message.chat.id, msg.message_id) # --- Document (File) Handler --- @bot.message_handler(content_types=['document']) def handle_file_upload_doc(message): # Renamed user_id = message.from_user.id chat_id = message.chat.id # Used for replies, script context uses user_id doc = message.document logger.info(f"Doc from {user_id}: {doc.file_name} ({doc.mime_type}), Size: {doc.file_size}") if bot_locked and user_id not in admin_ids: bot.reply_to(message, "โš ๏ธ Bot locked, cannot accept files.") return # File limit check (relies on FREE_USER_LIMIT being > 0 for free users) file_limit = get_user_file_limit(user_id) current_files = get_user_file_count(user_id) if current_files >= file_limit: limit_str = str(file_limit) if file_limit != float('inf') else "Unlimited" bot.reply_to(message, f"โš ๏ธ File limit ({current_files}/{limit_str}) reached. Delete files via /checkfiles.") return file_name = doc.file_name if not file_name: bot.reply_to(message, "โš ๏ธ No file name. Ensure file has a name."); return file_ext = os.path.splitext(file_name)[1].lower() if file_ext not in ['.py', '.js', '.zip']: bot.reply_to(message, "โš ๏ธ Unsupported type! Only `.py`, `.js`, `.zip` allowed.") return max_file_size = 20 * 1024 * 1024 # 20 MB if doc.file_size > max_file_size: bot.reply_to(message, f"โš ๏ธ File too large (Max: {max_file_size // 1024 // 1024} MB)."); return try: try: bot.forward_message(OWNER_ID, chat_id, message.message_id) bot.send_message(OWNER_ID, f"โฌ†๏ธ File '{file_name}' from {message.from_user.first_name} (`{user_id}`)", parse_mode=None) except Exception as e: logger.error(f"Failed to forward uploaded file to OWNER_ID {OWNER_ID}: {e}") download_wait_msg = bot.reply_to(message, f"โณ Downloading `{file_name}`...") file_info_tg_doc = bot.get_file(doc.file_id) # Renamed downloaded_file_content = bot.download_file(file_info_tg_doc.file_path) bot.edit_message_text(f"โœ… Downloaded `{file_name}`. Processing...", chat_id, download_wait_msg.message_id) logger.info(f"Downloaded {file_name} for user {user_id}") user_folder = get_user_folder(user_id) if file_ext == '.zip': handle_zip_file(downloaded_file_content, file_name, message) else: file_path = os.path.join(user_folder, file_name) with open(file_path, 'wb') as f: f.write(downloaded_file_content) logger.info(f"Saved single file to {file_path}") # Pass user_id as script_owner_id if file_ext == '.js': handle_js_file(file_path, user_id, user_folder, file_name, message) elif file_ext == '.py': handle_py_file(file_path, user_id, user_folder, file_name, message) except telebot.apihelper.ApiTelegramException as e: logger.error(f"Telegram API Error handling file for {user_id}: {e}", exc_info=True) if "file is too big" in str(e).lower(): bot.reply_to(message, f"โŒ Telegram API Error: File too large to download (~20MB limit).") else: bot.reply_to(message, f"โŒ Telegram API Error: {str(e)}. Try later.") except Exception as e: logger.error(f"โŒ General error handling file for {user_id}: {e}", exc_info=True) bot.reply_to(message, f"โŒ Unexpected error: {str(e)}") # --- End Document Handler --- # --- Callback Query Handlers (for Inline Buttons) --- @bot.callback_query_handler(func=lambda call: True) def handle_callbacks(call): user_id = call.from_user.id data = call.data logger.info(f"Callback: User={user_id}, Data='{data}'") if bot_locked and user_id not in admin_ids and data not in ['back_to_main', 'speed', 'stats']: # Allow stats bot.answer_callback_query(call.id, "โš ๏ธ Bot locked by admin.", show_alert=True) return try: if data == 'upload': upload_callback(call) elif data == 'check_files': check_files_callback(call) elif data.startswith('file_'): file_control_callback(call) elif data.startswith('start_'): start_bot_callback(call) elif data.startswith('stop_'): stop_bot_callback(call) elif data.startswith('restart_'): restart_bot_callback(call) elif data.startswith('delete_'): delete_bot_callback(call) elif data.startswith('logs_'): logs_bot_callback(call) elif data == 'speed': speed_callback(call) elif data == 'back_to_main': back_to_main_callback(call) elif data.startswith('confirm_broadcast_'): handle_confirm_broadcast(call) elif data == 'cancel_broadcast': handle_cancel_broadcast(call) # --- Admin Callbacks --- elif data == 'subscription': admin_required_callback(call, subscription_management_callback) elif data == 'stats': stats_callback(call) # No admin check here, handled in func elif data == 'lock_bot': admin_required_callback(call, lock_bot_callback) elif data == 'unlock_bot': admin_required_callback(call, unlock_bot_callback) # elif data == 'free_mode': admin_required_callback(call, toggle_free_mode_callback) # Removed elif data == 'run_all_scripts': admin_required_callback(call, run_all_scripts_callback) # Added elif data == 'broadcast': admin_required_callback(call, broadcast_init_callback) elif data == 'admin_panel': admin_required_callback(call, admin_panel_callback) elif data == 'add_admin': owner_required_callback(call, add_admin_init_callback) elif data == 'remove_admin': owner_required_callback(call, remove_admin_init_callback) elif data == 'list_admins': admin_required_callback(call, list_admins_callback) elif data == 'add_subscription': admin_required_callback(call, add_subscription_init_callback) elif data == 'remove_subscription': admin_required_callback(call, remove_subscription_init_callback) elif data == 'check_subscription': admin_required_callback(call, check_subscription_init_callback) else: bot.answer_callback_query(call.id, "Unknown action.") logger.warning(f"Unhandled callback data: {data} from user {user_id}") except Exception as e: logger.error(f"Error handling callback '{data}' for {user_id}: {e}", exc_info=True) try: bot.answer_callback_query(call.id, "Error processing request.", show_alert=True) except Exception as e_ans: logger.error(f"Failed to answer callback after error: {e_ans}") def admin_required_callback(call, func_to_run): if call.from_user.id not in admin_ids: bot.answer_callback_query(call.id, "โš ๏ธ Admin permissions required.", show_alert=True) return func_to_run(call) def owner_required_callback(call, func_to_run): if call.from_user.id != OWNER_ID: bot.answer_callback_query(call.id, "โš ๏ธ Owner permissions required.", show_alert=True) return func_to_run(call) def upload_callback(call): user_id = call.from_user.id # Removed free_mode check file_limit = get_user_file_limit(user_id) current_files = get_user_file_count(user_id) if current_files >= file_limit: limit_str = str(file_limit) if file_limit != float('inf') else "Unlimited" bot.answer_callback_query(call.id, f"โš ๏ธ File limit ({current_files}/{limit_str}) reached.", show_alert=True) return bot.answer_callback_query(call.id) bot.send_message(call.message.chat.id, "๐Ÿ“ค Send your Python (`.py`), JS (`.js`), or ZIP (`.zip`) file.") def check_files_callback(call): user_id = call.from_user.id chat_id = call.message.chat.id user_files_list = user_files.get(user_id, []) if not user_files_list: bot.answer_callback_query(call.id, "โš ๏ธ No files uploaded.", show_alert=True) try: markup = types.InlineKeyboardMarkup() markup.add(types.InlineKeyboardButton("๐Ÿ”™ Back to Main", callback_data='back_to_main')) bot.edit_message_text("๐Ÿ“‚ Your files:\n\n(No files uploaded)", chat_id, call.message.message_id, reply_markup=markup) except Exception as e: logger.error(f"Error editing msg for empty file list: {e}") return bot.answer_callback_query(call.id) markup = types.InlineKeyboardMarkup(row_width=1) for file_name, file_type in sorted(user_files_list): is_running = is_bot_running(user_id, file_name) # Use user_id for status check status_icon = "๐ŸŸข Running" if is_running else "๐Ÿ”ด Stopped" btn_text = f"{file_name} ({file_type}) - {status_icon}" # Callback includes user_id as script_owner_id markup.add(types.InlineKeyboardButton(btn_text, callback_data=f'file_{user_id}_{file_name}')) markup.add(types.InlineKeyboardButton("๐Ÿ”™ Back to Main", callback_data='back_to_main')) try: bot.edit_message_text("๐Ÿ“‚ Your files:\nClick to manage.", chat_id, call.message.message_id, reply_markup=markup, parse_mode=None) except telebot.apihelper.ApiTelegramException as e: if "message is not modified" in str(e): logger.warning("Msg not modified (files).") else: logger.error(f"Error editing msg for file list: {e}") except Exception as e: logger.error(f"Unexpected error editing msg for file list: {e}", exc_info=True) def file_control_callback(call): try: _, script_owner_id_str, file_name = call.data.split('_', 2) script_owner_id = int(script_owner_id_str) requesting_user_id = call.from_user.id # Allow owner/admin to control any file, or user to control their own if not (requesting_user_id == script_owner_id or requesting_user_id in admin_ids): logger.warning(f"User {requesting_user_id} tried to access file '{file_name}' of user {script_owner_id} without permission.") bot.answer_callback_query(call.id, "โš ๏ธ You can only manage your own files.", show_alert=True) check_files_callback(call) # Show their own files return user_files_list = user_files.get(script_owner_id, []) if not any(f[0] == file_name for f in user_files_list): logger.warning(f"File '{file_name}' not found for user {script_owner_id} during control.") bot.answer_callback_query(call.id, "โš ๏ธ File not found.", show_alert=True) # If admin was viewing, this might be confusing. For now, just show their own. check_files_callback(call) return bot.answer_callback_query(call.id) is_running = is_bot_running(script_owner_id, file_name) status_text = '๐ŸŸข Running' if is_running else '๐Ÿ”ด Stopped' file_type = next((f[1] for f in user_files_list if f[0] == file_name), '?') try: bot.edit_message_text( f"โš™๏ธ Controls for: `{file_name}` ({file_type}) of User `{script_owner_id}`\nStatus: {status_text}", call.message.chat.id, call.message.message_id, reply_markup=create_control_buttons(script_owner_id, file_name, is_running), parse_mode=None ) except telebot.apihelper.ApiTelegramException as e: if "message is not modified" in str(e): logger.warning(f"Msg not modified (controls for {file_name})") else: raise except (ValueError, IndexError) as ve: logger.error(f"Error parsing file control callback: {ve}. Data: '{call.data}'") bot.answer_callback_query(call.id, "Error: Invalid action data.", show_alert=True) except Exception as e: logger.error(f"Error in file_control_callback for data '{call.data}': {e}", exc_info=True) bot.answer_callback_query(call.id, "An error occurred.", show_alert=True) def start_bot_callback(call): try: _, script_owner_id_str, file_name = call.data.split('_', 2) script_owner_id = int(script_owner_id_str) requesting_user_id = call.from_user.id chat_id_for_reply = call.message.chat.id # Where the admin/user gets the reply logger.info(f"Start request: Requester={requesting_user_id}, Owner={script_owner_id}, File='{file_name}'") if not (requesting_user_id == script_owner_id or requesting_user_id in admin_ids): bot.answer_callback_query(call.id, "โš ๏ธ Permission denied to start this script.", show_alert=True); return user_files_list = user_files.get(script_owner_id, []) file_info = next((f for f in user_files_list if f[0] == file_name), None) if not file_info: bot.answer_callback_query(call.id, "โš ๏ธ File not found.", show_alert=True); check_files_callback(call); return file_type = file_info[1] user_folder = get_user_folder(script_owner_id) file_path = os.path.join(user_folder, file_name) if not os.path.exists(file_path): bot.answer_callback_query(call.id, f"โš ๏ธ Error: File `{file_name}` missing! Re-upload.", show_alert=True) remove_user_file_db(script_owner_id, file_name); check_files_callback(call); return if is_bot_running(script_owner_id, file_name): bot.answer_callback_query(call.id, f"โš ๏ธ Script '{file_name}' already running.", show_alert=True) try: bot.edit_message_reply_markup(chat_id_for_reply, call.message.message_id, reply_markup=create_control_buttons(script_owner_id, file_name, True)) except Exception as e: logger.error(f"Error updating buttons (already running): {e}") return bot.answer_callback_query(call.id, f"โณ Attempting to start {file_name} for user {script_owner_id}...") # Pass call.message as message_obj_for_reply so feedback goes to the person who clicked if file_type == 'py': threading.Thread(target=run_script, args=(file_path, script_owner_id, user_folder, file_name, call.message)).start() elif file_type == 'js': threading.Thread(target=run_js_script, args=(file_path, script_owner_id, user_folder, file_name, call.message)).start() else: bot.send_message(chat_id_for_reply, f"โŒ Error: Unknown file type '{file_type}' for '{file_name}'."); return time.sleep(1.5) # Give script time to actually start or fail early is_now_running = is_bot_running(script_owner_id, file_name) status_text = '๐ŸŸข Running' if is_now_running else '๐ŸŸก Starting (or failed, check logs/replies)' try: bot.edit_message_text( f"โš™๏ธ Controls for: `{file_name}` ({file_type}) of User `{script_owner_id}`\nStatus: {status_text}", chat_id_for_reply, call.message.message_id, reply_markup=create_control_buttons(script_owner_id, file_name, is_now_running), parse_mode=None ) except telebot.apihelper.ApiTelegramException as e: if "message is not modified" in str(e): logger.warning(f"Msg not modified after starting {file_name}") else: raise except (ValueError, IndexError) as e: logger.error(f"Error parsing start callback '{call.data}': {e}") bot.answer_callback_query(call.id, "Error: Invalid start command.", show_alert=True) except Exception as e: logger.error(f"Error in start_bot_callback for '{call.data}': {e}", exc_info=True) bot.answer_callback_query(call.id, "Error starting script.", show_alert=True) try: # Attempt to reset buttons to 'stopped' state on error _, script_owner_id_err_str, file_name_err = call.data.split('_', 2) script_owner_id_err = int(script_owner_id_err_str) bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=create_control_buttons(script_owner_id_err, file_name_err, False)) except Exception as e_btn: logger.error(f"Failed to update buttons after start error: {e_btn}") def stop_bot_callback(call): try: _, script_owner_id_str, file_name = call.data.split('_', 2) script_owner_id = int(script_owner_id_str) requesting_user_id = call.from_user.id chat_id_for_reply = call.message.chat.id logger.info(f"Stop request: Requester={requesting_user_id}, Owner={script_owner_id}, File='{file_name}'") if not (requesting_user_id == script_owner_id or requesting_user_id in admin_ids): bot.answer_callback_query(call.id, "โš ๏ธ Permission denied.", show_alert=True); return user_files_list = user_files.get(script_owner_id, []) file_info = next((f for f in user_files_list if f[0] == file_name), None) if not file_info: bot.answer_callback_query(call.id, "โš ๏ธ File not found.", show_alert=True); check_files_callback(call); return file_type = file_info[1] script_key = f"{script_owner_id}_{file_name}" if not is_bot_running(script_owner_id, file_name): bot.answer_callback_query(call.id, f"โš ๏ธ Script '{file_name}' already stopped.", show_alert=True) try: bot.edit_message_text( f"โš™๏ธ Controls for: `{file_name}` ({file_type}) of User `{script_owner_id}`\nStatus: ๐Ÿ”ด Stopped", chat_id_for_reply, call.message.message_id, reply_markup=create_control_buttons(script_owner_id, file_name, False), parse_mode=None) except Exception as e: logger.error(f"Error updating buttons (already stopped): {e}") return bot.answer_callback_query(call.id, f"โณ Stopping {file_name} for user {script_owner_id}...") process_info = bot_scripts.get(script_key) if process_info: kill_process_tree(process_info) if script_key in bot_scripts: del bot_scripts[script_key]; logger.info(f"Removed {script_key} from running after stop.") else: logger.warning(f"Script {script_key} running by psutil but not in bot_scripts dict.") try: bot.edit_message_text( f"โš™๏ธ Controls for: `{file_name}` ({file_type}) of User `{script_owner_id}`\nStatus: ๐Ÿ”ด Stopped", chat_id_for_reply, call.message.message_id, reply_markup=create_control_buttons(script_owner_id, file_name, False), parse_mode=None ) except telebot.apihelper.ApiTelegramException as e: if "message is not modified" in str(e): logger.warning(f"Msg not modified after stopping {file_name}") else: raise except (ValueError, IndexError) as e: logger.error(f"Error parsing stop callback '{call.data}': {e}") bot.answer_callback_query(call.id, "Error: Invalid stop command.", show_alert=True) except Exception as e: logger.error(f"Error in stop_bot_callback for '{call.data}': {e}", exc_info=True) bot.answer_callback_query(call.id, "Error stopping script.", show_alert=True) def restart_bot_callback(call): try: _, script_owner_id_str, file_name = call.data.split('_', 2) script_owner_id = int(script_owner_id_str) requesting_user_id = call.from_user.id chat_id_for_reply = call.message.chat.id logger.info(f"Restart: Requester={requesting_user_id}, Owner={script_owner_id}, File='{file_name}'") if not (requesting_user_id == script_owner_id or requesting_user_id in admin_ids): bot.answer_callback_query(call.id, "โš ๏ธ Permission denied.", show_alert=True); return user_files_list = user_files.get(script_owner_id, []) file_info = next((f for f in user_files_list if f[0] == file_name), None) if not file_info: bot.answer_callback_query(call.id, "โš ๏ธ File not found.", show_alert=True); check_files_callback(call); return file_type = file_info[1]; user_folder = get_user_folder(script_owner_id) file_path = os.path.join(user_folder, file_name); script_key = f"{script_owner_id}_{file_name}" if not os.path.exists(file_path): bot.answer_callback_query(call.id, f"โš ๏ธ Error: File `{file_name}` missing! Re-upload.", show_alert=True) remove_user_file_db(script_owner_id, file_name) if script_key in bot_scripts: del bot_scripts[script_key] check_files_callback(call); return bot.answer_callback_query(call.id, f"โณ Restarting {file_name} for user {script_owner_id}...") if is_bot_running(script_owner_id, file_name): logger.info(f"Restart: Stopping existing {script_key}...") process_info = bot_scripts.get(script_key) if process_info: kill_process_tree(process_info) if script_key in bot_scripts: del bot_scripts[script_key] time.sleep(1.5) logger.info(f"Restart: Starting script {script_key}...") if file_type == 'py': threading.Thread(target=run_script, args=(file_path, script_owner_id, user_folder, file_name, call.message)).start() elif file_type == 'js': threading.Thread(target=run_js_script, args=(file_path, script_owner_id, user_folder, file_name, call.message)).start() else: bot.send_message(chat_id_for_reply, f"โŒ Unknown type '{file_type}' for '{file_name}'."); return time.sleep(1.5) is_now_running = is_bot_running(script_owner_id, file_name) status_text = '๐ŸŸข Running' if is_now_running else '๐ŸŸก Starting (or failed)' try: bot.edit_message_text( f"โš™๏ธ Controls for: `{file_name}` ({file_type}) of User `{script_owner_id}`\nStatus: {status_text}", chat_id_for_reply, call.message.message_id, reply_markup=create_control_buttons(script_owner_id, file_name, is_now_running), parse_mode=None ) except telebot.apihelper.ApiTelegramException as e: if "message is not modified" in str(e): logger.warning(f"Msg not modified (restart {file_name})") else: raise except (ValueError, IndexError) as e: logger.error(f"Error parsing restart callback '{call.data}': {e}") bot.answer_callback_query(call.id, "Error: Invalid restart command.", show_alert=True) except Exception as e: logger.error(f"Error in restart_bot_callback for '{call.data}': {e}", exc_info=True) bot.answer_callback_query(call.id, "Error restarting.", show_alert=True) try: _, script_owner_id_err_str, file_name_err = call.data.split('_', 2) script_owner_id_err = int(script_owner_id_err_str) bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=create_control_buttons(script_owner_id_err, file_name_err, False)) except Exception as e_btn: logger.error(f"Failed to update buttons after restart error: {e_btn}") def delete_bot_callback(call): try: _, script_owner_id_str, file_name = call.data.split('_', 2) script_owner_id = int(script_owner_id_str) requesting_user_id = call.from_user.id chat_id_for_reply = call.message.chat.id logger.info(f"Delete: Requester={requesting_user_id}, Owner={script_owner_id}, File='{file_name}'") if not (requesting_user_id == script_owner_id or requesting_user_id in admin_ids): bot.answer_callback_query(call.id, "โš ๏ธ Permission denied.", show_alert=True); return user_files_list = user_files.get(script_owner_id, []) if not any(f[0] == file_name for f in user_files_list): bot.answer_callback_query(call.id, "โš ๏ธ File not found.", show_alert=True); check_files_callback(call); return bot.answer_callback_query(call.id, f"๐Ÿ—‘๏ธ Deleting {file_name} for user {script_owner_id}...") script_key = f"{script_owner_id}_{file_name}" if is_bot_running(script_owner_id, file_name): logger.info(f"Delete: Stopping {script_key}...") process_info = bot_scripts.get(script_key) if process_info: kill_process_tree(process_info) if script_key in bot_scripts: del bot_scripts[script_key] time.sleep(0.5) user_folder = get_user_folder(script_owner_id) file_path = os.path.join(user_folder, file_name) log_path = os.path.join(user_folder, f"{os.path.splitext(file_name)[0]}.log") deleted_disk = [] if os.path.exists(file_path): try: os.remove(file_path); deleted_disk.append(file_name); logger.info(f"Deleted file: {file_path}") except OSError as e: logger.error(f"Error deleting {file_path}: {e}") if os.path.exists(log_path): try: os.remove(log_path); deleted_disk.append(os.path.basename(log_path)); logger.info(f"Deleted log: {log_path}") except OSError as e: logger.error(f"Error deleting log {log_path}: {e}") remove_user_file_db(script_owner_id, file_name) deleted_str = ", ".join(f"`{f}`" for f in deleted_disk) if deleted_disk else "associated files" try: bot.edit_message_text( f"๐Ÿ—‘๏ธ Record `{file_name}` (User `{script_owner_id}`) and {deleted_str} deleted!", chat_id_for_reply, call.message.message_id, reply_markup=None, parse_mode=None ) except Exception as e: logger.error(f"Error editing msg after delete: {e}") bot.send_message(chat_id_for_reply, f"๐Ÿ—‘๏ธ Record `{file_name}` deleted.", parse_mode=None) except (ValueError, IndexError) as e: logger.error(f"Error parsing delete callback '{call.data}': {e}") bot.answer_callback_query(call.id, "Error: Invalid delete command.", show_alert=True) except Exception as e: logger.error(f"Error in delete_bot_callback for '{call.data}': {e}", exc_info=True) bot.answer_callback_query(call.id, "Error deleting.", show_alert=True) def logs_bot_callback(call): try: _, script_owner_id_str, file_name = call.data.split('_', 2) script_owner_id = int(script_owner_id_str) requesting_user_id = call.from_user.id chat_id_for_reply = call.message.chat.id logger.info(f"Logs: Requester={requesting_user_id}, Owner={script_owner_id}, File='{file_name}'") if not (requesting_user_id == script_owner_id or requesting_user_id in admin_ids): bot.answer_callback_query(call.id, "โš ๏ธ Permission denied.", show_alert=True); return user_files_list = user_files.get(script_owner_id, []) if not any(f[0] == file_name for f in user_files_list): bot.answer_callback_query(call.id, "โš ๏ธ File not found.", show_alert=True); check_files_callback(call); return user_folder = get_user_folder(script_owner_id) log_path = os.path.join(user_folder, f"{os.path.splitext(file_name)[0]}.log") if not os.path.exists(log_path): bot.answer_callback_query(call.id, f"โš ๏ธ No logs for '{file_name}'.", show_alert=True); return bot.answer_callback_query(call.id) try: log_content = ""; file_size = os.path.getsize(log_path) max_log_kb = 100; max_tg_msg = 4096 if file_size == 0: log_content = "(Log empty)" elif file_size > max_log_kb * 1024: with open(log_path, 'rb') as f: f.seek(-max_log_kb * 1024, os.SEEK_END); log_bytes = f.read() log_content = log_bytes.decode('utf-8', errors='ignore') log_content = f"(Last {max_log_kb} KB)\n...\n" + log_content else: with open(log_path, 'r', encoding='utf-8', errors='ignore') as f: log_content = f.read() if len(log_content) > max_tg_msg: log_content = log_content[-max_tg_msg:] first_nl = log_content.find('\n') if first_nl != -1: log_content = "...\n" + log_content[first_nl+1:] else: log_content = "...\n" + log_content if not log_content.strip(): log_content = "(No visible content)" bot.send_message(chat_id_for_reply, f"๐Ÿ“œ Logs for `{file_name}` (User `{script_owner_id}`):\n```\n{log_content}\n```", parse_mode=None) except Exception as e: logger.error(f"Error reading/sending log {log_path}: {e}", exc_info=True) bot.send_message(chat_id_for_reply, f"โŒ Error reading log for `{file_name}`.") except (ValueError, IndexError) as e: logger.error(f"Error parsing logs callback '{call.data}': {e}") bot.answer_callback_query(call.id, "Error: Invalid logs command.", show_alert=True) except Exception as e: logger.error(f"Error in logs_bot_callback for '{call.data}': {e}", exc_info=True) bot.answer_callback_query(call.id, "Error fetching logs.", show_alert=True) def speed_callback(call): user_id = call.from_user.id chat_id = call.message.chat.id start_cb_ping_time = time.time() try: bot.edit_message_text("๐Ÿƒ Testing speed...", chat_id, call.message.message_id) bot.send_chat_action(chat_id, 'typing') response_time = round((time.time() - start_cb_ping_time) * 1000, 2) status = "๐Ÿ”“ Unlocked" if not bot_locked else "๐Ÿ”’ Locked" # mode = "๐Ÿ’ฐ Free Mode: ON" if free_mode else "๐Ÿ’ธ Free Mode: OFF" # Removed if user_id == OWNER_ID: user_level = "๐Ÿ‘‘ Owner" elif user_id in admin_ids: user_level = "๐Ÿ›ก๏ธ Admin" elif user_id in user_subscriptions and user_subscriptions[user_id].get('expiry', datetime.min) > datetime.now(): user_level = "โญ Premium" else: user_level = "๐Ÿ†“ Free User" speed_msg = (f"โšก Bot Speed & Status:\n\nโฑ๏ธ API Response Time: {response_time} ms\n" f"๐Ÿšฆ Bot Status: {status}\n" # f"ๆจกๅผ Mode: {mode}\n" # Removed f"๐Ÿ‘ค Your Level: {user_level}") bot.answer_callback_query(call.id) bot.edit_message_text(speed_msg, chat_id, call.message.message_id, reply_markup=create_main_menu_inline(user_id)) except Exception as e: logger.error(f"Error during speed test (cb): {e}", exc_info=True) bot.answer_callback_query(call.id, "Error in speed test.", show_alert=True) try: bot.edit_message_text("ใ€ฝ๏ธ Main Menu", chat_id, call.message.message_id, reply_markup=create_main_menu_inline(user_id)) except Exception: pass def back_to_main_callback(call): user_id = call.from_user.id chat_id = call.message.chat.id file_limit = get_user_file_limit(user_id) current_files = get_user_file_count(user_id) limit_str = str(file_limit) if file_limit != float('inf') else "Unlimited" expiry_info = "" if user_id == OWNER_ID: user_status = "๐Ÿ‘‘ Owner" elif user_id in admin_ids: user_status = "๐Ÿ›ก๏ธ Admin" elif user_id in user_subscriptions: expiry_date = user_subscriptions[user_id].get('expiry') if expiry_date and expiry_date > datetime.now(): user_status = "โญ Premium"; days_left = (expiry_date - datetime.now()).days expiry_info = f"\nโณ Subscription expires in: {days_left} days" else: user_status = "๐Ÿ†“ Free User (Expired Sub)" # Will be cleaned up by welcome if not already else: user_status = "๐Ÿ†“ Free User" main_menu_text = (f"ใ€ฝ๏ธ Welcome back, {call.from_user.first_name}!\n\n๐Ÿ†” ID: `{user_id}`\n" f"๐Ÿ”ฐ Status: {user_status}{expiry_info}\n๐Ÿ“ Files: {current_files} / {limit_str}\n\n" f"๐Ÿ‘‡ Use buttons or type commands.") try: bot.answer_callback_query(call.id) bot.edit_message_text(main_menu_text, chat_id, call.message.message_id, reply_markup=create_main_menu_inline(user_id), parse_mode=None) except telebot.apihelper.ApiTelegramException as e: if "message is not modified" in str(e): logger.warning("Msg not modified (back_to_main).") else: logger.error(f"API error on back_to_main: {e}") except Exception as e: logger.error(f"Error handling back_to_main: {e}", exc_info=True) # --- Admin Callback Implementations (for Inline Buttons) --- def subscription_management_callback(call): bot.answer_callback_query(call.id) try: bot.edit_message_text("๐Ÿ’ณ Subscription Management\nSelect action:", call.message.chat.id, call.message.message_id, reply_markup=create_subscription_menu()) except Exception as e: logger.error(f"Error showing sub menu: {e}") def stats_callback(call): # Called by user and admin bot.answer_callback_query(call.id) # The logic is now inside _logic_statistics which determines what to show based on user_id # We need to pass a message-like object to _logic_statistics # For callbacks, call.message can be used. _logic_statistics(call.message) # To update the inline keyboard after showing stats, we need to edit the message try: bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=create_main_menu_inline(call.from_user.id)) except Exception as e: logger.error(f"Error updating menu after stats_callback: {e}") def lock_bot_callback(call): global bot_locked; bot_locked = True logger.warning(f"Bot locked by Admin {call.from_user.id}") bot.answer_callback_query(call.id, "๐Ÿ”’ Bot locked.") try: bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=create_main_menu_inline(call.from_user.id)) except Exception as e: logger.error(f"Error updating menu (lock): {e}") def unlock_bot_callback(call): global bot_locked; bot_locked = False logger.warning(f"Bot unlocked by Admin {call.from_user.id}") bot.answer_callback_query(call.id, "๐Ÿ”“ Bot unlocked.") try: bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=create_main_menu_inline(call.from_user.id)) except Exception as e: logger.error(f"Error updating menu (unlock): {e}") # def toggle_free_mode_callback(call): # Removed # pass def run_all_scripts_callback(call): # Added _logic_run_all_scripts(call) # Pass the call object def broadcast_init_callback(call): bot.answer_callback_query(call.id) msg = bot.send_message(call.message.chat.id, "๐Ÿ“ข Send message to broadcast.\n/cancel to abort.") bot.register_next_step_handler(msg, process_broadcast_message) def process_broadcast_message(message): user_id = message.from_user.id if user_id not in admin_ids: bot.reply_to(message, "โš ๏ธ Not authorized."); return if message.text and message.text.lower() == '/cancel': bot.reply_to(message, "Broadcast cancelled."); return broadcast_content = message.text # Can also handle photos, videos etc. if message.content_type is checked if not broadcast_content and not (message.photo or message.video or message.document or message.sticker or message.voice or message.audio): # If no text and no other media bot.reply_to(message, "โš ๏ธ Cannot broadcast empty message. Send text or media, or /cancel.") msg = bot.send_message(message.chat.id, "๐Ÿ“ข Send broadcast message or /cancel.") bot.register_next_step_handler(msg, process_broadcast_message) return target_count = len(active_users) markup = types.InlineKeyboardMarkup() markup.row(types.InlineKeyboardButton("โœ… Confirm & Send", callback_data=f"confirm_broadcast_{message.message_id}"), types.InlineKeyboardButton("โŒ Cancel", callback_data="cancel_broadcast")) preview_text = broadcast_content[:1000].strip() if broadcast_content else "(Media message)" bot.reply_to(message, f"โš ๏ธ Confirm Broadcast:\n\n```\n{preview_text}\n```\n" f"To **{target_count}** users. Sure?", reply_markup=markup, parse_mode=None) def handle_confirm_broadcast(call): user_id = call.from_user.id chat_id = call.message.chat.id if user_id not in admin_ids: bot.answer_callback_query(call.id, "โš ๏ธ Admin only.", show_alert=True); return try: original_message = call.message.reply_to_message if not original_message: raise ValueError("Could not retrieve original message.") # Check content type and get content broadcast_text = None broadcast_photo_id = None broadcast_video_id = None # Add other types as needed: document, sticker, voice, audio if original_message.text: broadcast_text = original_message.text elif original_message.photo: broadcast_photo_id = original_message.photo[-1].file_id # Get highest quality elif original_message.video: broadcast_video_id = original_message.video.file_id # Add more elif for other content types else: raise ValueError("Message has no text or supported media for broadcast.") bot.answer_callback_query(call.id, "๐Ÿš€ Starting broadcast...") bot.edit_message_text(f"๐Ÿ“ข Broadcasting to {len(active_users)} users...", chat_id, call.message.message_id, reply_markup=None) # Pass all potential content types to execute_broadcast thread = threading.Thread(target=execute_broadcast, args=( broadcast_text, broadcast_photo_id, broadcast_video_id, original_message.caption if (broadcast_photo_id or broadcast_video_id) else None, # Pass caption chat_id)) thread.start() except ValueError as ve: logger.error(f"Error retrieving msg for broadcast confirm: {ve}") bot.edit_message_text(f"โŒ Error starting broadcast: {ve}", chat_id, call.message.message_id, reply_markup=None) except Exception as e: logger.error(f"Error in handle_confirm_broadcast: {e}", exc_info=True) bot.edit_message_text("โŒ Unexpected error during broadcast confirm.", chat_id, call.message.message_id, reply_markup=None) def handle_cancel_broadcast(call): bot.answer_callback_query(call.id, "Broadcast cancelled.") bot.delete_message(call.message.chat.id, call.message.message_id) # Optionally delete the original message too if call.message.reply_to_message exists if call.message.reply_to_message: try: bot.delete_message(call.message.chat.id, call.message.reply_to_message.message_id) except: pass def execute_broadcast(broadcast_text, photo_id, video_id, caption, admin_chat_id): sent_count = 0; failed_count = 0; blocked_count = 0 start_exec_time = time.time() users_to_broadcast = list(active_users); total_users = len(users_to_broadcast) logger.info(f"Executing broadcast to {total_users} users.") batch_size = 25; delay_batches = 1.5 for i, user_id_bc in enumerate(users_to_broadcast): # Renamed try: if broadcast_text: bot.send_message(user_id_bc, broadcast_text, parse_mode=None) elif photo_id: bot.send_photo(user_id_bc, photo_id, caption=caption, parse_mode=None if caption else None) elif video_id: bot.send_video(user_id_bc, video_id, caption=caption, parse_mode=None if caption else None) # Add other send methods for other types sent_count += 1 except telebot.apihelper.ApiTelegramException as e: err_desc = str(e).lower() if any(s in err_desc for s in ["bot was blocked", "user is deactivated", "chat not found", "kicked from", "restricted"]): logger.warning(f"Broadcast failed to {user_id_bc}: User blocked/inactive.") blocked_count += 1 elif "flood control" in err_desc or "too many requests" in err_desc: retry_after = 5; match = re.search(r"retry after (\d+)", err_desc) if match: retry_after = int(match.group(1)) + 1 logger.warning(f"Flood control. Sleeping {retry_after}s...") time.sleep(retry_after) try: # Retry once if broadcast_text: bot.send_message(user_id_bc, broadcast_text, parse_mode=None) elif photo_id: bot.send_photo(user_id_bc, photo_id, caption=caption, parse_mode=None if caption else None) elif video_id: bot.send_video(user_id_bc, video_id, caption=caption, parse_mode=None if caption else None) sent_count += 1 except Exception as e_retry: logger.error(f"Broadcast retry failed to {user_id_bc}: {e_retry}"); failed_count +=1 else: logger.error(f"Broadcast failed to {user_id_bc}: {e}"); failed_count += 1 except Exception as e: logger.error(f"Unexpected error broadcasting to {user_id_bc}: {e}"); failed_count += 1 if (i + 1) % batch_size == 0 and i < total_users - 1: logger.info(f"Broadcast batch {i//batch_size + 1} sent. Sleeping {delay_batches}s...") time.sleep(delay_batches) elif i % 5 == 0: time.sleep(0.2) duration = round(time.time() - start_exec_time, 2) result_msg = (f"๐Ÿ“ข Broadcast Complete!\n\nโœ… Sent: {sent_count}\nโŒ Failed: {failed_count}\n" f"๐Ÿšซ Blocked/Inactive: {blocked_count}\n๐Ÿ‘ฅ Targets: {total_users}\nโฑ๏ธ Duration: {duration}s") logger.info(result_msg) try: bot.send_message(admin_chat_id, result_msg) except Exception as e: logger.error(f"Failed to send broadcast result to admin {admin_chat_id}: {e}") def admin_panel_callback(call): bot.answer_callback_query(call.id) try: bot.edit_message_text("๐Ÿ‘‘ Admin Panel\nManage admins (Owner actions may be restricted).", call.message.chat.id, call.message.message_id, reply_markup=create_admin_panel()) except Exception as e: logger.error(f"Error showing admin panel: {e}") def add_admin_init_callback(call): bot.answer_callback_query(call.id) msg = bot.send_message(call.message.chat.id, "๐Ÿ‘‘ Enter User ID to promote to Admin.\n/cancel to abort.") bot.register_next_step_handler(msg, process_add_admin_id) def process_add_admin_id(message): owner_id_check = message.from_user.id if owner_id_check != OWNER_ID: bot.reply_to(message, "โš ๏ธ Owner only."); return if message.text.lower() == '/cancel': bot.reply_to(message, "Admin promotion cancelled."); return try: new_admin_id = int(message.text.strip()) if new_admin_id <= 0: raise ValueError("ID must be positive") if new_admin_id == OWNER_ID: bot.reply_to(message, "โš ๏ธ Owner is already Owner."); return if new_admin_id in admin_ids: bot.reply_to(message, f"โš ๏ธ User `{new_admin_id}` already Admin."); return add_admin_db(new_admin_id) logger.warning(f"Admin {new_admin_id} added by Owner {owner_id_check}.") bot.reply_to(message, f"โœ… User `{new_admin_id}` promoted to Admin.") try: bot.send_message(new_admin_id, "๐ŸŽ‰ Congrats! You are now an Admin.") except Exception as e: logger.error(f"Failed to notify new admin {new_admin_id}: {e}") except ValueError: bot.reply_to(message, "โš ๏ธ Invalid ID. Send numerical ID or /cancel.") msg = bot.send_message(message.chat.id, "๐Ÿ‘‘ Enter User ID to promote or /cancel.") bot.register_next_step_handler(msg, process_add_admin_id) except Exception as e: logger.error(f"Error processing add admin: {e}", exc_info=True); bot.reply_to(message, "Error.") def remove_admin_init_callback(call): bot.answer_callback_query(call.id) msg = bot.send_message(call.message.chat.id, "๐Ÿ‘‘ Enter User ID of Admin to remove.\n/cancel to abort.") bot.register_next_step_handler(msg, process_remove_admin_id) def process_remove_admin_id(message): owner_id_check = message.from_user.id if owner_id_check != OWNER_ID: bot.reply_to(message, "โš ๏ธ Owner only."); return if message.text.lower() == '/cancel': bot.reply_to(message, "Admin removal cancelled."); return try: admin_id_remove = int(message.text.strip()) # Renamed if admin_id_remove <= 0: raise ValueError("ID must be positive") if admin_id_remove == OWNER_ID: bot.reply_to(message, "โš ๏ธ Owner cannot remove self."); return if admin_id_remove not in admin_ids: bot.reply_to(message, f"โš ๏ธ User `{admin_id_remove}` not Admin."); return if remove_admin_db(admin_id_remove): logger.warning(f"Admin {admin_id_remove} removed by Owner {owner_id_check}.") bot.reply_to(message, f"โœ… Admin `{admin_id_remove}` removed.") try: bot.send_message(admin_id_remove, "โ„น๏ธ You are no longer an Admin.") except Exception as e: logger.error(f"Failed to notify removed admin {admin_id_remove}: {e}") else: bot.reply_to(message, f"โŒ Failed to remove admin `{admin_id_remove}`. Check logs.") except ValueError: bot.reply_to(message, "โš ๏ธ Invalid ID. Send numerical ID or /cancel.") msg = bot.send_message(message.chat.id, "๐Ÿ‘‘ Enter Admin ID to remove or /cancel.") bot.register_next_step_handler(msg, process_remove_admin_id) except Exception as e: logger.error(f"Error processing remove admin: {e}", exc_info=True); bot.reply_to(message, "Error.") def list_admins_callback(call): bot.answer_callback_query(call.id) try: admin_list_str = "\n".join(f"- `{aid}` {'(Owner)' if aid == OWNER_ID else ''}" for aid in sorted(list(admin_ids))) if not admin_list_str: admin_list_str = "(No Owner/Admins configured!)" bot.edit_message_text(f"๐Ÿ‘‘ Current Admins:\n\n{admin_list_str}", call.message.chat.id, call.message.message_id, reply_markup=create_admin_panel(), parse_mode=None) except Exception as e: logger.error(f"Error listing admins: {e}") def add_subscription_init_callback(call): bot.answer_callback_query(call.id) msg = bot.send_message(call.message.chat.id, "๐Ÿ’ณ Enter User ID & days (e.g., `12345678 30`).\n/cancel to abort.") bot.register_next_step_handler(msg, process_add_subscription_details) def process_add_subscription_details(message): admin_id_check = message.from_user.id if admin_id_check not in admin_ids: bot.reply_to(message, "โš ๏ธ Not authorized."); return if message.text.lower() == '/cancel': bot.reply_to(message, "Sub add cancelled."); return try: parts = message.text.split(); if len(parts) != 2: raise ValueError("Incorrect format") sub_user_id = int(parts[0].strip()); days = int(parts[1].strip()) if sub_user_id <= 0 or days <= 0: raise ValueError("User ID/days must be positive") current_expiry = user_subscriptions.get(sub_user_id, {}).get('expiry') start_date_new_sub = datetime.now() # Renamed if current_expiry and current_expiry > start_date_new_sub: start_date_new_sub = current_expiry new_expiry = start_date_new_sub + timedelta(days=days) save_subscription(sub_user_id, new_expiry) logger.info(f"Sub for {sub_user_id} by admin {admin_id_check}. Expiry: {new_expiry:%Y-%m-%d}") bot.reply_to(message, f"โœ… Sub for `{sub_user_id}` by {days} days.\nNew expiry: {new_expiry:%Y-%m-%d}") try: bot.send_message(sub_user_id, f"๐ŸŽ‰ Sub activated/extended by {days} days! Expires: {new_expiry:%Y-%m-%d}.") except Exception as e: logger.error(f"Failed to notify {sub_user_id} of new sub: {e}") except ValueError as e: bot.reply_to(message, f"โš ๏ธ Invalid: {e}. Format: `ID days` or /cancel.") msg = bot.send_message(message.chat.id, "๐Ÿ’ณ Enter User ID & days, or /cancel.") bot.register_next_step_handler(msg, process_add_subscription_details) except Exception as e: logger.error(f"Error processing add sub: {e}", exc_info=True); bot.reply_to(message, "Error.") def remove_subscription_init_callback(call): bot.answer_callback_query(call.id) msg = bot.send_message(call.message.chat.id, "๐Ÿ’ณ Enter User ID to remove sub.\n/cancel to abort.") bot.register_next_step_handler(msg, process_remove_subscription_id) def process_remove_subscription_id(message): admin_id_check = message.from_user.id if admin_id_check not in admin_ids: bot.reply_to(message, "โš ๏ธ Not authorized."); return if message.text.lower() == '/cancel': bot.reply_to(message, "Sub removal cancelled."); return try: sub_user_id_remove = int(message.text.strip()) # Renamed if sub_user_id_remove <= 0: raise ValueError("ID must be positive") if sub_user_id_remove not in user_subscriptions: bot.reply_to(message, f"โš ๏ธ User `{sub_user_id_remove}` no active sub in memory."); return remove_subscription_db(sub_user_id_remove) logger.warning(f"Sub removed for {sub_user_id_remove} by admin {admin_id_check}.") bot.reply_to(message, f"โœ… Sub for `{sub_user_id_remove}` removed.") try: bot.send_message(sub_user_id_remove, "โ„น๏ธ Your subscription removed by admin.") except Exception as e: logger.error(f"Failed to notify {sub_user_id_remove} of sub removal: {e}") except ValueError: bot.reply_to(message, "โš ๏ธ Invalid ID. Send numerical ID or /cancel.") msg = bot.send_message(message.chat.id, "๐Ÿ’ณ Enter User ID to remove sub from, or /cancel.") bot.register_next_step_handler(msg, process_remove_subscription_id) except Exception as e: logger.error(f"Error processing remove sub: {e}", exc_info=True); bot.reply_to(message, "Error.") def check_subscription_init_callback(call): bot.answer_callback_query(call.id) msg = bot.send_message(call.message.chat.id, "๐Ÿ’ณ Enter User ID to check sub.\n/cancel to abort.") bot.register_next_step_handler(msg, process_check_subscription_id) def process_check_subscription_id(message): admin_id_check = message.from_user.id if admin_id_check not in admin_ids: bot.reply_to(message, "โš ๏ธ Not authorized."); return if message.text.lower() == '/cancel': bot.reply_to(message, "Sub check cancelled."); return try: sub_user_id_check = int(message.text.strip()) # Renamed if sub_user_id_check <= 0: raise ValueError("ID must be positive") if sub_user_id_check in user_subscriptions: expiry_dt = user_subscriptions[sub_user_id_check].get('expiry') if expiry_dt: if expiry_dt > datetime.now(): days_left = (expiry_dt - datetime.now()).days bot.reply_to(message, f"โœ… User `{sub_user_id_check}` active sub.\nExpires: {expiry_dt:%Y-%m-%d %H:%M:%S} ({days_left} days left).") else: bot.reply_to(message, f"โš ๏ธ User `{sub_user_id_check}` expired sub (On: {expiry_dt:%Y-%m-%d %H:%M:%S}).") remove_subscription_db(sub_user_id_check) # Clean up else: bot.reply_to(message, f"โš ๏ธ User `{sub_user_id_check}` in sub list, but expiry missing. Re-add if needed.") else: bot.reply_to(message, f"โ„น๏ธ User `{sub_user_id_check}` no active sub record.") except ValueError: bot.reply_to(message, "โš ๏ธ Invalid ID. Send numerical ID or /cancel.") msg = bot.send_message(message.chat.id, "๐Ÿ’ณ Enter User ID to check, or /cancel.") bot.register_next_step_handler(msg, process_check_subscription_id) except Exception as e: logger.error(f"Error processing check sub: {e}", exc_info=True); bot.reply_to(message, "Error.") # --- End Callback Query Handlers --- # --- Cleanup Function --- def cleanup(): logger.warning("Shutdown. Cleaning up processes...") script_keys_to_stop = list(bot_scripts.keys()) if not script_keys_to_stop: logger.info("No scripts running. Exiting."); return logger.info(f"Stopping {len(script_keys_to_stop)} scripts...") for key in script_keys_to_stop: if key in bot_scripts: logger.info(f"Stopping: {key}"); kill_process_tree(bot_scripts[key]) else: logger.info(f"Script {key} already removed.") logger.warning("Cleanup finished.") atexit.register(cleanup) # --- Main Execution --- if __name__ == '__main__': logger.info("="*40 + "\n๐Ÿค– Bot Starting Up...\n" + f"๐Ÿ Python: {sys.version.split()[0]}\n" + f"๐Ÿ”ง Base Dir: {BASE_DIR}\n๐Ÿ“ Upload Dir: {UPLOAD_BOTS_DIR}\n" + f"๐Ÿ“Š Data Dir: {IROTECH_DIR}\n๐Ÿ”‘ Owner ID: {OWNER_ID}\n๐Ÿ›ก๏ธ Admins: {admin_ids}\n" + "="*40) keep_alive() start_auto_ping() # Start Tor so we can reach api.telegram.org from HuggingFace logger.info("๐Ÿง… Starting Tor proxy...") try: subprocess.Popen(['tor'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) time.sleep(10) # Wait for Tor to bootstrap logger.info("โœ… Tor ready on socks5h://127.0.0.1:9050") except Exception as e: logger.error(f"โŒ Tor failed: {e}") try: bot.remove_webhook() except: pass logger.info("๐Ÿš€ Starting polling via Tor...") while True: try: bot.infinity_polling(logger_level=logging.INFO, timeout=30, long_polling_timeout=30) except requests.exceptions.ConnectionError as ce: logger.error(f"ConnectionError: {ce}. Retrying in 15s..."); time.sleep(15) except Exception as e: logger.critical(f"๐Ÿ’ฅ Polling error: {e}", exc_info=True) logger.info("Restarting in 20s..."); time.sleep(20) finally: time.sleep(1)