| | import httpx |
| | from bs4 import BeautifulSoup |
| | import re |
| | from datetime import datetime, timedelta, timezone |
| | import time |
| | from flask import Flask, Response, request, redirect, url_for, render_template_string |
| | from threading import Thread |
| | import json |
| | from queue import Queue |
| | import os |
| | import uuid |
| | import sys |
| | import builtins |
| |
|
| | |
| | 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 mask_email(email): |
| | if not email or '@' not in email: |
| | return email |
| | local, domain = email.split('@', 1) |
| | if len(local) <= 3: |
| | masked_local = local[0] + '***' + local[-1] if len(local) > 1 else '***' |
| | else: |
| | masked_local = local[:2] + '***' + local[-2:] |
| | return f"{masked_local}@{domain}" |
| |
|
| | |
| | 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() |
| | masked_username = mask_email(accounts[acc_id].get('username', '')) |
| | print(f" ✅ Account {acc_id}: {masked_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: |
| | masked_username = mask_email(username) |
| | print(f"\n{'='*60}") |
| | print(f"🔐 PROSES LOGIN UNTUK: {masked_username} (ID: {account_id})") |
| | print(f"{'='*60}") |
| | |
| | session = httpx.Client(follow_redirects=True, timeout=30.0) |
| | |
| | r = session.get(LOGIN_URL, timeout=30) |
| | if r.status_code != 200: |
| | return False, f"HTTP {r.status_code}" |
| | |
| | soup = BeautifulSoup(r.text, "html.parser") |
| | token = soup.find("input", {"name": "_token"}) |
| | if not token: |
| | return False, "Token tidak ditemukan" |
| | |
| | csrf_token = token.get("value") |
| | |
| | r = session.post(LOGIN_URL, data={ |
| | "_token": csrf_token, |
| | "email": username, |
| | "password": password |
| | }, timeout=30) |
| | |
| | if "dashboard" in r.text.lower() or "logout" in r.text.lower(): |
| | 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: |
| | return False, "Login gagal - cek username/password" |
| | |
| | except Exception as 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): |
| | """Ambil daftar range/country dengan jumlah SMS""" |
| | account = accounts.get(account_id) |
| | if not account or not account.get("session") or not account.get("csrf"): |
| | return [] |
| | |
| | try: |
| | date = get_search_date() |
| | print(f"📡 GET RANGES: {date}") |
| | |
| | r = account["session"].post(GET_RANGE_URL, data={ |
| | "_token": account["csrf"], |
| | "from": date, |
| | "to": date |
| | }, timeout=15) |
| |
|
| | |
| | with open(f"debug_ranges_{account_id}.html", "w") as f: |
| | f.write(r.text) |
| | |
| | soup = BeautifulSoup(r.text, "html.parser") |
| | ranges_data = [] |
| |
|
| | |
| | for rng_div in soup.find_all("div", class_="rng"): |
| | try: |
| | |
| | name_span = rng_div.find("span", class_="rname") |
| | if not name_span: |
| | continue |
| | |
| | rng = name_span.get_text(strip=True) |
| | |
| | |
| | count_div = rng_div.find("div", class_="v-count") |
| | count = 0 |
| | if count_div: |
| | count_text = count_div.get_text(strip=True) |
| | try: |
| | count = int(count_text) |
| | except: |
| | count = 0 |
| | |
| | ranges_data.append({ |
| | "name": rng, |
| | "count": count |
| | }) |
| | |
| | print(f" ✅ Range: {rng} | Count: {count}") |
| | |
| | except Exception as e: |
| | print(f" ❌ Error parse range: {e}") |
| | continue |
| |
|
| | print(f"📊 Total ranges: {len(ranges_data)}") |
| | return ranges_data |
| | |
| | except Exception as e: |
| | print(f"❌ Error get_ranges: {e}") |
| | return [] |
| |
|
| | def get_numbers_with_count(account_id, rng): |
| | """Ambil daftar nomor untuk range tertentu""" |
| | account = accounts.get(account_id) |
| | if not account or not account.get("session") or not account.get("csrf"): |
| | return [] |
| | |
| | try: |
| | date = get_search_date() |
| | print(f"📡 GET NUMBERS for {rng}") |
| | |
| | r = account["session"].post(GET_NUMBER_URL, data={ |
| | "_token": account["csrf"], |
| | "start": date, |
| | "end": date, |
| | "range": rng |
| | }, timeout=15) |
| |
|
| | |
| | with open(f"debug_numbers_{account_id}_{rng.replace(' ', '_')}.html", "w") as f: |
| | f.write(r.text) |
| | |
| | soup = BeautifulSoup(r.text, "html.parser") |
| | numbers_data = [] |
| |
|
| | |
| | for nrow_div in soup.find_all("div", class_="nrow"): |
| | try: |
| | |
| | num_span = nrow_div.find("span", class_="nnum") |
| | if not num_span: |
| | continue |
| | |
| | |
| | num_text = num_span.get_text(strip=True) |
| | num = re.sub(r'[^\d]', '', num_text) |
| | |
| | if not num or len(num) < 5: |
| | continue |
| | |
| | |
| | count_div = nrow_div.find("div", class_="v-count") |
| | count = 0 |
| | if count_div: |
| | try: |
| | count = int(count_div.get_text(strip=True)) |
| | except: |
| | count = 0 |
| | |
| | numbers_data.append({ |
| | "number": num, |
| | "count": count |
| | }) |
| | |
| | print(f" ✅ Number: {mask_number(num)} | Count: {count}") |
| | |
| | except Exception as e: |
| | print(f" ❌ Error parse number: {e}") |
| | continue |
| |
|
| | print(f" 📊 Total numbers: {len(numbers_data)}") |
| | return numbers_data |
| | |
| | except Exception as e: |
| | print(f"❌ Error get_numbers: {e}") |
| | return [] |
| |
|
| | def get_sms_fast(account_id, rng, number): |
| | """Ambil SMS untuk nomor tertentu""" |
| | 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 |
| |
|
| | print(f" 📨 GET SMS for {mask_number(number)}") |
| |
|
| | r = account["session"].post(GET_SMS_URL, data={ |
| | "_token": account["csrf"], |
| | "start": date, |
| | "end": date, |
| | "Number": number, |
| | "Range": rng |
| | }, timeout=20) |
| |
|
| | |
| | with open(f"debug_sms_{account_id}_{number}.html", "w") as f: |
| | f.write(r.text) |
| | |
| | soup = BeautifulSoup(r.text, "html.parser") |
| | results = [] |
| |
|
| | |
| | table = soup.find("table") |
| | if table: |
| | tbody = table.find("tbody") |
| | if tbody: |
| | for row in tbody.find_all("tr"): |
| | try: |
| | cells = row.find_all("td") |
| | if len(cells) >= 2: |
| | |
| | sender_cell = cells[0] |
| | service = "UNKNOWN" |
| | cli_tag = sender_cell.find("span", class_="cli-tag") |
| | if cli_tag: |
| | service = map_service(cli_tag.get_text(strip=True)) |
| | |
| | |
| | msg_cell = cells[1] |
| | msg_div = msg_cell.find("div", class_="msg-text") |
| | sms = msg_div.get_text(strip=True) if msg_div else msg_cell.get_text(strip=True) |
| | |
| | |
| | time_cell = cells[2] if len(cells) > 2 else None |
| | sms_time = time_cell.get_text(strip=True) if time_cell else "" |
| | |
| | |
| | otp = extract_otp(sms) |
| | |
| | if otp: |
| | results.append((service, sms, otp, sms_time)) |
| | print(f" ✅ OTP: {otp} | Service: {service}") |
| | |
| | except Exception as e: |
| | print(f" ❌ Error parse SMS row: {e}") |
| | continue |
| |
|
| | |
| | account["sms_cache"][cache_key] = (time.time(), results) |
| | return results |
| | |
| | except Exception as e: |
| | print(f" ❌ Error get_sms: {e}") |
| | 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": mask_email(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) |
| |
|
| | |
| | HTML_TEMPLATE = """ |
| | <!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; } |
| | |
| | /* Navbar */ |
| | .navbar { |
| | background: linear-gradient(145deg, #1a1f2c, #0f131c); |
| | border-bottom: 1px solid #2d3540; |
| | padding: 16px 32px; |
| | position: sticky; |
| | top: 0; |
| | z-index: 1000; |
| | display: flex; |
| | justify-content: space-between; |
| | align-items: center; |
| | flex-wrap: wrap; |
| | gap: 20px; |
| | } |
| | .nav-brand { |
| | display: flex; |
| | align-items: center; |
| | gap: 20px; |
| | } |
| | .nav-brand h1 { |
| | font-size: 24px; |
| | font-weight: 700; |
| | background: linear-gradient(135deg, #00f2fe, #4facfe); |
| | -webkit-background-clip: text; |
| | -webkit-text-fill-color: transparent; |
| | } |
| | .nav-stats { |
| | display: flex; |
| | gap: 20px; |
| | color: #8b949e; |
| | font-size: 14px; |
| | } |
| | .nav-stats span { |
| | color: #00f2fe; |
| | font-weight: 600; |
| | margin-left: 5px; |
| | } |
| | .nav-actions { |
| | display: flex; |
| | gap: 12px; |
| | } |
| | .nav-btn { |
| | padding: 8px 20px; |
| | border-radius: 100px; |
| | font-size: 14px; |
| | font-weight: 500; |
| | cursor: pointer; |
| | transition: all 0.2s; |
| | border: 1px solid #2d3540; |
| | background: #1a1f2c; |
| | color: #e4e6eb; |
| | } |
| | .nav-btn:hover { |
| | border-color: #00f2fe; |
| | color: #00f2fe; |
| | } |
| | .nav-btn.active { |
| | background: #00f2fe; |
| | color: #0a0c10; |
| | border-color: #00f2fe; |
| | } |
| | |
| | /* Panel Akun (Dropdown) */ |
| | .accounts-panel { |
| | background: #1a1f2c; |
| | border-radius: 16px; |
| | margin: 0 24px 24px 24px; |
| | border: 1px solid #2d3540; |
| | overflow: hidden; |
| | display: none; |
| | } |
| | .accounts-panel.show { |
| | display: block; |
| | } |
| | .panel-header { |
| | padding: 20px; |
| | background: #0f131c; |
| | border-bottom: 1px solid #2d3540; |
| | display: flex; |
| | justify-content: space-between; |
| | align-items: center; |
| | } |
| | .panel-header h3 { |
| | color: #00f2fe; |
| | } |
| | .close-panel { |
| | background: none; |
| | border: none; |
| | color: #8b949e; |
| | font-size: 24px; |
| | cursor: pointer; |
| | } |
| | .accounts-grid { |
| | padding: 20px; |
| | display: grid; |
| | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); |
| | gap: 20px; |
| | } |
| | .account-card { |
| | background: #0f131c; |
| | border-radius: 12px; |
| | padding: 16px; |
| | border: 1px solid #2d3540; |
| | position: relative; |
| | } |
| | .account-card.online { border-color: #00f2fe; } |
| | .account-status { |
| | position: absolute; |
| | top: 16px; |
| | right: 16px; |
| | width: 10px; |
| | height: 10px; |
| | border-radius: 50%; |
| | } |
| | .status-online { background: #00f2fe; box-shadow: 0 0 10px #00f2fe; } |
| | .status-offline { background: #ff4d4d; } |
| | .account-email { |
| | font-weight: 600; |
| | margin-bottom: 8px; |
| | padding-right: 20px; |
| | } |
| | .account-actions { |
| | display: flex; |
| | gap: 8px; |
| | margin-top: 12px; |
| | flex-wrap: wrap; |
| | } |
| | .btn { |
| | padding: 6px 12px; |
| | border-radius: 8px; |
| | font-size: 12px; |
| | font-weight: 500; |
| | cursor: pointer; |
| | border: none; |
| | transition: all 0.2s; |
| | } |
| | .btn-primary { |
| | background: #00f2fe; |
| | color: #0a0c10; |
| | } |
| | .btn-danger { |
| | background: #ff4d4d; |
| | color: white; |
| | } |
| | .btn-secondary { |
| | background: #2d3540; |
| | color: #e4e6eb; |
| | } |
| | |
| | /* Form Tambah Akun (Modal) */ |
| | .modal { |
| | display: none; |
| | position: fixed; |
| | top: 0; |
| | left: 0; |
| | width: 100%; |
| | height: 100%; |
| | background: rgba(0,0,0,0.8); |
| | z-index: 2000; |
| | justify-content: center; |
| | align-items: center; |
| | } |
| | .modal.show { |
| | display: flex; |
| | } |
| | .modal-content { |
| | background: #1a1f2c; |
| | border-radius: 24px; |
| | padding: 32px; |
| | width: 90%; |
| | max-width: 500px; |
| | border: 1px solid #2d3540; |
| | } |
| | .modal-content h3 { |
| | margin-bottom: 24px; |
| | color: #00f2fe; |
| | } |
| | .form-group { |
| | margin-bottom: 16px; |
| | } |
| | .form-input { |
| | width: 100%; |
| | padding: 12px 16px; |
| | background: #0a0c10; |
| | border: 1px solid #2d3540; |
| | border-radius: 12px; |
| | color: #e4e6eb; |
| | font-size: 14px; |
| | } |
| | .form-input:focus { |
| | outline: none; |
| | border-color: #00f2fe; |
| | } |
| | .modal-actions { |
| | display: flex; |
| | gap: 12px; |
| | margin-top: 24px; |
| | } |
| | .modal-btn { |
| | flex: 1; |
| | padding: 12px; |
| | border-radius: 12px; |
| | font-weight: 600; |
| | cursor: pointer; |
| | border: none; |
| | } |
| | .btn-submit { |
| | background: #00f2fe; |
| | color: #0a0c10; |
| | } |
| | .btn-cancel { |
| | background: #2d3540; |
| | color: #e4e6eb; |
| | } |
| | |
| | /* Search Section */ |
| | .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; |
| | } |
| | .result-count { |
| | color: #8b949e; |
| | font-size: 14px; |
| | margin-left: auto; |
| | } |
| | |
| | /* OTP Table */ |
| | .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; } |
| | .google { background: #4285F420; color: #4285F4; border: 1px solid #4285F440; } |
| | .facebook { background: #4267B220; color: #4267B2; border: 1px solid #4267B240; } |
| | .instagram { background: #C1358420; color: #C13584; border: 1px solid #C1358440; } |
| | .tiktok { background: #00000020; color: #000000; border: 1px solid #00000040; } |
| | .number { font-family: monospace; } |
| | .empty-state { text-align: center; padding: 60px; color: #8b949e; } |
| | .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> |
| | <!-- Navbar --> |
| | <div class="navbar"> |
| | <div class="nav-brand"> |
| | <h1>FOURSTORE OTP</h1> |
| | <div class="nav-stats"> |
| | <div>📊 Total Akun: <span>{{ total_accounts }}</span></div> |
| | <div>🟢 Online: <span>{{ online_accounts }}</span></div> |
| | <div>📨 OTP: <span>{{ total_otp }}</span></div> |
| | <div>🕐 <span id="wib-time">{{ wib_time }}</span></div> |
| | </div> |
| | </div> |
| | <div class="nav-actions"> |
| | <button class="nav-btn" onclick="toggleAccountsPanel()">👥 Lihat Semua Akun</button> |
| | <button class="nav-btn" onclick="showAddModal()">➕ Tambah Akun</button> |
| | </div> |
| | </div> |
| | |
| | <!-- Panel Akun (Dropdown) --> |
| | <div class="accounts-panel" id="accountsPanel"> |
| | <div class="panel-header"> |
| | <h3>📋 Daftar Semua Akun ({{ total_accounts }})</h3> |
| | <button class="close-panel" onclick="toggleAccountsPanel()">×</button> |
| | </div> |
| | <div class="accounts-grid"> |
| | {% for acc in accounts_list %} |
| | <div class="account-card {{ 'online' if acc.status else 'offline' }}"> |
| | <div class="account-status status-{{ 'online' if acc.status else 'offline' }}"></div> |
| | <div class="account-email">{{ acc.username }}</div> |
| | <div style="font-size: 12px; color: #8b949e; margin-bottom: 8px;"> |
| | ID: {{ acc.id }} |
| | </div> |
| | <div style="font-size: 12px; color: #8b949e; margin-bottom: 12px;"> |
| | Status: {{ '🟢 Online' if acc.status else '🔴 Offline' }} |
| | </div> |
| | <div class="account-actions"> |
| | <button onclick="loginAccount('{{ acc.id }}')" class="btn btn-primary">Login</button> |
| | <button onclick="logoutAccount('{{ acc.id }}')" class="btn btn-secondary">Logout</button> |
| | <button onclick="deleteAccount('{{ acc.id }}')" class="btn btn-danger">Hapus</button> |
| | </div> |
| | </div> |
| | {% endfor %} |
| | </div> |
| | </div> |
| | |
| | <!-- Modal Tambah Akun --> |
| | <div class="modal" id="addModal"> |
| | <div class="modal-content"> |
| | <h3>➕ Tambah Akun Baru</h3> |
| | <form action="/add_account" method="POST" onsubmit="closeModalAfterSubmit()"> |
| | <div class="form-group"> |
| | <input type="email" name="username" placeholder="Email" class="form-input" required> |
| | </div> |
| | <div class="form-group"> |
| | <input type="password" name="password" placeholder="Password" class="form-input" required> |
| | </div> |
| | <div class="form-group"> |
| | <input type="text" name="bot_token" placeholder="Bot Token (opsional)" class="form-input"> |
| | </div> |
| | <div class="form-group"> |
| | <input type="text" name="chat_id" placeholder="Chat ID (opsional)" class="form-input"> |
| | </div> |
| | <div class="modal-actions"> |
| | <button type="submit" class="modal-btn btn-submit">Tambah Akun</button> |
| | <button type="button" class="modal-btn btn-cancel" onclick="hideAddModal()">Batal</button> |
| | </div> |
| | </form> |
| | </div> |
| | </div> |
| | |
| | <!-- Main Content --> |
| | <div class="container" style="max-width: 1600px; margin: 0 auto; padding: 24px;"> |
| | <!-- 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="{{ search_query }}"> |
| | </form> |
| | </div> |
| | <div class="filter-box"> |
| | <select class="filter-select" name="service" onchange="updateFilter('service', this.value)"> |
| | <option value="">📋 Semua Service</option> |
| | {% for s in all_services %} |
| | <option value="{{ s }}" {{ 'selected' if filter_service == s else '' }}>📱 {{ s }}</option> |
| | {% endfor %} |
| | </select> |
| | </div> |
| | <div class="filter-box"> |
| | <select class="filter-select" name="account" onchange="updateFilter('account', this.value)"> |
| | <option value="">👤 Semua Akun</option> |
| | {% for acc in accounts_list %} |
| | <option value="{{ acc.id }}" {{ 'selected' if filter_account == acc.id else '' }}>👤 {{ acc.username[:15] }}</option> |
| | {% endfor %} |
| | </select> |
| | </div> |
| | <a href="/" class="clear-btn">✕ Reset</a> |
| | <span class="result-count">📊 {{ logs_count }} 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"> |
| | {% for log in logs %} |
| | <tr class="new-row"> |
| | <td style="color:#00f2fe;">{{ log.time }}</td> |
| | <td><span class="account-badge">{{ log.account_username[:15] }}</span></td> |
| | <td>{{ log.country }}</td> |
| | <td><span class="number">{{ log.number }}</span></td> |
| | <td><span class="service-badge {{ log.service.lower() }}">{{ log.service }}</span></td> |
| | <td><span class="otp-badge" onclick="copyOTP('{{ log.otp }}')">{{ log.otp }}</span></td> |
| | <td><div style="max-width:300px; overflow:hidden; text-overflow:ellipsis;" title="{{ log.sms }}">{{ log.sms }}</div></td> |
| | </tr> |
| | {% else %} |
| | <tr><td colspan="7" class="empty-state">🔍 Belum ada OTP</td></tr> |
| | {% endfor %} |
| | </tbody> |
| | </table> |
| | </div> |
| | </div> |
| | |
| | <script> |
| | let eventSource; |
| | let addModal = document.getElementById('addModal'); |
| | let accountsPanel = document.getElementById('accountsPanel'); |
| | |
| | function connectSSE() { |
| | eventSource = new EventSource('/stream'); |
| | eventSource.onmessage = function(e) { |
| | try { |
| | const data = JSON.parse(e.data); |
| | // Reload halaman saat ada OTP baru |
| | location.reload(); |
| | } catch(err) {} |
| | }; |
| | eventSource.onerror = function() { |
| | setTimeout(connectSSE, 3000); |
| | }; |
| | } |
| | |
| | function toggleAccountsPanel() { |
| | accountsPanel.classList.toggle('show'); |
| | } |
| | |
| | function showAddModal() { |
| | addModal.classList.add('show'); |
| | } |
| | |
| | function hideAddModal() { |
| | addModal.classList.remove('show'); |
| | } |
| | |
| | function closeModalAfterSubmit() { |
| | setTimeout(() => hideAddModal(), 100); |
| | } |
| | |
| | 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 deleteAccount(accountId) { |
| | if (confirm('Hapus akun ini?')) { |
| | window.location.href = '/delete_account/' + accountId; |
| | } |
| | } |
| | |
| | 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) { |
| | const timeStr = now.toISOString().substr(11, 8); |
| | wibEl.textContent = timeStr; |
| | } |
| | } |
| | |
| | // Auto login for new accounts |
| | function checkNewAccounts() { |
| | // This will be triggered by SSE |
| | } |
| | |
| | connectSSE(); |
| | setInterval(updateTime, 1000); |
| | |
| | // Close modal when clicking outside |
| | window.onclick = function(event) { |
| | if (event.target == addModal) { |
| | hideAddModal(); |
| | } |
| | } |
| | </script> |
| | </body> |
| | </html> |
| | """ |
| |
|
| | @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"] = mask_email(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()) |
| | |
| | |
| | accounts_list = [] |
| | for acc_id, acc in accounts.items(): |
| | accounts_list.append({ |
| | "id": acc_id, |
| | "username": mask_email(acc.get("username", "Unknown")), |
| | "status": acc.get("status", False) |
| | }) |
| | |
| | return render_template_string( |
| | HTML_TEMPLATE, |
| | total_accounts=len(accounts), |
| | online_accounts=sum(1 for a in accounts.values() if a.get('status')), |
| | total_otp=total_otp, |
| | wib_time=get_wib_time().strftime('%H:%M:%S'), |
| | accounts_list=accounts_list, |
| | logs=all_logs[:100], |
| | logs_count=len(all_logs), |
| | search_query=search_query, |
| | filter_service=filter_service, |
| | filter_account=filter_account, |
| | all_services=sorted(all_services) |
| | ) |
| |
|
| | @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', '') |
| | |
| | masked_username = mask_email(username) |
| | print(f"\n➕ TAMBAH AKUN BARU: {masked_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: {masked_username}") |
| | |
| | |
| | print(f"🔄 Auto-login untuk {masked_username}...") |
| | success, msg = login_account( |
| | account_id, |
| | username, |
| | password, |
| | bot_token, |
| | chat_id |
| | ) |
| | if success: |
| | thread = Thread(target=run_account_scraper, args=(account_id,), daemon=True) |
| | thread.start() |
| | print(f"✅ {masked_username} online") |
| | else: |
| | print(f"❌ {masked_username} offline: {msg}") |
| | |
| | return redirect('/') |
| |
|
| | @app.route('/delete_account/<account_id>') |
| | def delete_account_route(account_id): |
| | if account_id in accounts: |
| | username = mask_email(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): |
| | if account_id in accounts: |
| | acc = accounts[account_id] |
| | masked_username = mask_email(acc.get('username')) |
| | print(f"\n🔔 LOGIN MANUAL: {masked_username}") |
| | |
| | success, msg = login_account( |
| | account_id, |
| | acc['username'], |
| | acc['password'], |
| | acc.get('bot_token', ''), |
| | acc.get('chat_id', '') |
| | ) |
| | |
| | if success: |
| | thread = Thread(target=run_account_scraper, args=(account_id,), daemon=True) |
| | thread.start() |
| | print(f"✅ {masked_username} online") |
| | else: |
| | print(f"❌ Login gagal: {msg}") |
| | |
| | return redirect('/') |
| |
|
| | @app.route('/logout_account/<account_id>', methods=['POST']) |
| | def logout_account_route(account_id): |
| | if account_id in accounts: |
| | username = mask_email(accounts[account_id].get('username', 'Unknown')) |
| | print(f"\n🔌 LOGOUT: {username}") |
| | 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 = mask_email(account['username']) |
| | print(f"\n🚀 STARTING SCRAPER FOR: {username}") |
| | loop_count = 0 |
| | |
| | while account.get("status"): |
| | loop_count += 1 |
| | try: |
| | |
| | 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) |
| | |
| | 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) |
| | |
| | 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: |
| | account["sms_counter"][key] = num_count |
| | |
| | |
| | all_sms = get_sms_fast(account_id, rng, num) |
| | new_sms = all_sms[prev_num_count:] if prev_num_count < len(all_sms) else [] |
| | |
| | for service, sms, otp, sms_time in new_sms: |
| | if otp: |
| | sms_id = f"{rng}-{num}-{otp}-{sms_time}" |
| | 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⏰ {sms_time}\n\n{sms[:300]}" |
| | |
| | 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} at {sms_time}") |
| | |
| | time.sleep(0.5) |
| | |
| | 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 (FIXED VERSION)") |
| | print(" ⚡ PORT: 7860") |
| | print(f" 🌐 DOMAIN: {CUSTOM_DOMAIN}") |
| | print(" 📁 Data tersimpan di accounts.json") |
| | print(" 📧 Email akan disensor otomatis") |
| | print(" 🕐 Menggunakan WIB dengan logic sebelum jam 7") |
| | print(" 🔄 Auto-login saat run & saat tambah akun") |
| | print(" ✅ Parsing HTML sudah diperbaiki sesuai struktur") |
| | print("="*60 + "\n") |
| | |
| | |
| | for acc_id, acc in accounts.items(): |
| | if acc.get("status"): |
| | masked_username = mask_email(acc['username']) |
| | print(f"🔄 Auto-login untuk {masked_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"✅ {masked_username} online") |
| | else: |
| | print(f"❌ {masked_username} offline: {msg}") |
| | acc["status"] = False |
| | save_accounts(accounts) |
| | |
| | print(f"\n✅ BOT SIAP! Dashboard: {CUSTOM_DOMAIN}") |
| | 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) |