Update app.py
Browse files
app.py
CHANGED
|
@@ -85,8 +85,8 @@ def save_accounts_to_mongodb(accounts_dict):
|
|
| 85 |
acc_copy = acc.copy()
|
| 86 |
acc_copy.pop("session", None)
|
| 87 |
acc_copy.pop("csrf", None)
|
| 88 |
-
acc_copy
|
| 89 |
-
|
| 90 |
acc_copy.pop("sms_cache", None)
|
| 91 |
acc_copy.pop("sms_counter", None)
|
| 92 |
acc_copy.pop("range_counter", None)
|
|
@@ -109,7 +109,8 @@ def load_accounts_from_file():
|
|
| 109 |
data[acc_id]["session"] = None
|
| 110 |
data[acc_id]["csrf"] = None
|
| 111 |
data[acc_id]["status"] = False
|
| 112 |
-
|
|
|
|
| 113 |
data[acc_id]["sent_cache"] = []
|
| 114 |
data[acc_id]["sms_cache"] = {}
|
| 115 |
data[acc_id]["sms_counter"] = {}
|
|
@@ -128,8 +129,8 @@ def save_accounts_to_file(accounts_dict):
|
|
| 128 |
acc_copy = acc.copy()
|
| 129 |
acc_copy.pop("session", None)
|
| 130 |
acc_copy.pop("csrf", None)
|
| 131 |
-
acc_copy
|
| 132 |
-
|
| 133 |
acc_copy.pop("sms_cache", None)
|
| 134 |
acc_copy.pop("sms_counter", None)
|
| 135 |
acc_copy.pop("range_counter", None)
|
|
@@ -151,12 +152,20 @@ app = Flask('')
|
|
| 151 |
app.secret_key = "fourstore-multi-account-secret"
|
| 152 |
sse_clients = []
|
| 153 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
def get_wib_time():
|
| 155 |
return datetime.now(timezone.utc) + timedelta(hours=7)
|
| 156 |
|
|
|
|
|
|
|
|
|
|
| 157 |
def get_search_date():
|
| 158 |
-
|
| 159 |
-
return
|
| 160 |
|
| 161 |
def login_account(account_id, username, password, bot_token, chat_id):
|
| 162 |
try:
|
|
@@ -242,22 +251,32 @@ def map_service(raw):
|
|
| 242 |
if 'instagram' in s or 'ig' in s: return "INSTAGRAM"
|
| 243 |
if 'tiktok' in s: return "TIKTOK"
|
| 244 |
if 'temu' in s: return "TEMU"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
return raw.upper()
|
| 246 |
|
| 247 |
def extract_otp(text):
|
| 248 |
if not text: return None
|
| 249 |
-
m = re.search(r"\b(\d{
|
| 250 |
if m:
|
| 251 |
-
return m.group(0)
|
| 252 |
-
m = re.search(r"\b(\d{4,
|
| 253 |
if m:
|
| 254 |
return m.group(0)
|
|
|
|
|
|
|
|
|
|
| 255 |
digits = re.findall(r'\d+', text)
|
| 256 |
for d in digits:
|
| 257 |
if 4 <= len(d) <= 6:
|
| 258 |
return d
|
| 259 |
return None
|
| 260 |
|
|
|
|
|
|
|
|
|
|
| 261 |
def get_ranges_with_count(account_id):
|
| 262 |
account = accounts.get(account_id)
|
| 263 |
if not account or not account.get("session") or not account.get("csrf"):
|
|
@@ -288,7 +307,8 @@ def get_ranges_with_count(account_id):
|
|
| 288 |
})
|
| 289 |
|
| 290 |
return ranges_data
|
| 291 |
-
except:
|
|
|
|
| 292 |
return []
|
| 293 |
|
| 294 |
def get_numbers_with_count(account_id, rng):
|
|
@@ -356,7 +376,8 @@ def get_numbers_with_count(account_id, rng):
|
|
| 356 |
})
|
| 357 |
|
| 358 |
return numbers_data
|
| 359 |
-
except:
|
|
|
|
| 360 |
return []
|
| 361 |
|
| 362 |
def get_sms_fast(account_id, rng, number):
|
|
@@ -403,24 +424,49 @@ def get_sms_fast(account_id, rng, number):
|
|
| 403 |
|
| 404 |
account["sms_cache"][cache_key] = (time.time(), results)
|
| 405 |
return results
|
| 406 |
-
except:
|
|
|
|
| 407 |
return []
|
| 408 |
|
| 409 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
account = accounts.get(account_id)
|
| 411 |
if not account:
|
| 412 |
return
|
| 413 |
|
| 414 |
wib = get_wib_time()
|
|
|
|
|
|
|
|
|
|
| 415 |
log_entry = {
|
| 416 |
-
"time":
|
|
|
|
|
|
|
| 417 |
"country": country,
|
| 418 |
"number": number,
|
| 419 |
"service": service,
|
| 420 |
"otp": otp,
|
| 421 |
-
"sms": sms[:
|
| 422 |
"account_id": account_id,
|
| 423 |
-
"account_username": mask_email(account.get("username", "Unknown"))
|
|
|
|
| 424 |
}
|
| 425 |
|
| 426 |
if "otp_logs" not in account:
|
|
@@ -430,7 +476,18 @@ def add_otp_log(account_id, country, number, service, otp, sms):
|
|
| 430 |
if len(account["otp_logs"]) > 100:
|
| 431 |
account["otp_logs"] = account["otp_logs"][:100]
|
| 432 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
broadcast_sse(log_entry)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
return log_entry
|
| 435 |
|
| 436 |
def broadcast_sse(data):
|
|
@@ -446,15 +503,7 @@ def broadcast_sse(data):
|
|
| 446 |
|
| 447 |
@app.route('/')
|
| 448 |
def home():
|
| 449 |
-
all_logs =
|
| 450 |
-
for acc_id, acc in accounts.items():
|
| 451 |
-
if acc.get("otp_logs"):
|
| 452 |
-
for log in acc["otp_logs"]:
|
| 453 |
-
log_copy = log.copy()
|
| 454 |
-
log_copy["account_username"] = mask_email(acc.get("username", "Unknown"))
|
| 455 |
-
all_logs.append(log_copy)
|
| 456 |
-
|
| 457 |
-
all_logs = sorted(all_logs, key=lambda x: x.get("time", ""), reverse=True)
|
| 458 |
|
| 459 |
search_query = request.args.get('q', '').lower()
|
| 460 |
filter_service = request.args.get('service', '')
|
|
@@ -464,15 +513,21 @@ def home():
|
|
| 464 |
search_query in log.get('country', '').lower() or
|
| 465 |
search_query in log.get('number', '').lower() or
|
| 466 |
search_query in log.get('otp', '').lower() or
|
| 467 |
-
search_query in log.get('sms', '').lower()
|
|
|
|
| 468 |
|
| 469 |
if filter_service:
|
| 470 |
all_logs = [log for log in all_logs if log.get('service') == filter_service]
|
| 471 |
|
| 472 |
all_services = list(set([log.get('service') for log in all_logs if log.get('service')]))
|
| 473 |
|
| 474 |
-
total_otp =
|
| 475 |
-
today_otp = len(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 476 |
|
| 477 |
html = f"""
|
| 478 |
<!DOCTYPE html>
|
|
@@ -513,19 +568,31 @@ def home():
|
|
| 513 |
.filter-select {{ width: 100%; padding: 14px 20px; background: #1a1f2c; border: 1px solid #2d3540; border-radius: 100px; color: #e4e6eb; font-size: 15px; cursor: pointer; }}
|
| 514 |
.clear-btn {{ padding: 8px 16px; background: #2d3a4a; border: none; border-radius: 100px; color: white; font-size: 13px; cursor: pointer; text-decoration: none; }}
|
| 515 |
|
|
|
|
|
|
|
|
|
|
| 516 |
.otp-section {{ background: #0f131c; border-radius: 24px; padding: 28px; border: 1px solid #2d3540; overflow-x: auto; }}
|
| 517 |
-
table {{ width: 100%; border-collapse: collapse; min-width:
|
| 518 |
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; }}
|
| 519 |
td {{ padding: 16px 12px; border-bottom: 1px solid #262c38; font-size: 14px; }}
|
| 520 |
.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; }}
|
| 521 |
-
.service-badge {{
|
| 522 |
.whatsapp {{ background: #25D36620; color: #25D366; border: 1px solid #25D36640; }}
|
| 523 |
.telegram {{ background: #26A5E420; color: #26A5E4; border: 1px solid #26A5E440; }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
.number {{ font-family: monospace; }}
|
| 525 |
.empty-state {{ text-align: center; padding: 60px; color: #8b949e; }}
|
| 526 |
.highlight {{ background: #00f2fe30; border-radius: 4px; padding: 0 2px; }}
|
| 527 |
.new-row {{ animation: fadeIn 0.3s ease; background: linear-gradient(90deg, #00f2fe10, transparent); }}
|
| 528 |
.toast {{ position: fixed; bottom: 24px; right: 24px; background: #00f2fe; color: #000; padding: 14px 28px; border-radius: 100px; font-weight: 600; z-index: 9999; }}
|
|
|
|
| 529 |
@keyframes fadeIn {{ from {{ opacity: 0; transform: translateY(-10px); }} to {{ opacity: 1; transform: translateY(0); }} }}
|
| 530 |
</style>
|
| 531 |
</head>
|
|
@@ -534,10 +601,10 @@ def home():
|
|
| 534 |
<div class="header">
|
| 535 |
<div class="header-top">
|
| 536 |
<div class="title">
|
| 537 |
-
<h1
|
| 538 |
-
<p>{CUSTOM_DOMAIN}</p>
|
| 539 |
</div>
|
| 540 |
-
<div class="status-badge">● ONLINE</div>
|
| 541 |
</div>
|
| 542 |
<div class="stats-grid">
|
| 543 |
<div class="stat-card"><div class="stat-label">Total Akun</div><div class="stat-value">{len(accounts)}</div></div>
|
|
@@ -562,7 +629,7 @@ def home():
|
|
| 562 |
<div class="search-box">
|
| 563 |
<span class="search-icon">🔍</span>
|
| 564 |
<form action="/" method="get" id="searchForm">
|
| 565 |
-
<input type="text" class="search-input" name="q" placeholder="Cari OTP..." value="{request.args.get('q', '')}">
|
| 566 |
</form>
|
| 567 |
</div>
|
| 568 |
<div class="filter-box">
|
|
@@ -575,8 +642,12 @@ def home():
|
|
| 575 |
<span class="result-count">📊 {len(all_logs)} hasil</span>
|
| 576 |
</div>
|
| 577 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
<div class="otp-section">
|
| 579 |
-
<h3 style="margin-bottom: 20px;">📨 OTP TERBARU <span style="background:#00f2fe20; padding:4px 12px; border-radius:100px; font-size:12px;">LIVE</span></h3>
|
| 580 |
<table>
|
| 581 |
<thead>
|
| 582 |
<tr>
|
|
@@ -609,7 +680,9 @@ def home():
|
|
| 609 |
}}
|
| 610 |
}} catch(err) {{}}
|
| 611 |
}};
|
| 612 |
-
eventSource.onerror = function() {{
|
|
|
|
|
|
|
| 613 |
}}
|
| 614 |
|
| 615 |
function updateFilter(key, value) {{
|
|
@@ -626,7 +699,7 @@ def home():
|
|
| 626 |
navigator.clipboard.writeText(otp).then(() => {{
|
| 627 |
const toast = document.createElement('div');
|
| 628 |
toast.className = 'toast';
|
| 629 |
-
toast.textContent = '✅ OTP copied
|
| 630 |
document.body.appendChild(toast);
|
| 631 |
setTimeout(() => toast.remove(), 2000);
|
| 632 |
}});
|
|
@@ -639,6 +712,10 @@ def home():
|
|
| 639 |
if (wibEl) wibEl.textContent = now.toISOString().substr(11, 8);
|
| 640 |
}}
|
| 641 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 642 |
connectSSE();
|
| 643 |
setInterval(updateTime, 1000);
|
| 644 |
</script>
|
|
@@ -649,16 +726,17 @@ def home():
|
|
| 649 |
|
| 650 |
def generate_otp_rows(logs, search_query):
|
| 651 |
if not logs:
|
| 652 |
-
return '<tr><td colspan="7" class="empty-state"
|
| 653 |
|
| 654 |
rows = ""
|
| 655 |
-
for log in logs[:
|
| 656 |
country = log.get('country', '')
|
| 657 |
number = log.get('number', '')
|
| 658 |
otp = log.get('otp', '')
|
| 659 |
sms = log.get('sms', '')
|
| 660 |
service = log.get('service', 'UNKNOWN')
|
| 661 |
account = log.get('account_username', 'Unknown')
|
|
|
|
| 662 |
|
| 663 |
if search_query:
|
| 664 |
country = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', country, flags=re.I)
|
|
@@ -666,15 +744,18 @@ def generate_otp_rows(logs, search_query):
|
|
| 666 |
otp = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', otp, flags=re.I)
|
| 667 |
|
| 668 |
service_class = service.lower().replace(' ', '')
|
|
|
|
|
|
|
|
|
|
| 669 |
rows += f'''
|
| 670 |
<tr class="new-row">
|
| 671 |
-
<td style="color:#00f2fe;">{log.get('time', '')}</td>
|
| 672 |
<td><span class="account-badge">{account}</span></td>
|
| 673 |
<td>{country}</td>
|
| 674 |
<td><span class="number">{number}</span></td>
|
| 675 |
<td><span class="service-badge {service_class}">{service}</span></td>
|
| 676 |
<td><span class="otp-badge" onclick="copyOTP('{otp}')">{otp}</span></td>
|
| 677 |
-
<td><div style="max-width:
|
| 678 |
</tr>
|
| 679 |
'''
|
| 680 |
return rows
|
|
@@ -746,6 +827,10 @@ def stream():
|
|
| 746 |
sse_clients.remove(q)
|
| 747 |
return Response(generate(), mimetype="text/event-stream")
|
| 748 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 749 |
def run_account_scraper(account_id):
|
| 750 |
account = accounts.get(account_id)
|
| 751 |
if not account:
|
|
@@ -760,7 +845,7 @@ def run_account_scraper(account_id):
|
|
| 760 |
loop_count += 1
|
| 761 |
try:
|
| 762 |
print(f"\n{'='*60}")
|
| 763 |
-
print(f"🔄 [{masked}] LOOP #{loop_count}")
|
| 764 |
print(f"{'='*60}")
|
| 765 |
|
| 766 |
if time.time() - account.get("last_cleanup", 0) > 300:
|
|
@@ -802,25 +887,31 @@ def run_account_scraper(account_id):
|
|
| 802 |
if num_count > prev_num_count:
|
| 803 |
print(f" 📱 Nomor: {mask_number(num)}")
|
| 804 |
print(f" 📨 {prev_num_count} → {num_count} SMS")
|
| 805 |
-
account["sms_counter"][key] = num_count
|
| 806 |
|
| 807 |
all_sms = get_sms_fast(account_id, rng, num)
|
| 808 |
-
|
| 809 |
|
| 810 |
-
for
|
|
|
|
|
|
|
| 811 |
if otp:
|
| 812 |
-
|
| 813 |
-
|
|
|
|
| 814 |
masked_num = mask_number(num)
|
| 815 |
msg = f"🔔 *NEW OTP*\n🌍 {country}\n📞 `{masked_num}`\n💬 {service}\n🔐 `{otp}`\n\n{sms[:300]}"
|
| 816 |
print(f" 📤 Mengirim OTP {otp} ke Telegram...")
|
| 817 |
|
| 818 |
if tg_send(account_id, msg):
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
print(f"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 824 |
|
| 825 |
time.sleep(0.5)
|
| 826 |
else:
|
|
@@ -838,13 +929,13 @@ def run_account_scraper(account_id):
|
|
| 838 |
def run_server():
|
| 839 |
app.run(host='0.0.0.0', port=7860, debug=False, threaded=True)
|
| 840 |
|
| 841 |
-
# Jalankan server Flask
|
| 842 |
Thread(target=run_server, daemon=True).start()
|
| 843 |
|
| 844 |
def main():
|
| 845 |
-
print("\n" + "="*
|
| 846 |
-
print(" 🔥 OTP MULTI ACCOUNT - FOURSTORE")
|
| 847 |
-
print("
|
|
|
|
| 848 |
print(f" 🌐 DOMAIN: {CUSTOM_DOMAIN}")
|
| 849 |
if mongo_client:
|
| 850 |
print(" 📁 Data tersimpan di MongoDB")
|
|
@@ -853,10 +944,20 @@ def main():
|
|
| 853 |
print(" 📋 LOGGING: FULL DETAIL")
|
| 854 |
print(" 🔒 EMAIL SENSOR: AKTIF")
|
| 855 |
print(" 🤖 AUTO LOGIN: AKTIF")
|
| 856 |
-
print(" 📱 TELEGRAM: HANYA KIRIM OTP
|
| 857 |
-
print("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 858 |
|
| 859 |
-
# Auto login untuk akun yang sudah login sebelumnya
|
| 860 |
for acc_id, acc in accounts.items():
|
| 861 |
if acc.get("status"):
|
| 862 |
masked = mask_email(acc['username'])
|
|
@@ -880,10 +981,19 @@ def main():
|
|
| 880 |
else:
|
| 881 |
save_accounts_to_file(accounts)
|
| 882 |
|
| 883 |
-
print("\n
|
|
|
|
|
|
|
|
|
|
|
|
|
| 884 |
|
| 885 |
while True:
|
| 886 |
-
time.sleep(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 887 |
|
| 888 |
if __name__ == "__main__":
|
| 889 |
try:
|
|
|
|
| 85 |
acc_copy = acc.copy()
|
| 86 |
acc_copy.pop("session", None)
|
| 87 |
acc_copy.pop("csrf", None)
|
| 88 |
+
if "otp_logs" in acc_copy and len(acc_copy["otp_logs"]) > 100:
|
| 89 |
+
acc_copy["otp_logs"] = acc_copy["otp_logs"][:100]
|
| 90 |
acc_copy.pop("sms_cache", None)
|
| 91 |
acc_copy.pop("sms_counter", None)
|
| 92 |
acc_copy.pop("range_counter", None)
|
|
|
|
| 109 |
data[acc_id]["session"] = None
|
| 110 |
data[acc_id]["csrf"] = None
|
| 111 |
data[acc_id]["status"] = False
|
| 112 |
+
if "otp_logs" not in data[acc_id]:
|
| 113 |
+
data[acc_id]["otp_logs"] = []
|
| 114 |
data[acc_id]["sent_cache"] = []
|
| 115 |
data[acc_id]["sms_cache"] = {}
|
| 116 |
data[acc_id]["sms_counter"] = {}
|
|
|
|
| 129 |
acc_copy = acc.copy()
|
| 130 |
acc_copy.pop("session", None)
|
| 131 |
acc_copy.pop("csrf", None)
|
| 132 |
+
if "otp_logs" in acc_copy and len(acc_copy["otp_logs"]) > 100:
|
| 133 |
+
acc_copy["otp_logs"] = acc_copy["otp_logs"][:100]
|
| 134 |
acc_copy.pop("sms_cache", None)
|
| 135 |
acc_copy.pop("sms_counter", None)
|
| 136 |
acc_copy.pop("range_counter", None)
|
|
|
|
| 152 |
app.secret_key = "fourstore-multi-account-secret"
|
| 153 |
sse_clients = []
|
| 154 |
|
| 155 |
+
global_otp_logs = []
|
| 156 |
+
|
| 157 |
+
def get_utc_time():
|
| 158 |
+
return datetime.now(timezone.utc)
|
| 159 |
+
|
| 160 |
def get_wib_time():
|
| 161 |
return datetime.now(timezone.utc) + timedelta(hours=7)
|
| 162 |
|
| 163 |
+
def get_wib_time_str():
|
| 164 |
+
return get_wib_time().strftime("%H:%M:%S")
|
| 165 |
+
|
| 166 |
def get_search_date():
|
| 167 |
+
now_utc = datetime.now(timezone.utc)
|
| 168 |
+
return now_utc.strftime("%Y-%m-%d")
|
| 169 |
|
| 170 |
def login_account(account_id, username, password, bot_token, chat_id):
|
| 171 |
try:
|
|
|
|
| 251 |
if 'instagram' in s or 'ig' in s: return "INSTAGRAM"
|
| 252 |
if 'tiktok' in s: return "TIKTOK"
|
| 253 |
if 'temu' in s: return "TEMU"
|
| 254 |
+
if 'shopee' in s: return "SHOPEE"
|
| 255 |
+
if 'tokopedia' in s: return "TOKOPEDIA"
|
| 256 |
+
if 'grab' in s: return "GRAB"
|
| 257 |
+
if 'gojek' in s or 'go-jek' in s: return "GOJEK"
|
| 258 |
return raw.upper()
|
| 259 |
|
| 260 |
def extract_otp(text):
|
| 261 |
if not text: return None
|
| 262 |
+
m = re.search(r"\b(\d{6})\b", text)
|
| 263 |
if m:
|
| 264 |
+
return m.group(0)
|
| 265 |
+
m = re.search(r"\b(\d{4,5})\b", text)
|
| 266 |
if m:
|
| 267 |
return m.group(0)
|
| 268 |
+
m = re.search(r"\b(\d{3}[- ]?\d{3})\b", text)
|
| 269 |
+
if m:
|
| 270 |
+
return m.group(0).replace("-", "").replace(" ", "")
|
| 271 |
digits = re.findall(r'\d+', text)
|
| 272 |
for d in digits:
|
| 273 |
if 4 <= len(d) <= 6:
|
| 274 |
return d
|
| 275 |
return None
|
| 276 |
|
| 277 |
+
def generate_otp_id(account_id, rng, number, otp, service):
|
| 278 |
+
return f"{account_id}-{rng}-{number}-{otp}-{service}"
|
| 279 |
+
|
| 280 |
def get_ranges_with_count(account_id):
|
| 281 |
account = accounts.get(account_id)
|
| 282 |
if not account or not account.get("session") or not account.get("csrf"):
|
|
|
|
| 307 |
})
|
| 308 |
|
| 309 |
return ranges_data
|
| 310 |
+
except Exception as e:
|
| 311 |
+
print(f"Error get_ranges: {e}")
|
| 312 |
return []
|
| 313 |
|
| 314 |
def get_numbers_with_count(account_id, rng):
|
|
|
|
| 376 |
})
|
| 377 |
|
| 378 |
return numbers_data
|
| 379 |
+
except Exception as e:
|
| 380 |
+
print(f"Error get_numbers: {e}")
|
| 381 |
return []
|
| 382 |
|
| 383 |
def get_sms_fast(account_id, rng, number):
|
|
|
|
| 424 |
|
| 425 |
account["sms_cache"][cache_key] = (time.time(), results)
|
| 426 |
return results
|
| 427 |
+
except Exception as e:
|
| 428 |
+
print(f"Error get_sms: {e}")
|
| 429 |
return []
|
| 430 |
|
| 431 |
+
def is_otp_sent(account_id, otp_id):
|
| 432 |
+
account = accounts.get(account_id)
|
| 433 |
+
if not account:
|
| 434 |
+
return True
|
| 435 |
+
return otp_id in account.get("sent_cache", [])
|
| 436 |
+
|
| 437 |
+
def mark_otp_sent(account_id, otp_id):
|
| 438 |
+
account = accounts.get(account_id)
|
| 439 |
+
if not account:
|
| 440 |
+
return
|
| 441 |
+
|
| 442 |
+
if "sent_cache" not in account:
|
| 443 |
+
account["sent_cache"] = []
|
| 444 |
+
|
| 445 |
+
account["sent_cache"].append(otp_id)
|
| 446 |
+
if len(account["sent_cache"]) > 1000:
|
| 447 |
+
account["sent_cache"] = account["sent_cache"][-1000:]
|
| 448 |
+
|
| 449 |
+
def add_otp_log(account_id, country, number, service, otp, sms, otp_id):
|
| 450 |
account = accounts.get(account_id)
|
| 451 |
if not account:
|
| 452 |
return
|
| 453 |
|
| 454 |
wib = get_wib_time()
|
| 455 |
+
wib_str = wib.strftime("%Y-%m-%d %H:%M:%S WIB")
|
| 456 |
+
time_only = wib.strftime("%H:%M:%S")
|
| 457 |
+
|
| 458 |
log_entry = {
|
| 459 |
+
"time": time_only,
|
| 460 |
+
"time_full": wib_str,
|
| 461 |
+
"timestamp": time.time(),
|
| 462 |
"country": country,
|
| 463 |
"number": number,
|
| 464 |
"service": service,
|
| 465 |
"otp": otp,
|
| 466 |
+
"sms": sms[:150] + "..." if len(sms) > 150 else sms,
|
| 467 |
"account_id": account_id,
|
| 468 |
+
"account_username": mask_email(account.get("username", "Unknown")),
|
| 469 |
+
"otp_id": otp_id
|
| 470 |
}
|
| 471 |
|
| 472 |
if "otp_logs" not in account:
|
|
|
|
| 476 |
if len(account["otp_logs"]) > 100:
|
| 477 |
account["otp_logs"] = account["otp_logs"][:100]
|
| 478 |
|
| 479 |
+
global_otp_logs.insert(0, log_entry)
|
| 480 |
+
if len(global_otp_logs) > 500:
|
| 481 |
+
global_otp_logs[:] = global_otp_logs[:500]
|
| 482 |
+
|
| 483 |
broadcast_sse(log_entry)
|
| 484 |
+
|
| 485 |
+
if mongo_client:
|
| 486 |
+
save_accounts_to_mongodb(accounts)
|
| 487 |
+
else:
|
| 488 |
+
save_accounts_to_file(accounts)
|
| 489 |
+
|
| 490 |
+
print(f"✅ Log ditambahkan: {service} - {otp} - {time_only}")
|
| 491 |
return log_entry
|
| 492 |
|
| 493 |
def broadcast_sse(data):
|
|
|
|
| 503 |
|
| 504 |
@app.route('/')
|
| 505 |
def home():
|
| 506 |
+
all_logs = global_otp_logs.copy()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
|
| 508 |
search_query = request.args.get('q', '').lower()
|
| 509 |
filter_service = request.args.get('service', '')
|
|
|
|
| 513 |
search_query in log.get('country', '').lower() or
|
| 514 |
search_query in log.get('number', '').lower() or
|
| 515 |
search_query in log.get('otp', '').lower() or
|
| 516 |
+
search_query in log.get('sms', '').lower() or
|
| 517 |
+
search_query in log.get('account_username', '').lower()]
|
| 518 |
|
| 519 |
if filter_service:
|
| 520 |
all_logs = [log for log in all_logs if log.get('service') == filter_service]
|
| 521 |
|
| 522 |
all_services = list(set([log.get('service') for log in all_logs if log.get('service')]))
|
| 523 |
|
| 524 |
+
total_otp = len(global_otp_logs)
|
| 525 |
+
today_otp = len([l for l in global_otp_logs if l.get('time_full', '').startswith(get_wib_time().strftime("%Y-%m-%d"))])
|
| 526 |
+
|
| 527 |
+
service_stats = {}
|
| 528 |
+
for log in global_otp_logs[:50]:
|
| 529 |
+
service = log.get('service', 'UNKNOWN')
|
| 530 |
+
service_stats[service] = service_stats.get(service, 0) + 1
|
| 531 |
|
| 532 |
html = f"""
|
| 533 |
<!DOCTYPE html>
|
|
|
|
| 568 |
.filter-select {{ width: 100%; padding: 14px 20px; background: #1a1f2c; border: 1px solid #2d3540; border-radius: 100px; color: #e4e6eb; font-size: 15px; cursor: pointer; }}
|
| 569 |
.clear-btn {{ padding: 8px 16px; background: #2d3a4a; border: none; border-radius: 100px; color: white; font-size: 13px; cursor: pointer; text-decoration: none; }}
|
| 570 |
|
| 571 |
+
.stats-mini {{ display: flex; gap: 12px; flex-wrap: wrap; margin: 10px 0; }}
|
| 572 |
+
.service-tag {{ background: #1a1f2c; padding: 6px 14px; border-radius: 100px; font-size: 12px; border: 1px solid #2d3540; }}
|
| 573 |
+
|
| 574 |
.otp-section {{ background: #0f131c; border-radius: 24px; padding: 28px; border: 1px solid #2d3540; overflow-x: auto; }}
|
| 575 |
+
table {{ width: 100%; border-collapse: collapse; min-width: 1100px; }}
|
| 576 |
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; }}
|
| 577 |
td {{ padding: 16px 12px; border-bottom: 1px solid #262c38; font-size: 14px; }}
|
| 578 |
.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; }}
|
| 579 |
+
.service-badge {{ padding: 6px 14px; border-radius: 100px; font-size: 12px; font-weight: 600; display: inline-block; }}
|
| 580 |
.whatsapp {{ background: #25D36620; color: #25D366; border: 1px solid #25D36640; }}
|
| 581 |
.telegram {{ background: #26A5E420; color: #26A5E4; border: 1px solid #26A5E440; }}
|
| 582 |
+
.google {{ background: #4285F420; color: #4285F4; border: 1px solid #4285F440; }}
|
| 583 |
+
.facebook {{ background: #1877F220; color: #1877F2; border: 1px solid #1877F240; }}
|
| 584 |
+
.instagram {{ background: #E4405F20; color: #E4405F; border: 1px solid #E4405F40; }}
|
| 585 |
+
.tiktok {{ background: #00000020; color: #FFFFFF; border: 1px solid #FFFFFF40; }}
|
| 586 |
+
.shopee {{ background: #EE4D2D20; color: #EE4D2D; border: 1px solid #EE4D2D40; }}
|
| 587 |
+
.tokopedia {{ background: #42B54920; color: #42B549; border: 1px solid #42B54940; }}
|
| 588 |
+
.grab {{ background: #00B14F20; color: #00B14F; border: 1px solid #00B14F40; }}
|
| 589 |
+
.gojek {{ background: #00880A20; color: #00880A; border: 1px solid #00880A40; }}
|
| 590 |
.number {{ font-family: monospace; }}
|
| 591 |
.empty-state {{ text-align: center; padding: 60px; color: #8b949e; }}
|
| 592 |
.highlight {{ background: #00f2fe30; border-radius: 4px; padding: 0 2px; }}
|
| 593 |
.new-row {{ animation: fadeIn 0.3s ease; background: linear-gradient(90deg, #00f2fe10, transparent); }}
|
| 594 |
.toast {{ position: fixed; bottom: 24px; right: 24px; background: #00f2fe; color: #000; padding: 14px 28px; border-radius: 100px; font-weight: 600; z-index: 9999; }}
|
| 595 |
+
.account-badge {{ background: #2d3a4a; padding: 4px 12px; border-radius: 100px; font-size: 12px; }}
|
| 596 |
@keyframes fadeIn {{ from {{ opacity: 0; transform: translateY(-10px); }} to {{ opacity: 1; transform: translateY(0); }} }}
|
| 597 |
</style>
|
| 598 |
</head>
|
|
|
|
| 601 |
<div class="header">
|
| 602 |
<div class="header-top">
|
| 603 |
<div class="title">
|
| 604 |
+
<h1>📱 OTP MULTI ACCOUNT · FOURSTORE</h1>
|
| 605 |
+
<p>{CUSTOM_DOMAIN} · {get_wib_time().strftime('%d %B %Y')}</p>
|
| 606 |
</div>
|
| 607 |
+
<div class="status-badge">● ONLINE · 24 JAM</div>
|
| 608 |
</div>
|
| 609 |
<div class="stats-grid">
|
| 610 |
<div class="stat-card"><div class="stat-label">Total Akun</div><div class="stat-value">{len(accounts)}</div></div>
|
|
|
|
| 629 |
<div class="search-box">
|
| 630 |
<span class="search-icon">🔍</span>
|
| 631 |
<form action="/" method="get" id="searchForm">
|
| 632 |
+
<input type="text" class="search-input" name="q" placeholder="Cari country, nomor, OTP, akun..." value="{request.args.get('q', '')}">
|
| 633 |
</form>
|
| 634 |
</div>
|
| 635 |
<div class="filter-box">
|
|
|
|
| 642 |
<span class="result-count">📊 {len(all_logs)} hasil</span>
|
| 643 |
</div>
|
| 644 |
|
| 645 |
+
<div class="stats-mini">
|
| 646 |
+
{''.join([f'<span class="service-tag">{service}: {count}</span>' for service, count in sorted(service_stats.items())][:8])}
|
| 647 |
+
</div>
|
| 648 |
+
|
| 649 |
<div class="otp-section">
|
| 650 |
+
<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>
|
| 651 |
<table>
|
| 652 |
<thead>
|
| 653 |
<tr>
|
|
|
|
| 680 |
}}
|
| 681 |
}} catch(err) {{}}
|
| 682 |
}};
|
| 683 |
+
eventSource.onerror = function() {{
|
| 684 |
+
setTimeout(connectSSE, 3000);
|
| 685 |
+
}};
|
| 686 |
}}
|
| 687 |
|
| 688 |
function updateFilter(key, value) {{
|
|
|
|
| 699 |
navigator.clipboard.writeText(otp).then(() => {{
|
| 700 |
const toast = document.createElement('div');
|
| 701 |
toast.className = 'toast';
|
| 702 |
+
toast.textContent = '✅ OTP copied: ' + otp;
|
| 703 |
document.body.appendChild(toast);
|
| 704 |
setTimeout(() => toast.remove(), 2000);
|
| 705 |
}});
|
|
|
|
| 712 |
if (wibEl) wibEl.textContent = now.toISOString().substr(11, 8);
|
| 713 |
}}
|
| 714 |
|
| 715 |
+
setInterval(() => {{
|
| 716 |
+
location.reload();
|
| 717 |
+
}}, 30000);
|
| 718 |
+
|
| 719 |
connectSSE();
|
| 720 |
setInterval(updateTime, 1000);
|
| 721 |
</script>
|
|
|
|
| 726 |
|
| 727 |
def generate_otp_rows(logs, search_query):
|
| 728 |
if not logs:
|
| 729 |
+
return '<tr><td colspan="7" class="empty-state">📭 Belum ada OTP · Menunggu OTP masuk...</td></tr>'
|
| 730 |
|
| 731 |
rows = ""
|
| 732 |
+
for log in logs[:50]:
|
| 733 |
country = log.get('country', '')
|
| 734 |
number = log.get('number', '')
|
| 735 |
otp = log.get('otp', '')
|
| 736 |
sms = log.get('sms', '')
|
| 737 |
service = log.get('service', 'UNKNOWN')
|
| 738 |
account = log.get('account_username', 'Unknown')
|
| 739 |
+
time_full = log.get('time_full', '')
|
| 740 |
|
| 741 |
if search_query:
|
| 742 |
country = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', country, flags=re.I)
|
|
|
|
| 744 |
otp = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', otp, flags=re.I)
|
| 745 |
|
| 746 |
service_class = service.lower().replace(' ', '')
|
| 747 |
+
|
| 748 |
+
time_tooltip = f' title="{time_full}"'
|
| 749 |
+
|
| 750 |
rows += f'''
|
| 751 |
<tr class="new-row">
|
| 752 |
+
<td{time_tooltip} style="color:#00f2fe;">{log.get('time', '')}</td>
|
| 753 |
<td><span class="account-badge">{account}</span></td>
|
| 754 |
<td>{country}</td>
|
| 755 |
<td><span class="number">{number}</span></td>
|
| 756 |
<td><span class="service-badge {service_class}">{service}</span></td>
|
| 757 |
<td><span class="otp-badge" onclick="copyOTP('{otp}')">{otp}</span></td>
|
| 758 |
+
<td><div style="max-width:350px; overflow:hidden; text-overflow:ellipsis;" title="{log.get('sms', '')}">{log.get('sms', '')}</div></td>
|
| 759 |
</tr>
|
| 760 |
'''
|
| 761 |
return rows
|
|
|
|
| 827 |
sse_clients.remove(q)
|
| 828 |
return Response(generate(), mimetype="text/event-stream")
|
| 829 |
|
| 830 |
+
@app.route('/api/logs')
|
| 831 |
+
def api_logs():
|
| 832 |
+
return json.dumps(global_otp_logs[:100])
|
| 833 |
+
|
| 834 |
def run_account_scraper(account_id):
|
| 835 |
account = accounts.get(account_id)
|
| 836 |
if not account:
|
|
|
|
| 845 |
loop_count += 1
|
| 846 |
try:
|
| 847 |
print(f"\n{'='*60}")
|
| 848 |
+
print(f"🔄 [{masked}] LOOP #{loop_count} - {get_wib_time_str()}")
|
| 849 |
print(f"{'='*60}")
|
| 850 |
|
| 851 |
if time.time() - account.get("last_cleanup", 0) > 300:
|
|
|
|
| 887 |
if num_count > prev_num_count:
|
| 888 |
print(f" 📱 Nomor: {mask_number(num)}")
|
| 889 |
print(f" 📨 {prev_num_count} → {num_count} SMS")
|
|
|
|
| 890 |
|
| 891 |
all_sms = get_sms_fast(account_id, rng, num)
|
| 892 |
+
print(f" 📨 Total SMS ditemukan: {len(all_sms)}")
|
| 893 |
|
| 894 |
+
for i in range(prev_num_count, len(all_sms)):
|
| 895 |
+
service, sms, otp = all_sms[i]
|
| 896 |
+
|
| 897 |
if otp:
|
| 898 |
+
otp_id = generate_otp_id(account_id, rng, num, otp, service)
|
| 899 |
+
|
| 900 |
+
if not is_otp_sent(account_id, otp_id):
|
| 901 |
masked_num = mask_number(num)
|
| 902 |
msg = f"🔔 *NEW OTP*\n🌍 {country}\n📞 `{masked_num}`\n💬 {service}\n🔐 `{otp}`\n\n{sms[:300]}"
|
| 903 |
print(f" 📤 Mengirim OTP {otp} ke Telegram...")
|
| 904 |
|
| 905 |
if tg_send(account_id, msg):
|
| 906 |
+
mark_otp_sent(account_id, otp_id)
|
| 907 |
+
add_otp_log(account_id, country, masked_num, service, otp, sms, otp_id)
|
| 908 |
+
print(f" ✅ OTP: {otp} - {service} TERKIRIM! (ID: {otp_id})")
|
| 909 |
+
else:
|
| 910 |
+
print(f" ❌ Gagal mengirim OTP {otp}")
|
| 911 |
+
else:
|
| 912 |
+
print(f" ⏭️ OTP {otp} sudah pernah dikirim sebelumnya (ID: {otp_id})")
|
| 913 |
+
|
| 914 |
+
account["sms_counter"][key] = num_count
|
| 915 |
|
| 916 |
time.sleep(0.5)
|
| 917 |
else:
|
|
|
|
| 929 |
def run_server():
|
| 930 |
app.run(host='0.0.0.0', port=7860, debug=False, threaded=True)
|
| 931 |
|
|
|
|
| 932 |
Thread(target=run_server, daemon=True).start()
|
| 933 |
|
| 934 |
def main():
|
| 935 |
+
print("\n" + "="*70)
|
| 936 |
+
print(" 🔥 OTP MULTI ACCOUNT - FOURSTORE 🔥")
|
| 937 |
+
print("="*70)
|
| 938 |
+
print(f" ⚡ PORT: 7860")
|
| 939 |
print(f" 🌐 DOMAIN: {CUSTOM_DOMAIN}")
|
| 940 |
if mongo_client:
|
| 941 |
print(" 📁 Data tersimpan di MongoDB")
|
|
|
|
| 944 |
print(" 📋 LOGGING: FULL DETAIL")
|
| 945 |
print(" 🔒 EMAIL SENSOR: AKTIF")
|
| 946 |
print(" 🤖 AUTO LOGIN: AKTIF")
|
| 947 |
+
print(" 📱 TELEGRAM: HANYA KIRIM OTP")
|
| 948 |
+
print(" 🛡️ ANTI DUPLIKAT: AKTIF")
|
| 949 |
+
print(" 🌍 SERVER UTC: AKTIF")
|
| 950 |
+
print(" ⏰ 24 JAM MONITORING: AKTIF")
|
| 951 |
+
print("="*70 + "\n")
|
| 952 |
+
|
| 953 |
+
for acc_id, acc in accounts.items():
|
| 954 |
+
if "otp_logs" in acc and acc["otp_logs"]:
|
| 955 |
+
for log in acc["otp_logs"]:
|
| 956 |
+
global_otp_logs.append(log)
|
| 957 |
+
|
| 958 |
+
global_otp_logs.sort(key=lambda x: x.get('timestamp', 0), reverse=True)
|
| 959 |
+
print(f"📊 Total logs dimuat: {len(global_otp_logs)}")
|
| 960 |
|
|
|
|
| 961 |
for acc_id, acc in accounts.items():
|
| 962 |
if acc.get("status"):
|
| 963 |
masked = mask_email(acc['username'])
|
|
|
|
| 981 |
else:
|
| 982 |
save_accounts_to_file(accounts)
|
| 983 |
|
| 984 |
+
print("\n" + "="*70)
|
| 985 |
+
print("✅ BOT SIAP! Dashboard: https://fourstore-otp.hf.space")
|
| 986 |
+
print("🌍 SERVER MENGGUNAKAN UTC - SEARCH DATE: UTC HARI INI")
|
| 987 |
+
print("⏰ 24 JAM AKTIF - SEMUA OTP AKAN MUNCUL DI WEB")
|
| 988 |
+
print("="*70 + "\n")
|
| 989 |
|
| 990 |
while True:
|
| 991 |
+
time.sleep(300)
|
| 992 |
+
if mongo_client:
|
| 993 |
+
save_accounts_to_mongodb(accounts)
|
| 994 |
+
else:
|
| 995 |
+
save_accounts_to_file(accounts)
|
| 996 |
+
print(f"💾 Auto-save data - {get_wib_time_str()}")
|
| 997 |
|
| 998 |
if __name__ == "__main__":
|
| 999 |
try:
|