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