Spaces:
Paused
Paused
j49_c
Add advanced features: cleanup old data, subscription plans, firewall protection, enhanced UI
7d1d67d Download app.py from mohammed978/hostbot: direct link, hf CLI and curl.
- Browser
- Download file 60.1 kB
-
https://huggingface.co/spaces/mohammed978/hostbot/resolve/main/app.py
- Command line
-
hf download hf://spaces/mohammed978/hostbot/app.py
-
curl -L -o app.py https://huggingface.co/spaces/mohammed978/hostbot/resolve/main/app.py
60.1 kB
| 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): | |
| 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 | |
| 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): | |
| def wrapper(*args, **kwargs): | |
| if not session.get("user"): | |
| return redirect("/login") | |
| return fn(*args, **kwargs) | |
| return wrapper | |
| def admin_required(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) | |
| def home(): | |
| if not session.get("user"): | |
| return send_from_directory(BASE_DIR, "landing.html") | |
| return redirect("/dashboard") | |
| def dashboard(): | |
| return send_from_directory(BASE_DIR, "index.html") | |
| 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") | |
| def terms_page(): | |
| return send_from_directory(BASE_DIR, "terms.html") | |
| def privacy_page(): | |
| return send_from_directory(BASE_DIR, "privacy.html") | |
| def features_page(): | |
| return send_from_directory(BASE_DIR, "features.html") | |
| def docs_page(): | |
| return send_from_directory(BASE_DIR, "docs.html") | |
| def login_page(): | |
| return send_from_directory(BASE_DIR, "login.html") | |
| def create_page(): | |
| return send_from_directory(BASE_DIR, "create.html") | |
| def admin_page(): | |
| if not is_admin_session(): | |
| return redirect("/") | |
| return send_from_directory(BASE_DIR, "admin.html") | |
| def logout(): | |
| session.pop("user", None) | |
| return redirect("/login") | |
| 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 | |
| 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 | |
| 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) | |
| }) | |
| 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}) | |
| 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}) | |
| 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"]}) | |
| 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 | |
| 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())}) | |
| 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)}) | |
| 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") | |
| 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}) | |
| 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}) | |
| 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}) | |
| 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}) | |
| 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": ""}) | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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}) | |
| def admin_servers(): | |
| return jsonify({"success": True, "servers": list_all_servers_for_admin()}) | |
| 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}) | |
| 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 | |
| def api_ai_models(): | |
| return jsonify({ | |
| "success": True, | |
| "models": ["llama3.2:3b", "deepseek-r1:latest", "mistral:latest", "gpt-oss:20b"] | |
| }) | |
| def api_get_ads(): | |
| return jsonify(load_ads()) | |
| 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}) | |
| 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 للحميات | |
| def get_subscription_plans(): | |
| """الحصول على جميع خطط الاشتراك""" | |
| return jsonify({"success": True, "plans": SUBSCRIPTION_PLANS}) | |
| 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) | |
| } | |
| }) | |
| 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 للجدار الناري | |
| def get_blocked_ips(): | |
| """الحصول على قائمة IPs المحظورة""" | |
| return jsonify({"success": True, "blocked_ips": list(firewall.blocked_ips)}) | |
| 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"}) | |
| 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"}) | |
| def cleanup_now(): | |
| """تنفيذ تنظيف البيانات القديمة فوراً""" | |
| cleanup_old_data() | |
| return jsonify({"success": True, "message": "Cleanup completed"}) | |
| 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 | |
| def health_check(): | |
| return jsonify({"status": "ok", "uptime": time.time()}) | |
| 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) | |