import os import json import re import shutil import socket import hashlib import subprocess import threading import time import sys import signal import logging import requests # import dns_fix import psutil from flask import Flask, send_from_directory, request, jsonify, redirect, session from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.middleware.proxy_fix import ProxyFix from functools import wraps from collections import defaultdict ai_client = None def get_ai_client(): global ai_client if ai_client is not None: return ai_client try: from ollamafreeapi import OllamaFreeAPI ai_client = OllamaFreeAPI() return ai_client except Exception: return None logging.basicConfig( level=logging.INFO, format='[%(asctime)s] %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) logger = logging.getLogger('hostbot') BASE_DIR = os.path.dirname(os.path.abspath(__file__)) USERS_ROOT = os.path.join(BASE_DIR, "USERS") DATA_DIR = os.path.join(BASE_DIR, "DATA") USERS_DB = os.path.join(DATA_DIR, "users.json") os.makedirs(USERS_ROOT, exist_ok=True) os.makedirs(DATA_DIR, exist_ok=True) app = Flask(__name__) app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1) app.secret_key = os.environ.get("PANEL_SECRET_KEY") or os.urandom(24) app.config.update( SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SAMESITE='Strict', SESSION_COOKIE_SECURE=True, PERMANENT_SESSION_LIFETIME=604800, MAX_CONTENT_LENGTH=50 * 1024 * 1024 ) _rate_store = defaultdict(list) _rate_lock = threading.Lock() RATE_LIMIT = 120 RATE_WINDOW = 60 def rate_limited(fn): @wraps(fn) def wrapper(*args, **kwargs): ip = request.remote_addr or '0.0.0.0' now = time.time() with _rate_lock: _rate_store[ip] = [t for t in _rate_store[ip] if now - t < RATE_WINDOW] if len(_rate_store[ip]) >= RATE_LIMIT: return jsonify({"success": False, "message": "Rate limit exceeded"}), 429 _rate_store[ip].append(now) return fn(*args, **kwargs) return wrapper @app.after_request def add_security_headers(response): response.headers['X-Content-Type-Options'] = 'nosniff' response.headers['X-XSS-Protection'] = '1; mode=block' response.headers['X-Frame-Options'] = 'DENY' response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' response.headers['Content-Security-Policy'] = "default-src 'self' 'unsafe-inline' 'unsafe-eval' https://fonts.googleapis.com https://fonts.gstatic.com https://f.top4top.io; object-src 'none';" response.headers['Referrer-Policy'] = 'strict-origin-when-downgrade' return response ADMIN_USERNAME = os.environ.get("ADMIN_USER", "moh777") # If the environment variable is not set, we use the default password, # but it's better to store a hash or use a secret key. # For maximum privacy, we will allow the admin password to be an environment variable. ADMIN_PASSWORD = os.environ.get("ADMIN_PASS", "Mm@123456") MAX_PROCESS_MEMORY_MB = int(os.environ.get("MAX_PROC_MEM_MB", 512)) MAX_LOG_SIZE = 2 * 1024 * 1024 running_procs = {} server_states = {} lock = threading.Lock() def get_ip(): try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("10.255.255.255", 1)) ip = s.getsockname()[0] s.close() return ip except Exception: return "127.0.0.1" def sanitize_folder_name(name: str) -> str: name = (name or "").strip() name = re.sub(r"\s+", "-", name) name = re.sub(r"[^A-Za-z0-9\-_\.]", "", name) return name[:200] def safe_name(name: str) -> str: name = (name or "").strip() name = re.sub(r"[\\/]+", "", name) name = re.sub(r"[^A-Za-z0-9\-_\. ]", "", name) return name[:200].strip() def set_state(key: str, state: str): with lock: server_states[key] = state def get_state(key: str) -> str: with lock: return server_states.get(key, "Offline") def log_append(key: str, text: str): try: owner, folder = parse_server_key(key, allow_admin=True) p = os.path.join(get_server_dir(owner, folder), "server.log") with open(p, "a", encoding="utf-8", errors="ignore") as f: f.write(text) except Exception: pass def load_users(): if not os.path.exists(USERS_DB): return {"users": []} try: with open(USERS_DB, "r", encoding="utf-8") as f: return json.load(f) or {"users": []} except Exception: return {"users": []} def save_users(db): tmp = USERS_DB + ".tmp" with open(tmp, "w", encoding="utf-8") as f: json.dump(db, f, indent=2) os.replace(tmp, USERS_DB) def find_user(db, username: str): u = (username or "").strip().lower() for x in db.get("users", []): if (x.get("username") or "").strip().lower() == u: return x return None def is_admin_session(): u = session.get("user") or {} return bool(u.get("is_admin")) def current_username(): u = session.get("user") or {} return (u.get("username") or "").strip() PLANS = { "free": {"label": "Free Tier", "max_bots": 1, "max_mem_mb": 256, "max_disk_mb": 100}, "pro": {"label": "Silver Tier", "max_bots": 3, "max_mem_mb": 512, "max_disk_mb": 1000}, "enterprise": {"label": "Gold Tier", "max_bots": 10, "max_mem_mb": 1024, "max_disk_mb": 5000}, } # --- ADS SYSTEM --- ADS_FILE = os.path.join(DATA_DIR, "ads.json") def load_ads(): if not os.path.exists(ADS_FILE): return {"current_ad": "", "contact_link": "https://t.me/j49_c"} try: with open(ADS_FILE, "r", encoding="utf-8") as f: return json.load(f) except: return {"current_ad": "", "contact_link": "https://t.me/j49_c"} def save_ads(data): with open(ADS_FILE, "w", encoding="utf-8") as f: json.dump(data, f, indent=2) BOT_RENEW_DAYS = 4 AI_DAILY_LIMIT = 20 def get_user_plan(username: str) -> dict: db = load_users() u = find_user(db, username) if not u: return PLANS["free"] plan_name = u.get("plan", "free") return PLANS.get(plan_name, PLANS["free"]) def get_user_limit(username: str) -> int: if is_admin_session(): return 999999 plan = get_user_plan(username) return plan["max_bots"] def get_user_mem_limit(username: str) -> int: if is_admin_session(): return MAX_PROCESS_MEMORY_MB plan = get_user_plan(username) return plan["max_mem_mb"] def get_user_disk_limit(username: str) -> int: if is_admin_session(): return 999999 plan = get_user_plan(username) return plan.get("max_disk_mb", 50) def get_dir_size(path='.'): total = 0 with os.scandir(path) as it: for entry in it: if entry.is_file() and not entry.is_symlink(): total += entry.stat().st_size elif entry.is_dir() and not entry.is_symlink(): total += get_dir_size(entry.path) return total def login_required(fn): @wraps(fn) def wrapper(*args, **kwargs): if not session.get("user"): return redirect("/login") return fn(*args, **kwargs) return wrapper def admin_required(fn): @wraps(fn) def wrapper(*args, **kwargs): if not session.get("user"): return redirect("/login") if not is_admin_session(): return jsonify({"success": False, "message": "Admin only"}), 403 return fn(*args, **kwargs) return wrapper def get_user_servers_root(username: str) -> str: return os.path.join(USERS_ROOT, username, "servers") def get_server_dir(owner: str, folder: str) -> str: return os.path.join(get_user_servers_root(owner), folder) def ensure_user_dirs(username: str): os.makedirs(get_user_servers_root(username), exist_ok=True) def parse_server_key(key: str, allow_admin: bool): key = (key or "").strip() if "::" in key: owner, folder = key.split("::", 1) owner = owner.strip() folder = folder.strip() if not allow_admin: raise ValueError("not allowed") if not is_admin_session(): raise ValueError("forbidden") return owner, folder return current_username(), key def can_access_key(key: str) -> bool: try: owner, folder = parse_server_key(key, allow_admin=True) except Exception: return False if is_admin_session(): return True return owner == current_username() def safe_join_server_path(key: str, rel_path: str = "") -> str: owner, folder = parse_server_key(key, allow_admin=True) root = os.path.abspath(get_server_dir(owner, folder)) rel_path = (rel_path or "").replace("\\", "/").strip() if rel_path.startswith("/") or rel_path.startswith("~"): rel_path = rel_path.lstrip("/").lstrip("~") joined = os.path.abspath(os.path.join(root, rel_path)) if not (joined == root or joined.startswith(root + os.sep)): raise ValueError("Invalid path") return joined def ensure_meta(owner: str, folder: str): server_dir = get_server_dir(owner, folder) os.makedirs(server_dir, exist_ok=True) meta_path = os.path.join(server_dir, "meta.json") base = { "display_name": folder, "startup_file": "", "owner": owner, "banned": False, "last_renewed": time.time() # Bot needs renewal every 4 days } if not os.path.exists(meta_path): with open(meta_path, "w", encoding="utf-8") as f: json.dump(base, f, indent=2) else: try: with open(meta_path, "r", encoding="utf-8") as f: m = json.load(f) or {} except Exception: m = {} changed = False for k, v in base.items(): if k not in m: m[k] = v changed = True if m.get("owner") != owner: m["owner"] = owner changed = True if changed: with open(meta_path, "w", encoding="utf-8") as f: json.dump(m, f, indent=2) return meta_path def read_meta(owner: str, folder: str): meta_path = ensure_meta(owner, folder) try: with open(meta_path, "r", encoding="utf-8") as f: return json.load(f) or {} except Exception: return {"display_name": folder, "startup_file": "", "owner": owner, "banned": False} def write_meta(owner: str, folder: str, meta): meta_path = ensure_meta(owner, folder) with open(meta_path, "w", encoding="utf-8") as f: json.dump(meta, f, indent=2) def sha256_file(path): h = hashlib.sha256() with open(path, "rb") as f: for chunk in iter(lambda: f.read(1024 * 1024), b""): h.update(chunk) return h.hexdigest() def installed_file_path(owner: str, folder: str): return os.path.join(get_server_dir(owner, folder), ".installed") def read_installed(owner: str, folder: str): p = installed_file_path(owner, folder) data = {"req_sha": "", "pkgs": set()} if not os.path.exists(p): return data try: with open(p, "r", encoding="utf-8", errors="ignore") as f: for line in f.read().splitlines(): line = line.strip() if not line: continue if line.startswith("REQ_SHA="): data["req_sha"] = line.split("=", 1)[1].strip() else: data["pkgs"].add(line) except Exception: pass return data def write_installed(owner: str, folder: str, req_sha=None, add_pkgs=None): p = installed_file_path(owner, folder) cur = read_installed(owner, folder) if req_sha is not None: cur["req_sha"] = req_sha if add_pkgs: cur["pkgs"].update(add_pkgs) lines = [] if cur["req_sha"]: lines.append(f"REQ_SHA={cur['req_sha']}") lines.extend(sorted(cur["pkgs"])) with open(p, "w", encoding="utf-8") as f: f.write("\n".join(lines) + ("\n" if lines else "")) def ensure_requirements_installed(owner: str, folder: str): server_dir = get_server_dir(owner, folder) req_path = os.path.join(server_dir, "requirements.txt") if not os.path.exists(req_path): return False req_sha = sha256_file(req_path) cur = read_installed(owner, folder) if cur["req_sha"] == req_sha: return False log_append(f"{owner}::{folder}", "[SYSTEM] Installing requirements.txt...\n") try: subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], cwd=server_dir) write_installed(owner, folder, req_sha=req_sha) log_append(f"{owner}::{folder}", "[SYSTEM] requirements installed ✅\n") return True except subprocess.CalledProcessError as e: log_append(f"{owner}::{folder}", f"[SYSTEM] requirements install failed: {e}\n") return False def get_subprocess_env(): env = os.environ.copy() env['PYTHONUNBUFFERED'] = '1' env['PYTHONDONTWRITEBYTECODE'] = '1' return env def start_with_autoinstall(owner: str, folder: str, startup_file: str): dns_fix_path = os.path.join(BASE_DIR, 'dns_fix.py') wrapper_code = f''' import sys, os sys.path.insert(0, "{BASE_DIR}") try: import dns_fix except: pass import runpy, subprocess, traceback, re script = sys.argv[1] cwd = os.getcwd() def append_installed(pkg): try: p = os.path.join(cwd, ".installed") existing = set() if os.path.exists(p): with open(p, "r", encoding="utf-8", errors="ignore") as f: existing = set([x.strip() for x in f.read().splitlines() if x.strip()]) if pkg and pkg not in existing: with open(p, "a", encoding="utf-8") as f: f.write(pkg + "\\n") except: pass def parse_missing_name(e): n = getattr(e, "name", None) if n: return n s = str(e) m = re.search(r"No module named \'([^\']+)\'", s) if m: return m.group(1) return None while True: try: runpy.run_path(script, run_name="__main__") break except ModuleNotFoundError as e: pkg = parse_missing_name(e) if not pkg: traceback.print_exc() break print(f"[AUTO-INSTALL] Missing module: {{pkg}} -> installing...") try: subprocess.check_call([sys.executable, "-m", "pip", "install", pkg]) append_installed(pkg) print(f"[AUTO-INSTALL] Installed: {{pkg}} -> restarting...") continue except Exception as ex: print(f"[AUTO-INSTALL] Failed: {{ex}}") traceback.print_exc() break except Exception: traceback.print_exc() break ''' server_dir = get_server_dir(owner, folder) log_path = os.path.join(server_dir, "server.log") log_file = open(log_path, "a", encoding="utf-8", errors="ignore") if startup_file.lower().endswith(".php"): proc = subprocess.Popen( ["php", startup_file], cwd=server_dir, stdout=log_file, stderr=log_file, env=get_subprocess_env(), ) else: proc = subprocess.Popen( [sys.executable, "-u", "-c", wrapper_code, startup_file], cwd=server_dir, stdout=log_file, stderr=log_file, env=get_subprocess_env(), ) return proc, log_file def stop_proc(key: str): if key in running_procs: proc, logf = running_procs[key] try: p = psutil.Process(proc.pid) for child in p.children(recursive=True): child.kill() p.kill() except Exception: pass try: logf.close() except Exception: pass running_procs.pop(key, None) @app.route("/") def home(): if not session.get("user"): return send_from_directory(BASE_DIR, "landing.html") return redirect("/dashboard") @app.route("/dashboard") @login_required def dashboard(): return send_from_directory(BASE_DIR, "index.html") @app.route("/developer") def developer_page(): dev_url = os.environ.get("DEVELOPER_URL") if dev_url: return redirect(dev_url) return send_from_directory(BASE_DIR, "developer.html") @app.route("/terms") def terms_page(): return send_from_directory(BASE_DIR, "terms.html") @app.route("/privacy") def privacy_page(): return send_from_directory(BASE_DIR, "privacy.html") @app.route("/features-info") def features_page(): return send_from_directory(BASE_DIR, "features.html") @app.route("/docs") def docs_page(): return send_from_directory(BASE_DIR, "docs.html") @app.route("/login") def login_page(): return send_from_directory(BASE_DIR, "login.html") @app.route("/create") def create_page(): return send_from_directory(BASE_DIR, "create.html") @app.route("/admin") @login_required def admin_page(): if not is_admin_session(): return redirect("/") return send_from_directory(BASE_DIR, "admin.html") @app.route("/logout") def logout(): session.pop("user", None) return redirect("/login") @app.route("/api/auth/login", methods=["POST"]) @rate_limited def api_login(): data = request.get_json(silent=True) or {} username = (data.get("username") or "").strip() password = data.get("password") or "" if username == ADMIN_USERNAME and password == ADMIN_PASSWORD: session["user"] = {"username": ADMIN_USERNAME, "is_admin": True} session.permanent = True return jsonify({"success": True, "is_admin": True}) db = load_users() u = find_user(db, username) if not u: return jsonify({"success": False, "message": "Invalid username or password"}), 401 if not u.get("active", True): return jsonify({"success": False, "message": "حسابك معلق أوبانتظار موافقة الإدارة (Pending/Banned)"}), 403 if not check_password_hash(u.get("password_hash", ""), password): return jsonify({"success": False, "message": "Invalid username or password"}), 401 session["user"] = {"username": u.get("username"), "is_admin": False} session.permanent = True ensure_user_dirs(u.get("username")) return jsonify({"success": True, "is_admin": False}) import smtplib DISPOSABLE_DOMAINS = [ "tempmail.com", "10minutemail.com", "guerrillamail.com", "mailinator.com", "yopmail.com", "dropmail.me", "mohmal.com", "maildrop.cc", "dispostable.com", "temp-mail.org", "nada.ltd" ] def get_client_ip(): if request.headers.get('X-Forwarded-For'): return request.headers.get('X-Forwarded-For').split(',')[0].strip() return request.remote_addr or "" # OTP SYSTEM REMOVED @app.route("/api/auth/register-otp", methods=["POST"]) @rate_limited def api_register_otp(): data = request.get_json(silent=True) or {} username = (data.get("username") or "").strip() password = data.get("password") or "" email = (data.get("email") or "").strip() # Email validation removed - site fully open # if not email or '@' not in email: # return jsonify({"success": False, "message": "البريد الإلكتروني غير صالح"}), 400 # Disable disposable domain check to make it easy # if domain in DISPOSABLE_DOMAINS: # return jsonify({"success": False, "message": "عذراً، لا يتم قبول خوادم البريد الوهمية / المؤقتة."}), 400 if len(username) < 3 or len(username) > 20: return jsonify({"success": False, "message": "Username must be 3-20 characters"}), 400 if not re.match(r'^[A-Za-z0-9_]+$', username): return jsonify({"success": False, "message": "Username: letters, numbers, underscore only"}), 400 if len(password) < 6: return jsonify({"success": False, "message": "Password must be at least 6 characters"}), 400 if username.lower() == ADMIN_USERNAME.lower(): return jsonify({"success": False, "message": "Username not available"}), 400 db = load_users() if find_user(db, username): return jsonify({"success": False, "message": "Username already taken"}), 409 # Email duplicate check removed - site fully open # for u in db.get("users", []): # if u.get("email") == email: # return jsonify({"success": False, "message": "Email already in use"}), 409 client_ip = get_client_ip() new_user = { "username": username, "email": email or "no-email", "password_hash": generate_password_hash(password), "active": True, "plan": "free", "ip": client_ip, "created_at": time.strftime("%Y-%m-%d %H:%M:%S") } db.setdefault("users", []).append(new_user) save_users(db) logger.info(f"New user registered: {username} from IP: {client_ip}") session["user"] = {"username": username, "is_admin": False} session.permanent = True ensure_user_dirs(username) return jsonify({"success": True, "skip_otp": True, "message": "تم إنشاء حسابك بنجاح! جاري تحويلك للوحة التحكم..."}), 200 # Legacy route removed @app.route("/api/user/profile") @login_required def api_user_profile(): username = current_username() db = load_users() u = find_user(db, username) plan_name = "admin" if is_admin_session() else (u.get("plan", "free") if u else "free") plan_info = PLANS.get(plan_name, PLANS["free"]) return jsonify({ "success": True, "username": username, "is_admin": is_admin_session(), "plan": plan_name, "plan_label": plan_info.get("label", plan_name.title()), "max_bots": get_user_limit(username), "max_mem_mb": get_user_mem_limit(username) }) @app.route("/api/admin/users") @admin_required def api_admin_users(): db = load_users() users = [] for u in db.get("users", []): users.append({ "username": u.get("username"), "active": u.get("active", True), "plan": u.get("plan", "free"), "created_at": u.get("created_at", "unknown") }) return jsonify({"success": True, "users": users}) @app.route("/api/admin/user/set-plan", methods=["POST"]) @admin_required def api_admin_set_plan(): data = request.get_json(silent=True) or {} username = (data.get("username") or "").strip() plan = (data.get("plan") or "free").strip() if plan not in PLANS: return jsonify({"success": False, "message": "Invalid plan"}), 400 db = load_users() u = find_user(db, username) if not u: return jsonify({"success": False, "message": "User not found"}), 404 u["plan"] = plan save_users(db) return jsonify({"success": True}) @app.route("/api/admin/user/toggle-active", methods=["POST"]) @admin_required def api_admin_toggle_active(): data = request.get_json(silent=True) or {} username = (data.get("username") or "").strip() db = load_users() u = find_user(db, username) if not u: return jsonify({"success": False, "message": "User not found"}), 404 u["active"] = not u.get("active", True) save_users(db) return jsonify({"success": True, "active": u["active"]}) @app.route("/api/admin/user/delete", methods=["POST"]) @admin_required def api_admin_delete_user(): data = request.get_json(silent=True) or {} username = (data.get("username") or "").strip() db = load_users() u = find_user(db, username) if not u: return jsonify({"success": False, "message": "User not found"}), 404 db["users"] = [x for x in db.get("users", []) if (x.get("username") or "").strip().lower() != username.lower()] save_users(db) return jsonify({"success": True}) def list_all_servers_for_admin(): servers = [] if not os.path.isdir(USERS_ROOT): return servers for owner in sorted(os.listdir(USERS_ROOT)): root = get_user_servers_root(owner) if not os.path.isdir(root): continue for folder in sorted(os.listdir(root)): server_dir = get_server_dir(owner, folder) if not os.path.isdir(server_dir): continue meta = read_meta(owner, folder) banned = bool(meta.get("banned", False)) key = f"{owner}::{folder}" st = "Banned" if banned else get_state(key) servers.append({ "title": meta.get("display_name", folder), "folder": folder, "owner": owner, "key": key, "subtitle": f"Owner: {owner}", "startup_file": meta.get("startup_file", ""), "status": st, "banned": banned }) return servers def list_servers_for_user(username: str): ensure_user_dirs(username) root = get_user_servers_root(username) servers = [] for folder in sorted(os.listdir(root)): server_dir = get_server_dir(username, folder) if not os.path.isdir(server_dir): continue meta = read_meta(username, folder) banned = bool(meta.get("banned", False)) key = folder st = "Banned" if banned else get_state(key) servers.append({ "title": meta.get("display_name", folder), "folder": folder, "owner": username, "key": key, "subtitle": f"Owner: {username}", "startup_file": meta.get("startup_file", ""), "status": st, "banned": banned }) return servers @app.route("/servers") @login_required def servers(): if is_admin_session(): return jsonify({"success": True, "servers": list_all_servers_for_admin()}) return jsonify({"success": True, "servers": list_servers_for_user(current_username())}) @app.route("/add", methods=["POST"]) @login_required @rate_limited def add_server(): data = request.get_json(silent=True) or {} name = (data.get("name") or "").strip() folder = sanitize_folder_name(name) if not folder: return jsonify({"success": False, "message": "Invalid server name"}), 400 if is_admin_session(): owner = current_username() else: owner = current_username() ensure_user_dirs(owner) if not is_admin_session(): limit = get_user_limit(owner) existing = [d for d in os.listdir(get_user_servers_root(owner)) if os.path.isdir(get_server_dir(owner, d))] if len(existing) >= limit: return jsonify({"success": False, "message": f"لقد وصلت للحد الأقصى ({limit}). تواصل مع الدعم لزيادة السعة مجاناً."}), 403 target = get_server_dir(owner, folder) if os.path.exists(target): return jsonify({"success": False, "message": "Server already exists"}), 409 os.makedirs(target, exist_ok=True) open(os.path.join(target, "server.log"), "w", encoding="utf-8").close() meta = { "display_name": name or folder, "startup_file": "", "owner": owner, "banned": False } write_meta(owner, folder, meta) set_state(folder if not is_admin_session() else f"{owner}::{folder}", "Offline") if is_admin_session(): return jsonify({"success": True, "servers": list_all_servers_for_admin()}) return jsonify({"success": True, "servers": list_servers_for_user(owner)}) @app.route("/server/stats/") @login_required def server_stats(key): if not can_access_key(key): return jsonify({"success": False, "message": "Forbidden"}), 403 owner, folder = parse_server_key(key, allow_admin=True) server_dir = get_server_dir(owner, folder) if not os.path.isdir(server_dir): return jsonify({"status": "Offline", "cpu": "0%", "mem": "0 MB", "logs": "", "ip": get_ip()}), 404 meta = read_meta(owner, folder) if meta.get("banned", False): set_state(key, "Banned") proc_tuple = running_procs.get(key) running = False cpu, mem = "0%", "0 MB" if proc_tuple: proc, _logf = proc_tuple if psutil.pid_exists(proc.pid): try: p = psutil.Process(proc.pid) if p.is_running() and p.status() != psutil.STATUS_ZOMBIE: running = True cpu = f"{p.cpu_percent(interval=None)}%" mem = f"{p.memory_info().rss / 1024 / 1024:.1f} MB" except Exception: pass log_path = os.path.join(server_dir, "server.log") try: logs = open(log_path, "r", encoding="utf-8", errors="ignore").read() if os.path.exists(log_path) else "" except Exception: logs = "" state = get_state(key) if meta.get("banned", False): state = "Banned" elif running: state = "Running" set_state(key, "Running") elif state not in ("Installing", "Starting"): state = "Offline" set_state(key, "Offline") return jsonify({"status": state, "cpu": cpu, "mem": mem, "logs": logs, "ip": get_ip()}) def background_start(key: str, owner: str, folder: str, startup_file: str): try: set_state(key, "Installing") log_append(key, "[SYSTEM] Preparing...\n") ensure_requirements_installed(owner, folder) set_state(key, "Starting") log_append(key, "[SYSTEM] Starting...\n") proc, logf = start_with_autoinstall(owner, folder, startup_file) running_procs[key] = (proc, logf) time.sleep(1.0) if proc.poll() is None: set_state(key, "Running") else: set_state(key, "Offline") except Exception as e: log_append(key, f"[SYSTEM] Start failed: {e}\n") set_state(key, "Offline") @app.route("/server/action//", methods=["POST"]) @login_required @rate_limited def server_action(key, act): if not can_access_key(key): return jsonify({"success": False, "message": "Forbidden"}), 403 owner, folder = parse_server_key(key, allow_admin=True) server_dir = get_server_dir(owner, folder) if not os.path.isdir(server_dir): return jsonify({"success": False, "message": "Server not found"}), 404 meta = read_meta(owner, folder) if meta.get("banned", False): set_state(key, "Banned") return jsonify({"success": False, "message": "Server is banned by admin"}), 403 # RENEWAL CHECK days_passed = (time.time() - meta.get("last_renewed", 0)) / (24 * 3600) if not is_admin_session() and days_passed > BOT_RENEW_DAYS: set_state(key, "Offline") return jsonify({"success": False, "expired": True, "message": "Bot expired. Please reactivate it from the dashboard."}), 403 if act in ("stop", "restart"): stop_proc(key) set_state(key, "Offline") if act == "stop": return jsonify({"success": True}) startup = meta.get("startup_file") or "" if not startup: return jsonify({"success": False, "message": "No main file set"}), 400 open(os.path.join(server_dir, "server.log"), "w", encoding="utf-8").close() t = threading.Thread(target=background_start, args=(key, owner, folder, startup), daemon=True) t.start() return jsonify({"success": True}) @app.route("/server/set-startup/", methods=["POST"]) @login_required def set_startup(key): if not can_access_key(key): return jsonify({"success": False, "message": "Forbidden"}), 403 owner, folder = parse_server_key(key, allow_admin=True) server_dir = get_server_dir(owner, folder) if not os.path.isdir(server_dir): return jsonify({"success": False, "message": "Server not found"}), 404 data = request.get_json(silent=True) or {} f = (data.get("file") or "").strip() meta = read_meta(owner, folder) meta["startup_file"] = f write_meta(owner, folder, meta) return jsonify({"success": True}) @app.route("/server/delete/", methods=["POST"]) @login_required @rate_limited def server_delete(key): if not can_access_key(key): return jsonify({"success": False, "message": "Forbidden"}), 403 stop_proc(key) owner, folder = parse_server_key(key, allow_admin=True) server_dir = get_server_dir(owner, folder) if os.path.isdir(server_dir): shutil.rmtree(server_dir) return jsonify({"success": True}) @app.route("/files/list/") @login_required def files_list(key): if not can_access_key(key): return jsonify({"success": False, "message": "Forbidden", "path": ""}), 403 rel = request.args.get("path", "") or "" try: base = safe_join_server_path(key, rel) except Exception: return jsonify({"success": False, "message": "Invalid path", "path": ""}), 400 dirs, files = [], [] if os.path.isdir(base): for name in sorted(os.listdir(base), key=lambda x: (not os.path.isdir(os.path.join(base, x)), x.lower())): if rel == "" and name in ("meta.json", "server.log"): continue full = os.path.join(base, name) if os.path.isdir(full): dirs.append({"name": name}) elif os.path.isfile(full): try: size_kb = os.path.getsize(full) / 1024 size = f"{size_kb:.1f} KB" except Exception: size = "" files.append({"name": name, "size": size}) return jsonify({"success": True, "path": rel, "dirs": dirs, "files": files}) @app.route("/files/content/") @login_required def file_content(key): if not can_access_key(key): return jsonify({"content": ""}), 403 file_rel = request.args.get("file", "") or "" try: full = safe_join_server_path(key, file_rel) except Exception: return jsonify({"content": ""}), 400 if os.path.isdir(full): return jsonify({"content": ""}), 400 try: with open(full, "r", encoding="utf-8", errors="ignore") as f: return jsonify({"content": f.read()}) except Exception: return jsonify({"content": ""}) @app.route("/files/save/", methods=["POST"]) @login_required @rate_limited def file_save(key): if not can_access_key(key): return jsonify({"success": False, "message": "Forbidden"}), 403 data = request.get_json(silent=True) or {} file_rel = data.get("file", "") or "" content = data.get("content", "") try: full = safe_join_server_path(key, file_rel) except Exception: return jsonify({"success": False, "message": "Invalid path"}), 400 owner, folder = parse_server_key(key, allow_admin=True) server_dir = get_server_dir(owner, folder) current_size_mb = get_dir_size(server_dir) / 1024 / 1024 disk_limit_mb = get_user_disk_limit(owner) # We estimate size based on length of content if current_size_mb + (len(content.encode('utf-8')) / 1024 / 1024) > disk_limit_mb: return jsonify({"success": False, "message": f"Disk limit exceeded ({disk_limit_mb}MB max)"}), 403 os.makedirs(os.path.dirname(full), exist_ok=True) try: with open(full, "w", encoding="utf-8") as f: f.write(content) return jsonify({"success": True}) except Exception as e: return jsonify({"success": False, "message": str(e)}), 500 @app.route("/files/mkdir/", methods=["POST"]) @login_required def file_mkdir(key): if not can_access_key(key): return jsonify({"success": False, "message": "Forbidden"}), 403 data = request.get_json(silent=True) or {} rel = data.get("path", "") or "" name = safe_name(data.get("name", "")) if not name: return jsonify({"success": False, "message": "Bad name"}), 400 try: target = safe_join_server_path(key, os.path.join(rel, name)) os.makedirs(target, exist_ok=False) return jsonify({"success": True}) except FileExistsError: return jsonify({"success": False, "message": "Already exists"}), 409 except Exception as e: return jsonify({"success": False, "message": str(e)}), 500 @app.route("/files/rename/", methods=["POST"]) @login_required def file_rename(key): if not can_access_key(key): return jsonify({"success": False, "message": "Forbidden"}), 403 data = request.get_json(silent=True) or {} rel = data.get("path", "") or "" old = safe_name(data.get("old", "")) new = safe_name(data.get("new", "")) if not old or not new: return jsonify({"success": False, "message": "Bad name"}), 400 try: src = safe_join_server_path(key, os.path.join(rel, old)) dst = safe_join_server_path(key, os.path.join(rel, new)) os.rename(src, dst) return jsonify({"success": True}) except Exception as e: return jsonify({"success": False, "message": str(e)}), 500 @app.route("/files/delete/", methods=["POST"]) @login_required def file_delete(key): if not can_access_key(key): return jsonify({"success": False, "message": "Forbidden"}), 403 data = request.get_json(silent=True) or {} rel = data.get("path", "") or "" name = safe_name(data.get("name", "")) kind = (data.get("kind") or "file").lower() if not name: return jsonify({"success": False, "message": "Bad name"}), 400 try: target = safe_join_server_path(key, os.path.join(rel, name)) if kind == "dir": shutil.rmtree(target) else: os.remove(target) return jsonify({"success": True}) except Exception as e: return jsonify({"success": False, "message": str(e)}), 500 @app.route("/files/upload/", methods=["POST"]) @login_required @rate_limited def file_upload(key): if not can_access_key(key): return jsonify({"success": False, "message": "Forbidden"}), 403 rel = request.args.get("path", "") or "" try: base_dir = safe_join_server_path(key, rel) except Exception: return jsonify({"success": False, "message": "Invalid path"}), 400 os.makedirs(base_dir, exist_ok=True) owner, folder = parse_server_key(key, allow_admin=True) server_dir = get_server_dir(owner, folder) current_size_mb = get_dir_size(server_dir) / 1024 / 1024 disk_limit_mb = get_user_disk_limit(owner) files = request.files.getlist("files") or [] if not files: one = request.files.get("file") if one: files = [one] if not files: return jsonify({"success": False, "message": "No file"}), 400 relpaths = request.form.getlist("relpaths") saved = 0 for i, f in enumerate(files): if not f or not f.filename: continue filename = os.path.basename(f.filename) rp = "" if relpaths and i < len(relpaths): rp = (relpaths[i] or "").replace("\\", "/").lstrip("/") try: if rp: target_dir = safe_join_server_path(key, os.path.join(rel, os.path.dirname(rp))) else: target_dir = base_dir except Exception: continue f.seek(0, os.SEEK_END) file_length = f.tell() f.seek(0) if current_size_mb + (file_length / 1024 / 1024) > disk_limit_mb: continue current_size_mb += file_length / 1024 / 1024 os.makedirs(target_dir, exist_ok=True) f.save(os.path.join(target_dir, filename)) saved += 1 return jsonify({"success": True, "saved": saved}) @app.route("/api/admin/servers") @admin_required def admin_servers(): return jsonify({"success": True, "servers": list_all_servers_for_admin()}) @app.route("/api/admin/server/ban", methods=["POST"]) @admin_required def admin_server_ban(): data = request.get_json(silent=True) or {} key = (data.get("key") or "").strip() banned = bool(data.get("banned", True)) owner, folder = parse_server_key(key, allow_admin=True) server_dir = get_server_dir(owner, folder) if not os.path.isdir(server_dir): return jsonify({"success": False, "message": "Server not found"}), 404 meta = read_meta(owner, folder) meta["banned"] = banned write_meta(owner, folder, meta) if banned: stop_proc(key) set_state(key, "Banned") log_append(key, "[ADMIN] Server banned.\n") else: set_state(key, "Offline") log_append(key, "[ADMIN] Server unbanned.\n") return jsonify({"success": True}) @app.route("/api/ai/chat", methods=["POST"]) @login_required @rate_limited def api_ai_chat(): client = get_ai_client() if client is None: return jsonify({"success": False, "message": "AI service currently unavailable"}), 503 username = current_username() db = load_users() u = find_user(db, username) if not u and not is_admin_session(): return jsonify({"success": False, "message": "User not found"}), 404 # AI LIMITS now_date = time.strftime("%Y-%m-%d") if u: ai_status = u.setdefault("ai_usage", {"date": "", "count": 0}) else: # Admin or special user not in DB, create temporary status ai_status = {"date": now_date, "count": 0} if ai_status["date"] != now_date: ai_status["date"] = now_date ai_status["count"] = 0 if not is_admin_session() and ai_status["count"] >= AI_DAILY_LIMIT: return jsonify({"success": False, "message": f"Daily limit reached ({AI_DAILY_LIMIT} requests). Please try again tomorrow."}), 429 data = request.get_json(silent=True) or {} message = data.get("message", "").strip() model = data.get("model", "deepseek-r1:latest") if not message: return jsonify({"success": False, "message": "No message provided"}), 400 SYSTEM_PROMPT = "You are White Wolf AI, a specialized assistant for the White Wolf Bot Hosting platform. " \ "Help users with their bots, code, and platform issues. Be helpful and expert." try: # Integrating system prompt into the user's prompt for simple API call full_prompt = f"System: {SYSTEM_PROMPT}\nUser: {message}" response = client.chat(model=model, prompt=full_prompt) ai_status["count"] += 1 save_users(db) return jsonify({"success": True, "response": response}) except Exception as e: logger.error(f"AI Error: {e}") return jsonify({"success": False, "message": str(e)}), 500 @app.route("/api/ai/models") @login_required def api_ai_models(): return jsonify({ "success": True, "models": ["llama3.2:3b", "deepseek-r1:latest", "mistral:latest", "gpt-oss:20b"] }) @app.route("/api/ads") def api_get_ads(): return jsonify(load_ads()) @app.route("/api/server/renew/", methods=["POST"]) @login_required def api_renew_server(key): if not can_access_key(key): return jsonify({"success": False, "message": "Forbidden"}), 403 owner, folder = parse_server_key(key, allow_admin=True) meta = read_meta(owner, folder) meta["last_renewed"] = time.time() write_meta(owner, folder, meta) return jsonify({"success": True}) @app.route("/api/admin/quickstats") @admin_required def admin_quickstats(): total_servers = 0 running = 0 installing = 0 banned = 0 for s in list_all_servers_for_admin(): total_servers += 1 if s.get("status") == "Banned": banned += 1 elif s.get("status") == "Running": running += 1 elif s.get("status") in ("Installing", "Starting"): installing += 1 db = load_users() total_users = len(db.get("users", [])) active_users = sum(1 for u in db.get("users", []) if u.get("active", True)) premium_users = sum(1 for u in db.get("users", []) if u.get("premium", False)) return jsonify({"success": True, "stats": { "servers_total": total_servers, "servers_running": running, "servers_installing": installing, "servers_banned": banned, "users_total": total_users, "users_active": active_users, "users_premium": premium_users }}) def run_keep_alive(): port = int(os.environ.get("SERVER_PORT", 30170)) url = f"http://127.0.0.1:{port}/health" time.sleep(15) # Wait for server to boot while True: try: requests.get(url, timeout=10) with lock: for key in list(running_procs.keys()): proc, logf = running_procs[key] if not psutil.pid_exists(proc.pid) or proc.poll() is not None: logger.info(f"Process dead for '{key}', cleaning up") try: logf.close() except Exception: pass running_procs.pop(key, None) server_states[key] = "Offline" else: try: p = psutil.Process(proc.pid) mem_mb = p.memory_info().rss / 1024 / 1024 if mem_mb > MAX_PROCESS_MEMORY_MB: logger.warning(f"Process '{key}' using {mem_mb:.0f}MB, killing") for child in p.children(recursive=True): child.kill() p.kill() running_procs.pop(key, None) server_states[key] = "Offline" log_append(key, f"[SYSTEM] Killed: memory limit exceeded ({mem_mb:.0f}MB)\n") except Exception: pass for key, state in list(server_states.items()): if state == "Running" and key not in running_procs: logger.info(f"Auto-Recovery: restarting '{key}'") try: owner, folder = key.split("::", 1) if "::" in key else (ADMIN_USERNAME, key) meta = read_meta(owner, folder) startup = meta.get("startup_file") if startup and not meta.get("banned", False): t = threading.Thread(target=background_start, args=(key, owner, folder, startup), daemon=True) t.start() else: server_states[key] = "Offline" except Exception as e: logger.error(f"Recovery failed for '{key}': {e}") server_states[key] = "Offline" except Exception as e: logger.error(f"Keep-Alive Error: {e}") time.sleep(300) def run_git_sync(): if os.environ.get("SPACE_ID"): logger.info("Git Auto-Sync disabled inside Hugging Face Space (to prevent loops)") return logger.info("Git Auto-Sync system started") while True: try: cwd = os.getcwd() # Stage changes subprocess.run(["git", "add", "."], cwd=cwd, capture_output=True) # Commit with timestamp msg = f"Auto-Sync: {time.strftime('%Y-%m-%d %H:%M:%S')}" subprocess.run(["git", "commit", "-m", msg], cwd=cwd, capture_output=True) # Push to GitHub subprocess.run(["git", "push", "origin", "main"], cwd=cwd, capture_output=True) # Push to Hugging Face subprocess.run(["git", "push", "hf", "main"], cwd=cwd, capture_output=True) logger.info("Auto-Sync: Platform synced with GitHub & Hugging Face") except Exception as e: logger.error(f"Auto-Sync Error: {e}") time.sleep(3600) # Sync every hour def truncate_large_logs(): if not os.path.isdir(USERS_ROOT): return for owner in os.listdir(USERS_ROOT): root = get_user_servers_root(owner) if not os.path.isdir(root): continue for folder in os.listdir(root): log_path = os.path.join(get_server_dir(owner, folder), "server.log") try: if os.path.exists(log_path) and os.path.getsize(log_path) > MAX_LOG_SIZE: with open(log_path, 'r', encoding='utf-8', errors='ignore') as f: f.seek(os.path.getsize(log_path) - MAX_LOG_SIZE // 2) content = f.read() with open(log_path, 'w', encoding='utf-8') as f: f.write("[SYSTEM] Log truncated\n" + content) except Exception: pass def run_log_cleaner(): while True: time.sleep(600) truncate_large_logs() cleanup_old_data() def cleanup_old_data(): """حذف البيانات القديمة والغير نشطة""" try: db = load_db() current_time = time.time() # حذف السيرفرات غير النشطة لأكثر من 30 يوم if "servers" in db: active_servers = [] for server in db["servers"]: # حذف السيرفرات غير النشطة لأكثر من 30 يوم if server.get("last_renewed") and (current_time - server["last_renewed"]) > 30 * 24 * 3600: server_folder = os.path.join(USERS_ROOT, server.get("folder", "")) if os.path.exists(server_folder): shutil.rmtree(server_folder, ignore_errors=True) logger.info(f"Deleted old server: {server.get('folder')}") else: active_servers.append(server) db["servers"] = active_servers # حذف المستخدمين غير النشطين لأكثر من 90 يوم if "users" in db: active_users = [] for user in db["users"]: if user.get("last_active") and (current_time - user["last_active"]) > 90 * 24 * 3600: user_folder = os.path.join(USERS_ROOT, user.get("username", "")) if os.path.exists(user_folder): shutil.rmtree(user_folder, ignore_errors=True) logger.info(f"Deleted inactive user: {user.get('username')}") else: active_users.append(user) db["users"] = active_users save_db(db) logger.info("Cleanup of old data completed") except Exception as e: logger.error(f"Error during cleanup: {e}") # نظام الحماية والجدار الناري class Firewall: def __init__(self): self.blocked_ips = set() self.rate_limits = defaultdict(list) self.max_requests_per_minute = 60 self.block_duration = 3600 # ساعة def is_blocked(self, ip): """التحقق إذا كان IP محظور""" return ip in self.blocked_ips def block_ip(self, ip): """حظر IP""" self.blocked_ips.add(ip) logger.warning(f"Blocked IP: {ip}") def check_rate_limit(self, ip): """التحقق من الحد الأقصى للطلبات""" current_time = time.time() # إزالة الطلبات القديمة (أقدم من دقيقة) self.rate_limits[ip] = [t for t in self.rate_limits[ip] if current_time - t < 60] if len(self.rate_limits[ip]) >= self.max_requests_per_minute: self.block_ip(ip) return False self.rate_limits[ip].append(current_time) return True def unblock_ip(self, ip): """إلغاء حظر IP""" if ip in self.blocked_ips: self.blocked_ips.remove(ip) logger.info(f"Unblocked IP: {ip}") firewall = Firewall() # نظام الحميات (Subscription Plans) SUBSCRIPTION_PLANS = { "free": { "name": "Free", "price": 0, "max_bots": 3, "max_mem_mb": 256, "max_cpu_percent": 50, "features": ["3 Bots", "256MB RAM", "Basic Support"] }, "basic": { "name": "Basic", "price": 10, "max_bots": 5, "max_mem_mb": 512, "max_cpu_percent": 70, "features": ["5 Bots", "512MB RAM", "Priority Support", "Advanced Analytics"] }, "pro": { "name": "Pro", "price": 25, "max_bots": 10, "max_mem_mb": 1024, "max_cpu_percent": 80, "features": ["10 Bots", "1GB RAM", "24/7 Support", "Advanced Analytics", "Custom Domain"] }, "enterprise": { "name": "Enterprise", "price": 100, "max_bots": 50, "max_mem_mb": 4096, "max_cpu_percent": 90, "features": ["50 Bots", "4GB RAM", "Dedicated Support", "Advanced Analytics", "Custom Domain", "API Access", "White Label"] } } def get_user_plan(username): """الحصول على خطة المستخدم""" db = load_db() for user in db.get("users", []): if user.get("username") == username: return user.get("plan", "free") return "free" def get_plan_limits(plan): """الحصول على حدود الخطة""" return SUBSCRIPTION_PLANS.get(plan, SUBSCRIPTION_PLANS["free"]) def check_plan_limits(username): """التحقق من حدود خطة المستخدم""" plan = get_user_plan(username) limits = get_plan_limits(plan) db = load_db() user_servers = [s for s in db.get("servers", []) if s.get("owner") == username] if len(user_servers) >= limits["max_bots"]: return False, f"You have reached the maximum number of bots ({limits['max_bots']}) for your {limits['name']} plan" return True, None # API endpoints للحميات @app.route("/api/subscription/plans", methods=["GET"]) @login_required def get_subscription_plans(): """الحصول على جميع خطط الاشتراك""" return jsonify({"success": True, "plans": SUBSCRIPTION_PLANS}) @app.route("/api/subscription/current", methods=["GET"]) @login_required def get_current_subscription(): """الحصول على خطة المستخدم الحالية""" username = session.get("username") plan = get_user_plan(username) limits = get_plan_limits(plan) db = load_db() user_servers = [s for s in db.get("servers", []) if s.get("owner") == username] return jsonify({ "success": True, "plan": plan, "limits": limits, "usage": { "bots_used": len(user_servers), "bots_remaining": limits["max_bots"] - len(user_servers) } }) @app.route("/api/subscription/upgrade", methods=["POST"]) @login_required def upgrade_subscription(): """ترقية خطة الاشتراك""" username = session.get("username") data = request.get_json() new_plan = data.get("plan") if new_plan not in SUBSCRIPTION_PLANS: return jsonify({"success": False, "message": "Invalid plan"}), 400 db = load_db() for user in db.get("users", []): if user.get("username") == username: user["plan"] = new_plan user["plan_updated_at"] = time.time() save_db(db) logger.info(f"User {username} upgraded to {new_plan} plan") return jsonify({"success": True, "message": f"Successfully upgraded to {new_plan} plan"}) return jsonify({"success": False, "message": "User not found"}), 404 # API endpoints للجدار الناري @app.route("/api/firewall/blocked-ips", methods=["GET"]) @admin_required def get_blocked_ips(): """الحصول على قائمة IPs المحظورة""" return jsonify({"success": True, "blocked_ips": list(firewall.blocked_ips)}) @app.route("/api/firewall/block-ip", methods=["POST"]) @admin_required def block_ip_admin(): """حظر IP من قبل الأدمن""" data = request.get_json() ip = data.get("ip") if not ip: return jsonify({"success": False, "message": "IP required"}), 400 firewall.block_ip(ip) return jsonify({"success": True, "message": f"IP {ip} blocked"}) @app.route("/api/firewall/unblock-ip", methods=["POST"]) @admin_required def unblock_ip_admin(): """إلغاء حظر IP من قبل الأدمن""" data = request.get_json() ip = data.get("ip") if not ip: return jsonify({"success": False, "message": "IP required"}), 400 firewall.unblock_ip(ip) return jsonify({"success": True, "message": f"IP {ip} unblocked"}) @app.route("/api/firewall/cleanup-now", methods=["POST"]) @admin_required def cleanup_now(): """تنفيذ تنظيف البيانات القديمة فوراً""" cleanup_old_data() return jsonify({"success": True, "message": "Cleanup completed"}) @app.before_request def firewall_check(): """التحقق من الجدار الناري قبل كل طلب""" if request.path.startswith('/static') or request.path.startswith('/health'): return None client_ip = request.remote_addr or '127.0.0.1' # التحقق من الحظر if firewall.is_blocked(client_ip): return jsonify({"success": False, "message": "IP blocked"}), 403 # التحقق من الحد الأقصى للطلبات if not firewall.check_rate_limit(client_ip): return jsonify({"success": False, "message": "Rate limit exceeded"}), 429 return None @app.route("/health") def health_check(): return jsonify({"status": "ok", "uptime": time.time()}) @app.route("/api/system/status") @admin_required def system_status(): try: cpu = psutil.cpu_percent(interval=0.5) mem = psutil.virtual_memory() disk = psutil.disk_usage('/') procs = len(running_procs) return jsonify({ "success": True, "cpu_percent": cpu, "memory_used_mb": round(mem.used / 1024 / 1024), "memory_total_mb": round(mem.total / 1024 / 1024), "memory_percent": mem.percent, "disk_used_gb": round(disk.used / 1024 / 1024 / 1024, 1), "disk_total_gb": round(disk.total / 1024 / 1024 / 1024, 1), "disk_percent": round(disk.percent, 1), "running_processes": procs }) except Exception as e: return jsonify({"success": False, "message": str(e)}), 500 def graceful_shutdown(signum, frame): logger.info("Shutting down gracefully...") with lock: for key in list(running_procs.keys()): stop_proc(key) logger.info("All processes stopped.") sys.exit(0) signal.signal(signal.SIGTERM, graceful_shutdown) signal.signal(signal.SIGINT, graceful_shutdown) if __name__ == "__main__": port = int(os.environ.get("SERVER_PORT", 7860)) logger.info(f"--- SYSTEM INITIALIZATION: Port {port} ---") # Start background tasks threading.Thread(target=run_keep_alive, daemon=True).start() threading.Thread(target=run_log_cleaner, daemon=True).start() threading.Thread(target=run_git_sync, daemon=True).start() try: from gunicorn.app.wsgiapp import run as gunicorn_run logger.info("Launching Production WSGI Engine (Gunicorn)...") sys.argv = [ 'gunicorn', '--bind', f'0.0.0.0:{port}', '--worker-class', 'sync', '--workers', '1', '--timeout', '600', 'app:app' ] gunicorn_run() except Exception as e: logger.error(f"Gunicorn failed: {e}. Falling back to development server.") app.run(host="0.0.0.0", port=port, threaded=True)