otp / app.py
Fourstore's picture
Update app.py
3f4ea2c verified
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
from pymongo import MongoClient
from pymongo.errors import ConnectionFailure
MONGODB_URI = os.environ.get("MONGODB_URI")
DB_NAME = "otp_bot"
COLLECTION_NAME = "accounts"
if MONGODB_URI:
try:
mongo_client = MongoClient(MONGODB_URI)
db = mongo_client[DB_NAME]
accounts_collection = db[COLLECTION_NAME]
mongo_client.admin.command('ping')
print("✅ MongoDB Connected!")
# Cek isi database
count = accounts_collection.count_documents({})
print(f"📊 Total dokumen di MongoDB: {count}")
except ConnectionFailure as e:
print(f"❌ MongoDB Connection Failed: {e}")
mongo_client = None
else:
print("⚠️ MONGODB_URI tidak di set, menggunakan penyimpanan lokal")
mongo_client = None
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
parts = email.split('@')
username = parts[0]
domain = parts[1]
if len(username) <= 3:
masked_username = username[0] + '*' * (len(username) - 1)
else:
masked_username = username[:2] + '*' * (len(username) - 3) + username[-1]
return f"{masked_username}@{domain}"
def load_accounts_from_mongodb():
accounts_dict = {}
try:
if mongo_client:
print("📥 Loading accounts from MongoDB...")
cursor = accounts_collection.find({})
count = 0
for doc in cursor:
count += 1
acc_id = doc.pop("_id")
doc["session"] = None
doc["csrf"] = None
doc["status"] = False
doc["otp_logs"] = doc.get("otp_logs", [])
doc["sent_cache"] = []
doc["sms_cache"] = {}
doc["sms_counter"] = {}
doc["range_counter"] = {}
doc["last_cleanup"] = time.time()
accounts_dict[acc_id] = doc
print(f"📊 Loaded {len(accounts_dict)} accounts from MongoDB")
if count > 0:
print(f"✅ Data MongoDB berhasil dimuat: {count} akun")
# Tampilkan sample
for acc_id, acc in list(accounts_dict.items())[:2]:
email_masked = mask_email(acc.get('username', 'Unknown'))
log_count = len(acc.get('otp_logs', []))
print(f" - Akun: {email_masked} | OTP Logs: {log_count}")
else:
print("⚠️ Tidak ada data di MongoDB")
except Exception as e:
print(f"❌ Error load from MongoDB: {e}")
return accounts_dict
def save_accounts_to_mongodb(accounts_dict):
try:
if mongo_client:
print("💾 Saving accounts to MongoDB...")
saved_count = 0
for acc_id, acc in accounts_dict.items():
acc_copy = acc.copy()
acc_copy.pop("session", None)
acc_copy.pop("csrf", None)
acc_copy.pop("sms_cache", None)
acc_copy.pop("sms_counter", None)
acc_copy.pop("range_counter", None)
acc_copy.pop("last_cleanup", None)
# Batasi logs
if "otp_logs" in acc_copy and len(acc_copy["otp_logs"]) > 100:
acc_copy["otp_logs"] = acc_copy["otp_logs"][:100]
result = accounts_collection.update_one(
{"_id": acc_id},
{"$set": acc_copy},
upsert=True
)
if result.upserted_id or result.modified_count > 0:
saved_count += 1
print(f"💾 Saved {saved_count} accounts to MongoDB")
except Exception as e:
print(f"❌ Error save to MongoDB: {e}")
def load_accounts_from_file():
if os.path.exists(ACCOUNTS_FILE):
try:
with open(ACCOUNTS_FILE, 'r') as f:
data = json.load(f)
for acc_id in data:
data[acc_id]["session"] = None
data[acc_id]["csrf"] = None
data[acc_id]["status"] = False
if "otp_logs" not in data[acc_id]:
data[acc_id]["otp_logs"] = []
data[acc_id]["sent_cache"] = []
data[acc_id]["sms_cache"] = {}
data[acc_id]["sms_counter"] = {}
data[acc_id]["range_counter"] = {}
data[acc_id]["last_cleanup"] = time.time()
print(f"📊 Loaded {len(data)} accounts from file")
return data
except:
return {}
return {}
def save_accounts_to_file(accounts_dict):
try:
accounts_to_save = {}
for acc_id, acc in accounts_dict.items():
acc_copy = acc.copy()
acc_copy.pop("session", None)
acc_copy.pop("csrf", None)
if "otp_logs" in acc_copy and len(acc_copy["otp_logs"]) > 100:
acc_copy["otp_logs"] = acc_copy["otp_logs"][:100]
acc_copy.pop("sms_cache", None)
acc_copy.pop("sms_counter", None)
acc_copy.pop("range_counter", None)
acc_copy.pop("last_cleanup", None)
accounts_to_save[acc_id] = acc_copy
with open(ACCOUNTS_FILE, 'w') as f:
json.dump(accounts_to_save, f, indent=2)
print(f"💾 Accounts saved to file")
except Exception as e:
print(f"❌ Error save to file: {e}")
# Load accounts
if mongo_client:
accounts = load_accounts_from_mongodb()
else:
accounts = load_accounts_from_file()
app = Flask('')
app.secret_key = "fourstore-multi-account-secret"
sse_clients = []
global_otp_logs = []
def get_utc_time():
return datetime.now(timezone.utc)
def get_wib_time():
return datetime.now(timezone.utc) + timedelta(hours=7)
def get_wib_time_str():
return get_wib_time().strftime("%H:%M:%S")
def get_search_date():
now_utc = datetime.now(timezone.utc)
return now_utc.strftime("%Y-%m-%d")
def login_account(account_id, username, password, bot_token, chat_id):
try:
masked = mask_email(username)
print(f"\n{'='*60}")
print(f"🔐 PROSES LOGIN UNTUK: {masked} (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()
if mongo_client:
save_accounts_to_mongodb(accounts)
else:
save_accounts_to_file(accounts)
return True, "Login berhasil"
else:
return False, "Login gagal"
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)
print(f"✅ OTP terkirim ke chat {account['chat_id']}")
return True
except Exception as e:
print(f"❌ Gagal kirim Telegram: {e}")
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"
if 'shopee' in s: return "SHOPEE"
if 'tokopedia' in s: return "TOKOPEDIA"
if 'grab' in s: return "GRAB"
if 'gojek' in s or 'go-jek' in s: return "GOJEK"
return raw.upper()
def extract_otp(text):
if not text: return None
m = re.search(r"\b(\d{6})\b", text)
if m:
return m.group(0)
m = re.search(r"\b(\d{4,5})\b", text)
if m:
return m.group(0)
m = re.search(r"\b(\d{3}[- ]?\d{3})\b", text)
if m:
return m.group(0).replace("-", "").replace(" ", "")
digits = re.findall(r'\d+', text)
for d in digits:
if 4 <= len(d) <= 6:
return d
return None
def generate_otp_id(account_id, rng, number, otp, service):
return f"{account_id}-{rng}-{number}-{otp}-{service}"
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 Exception as e:
print(f"Error get_ranges: {e}")
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 Exception as e:
print(f"Error get_numbers: {e}")
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 Exception as e:
print(f"Error get_sms: {e}")
return []
def is_otp_sent(account_id, otp_id):
account = accounts.get(account_id)
if not account:
return True
return otp_id in account.get("sent_cache", [])
def mark_otp_sent(account_id, otp_id):
account = accounts.get(account_id)
if not account:
return
if "sent_cache" not in account:
account["sent_cache"] = []
account["sent_cache"].append(otp_id)
if len(account["sent_cache"]) > 1000:
account["sent_cache"] = account["sent_cache"][-1000:]
def add_otp_log(account_id, country, number, service, otp, sms, otp_id):
account = accounts.get(account_id)
if not account:
return
wib = get_wib_time()
wib_str = wib.strftime("%Y-%m-%d %H:%M:%S WIB")
time_only = wib.strftime("%H:%M:%S")
log_entry = {
"time": time_only,
"time_full": wib_str,
"timestamp": time.time(),
"country": country,
"number": number,
"service": service,
"otp": otp,
"sms": sms[:150] + "..." if len(sms) > 150 else sms,
"account_id": account_id,
"account_username": mask_email(account.get("username", "Unknown")),
"otp_id": otp_id
}
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]
global_otp_logs.insert(0, log_entry)
if len(global_otp_logs) > 500:
global_otp_logs[:] = global_otp_logs[:500]
broadcast_sse(log_entry)
if mongo_client:
save_accounts_to_mongodb(accounts)
else:
save_accounts_to_file(accounts)
print(f"✅ Log ditambahkan: {service} - {otp} - {time_only}")
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 = global_otp_logs.copy()
search_query = request.args.get('q', '').lower()
filter_service = request.args.get('service', '')
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() or
search_query in log.get('account_username', '').lower()]
if filter_service:
all_logs = [log for log in all_logs if log.get('service') == filter_service]
all_services = list(set([log.get('service') for log in all_logs if log.get('service')]))
total_otp = len(global_otp_logs)
today_otp = len([l for l in global_otp_logs if l.get('time_full', '').startswith(get_wib_time().strftime("%Y-%m-%d"))])
service_stats = {}
for log in global_otp_logs[:50]:
service = log.get('service', 'UNKNOWN')
service_stats[service] = service_stats.get(service, 0) + 1
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); }}
.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; }}
.stats-mini {{ display: flex; gap: 12px; flex-wrap: wrap; margin: 10px 0; }}
.service-tag {{ background: #1a1f2c; padding: 6px 14px; border-radius: 100px; font-size: 12px; border: 1px solid #2d3540; }}
.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 {{ padding: 6px 14px; border-radius: 100px; font-size: 12px; font-weight: 600; 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: #1877F220; color: #1877F2; border: 1px solid #1877F240; }}
.instagram {{ background: #E4405F20; color: #E4405F; border: 1px solid #E4405F40; }}
.tiktok {{ background: #00000020; color: #FFFFFF; border: 1px solid #FFFFFF40; }}
.shopee {{ background: #EE4D2D20; color: #EE4D2D; border: 1px solid #EE4D2D40; }}
.tokopedia {{ background: #42B54920; color: #42B549; border: 1px solid #42B54940; }}
.grab {{ background: #00B14F20; color: #00B14F; border: 1px solid #00B14F40; }}
.gojek {{ background: #00880A20; color: #00880A; border: 1px solid #00880A40; }}
.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; }}
.account-badge {{ background: #2d3a4a; padding: 4px 12px; border-radius: 100px; font-size: 12px; }}
@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} · {get_wib_time().strftime('%d %B %Y')}</p>
</div>
<div class="status-badge">● ONLINE · 24 JAM</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>
<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 & Login Otomatis</button>
</form>
</div>
<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 country, nomor, OTP, akun..." 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>
<a href="/" class="clear-btn">✕ Reset</a>
<span class="result-count">📊 {len(all_logs)} hasil</span>
</div>
<div class="stats-mini">
{''.join([f'<span class="service-tag">{service}: {count}</span>' for service, count in sorted(service_stats.items())][:8])}
</div>
<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 · 24 JAM</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 copyOTP(otp) {{
navigator.clipboard.writeText(otp).then(() => {{
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = '✅ OTP copied: ' + otp;
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);
}}
setInterval(() => {{
location.reload();
}}, 30000);
connectSSE();
setInterval(updateTime, 1000);
</script>
</body>
</html>
"""
return html
def generate_otp_rows(logs, search_query):
if not logs:
return '<tr><td colspan="7" class="empty-state">📭 Belum ada OTP · Menunggu OTP masuk...</td></tr>'
rows = ""
for log in logs[:50]:
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')
time_full = log.get('time_full', '')
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(' ', '')
time_tooltip = f' title="{time_full}"'
rows += f'''
<tr class="new-row">
<td{time_tooltip} 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:350px; 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', '')
masked = mask_email(username)
print(f"\n➕ TAMBAH AKUN BARU: {masked} (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()
}
if mongo_client:
save_accounts_to_mongodb(accounts)
else:
save_accounts_to_file(accounts)
print(f"✅ Akun ditambahkan: {masked}")
print(f"🔄 Mencoba login otomatis untuk {masked}...")
success, msg = login_account(
account_id,
username,
password,
bot_token,
chat_id
)
if success:
print(f"✅✅✅ LOGIN OTOMATIS BERHASIL! Memulai thread scraper...")
thread = Thread(target=run_account_scraper, args=(account_id,), daemon=True)
thread.start()
print(f"✅ Thread scraper dimulai untuk {masked}")
else:
print(f"❌❌❌ LOGIN OTOMATIS GAGAL: {msg}")
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")
@app.route('/api/logs')
def api_logs():
return json.dumps(global_otp_logs[:100])
@app.route('/api/stats')
def api_stats():
return json.dumps({
"total_accounts": len(accounts),
"online_accounts": sum(1 for a in accounts.values() if a.get('status')),
"total_otp": len(global_otp_logs),
"mongo_connected": mongo_client is not None,
"mongo_docs": accounts_collection.count_documents({}) if mongo_client else 0
})
def run_account_scraper(account_id):
account = accounts.get(account_id)
if not account:
return
username = account['username']
masked = mask_email(username)
print(f"\n🚀🚀🚀 STARTING SCRAPER FOR: {masked} 🚀🚀🚀")
loop_count = 0
while account.get("status"):
loop_count += 1
try:
print(f"\n{'='*60}")
print(f"🔄 [{masked}] LOOP #{loop_count} - {get_wib_time_str()}")
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"🧹 [{masked}] Cache cleared")
ranges_data = get_ranges_with_count(account_id)
print(f"📊 [{masked}] 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} ({masked})")
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")
all_sms = get_sms_fast(account_id, rng, num)
print(f" 📨 Total SMS ditemukan: {len(all_sms)}")
for i in range(prev_num_count, len(all_sms)):
service, sms, otp = all_sms[i]
if otp:
otp_id = generate_otp_id(account_id, rng, num, otp, service)
if not is_otp_sent(account_id, otp_id):
masked_num = mask_number(num)
msg = f"🔔 *NEW OTP*\n🌍 {country}\n📞 `{masked_num}`\n💬 {service}\n🔐 `{otp}`\n\n{sms[:300]}"
print(f" 📤 Mengirim OTP {otp} ke Telegram...")
if tg_send(account_id, msg):
mark_otp_sent(account_id, otp_id)
add_otp_log(account_id, country, masked_num, service, otp, sms, otp_id)
print(f" ✅ OTP: {otp} - {service} TERKIRIM! (ID: {otp_id})")
else:
print(f" ❌ Gagal mengirim OTP {otp}")
else:
print(f" ⏭️ OTP {otp} sudah pernah dikirim sebelumnya (ID: {otp_id})")
account["sms_counter"][key] = num_count
time.sleep(0.5)
else:
print(f" ⏭️ Range {clean_country(rng)} tidak berubah (count: {current_count})")
print(f"\n⏳ [{masked}] Tidur 2 detik...")
time.sleep(2)
except Exception as e:
print(f"❌ ERROR in scraper for {masked}: {str(e)}")
time.sleep(5)
print(f"\n🛑🛑🛑 SCRAPER STOPPED FOR: {masked} 🛑🛑🛑")
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" + "="*70)
print(" 🔥 OTP MULTI ACCOUNT - FOURSTORE 🔥")
print("="*70)
# CEK MONGODB
if mongo_client:
print(" ✅ MongoDB: TERHUBUNG")
try:
count = accounts_collection.count_documents({})
print(f" 📊 Data di MongoDB: {count} akun")
# Coba baca sample
sample = accounts_collection.find_one()
if sample:
print(f" 📝 Sample akun: {mask_email(sample.get('username', 'Unknown'))}")
log_count = len(sample.get('otp_logs', []))
print(f" 📝 OTP logs: {log_count}")
except Exception as e:
print(f" ❌ Error cek MongoDB: {e}")
else:
print(" ⚠️ MongoDB: TIDAK TERHUBUNG (pakai file)")
print(f" ⚡ PORT: 7860")
print(f" 🌐 DOMAIN: {CUSTOM_DOMAIN}")
print(" 📋 LOGGING: FULL DETAIL")
print(" 🔒 EMAIL SENSOR: AKTIF")
print(" 🤖 AUTO LOGIN: AKTIF")
print(" 📱 TELEGRAM: HANYA KIRIM OTP")
print(" 🛡️ ANTI DUPLIKAT: AKTIF")
print(" 🌍 SERVER UTC: AKTIF")
print(" ⏰ 24 JAM MONITORING: AKTIF")
print("="*70 + "\n")
# Muat ulang log dari akun yang ada
print("📥 Memuat ulang logs dari akun tersimpan...")
for acc_id, acc in accounts.items():
if "otp_logs" in acc and acc["otp_logs"]:
for log in acc["otp_logs"]:
global_otp_logs.append(log)
global_otp_logs.sort(key=lambda x: x.get('timestamp', 0), reverse=True)
print(f"📊 Total logs dimuat: {len(global_otp_logs)}")
# Auto login untuk akun yang sudah login sebelumnya
for acc_id, acc in accounts.items():
if acc.get("status"):
masked = mask_email(acc['username'])
print(f"🔄 Auto-login untuk {masked}...")
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} online")
else:
print(f"❌ {masked} offline: {msg}")
acc["status"] = False
if mongo_client:
save_accounts_to_mongodb(accounts)
else:
save_accounts_to_file(accounts)
print("\n" + "="*70)
print("✅ BOT SIAP! Dashboard: https://fourstore-otp.hf.space")
print("🌍 SERVER MENGGUNAKAN UTC - SEARCH DATE: UTC HARI INI")
print("⏰ 24 JAM AKTIF - SEMUA OTP AKAN MUNCUL DI WEB")
print("="*70 + "\n")
while True:
time.sleep(300)
if mongo_client:
save_accounts_to_mongodb(accounts)
# Cek status MongoDB
try:
count = accounts_collection.count_documents({})
print(f"💾 Auto-save: {count} akun tersimpan - {get_wib_time_str()}")
except:
print(f"💾 Auto-save - {get_wib_time_str()}")
else:
save_accounts_to_file(accounts)
print(f"💾 Auto-save data - {get_wib_time_str()}")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n🛑 BOT STOPPED")
if mongo_client:
save_accounts_to_mongodb(accounts)
else:
save_accounts_to_file(accounts)