| |
| """ |
| Sukuna Webshare Harvester β Advanced Web UI |
| Flask web server: auto-register + proxy harvesting + proxy/email management |
| API key: @BaignX |
| """ |
|
|
| import os, sys, re, json, time, random, threading, queue, uuid, signal, base64, select |
| import sqlite3, hashlib, secrets |
| from datetime import datetime |
| from functools import wraps |
|
|
| from flask import (Flask, render_template_string, request, jsonify, |
| Response, session, send_file) |
| import requests |
| import concurrent.futures |
| import socket |
| import io |
|
|
| |
| import argparse |
| _parser = argparse.ArgumentParser(description="Sukuna Webshare Harvester") |
| _parser.add_argument("--port", type=int, default=int(os.environ.get("PORT", 7860)), help="Port (default 7860)") |
| _parser.add_argument("--host", default=os.environ.get("HOST", "0.0.0.0"), help="Bind host") |
| _parser.add_argument("--debug", action="store_true", help="Flask debug mode") |
| _args, _ = _parser.parse_known_args() |
|
|
| PORT = _args.port |
| HOST = _args.host |
| DEBUG = _args.debug |
|
|
| |
| API_KEY = "@BaignX" |
| OUTPUT_DIR = "task_output" |
| AUDIO_DIR = os.path.expanduser("~/sle") |
| TARGET_URL = "https://dashboard.webshare.io/register" |
| WEBSHARE_API = "https://proxy.webshare.io/api/v2" |
| DEFAULT_PASS = "God@111983" |
| DB_PATH = os.path.join(os.path.dirname(__file__), "webshare.sqlite3") |
| SECRET_KEY_FILE = os.path.join(os.path.dirname(__file__), ".flask_session_secret") |
| REMEMBER_COOKIE = "sukuna_remember" |
| REMEMBER_MAX_AGE = 86400 * 90 |
|
|
| os.makedirs(OUTPUT_DIR, exist_ok=True) |
| os.makedirs(AUDIO_DIR, exist_ok=True) |
|
|
| |
| def _load_or_create_secret(path: str) -> str: |
| if os.path.exists(path): |
| with open(path, "r", encoding="utf-8") as f: |
| secret = f.read().strip() |
| if secret: |
| return secret |
| secret = secrets.token_hex(32) |
| with open(path, "w", encoding="utf-8") as f: |
| f.write(secret) |
| os.chmod(path, 0o600) |
| return secret |
|
|
| app = Flask(__name__) |
| app.secret_key = os.environ.get("SECRET_KEY") or _load_or_create_secret(SECRET_KEY_FILE) |
| app.config["SESSION_COOKIE_SAMESITE"] = "Lax" |
| app.config["SESSION_COOKIE_HTTPONLY"] = True |
| app.config["PERMANENT_SESSION_LIFETIME"] = 86400 * 7 |
|
|
| |
| _state = { |
| "proxies": [], |
| "emails": [], |
| "tasks": [], |
| "current_task": None, |
| "stop_flag": False, |
| } |
| _log_listeners = [] |
| _state_lock = threading.Lock() |
|
|
| def _db_conn(): |
| conn = sqlite3.connect(DB_PATH, timeout=30) |
| conn.row_factory = sqlite3.Row |
| return conn |
|
|
| def _db_init(): |
| with _db_conn() as conn: |
| conn.execute(""" |
| CREATE TABLE IF NOT EXISTS proxies ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| host TEXT NOT NULL, |
| port INTEGER NOT NULL, |
| user TEXT NOT NULL DEFAULT '', |
| pass TEXT NOT NULL DEFAULT '', |
| status TEXT NOT NULL DEFAULT 'unchecked', |
| type TEXT NOT NULL DEFAULT 'rotating', |
| exit_ip TEXT NOT NULL DEFAULT '', |
| UNIQUE(host, port) |
| ) |
| """) |
| conn.execute(""" |
| CREATE TABLE IF NOT EXISTS emails ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| email TEXT NOT NULL UNIQUE, |
| password TEXT NOT NULL DEFAULT '', |
| domain TEXT NOT NULL |
| ) |
| """) |
| conn.execute(""" |
| CREATE TABLE IF NOT EXISTS task_history ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| task_id TEXT NOT NULL UNIQUE, |
| status TEXT NOT NULL, |
| progress INTEGER NOT NULL DEFAULT 0, |
| total INTEGER NOT NULL DEFAULT 0, |
| threads INTEGER NOT NULL DEFAULT 0, |
| rotating_fetched INTEGER NOT NULL DEFAULT 0, |
| static_fetched INTEGER NOT NULL DEFAULT 0, |
| start_time TEXT, |
| end_time TEXT, |
| logs_json TEXT NOT NULL DEFAULT '[]', |
| rotating_json TEXT NOT NULL DEFAULT '[]', |
| static_json TEXT NOT NULL DEFAULT '[]' |
| ) |
| """) |
| conn.execute(""" |
| CREATE TABLE IF NOT EXISTS auth_tokens ( |
| token_hash TEXT PRIMARY KEY, |
| created_at TEXT NOT NULL, |
| last_used_at TEXT NOT NULL |
| ) |
| """) |
| conn.commit() |
|
|
| def _safe_json_load(text, default): |
| try: |
| val = json.loads(text or "") |
| return val if isinstance(val, type(default)) else default |
| except Exception: |
| return default |
|
|
| def _db_load_proxies(): |
| with _db_conn() as conn: |
| rows = conn.execute( |
| "SELECT host,port,user,pass,status,type,exit_ip FROM proxies ORDER BY id ASC" |
| ).fetchall() |
| proxies = [] |
| for r in rows: |
| proxies.append({ |
| "host": r["host"], |
| "port": int(r["port"]), |
| "user": r["user"] or "", |
| "pass": r["pass"] or "", |
| "status": r["status"] or "unchecked", |
| "type": r["type"] or "rotating", |
| "exit_ip": r["exit_ip"] or "", |
| }) |
| return proxies |
|
|
| def _db_replace_proxies(proxies: list): |
| rows = [( |
| p.get("host", "").strip(), |
| int(p.get("port", 0)), |
| p.get("user", ""), |
| p.get("pass", ""), |
| p.get("status", "unchecked"), |
| p.get("type", "rotating"), |
| p.get("exit_ip", ""), |
| ) for p in proxies if p.get("host") and p.get("port")] |
| with _db_conn() as conn: |
| conn.execute("DELETE FROM proxies") |
| if rows: |
| conn.executemany( |
| "INSERT INTO proxies(host,port,user,pass,status,type,exit_ip) VALUES(?,?,?,?,?,?,?)", |
| rows |
| ) |
| conn.commit() |
|
|
| def _db_load_emails(): |
| with _db_conn() as conn: |
| rows = conn.execute( |
| "SELECT email,password,domain FROM emails ORDER BY id ASC" |
| ).fetchall() |
| return [{"email": r["email"], "password": r["password"] or "", "domain": r["domain"]} for r in rows] |
|
|
| def _db_replace_emails(emails: list): |
| rows = [( |
| e.get("email", "").strip().lower(), |
| e.get("password", ""), |
| e.get("domain", ""), |
| ) for e in emails if e.get("email")] |
| with _db_conn() as conn: |
| conn.execute("DELETE FROM emails") |
| if rows: |
| conn.executemany("INSERT INTO emails(email,password,domain) VALUES(?,?,?)", rows) |
| conn.commit() |
|
|
| def _db_load_tasks(): |
| with _db_conn() as conn: |
| rows = conn.execute(""" |
| SELECT task_id,status,progress,total,threads,rotating_fetched,static_fetched, |
| start_time,end_time,logs_json,rotating_json,static_json |
| FROM task_history |
| ORDER BY id ASC |
| """).fetchall() |
| tasks = [] |
| for r in rows: |
| tasks.append({ |
| "id": r["task_id"], |
| "status": r["status"], |
| "progress": int(r["progress"] or 0), |
| "total": int(r["total"] or 0), |
| "threads": int(r["threads"] or 0), |
| "rotating_fetched": int(r["rotating_fetched"] or 0), |
| "static_fetched": int(r["static_fetched"] or 0), |
| "start_time": r["start_time"], |
| "end_time": r["end_time"], |
| "logs": _safe_json_load(r["logs_json"], []), |
| "rotating_list": _safe_json_load(r["rotating_json"], []), |
| "static_list": _safe_json_load(r["static_json"], []), |
| }) |
| return tasks |
|
|
| def _db_upsert_task(task: dict): |
| with _db_conn() as conn: |
| conn.execute(""" |
| INSERT INTO task_history( |
| task_id,status,progress,total,threads,rotating_fetched,static_fetched, |
| start_time,end_time,logs_json,rotating_json,static_json |
| ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?) |
| ON CONFLICT(task_id) DO UPDATE SET |
| status=excluded.status, |
| progress=excluded.progress, |
| total=excluded.total, |
| threads=excluded.threads, |
| rotating_fetched=excluded.rotating_fetched, |
| static_fetched=excluded.static_fetched, |
| start_time=excluded.start_time, |
| end_time=excluded.end_time, |
| logs_json=excluded.logs_json, |
| rotating_json=excluded.rotating_json, |
| static_json=excluded.static_json |
| """, ( |
| task.get("id"), |
| task.get("status", ""), |
| int(task.get("progress", 0) or 0), |
| int(task.get("total", 0) or 0), |
| int(task.get("threads", 0) or 0), |
| int(task.get("rotating_fetched", 0) or 0), |
| int(task.get("static_fetched", 0) or 0), |
| task.get("start_time"), |
| task.get("end_time"), |
| json.dumps(task.get("logs", []), ensure_ascii=False), |
| json.dumps(task.get("rotating_list", []), ensure_ascii=False), |
| json.dumps(task.get("static_list", []), ensure_ascii=False), |
| )) |
| conn.commit() |
|
|
| def _hash_token(token: str) -> str: |
| return hashlib.sha256(token.encode("utf-8")).hexdigest() |
|
|
| def _db_save_auth_token(raw_token: str): |
| now = datetime.now().isoformat() |
| with _db_conn() as conn: |
| conn.execute( |
| "INSERT OR REPLACE INTO auth_tokens(token_hash,created_at,last_used_at) VALUES(?,?,?)", |
| (_hash_token(raw_token), now, now) |
| ) |
| conn.commit() |
|
|
| def _db_touch_auth_token(raw_token: str) -> bool: |
| token_hash = _hash_token(raw_token) |
| now = datetime.now().isoformat() |
| with _db_conn() as conn: |
| row = conn.execute("SELECT token_hash FROM auth_tokens WHERE token_hash=?", (token_hash,)).fetchone() |
| if not row: |
| return False |
| conn.execute("UPDATE auth_tokens SET last_used_at=? WHERE token_hash=?", (now, token_hash)) |
| conn.commit() |
| return True |
|
|
| def _db_delete_auth_token(raw_token: str): |
| with _db_conn() as conn: |
| conn.execute("DELETE FROM auth_tokens WHERE token_hash=?", (_hash_token(raw_token),)) |
| conn.commit() |
|
|
| def _load_state_from_db(): |
| with _state_lock: |
| _state["proxies"] = _db_load_proxies() |
| _state["emails"] = _db_load_emails() |
| _state["tasks"] = _db_load_tasks() |
|
|
| _db_init() |
| _load_state_from_db() |
|
|
| |
| |
| |
| try: |
| from selenium import webdriver |
| from selenium.webdriver.firefox.options import Options as FFOptions |
| from selenium.webdriver.common.by import By |
| from selenium.webdriver.support.ui import WebDriverWait |
| from selenium.webdriver.support import expected_conditions as EC |
| from selenium.webdriver.common.action_chains import ActionChains |
| from selenium.common.exceptions import ( |
| TimeoutException, NoSuchElementException, |
| StaleElementReferenceException, MoveTargetOutOfBoundsException, |
| ) |
| SELENIUM_OK = True |
| except ImportError: |
| SELENIUM_OK = False |
|
|
| try: |
| import speech_recognition as sr |
| from pydub import AudioSegment |
| AUDIO_OK = True |
| except ImportError: |
| AUDIO_OK = False |
|
|
| |
| |
| |
| STEALTH_JS = """ |
| (function() { |
| Object.defineProperty(navigator, 'webdriver', {get: () => undefined, configurable: true}); |
| Object.defineProperty(navigator, 'languages', {get: () => ['en-US','en'], configurable: true}); |
| Object.defineProperty(navigator, 'hardwareConcurrency', {get:()=>8, configurable:true}); |
| try { Object.defineProperty(navigator, 'deviceMemory', {get:()=>8, configurable:true}); } catch(e){} |
| Object.defineProperty(screen, 'width', {get:()=>1920, configurable:true}); |
| Object.defineProperty(screen, 'height', {get:()=>1080, configurable:true}); |
| Object.defineProperty(screen, 'availWidth', {get:()=>1920, configurable:true}); |
| Object.defineProperty(screen, 'availHeight', {get:()=>1040, configurable:true}); |
| delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array; |
| delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise; |
| delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol; |
| if (navigator.connection) { |
| Object.defineProperty(navigator.connection,'rtt',{get:()=>50,configurable:true}); |
| Object.defineProperty(navigator.connection,'downlink',{get:()=>10,configurable:true}); |
| Object.defineProperty(navigator.connection,'effectiveType',{get:()=>'4g',configurable:true}); |
| } |
| })(); |
| """ |
|
|
| |
| |
| |
| def ts(): |
| return datetime.now().strftime("%H:%M:%S") |
|
|
| def emit_log(msg: str, level: str = "info"): |
| """Push log entry to all SSE listeners and current task.""" |
| entry = {"ts": ts(), "msg": msg, "level": level} |
| line = f"data: {json.dumps(entry)}\n\n" |
| with _state_lock: |
| if _state["current_task"] is not None: |
| _state["current_task"]["logs"].append(entry) |
| dead = [] |
| for q in _log_listeners: |
| try: |
| q.put_nowait(line) |
| except Exception: |
| dead.append(q) |
| for q in dead: |
| try: _log_listeners.remove(q) |
| except ValueError: pass |
|
|
| def emit_proxy(proxy_line: str, ptype: str): |
| """Push a freshly fetched proxy line to all SSE listeners and live task lists. |
| ptype: 'rotating' or 'static' |
| SSE level: 'proxy_rot' or 'proxy_sta' β JS uses this to append to live list. |
| """ |
| level = "proxy_rot" if ptype == "rotating" else "proxy_sta" |
| entry = {"ts": ts(), "msg": proxy_line, "level": level} |
| line = f"data: {json.dumps(entry)}\n\n" |
| with _state_lock: |
| if _state["current_task"] is not None: |
| _state["current_task"]["logs"].append(entry) |
| if ptype == "rotating": |
| _state["current_task"]["rotating_list"].append(proxy_line) |
| _state["current_task"]["rotating_fetched"] = len(_state["current_task"]["rotating_list"]) |
| else: |
| _state["current_task"]["static_list"].append(proxy_line) |
| _state["current_task"]["static_fetched"] = len(_state["current_task"]["static_list"]) |
| dead = [] |
| for q in _log_listeners: |
| try: |
| q.put_nowait(line) |
| except Exception: |
| dead.append(q) |
| for q in dead: |
| try: _log_listeners.remove(q) |
| except ValueError: pass |
|
|
| |
| |
| |
| def _set_remember_cookie(resp): |
| token = secrets.token_urlsafe(32) |
| _db_save_auth_token(token) |
| resp.set_cookie( |
| REMEMBER_COOKIE, |
| token, |
| max_age=REMEMBER_MAX_AGE, |
| httponly=True, |
| samesite="Lax", |
| ) |
| return resp |
|
|
| def _clear_remember_cookie(resp): |
| token = (request.cookies.get(REMEMBER_COOKIE) or "").strip() |
| if token: |
| _db_delete_auth_token(token) |
| resp.delete_cookie(REMEMBER_COOKIE) |
| return resp |
|
|
| def _authorize_from_cookie() -> bool: |
| token = (request.cookies.get(REMEMBER_COOKIE) or "").strip() |
| if not token: |
| return False |
| if not _db_touch_auth_token(token): |
| return False |
| session.permanent = True |
| session["authed"] = True |
| return True |
|
|
| def _is_authenticated() -> bool: |
| if session.get("authed"): |
| return True |
| return _authorize_from_cookie() |
|
|
| def auth_required(f): |
| @wraps(f) |
| def decorated(*args, **kwargs): |
| if not _is_authenticated(): |
| return jsonify({"error": "unauthorized"}), 401 |
| return f(*args, **kwargs) |
| return decorated |
|
|
| @app.route("/api/login", methods=["POST"]) |
| def login(): |
| data = request.get_json(silent=True) or {} |
| if data.get("key") == API_KEY: |
| session.permanent = True |
| session["authed"] = True |
| resp = jsonify({"ok": True}) |
| return _set_remember_cookie(resp) |
| return jsonify({"error": "Invalid API key"}), 403 |
|
|
| @app.route("/api/logout", methods=["POST"]) |
| def logout(): |
| session.clear() |
| resp = jsonify({"ok": True}) |
| return _clear_remember_cookie(resp) |
|
|
| @app.route("/api/auth/check") |
| def auth_check(): |
| return jsonify({"authed": bool(_is_authenticated())}) |
|
|
| |
| |
| |
| def parse_proxy_line(line: str): |
| line = line.strip() |
| if not line or line.startswith("#"): |
| return None |
| parts = line.split(":") |
| try: |
| if len(parts) == 4: |
| return {"host": parts[0].strip(), "port": int(parts[1].strip()), |
| "user": parts[2].strip(), "pass": parts[3].strip(), |
| "status": "unchecked", "type": "rotating"} |
| if len(parts) == 2: |
| return {"host": parts[0].strip(), "port": int(parts[1].strip()), |
| "user": "", "pass": "", |
| "status": "unchecked", "type": "static"} |
| except (ValueError, IndexError): |
| pass |
| return None |
|
|
| def _check_one_proxy(proxy: dict) -> dict: |
| proxy = dict(proxy) |
| try: |
| if proxy["user"]: |
| pu = f"http://{proxy['user']}:{proxy['pass']}@{proxy['host']}:{proxy['port']}" |
| else: |
| pu = f"http://{proxy['host']}:{proxy['port']}" |
| r = requests.get("http://httpbin.org/ip", |
| proxies={"http": pu, "https": pu}, timeout=8) |
| if r.status_code == 200: |
| proxy["status"] = "alive" |
| proxy["exit_ip"] = r.json().get("origin", "") |
| else: |
| proxy["status"] = "dead" |
| except Exception: |
| proxy["status"] = "dead" |
| return proxy |
|
|
| |
| @app.route("/api/proxies") |
| @auth_required |
| def get_proxies(): |
| with _state_lock: |
| pl = list(_state["proxies"]) |
| total = len(pl) |
| alive = sum(1 for p in pl if p["status"] == "alive") |
| dead = sum(1 for p in pl if p["status"] == "dead") |
| rotating = sum(1 for p in pl if p["type"] == "rotating" and p["status"] == "alive") |
| static = sum(1 for p in pl if p["type"] == "static" and p["status"] == "alive") |
| return jsonify({"proxies": pl, "total": total, "alive": alive, |
| "dead": dead, "rotating": rotating, "static": static}) |
|
|
| @app.route("/api/proxies/add", methods=["POST"]) |
| @auth_required |
| def add_proxies(): |
| text = (request.get_json(silent=True) or {}).get("text", "") |
| added = 0 |
| with _state_lock: |
| existing = {f"{p['host']}:{p['port']}" for p in _state["proxies"]} |
| for line in text.splitlines(): |
| p = parse_proxy_line(line) |
| if p: |
| key = f"{p['host']}:{p['port']}" |
| if key not in existing: |
| _state["proxies"].append(p) |
| existing.add(key) |
| added += 1 |
| total = len(_state["proxies"]) |
| proxies_snapshot = list(_state["proxies"]) |
| _db_replace_proxies(proxies_snapshot) |
| return jsonify({"ok": True, "added": added, "total": total}) |
|
|
| @app.route("/api/proxies/upload", methods=["POST"]) |
| @auth_required |
| def upload_proxies(): |
| f = request.files.get("file") |
| if not f: |
| return jsonify({"error": "No file"}), 400 |
| text = f.read().decode("utf-8", errors="ignore") |
| added = 0 |
| with _state_lock: |
| existing = {f"{p['host']}:{p['port']}" for p in _state["proxies"]} |
| for line in text.splitlines(): |
| p = parse_proxy_line(line) |
| if p: |
| key = f"{p['host']}:{p['port']}" |
| if key not in existing: |
| _state["proxies"].append(p) |
| existing.add(key) |
| added += 1 |
| total = len(_state["proxies"]) |
| proxies_snapshot = list(_state["proxies"]) |
| _db_replace_proxies(proxies_snapshot) |
| return jsonify({"ok": True, "added": added, "total": total}) |
|
|
| @app.route("/api/proxies/check", methods=["POST"]) |
| @auth_required |
| def check_proxies(): |
| data = request.get_json(silent=True) or {} |
| threads = max(1, min(int(data.get("threads", 10)), 50)) |
|
|
| def run(): |
| with _state_lock: |
| to_check = list(_state["proxies"]) |
| if not to_check: |
| emit_log("No proxies to check", "warn") |
| return |
| emit_log(f"Checking {len(to_check)} proxies with {threads} threads...", "info") |
| with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as ex: |
| results = list(ex.map(_check_one_proxy, to_check)) |
| with _state_lock: |
| _state["proxies"] = results |
| _db_replace_proxies(results) |
| alive = sum(1 for p in results if p["status"] == "alive") |
| rotating = sum(1 for p in results if p["type"] == "rotating" and p["status"] == "alive") |
| static = sum(1 for p in results if p["type"] == "static" and p["status"] == "alive") |
| emit_log(f"Proxy check done: {alive}/{len(results)} alive | " |
| f"Rotating: {rotating} | Static: {static}", "success") |
|
|
| threading.Thread(target=run, daemon=True).start() |
| return jsonify({"ok": True}) |
|
|
| @app.route("/api/proxies/clear", methods=["POST"]) |
| @auth_required |
| def clear_proxies(): |
| with _state_lock: |
| _state["proxies"] = [] |
| _db_replace_proxies([]) |
| return jsonify({"ok": True}) |
|
|
| @app.route("/api/proxies/export") |
| @auth_required |
| def export_proxies(): |
| ptype = request.args.get("type", "all") |
| with _state_lock: |
| pl = list(_state["proxies"]) |
| if ptype == "rotating": |
| pl = [p for p in pl if p["type"] == "rotating" and p["status"] == "alive"] |
| elif ptype == "static": |
| pl = [p for p in pl if p["type"] == "static" and p["status"] == "alive"] |
| elif ptype == "alive": |
| pl = [p for p in pl if p["status"] == "alive"] |
| lines = [] |
| for p in pl: |
| if p["user"]: |
| lines.append(f"{p['host']}:{p['port']}:{p['user']}:{p['pass']}") |
| else: |
| lines.append(f"{p['host']}:{p['port']}") |
| return Response("\n".join(lines), mimetype="text/plain", |
| headers={"Content-Disposition": f"attachment; filename=proxies_{ptype}.txt"}) |
|
|
| |
| |
| |
| _EMAIL_RE = re.compile(r"[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}") |
|
|
| def parse_email_line(line: str): |
| line = line.strip() |
| if not line or line.startswith("#"): |
| return None |
| emails = _EMAIL_RE.findall(line) |
| if not emails: |
| return None |
| email = emails[0].lower() |
| domain = email.split("@")[1] |
| password = "" |
| |
| if ":" in line: |
| parts = line.split(":") |
| for i, part in enumerate(parts): |
| if email in part.lower() and i + 1 < len(parts): |
| password = ":".join(parts[i + 1:]).strip() |
| break |
| |
| if not password and len(parts) == 2 and _EMAIL_RE.match(parts[0].strip()): |
| password = parts[1].strip() |
| return {"email": email, "password": password, "domain": domain} |
|
|
| |
| @app.route("/api/emails") |
| @auth_required |
| def get_emails(): |
| with _state_lock: |
| el = list(_state["emails"]) |
| domains: dict = {} |
| for e in el: |
| domains[e["domain"]] = domains.get(e["domain"], 0) + 1 |
| return jsonify({"emails": el, "total": len(el), "domains": domains}) |
|
|
| @app.route("/api/emails/add", methods=["POST"]) |
| @auth_required |
| def add_emails(): |
| text = (request.get_json(silent=True) or {}).get("text", "") |
| added = 0 |
| with _state_lock: |
| existing = {e["email"] for e in _state["emails"]} |
| for line in text.splitlines(): |
| e = parse_email_line(line) |
| if e and e["email"] not in existing: |
| _state["emails"].append(e) |
| existing.add(e["email"]) |
| added += 1 |
| total = len(_state["emails"]) |
| emails_snapshot = list(_state["emails"]) |
| _db_replace_emails(emails_snapshot) |
| return jsonify({"ok": True, "added": added, "total": total}) |
|
|
| @app.route("/api/emails/upload", methods=["POST"]) |
| @auth_required |
| def upload_emails(): |
| f = request.files.get("file") |
| if not f: |
| return jsonify({"error": "No file"}), 400 |
| text = f.read().decode("utf-8", errors="ignore") |
| added = 0 |
| with _state_lock: |
| existing = {e["email"] for e in _state["emails"]} |
| for line in text.splitlines(): |
| e = parse_email_line(line) |
| if e and e["email"] not in existing: |
| _state["emails"].append(e) |
| existing.add(e["email"]) |
| added += 1 |
| total = len(_state["emails"]) |
| emails_snapshot = list(_state["emails"]) |
| _db_replace_emails(emails_snapshot) |
| return jsonify({"ok": True, "added": added, "total": total}) |
|
|
| @app.route("/api/emails/filter", methods=["POST"]) |
| @auth_required |
| def filter_emails(): |
| data = request.get_json(silent=True) or {} |
| domain = data.get("domain", "").lower().strip().lstrip("@") |
| if not domain: |
| return jsonify({"error": "domain required"}), 400 |
| with _state_lock: |
| _state["emails"] = [e for e in _state["emails"] if e["domain"] == domain] |
| total = len(_state["emails"]) |
| emails_snapshot = list(_state["emails"]) |
| _db_replace_emails(emails_snapshot) |
| return jsonify({"ok": True, "total": total, "domain": domain}) |
|
|
| @app.route("/api/emails/clear", methods=["POST"]) |
| @auth_required |
| def clear_emails(): |
| with _state_lock: |
| _state["emails"] = [] |
| _db_replace_emails([]) |
| return jsonify({"ok": True}) |
|
|
| @app.route("/api/emails/export") |
| @auth_required |
| def export_emails(): |
| with_pass = request.args.get("with_pass", "0") == "1" |
| with _state_lock: |
| el = list(_state["emails"]) |
| lines = [] |
| for e in el: |
| if with_pass and e["password"]: |
| lines.append(f"{e['email']}:{e['password']}") |
| else: |
| lines.append(e["email"]) |
| return Response("\n".join(lines), mimetype="text/plain", |
| headers={"Content-Disposition": "attachment; filename=emails.txt"}) |
|
|
| |
| |
| |
| TUNNEL_PORT = 18888 |
|
|
| def _proxy_relay(src, dst): |
| src.setblocking(False); dst.setblocking(False) |
| try: |
| while True: |
| r, _, err = select.select([src, dst], [], [src, dst], 60) |
| if err or not r: break |
| for s in r: |
| try: |
| data = s.recv(65536) |
| if not data: return |
| other = dst if s is src else src |
| other.setblocking(True); other.sendall(data); other.setblocking(False) |
| except (BlockingIOError, InterruptedError): pass |
| except Exception: pass |
|
|
| def _read_headers(sock, timeout=20.0): |
| sock.settimeout(timeout); buf = b"" |
| while b"\r\n\r\n" not in buf: |
| chunk = sock.recv(4096) |
| if not chunk: break |
| buf += chunk |
| if len(buf) > 65536: break |
| return buf |
|
|
| def _make_proxy_handler(ws_host, ws_port, ws_auth_b64): |
| def _proxy_handle(client): |
| up = None |
| try: |
| raw = _read_headers(client) |
| if not raw or b"\r\n\r\n" not in raw: return |
| sep = raw.index(b"\r\n\r\n") |
| header = raw[:sep].decode("latin-1", errors="replace") |
| body = raw[sep + 4:] |
| lines = header.split("\r\n") |
| parts = lines[0].split(" ", 2) |
| if len(parts) < 2: return |
| method, target = parts[0], parts[1] |
| up = socket.create_connection((ws_host, ws_port), timeout=15) |
| up.settimeout(30) |
| rest = [l for l in lines[1:] if l.strip() |
| and not l.lower().startswith("proxy-authorization")] |
| rest.insert(0, f"Proxy-Authorization: Basic {ws_auth_b64}") |
| upstream_req = (lines[0] + "\r\n" + "\r\n".join(rest) + "\r\n\r\n").encode("latin-1") |
| if method == "CONNECT": |
| up.sendall(upstream_req) |
| resp = _read_headers(up, timeout=20) |
| status = resp.split(b"\r\n")[0].split() |
| code = int(status[1]) if len(status) >= 2 and status[1].isdigit() else 0 |
| if code == 200: |
| client.sendall(b"HTTP/1.1 200 Connection established\r\n\r\n") |
| _proxy_relay(client, up) |
| else: |
| client.sendall(b"HTTP/1.1 502 Bad Gateway\r\n\r\n") |
| else: |
| up.sendall(upstream_req + body) |
| _proxy_relay(client, up) |
| except Exception: |
| pass |
| finally: |
| try: client.close() |
| except: pass |
| if up: |
| try: up.close() |
| except: pass |
| return _proxy_handle |
|
|
| class LocalTunnel: |
| def __init__(self, ws_host, ws_port, ws_user, ws_pass): |
| self.ws_host, self.ws_port = ws_host, ws_port |
| self.ws_auth_b64 = base64.b64encode(f"{ws_user}:{ws_pass}".encode()).decode() |
| self._stop = threading.Event() |
| self._thread = None |
| self._srv = None |
| self.port = None |
|
|
| def start(self): |
| srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| for p in range(TUNNEL_PORT, TUNNEL_PORT + 20): |
| try: |
| srv.bind(("127.0.0.1", p)); self.port = p; break |
| except OSError: |
| continue |
| else: |
| raise RuntimeError("Could not bind tunnel port") |
| srv.listen(200); srv.settimeout(1.0); self._srv = srv |
| handler = _make_proxy_handler(self.ws_host, self.ws_port, self.ws_auth_b64) |
| def _loop(): |
| while not self._stop.is_set(): |
| try: |
| conn, _ = srv.accept() |
| threading.Thread(target=handler, args=(conn,), daemon=True).start() |
| except socket.timeout: continue |
| except Exception: continue |
| try: srv.close() |
| except: pass |
| self._thread = threading.Thread(target=_loop, daemon=True, name="ws-tunnel") |
| self._thread.start() |
| time.sleep(0.1) |
| return self |
|
|
| def stop(self): |
| self._stop.set() |
| if self._srv: |
| try: self._srv.close() |
| except: pass |
|
|
| |
| |
| |
| def build_driver(proxy: dict = None, headless: bool = True): |
| if not SELENIUM_OK: |
| raise RuntimeError("Selenium not available. Run install.sh first.") |
| opts = FFOptions() |
| opts.set_preference("dom.webdriver.enabled", False) |
| opts.set_preference("useAutomationExtension", False) |
| opts.set_preference("marionette", False) |
| opts.set_preference("general.useragent.override", |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0") |
| opts.set_preference("media.volume_scale", "0.0") |
| opts.set_preference("media.peerconnection.enabled", False) |
| opts.set_preference("media.peerconnection.ice.no_host", True) |
| opts.set_preference("dom.push.enabled", False) |
| opts.set_preference("permissions.default.desktop-notification", 2) |
| opts.set_preference("browser.safebrowsing.malware.enabled", False) |
| opts.set_preference("browser.safebrowsing.phishing.enabled", False) |
| opts.set_preference("datareporting.healthreport.uploadEnabled",False) |
| opts.set_preference("toolkit.telemetry.enabled", False) |
| opts.set_preference("toolkit.telemetry.unified", False) |
| opts.set_preference("intl.accept_languages", "en-US, en;q=0.9") |
| opts.set_preference("security.fileuri.strict_origin_policy", False) |
| if headless: |
| opts.add_argument("--headless") |
| tunnel = None |
| if proxy and proxy.get("user"): |
| try: |
| tunnel = LocalTunnel(proxy["host"], proxy["port"], |
| proxy["user"], proxy["pass"]).start() |
| opts.set_preference("network.proxy.type", 1) |
| opts.set_preference("network.proxy.http", "127.0.0.1") |
| opts.set_preference("network.proxy.http_port", tunnel.port) |
| opts.set_preference("network.proxy.ssl", "127.0.0.1") |
| opts.set_preference("network.proxy.ssl_port", tunnel.port) |
| opts.set_preference("network.proxy.no_proxies_on", |
| "localhost,127.0.0.1,google.com,*.google.com," |
| "googleapis.com,*.googleapis.com,gstatic.com,*.gstatic.com," |
| "recaptcha.net,*.recaptcha.net,recaptcha.google.com") |
| except Exception as ex: |
| emit_log(f"Tunnel error: {ex}", "warn") |
| elif proxy and proxy.get("host"): |
| opts.set_preference("network.proxy.type", 1) |
| opts.set_preference("network.proxy.http", proxy["host"]) |
| opts.set_preference("network.proxy.http_port", proxy["port"]) |
| opts.set_preference("network.proxy.ssl", proxy["host"]) |
| opts.set_preference("network.proxy.ssl_port", proxy["port"]) |
| os.environ["TZ"] = "America/New_York" |
| drv = webdriver.Firefox(options=opts) |
| drv.set_window_size(1263, 893) |
| try: |
| drv.execute_script(STEALTH_JS) |
| except Exception: |
| pass |
| return drv, tunnel |
|
|
| |
| |
| |
| def _jitter(lo=0.08, hi=0.25): time.sleep(random.uniform(lo, hi)) |
| def _pause(lo=0.6, hi=2.0): time.sleep(random.uniform(lo, hi)) |
|
|
| def _scroll_to(drv, el): |
| try: |
| drv.execute_script("arguments[0].scrollIntoView({behavior:'smooth',block:'center'});", el) |
| time.sleep(random.uniform(0.2, 0.5)) |
| except Exception: pass |
|
|
| def _click(drv, el): |
| try: |
| _scroll_to(drv, el) |
| ac = ActionChains(drv) |
| ac.move_to_element_with_offset(el, random.randint(-4, 4), random.randint(-3, 3)) |
| ac.pause(random.uniform(0.1, 0.25)) |
| ac.click().perform() |
| except Exception: |
| try: el.click() |
| except Exception: drv.execute_script("arguments[0].click();", el) |
|
|
| def _type(drv, el, text, wpm=55): |
| from selenium.webdriver.common.keys import Keys |
| cps = wpm * 5 / 60 |
| el.clear(); time.sleep(random.uniform(0.1, 0.3)) |
| for i, ch in enumerate(text): |
| if i < len(text) - 1 and random.random() < 0.03: |
| el.send_keys(random.choice("qwertyuiop")) |
| time.sleep(random.uniform(0.08, 0.16)) |
| el.send_keys(Keys.BACK_SPACE) |
| el.send_keys(ch) |
| delay = 1.0 / (cps * random.uniform(0.6, 1.8)) |
| if random.random() < 0.05: |
| delay += random.uniform(0.2, 0.5) |
| time.sleep(delay) |
|
|
| def _type_pw(el, text): |
| el.clear(); time.sleep(random.uniform(0.15, 0.35)) |
| for ch in text: |
| el.send_keys(ch) |
| time.sleep(random.uniform(0.07, 0.18)) |
|
|
| |
| |
| |
| def fill_form(drv, email: str, password: str): |
| emit_log(f"Filling registration form for {email}", "info") |
| |
| email_el = None |
| for sel in ["email-input", "email", "Email"]: |
| try: |
| email_el = WebDriverWait(drv, 10).until( |
| EC.presence_of_element_located((By.ID, sel))); break |
| except TimeoutException: pass |
| if not email_el: |
| try: email_el = drv.find_element(By.CSS_SELECTOR, 'input[type="email"]') |
| except NoSuchElementException: pass |
| if email_el: |
| _click(drv, email_el); _jitter(0.2, 0.5); _type(drv, email_el, email) |
| emit_log(f"Email entered", "info") |
| else: |
| emit_log("Email field not found", "error"); return False |
| _pause(0.5, 1.0) |
| |
| pw_els = drv.find_elements(By.CSS_SELECTOR, 'input[type="password"]') |
| if pw_els: |
| _click(drv, pw_els[0]); _jitter(); _type_pw(pw_els[0], password) |
| if len(pw_els) >= 2: |
| _pause(0.3, 0.7); _click(drv, pw_els[1]); _jitter(); _type_pw(pw_els[1], password) |
| emit_log("Password entered", "info") |
| else: |
| emit_log("Password field not found", "error"); return False |
| _pause(0.5, 1.0) |
| |
| agreed = False |
| for by, sel in [(By.CSS_SELECTOR, 'input[type="checkbox"]'), |
| (By.XPATH, '//input[@type="checkbox"]')]: |
| try: |
| for box in drv.find_elements(by, sel): |
| if not box.is_selected(): |
| _scroll_to(drv, box); _jitter() |
| try: _click(drv, box) |
| except Exception: drv.execute_script("arguments[0].click();", box) |
| agreed = True; break |
| if agreed: break |
| except Exception: pass |
| if not agreed: |
| emit_log("Checkbox not found (may already be ticked)", "warn") |
| _pause(0.5, 1.0) |
| return True |
|
|
| def click_signup(drv) -> bool: |
| for by, sel in [ |
| (By.XPATH, '//button[contains(.,"Sign Up With Email")]'), |
| (By.XPATH, '//button[@type="submit"]'), |
| (By.CSS_SELECTOR, 'button[type="submit"]'), |
| ]: |
| try: |
| btn = WebDriverWait(drv, 5).until(EC.element_to_be_clickable((by, sel))) |
| _scroll_to(drv, btn); _jitter(0.3, 0.6); _click(drv, btn) |
| emit_log(f"Sign Up clicked: '{btn.text.strip()[:40]}'", "info") |
| return True |
| except TimeoutException: pass |
| emit_log("Sign Up button not found", "error") |
| return False |
|
|
| |
| |
| |
| def _get_audio_url(drv): |
| for sel in ['//a[contains(@href,"recaptcha/api2/payload")]', |
| '//a[@class="rc-audiochallenge-download-link"]', |
| '//audio']: |
| try: |
| el = drv.find_element(By.XPATH, sel) |
| src = el.get_attribute("href") or el.get_attribute("src") |
| if src and src.startswith("http"): return src |
| except NoSuchElementException: pass |
| try: |
| src = drv.execute_script(""" |
| var a=document.querySelectorAll('audio'); |
| for(var i=0;i<a.length;i++){ |
| if(a[i].src)return a[i].src; |
| var s=a[i].querySelectorAll('source'); |
| for(var j=0;j<s.length;j++)if(s[j].src)return s[j].src; |
| } |
| var lnk=document.querySelectorAll('a[href]'); |
| for(var k=0;k<lnk.length;k++){ |
| var h=lnk[k].href; |
| if(h&&(h.includes('mp3')||h.includes('audio')||h.includes('payload')))return h; |
| } |
| return null; |
| """) |
| if src: return src |
| except Exception: pass |
| return None |
|
|
| def _transcribe_mp3(mp3_path: str) -> str | None: |
| if not AUDIO_OK: |
| return None |
| wav_path = mp3_path.replace(".mp3", ".wav") |
| try: |
| sound = AudioSegment.from_mp3(mp3_path) |
| sound = sound.set_channels(1).set_frame_rate(16000) |
| peak = sound.max_dBFS |
| if peak < -1: sound = sound.apply_gain(-peak - 1) |
| sound = sound + 8 |
| sound.export(wav_path, format="wav") |
| except Exception as ex: |
| emit_log(f"Audio conversion error: {ex}", "error"); return None |
| rec = sr.Recognizer() |
| rec.energy_threshold = 200 |
| rec.dynamic_energy_threshold = False |
| rec.pause_threshold = 0.5 |
| try: |
| with sr.AudioFile(wav_path) as src: |
| audio_data = rec.record(src) |
| for _ in range(3): |
| try: |
| text = rec.recognize_google(audio_data) |
| emit_log(f"Captcha transcription: '{text}'", "info") |
| return text.strip().lower() |
| except sr.UnknownValueError: |
| time.sleep(1.5) |
| except sr.RequestError: |
| break |
| except Exception as ex: |
| emit_log(f"Transcription error: {ex}", "error") |
| finally: |
| for p in (mp3_path, wav_path): |
| try: |
| if os.path.exists(p): os.remove(p) |
| except Exception: pass |
| return None |
|
|
| def solve_captcha(drv) -> bool: |
| emit_log("Looking for captcha challenge...", "info") |
| BFRAME_XPATHS = [ |
| '//iframe[contains(@src,"bframe")]', |
| '//iframe[contains(@title,"recaptcha challenge")]', |
| '//iframe[contains(@name,"c-")]', |
| ] |
| ch_frame = None |
| drv.switch_to.default_content() |
| for xp in BFRAME_XPATHS: |
| try: |
| ch_frame = WebDriverWait(drv, 20).until( |
| EC.presence_of_element_located((By.XPATH, xp))) |
| break |
| except TimeoutException: pass |
| if not ch_frame: |
| emit_log("No captcha challenge detected (auto-passed)", "success") |
| return True |
| drv.switch_to.frame(ch_frame) |
| time.sleep(random.uniform(1.5, 2.5)) |
| |
| clicked = False |
| for by, sel in [(By.ID, "recaptcha-audio-button"), |
| (By.XPATH, '//button[@title="Get an audio challenge"]')]: |
| try: |
| el = WebDriverWait(drv, 8).until(EC.element_to_be_clickable((by, sel))) |
| _click(drv, el); clicked = True; break |
| except TimeoutException: pass |
| if not clicked: |
| emit_log("Audio button not found", "error"); return False |
| emit_log("Audio challenge started", "info") |
| |
| for attempt in range(1, 4): |
| time.sleep(random.uniform(4, 6)) |
| audio_url = _get_audio_url(drv) |
| if not audio_url: |
| emit_log("Could not get audio URL", "error"); return False |
| mp3_path = os.path.join(AUDIO_DIR, f"cap_{datetime.now().strftime('%H%M%S%f')}.mp3") |
| try: |
| r = requests.get(audio_url, timeout=20, |
| headers={"User-Agent": "Mozilla/5.0"}, |
| proxies={"http": None, "https": None}) |
| if r.status_code != 200: |
| emit_log(f"MP3 download failed: HTTP {r.status_code}", "error"); return False |
| with open(mp3_path, "wb") as f: f.write(r.content) |
| except Exception as ex: |
| emit_log(f"MP3 download error: {ex}", "error"); return False |
| answer = _transcribe_mp3(mp3_path) |
| if answer: break |
| emit_log(f"Transcription attempt {attempt} failed, requesting new challenge", "warn") |
| drv.switch_to.default_content() |
| for xp in BFRAME_XPATHS: |
| try: |
| ch_frame = WebDriverWait(drv, 10).until( |
| EC.presence_of_element_located((By.XPATH, xp))) |
| break |
| except TimeoutException: pass |
| if ch_frame: |
| drv.switch_to.frame(ch_frame) |
| for by, sel in [(By.ID, "recaptcha-reload-button"), |
| (By.XPATH, '//button[@title="Get a new challenge"]')]: |
| try: |
| el = WebDriverWait(drv, 5).until(EC.element_to_be_clickable((by, sel))) |
| _click(drv, el); break |
| except TimeoutException: pass |
| if not answer: |
| emit_log("Captcha solving failed", "error"); return False |
| drv.switch_to.default_content() |
| for xp in BFRAME_XPATHS: |
| try: |
| ch_frame = WebDriverWait(drv, 10).until( |
| EC.presence_of_element_located((By.XPATH, xp))) |
| break |
| except TimeoutException: pass |
| if ch_frame: |
| drv.switch_to.frame(ch_frame) |
| try: |
| field = WebDriverWait(drv, 10).until( |
| EC.presence_of_element_located((By.ID, "audio-response"))) |
| _click(drv, field); _jitter(); _type(drv, field, answer) |
| except TimeoutException: |
| emit_log("audio-response field not found", "error"); return False |
| _pause(0.4, 0.8) |
| for by, sel in [(By.ID, "recaptcha-verify-button"), |
| (By.XPATH, '//button[contains(text(),"Verify")]')]: |
| try: |
| el = WebDriverWait(drv, 8).until(EC.element_to_be_clickable((by, sel))) |
| _click(drv, el); break |
| except TimeoutException: pass |
| time.sleep(random.uniform(3, 5)) |
| drv.switch_to.default_content() |
| |
| for fn in [ |
| lambda: drv.find_element(By.ID, "g-recaptcha-response").get_attribute("value"), |
| lambda: drv.execute_script("return document.getElementById('g-recaptcha-response').value;"), |
| lambda: drv.execute_script("return grecaptcha.getResponse();"), |
| ]: |
| try: |
| t = fn() |
| if t and len(t) > 20: |
| emit_log("Captcha solved!", "success"); return True |
| except Exception: pass |
| emit_log("Captcha token not found", "error") |
| return False |
|
|
| |
| |
| |
| def _navigate_to_proxy_list(drv) -> bool: |
| """Click 'View proxy list' or navigate directly β mirrors web.py _click_view_proxy_list.""" |
| emit_log("Navigating to proxy list...", "info") |
| selectors = [ |
| (By.XPATH, '//button[contains(@class,"MuiButton-containedPrimary")]'), |
| (By.XPATH, '//button[contains(.,"proxy") or contains(.,"Proxy") or contains(.,"list") or contains(.,"List")]'), |
| (By.XPATH, '//a[contains(@href,"/proxy/list")]'), |
| (By.CSS_SELECTOR, 'button.MuiButton-containedPrimary'), |
| (By.XPATH, '//button[contains(.,"Get Started") or contains(.,"Continue") or contains(.,"Dashboard")]'), |
| ] |
| deadline = time.time() + 45 |
| while time.time() < deadline: |
| try: |
| if "/proxy/list" in drv.current_url: |
| emit_log("Proxy list page reached", "success") |
| return True |
| except Exception: pass |
| for by, sel in selectors: |
| try: |
| els = drv.find_elements(by, sel) |
| for el in els: |
| if el.is_displayed() and el.is_enabled(): |
| _scroll_to(drv, el); _jitter(0.3, 0.7); _click(drv, el) |
| emit_log(f"Clicked: '{el.text.strip()[:50]}'", "info") |
| time.sleep(random.uniform(1.5, 2.5)) |
| try: |
| if "/proxy/list" in drv.current_url: |
| emit_log("Proxy list page reached", "success") |
| return True |
| except Exception: pass |
| except Exception: pass |
| time.sleep(1) |
|
|
| |
| try: |
| m = re.search(r'/(\d{5,12})/', drv.current_url) |
| if m: |
| acct = m.group(1) |
| direct = (f"https://dashboard.webshare.io/{acct}/proxy/list" |
| "?authenticationMethod=%22username_password%22" |
| "&connectionMethod=%22rotating%22") |
| drv.get(direct) |
| time.sleep(random.uniform(3, 5)) |
| emit_log("Navigated directly to proxy list", "info") |
| return True |
| except Exception: pass |
|
|
| |
| try: |
| drv.get("https://dashboard.webshare.io/proxy/list") |
| time.sleep(random.uniform(3, 5)) |
| emit_log("Navigated to generic proxy list URL", "info") |
| return True |
| except Exception: pass |
|
|
| emit_log("Could not navigate to proxy list page", "warn") |
| return False |
|
|
| |
| |
| |
| def _get_ws_token(drv, session_req) -> str | None: |
| """Extract Webshare API token β mirrors web.py _get_api_token exactly.""" |
| |
| try: |
| all_keys = drv.execute_script("return Object.keys(localStorage);") or [] |
| for key in all_keys: |
| val = drv.execute_script("return localStorage.getItem(arguments[0]);", key) or "" |
| val = val.strip().strip('"').strip("'") |
| if len(val) < 10: continue |
| try: |
| parsed = json.loads(val) |
| if isinstance(parsed, dict): |
| for sub in ("token", "access_token", "apiToken", "key"): |
| if sub in parsed and len(str(parsed[sub])) > 15: |
| emit_log(f"Token from localStorage[{key!r}][{sub!r}]", "info") |
| return str(parsed[sub]) |
| elif isinstance(parsed, str) and len(parsed) > 20: |
| if any(k in key.lower() for k in ("token", "auth", "key")): |
| return parsed |
| except (json.JSONDecodeError, TypeError): |
| if any(k in key.lower() for k in ("token", "auth", "apikey", "access")): |
| emit_log(f"Token from localStorage[{key!r}]", "info") |
| return val |
| except Exception: pass |
|
|
| |
| try: |
| for c in drv.get_cookies(): |
| name = c.get("name", "").lower() |
| if any(k in name for k in ("token", "auth", "session", "key")): |
| v = c.get("value", "") |
| if len(v) > 15: |
| emit_log(f"Token from cookie {c['name']!r}", "info") |
| return v |
| except Exception: pass |
|
|
| |
| try: |
| token = drv.execute_script(""" |
| try { |
| var store = window.__NEXT_REDUX_STORE__; |
| if (store) { |
| var state = store.getState(); |
| if (state && state.auth && state.auth.token) return state.auth.token; |
| if (state && state.user && state.user.token) return state.user.token; |
| } |
| var keys = ['token','apiToken','authToken','accessToken','API_TOKEN']; |
| for (var i=0;i<keys.length;i++) { |
| if (window[keys[i]] && window[keys[i]].length > 15) return window[keys[i]]; |
| } |
| } catch(e){} |
| return null; |
| """) |
| if token and len(str(token)) > 15: |
| emit_log("Token from JS global/Redux store", "info") |
| return str(token) |
| except Exception: pass |
|
|
| |
| try: |
| cookies = {c["name"]: c["value"] for c in drv.get_cookies()} |
| hdrs = { |
| "Referer": "https://dashboard.webshare.io/", |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0", |
| } |
| r = session_req.get(f"{WEBSHARE_API}/profile/", cookies=cookies, |
| headers=hdrs, timeout=10) |
| if r.status_code == 200: |
| data = r.json() |
| tok = data.get("token") or data.get("api_key") or data.get("key") |
| if tok: |
| emit_log("Token from /api/v2/profile/", "info") |
| return str(tok) |
| except Exception: pass |
|
|
| |
| try: |
| token = drv.execute_script(""" |
| try { |
| // Check if page stored any auth header in window |
| if (window._authToken && window._authToken.length > 15) return window._authToken; |
| // Scan all elements for data-token attributes |
| var els = document.querySelectorAll('[data-token],[data-api-key],[data-auth]'); |
| for (var i=0;i<els.length;i++) { |
| var t = els[i].getAttribute('data-token') || |
| els[i].getAttribute('data-api-key') || |
| els[i].getAttribute('data-auth'); |
| if (t && t.length > 15) return t; |
| } |
| } catch(e){} |
| return null; |
| """) |
| if token and len(str(token)) > 15: |
| emit_log("Token from DOM data attribute", "info") |
| return str(token) |
| except Exception: pass |
|
|
| return None |
|
|
| def _api_headers(token: str) -> dict: |
| return { |
| "Authorization": f"Token {token}", |
| "Content-Type": "application/json", |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0", |
| "Referer": "https://dashboard.webshare.io/", |
| "Origin": "https://dashboard.webshare.io", |
| } |
|
|
| def _dom_scrape_proxies(drv) -> list: |
| """DOM-scrape proxy table rows as fallback β ported from web.py.""" |
| try: |
| raw = drv.execute_script(""" |
| var rows = [], seen = new Set(); |
| document.querySelectorAll('tbody tr').forEach(function(tr) { |
| var cells = tr.querySelectorAll('td'); |
| if (cells.length < 2) return; |
| var row = []; |
| cells.forEach(function(td){ row.push((td.innerText||'').trim()); }); |
| var key = row.join('|'); |
| if (!seen.has(key)){ seen.add(key); rows.push(row); } |
| }); |
| if (!rows.length) { |
| document.querySelectorAll('[role="row"]').forEach(function(tr) { |
| var cells = tr.querySelectorAll('[role="cell"],[role="columnheader"]'); |
| if (cells.length < 2) return; |
| var row = []; |
| cells.forEach(function(td){ row.push((td.innerText||'').trim()); }); |
| var key = row.join('|'); |
| if (!seen.has(key)){ seen.add(key); rows.push(row); } |
| }); |
| } |
| return JSON.stringify(rows); |
| """) |
| if raw: |
| return json.loads(raw) |
| except Exception: pass |
| return [] |
|
|
| def _dom_scrape_rotating(drv) -> dict: |
| """Extract rotating endpoint details from DOM code/input elements.""" |
| try: |
| raw = drv.execute_script(""" |
| var info = { codeBlocks: [], pageSnippet: '' }; |
| document.querySelectorAll('code, pre, [class*="code"], [class*="endpoint"]').forEach(function(el){ |
| var t = (el.innerText||el.textContent||'').trim(); |
| if (t.length > 3 && t.length < 500) info.codeBlocks.push(t); |
| }); |
| document.querySelectorAll('input[readonly], input[disabled], input[value]').forEach(function(el){ |
| var v = (el.value||el.getAttribute('value')||'').trim(); |
| if (v.length > 3) info.codeBlocks.push((el.getAttribute('placeholder')||el.id||'?')+'='+v); |
| }); |
| info.pageSnippet = (document.body.innerText||'').substring(0,4000); |
| return JSON.stringify(info); |
| """) |
| if raw: |
| return json.loads(raw) |
| except Exception: pass |
| return {} |
|
|
| def _fetch_proxies_from_ws(token: str, session_req, drv=None) -> dict: |
| """Full proxy fetch β mirrors web.py _fetch_static_proxies + _fetch_rotating_config.""" |
| hdrs = _api_headers(token) |
| result = {"rotating": [], "static": []} |
|
|
| |
| static_raw = [] |
| for mode in ("direct", "backconnect", ""): |
| if static_raw: break |
| params = f"mode={mode}&page=1&page_size=100" if mode else "page=1&page_size=100" |
| url = f"{WEBSHARE_API}/proxy/list/?{params}" |
| try: |
| r = session_req.get(url, headers=hdrs, timeout=20) |
| emit_log(f"proxy/list ({mode or 'default'}) β HTTP {r.status_code}", "info") |
| if r.status_code == 200: |
| data = r.json() |
| raw = data.get("results", data.get("proxy_list", [])) |
| if raw: |
| static_raw = raw |
| except Exception as ex: |
| emit_log(f"proxy/list error ({mode}): {ex}", "warn") |
|
|
| for p in static_raw: |
| host = (p.get("proxy_address") or p.get("address") or |
| p.get("host") or p.get("hostname") or p.get("ip") or "") |
| port = str(p.get("port") or p.get("proxy_port") or "") |
| user = p.get("username") or p.get("user") or "" |
| pw = p.get("password") or p.get("pass") or "" |
| if host and port: |
| result["static"].append(f"{host}:{port}:{user}:{pw}" if user else f"{host}:{port}") |
|
|
| |
| rotating_cfg = {} |
| for endpoint in [f"{WEBSHARE_API}/proxy/config/", |
| f"{WEBSHARE_API}/profile/", |
| f"{WEBSHARE_API}/proxy/rotating/", |
| f"{WEBSHARE_API}/proxy/stats/", |
| f"{WEBSHARE_API}/subscription/"]: |
| if rotating_cfg: break |
| try: |
| r = session_req.get(endpoint, headers=hdrs, timeout=15) |
| ep_name = endpoint.split("v2/")[1] |
| emit_log(f"{ep_name} β HTTP {r.status_code}", "info") |
| if r.status_code == 200: |
| rotating_cfg = r.json() |
| except Exception: pass |
|
|
| |
| rhost = "" |
| for k in ("proxy_address", "address", "host", "hostname", "rotating_proxy_address"): |
| if rotating_cfg.get(k): |
| rhost = rotating_cfg[k]; break |
| if not rhost: |
| rhost = "rotating-proxy.webshare.io" |
|
|
| rport = str(rotating_cfg.get("port") or |
| rotating_cfg.get("ports", {}).get("http", "") or |
| rotating_cfg.get("rotating_proxy_port") or "80") |
| ruser = rotating_cfg.get("username") or rotating_cfg.get("user") or "" |
| rpw = rotating_cfg.get("password") or rotating_cfg.get("pass") or "" |
|
|
| |
| if not ruser and static_raw: |
| ruser = static_raw[0].get("username") or static_raw[0].get("user") or "" |
| rpw = static_raw[0].get("password") or static_raw[0].get("pass") or "" |
|
|
| if rhost and rport: |
| line = f"{rhost}:{rport}:{ruser}:{rpw}" if ruser else f"{rhost}:{rport}" |
| result["rotating"].append(line) |
|
|
| |
| if not result["static"] and drv: |
| emit_log("API returned no static proxies, trying DOM scrape...", "warn") |
| dom_rows = _dom_scrape_proxies(drv) |
| emit_log(f"DOM scraped {len(dom_rows)} proxy rows", "info") |
| |
| for row in dom_rows: |
| if len(row) >= 2: |
| ip = row[0].strip() |
| port = row[1].strip() |
| user = row[2].strip() if len(row) > 2 else "" |
| pw = row[3].strip() if len(row) > 3 else "" |
| if re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', ip) and port.isdigit(): |
| result["static"].append(f"{ip}:{port}:{user}:{pw}" if user else f"{ip}:{port}") |
|
|
| if not result["rotating"] and drv: |
| emit_log("Trying DOM scrape for rotating endpoint...", "warn") |
| rot_dom = _dom_scrape_rotating(drv) |
| |
| snippet = rot_dom.get("pageSnippet", "") |
| matches = re.findall(r'((?:rotating[-.])?[\w.-]+\.webshare\.io):(\d{2,5})', snippet) |
| for h, p in matches[:2]: |
| result["rotating"].append(f"{h}:{p}:{ruser}:{rpw}" if ruser else f"{h}:{p}") |
| |
| for block in rot_dom.get("codeBlocks", [])[:8]: |
| m = re.search(r'([\w.-]+\.webshare\.io):(\d{2,5})', block) |
| if m and not any(m.group(1) in r for r in result["rotating"]): |
| h, p = m.group(1), m.group(2) |
| result["rotating"].append(f"{h}:{p}:{ruser}:{rpw}" if ruser else f"{h}:{p}") |
|
|
| return result |
|
|
| |
| |
| |
| _EMAIL_ALREADY_USED_HINTS = [ |
| "already registered", "already in use", "email is already", |
| "already exists", "already taken", "account already", |
| "already have an account", "email already", |
| ] |
|
|
| def _check_email_already_used(drv) -> bool: |
| """Return True if the page shows an 'email already registered' error.""" |
| try: |
| src = drv.page_source.lower() |
| if any(h in src for h in _EMAIL_ALREADY_USED_HINTS): |
| return True |
| |
| for sel in ['.error', '.alert', '[class*="error"]', '[class*="Error"]', |
| '[class*="alert"]', '[role="alert"]', '.MuiFormHelperText-root']: |
| try: |
| els = drv.find_elements(By.CSS_SELECTOR, sel) |
| for el in els: |
| txt = (el.text or "").lower() |
| if any(h in txt for h in _EMAIL_ALREADY_USED_HINTS): |
| return True |
| except Exception: |
| pass |
| except Exception: |
| pass |
| return False |
|
|
| def _harvest_worker(email_data: dict, password: str, proxy: dict | None) -> dict: |
| """Register one email, retry up to 3Γ, detect already-used, emit proxies live.""" |
| email = email_data["email"] |
| result = {"email": email, "rotating": [], "static": [], "ok": False, "error": ""} |
|
|
| MAX_RETRIES = 3 |
| for attempt in range(1, MAX_RETRIES + 1): |
| drv = None |
| tunnel = None |
| req_session = requests.Session() |
| start_ts = time.time() |
| try: |
| prefix = f"[{email}] Attempt {attempt}/{MAX_RETRIES}" |
| emit_log(f"{prefix} β starting", "info") |
| if proxy: |
| ptype = proxy.get("type", "static") |
| emit_log(f"{prefix} β proxy ({ptype}): {proxy['host']}:{proxy['port']}", "proxy") |
| if proxy.get("user"): |
| pu = f"http://{proxy['user']}:{proxy['pass']}@{proxy['host']}:{proxy['port']}" |
| req_session.proxies.update({"http": pu, "https": pu}) |
| drv, tunnel = build_driver(proxy=proxy, headless=True) |
| elapsed = time.time() - start_ts |
| emit_log(f"{prefix} β browser launched in {elapsed:.1f}s", "info") |
|
|
| drv.get(TARGET_URL) |
| emit_log(f"{prefix} β register page loaded", "info") |
| time.sleep(random.uniform(1.5, 3.0)) |
| drv.execute_script(STEALTH_JS) |
|
|
| if not fill_form(drv, email, password): |
| result["error"] = "Form fill failed" |
| if attempt < MAX_RETRIES: |
| emit_log(f"{prefix} β form fill failed, retrying...", "warn") |
| continue |
| return result |
|
|
| if not click_signup(drv): |
| result["error"] = "Signup click failed" |
| if attempt < MAX_RETRIES: |
| emit_log(f"{prefix} β signup click failed, retrying...", "warn") |
| continue |
| return result |
|
|
| _pause(2.0, 4.0) |
|
|
| |
| if _check_email_already_used(drv): |
| emit_log(f"{prefix} β email already registered on Webshare, skipping", "warn") |
| result["error"] = "email_already_used" |
| return result |
|
|
| |
| captcha_ok = solve_captcha(drv) |
| if captcha_ok: |
| emit_log(f"{prefix} β captcha solved", "success") |
| else: |
| emit_log(f"{prefix} β captcha failed", "warn") |
| if attempt < MAX_RETRIES: |
| emit_log(f"{prefix} β retrying after captcha failure...", "warn") |
| continue |
|
|
| |
| _pause(1.5, 3.0) |
| if _check_email_already_used(drv): |
| emit_log(f"{prefix} β email already registered (post-captcha), skipping", "warn") |
| result["error"] = "email_already_used" |
| return result |
|
|
| |
| deadline = time.time() + 35 |
| reached_dash = False |
| while time.time() < deadline: |
| try: |
| cur = drv.current_url |
| if "dashboard" in cur: |
| reached_dash = True; break |
| |
| if "register" in cur and _check_email_already_used(drv): |
| emit_log(f"{prefix} β email already used (register loop), skipping", "warn") |
| result["error"] = "email_already_used" |
| return result |
| except Exception: |
| pass |
| time.sleep(1) |
|
|
| if not reached_dash: |
| emit_log(f"{prefix} β did not reach dashboard, retrying...", "warn") |
| if attempt < MAX_RETRIES: |
| continue |
| result["error"] = "Dashboard redirect timeout" |
| return result |
|
|
| emit_log(f"{prefix} β dashboard reached", "success") |
| drv.execute_script(STEALTH_JS) |
|
|
| |
| time.sleep(random.uniform(2, 3)) |
| _navigate_to_proxy_list(drv) |
| time.sleep(random.uniform(3, 5)) |
|
|
| |
| token = _get_ws_token(drv, req_session) |
|
|
| |
| if not token: |
| emit_log(f"{prefix} β token not found yet, waiting 5s...", "warn") |
| time.sleep(5) |
| token = _get_ws_token(drv, req_session) |
|
|
| if token: |
| emit_log(f"{prefix} β API token obtained β", "success") |
| |
| fetched = _fetch_proxies_from_ws(token, req_session, drv) |
| for pline in fetched["rotating"]: |
| emit_proxy(pline, "rotating") |
| result["rotating"].append(pline) |
| for pline in fetched["static"]: |
| emit_proxy(pline, "static") |
| result["static"].append(pline) |
| emit_log(f"{prefix} β fetched {len(result['rotating'])} rotating, " |
| f"{len(result['static'])} static", "success") |
| else: |
| |
| emit_log(f"{prefix} β no API token, falling back to DOM scrape", "warn") |
| dom_rows = _dom_scrape_proxies(drv) |
| emit_log(f"{prefix} β DOM scraped {len(dom_rows)} rows", "info") |
| for row in dom_rows: |
| if len(row) >= 2: |
| ip = row[0].strip() |
| port = row[1].strip() |
| user = row[2].strip() if len(row) > 2 else "" |
| pw = row[3].strip() if len(row) > 3 else "" |
| if re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', ip) and port.isdigit(): |
| line = f"{ip}:{port}:{user}:{pw}" if user else f"{ip}:{port}" |
| emit_proxy(line, "static") |
| result["static"].append(line) |
| rot_dom = _dom_scrape_rotating(drv) |
| snippet = rot_dom.get("pageSnippet", "") |
| for h, p in re.findall(r'([\w.-]+\.webshare\.io):(\d{2,5})', snippet)[:2]: |
| line = f"{h}:{p}" |
| emit_proxy(line, "rotating") |
| result["rotating"].append(line) |
| if result["static"] or result["rotating"]: |
| emit_log(f"{prefix} β DOM fallback: {len(result['rotating'])} rot, " |
| f"{len(result['static'])} sta", "success") |
|
|
| result["ok"] = True |
| return result |
|
|
| except Exception as ex: |
| result["error"] = str(ex) |
| emit_log(f"{prefix} β exception: {ex}", "error") |
| if attempt < MAX_RETRIES: |
| emit_log(f"{prefix} β retrying in 3s...", "warn") |
| time.sleep(3) |
| finally: |
| if drv: |
| try: drv.quit() |
| except Exception: pass |
| if tunnel: |
| try: tunnel.stop() |
| except Exception: pass |
| req_session.close() |
|
|
| return result |
|
|
| def _run_task(task_id: str, emails: list, threads: int, password: str): |
| """Background thread β processes all emails, updates task state live.""" |
| emit_log(f"Task {task_id} started | {len(emails)} emails | {threads} threads", "success") |
| total = len(emails) |
|
|
| with _state_lock: |
| _state["current_task"]["total"] = total |
| _state["current_task"]["status"] = "running" |
|
|
| def process_one(email_data): |
| with _state_lock: |
| if _state["stop_flag"]: |
| return None |
| with _state_lock: |
| alive_pool = [p for p in _state["proxies"] if p["status"] == "alive"] |
| proxy = random.choice(alive_pool) if alive_pool else None |
| return _harvest_worker(email_data, password, proxy) |
|
|
| done = 0 |
| pool = concurrent.futures.ThreadPoolExecutor(max_workers=threads) |
| futures = {pool.submit(process_one, e): e for e in emails} |
| try: |
| for fut in concurrent.futures.as_completed(futures): |
| with _state_lock: |
| if _state["stop_flag"]: |
| emit_log("Stop flag β cancelling remaining jobs", "warn") |
| pool.shutdown(wait=False, cancel_futures=True) |
| break |
| try: |
| fut.result() |
| except Exception as ex2: |
| emit_log(f"Worker exception: {ex2}", "error") |
| done += 1 |
| with _state_lock: |
| if _state["current_task"]: |
| _state["current_task"]["progress"] = int((done / total) * 100) |
| finally: |
| pool.shutdown(wait=False) |
|
|
| |
| now = datetime.now().isoformat() |
| with _state_lock: |
| ct = _state["current_task"] |
| if ct: |
| all_rotating = list(ct.get("rotating_list", [])) |
| all_static = list(ct.get("static_list", [])) |
| ct["status"] = "complete" |
| ct["progress"] = 100 |
| ct["rotating_fetched"] = len(all_rotating) |
| ct["static_fetched"] = len(all_static) |
| ct["end_time"] = now |
| task_copy = dict(ct) |
| _state["tasks"].append(task_copy) |
| else: |
| all_rotating, all_static = [], [] |
| task_copy = None |
| if task_copy: |
| _db_upsert_task(task_copy) |
|
|
| |
| out_dir = os.path.join(OUTPUT_DIR, task_id) |
| os.makedirs(out_dir, exist_ok=True) |
| for fname, lines in [("rotating.txt", all_rotating), ("static.txt", all_static)]: |
| if lines: |
| with open(os.path.join(out_dir, fname), "w") as f: |
| f.write("\n".join(lines)) |
|
|
| emit_log(f"Task {task_id} complete | Rotating: {len(all_rotating)} | " |
| f"Static: {len(all_static)}", "success") |
|
|
| |
| @app.route("/api/task/start", methods=["POST"]) |
| @auth_required |
| def start_task(): |
| with _state_lock: |
| if (_state["current_task"] and |
| _state["current_task"]["status"] in ("running", "starting")): |
| return jsonify({"error": "Task already running"}), 400 |
| emails = list(_state["emails"]) |
| if not emails: |
| return jsonify({"error": "No emails loaded"}), 400 |
| data = request.get_json(silent=True) or {} |
| threads = max(1, min(int(data.get("threads", 5)), 50)) |
| password = data.get("password", DEFAULT_PASS) or DEFAULT_PASS |
| task_id = str(uuid.uuid4())[:8].upper() |
| with _state_lock: |
| _state["stop_flag"] = False |
| _state["current_task"] = { |
| "id": task_id, |
| "status": "starting", |
| "progress": 0, |
| "total": len(emails), |
| "threads": threads, |
| "rotating_fetched": 0, |
| "static_fetched": 0, |
| "start_time": datetime.now().isoformat(), |
| "end_time": None, |
| "logs": [], |
| "rotating_list": [], |
| "static_list": [], |
| } |
| threading.Thread( |
| target=_run_task, |
| args=(task_id, emails, threads, password), |
| daemon=True, name=f"task-{task_id}" |
| ).start() |
| return jsonify({"ok": True, "task_id": task_id}) |
|
|
| @app.route("/api/task/stop", methods=["POST"]) |
| @auth_required |
| def stop_task(): |
| with _state_lock: |
| _state["stop_flag"] = True |
| if _state["current_task"]: |
| _state["current_task"]["status"] = "stopping" |
| emit_log("Stop signal sent by user", "warn") |
| return jsonify({"ok": True}) |
|
|
| @app.route("/api/task/status") |
| @auth_required |
| def task_status(): |
| with _state_lock: |
| task = dict(_state["current_task"]) if _state["current_task"] else None |
| if task: task.pop("logs", None) |
| return jsonify({"task": task}) |
|
|
| @app.route("/api/task/live_export") |
| @auth_required |
| def task_live_export(): |
| ptype = request.args.get("type", "rotating") |
| with _state_lock: |
| ct = _state["current_task"] |
| if ct: |
| if ptype == "rotating": |
| lines = list(ct.get("rotating_list", [])) |
| fname = "live_rotating.txt" |
| else: |
| lines = list(ct.get("static_list", [])) |
| fname = "live_static.txt" |
| else: |
| lines, fname = [], f"live_{ptype}.txt" |
| return Response("\n".join(lines), mimetype="text/plain", |
| headers={"Content-Disposition": f"attachment; filename={fname}"}) |
|
|
| @app.route("/api/task/live") |
| @auth_required |
| def task_live_proxies(): |
| """Return live proxy lists of the currently running (or last) task.""" |
| with _state_lock: |
| ct = _state["current_task"] |
| if ct: |
| return jsonify({ |
| "rotating": list(ct.get("rotating_list", [])), |
| "static": list(ct.get("static_list", [])), |
| "rotating_count": ct.get("rotating_fetched", 0), |
| "static_count": ct.get("static_fetched", 0), |
| }) |
| return jsonify({"rotating": [], "static": [], "rotating_count": 0, "static_count": 0}) |
|
|
| @app.route("/api/task/history") |
| @auth_required |
| def get_task_history(): |
| with _state_lock: |
| tasks = [] |
| for t in _state["tasks"]: |
| tc = dict(t) |
| tc.pop("logs", None) |
| tc.pop("rotating_list", None) |
| tc.pop("static_list", None) |
| tasks.append(tc) |
| active = dict(_state["current_task"]) if _state["current_task"] else None |
| if active: |
| active.pop("logs", None) |
| active.pop("rotating_list", None) |
| active.pop("static_list", None) |
| return jsonify({"active_task": active, "tasks": tasks}) |
|
|
| @app.route("/api/task/history/download") |
| @auth_required |
| def download_task_history_summary(): |
| with _state_lock: |
| active = dict(_state["current_task"]) if _state["current_task"] else None |
| tasks = [dict(t) for t in _state["tasks"]] |
| rows = [] |
| if active and active.get("status") in ("starting", "running", "stopping"): |
| rows.append(active) |
| rows.extend(tasks) |
| lines = ["task_id,status,start_time,end_time,total,threads,rotating,static,total_fetched"] |
| for t in rows: |
| rot = int(t.get("rotating_fetched", 0) or 0) |
| sta = int(t.get("static_fetched", 0) or 0) |
| parts = [ |
| str(t.get("id", "")), |
| str(t.get("status", "")), |
| str(t.get("start_time", "") or ""), |
| str(t.get("end_time", "") or ""), |
| str(int(t.get("total", 0) or 0)), |
| str(int(t.get("threads", 0) or 0)), |
| str(rot), |
| str(sta), |
| str(rot + sta), |
| ] |
| lines.append(",".join(parts)) |
| return Response( |
| "\n".join(lines), |
| mimetype="text/csv", |
| headers={"Content-Disposition": "attachment; filename=task_history_summary.csv"}, |
| ) |
|
|
| @app.route("/api/task/<tid>/download/<ptype>") |
| @auth_required |
| def download_task_proxies(tid, ptype): |
| with _state_lock: |
| task = next((t for t in _state["tasks"] if t["id"] == tid), None) |
| if not task: |
| cur = _state["current_task"] |
| if cur and cur["id"] == tid: |
| task = cur |
| if not task: |
| return jsonify({"error": "Task not found"}), 404 |
| if ptype == "rotating": |
| lines = task.get("rotating_list", []) |
| fname = f"rotating_{tid}.txt" |
| elif ptype == "static": |
| lines = task.get("static_list", []) |
| fname = f"static_{tid}.txt" |
| elif ptype == "all": |
| rot = task.get("rotating_list", []) |
| sta = task.get("static_list", []) |
| lines = [ |
| "# rotating", |
| *rot, |
| "", |
| "# static", |
| *sta, |
| ] |
| fname = f"all_{tid}.txt" |
| else: |
| return jsonify({"error": "Invalid type"}), 400 |
| return Response("\n".join(lines), mimetype="text/plain", |
| headers={"Content-Disposition": f"attachment; filename={fname}"}) |
|
|
| @app.route("/api/task/<tid>/logs") |
| @auth_required |
| def get_task_logs(tid): |
| with _state_lock: |
| task = next((t for t in _state["tasks"] if t["id"] == tid), None) |
| if not task: |
| cur = _state["current_task"] |
| if cur and cur["id"] == tid: |
| task = cur |
| if not task: |
| return jsonify({"error": "Task not found"}), 404 |
| return jsonify({"logs": task.get("logs", [])}) |
|
|
| |
| @app.route("/api/logs/stream") |
| @auth_required |
| def log_stream(): |
| def generate(): |
| q = queue.Queue(maxsize=500) |
| with _state_lock: |
| _log_listeners.append(q) |
| try: |
| yield 'data: {"ts":"","msg":"Log stream connected","level":"info"}\n\n' |
| while True: |
| try: |
| data = q.get(timeout=25) |
| yield data |
| except queue.Empty: |
| yield ": ping\n\n" |
| except GeneratorExit: |
| pass |
| finally: |
| with _state_lock: |
| try: _log_listeners.remove(q) |
| except ValueError: pass |
| return Response(generate(), mimetype="text/event-stream", |
| headers={"Cache-Control": "no-cache", |
| "X-Accel-Buffering": "no", |
| "Connection": "keep-alive"}) |
|
|
| |
| |
| |
| HTML = r"""<!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width,initial-scale=1"> |
| <title>Sukuna Webshare Harvester</title> |
| <style> |
| *{box-sizing:border-box;margin:0;padding:0} |
| :root{ |
| --bg:#080808;--bg2:#0f0f0f;--bg3:#161616;--bg4:#1c1c1c; |
| --bd:#222;--bd2:#2a2a2a; |
| --acc:#00ff88;--acc2:#00ccff;--acc3:#ff9500; |
| --txt:#d0d0d0;--txt2:#666;--txt3:#444; |
| --red:#ff3b30;--grn:#00ff88;--ylw:#ffcc00; |
| --font:'Courier New',Courier,monospace; |
| } |
| html,body{height:100%;background:var(--bg);color:var(--txt);font-family:var(--font);font-size:12px} |
| |
| /* ββ Scrollbars ββ */ |
| ::-webkit-scrollbar{width:4px;height:4px} |
| ::-webkit-scrollbar-track{background:var(--bg2)} |
| ::-webkit-scrollbar-thumb{background:var(--bd2);border-radius:2px} |
| |
| /* ββ LOGIN ββ */ |
| #login-overlay{ |
| display:flex;position:fixed;inset:0;background:var(--bg); |
| align-items:center;justify-content:center;z-index:9000; |
| } |
| .login-box{ |
| background:var(--bg2);border:1px solid var(--bd2);border-radius:8px; |
| padding:36px 28px;width:300px; |
| } |
| .login-logo{color:var(--acc);font-size:16px;letter-spacing:3px;font-weight:bold;margin-bottom:4px} |
| .login-sub{color:var(--txt2);font-size:10px;margin-bottom:24px;letter-spacing:1px} |
| |
| /* ββ HEADER ββ */ |
| .hdr{ |
| background:var(--bg2);border-bottom:1px solid var(--bd); |
| padding:10px 16px;display:flex;align-items:center; |
| justify-content:space-between;position:sticky;top:0;z-index:100; |
| } |
| .hdr-left{display:flex;align-items:center;gap:10px} |
| .hdr-title{color:var(--acc);font-size:14px;letter-spacing:3px;font-weight:bold} |
| .hdr-right{display:flex;align-items:center;gap:10px} |
| .menu-btn{ |
| width:28px;height:24px;background:transparent;border:1px solid var(--bd2);border-radius:4px; |
| display:flex;flex-direction:column;justify-content:center;gap:3px;padding:0 6px;cursor:pointer; |
| } |
| .menu-btn span{display:block;height:1px;background:var(--acc);width:100%} |
| .menu-btn:hover{border-color:var(--acc)} |
| .menu-backdrop{ |
| display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:3000; |
| } |
| .menu-backdrop.show{display:block} |
| .menu-drawer{ |
| position:fixed;left:-240px;top:0;bottom:0;width:220px;background:var(--bg2); |
| border-right:1px solid var(--bd2);padding:16px 10px;z-index:3200;transition:left .2s ease; |
| } |
| .menu-drawer.show{left:0} |
| .menu-title{ |
| color:var(--acc);font-size:10px;letter-spacing:2px;text-transform:uppercase; |
| border-bottom:1px solid var(--bd);padding-bottom:8px;margin-bottom:10px; |
| } |
| .menu-item{ |
| width:100%;text-align:left;background:transparent;border:1px solid var(--bd);color:var(--txt); |
| border-radius:4px;padding:8px 10px;font-family:var(--font);font-size:10px;letter-spacing:1px; |
| text-transform:uppercase;cursor:pointer;margin-bottom:6px; |
| } |
| .menu-item:hover,.menu-item.active{border-color:var(--acc);color:var(--acc)} |
| .view-section{display:none} |
| .view-section.show{display:block} |
| .dot{width:7px;height:7px;border-radius:50%;background:var(--txt3);display:inline-block} |
| .dot.on{background:var(--grn);box-shadow:0 0 6px var(--grn)} |
| .dot.run{background:var(--acc3);box-shadow:0 0 6px var(--acc3);animation:pulse 1s infinite} |
| @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}} |
| |
| /* ββ MAIN GRID ββ */ |
| .grid{ |
| display:grid; |
| grid-template-columns:1fr 1fr 1fr; |
| gap:10px;padding:12px; |
| } |
| @media(max-width:900px){.grid{grid-template-columns:1fr 1fr}} |
| @media(max-width:560px){.grid{grid-template-columns:1fr}} |
| |
| /* ββ CARD ββ */ |
| .card{background:var(--bg2);border:1px solid var(--bd);border-radius:6px;padding:10px} |
| .card-title{ |
| font-size:9px;letter-spacing:2px;text-transform:uppercase; |
| color:var(--acc);border-bottom:1px solid var(--bd);padding-bottom:6px;margin-bottom:8px; |
| display:flex;align-items:center;justify-content:space-between; |
| } |
| .card-title span{color:var(--acc2);font-size:10px;letter-spacing:0} |
| |
| /* ββ INPUTS ββ */ |
| textarea,input[type=text],input[type=password]{ |
| width:100%;background:var(--bg);border:1px solid var(--bd);border-radius:4px; |
| color:var(--txt);font-family:var(--font);font-size:11px;padding:6px 8px; |
| outline:none;transition:border-color .15s; |
| } |
| textarea:focus,input:focus{border-color:var(--acc)} |
| textarea{resize:vertical;min-height:80px} |
| input[type=range]{width:100%;accent-color:var(--acc);margin:6px 0} |
| .input-row{display:flex;gap:6px;align-items:center;margin-bottom:6px} |
| .input-row input{flex:1} |
| |
| /* ββ BUTTONS ββ */ |
| .btn{ |
| background:transparent;border:1px solid var(--acc);color:var(--acc); |
| border-radius:4px;padding:5px 10px;font-size:10px;letter-spacing:1px; |
| text-transform:uppercase;cursor:pointer;font-family:var(--font); |
| transition:background .15s,color .15s;white-space:nowrap; |
| } |
| .btn:hover{background:var(--acc);color:var(--bg)} |
| .btn:active{opacity:.7} |
| .btn.red{border-color:var(--red);color:var(--red)} |
| .btn.red:hover{background:var(--red);color:#fff} |
| .btn.blue{border-color:var(--acc2);color:var(--acc2)} |
| .btn.blue:hover{background:var(--acc2);color:var(--bg)} |
| .btn.ylw{border-color:var(--ylw);color:var(--ylw)} |
| .btn.ylw:hover{background:var(--ylw);color:var(--bg)} |
| .btn.big{padding:7px 16px;font-size:11px} |
| .btn.block{width:100%;margin-bottom:6px} |
| .btn-row{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px} |
| .btn:disabled{opacity:.3;cursor:not-allowed} |
| |
| /* ββ BADGES ββ */ |
| .badge{ |
| display:inline-block;background:var(--bg3);border:1px solid var(--bd2); |
| border-radius:10px;padding:1px 7px;font-size:9px;color:var(--acc2); |
| } |
| .badge.grn{color:var(--grn);border-color:var(--grn)30} |
| .badge.red{color:var(--red);border-color:var(--red)30} |
| .badge.ylw{color:var(--ylw);border-color:var(--ylw)30} |
| |
| /* ββ STATS ROW ββ */ |
| .stats{display:flex;gap:8px;flex-wrap:wrap;margin:6px 0} |
| .stat{background:var(--bg3);border:1px solid var(--bd);border-radius:4px;padding:4px 8px;flex:1;min-width:60px} |
| .stat-label{font-size:8px;color:var(--txt2);letter-spacing:1px;text-transform:uppercase} |
| .stat-val{font-size:14px;color:var(--acc);font-weight:bold} |
| .stat-val.blue{color:var(--acc2)} |
| .stat-val.red{color:var(--red)} |
| |
| /* ββ PROGRESS ββ */ |
| .prog-wrap{background:var(--bg3);border:1px solid var(--bd);border-radius:3px;height:14px;overflow:hidden;margin:8px 0} |
| .prog-bar{height:100%;background:var(--acc);transition:width .4s ease;width:0%;position:relative;min-width:0} |
| .prog-bar::after{ |
| content:attr(data-p);position:absolute;right:4px;top:0; |
| font-size:9px;line-height:14px;color:var(--bg); |
| } |
| |
| /* ββ LOG BOX ββ */ |
| .log-box{ |
| background:var(--bg);border:1px solid var(--bd);border-radius:4px; |
| height:220px;overflow-y:auto;padding:6px;font-size:10px; |
| } |
| .log-line{padding:1px 0;border-bottom:1px solid #111;display:flex;gap:6px} |
| .log-ts{color:var(--txt3);flex-shrink:0;font-size:9px} |
| .log-msg{word-break:break-all} |
| .log-line.info .log-msg{color:#aaa} |
| .log-line.success .log-msg{color:var(--grn)} |
| .log-line.warn .log-msg{color:var(--acc3)} |
| .log-line.error .log-msg{color:var(--red)} |
| .log-line.proxy .log-msg{color:var(--acc2)} |
| .log-line.proxy_rot .log-msg{color:var(--grn);font-weight:bold} |
| .log-line.proxy_sta .log-msg{color:var(--acc2);font-weight:bold} |
| |
| /* ββ LIVE PROXY FEED ββ */ |
| .live-feed{margin-top:8px} |
| .live-total{ |
| display:flex;align-items:center;gap:8px;margin-bottom:6px; |
| background:var(--bg3);border:1px solid var(--bd);border-radius:4px;padding:5px 8px; |
| } |
| .live-total-num{font-size:18px;font-weight:bold;color:var(--acc)} |
| .live-total-label{font-size:9px;color:var(--txt2);letter-spacing:1px} |
| .live-total-sep{color:var(--bd2);margin:0 2px} |
| .live-tabs{display:flex;gap:0;margin-bottom:0;border-bottom:1px solid var(--bd)} |
| .live-tab{ |
| padding:4px 10px;font-size:9px;letter-spacing:1px;text-transform:uppercase; |
| color:var(--txt2);cursor:pointer;border-bottom:2px solid transparent; |
| transition:color .15s,border-color .15s; |
| } |
| .live-tab.active{color:var(--acc);border-bottom-color:var(--acc)} |
| .live-tab:hover:not(.active){color:var(--txt)} |
| .live-panel{display:none} |
| .live-panel.show{display:block} |
| .live-list{ |
| background:var(--bg);border:1px solid var(--bd);border-top:none; |
| border-radius:0 0 4px 4px;height:140px;overflow-y:auto; |
| padding:4px 6px;font-size:10px;font-family:var(--font); |
| } |
| .live-list-item{ |
| padding:1px 0;border-bottom:1px solid #111; |
| word-break:break-all;color:var(--grn); |
| } |
| .live-list-item.sta{color:var(--acc2)} |
| .live-actions{display:flex;gap:6px;margin-top:5px;align-items:center} |
| .live-count-pill{ |
| background:var(--bg3);border:1px solid var(--bd);border-radius:10px; |
| padding:1px 8px;font-size:9px;color:var(--acc); |
| } |
| |
| /* ββ TASK HISTORY ββ */ |
| .history-wrap{padding:12px} |
| .history-head,.history-row{ |
| display:grid; |
| grid-template-columns:100px 90px 156px 64px 64px 52px 52px 58px 92px; |
| gap:6px;align-items:center; |
| } |
| .history-head{ |
| color:var(--txt2);font-size:9px;letter-spacing:1px;text-transform:uppercase; |
| border-bottom:1px solid var(--bd);padding:0 6px 6px; |
| } |
| .history-list{max-height:420px;overflow-y:auto;padding-top:6px} |
| .history-row{ |
| background:var(--bg3);border:1px solid var(--bd);border-radius:4px; |
| padding:6px;margin-bottom:6px;font-size:10px;cursor:pointer;transition:border-color .15s; |
| } |
| .history-row:hover{border-color:var(--acc)} |
| .history-id{color:var(--acc2);letter-spacing:1px} |
| .active-box{ |
| border:1px solid var(--bd);border-radius:4px;background:var(--bg3);padding:8px;margin-bottom:10px; |
| font-size:10px;color:var(--txt2) |
| } |
| .active-box b{color:var(--acc);font-size:11px} |
| @media(max-width:1100px){ |
| .history-head,.history-row{grid-template-columns:90px 80px 140px 56px 56px 46px 46px 52px 86px} |
| } |
| @media(max-width:900px){ |
| .history-head,.history-row{min-width:760px} |
| .history-scroll{overflow-x:auto} |
| } |
| |
| /* ββ BOTTOM GRID ββ */ |
| .bottom-grid{ |
| display:grid;grid-template-columns:1fr; |
| gap:10px;padding:0 12px 12px; |
| } |
| |
| /* ββ MODAL ββ */ |
| .modal-bg{ |
| display:none;position:fixed;inset:0;background:rgba(0,0,0,.85); |
| z-index:5000;align-items:center;justify-content:center; |
| } |
| .modal-bg.show{display:flex} |
| .modal{ |
| background:var(--bg2);border:1px solid var(--bd2);border-radius:8px; |
| padding:20px;width:90%;max-width:480px;max-height:80vh;overflow-y:auto; |
| } |
| .modal-title{color:var(--acc);font-size:12px;letter-spacing:2px; |
| text-transform:uppercase;margin-bottom:12px;border-bottom:1px solid var(--bd);padding-bottom:8px} |
| |
| /* ββ NOTIF ββ */ |
| .notif{ |
| position:fixed;bottom:16px;right:16px;background:var(--bg2); |
| border:1px solid var(--acc);color:var(--acc);padding:7px 14px; |
| border-radius:4px;font-size:10px;z-index:9999;opacity:0; |
| transition:opacity .25s;pointer-events:none;letter-spacing:1px; |
| } |
| .notif.show{opacity:1} |
| .notif.red{border-color:var(--red);color:var(--red)} |
| .notif.ylw{border-color:var(--ylw);color:var(--ylw)} |
| |
| /* ββ RANGE LABEL ββ */ |
| .range-row{display:flex;align-items:center;gap:8px;margin-bottom:4px} |
| .range-row label{font-size:9px;color:var(--txt2);letter-spacing:1px;white-space:nowrap} |
| .range-val{color:var(--acc);font-size:12px;font-weight:bold;min-width:24px} |
| |
| /* ββ TASK ID DISPLAY ββ */ |
| .task-id-box{ |
| background:var(--bg3);border:1px solid var(--bd);border-radius:4px; |
| padding:4px 8px;font-size:11px;color:var(--acc2);letter-spacing:2px; |
| text-align:center;min-height:22px; |
| } |
| </style> |
| </head> |
| <body> |
| |
| <!-- LOGIN --> |
| <div id="login-overlay"> |
| <div class="login-box"> |
| <div class="login-logo">SUKUNA</div> |
| <div class="login-sub">WEBSHARE HARVESTER Β· ADVANCED</div> |
| <input type="password" id="key-input" placeholder="Enter API key..." style="margin-bottom:10px" autocomplete="off"> |
| <button class="btn big block" onclick="doLogin()">AUTHENTICATE</button> |
| <div id="login-err" style="color:var(--red);font-size:10px;margin-top:8px;display:none">Invalid API key</div> |
| </div> |
| </div> |
| |
| <!-- HEADER --> |
| <div class="hdr"> |
| <div class="hdr-left"> |
| <button class="menu-btn" id="menu-btn" onclick="toggleMenu()" aria-label="Open menu"> |
| <span></span><span></span><span></span> |
| </button> |
| <div class="hdr-title">β SUKUNA WEBSHARE HARVESTER</div> |
| </div> |
| <div class="hdr-right"> |
| <div><span class="dot" id="status-dot"></span><span id="status-txt" style="font-size:9px;letter-spacing:1px;color:var(--txt2)">IDLE</span></div> |
| <button class="btn" style="font-size:9px" onclick="doLogout()">LOGOUT</button> |
| </div> |
| </div> |
| |
| <div class="menu-backdrop" id="menu-backdrop" onclick="closeMenu()"></div> |
| <div class="menu-drawer" id="menu-drawer"> |
| <div class="menu-title">Sections</div> |
| <button class="menu-item active" id="menu-harvester" onclick="openSection('harvester')">Harvester</button> |
| <button class="menu-item" id="menu-history" onclick="openSection('history')">Task History</button> |
| </div> |
| |
| <!-- HARVESTER SECTION --> |
| <div class="view-section" id="section-harvester"> |
| <div class="grid" id="main-ui"> |
| |
| <!-- PROXY MANAGER --> |
| <div class="card"> |
| <div class="card-title">Proxy Manager <span id="proxy-count-badge">0</span></div> |
| <textarea id="proxy-input" placeholder="Paste proxies line by line: ip:port:user:pass ip:port:user:pass" rows="6"></textarea> |
| <div class="btn-row"> |
| <button class="btn" onclick="addProxies()">SAVE</button> |
| <button class="btn blue" onclick="document.getElementById('proxy-file').click()">UPLOAD</button> |
| <button class="btn ylw" onclick="checkProxies()">CHECK</button> |
| <button class="btn red" onclick="clearProxies()">CLEAR</button> |
| </div> |
| <input type="file" id="proxy-file" style="display:none" accept=".txt" onchange="uploadProxies(this)"> |
| <div class="stats" id="proxy-stats"> |
| <div class="stat"><div class="stat-label">Total</div><div class="stat-val" id="ps-total">0</div></div> |
| <div class="stat"><div class="stat-label">Alive</div><div class="stat-val" id="ps-alive">0</div></div> |
| <div class="stat"><div class="stat-label">Rotating</div><div class="stat-val blue" id="ps-rot">0</div></div> |
| <div class="stat"><div class="stat-label">Static</div><div class="stat-val" id="ps-sta">0</div></div> |
| <div class="stat"><div class="stat-label">Dead</div><div class="stat-val red" id="ps-dead">0</div></div> |
| </div> |
| <div class="btn-row" style="margin-top:4px"> |
| <button class="btn blue" onclick="exportProxies('rotating')">β ROTATING</button> |
| <button class="btn" onclick="exportProxies('static')">β STATIC</button> |
| <button class="btn" onclick="exportProxies('alive')">β ALIVE</button> |
| </div> |
| </div> |
| |
| <!-- EMAIL MANAGER --> |
| <div class="card"> |
| <div class="card-title">Email Manager <span id="email-count-badge">0</span></div> |
| <textarea id="email-input" placeholder="Paste emails line by line: email@domain.com email@domain.com:password email@domain.com:pass123" rows="6"></textarea> |
| <div class="btn-row"> |
| <button class="btn" onclick="addEmails()">SAVE</button> |
| <button class="btn blue" onclick="document.getElementById('email-file').click()">UPLOAD</button> |
| <button class="btn red" onclick="clearEmails()">CLEAR</button> |
| </div> |
| <input type="file" id="email-file" style="display:none" accept=".txt" onchange="uploadEmails(this)"> |
| <div class="input-row" style="margin-top:8px"> |
| <input type="text" id="filter-domain" placeholder="Filter domain (e.g. gmail.com)"> |
| <button class="btn ylw" onclick="filterEmails()">FILTER</button> |
| </div> |
| <div class="stats"> |
| <div class="stat"><div class="stat-label">Total</div><div class="stat-val" id="em-total">0</div></div> |
| <div class="stat"><div class="stat-label">With Pass</div><div class="stat-val blue" id="em-withpass">0</div></div> |
| </div> |
| <div id="domain-list" style="font-size:9px;color:var(--txt2);margin:4px 0;max-height:48px;overflow-y:auto"></div> |
| <div class="btn-row"> |
| <button class="btn" onclick="exportEmails(0)">β EMAILS</button> |
| <button class="btn blue" onclick="exportEmails(1)">β EMAIL:PASS</button> |
| </div> |
| </div> |
| |
| <!-- CONTROLS --> |
| <div class="card"> |
| <div class="card-title">Task Controls</div> |
| <div class="range-row"> |
| <label>THREADS</label> |
| <input type="range" id="thread-slider" min="1" max="50" value="5" oninput="document.getElementById('thread-val').textContent=this.value"> |
| <span class="range-val" id="thread-val">5</span> |
| </div> |
| <div class="input-row" style="margin-bottom:6px"> |
| <span style="font-size:9px;color:var(--txt2);letter-spacing:1px;white-space:nowrap">PASSWORD</span> |
| <input type="text" id="task-pass" value="God@111983" placeholder="Registration password"> |
| </div> |
| <button class="btn big block" id="btn-start" onclick="startTask()">βΆ START TASK</button> |
| <button class="btn big block red" id="btn-stop" onclick="stopTask()" disabled>β STOP TASK</button> |
| <div class="prog-wrap" style="margin-top:6px"> |
| <div class="prog-bar" id="prog-bar" data-p="0%"></div> |
| </div> |
| <div style="display:flex;align-items:center;gap:8px;margin-top:5px"> |
| <span style="font-size:9px;color:var(--txt2)">TASK ID</span> |
| <div class="task-id-box" id="task-id-box">β</div> |
| </div> |
| <div class="stats" style="margin-top:6px"> |
| <div class="stat"><div class="stat-label">Done</div><div class="stat-val" id="t-done">0</div></div> |
| <div class="stat"><div class="stat-label">Total</div><div class="stat-val" id="t-total">0</div></div> |
| </div> |
| |
| <!-- ββ LIVE PROXY FEED ββ --> |
| <div class="live-feed"> |
| <div class="live-total"> |
| <div> |
| <div class="live-total-num" id="lf-total">0</div> |
| <div class="live-total-label">TOTAL FETCHED</div> |
| </div> |
| <div class="live-total-sep">|</div> |
| <div> |
| <div style="font-size:14px;font-weight:bold;color:var(--grn)" id="lf-rot">0</div> |
| <div class="live-total-label">ROTATING</div> |
| </div> |
| <div class="live-total-sep">|</div> |
| <div> |
| <div style="font-size:14px;font-weight:bold;color:var(--acc2)" id="lf-sta">0</div> |
| <div class="live-total-label">STATIC</div> |
| </div> |
| </div> |
| <div class="live-tabs"> |
| <div class="live-tab active" id="tab-rot" onclick="switchTab('rot')">Rotating <span class="live-count-pill" id="tab-rot-cnt">0</span></div> |
| <div class="live-tab" id="tab-sta" onclick="switchTab('sta')">Static <span class="live-count-pill" id="tab-sta-cnt">0</span></div> |
| </div> |
| <div class="live-panel show" id="panel-rot"> |
| <div class="live-list" id="list-rot"></div> |
| </div> |
| <div class="live-panel" id="panel-sta"> |
| <div class="live-list" id="list-sta"></div> |
| </div> |
| <div class="live-actions"> |
| <button class="btn blue" style="font-size:9px;padding:3px 8px" onclick="copyLiveProxies('rot')">β COPY ROT</button> |
| <button class="btn" style="font-size:9px;padding:3px 8px" onclick="copyLiveProxies('sta')">β COPY STA</button> |
| <button class="btn" style="font-size:9px;padding:3px 8px" onclick="exportLiveProxies('rotating')">β ROT</button> |
| <button class="btn" style="font-size:9px;padding:3px 8px" onclick="exportLiveProxies('static')">β STA</button> |
| <button class="btn red" style="font-size:9px;padding:3px 8px" onclick="clearLiveLists()">CLR</button> |
| </div> |
| </div> |
| </div> |
| |
| </div> |
| |
| <!-- BOTTOM GRID --> |
| <div class="bottom-grid" id="bottom-ui"> |
| |
| <!-- LIVE LOGS --> |
| <div class="card"> |
| <div class="card-title"> |
| Live Logs |
| <button class="btn" style="font-size:8px;padding:2px 6px" onclick="clearLogs()">CLEAR</button> |
| </div> |
| <div class="log-box" id="log-box"></div> |
| </div> |
| </div> |
| </div> |
| |
| <!-- TASK HISTORY SECTION --> |
| <div class="view-section" id="section-history"> |
| <div class="history-wrap" id="history-ui"> |
| <div class="card"> |
| <div class="card-title">Task History <span id="history-count">0</span></div> |
| <div class="active-box" id="active-task-box">No active task</div> |
| <div class="btn-row" style="margin-bottom:8px"> |
| <button class="btn blue" onclick="downloadHistorySummary()">β DOWNLOAD SUMMARY</button> |
| </div> |
| <div class="history-scroll"> |
| <div class="history-head"> |
| <div>Task ID</div><div>Status</div><div>Start</div><div>Emails</div><div>Threads</div> |
| <div>Rot</div><div>Sta</div><div>Total</div><div>Action</div> |
| </div> |
| <div class="history-list" id="history-list"> |
| <div style="color:var(--txt3);font-size:10px;padding:8px 0">No completed tasks yet</div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <!-- TASK DETAIL MODAL --> |
| <div class="modal-bg" id="modal-bg" onclick="if(event.target===this)closeModal()"> |
| <div class="modal"> |
| <div class="modal-title" id="modal-title">Task Detail</div> |
| <div id="modal-body"></div> |
| <div class="btn-row" style="margin-top:12px"> |
| <button class="btn red" onclick="closeModal()">CLOSE</button> |
| </div> |
| </div> |
| </div> |
| |
| <div class="notif" id="notif"></div> |
| |
| <script> |
| // ββ State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| let _authed = false; |
| let _evtSrc = null; |
| let _pollInt = null; |
| let _histTasks = []; |
| let _activeTask = null; |
| let _liveRot = []; // live rotating proxies fetched this task |
| let _liveSta = []; // live static proxies fetched this task |
| let _activeTab = 'rot'; |
| let _activeSection = 'harvester'; |
| |
| // ββ Notify βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| function notify(msg, cls='') { |
| const el = document.getElementById('notif'); |
| el.textContent = msg; |
| el.className = 'notif show ' + cls; |
| clearTimeout(el._t); |
| el._t = setTimeout(() => el.className = 'notif', 2400); |
| } |
| |
| function openMenu() { |
| document.getElementById('menu-drawer').classList.add('show'); |
| document.getElementById('menu-backdrop').classList.add('show'); |
| } |
| function closeMenu() { |
| document.getElementById('menu-drawer').classList.remove('show'); |
| document.getElementById('menu-backdrop').classList.remove('show'); |
| } |
| function toggleMenu() { |
| const drawer = document.getElementById('menu-drawer'); |
| if (drawer.classList.contains('show')) closeMenu(); |
| else openMenu(); |
| } |
| |
| function openSection(section) { |
| _activeSection = section; |
| document.getElementById('section-harvester').className = 'view-section' + (section === 'harvester' ? ' show' : ''); |
| document.getElementById('section-history').className = 'view-section' + (section === 'history' ? ' show' : ''); |
| document.getElementById('menu-harvester').className = 'menu-item' + (section === 'harvester' ? ' active' : ''); |
| document.getElementById('menu-history').className = 'menu-item' + (section === 'history' ? ' active' : ''); |
| closeMenu(); |
| } |
| |
| function showAuthedUI() { |
| document.getElementById('login-overlay').style.display = 'none'; |
| openSection('harvester'); |
| } |
| |
| // ββ Login / Logout βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| async function doLogin() { |
| const key = document.getElementById('key-input').value.trim(); |
| const res = await fetch('/api/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({key})}); |
| const data = await res.json(); |
| if (data.ok) { |
| _authed = true; |
| showAuthedUI(); |
| startSSE(); startPoll(); loadAll(); |
| } else { |
| document.getElementById('login-err').style.display = ''; |
| } |
| } |
| document.getElementById('key-input').addEventListener('keydown', e => { if(e.key==='Enter') doLogin(); }); |
| |
| async function doLogout() { |
| await fetch('/api/logout',{method:'POST'}); |
| location.reload(); |
| } |
| |
| async function checkAuth() { |
| const res = await fetch('/api/auth/check'); |
| const data = await res.json(); |
| if (data.authed) { |
| _authed = true; |
| showAuthedUI(); |
| startSSE(); startPoll(); loadAll(); |
| } |
| } |
| |
| // ββ SSE Logs βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| function startSSE() { |
| if (_evtSrc) _evtSrc.close(); |
| _evtSrc = new EventSource('/api/logs/stream'); |
| _evtSrc.onmessage = e => { |
| try { |
| const d = JSON.parse(e.data); |
| // Proxy lines come with special levels β handle before appending to log |
| if (d.level === 'proxy_rot') { |
| appendProxyLive(d.msg, 'rot'); |
| } else if (d.level === 'proxy_sta') { |
| appendProxyLive(d.msg, 'sta'); |
| } |
| // Always show in log box too (with colour class) |
| appendLog(d); |
| } catch(err) {} |
| }; |
| _evtSrc.onerror = () => setTimeout(startSSE, 3000); |
| } |
| |
| function appendLog(entry) { |
| const box = document.getElementById('log-box'); |
| const line = document.createElement('div'); |
| line.className = 'log-line ' + (entry.level || 'info'); |
| line.innerHTML = `<span class="log-ts">${entry.ts}</span><span class="log-msg">${escHtml(entry.msg)}</span>`; |
| box.appendChild(line); |
| while (box.children.length > 400) box.removeChild(box.firstChild); |
| box.scrollTop = box.scrollHeight; |
| } |
| |
| function clearLogs() { document.getElementById('log-box').innerHTML = ''; } |
| function escHtml(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); } |
| |
| // ββ Live Proxy Feed βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| function appendProxyLive(line, type) { |
| if (type === 'rot') { |
| _liveRot.push(line); |
| } else { |
| _liveSta.push(line); |
| } |
| updateLiveCounts(); |
| |
| // Append to the visible list |
| const listId = type === 'rot' ? 'list-rot' : 'list-sta'; |
| const listEl = document.getElementById(listId); |
| const item = document.createElement('div'); |
| item.className = 'live-list-item' + (type === 'sta' ? ' sta' : ''); |
| item.textContent = line; |
| listEl.appendChild(item); |
| // Keep max 500 items in DOM |
| while (listEl.children.length > 500) listEl.removeChild(listEl.firstChild); |
| listEl.scrollTop = listEl.scrollHeight; |
| } |
| |
| function updateLiveCounts() { |
| const total = _liveRot.length + _liveSta.length; |
| document.getElementById('lf-total').textContent = total; |
| document.getElementById('lf-rot').textContent = _liveRot.length; |
| document.getElementById('lf-sta').textContent = _liveSta.length; |
| document.getElementById('tab-rot-cnt').textContent = _liveRot.length; |
| document.getElementById('tab-sta-cnt').textContent = _liveSta.length; |
| } |
| |
| function switchTab(tab) { |
| _activeTab = tab; |
| document.getElementById('tab-rot').className = 'live-tab' + (tab==='rot'?' active':''); |
| document.getElementById('tab-sta').className = 'live-tab' + (tab==='sta'?' active':''); |
| document.getElementById('panel-rot').className = 'live-panel' + (tab==='rot'?' show':''); |
| document.getElementById('panel-sta').className = 'live-panel' + (tab==='sta'?' show':''); |
| } |
| |
| function copyLiveProxies(type) { |
| const lines = type === 'rot' ? _liveRot : _liveSta; |
| if (!lines.length) { notify('No proxies to copy','ylw'); return; } |
| navigator.clipboard.writeText(lines.join('\n')).then( |
| () => notify(`Copied ${lines.length} ${type==='rot'?'rotating':'static'} proxies`), |
| () => { |
| // Fallback for browsers without clipboard API |
| const ta = document.createElement('textarea'); |
| ta.value = lines.join('\n'); |
| document.body.appendChild(ta); |
| ta.select(); document.execCommand('copy'); |
| document.body.removeChild(ta); |
| notify(`Copied ${lines.length} proxies`); |
| } |
| ); |
| } |
| |
| function clearLiveLists() { |
| _liveRot = []; _liveSta = []; |
| document.getElementById('list-rot').innerHTML = ''; |
| document.getElementById('list-sta').innerHTML = ''; |
| updateLiveCounts(); |
| } |
| |
| function exportLiveProxies(ptype) { |
| // Use the task/live endpoint which serves current lists |
| window.open('/api/task/live_export?type='+ptype,'_blank'); |
| } |
| |
| // Sync live lists on page load / reconnect from current task |
| async function syncLiveLists() { |
| if (!_authed) return; |
| const res = await fetch('/api/task/live'); |
| if (!res.ok) return; |
| const d = await res.json(); |
| // Only populate if lists are empty (don't duplicate on poll) |
| if (_liveRot.length === 0 && d.rotating && d.rotating.length) { |
| _liveRot = d.rotating; |
| const listEl = document.getElementById('list-rot'); |
| listEl.innerHTML = ''; |
| _liveRot.forEach(line => { |
| const item = document.createElement('div'); |
| item.className = 'live-list-item'; |
| item.textContent = line; |
| listEl.appendChild(item); |
| }); |
| listEl.scrollTop = listEl.scrollHeight; |
| } |
| if (_liveSta.length === 0 && d.static && d.static.length) { |
| _liveSta = d.static; |
| const listEl = document.getElementById('list-sta'); |
| listEl.innerHTML = ''; |
| _liveSta.forEach(line => { |
| const item = document.createElement('div'); |
| item.className = 'live-list-item sta'; |
| item.textContent = line; |
| listEl.appendChild(item); |
| }); |
| listEl.scrollTop = listEl.scrollHeight; |
| } |
| updateLiveCounts(); |
| } |
| |
| // ββ Poll ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| function startPoll() { |
| if (_pollInt) clearInterval(_pollInt); |
| _pollInt = setInterval(pollAll, 2500); |
| } |
| function pollAll() { pollTaskStatus(); pollProxies(); pollEmails(); pollHistory(); } |
| function loadAll() { pollProxies(); pollEmails(); pollHistory(); pollTaskStatus(); syncLiveLists(); } |
| |
| // ββ Proxies βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| async function pollProxies() { |
| if (!_authed) return; |
| const res = await fetch('/api/proxies'); |
| if (!res.ok) return; |
| const d = await res.json(); |
| document.getElementById('proxy-count-badge').textContent = d.total; |
| document.getElementById('ps-total').textContent = d.total; |
| document.getElementById('ps-alive').textContent = d.alive; |
| document.getElementById('ps-rot').textContent = d.rotating; |
| document.getElementById('ps-sta').textContent = d.static; |
| document.getElementById('ps-dead').textContent = d.dead; |
| } |
| |
| async function addProxies() { |
| const text = document.getElementById('proxy-input').value; |
| if (!text.trim()) { notify('Paste proxy list first','ylw'); return; } |
| const res = await fetch('/api/proxies/add',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text})}); |
| const d = await res.json(); |
| if (d.ok) { notify(`Added ${d.added} proxies | Total: ${d.total}`); document.getElementById('proxy-input').value=''; pollProxies(); } |
| } |
| |
| async function uploadProxies(input) { |
| if (!input.files[0]) return; |
| const fd = new FormData(); fd.append('file', input.files[0]); |
| const res = await fetch('/api/proxies/upload',{method:'POST',body:fd}); |
| const d = await res.json(); |
| if (d.ok) { notify(`Uploaded: +${d.added} | Total: ${d.total}`); pollProxies(); } |
| input.value = ''; |
| } |
| |
| async function checkProxies() { |
| const threads = parseInt(document.getElementById('thread-slider').value); |
| notify('Checking proxies...'); |
| await fetch('/api/proxies/check',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({threads})}); |
| } |
| |
| async function clearProxies() { |
| if (!confirm('Clear all proxies?')) return; |
| await fetch('/api/proxies/clear',{method:'POST'}); |
| pollProxies(); notify('Proxies cleared','ylw'); |
| } |
| |
| function exportProxies(type) { window.open('/api/proxies/export?type='+type,'_blank'); } |
| |
| // ββ Emails ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| async function pollEmails() { |
| if (!_authed) return; |
| const res = await fetch('/api/emails'); |
| if (!res.ok) return; |
| const d = await res.json(); |
| document.getElementById('email-count-badge').textContent = d.total; |
| document.getElementById('em-total').textContent = d.total; |
| const withPass = (d.emails||[]).filter(e=>e.password).length; |
| document.getElementById('em-withpass').textContent = withPass; |
| const dl = document.getElementById('domain-list'); |
| const domains = d.domains || {}; |
| dl.innerHTML = Object.entries(domains).sort((a,b)=>b[1]-a[1]) |
| .map(([dm,cnt])=>`<span style="margin-right:8px">${escHtml(dm)}: <b style="color:var(--acc2)">${cnt}</b></span>`).join(''); |
| } |
| |
| async function addEmails() { |
| const text = document.getElementById('email-input').value; |
| if (!text.trim()) { notify('Paste email list first','ylw'); return; } |
| const res = await fetch('/api/emails/add',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text})}); |
| const d = await res.json(); |
| if (d.ok) { notify(`Added ${d.added} emails | Total: ${d.total}`); document.getElementById('email-input').value=''; pollEmails(); } |
| } |
| |
| async function uploadEmails(input) { |
| if (!input.files[0]) return; |
| const fd = new FormData(); fd.append('file', input.files[0]); |
| const res = await fetch('/api/emails/upload',{method:'POST',body:fd}); |
| const d = await res.json(); |
| if (d.ok) { notify(`Uploaded: +${d.added} | Total: ${d.total}`); pollEmails(); } |
| input.value = ''; |
| } |
| |
| async function filterEmails() { |
| const domain = document.getElementById('filter-domain').value.trim(); |
| if (!domain) { notify('Enter a domain to filter','ylw'); return; } |
| const res = await fetch('/api/emails/filter',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({domain})}); |
| const d = await res.json(); |
| if (d.ok) { notify(`Filtered to ${d.total} ${escHtml(d.domain)} emails`); pollEmails(); } |
| } |
| |
| async function clearEmails() { |
| if (!confirm('Clear all emails?')) return; |
| await fetch('/api/emails/clear',{method:'POST'}); |
| pollEmails(); notify('Emails cleared','ylw'); |
| } |
| |
| function exportEmails(withPass) { window.open('/api/emails/export?with_pass='+withPass,'_blank'); } |
| |
| // ββ Task ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| async function startTask() { |
| const threads = parseInt(document.getElementById('thread-slider').value); |
| const password = document.getElementById('task-pass').value.trim() || 'God@111983'; |
| // Clear live feed for new task |
| clearLiveLists(); |
| const res = await fetch('/api/task/start',{method:'POST',headers:{'Content-Type':'application/json'}, |
| body:JSON.stringify({threads,password})}); |
| const d = await res.json(); |
| if (d.ok) { |
| notify(`Task ${d.task_id} started`); |
| document.getElementById('task-id-box').textContent = d.task_id; |
| document.getElementById('btn-start').disabled = true; |
| document.getElementById('btn-stop').disabled = false; |
| setStatusDot('run','RUNNING'); |
| } else { |
| notify(d.error || 'Start failed','red'); |
| } |
| } |
| |
| async function stopTask() { |
| await fetch('/api/task/stop',{method:'POST'}); |
| notify('Stop signal sent','ylw'); |
| document.getElementById('btn-stop').disabled = true; |
| setStatusDot('','STOPPING'); |
| } |
| |
| async function pollTaskStatus() { |
| if (!_authed) return; |
| const res = await fetch('/api/task/status'); |
| if (!res.ok) return; |
| const d = await res.json(); |
| const t = d.task; |
| if (!t) { setStatusDot('','IDLE'); return; } |
| |
| document.getElementById('task-id-box').textContent = t.id || 'β'; |
| document.getElementById('t-total').textContent = t.total || 0; |
| const pct = t.progress || 0; |
| const done = Math.round((pct / 100) * (t.total || 0)); |
| document.getElementById('t-done').textContent = done; |
| const bar = document.getElementById('prog-bar'); |
| bar.style.width = pct + '%'; |
| bar.setAttribute('data-p', pct + '%'); |
| |
| // Update live totals from server (for reconnect accuracy) |
| const rotC = t.rotating_fetched || 0; |
| const staC = t.static_fetched || 0; |
| // Only update DOM counters from server if SSE hasn't been streaming (prevents flicker) |
| if (rotC > _liveRot.length || staC > _liveSta.length) { |
| document.getElementById('lf-total').textContent = rotC + staC; |
| document.getElementById('lf-rot').textContent = rotC; |
| document.getElementById('lf-sta').textContent = staC; |
| document.getElementById('tab-rot-cnt').textContent = rotC; |
| document.getElementById('tab-sta-cnt').textContent = staC; |
| } |
| |
| if (t.status === 'running' || t.status === 'starting') { |
| setStatusDot('run','RUNNING'); |
| document.getElementById('btn-start').disabled = true; |
| document.getElementById('btn-stop').disabled = false; |
| } else if (t.status === 'stopping') { |
| setStatusDot('','STOPPING'); |
| document.getElementById('btn-stop').disabled = true; |
| } else { |
| setStatusDot('on', t.status === 'complete' ? 'DONE' : 'IDLE'); |
| document.getElementById('btn-start').disabled = false; |
| document.getElementById('btn-stop').disabled = true; |
| if (t.status === 'complete') pollHistory(); |
| } |
| } |
| |
| function setStatusDot(cls, txt) { |
| document.getElementById('status-dot').className = 'dot ' + cls; |
| document.getElementById('status-txt').textContent = txt; |
| } |
| |
| // ββ Task History ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| async function pollHistory() { |
| if (!_authed) return; |
| const res = await fetch('/api/task/history'); |
| if (!res.ok) return; |
| const d = await res.json(); |
| _activeTask = d.active_task || null; |
| _histTasks = d.tasks || []; |
| renderHistory(); |
| } |
| |
| function fmtTime(v) { |
| if (!v) return 'β'; |
| return String(v).replace('T',' ').slice(0,19); |
| } |
| |
| function renderHistory() { |
| const container = document.getElementById('history-list'); |
| const countEl = document.getElementById('history-count'); |
| const activeEl = document.getElementById('active-task-box'); |
| countEl.textContent = _histTasks.length; |
| if (_activeTask && ['starting','running','stopping'].includes(_activeTask.status)) { |
| activeEl.innerHTML = ` |
| <div style="display:flex;justify-content:space-between;gap:10px;align-items:center;flex-wrap:wrap"> |
| <div> |
| Active Task: <b>${escHtml(_activeTask.id || 'β')}</b> | |
| Status: ${escHtml(String((_activeTask.status || '').toUpperCase()))} | |
| Emails: ${_activeTask.total || 0} | |
| Threads: ${_activeTask.threads || 0} | |
| Rot: ${_activeTask.rotating_fetched || 0} | |
| Sta: ${_activeTask.static_fetched || 0} |
| </div> |
| <div class="btn-row" style="margin:0"> |
| <button class="btn" style="font-size:9px;padding:3px 8px" onclick="showTaskDetail('${escHtml(_activeTask.id)}')">DETAIL</button> |
| <button class="btn blue" style="font-size:9px;padding:3px 8px" onclick="window.open('/api/task/${escHtml(_activeTask.id)}/download/all','_blank')">DOWNLOAD</button> |
| </div> |
| </div> |
| `; |
| } else { |
| activeEl.textContent = 'No active task'; |
| } |
| if (!_histTasks.length) { |
| container.innerHTML = '<div style="color:var(--txt3);font-size:10px;padding:8px 0">No completed tasks yet</div>'; |
| return; |
| } |
| container.innerHTML = _histTasks.slice().reverse().map(t => { |
| const rot = t.rotating_fetched || 0; |
| const sta = t.static_fetched || 0; |
| return `<div class="history-row" onclick="showTaskDetail('${escHtml(t.id)}')"> |
| <div class="history-id">${escHtml(t.id || 'β')}</div> |
| <div>${escHtml(String((t.status || '').toUpperCase()))}</div> |
| <div>${escHtml(fmtTime(t.start_time))}</div> |
| <div>${t.total || 0}</div> |
| <div>${t.threads || 0}</div> |
| <div>${rot}</div> |
| <div>${sta}</div> |
| <div>${rot + sta}</div> |
| <div><button class="btn blue" style="font-size:9px;padding:3px 8px" onclick="event.stopPropagation();window.open('/api/task/${escHtml(t.id)}/download/all','_blank')">DOWNLOAD</button></div> |
| </div>`; |
| }).join(''); |
| } |
| |
| function downloadHistorySummary() { |
| window.open('/api/task/history/download', '_blank'); |
| } |
| |
| async function showTaskDetail(tid) { |
| const [logsRes] = await Promise.all([fetch(`/api/task/${tid}/logs`)]); |
| const logsData = await logsRes.json(); |
| const task = _histTasks.find(t => t.id === tid) || (_activeTask && _activeTask.id === tid ? _activeTask : {}); |
| const logs = logsData.logs || []; |
| const start = task.start_time ? task.start_time.replace('T',' ').slice(0,19) : ''; |
| const end = task.end_time ? task.end_time.replace('T',' ').slice(0,19) : ''; |
| // Filter proxy lines for display in modal |
| const proxyLogs = logs.filter(l => l.level==='proxy_rot'||l.level==='proxy_sta'); |
| const otherLogs = logs.filter(l => l.level!=='proxy_rot'&&l.level!=='proxy_sta'); |
| document.getElementById('modal-title').textContent = `TASK ${tid}`; |
| document.getElementById('modal-body').innerHTML = ` |
| <div style="margin-bottom:10px"> |
| <div style="font-size:10px;color:var(--txt2);margin-bottom:4px">Started: ${escHtml(start)} | Ended: ${escHtml(end)}</div> |
| <div style="font-size:10px;color:var(--txt2);margin-bottom:8px">Emails: ${task.total||0} | Threads: ${task.threads||'?'}</div> |
| <div class="stats" style="margin-bottom:8px"> |
| <div class="stat"><div class="stat-label">Total</div><div class="stat-val">${(task.rotating_fetched||0)+(task.static_fetched||0)}</div></div> |
| <div class="stat"><div class="stat-label">Rotating</div><div class="stat-val">${task.rotating_fetched||0}</div></div> |
| <div class="stat"><div class="stat-label">Static</div><div class="stat-val blue">${task.static_fetched||0}</div></div> |
| </div> |
| <div class="btn-row" style="margin-bottom:10px"> |
| <button class="btn ylw" onclick="window.open('/api/task/${tid}/download/all','_blank')">β ALL</button> |
| <button class="btn blue" onclick="window.open('/api/task/${tid}/download/rotating','_blank')">β ROTATING</button> |
| <button class="btn" onclick="window.open('/api/task/${tid}/download/static','_blank')">β STATIC</button> |
| </div> |
| ${proxyLogs.length ? ` |
| <div style="font-size:9px;color:var(--txt2);letter-spacing:1px;margin-bottom:4px">FETCHED PROXIES (${proxyLogs.length})</div> |
| <div class="live-list" style="height:100px;border-radius:4px;border:1px solid var(--bd);margin-bottom:8px"> |
| ${proxyLogs.map(l=>`<div class="live-list-item${l.level==='proxy_sta'?' sta':''}">${escHtml(l.msg)}</div>`).join('')} |
| </div>` : ''} |
| </div> |
| <div style="font-size:9px;color:var(--txt2);letter-spacing:1px;margin-bottom:4px">TASK LOGS (${otherLogs.length})</div> |
| <div class="log-box" style="height:180px"> |
| ${otherLogs.map(l=>`<div class="log-line ${escHtml(l.level)}"><span class="log-ts">${escHtml(l.ts)}</span><span class="log-msg">${escHtml(l.msg)}</span></div>`).join('')} |
| </div>`; |
| document.getElementById('modal-bg').className = 'modal-bg show'; |
| setTimeout(()=>{ |
| const lb = document.querySelector('#modal-body .log-box'); |
| if(lb) lb.scrollTop = lb.scrollHeight; |
| },50); |
| } |
| |
| function closeModal() { document.getElementById('modal-bg').className = 'modal-bg'; } |
| |
| // ββ Init ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| window.addEventListener('DOMContentLoaded', checkAuth); |
| </script> |
| </body> |
| </html>""" |
|
|
| |
| @app.route("/") |
| def index(): |
| return render_template_string(HTML) |
|
|
| |
| |
| |
| if __name__ == "__main__": |
| print(f""" |
| ββββββββββββββββββββββββββββββββββββββββββββββββ |
| β SUKUNA WEBSHARE HARVESTER - ADVANCED β |
| β http://{HOST}:{PORT:<5} β |
| β API Key: {API_KEY:<35}β |
| ββββββββββββββββββββββββββββββββββββββββββββββββ |
| """) |
| app.run(host=HOST, port=PORT, debug=DEBUG, threaded=True, use_reloader=False) |
|
|