| | import httpx |
| | from bs4 import BeautifulSoup |
| | import re |
| | from datetime import datetime, timedelta, timezone |
| | import time |
| | from flask import Flask, Response, request, redirect |
| | from threading import Thread |
| | from collections import deque |
| | import json |
| | from queue import Queue |
| | import os |
| | import uuid |
| | import sys |
| |
|
| | |
| | print = lambda *args, **kwargs: __builtins__.print(*args, **kwargs, flush=True) |
| |
|
| | BASE = "http://159.69.3.189" |
| | LOGIN_URL = f"{BASE}/login" |
| | GET_RANGE_URL = f"{BASE}/portal/sms/received/getsms" |
| | GET_NUMBER_URL = f"{BASE}/portal/sms/received/getsms/number" |
| | GET_SMS_URL = f"{BASE}/portal/sms/received/getsms/number/sms" |
| |
|
| | TELEGRAM_PROXY_URL = "https://danihitambangetjir.termai.cc/api/proxy" |
| | CUSTOM_DOMAIN = "https://fourstore-otp.hf.space" |
| | ACCOUNTS_FILE = "accounts.json" |
| |
|
| | |
| | def load_accounts(): |
| | if os.path.exists(ACCOUNTS_FILE): |
| | try: |
| | with open(ACCOUNTS_FILE, 'r') as f: |
| | return json.load(f) |
| | except Exception as e: |
| | print(f"β Error load accounts: {e}") |
| | return {} |
| | return {} |
| |
|
| | def save_accounts(accounts): |
| | try: |
| | with open(ACCOUNTS_FILE, 'w') as f: |
| | json.dump(accounts, f, indent=2) |
| | print(f"πΎ Accounts saved to {ACCOUNTS_FILE}") |
| | except Exception as e: |
| | print(f"β Error save accounts: {e}") |
| |
|
| | |
| | accounts = load_accounts() |
| | print(f"\nπ Loaded {len(accounts)} accounts from file") |
| |
|
| | |
| | for acc_id in accounts: |
| | accounts[acc_id]["session"] = None |
| | accounts[acc_id]["csrf"] = None |
| | accounts[acc_id]["status"] = False |
| | accounts[acc_id]["otp_logs"] = [] |
| | accounts[acc_id]["sent_cache"] = [] |
| | accounts[acc_id]["sms_cache"] = {} |
| | accounts[acc_id]["sms_counter"] = {} |
| | accounts[acc_id]["range_counter"] = {} |
| | accounts[acc_id]["last_cleanup"] = time.time() |
| | print(f" β
Account {acc_id}: {accounts[acc_id].get('username')}") |
| |
|
| | app = Flask('') |
| | app.secret_key = "fourstore-multi-account-secret" |
| | sse_clients = [] |
| |
|
| | def get_wib_time(): |
| | return datetime.now(timezone.utc) + timedelta(hours=7) |
| |
|
| | def get_search_date(): |
| | wib = get_wib_time() |
| | return (wib - timedelta(days=1)).strftime("%Y-%m-%d") if wib.hour < 7 else wib.strftime("%Y-%m-%d") |
| |
|
| | def login_account(account_id, username, password, bot_token, chat_id): |
| | try: |
| | print(f"\n{'='*60}") |
| | print(f"π PROSES LOGIN UNTUK: {username} (ID: {account_id})") |
| | print(f"{'='*60}") |
| | |
| | print(f"π€ Membuat session baru...") |
| | session = httpx.Client(follow_redirects=True, timeout=30.0) |
| | |
| | print(f"π₯ GET login page: {LOGIN_URL}") |
| | r = session.get(LOGIN_URL, timeout=30) |
| | print(f"π Status code: {r.status_code}") |
| | |
| | if r.status_code != 200: |
| | print(f"β Gagal load login page") |
| | return False, f"HTTP {r.status_code}" |
| | |
| | soup = BeautifulSoup(r.text, "html.parser") |
| | token = soup.find("input", {"name": "_token"}) |
| | if not token: |
| | print(f"β Token CSRF tidak ditemukan dalam HTML") |
| | print(f"π Preview HTML: {r.text[:200]}") |
| | return False, "Token tidak ditemukan" |
| | |
| | csrf_token = token.get("value") |
| | print(f"β
CSRF Token: {csrf_token[:20]}...") |
| | |
| | print(f"π€ POST login data...") |
| | r = session.post(LOGIN_URL, data={ |
| | "_token": csrf_token, |
| | "email": username, |
| | "password": password |
| | }, timeout=30) |
| | print(f"π Response status: {r.status_code}") |
| | |
| | if "dashboard" in r.text.lower() or "logout" in r.text.lower(): |
| | print(f"β
β
β
LOGIN BERHASIL! β
β
β
") |
| | print(f"π¦ Menyimpan session untuk {username}") |
| | |
| | accounts[account_id]["session"] = session |
| | accounts[account_id]["csrf"] = csrf_token |
| | accounts[account_id]["status"] = True |
| | accounts[account_id]["username"] = username |
| | accounts[account_id]["password"] = password |
| | accounts[account_id]["bot_token"] = bot_token |
| | accounts[account_id]["chat_id"] = chat_id |
| | accounts[account_id]["last_login"] = time.time() |
| | save_accounts(accounts) |
| | |
| | return True, "Login berhasil" |
| | else: |
| | print(f"βββ LOGIN GAGAL! βββ") |
| | print(f"π Response preview: {r.text[:300]}") |
| | return False, "Login gagal - cek username/password" |
| | |
| | except Exception as e: |
| | print(f"β EXCEPTION: {str(e)}") |
| | return False, str(e) |
| |
|
| | def tg_send(account_id, msg): |
| | try: |
| | account = accounts.get(account_id) |
| | if not account or not account.get("bot_token") or not account.get("chat_id"): |
| | return False |
| | |
| | url = f"{TELEGRAM_PROXY_URL}?url=https://api.telegram.org/bot{account['bot_token']}/sendMessage" |
| | payload = { |
| | "chat_id": account['chat_id'], |
| | "text": msg, |
| | "parse_mode": "Markdown" |
| | } |
| | httpx.post(url, json=payload, timeout=30) |
| | return True |
| | except: |
| | return False |
| |
|
| | def clean_country(rng): |
| | return re.sub(r"\s*\d+$", "", rng).strip() if rng else "UNKNOWN" |
| |
|
| | def mask_number(number): |
| | if not number: return "UNKNOWN" |
| | clean = re.sub(r"[^\d+]", "", number) |
| | if len(clean) <= 6: return clean |
| | return f"{clean[:4]}****{clean[-3:]}" |
| |
|
| | def map_service(raw): |
| | if not raw: return "UNKNOWN" |
| | s = raw.lower().strip() |
| | if 'whatsapp' in s: return "WHATSAPP" |
| | if 'telegram' in s: return "TELEGRAM" |
| | if 'google' in s or 'gmail' in s: return "GOOGLE" |
| | if 'facebook' in s or 'fb' in s: return "FACEBOOK" |
| | if 'instagram' in s or 'ig' in s: return "INSTAGRAM" |
| | if 'tiktok' in s: return "TIKTOK" |
| | if 'temu' in s: return "TEMU" |
| | return raw.upper() |
| |
|
| | def extract_otp(text): |
| | if not text: return None |
| | m = re.search(r"\b(\d{3}[- ]?\d{3})\b", text) |
| | if m: |
| | return m.group(0).replace("-", "").replace(" ", "") |
| | m = re.search(r"\b(\d{4,6})\b", text) |
| | if m: |
| | return m.group(0) |
| | digits = re.findall(r'\d+', text) |
| | for d in digits: |
| | if 4 <= len(d) <= 6: |
| | return d |
| | return None |
| |
|
| | def get_ranges_with_count(account_id): |
| | account = accounts.get(account_id) |
| | if not account or not account.get("session") or not account.get("csrf"): |
| | return [] |
| | |
| | try: |
| | date = get_search_date() |
| | r = account["session"].post(GET_RANGE_URL, data={ |
| | "_token": account["csrf"], |
| | "from": date, |
| | "to": date |
| | }, timeout=15) |
| |
|
| | soup = BeautifulSoup(r.text, "html.parser") |
| | ranges_data = [] |
| |
|
| | for item in soup.select(".item"): |
| | name_div = item.select_one(".col-sm-4") |
| | if not name_div: continue |
| | rng = name_div.get_text(strip=True) |
| |
|
| | count_p = item.select_one(".col-3 .mb-0.pb-0") |
| | count = int(count_p.get_text(strip=True)) if count_p else 0 |
| |
|
| | ranges_data.append({ |
| | "name": rng, |
| | "count": count |
| | }) |
| |
|
| | return ranges_data |
| | except: |
| | return [] |
| |
|
| | def get_numbers_with_count(account_id, rng): |
| | account = accounts.get(account_id) |
| | if not account or not account.get("session") or not account.get("csrf"): |
| | return [] |
| | |
| | try: |
| | date = get_search_date() |
| | r = account["session"].post(GET_NUMBER_URL, data={ |
| | "_token": account["csrf"], |
| | "start": date, |
| | "end": date, |
| | "range": rng |
| | }, timeout=15) |
| |
|
| | soup = BeautifulSoup(r.text, "html.parser") |
| | numbers_data = [] |
| |
|
| | for div in soup.find_all("div", onclick=True): |
| | onclick = div.get("onclick", "") |
| | match = re.search(r"getDetialsNumber\w*\('?(\d+)'?", onclick) |
| | if not match: |
| | match = re.search(r"open_(\d+)", onclick) |
| | if not match: |
| | match = re.search(r"'(\d+)'", onclick) |
| | |
| | if match: |
| | num = match.group(1) |
| | if num and len(num) > 5: |
| | parent = div.find_parent("div", class_="card") |
| | count = 0 |
| | if parent: |
| | p_tag = parent.find("p", class_="mb-0 pb-0") |
| | if p_tag: |
| | try: |
| | count = int(p_tag.get_text(strip=True)) |
| | except: |
| | count = 0 |
| |
|
| | numbers_data.append({ |
| | "number": num, |
| | "count": count |
| | }) |
| |
|
| | if len(numbers_data) == 0: |
| | for div in soup.find_all("div", class_="col-sm-4"): |
| | text = div.get_text(strip=True) |
| | match = re.search(r'\b(\d{10,15})\b', text) |
| | if match: |
| | num = match.group(1) |
| | parent = div.find_parent("div", class_="card") |
| | count = 0 |
| | if parent: |
| | p_tag = parent.find("p", class_="mb-0 pb-0") |
| | if p_tag: |
| | try: |
| | count = int(p_tag.get_text(strip=True)) |
| | except: |
| | count = 0 |
| | |
| | numbers_data.append({ |
| | "number": num, |
| | "count": count |
| | }) |
| |
|
| | return numbers_data |
| | except: |
| | return [] |
| |
|
| | def get_sms_fast(account_id, rng, number): |
| | account = accounts.get(account_id) |
| | if not account or not account.get("session") or not account.get("csrf"): |
| | return [] |
| | |
| | try: |
| | date = get_search_date() |
| | cache_key = f"{rng}-{number}" |
| |
|
| | if cache_key in account["sms_cache"]: |
| | timestamp, results = account["sms_cache"][cache_key] |
| | if time.time() - timestamp < 5: |
| | return results |
| |
|
| | r = account["session"].post(GET_SMS_URL, data={ |
| | "_token": account["csrf"], |
| | "start": date, |
| | "end": date, |
| | "Number": number, |
| | "Range": rng |
| | }, timeout=20) |
| |
|
| | soup = BeautifulSoup(r.text, "html.parser") |
| | results = [] |
| |
|
| | for card in soup.select("div.card.card-body"): |
| | try: |
| | service = "UNKNOWN" |
| | service_div = card.select_one("div.col-sm-4") |
| | if service_div: |
| | raw = service_div.get_text(strip=True) |
| | service = map_service(raw) |
| |
|
| | msg_p = card.find("p", class_="mb-0 pb-0") |
| | if msg_p: |
| | sms = msg_p.get_text(strip=True) |
| | otp = extract_otp(sms) |
| | if otp: |
| | results.append((service, sms, otp)) |
| | except: |
| | continue |
| |
|
| | account["sms_cache"][cache_key] = (time.time(), results) |
| | return results |
| | except: |
| | return [] |
| |
|
| | def add_otp_log(account_id, country, number, service, otp, sms): |
| | account = accounts.get(account_id) |
| | if not account: |
| | return |
| | |
| | wib = get_wib_time() |
| | log_entry = { |
| | "time": wib.strftime("%H:%M:%S"), |
| | "country": country, |
| | "number": number, |
| | "service": service, |
| | "otp": otp, |
| | "sms": sms[:80] + "..." if len(sms) > 80 else sms, |
| | "account_id": account_id, |
| | "account_username": account.get("username", "Unknown") |
| | } |
| | |
| | if "otp_logs" not in account: |
| | account["otp_logs"] = [] |
| | |
| | account["otp_logs"].insert(0, log_entry) |
| | if len(account["otp_logs"]) > 100: |
| | account["otp_logs"] = account["otp_logs"][:100] |
| | |
| | broadcast_sse(log_entry) |
| | save_accounts(accounts) |
| | return log_entry |
| |
|
| | def broadcast_sse(data): |
| | msg = f"data: {json.dumps(data)}\n\n" |
| | dead = [] |
| | for q in sse_clients: |
| | try: |
| | q.put(msg) |
| | except: |
| | dead.append(q) |
| | for q in dead: |
| | sse_clients.remove(q) |
| |
|
| | @app.route('/') |
| | def home(): |
| | |
| | all_logs = [] |
| | for acc_id, acc in accounts.items(): |
| | if acc.get("otp_logs"): |
| | for log in acc["otp_logs"]: |
| | log_copy = log.copy() |
| | log_copy["account_username"] = acc.get("username", "Unknown") |
| | all_logs.append(log_copy) |
| | |
| | all_logs = sorted(all_logs, key=lambda x: x.get("time", ""), reverse=True) |
| | |
| | search_query = request.args.get('q', '').lower() |
| | filter_service = request.args.get('service', '') |
| | filter_account = request.args.get('account', '') |
| | |
| | if search_query: |
| | all_logs = [log for log in all_logs if |
| | search_query in log.get('country', '').lower() or |
| | search_query in log.get('number', '').lower() or |
| | search_query in log.get('otp', '').lower() or |
| | search_query in log.get('sms', '').lower()] |
| |
|
| | if filter_service: |
| | all_logs = [log for log in all_logs if log.get('service') == filter_service] |
| | |
| | if filter_account: |
| | all_logs = [log for log in all_logs if log.get('account_id') == filter_account] |
| | |
| | all_services = list(set([log.get('service') for log in all_logs if log.get('service')])) |
| | |
| | |
| | total_otp = sum(len(acc.get('sent_cache', [])) for acc in accounts.values()) |
| | today_otp = len(all_logs) |
| | |
| | |
| | accounts_list = [] |
| | for acc_id, acc in accounts.items(): |
| | accounts_list.append({ |
| | "id": acc_id, |
| | "username": acc.get("username", "Unknown"), |
| | "status": acc.get("status", False) |
| | }) |
| | |
| | html = f""" |
| | <!DOCTYPE html> |
| | <html lang="en"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | <title>OTP MULTI ACCOUNT Β· FOURSTORE</title> |
| | <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
| | <style> |
| | * {{ margin: 0; padding: 0; box-sizing: border-box; }} |
| | body {{ font-family: 'Inter', sans-serif; background: #0a0c10; color: #e4e6eb; padding: 24px; }} |
| | .container {{ max-width: 1600px; margin: 0 auto; }} |
| | .header {{ background: linear-gradient(145deg, #1a1f2c, #0f131c); border-radius: 24px; padding: 28px; margin-bottom: 28px; border: 1px solid #2d3540; }} |
| | .header-top {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 15px; }} |
| | .title h1 {{ font-size: 28px; font-weight: 700; background: linear-gradient(135deg, #00f2fe, #4facfe); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }} |
| | .title p {{ color: #8b949e; font-size: 14px; }} |
| | .status-badge {{ padding: 10px 24px; border-radius: 100px; font-weight: 600; font-size: 14px; |
| | background: #0a4d3c; color: #a0f0d0; border: 1px solid #1a6e5a; }} |
| | .stats-grid {{ display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-top: 20px; }} |
| | .stat-card {{ background: #1a1f2c; padding: 20px; border-radius: 20px; border: 1px solid #2d3540; }} |
| | .stat-label {{ color: #8b949e; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }} |
| | .stat-value {{ font-size: 32px; font-weight: 700; color: #00f2fe; }} |
| | |
| | .add-account-form {{ background: #1a1f2c; padding: 20px; border-radius: 16px; margin-bottom: 30px; border: 1px solid #2d3540; }} |
| | .form-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; }} |
| | .form-input {{ background: #0a0c10; border: 1px solid #2d3540; padding: 12px 16px; border-radius: 12px; color: #e4e6eb; width: 100%; }} |
| | .form-input:focus {{ outline: none; border-color: #00f2fe; }} |
| | .btn {{ background: #00f2fe; color: #0a0c10; border: none; padding: 12px 24px; border-radius: 12px; font-weight: 600; cursor: pointer; transition: all 0.2s; }} |
| | .btn:hover {{ background: #00d8e4; transform: translateY(-2px); }} |
| | .btn-danger {{ background: #ff4d4d; color: white; }} |
| | .btn-small {{ padding: 6px 12px; font-size: 12px; }} |
| | |
| | .account-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 30px; }} |
| | .account-card {{ background: #1a1f2c; border-radius: 16px; padding: 20px; border: 1px solid #2d3540; position: relative; }} |
| | .account-card.online {{ border-color: #00f2fe; box-shadow: 0 0 15px #00f2fe20; }} |
| | .account-status {{ position: absolute; top: 20px; right: 20px; width: 12px; height: 12px; border-radius: 50%; }} |
| | .status-online {{ background: #00f2fe; box-shadow: 0 0 10px #00f2fe; }} |
| | .status-offline {{ background: #ff4d4d; }} |
| | .account-actions {{ display: flex; gap: 8px; margin-top: 15px; flex-wrap: wrap; }} |
| | |
| | .search-section {{ background: #0f131c; border-radius: 20px; padding: 20px; margin-bottom: 24px; border: 1px solid #2d3540; display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }} |
| | .search-box {{ flex: 2; min-width: 280px; position: relative; }} |
| | .search-icon {{ position: absolute; left: 16px; top: 14px; color: #8b949e; }} |
| | .search-input {{ width: 100%; padding: 14px 20px 14px 48px; background: #1a1f2c; border: 1px solid #2d3540; border-radius: 100px; color: #e4e6eb; font-size: 15px; }} |
| | .filter-box {{ flex: 1; min-width: 150px; }} |
| | .filter-select {{ width: 100%; padding: 14px 20px; background: #1a1f2c; border: 1px solid #2d3540; border-radius: 100px; color: #e4e6eb; font-size: 15px; cursor: pointer; }} |
| | .clear-btn {{ padding: 8px 16px; background: #2d3a4a; border: none; border-radius: 100px; color: white; font-size: 13px; cursor: pointer; text-decoration: none; }} |
| | |
| | .otp-section {{ background: #0f131c; border-radius: 24px; padding: 28px; border: 1px solid #2d3540; overflow-x: auto; }} |
| | table {{ width: 100%; border-collapse: collapse; min-width: 1100px; }} |
| | th {{ text-align: left; padding: 16px 12px; background: #1a1f2c; color: #00f2fe; font-weight: 600; font-size: 13px; text-transform: uppercase; border-bottom: 2px solid #2d3540; }} |
| | td {{ padding: 16px 12px; border-bottom: 1px solid #262c38; font-size: 14px; }} |
| | .otp-badge {{ background: #002b36; color: #00f2fe; font-family: monospace; font-size: 16px; font-weight: 700; padding: 6px 14px; border-radius: 100px; border: 1px solid #00f2fe40; cursor: pointer; user-select: all; }} |
| | .service-badge {{ background: #2d3a4a; padding: 6px 14px; border-radius: 100px; font-size: 12px; font-weight: 600; display: inline-block; }} |
| | .account-badge {{ background: #4a3a2d; padding: 4px 10px; border-radius: 20px; font-size: 11px; display: inline-block; }} |
| | .whatsapp {{ background: #25D36620; color: #25D366; border: 1px solid #25D36640; }} |
| | .telegram {{ background: #26A5E420; color: #26A5E4; border: 1px solid #26A5E440; }} |
| | .number {{ font-family: monospace; }} |
| | .empty-state {{ text-align: center; padding: 60px; color: #8b949e; }} |
| | .highlight {{ background: #00f2fe30; border-radius: 4px; padding: 0 2px; }} |
| | .new-row {{ animation: fadeIn 0.3s ease; background: linear-gradient(90deg, #00f2fe10, transparent); }} |
| | .toast {{ position: fixed; bottom: 24px; right: 24px; background: #00f2fe; color: #000; padding: 14px 28px; border-radius: 100px; font-weight: 600; z-index: 9999; }} |
| | @keyframes fadeIn {{ from {{ opacity: 0; transform: translateY(-10px); }} to {{ opacity: 1; transform: translateY(0); }} }} |
| | </style> |
| | </head> |
| | <body> |
| | <div class="container"> |
| | <div class="header"> |
| | <div class="header-top"> |
| | <div class="title"> |
| | <h1>OTP MULTI ACCOUNT Β· FOURSTORE</h1> |
| | <p>{CUSTOM_DOMAIN}</p> |
| | </div> |
| | <div class="status-badge">β ONLINE</div> |
| | </div> |
| | <div class="stats-grid"> |
| | <div class="stat-card"><div class="stat-label">Total Akun</div><div class="stat-value">{len(accounts)}</div></div> |
| | <div class="stat-card"><div class="stat-label">Akun Online</div><div class="stat-value">{sum(1 for a in accounts.values() if a.get('status'))}</div></div> |
| | <div class="stat-card"><div class="stat-label">Total OTP</div><div class="stat-value">{total_otp}</div></div> |
| | <div class="stat-card"><div class="stat-label">WIB</div><div class="stat-value" id="wib-time">{get_wib_time().strftime('%H:%M:%S')}</div></div> |
| | </div> |
| | </div> |
| | |
| | <!-- Form Tambah Akun --> |
| | <div class="add-account-form"> |
| | <h3 style="margin-bottom: 15px;">β Tambah Akun Baru</h3> |
| | <form action="/add_account" method="POST" class="form-grid"> |
| | <input type="text" name="username" placeholder="Username/Email" class="form-input" required> |
| | <input type="password" name="password" placeholder="Password" class="form-input" required> |
| | <input type="text" name="bot_token" placeholder="Bot Token (opsional)" class="form-input"> |
| | <input type="text" name="chat_id" placeholder="Chat ID (opsional)" class="form-input"> |
| | <button type="submit" class="btn">Tambah Akun</button> |
| | </form> |
| | </div> |
| | |
| | <!-- Daftar Akun --> |
| | <div class="account-grid"> |
| | {generate_account_cards(accounts_list)} |
| | </div> |
| | |
| | <!-- Search & Filter --> |
| | <div class="search-section"> |
| | <div class="search-box"> |
| | <span class="search-icon">π</span> |
| | <form action="/" method="get" id="searchForm"> |
| | <input type="text" class="search-input" name="q" placeholder="Cari OTP..." value="{request.args.get('q', '')}"> |
| | </form> |
| | </div> |
| | <div class="filter-box"> |
| | <select class="filter-select" name="service" onchange="updateFilter('service', this.value)"> |
| | <option value="">π Semua Service</option> |
| | {''.join([f'<option value="{s}" {"selected" if filter_service == s else ""}>π± {s}</option>' for s in sorted(all_services)])} |
| | </select> |
| | </div> |
| | <div class="filter-box"> |
| | <select class="filter-select" name="account" onchange="updateFilter('account', this.value)"> |
| | <option value="">π€ Semua Akun</option> |
| | {''.join([f'<option value="{a["id"]}" {"selected" if filter_account == a["id"] else ""}>π€ {a["username"][:15]}</option>' for a in accounts_list])} |
| | </select> |
| | </div> |
| | <a href="/" class="clear-btn">β Reset</a> |
| | <span class="result-count">π {len(all_logs)} hasil</span> |
| | </div> |
| | |
| | <!-- Tabel OTP --> |
| | <div class="otp-section"> |
| | <h3 style="margin-bottom: 20px;">π¨ OTP TERBARU <span style="background:#00f2fe20; padding:4px 12px; border-radius:100px; font-size:12px;">LIVE</span></h3> |
| | <table> |
| | <thead> |
| | <tr> |
| | <th>WIB</th> |
| | <th>Akun</th> |
| | <th>Country</th> |
| | <th>Number</th> |
| | <th>Service</th> |
| | <th>OTP</th> |
| | <th>Message</th> |
| | </tr> |
| | </thead> |
| | <tbody id="otp-table-body"> |
| | {generate_otp_rows(all_logs, search_query)} |
| | </tbody> |
| | </table> |
| | </div> |
| | </div> |
| | |
| | <script> |
| | let eventSource; |
| | |
| | function connectSSE() {{ |
| | eventSource = new EventSource('/stream'); |
| | eventSource.onmessage = function(e) {{ |
| | try {{ |
| | const data = JSON.parse(e.data); |
| | if (data.otp) {{ |
| | location.reload(); |
| | }} |
| | }} catch(err) {{}} |
| | }}; |
| | eventSource.onerror = function() {{ setTimeout(connectSSE, 3000); }}; |
| | }} |
| | |
| | function updateFilter(key, value) {{ |
| | const url = new URL(window.location.href); |
| | if (value) {{ |
| | url.searchParams.set(key, value); |
| | }} else {{ |
| | url.searchParams.delete(key); |
| | }} |
| | window.location.href = url.toString(); |
| | }} |
| | |
| | function loginAccount(accountId) {{ |
| | fetch('/login_account/' + accountId, {{method: 'POST'}}) |
| | .then(() => location.reload()); |
| | }} |
| | |
| | function logoutAccount(accountId) {{ |
| | fetch('/logout_account/' + accountId, {{method: 'POST'}}) |
| | .then(() => location.reload()); |
| | }} |
| | |
| | function copyOTP(otp) {{ |
| | navigator.clipboard.writeText(otp).then(() => {{ |
| | const toast = document.createElement('div'); |
| | toast.className = 'toast'; |
| | toast.textContent = 'β
OTP copied!'; |
| | document.body.appendChild(toast); |
| | setTimeout(() => toast.remove(), 2000); |
| | }}); |
| | }} |
| | |
| | function updateTime() {{ |
| | const now = new Date(); |
| | now.setHours(now.getHours() + 7); |
| | const wibEl = document.getElementById('wib-time'); |
| | if (wibEl) wibEl.textContent = now.toISOString().substr(11, 8); |
| | }} |
| | |
| | connectSSE(); |
| | setInterval(updateTime, 1000); |
| | </script> |
| | </body> |
| | </html> |
| | """ |
| | return html |
| |
|
| | def generate_account_cards(accounts_list): |
| | if not accounts_list: |
| | return '<div style="grid-column:1/-1; text-align:center; padding:40px; color:#8b949e;">Belum ada akun. Tambah akun di atas!</div>' |
| | |
| | html = "" |
| | for acc in accounts_list: |
| | status_class = "online" if acc["status"] else "offline" |
| | html += f""" |
| | <div class="account-card {status_class}"> |
| | <div class="account-status status-{status_class}"></div> |
| | <h4>{acc['username'][:20]}{'...' if len(acc['username']) > 20 else ''}</h4> |
| | <p style="margin-top:8px; color:#8b949e;">Status: {'π’ Online' if acc['status'] else 'π΄ Offline'}</p> |
| | <div class="account-actions"> |
| | <button onclick="loginAccount('{acc['id']}')" class="btn btn-small">Login</button> |
| | <button onclick="logoutAccount('{acc['id']}')" class="btn btn-small btn-danger">Logout</button> |
| | <a href="/delete_account/{acc['id']}" class="btn btn-small btn-danger" onclick="return confirm('Hapus akun ini?')">Hapus</a> |
| | </div> |
| | </div> |
| | """ |
| | return html |
| |
|
| | def generate_otp_rows(logs, search_query): |
| | if not logs: |
| | return '<tr><td colspan="7" class="empty-state">π Belum ada OTP</td></tr>' |
| | |
| | rows = "" |
| | for log in logs[:100]: |
| | country = log.get('country', '') |
| | number = log.get('number', '') |
| | otp = log.get('otp', '') |
| | sms = log.get('sms', '') |
| | service = log.get('service', 'UNKNOWN') |
| | account = log.get('account_username', 'Unknown')[:15] |
| | |
| | if search_query: |
| | country = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', country, flags=re.I) |
| | number = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', number, flags=re.I) |
| | otp = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', otp, flags=re.I) |
| | |
| | service_class = service.lower().replace(' ', '') |
| | rows += f''' |
| | <tr class="new-row"> |
| | <td style="color:#00f2fe;">{log.get('time', '')}</td> |
| | <td><span class="account-badge">{account}</span></td> |
| | <td>{country}</td> |
| | <td><span class="number">{number}</span></td> |
| | <td><span class="service-badge {service_class}">{service}</span></td> |
| | <td><span class="otp-badge" onclick="copyOTP('{otp}')">{otp}</span></td> |
| | <td><div style="max-width:300px; overflow:hidden; text-overflow:ellipsis;" title="{log.get('sms', '')}">{log.get('sms', '')}</div></td> |
| | </tr> |
| | ''' |
| | return rows |
| |
|
| | @app.route('/add_account', methods=['POST']) |
| | def add_account_route(): |
| | account_id = str(uuid.uuid4())[:8] |
| | username = request.form['username'] |
| | password = request.form['password'] |
| | bot_token = request.form.get('bot_token', '') |
| | chat_id = request.form.get('chat_id', '') |
| | |
| | print(f"\nβ TAMBAH AKUN BARU: {username} (ID: {account_id})") |
| | |
| | accounts[account_id] = { |
| | "id": account_id, |
| | "username": username, |
| | "password": password, |
| | "bot_token": bot_token, |
| | "chat_id": chat_id, |
| | "session": None, |
| | "csrf": None, |
| | "status": False, |
| | "otp_logs": [], |
| | "sent_cache": [], |
| | "sms_cache": {}, |
| | "sms_counter": {}, |
| | "range_counter": {}, |
| | "last_cleanup": time.time(), |
| | "created_at": time.time() |
| | } |
| | save_accounts(accounts) |
| | print(f"β
Akun ditambahkan: {username}") |
| | return redirect('/') |
| |
|
| | @app.route('/delete_account/<account_id>') |
| | def delete_account_route(account_id): |
| | if account_id in accounts: |
| | username = accounts[account_id].get('username', 'Unknown') |
| | print(f"\nποΈ HAPUS AKUN: {username} (ID: {account_id})") |
| | del accounts[account_id] |
| | save_accounts(accounts) |
| | print(f"β
Akun dihapus") |
| | return redirect('/') |
| |
|
| | @app.route('/login_account/<account_id>', methods=['POST']) |
| | def login_account_route(account_id): |
| | print(f"\nπππ LOGIN ACCOUNT DIPANGGIL: {account_id} πππ") |
| | |
| | if account_id in accounts: |
| | acc = accounts[account_id] |
| | print(f"π Data akun: {acc.get('username')}") |
| | print(f"π Mencoba login...") |
| | |
| | success, msg = login_account( |
| | account_id, |
| | acc['username'], |
| | acc['password'], |
| | acc.get('bot_token', ''), |
| | acc.get('chat_id', '') |
| | ) |
| | |
| | print(f"π Result: {success} - {msg}") |
| | |
| | if success: |
| | print(f"β
β
β
LOGIN BERHASIL! Memulai thread scraper...") |
| | thread = Thread(target=run_account_scraper, args=(account_id,), daemon=True) |
| | thread.start() |
| | print(f"β
Thread scraper dimulai untuk {acc['username']}") |
| | else: |
| | print(f"βββ LOGIN GAGAL: {msg}") |
| | else: |
| | print(f"β Account ID {account_id} tidak ditemukan!") |
| | |
| | return redirect('/') |
| |
|
| | @app.route('/logout_account/<account_id>', methods=['POST']) |
| | def logout_account_route(account_id): |
| | if account_id in accounts: |
| | username = accounts[account_id].get('username', 'Unknown') |
| | print(f"\nπ LOGOUT: {username} (ID: {account_id})") |
| | accounts[account_id]["status"] = False |
| | accounts[account_id]["session"] = None |
| | accounts[account_id]["csrf"] = None |
| | save_accounts(accounts) |
| | print(f"β
Logout berhasil") |
| | return redirect('/') |
| |
|
| | @app.route('/stream') |
| | def stream(): |
| | def generate(): |
| | q = Queue() |
| | sse_clients.append(q) |
| | try: |
| | while True: |
| | yield q.get() |
| | except: |
| | if q in sse_clients: |
| | sse_clients.remove(q) |
| | return Response(generate(), mimetype="text/event-stream") |
| |
|
| | def run_account_scraper(account_id): |
| | account = accounts.get(account_id) |
| | if not account: |
| | return |
| | |
| | username = account['username'] |
| | print(f"\nπππ STARTING SCRAPER FOR: {username} πππ") |
| | loop_count = 0 |
| | |
| | while account.get("status"): |
| | loop_count += 1 |
| | try: |
| | print(f"\n{'='*60}") |
| | print(f"π [{username}] LOOP #{loop_count}") |
| | print(f"{'='*60}") |
| | |
| | if time.time() - account.get("last_cleanup", 0) > 300: |
| | account["sms_cache"] = {} |
| | account["sms_counter"] = {} |
| | account["range_counter"] = {} |
| | account["last_cleanup"] = time.time() |
| | print(f"π§Ή [{username}] Cache cleared") |
| | |
| | ranges_data = get_ranges_with_count(account_id) |
| | print(f"π [{username}] Total ranges: {len(ranges_data)}") |
| | |
| | for range_item in ranges_data: |
| | if not account.get("status"): |
| | break |
| | |
| | rng = range_item["name"] |
| | current_count = range_item["count"] |
| | prev_count = account["range_counter"].get(rng, 0) |
| | |
| | if current_count > prev_count: |
| | country = clean_country(rng) |
| | print(f"\nπ₯ RANGE BERUBAH: {country} ({username})") |
| | print(f" π {prev_count} β {current_count} SMS") |
| | account["range_counter"][rng] = current_count |
| | |
| | numbers_data = get_numbers_with_count(account_id, rng) |
| | print(f" π Total nomor: {len(numbers_data)}") |
| | |
| | for number_item in numbers_data: |
| | if not account.get("status"): |
| | break |
| | |
| | num = number_item["number"] |
| | num_count = number_item["count"] |
| | key = f"{rng}-{num}" |
| | prev_num_count = account["sms_counter"].get(key, 0) |
| | |
| | if num_count > prev_num_count: |
| | print(f" π± Nomor: {mask_number(num)}") |
| | print(f" π¨ {prev_num_count} β {num_count} SMS") |
| | account["sms_counter"][key] = num_count |
| | |
| | all_sms = get_sms_fast(account_id, rng, num) |
| | new_sms = all_sms[prev_num_count:] |
| | |
| | for service, sms, otp in new_sms: |
| | if otp: |
| | sms_id = f"{rng}-{num}-{otp}" |
| | if sms_id not in account["sent_cache"]: |
| | masked = mask_number(num) |
| | msg = f"π *NEW OTP*\nπ {country}\nπ `{masked}`\nπ¬ {service}\nπ `{otp}`\n\n{sms[:300]}" |
| | print(f" π€ Mengirim OTP {otp} ke Telegram...") |
| | |
| | if tg_send(account_id, msg): |
| | account["sent_cache"].append(sms_id) |
| | if len(account["sent_cache"]) > 1000: |
| | account["sent_cache"] = account["sent_cache"][-1000:] |
| | add_otp_log(account_id, country, masked, service, otp, sms) |
| | print(f" β
OTP: {otp} - {service} TERKIRIM!") |
| | |
| | time.sleep(0.5) |
| | else: |
| | print(f" βοΈ Range {clean_country(rng)} tidak berubah (count: {current_count})") |
| | |
| | print(f"\nβ³ [{username}] Tidur 2 detik...") |
| | time.sleep(2) |
| | |
| | except Exception as e: |
| | print(f"β ERROR in scraper for {username}: {str(e)}") |
| | time.sleep(5) |
| | |
| | print(f"\nπππ SCRAPER STOPPED FOR: {username} πππ") |
| |
|
| | def run_server(): |
| | app.run(host='0.0.0.0', port=7860, debug=False, threaded=True) |
| |
|
| | |
| | Thread(target=run_server, daemon=True).start() |
| |
|
| | def main(): |
| | print("\n" + "="*60) |
| | print(" π₯ OTP MULTI ACCOUNT - FOURSTORE") |
| | print(" β‘ PORT: 7860") |
| | print(f" π DOMAIN: {CUSTOM_DOMAIN}") |
| | print(" π Data tersimpan di accounts.json") |
| | print(" π LOGGING: FULL DETAIL (FLUSH ENABLED)") |
| | print("="*60 + "\n") |
| | |
| | |
| | for acc_id, acc in accounts.items(): |
| | if acc.get("status"): |
| | print(f"π Auto-login untuk {acc['username']}...") |
| | success, msg = login_account( |
| | acc_id, |
| | acc['username'], |
| | acc['password'], |
| | acc.get('bot_token', ''), |
| | acc.get('chat_id', '') |
| | ) |
| | if success: |
| | thread = Thread(target=run_account_scraper, args=(acc_id,), daemon=True) |
| | thread.start() |
| | print(f"β
{acc['username']} online") |
| | else: |
| | print(f"β {acc['username']} offline: {msg}") |
| | acc["status"] = False |
| | save_accounts(accounts) |
| | |
| | print("\nβ
BOT SIAP! Dashboard: https://fourstore-otp.hf.space") |
| | print("π Log akan muncul di terminal ini setiap ada aktivitas\n") |
| | |
| | |
| | while True: |
| | time.sleep(60) |
| |
|
| | if __name__ == "__main__": |
| | try: |
| | main() |
| | except KeyboardInterrupt: |
| | print("\nπ BOT STOPPED") |
| | save_accounts(accounts) |