otp / app.py
Fourstore's picture
Update app.py
202a3b8 verified
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
# Force print to flush immediately
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}"
# Load accounts dari file
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}")
# Inisialisasi accounts
accounts = load_accounts()
print(f"\n📊 Loaded {len(accounts)} accounts from file")
# Tambahkan runtime data ke setiap account
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
# Format: 103-929 atau 103929 atau 6 digit
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)
# Debug: simpan response
with open(f"debug_ranges_{account_id}.html", "w") as f:
f.write(r.text)
soup = BeautifulSoup(r.text, "html.parser")
ranges_data = []
# Mencari div dengan class "rng"
for rng_div in soup.find_all("div", class_="rng"):
try:
# Ambil nama range dari span dengan class "rname"
name_span = rng_div.find("span", class_="rname")
if not name_span:
continue
rng = name_span.get_text(strip=True)
# Ambil count dari div dengan class "v-count"
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)
# Debug
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 = []
# Mencari div dengan class "nrow"
for nrow_div in soup.find_all("div", class_="nrow"):
try:
# Ambil nomor dari span dengan class "nnum"
num_span = nrow_div.find("span", class_="nnum")
if not num_span:
continue
# Extract nomor dari teks
num_text = num_span.get_text(strip=True)
num = re.sub(r'[^\d]', '', num_text)
if not num or len(num) < 5:
continue
# Ambil count dari div dengan class "v-count"
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}"
# Cek cache (5 detik)
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)
# Debug
with open(f"debug_sms_{account_id}_{number}.html", "w") as f:
f.write(r.text)
soup = BeautifulSoup(r.text, "html.parser")
results = []
# Mencari tabel SMS
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/Service
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))
# Message
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
time_cell = cells[2] if len(cells) > 2 else None
sms_time = time_cell.get_text(strip=True) if time_cell else ""
# Extract OTP
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
# Simpan ke cache
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 LENGKAP
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()">&times;</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():
# Gabungkan semua log dari semua akun
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')]))
# Hitung total OTP dari semua akun
total_otp = sum(len(acc.get('sent_cache', [])) for acc in accounts.values())
# Daftar akun untuk filter (dengan email sudah di-sensor)
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}")
# Auto login setelah tambah akun
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:
# Cleanup cache setiap 5 menit
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")
# Ambil daftar ranges
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)
# Cek apakah ada SMS baru di range ini
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
# Ambil daftar nomor untuk range ini
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)
# Cek apakah ada SMS baru di nomor ini
if num_count > prev_num_count:
account["sms_counter"][key] = num_count
# Ambil SMS baru
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) # Delay antar range
time.sleep(2) # Delay utama
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)
# Start server thread
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")
# Login otomatis untuk akun yang sudah login sebelumnya
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")
# Keep main thread alive
while True:
time.sleep(60)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n🛑 BOT STOPPED")
save_accounts(accounts)