Fourstore commited on
Commit
7f89443
·
1 Parent(s): 40a157f
Files changed (1) hide show
  1. app.py +863 -258
app.py CHANGED
@@ -3,12 +3,33 @@ from bs4 import BeautifulSoup
3
  import re
4
  from datetime import datetime, timedelta, timezone
5
  import time
6
- from flask import Flask, Response, request
7
  from threading import Thread
8
  from collections import deque
9
  import json
10
  from queue import Queue
11
  import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  BASE = "http://159.69.3.189"
14
  LOGIN_URL = f"{BASE}/login"
@@ -16,25 +37,81 @@ GET_RANGE_URL = f"{BASE}/portal/sms/received/getsms"
16
  GET_NUMBER_URL = f"{BASE}/portal/sms/received/getsms/number"
17
  GET_SMS_URL = f"{BASE}/portal/sms/received/getsms/number/sms"
18
 
19
- USERNAME = os.environ.get("USERNAME")
20
- PASSWORD = os.environ.get("PASSWORD")
21
- BOT_TOKEN = os.environ.get("BOT_TOKEN")
22
- CHAT_ID = os.environ.get("CHAT_ID")
23
-
24
  TELEGRAM_PROXY_URL = "https://danihitambangetjir.termai.cc/api/proxy"
25
  CUSTOM_DOMAIN = "https://fourstore-otp.hf.space"
26
 
27
- session = httpx.Client(follow_redirects=True, timeout=30.0)
28
- sent_cache = set()
29
- csrf_token = None
30
- LOGIN_SUCCESS = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
- sms_cache = {}
33
- sms_counter = {}
34
- range_counter = {}
35
- otp_logs = deque(maxlen=100)
36
  sse_clients = []
37
 
 
 
 
 
 
 
 
 
 
 
38
  def get_wib_time():
39
  return datetime.now(timezone.utc) + timedelta(hours=7)
40
 
@@ -42,42 +119,110 @@ def get_search_date():
42
  wib = get_wib_time()
43
  return (wib - timedelta(days=1)).strftime("%Y-%m-%d") if wib.hour < 7 else wib.strftime("%Y-%m-%d")
44
 
45
- def login():
46
- global csrf_token, LOGIN_SUCCESS
47
  try:
48
- print("🔐 Login...")
 
 
 
 
 
49
  r = session.get(LOGIN_URL, timeout=30)
 
 
 
 
50
  soup = BeautifulSoup(r.text, "html.parser")
51
  token = soup.find("input", {"name": "_token"})
52
  if not token:
53
- print("Token tidak ditemukan!")
54
- return False
55
  csrf_token = token.get("value")
56
-
57
  r = session.post(LOGIN_URL, data={
58
  "_token": csrf_token,
59
- "email": USERNAME,
60
- "password": PASSWORD
61
  }, timeout=30)
62
-
63
  if "dashboard" in r.text.lower() or "logout" in r.text.lower():
64
- LOGIN_SUCCESS = True
65
- print(" Login BERHASIL!")
66
- return True
 
 
 
 
 
 
 
 
67
  else:
68
- LOGIN_SUCCESS = False
69
- print("❌ Login GAGAL!")
70
- return False
71
  except Exception as e:
72
- LOGIN_SUCCESS = False
73
- print(f"❌ Login Error: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  return False
75
 
76
- def get_ranges_with_count():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  try:
78
  date = get_search_date()
79
- r = session.post(GET_RANGE_URL, data={
80
- "_token": csrf_token,
81
  "from": date,
82
  "to": date
83
  }, timeout=15)
@@ -99,14 +244,18 @@ def get_ranges_with_count():
99
  })
100
 
101
  return ranges_data
102
- except Exception as e:
103
  return []
104
 
105
- def get_numbers_with_count(rng):
 
 
 
 
106
  try:
107
  date = get_search_date()
108
- r = session.post(GET_NUMBER_URL, data={
109
- "_token": csrf_token,
110
  "start": date,
111
  "end": date,
112
  "range": rng
@@ -163,21 +312,25 @@ def get_numbers_with_count(rng):
163
  })
164
 
165
  return numbers_data
166
- except Exception as e:
167
  return []
168
 
169
- def get_sms_fast(rng, number):
 
 
 
 
170
  try:
171
  date = get_search_date()
172
  cache_key = f"{rng}-{number}"
173
 
174
- if cache_key in sms_cache:
175
- timestamp, results = sms_cache[cache_key]
176
  if time.time() - timestamp < 5:
177
  return results
178
 
179
- r = session.post(GET_SMS_URL, data={
180
- "_token": csrf_token,
181
  "start": date,
182
  "end": date,
183
  "Number": number,
@@ -204,64 +357,16 @@ def get_sms_fast(rng, number):
204
  except:
205
  continue
206
 
207
- sms_cache[cache_key] = (time.time(), results)
208
  return results
209
  except:
210
  return []
211
 
212
- def extract_otp(text):
213
- if not text: return None
214
-
215
- m = re.search(r"\b(\d{3}[- ]?\d{3})\b", text)
216
- if m:
217
- return m.group(0).replace("-", "").replace(" ", "")
218
-
219
- m = re.search(r"\b(\d{4,6})\b", text)
220
- if m:
221
- return m.group(0)
222
-
223
- digits = re.findall(r'\d+', text)
224
- for d in digits:
225
- if 4 <= len(d) <= 6:
226
- return d
227
 
228
- return None
229
-
230
- def clean_country(rng):
231
- return re.sub(r"\s*\d+$", "", rng).strip() if rng else "UNKNOWN"
232
-
233
- def mask_number(number):
234
- if not number: return "UNKNOWN"
235
- clean = re.sub(r"[^\d+]", "", number)
236
- if len(clean) <= 6: return clean
237
- return f"{clean[:4]}****{clean[-3:]}"
238
-
239
- def map_service(raw):
240
- if not raw: return "UNKNOWN"
241
- s = raw.lower().strip()
242
- if 'whatsapp' in s: return "WHATSAPP"
243
- if 'telegram' in s: return "TELEGRAM"
244
- if 'google' in s or 'gmail' in s: return "GOOGLE"
245
- if 'facebook' in s or 'fb' in s: return "FACEBOOK"
246
- if 'instagram' in s or 'ig' in s: return "INSTAGRAM"
247
- if 'tiktok' in s: return "TIKTOK"
248
- if 'temu' in s: return "TEMU"
249
- return raw.upper()
250
-
251
- def tg_send(msg):
252
- try:
253
- url = f"{TELEGRAM_PROXY_URL}?url=https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
254
- payload = {
255
- "chat_id": CHAT_ID,
256
- "text": msg,
257
- "parse_mode": "Markdown"
258
- }
259
- httpx.post(url, json=payload, timeout=30)
260
- return True
261
- except:
262
- return False
263
-
264
- def add_otp_log(country, number, service, otp, sms):
265
  wib = get_wib_time()
266
  log_entry = {
267
  "time": wib.strftime("%H:%M:%S"),
@@ -269,9 +374,18 @@ def add_otp_log(country, number, service, otp, sms):
269
  "number": number,
270
  "service": service,
271
  "otp": otp,
272
- "sms": sms[:80] + "..." if len(sms) > 80 else sms
 
 
273
  }
274
- otp_logs.appendleft(log_entry)
 
 
 
 
 
 
 
275
  broadcast_sse(log_entry)
276
  return log_entry
277
 
@@ -286,175 +400,314 @@ def broadcast_sse(data):
286
  for q in dead:
287
  sse_clients.remove(q)
288
 
289
- app = Flask('')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
  @app.route('/')
 
292
  def home():
293
- wib = get_wib_time()
294
- search_date = get_search_date()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  search_query = request.args.get('q', '').lower()
296
  filter_service = request.args.get('service', '')
 
297
 
298
- filtered_logs = list(otp_logs)
299
  if search_query:
300
- filtered_logs = [log for log in filtered_logs if
301
- search_query in log['country'].lower() or
302
- search_query in log['number'].lower() or
303
- search_query in log['otp'].lower() or
304
- search_query in log['sms'].lower()]
305
 
306
  if filter_service:
307
- filtered_logs = [log for log in filtered_logs if log['service'] == filter_service]
308
-
309
- all_services = list(set([log['service'] for log in otp_logs]))
310
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  html = f"""
312
  <!DOCTYPE html>
313
  <html lang="en">
314
  <head>
315
  <meta charset="UTF-8">
316
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
317
- <title>OTP DASHBOARD · SEARCH</title>
318
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
319
  <style>
320
  * {{ margin: 0; padding: 0; box-sizing: border-box; }}
321
  body {{ font-family: 'Inter', sans-serif; background: #0a0c10; color: #e4e6eb; padding: 24px; }}
322
- .container {{ max-width: 1400px; margin: 0 auto; }}
 
 
 
 
 
 
 
 
 
 
323
  .header {{ background: linear-gradient(145deg, #1a1f2c, #0f131c); border-radius: 24px; padding: 28px; margin-bottom: 28px; border: 1px solid #2d3540; }}
324
- .header-top {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }}
325
  .title h1 {{ font-size: 28px; font-weight: 700; background: linear-gradient(135deg, #00f2fe, #4facfe); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }}
326
  .title p {{ color: #8b949e; font-size: 14px; }}
327
  .status-badge {{ padding: 10px 24px; border-radius: 100px; font-weight: 600; font-size: 14px;
328
- background: {'#0a4d3c' if LOGIN_SUCCESS else '#4a2c2c'};
329
- color: {'#a0f0d0' if LOGIN_SUCCESS else '#ffb3b3'};
330
- border: 1px solid {'#1a6e5a' if LOGIN_SUCCESS else '#6e3a3a'}; }}
331
  .stats-grid {{ display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-top: 20px; }}
332
  .stat-card {{ background: #1a1f2c; padding: 20px; border-radius: 20px; border: 1px solid #2d3540; }}
333
  .stat-label {{ color: #8b949e; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }}
334
  .stat-value {{ font-size: 32px; font-weight: 700; color: #00f2fe; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  .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; }}
336
  .search-box {{ flex: 2; min-width: 280px; position: relative; }}
337
  .search-icon {{ position: absolute; left: 16px; top: 14px; color: #8b949e; }}
338
  .search-input {{ width: 100%; padding: 14px 20px 14px 48px; background: #1a1f2c; border: 1px solid #2d3540; border-radius: 100px; color: #e4e6eb; font-size: 15px; }}
339
- .filter-box {{ flex: 1; min-width: 180px; }}
340
  .filter-select {{ width: 100%; padding: 14px 20px; background: #1a1f2c; border: 1px solid #2d3540; border-radius: 100px; color: #e4e6eb; font-size: 15px; cursor: pointer; }}
341
- .clear-btn {{ padding: 14px 28px; background: #2d3a4a; border: none; border-radius: 100px; color: white; font-size: 14px; font-weight: 600; cursor: pointer; text-decoration: none; }}
342
- .result-count {{ color: #8b949e; font-size: 13px; margin-left: 8px; }}
343
- .otp-section {{ background: #0f131c; border-radius: 24px; padding: 28px; border: 1px solid #2d3540; }}
344
- .section-title {{ font-size: 18px; font-weight: 600; margin-bottom: 20px; }}
345
- .section-title span {{ background: #00f2fe20; padding: 6px 12px; border-radius: 100px; font-size: 12px; color: #00f2fe; }}
346
- table {{ width: 100%; border-collapse: collapse; }}
347
  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; }}
348
  td {{ padding: 16px 12px; border-bottom: 1px solid #262c38; font-size: 14px; }}
349
  .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; }}
350
- .otp-badge:hover {{ background: #003d4a; border-color: #00f2fe; transform: scale(1.05); }}
351
  .service-badge {{ background: #2d3a4a; padding: 6px 14px; border-radius: 100px; font-size: 12px; font-weight: 600; display: inline-block; }}
352
- .service-badge.whatsapp {{ background: #25D36620; color: #25D366; border: 1px solid #25D36640; }}
353
- .service-badge.telegram {{ background: #26A5E420; color: #26A5E4; border: 1px solid #26A5E440; }}
354
- .service-badge.google {{ background: #EA433520; color: #EA4335; border: 1px solid #EA433540; }}
355
- .service-badge.facebook {{ background: #4267B220; color: #4267B2; border: 1px solid #4267B240; }}
356
- .service-badge.instagram {{ background: #E4405F20; color: #E4405F; border: 1px solid #E4405F40; }}
357
- .service-badge.tiktok {{ background: #00000020; color: #ffffff; border: 1px solid #ffffff40; }}
358
- .service-badge.temu {{ background: #FF6B6B20; color: #FF6B6B; border: 1px solid #FF6B6B40; }}
359
  .number {{ font-family: monospace; }}
360
  .empty-state {{ text-align: center; padding: 60px; color: #8b949e; }}
361
  .highlight {{ background: #00f2fe30; border-radius: 4px; padding: 0 2px; }}
362
  .new-row {{ animation: fadeIn 0.3s ease; background: linear-gradient(90deg, #00f2fe10, transparent); }}
363
- .toast {{ position: fixed; bottom: 24px; right: 24px; background: #00f2fe; color: #000; padding: 14px 28px; border-radius: 100px; font-weight: 600; animation: slideIn 0.3s ease; z-index: 9999; }}
364
  @keyframes fadeIn {{ from {{ opacity: 0; transform: translateY(-10px); }} to {{ opacity: 1; transform: translateY(0); }} }}
365
- @keyframes slideIn {{ from {{ opacity: 0; transform: translateX(30px); }} to {{ opacity: 1; transform: translateX(0); }} }}
366
  </style>
367
  </head>
368
  <body>
369
  <div class="container">
 
 
 
 
 
 
 
 
 
 
 
370
  <div class="header">
371
  <div class="header-top">
372
  <div class="title">
373
- <h1>OTP MONITOR · SEARCH</h1>
374
  <p>{CUSTOM_DOMAIN}</p>
375
  </div>
376
- <div class="status-badge">
377
- {'● ONLINE' if LOGIN_SUCCESS else '○ OFFLINE'}
378
- </div>
379
  </div>
380
  <div class="stats-grid">
381
- <div class="stat-card"><div class="stat-label">Total OTP</div><div class="stat-value" id="total-otp">{len(sent_cache)}</div></div>
382
- <div class="stat-card"><div class="stat-label">Today</div><div class="stat-value" id="today-otp">{len(otp_logs)}</div></div>
383
- <div class="stat-card"><div class="stat-label">WIB</div><div class="stat-value" id="wib-time">{wib.strftime('%H:%M:%S')}</div></div>
384
- <div class="stat-card"><div class="stat-label">Date</div><div class="stat-value">{search_date}</div></div>
385
  </div>
386
  </div>
387
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  <div class="search-section">
389
  <div class="search-box">
390
  <span class="search-icon">🔍</span>
391
  <form action="/" method="get" id="searchForm">
392
- <input type="text" class="search-input" name="q" placeholder="Cari negara, nomor, OTP, atau SMS..." value="{request.args.get('q', '')}">
393
  </form>
394
  </div>
395
  <div class="filter-box">
396
- <select class="filter-select" name="service" onchange="window.location.href='/?' + (document.getElementById('searchForm').q.value ? 'q=' + document.getElementById('searchForm').q.value + '&' : '') + 'service=' + this.value">
397
  <option value="">📋 Semua Service</option>
398
  {''.join([f'<option value="{s}" {"selected" if filter_service == s else ""}>📱 {s}</option>' for s in sorted(all_services)])}
399
  </select>
400
  </div>
401
- <a href="/" class="clear-btn">✕ Clear</a>
402
- <span class="result-count">📊 {len(filtered_logs)} results</span>
 
 
 
 
 
 
403
  </div>
404
 
405
  <div class="otp-section">
406
- <div class="section-title">📨 OTP TERBARU <span>LIVE</span></div>
407
- <div style="overflow-x: auto;">
408
- <table>
409
- <thead><tr><th>WIB</th><th>Country</th><th>Number</th><th>Service</th><th>OTP</th><th>Message</th></tr></thead>
410
- <tbody id="otp-table-body">{generate_filtered_rows(filtered_logs, search_query)}</tbody>
411
- </table>
412
- </div>
 
 
 
 
 
 
 
 
 
 
413
  </div>
414
  </div>
415
 
416
  <script>
417
  let eventSource;
 
418
  function connectSSE() {{
419
  eventSource = new EventSource('/stream');
420
  eventSource.onmessage = function(e) {{
421
  try {{
422
  const data = JSON.parse(e.data);
423
  if (data.otp) {{
424
- addRow(data);
425
- updateStats();
426
  }}
427
  }} catch(err) {{}}
428
  }};
429
  eventSource.onerror = function() {{ setTimeout(connectSSE, 3000); }};
430
  }}
431
 
432
- function addRow(data) {{
433
- const tbody = document.getElementById('otp-table-body');
434
- if (tbody.children.length === 1 && tbody.children[0].innerHTML.includes('Tidak ada hasil')) tbody.innerHTML = '';
435
- const row = tbody.insertRow(0);
436
- row.className = 'new-row';
437
- const serviceClass = data.service.toLowerCase().replace(' ', '');
438
- row.innerHTML = `<td style="color: #00f2fe;">${{data.time}}</td><td>${{data.country}}</td><td><span class="number">${{data.number}}</span></td><td><span class="service-badge ${{serviceClass}}">${{data.service}}</span></td><td><span class="otp-badge" onclick="copyOTP('${{data.otp}}')">${{data.otp}}</span></td><td><div title="${{data.sms}}">${{data.sms}}</div></td>`;
439
- while (tbody.children.length > 50) tbody.removeChild(tbody.lastChild);
 
 
 
 
 
 
 
 
 
 
440
  }}
441
 
442
  function copyOTP(otp) {{
443
  navigator.clipboard.writeText(otp).then(() => {{
444
- const toast = document.createElement('div'); toast.className = 'toast'; toast.textContent = '✅ OTP copied!';
445
- document.body.appendChild(toast); setTimeout(() => toast.remove(), 2000);
 
 
 
446
  }});
447
  }}
448
 
449
- function updateStats() {{
450
- const totalEl = document.getElementById('total-otp');
451
- const todayEl = document.getElementById('today-otp');
452
- if (totalEl) totalEl.textContent = parseInt(totalEl.textContent || 0) + 1;
453
- if (todayEl) todayEl.textContent = parseInt(todayEl.textContent || 0) + 1;
454
- }}
455
-
456
  function updateTime() {{
457
- const now = new Date(); now.setHours(now.getHours() + 7);
 
458
  const wibEl = document.getElementById('wib-time');
459
  if (wibEl) wibEl.textContent = now.toISOString().substr(11, 8);
460
  }}
@@ -467,36 +720,344 @@ def home():
467
  """
468
  return html
469
 
470
- def generate_filtered_rows(filtered_logs, search_query=""):
471
- if not filtered_logs:
472
- return '<tr><td colspan="6" class="empty-state"><h3>🔍 Tidak ada hasil</h3><p>Coba kata kunci lain</p></td></tr>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
473
 
474
  rows = ""
475
- for entry in filtered_logs[:50]:
476
- country = entry["country"]
477
- number = entry["number"]
478
- otp = entry["otp"]
479
- sms = entry["sms"]
 
 
480
 
481
  if search_query:
482
  country = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', country, flags=re.I)
483
  number = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', number, flags=re.I)
484
  otp = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', otp, flags=re.I)
485
- sms = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', sms, flags=re.I)
486
 
487
- service_class = entry["service"].lower().replace(' ', '')
488
  rows += f'''
489
- <tr>
490
- <td style="color: #00f2fe;">{entry["time"]}</td>
 
491
  <td>{country}</td>
492
  <td><span class="number">{number}</span></td>
493
- <td><span class="service-badge {service_class}">{entry["service"]}</span></td>
494
- <td><span class="otp-badge" onclick="copyOTP('{entry["otp"]}')">{otp}</span></td>
495
- <td><div title="{entry["sms"]}">{sms}</div></td>
496
  </tr>
497
  '''
498
  return rows
499
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  @app.route('/stream')
501
  def stream():
502
  def generate():
@@ -510,99 +1071,143 @@ def stream():
510
  sse_clients.remove(q)
511
  return Response(generate(), mimetype="text/event-stream")
512
 
513
- def run_server():
514
- app.run(host='0.0.0.0', port=7860, debug=False, threaded=True)
515
-
516
- Thread(target=run_server, daemon=True).start()
517
-
518
- def main():
519
- print("\n" + "="*60)
520
- print(" 🔥 OTP BOT - HUGGING FACE")
521
- print(" ⚡ PORT: 7860")
522
- print(" 🌐 DOMAIN: fourstore-tele.hf.space")
523
- print(" 🔑 Using Hugging Face Secrets")
524
- print(" 🔌 Telegram via Proxy")
525
- print("="*60 + "\n")
526
-
527
- if not USERNAME or not PASSWORD:
528
- print("❌ ERROR: USERNAME dan PASSWORD harus diisi di Secrets!")
529
  return
530
-
531
- for i in range(5):
532
- if login():
533
- break
534
- time.sleep(3)
535
-
536
- last_cleanup = time.time()
537
-
538
- while True:
539
  try:
540
- if not LOGIN_SUCCESS or not csrf_token:
541
- login()
542
- time.sleep(2)
543
- continue
544
-
545
- if time.time() - last_cleanup > 300:
546
- sms_cache.clear()
547
- sms_counter.clear()
548
- range_counter.clear()
549
- print("🧹 Cache cleared")
550
- last_cleanup = time.time()
551
-
552
- ranges_data = get_ranges_with_count()
553
- print(f"\n📡 Scan {len(ranges_data)} ranges...")
554
-
555
  for range_item in ranges_data:
 
 
 
556
  rng = range_item["name"]
557
  current_count = range_item["count"]
558
- prev_count = range_counter.get(rng, 0)
559
-
560
  if current_count > prev_count:
561
  country = clean_country(rng)
562
- print(f"\n🔥 RANGE BERUBAH: {country}")
563
  print(f" 📊 {prev_count} → {current_count} SMS")
564
- range_counter[rng] = current_count
565
-
566
- numbers_data = get_numbers_with_count(rng)
567
- print(f" 📞 Scan {len(numbers_data)} nomor...")
568
-
569
  for number_item in numbers_data:
 
 
 
570
  num = number_item["number"]
571
  num_count = number_item["count"]
572
  key = f"{rng}-{num}"
573
- prev_num_count = sms_counter.get(key, 0)
574
-
575
  if num_count > prev_num_count:
576
  print(f" 📱 Nomor: {mask_number(num)}")
577
  print(f" 📨 {prev_num_count} → {num_count} SMS")
578
- sms_counter[key] = num_count
579
-
580
- all_sms = get_sms_fast(rng, num)
581
  new_sms = all_sms[prev_num_count:]
582
-
583
  for service, sms, otp in new_sms:
584
  if otp:
585
  sms_id = f"{rng}-{num}-{otp}"
586
- if sms_id not in sent_cache:
587
- masked = mask_number(num)
588
- msg = f"🔔 *NEW OTP*\n🌍 {country}\n📞 `{masked}`\n💬 {service}\n🔐 `{otp}`\n\n{sms[:300]}"
589
- if tg_send(msg):
590
- sent_cache.add(sms_id)
591
- add_otp_log(country, masked, service, otp, sms)
592
- print(f" ✅ OTP: {otp} - {service} (via proxy)")
593
-
594
- print(f" Selesai proses {country}")
 
 
 
595
  time.sleep(0.5)
596
-
597
- print("\n⏳ Tidur 2 detik...")
 
 
598
  time.sleep(2)
599
-
600
  except Exception as e:
601
- print(f"❌ Error: {e}")
602
- time.sleep(3)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
603
 
604
  if __name__ == "__main__":
605
  try:
606
  main()
607
  except KeyboardInterrupt:
608
- print("\n🛑 BOT STOPPED")
 
 
3
  import re
4
  from datetime import datetime, timedelta, timezone
5
  import time
6
+ from flask import Flask, Response, request, redirect, session as flask_session
7
  from threading import Thread
8
  from collections import deque
9
  import json
10
  from queue import Queue
11
  import os
12
+ import uuid
13
+ import sys
14
+ from pymongo import MongoClient
15
+ from pymongo.errors import ConnectionFailure
16
+ from functools import wraps
17
+
18
+ MONGO_URI = os.environ.get("MONGGODB_URI")
19
+ DB_NAME = "otp_bot"
20
+ COLLECTION_NAME = "accounts"
21
+
22
+ try:
23
+ mongo_client = MongoClient(MONGO_URI)
24
+ db = mongo_client[DB_NAME]
25
+ accounts_collection = db[COLLECTION_NAME]
26
+ mongo_client.admin.command('ping')
27
+ print("✅ MongoDB Connected!")
28
+ except ConnectionFailure as e:
29
+ print(f"❌ MongoDB Connection Failed: {e}")
30
+ mongo_client = None
31
+
32
+ print = lambda *args, **kwargs: __builtins__.print(*args, **kwargs, flush=True)
33
 
34
  BASE = "http://159.69.3.189"
35
  LOGIN_URL = f"{BASE}/login"
 
37
  GET_NUMBER_URL = f"{BASE}/portal/sms/received/getsms/number"
38
  GET_SMS_URL = f"{BASE}/portal/sms/received/getsms/number/sms"
39
 
 
 
 
 
 
40
  TELEGRAM_PROXY_URL = "https://danihitambangetjir.termai.cc/api/proxy"
41
  CUSTOM_DOMAIN = "https://fourstore-otp.hf.space"
42
 
43
+ def mask_email(email):
44
+ if not email or '@' not in email:
45
+ return email
46
+ parts = email.split('@')
47
+ username = parts[0]
48
+ domain = parts[1]
49
+ if len(username) <= 3:
50
+ masked_username = username[0] + '*' * (len(username) - 1)
51
+ else:
52
+ masked_username = username[:2] + '*' * (len(username) - 3) + username[-1]
53
+ return f"{masked_username}@{domain}"
54
+
55
+ def load_accounts_from_mongodb():
56
+ accounts_dict = {}
57
+ try:
58
+ if mongo_client:
59
+ cursor = accounts_collection.find({})
60
+ for doc in cursor:
61
+ acc_id = doc.pop("_id")
62
+ doc["session"] = None
63
+ doc["csrf"] = None
64
+ doc["status"] = False
65
+ doc["otp_logs"] = []
66
+ doc["sent_cache"] = []
67
+ doc["sms_cache"] = {}
68
+ doc["sms_counter"] = {}
69
+ doc["range_counter"] = {}
70
+ doc["last_cleanup"] = time.time()
71
+ accounts_dict[acc_id] = doc
72
+ print(f"📊 Loaded {len(accounts_dict)} accounts from MongoDB")
73
+ except Exception as e:
74
+ print(f"❌ Error load from MongoDB: {e}")
75
+ return accounts_dict
76
+
77
+ def save_accounts_to_mongodb(accounts_dict):
78
+ try:
79
+ if mongo_client:
80
+ for acc_id, acc in accounts_dict.items():
81
+ acc_copy = acc.copy()
82
+ acc_copy.pop("session", None)
83
+ acc_copy.pop("csrf", None)
84
+ acc_copy.pop("otp_logs", None)
85
+ acc_copy.pop("sent_cache", None)
86
+ acc_copy.pop("sms_cache", None)
87
+ acc_copy.pop("sms_counter", None)
88
+ acc_copy.pop("range_counter", None)
89
+ acc_copy.pop("last_cleanup", None)
90
+ accounts_collection.update_one(
91
+ {"_id": acc_id},
92
+ {"$set": acc_copy},
93
+ upsert=True
94
+ )
95
+ print(f"💾 Accounts saved to MongoDB")
96
+ except Exception as e:
97
+ print(f"❌ Error save to MongoDB: {e}")
98
+
99
+ accounts = load_accounts_from_mongodb()
100
 
101
+ app = Flask('')
102
+ app.secret_key = "fourstore-multi-account-secret"
 
 
103
  sse_clients = []
104
 
105
+ # Decorator untuk cek owner via session
106
+ def owner_required(f):
107
+ @wraps(f)
108
+ def decorated_function(*args, **kwargs):
109
+ owner_id = flask_session.get('owner_id')
110
+ if not owner_id:
111
+ return redirect('/login_owner')
112
+ return f(*args, **kwargs)
113
+ return decorated_function
114
+
115
  def get_wib_time():
116
  return datetime.now(timezone.utc) + timedelta(hours=7)
117
 
 
119
  wib = get_wib_time()
120
  return (wib - timedelta(days=1)).strftime("%Y-%m-%d") if wib.hour < 7 else wib.strftime("%Y-%m-%d")
121
 
122
+ def login_account(account_id, username, password, bot_token, chat_id, owner_id):
 
123
  try:
124
+ masked = mask_email(username)
125
+ print(f"\n{'='*60}")
126
+ print(f"🔐 PROSES LOGIN UNTUK: {masked} (ID: {account_id})")
127
+ print(f"{'='*60}")
128
+
129
+ session = httpx.Client(follow_redirects=True, timeout=30.0)
130
  r = session.get(LOGIN_URL, timeout=30)
131
+
132
+ if r.status_code != 200:
133
+ return False, f"HTTP {r.status_code}"
134
+
135
  soup = BeautifulSoup(r.text, "html.parser")
136
  token = soup.find("input", {"name": "_token"})
137
  if not token:
138
+ return False, "Token tidak ditemukan"
139
+
140
  csrf_token = token.get("value")
 
141
  r = session.post(LOGIN_URL, data={
142
  "_token": csrf_token,
143
+ "email": username,
144
+ "password": password
145
  }, timeout=30)
146
+
147
  if "dashboard" in r.text.lower() or "logout" in r.text.lower():
148
+ accounts[account_id]["session"] = session
149
+ accounts[account_id]["csrf"] = csrf_token
150
+ accounts[account_id]["status"] = True
151
+ accounts[account_id]["username"] = username
152
+ accounts[account_id]["password"] = password
153
+ accounts[account_id]["bot_token"] = bot_token
154
+ accounts[account_id]["chat_id"] = chat_id
155
+ accounts[account_id]["owner_id"] = owner_id
156
+ accounts[account_id]["last_login"] = time.time()
157
+ save_accounts_to_mongodb(accounts)
158
+ return True, "Login berhasil"
159
  else:
160
+ return False, "Login gagal"
161
+
 
162
  except Exception as e:
163
+ return False, str(e)
164
+
165
+ def tg_send(account_id, msg):
166
+ try:
167
+ account = accounts.get(account_id)
168
+ if not account or not account.get("bot_token") or not account.get("chat_id"):
169
+ return False
170
+
171
+ url = f"{TELEGRAM_PROXY_URL}?url=https://api.telegram.org/bot{account['bot_token']}/sendMessage"
172
+ payload = {
173
+ "chat_id": account['chat_id'],
174
+ "text": msg,
175
+ "parse_mode": "Markdown"
176
+ }
177
+ httpx.post(url, json=payload, timeout=30)
178
+ return True
179
+ except:
180
  return False
181
 
182
+ def clean_country(rng):
183
+ return re.sub(r"\s*\d+$", "", rng).strip() if rng else "UNKNOWN"
184
+
185
+ def mask_number(number):
186
+ if not number: return "UNKNOWN"
187
+ clean = re.sub(r"[^\d+]", "", number)
188
+ if len(clean) <= 6: return clean
189
+ return f"{clean[:4]}****{clean[-3:]}"
190
+
191
+ def map_service(raw):
192
+ if not raw: return "UNKNOWN"
193
+ s = raw.lower().strip()
194
+ if 'whatsapp' in s: return "WHATSAPP"
195
+ if 'telegram' in s: return "TELEGRAM"
196
+ if 'google' in s or 'gmail' in s: return "GOOGLE"
197
+ if 'facebook' in s or 'fb' in s: return "FACEBOOK"
198
+ if 'instagram' in s or 'ig' in s: return "INSTAGRAM"
199
+ if 'tiktok' in s: return "TIKTOK"
200
+ if 'temu' in s: return "TEMU"
201
+ return raw.upper()
202
+
203
+ def extract_otp(text):
204
+ if not text: return None
205
+ m = re.search(r"\b(\d{3}[- ]?\d{3})\b", text)
206
+ if m:
207
+ return m.group(0).replace("-", "").replace(" ", "")
208
+ m = re.search(r"\b(\d{4,6})\b", text)
209
+ if m:
210
+ return m.group(0)
211
+ digits = re.findall(r'\d+', text)
212
+ for d in digits:
213
+ if 4 <= len(d) <= 6:
214
+ return d
215
+ return None
216
+
217
+ def get_ranges_with_count(account_id):
218
+ account = accounts.get(account_id)
219
+ if not account or not account.get("session") or not account.get("csrf"):
220
+ return []
221
+
222
  try:
223
  date = get_search_date()
224
+ r = account["session"].post(GET_RANGE_URL, data={
225
+ "_token": account["csrf"],
226
  "from": date,
227
  "to": date
228
  }, timeout=15)
 
244
  })
245
 
246
  return ranges_data
247
+ except:
248
  return []
249
 
250
+ def get_numbers_with_count(account_id, rng):
251
+ account = accounts.get(account_id)
252
+ if not account or not account.get("session") or not account.get("csrf"):
253
+ return []
254
+
255
  try:
256
  date = get_search_date()
257
+ r = account["session"].post(GET_NUMBER_URL, data={
258
+ "_token": account["csrf"],
259
  "start": date,
260
  "end": date,
261
  "range": rng
 
312
  })
313
 
314
  return numbers_data
315
+ except:
316
  return []
317
 
318
+ def get_sms_fast(account_id, rng, number):
319
+ account = accounts.get(account_id)
320
+ if not account or not account.get("session") or not account.get("csrf"):
321
+ return []
322
+
323
  try:
324
  date = get_search_date()
325
  cache_key = f"{rng}-{number}"
326
 
327
+ if cache_key in account["sms_cache"]:
328
+ timestamp, results = account["sms_cache"][cache_key]
329
  if time.time() - timestamp < 5:
330
  return results
331
 
332
+ r = account["session"].post(GET_SMS_URL, data={
333
+ "_token": account["csrf"],
334
  "start": date,
335
  "end": date,
336
  "Number": number,
 
357
  except:
358
  continue
359
 
360
+ account["sms_cache"][cache_key] = (time.time(), results)
361
  return results
362
  except:
363
  return []
364
 
365
+ def add_otp_log(account_id, country, number, service, otp, sms):
366
+ account = accounts.get(account_id)
367
+ if not account:
368
+ return
 
 
 
 
 
 
 
 
 
 
 
369
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  wib = get_wib_time()
371
  log_entry = {
372
  "time": wib.strftime("%H:%M:%S"),
 
374
  "number": number,
375
  "service": service,
376
  "otp": otp,
377
+ "sms": sms[:80] + "..." if len(sms) > 80 else sms,
378
+ "account_id": account_id,
379
+ "account_username": mask_email(account.get("username", "Unknown"))
380
  }
381
+
382
+ if "otp_logs" not in account:
383
+ account["otp_logs"] = []
384
+
385
+ account["otp_logs"].insert(0, log_entry)
386
+ if len(account["otp_logs"]) > 100:
387
+ account["otp_logs"] = account["otp_logs"][:100]
388
+
389
  broadcast_sse(log_entry)
390
  return log_entry
391
 
 
400
  for q in dead:
401
  sse_clients.remove(q)
402
 
403
+ @app.route('/login_owner', methods=['GET', 'POST'])
404
+ def login_owner():
405
+ if request.method == 'POST':
406
+ owner_id = request.form.get('owner_id')
407
+ if owner_id:
408
+ flask_session['owner_id'] = owner_id
409
+ return redirect('/')
410
+ return '''
411
+ <!DOCTYPE html>
412
+ <html>
413
+ <head>
414
+ <title>Owner Login</title>
415
+ <style>
416
+ body { background: #0a0c10; color: #e4e6eb; font-family: Inter, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
417
+ .login-box { background: #1a1f2c; padding: 40px; border-radius: 24px; border: 1px solid #2d3540; width: 300px; }
418
+ h2 { color: #00f2fe; margin-bottom: 20px; }
419
+ input { width: 100%; padding: 12px; background: #0a0c10; border: 1px solid #2d3540; border-radius: 12px; color: #e4e6eb; margin-bottom: 20px; }
420
+ button { background: #00f2fe; color: #0a0c10; border: none; padding: 12px; border-radius: 12px; font-weight: 600; cursor: pointer; width: 100%; }
421
+ </style>
422
+ </head>
423
+ <body>
424
+ <div class="login-box">
425
+ <h2>Owner Login</h2>
426
+ <form method="post">
427
+ <input type="text" name="owner_id" placeholder="Masukkan Owner ID" required>
428
+ <button type="submit">Login</button>
429
+ </form>
430
+ </div>
431
+ </body>
432
+ </html>
433
+ '''
434
+
435
+ @app.route('/logout_owner')
436
+ def logout_owner():
437
+ flask_session.pop('owner_id', None)
438
+ return redirect('/')
439
 
440
  @app.route('/')
441
+ @owner_required
442
  def home():
443
+ owner_id = flask_session.get('owner_id')
444
+
445
+ # Filter accounts berdasarkan owner_id
446
+ user_accounts = {}
447
+ for acc_id, acc in accounts.items():
448
+ if acc.get("owner_id") == owner_id:
449
+ user_accounts[acc_id] = acc
450
+
451
+ all_logs = []
452
+ for acc_id, acc in user_accounts.items():
453
+ if acc.get("otp_logs"):
454
+ for log in acc["otp_logs"]:
455
+ log_copy = log.copy()
456
+ log_copy["account_username"] = mask_email(acc.get("username", "Unknown"))
457
+ all_logs.append(log_copy)
458
+
459
+ all_logs = sorted(all_logs, key=lambda x: x.get("time", ""), reverse=True)
460
+
461
  search_query = request.args.get('q', '').lower()
462
  filter_service = request.args.get('service', '')
463
+ filter_account = request.args.get('account', '')
464
 
 
465
  if search_query:
466
+ all_logs = [log for log in all_logs if
467
+ search_query in log.get('country', '').lower() or
468
+ search_query in log.get('number', '').lower() or
469
+ search_query in log.get('otp', '').lower() or
470
+ search_query in log.get('sms', '').lower()]
471
 
472
  if filter_service:
473
+ all_logs = [log for log in all_logs if log.get('service') == filter_service]
474
+
475
+ if filter_account:
476
+ all_logs = [log for log in all_logs if log.get('account_id') == filter_account]
477
+
478
+ all_services = list(set([log.get('service') for log in all_logs if log.get('service')]))
479
+
480
+ total_otp = sum(len(acc.get('sent_cache', [])) for acc in user_accounts.values())
481
+ today_otp = len(all_logs)
482
+
483
+ accounts_list = []
484
+ for acc_id, acc in user_accounts.items():
485
+ accounts_list.append({
486
+ "id": acc_id,
487
+ "username": mask_email(acc.get("username", "Unknown")),
488
+ "status": acc.get("status", False),
489
+ "bot_token": "✅" if acc.get("bot_token") else "❌",
490
+ "chat_id": acc.get("chat_id", "-")[:5] + "..." if acc.get("chat_id") and len(acc.get("chat_id")) > 5 else acc.get("chat_id", "-")
491
+ })
492
+
493
  html = f"""
494
  <!DOCTYPE html>
495
  <html lang="en">
496
  <head>
497
  <meta charset="UTF-8">
498
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
499
+ <title>OTP MULTI ACCOUNT · FOURSTORE</title>
500
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
501
  <style>
502
  * {{ margin: 0; padding: 0; box-sizing: border-box; }}
503
  body {{ font-family: 'Inter', sans-serif; background: #0a0c10; color: #e4e6eb; padding: 24px; }}
504
+ .container {{ max-width: 1600px; margin: 0 auto; }}
505
+
506
+ /* Navbar */
507
+ .navbar {{ background: #1a1f2c; border-radius: 16px; padding: 16px 24px; margin-bottom: 24px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #2d3540; }}
508
+ .nav-brand {{ font-size: 20px; font-weight: 700; color: #00f2fe; }}
509
+ .nav-menu {{ display: flex; gap: 20px; align-items: center; }}
510
+ .nav-item {{ color: #8b949e; text-decoration: none; padding: 8px 16px; border-radius: 8px; transition: all 0.2s; }}
511
+ .nav-item:hover {{ background: #2d3540; color: #00f2fe; }}
512
+ .nav-item.active {{ background: #00f2fe20; color: #00f2fe; border: 1px solid #00f2fe40; }}
513
+ .owner-badge {{ background: #4a3a2d; padding: 8px 16px; border-radius: 100px; font-size: 14px; }}
514
+
515
  .header {{ background: linear-gradient(145deg, #1a1f2c, #0f131c); border-radius: 24px; padding: 28px; margin-bottom: 28px; border: 1px solid #2d3540; }}
516
+ .header-top {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 15px; }}
517
  .title h1 {{ font-size: 28px; font-weight: 700; background: linear-gradient(135deg, #00f2fe, #4facfe); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }}
518
  .title p {{ color: #8b949e; font-size: 14px; }}
519
  .status-badge {{ padding: 10px 24px; border-radius: 100px; font-weight: 600; font-size: 14px;
520
+ background: #0a4d3c; color: #a0f0d0; border: 1px solid #1a6e5a; }}
 
 
521
  .stats-grid {{ display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-top: 20px; }}
522
  .stat-card {{ background: #1a1f2c; padding: 20px; border-radius: 20px; border: 1px solid #2d3540; }}
523
  .stat-label {{ color: #8b949e; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }}
524
  .stat-value {{ font-size: 32px; font-weight: 700; color: #00f2fe; }}
525
+
526
+ .add-account-form {{ background: #1a1f2c; padding: 20px; border-radius: 16px; margin-bottom: 30px; border: 1px solid #2d3540; }}
527
+ .form-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; }}
528
+ .form-input {{ background: #0a0c10; border: 1px solid #2d3540; padding: 12px 16px; border-radius: 12px; color: #e4e6eb; width: 100%; }}
529
+ .form-input:focus {{ outline: none; border-color: #00f2fe; }}
530
+ .btn {{ background: #00f2fe; color: #0a0c10; border: none; padding: 12px 24px; border-radius: 12px; font-weight: 600; cursor: pointer; transition: all 0.2s; }}
531
+ .btn:hover {{ background: #00d8e4; transform: translateY(-2px); }}
532
+ .btn-danger {{ background: #ff4d4d; color: white; }}
533
+ .btn-small {{ padding: 6px 12px; font-size: 12px; }}
534
+
535
+ .account-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px; }}
536
+ .account-card {{ background: #1a1f2c; border-radius: 16px; padding: 20px; border: 1px solid #2d3540; position: relative; }}
537
+ .account-card.online {{ border-color: #00f2fe; box-shadow: 0 0 15px #00f2fe20; }}
538
+ .account-status {{ position: absolute; top: 20px; right: 20px; width: 12px; height: 12px; border-radius: 50%; }}
539
+ .status-online {{ background: #00f2fe; box-shadow: 0 0 10px #00f2fe; }}
540
+ .status-offline {{ background: #ff4d4d; }}
541
+ .account-actions {{ display: flex; gap: 8px; margin-top: 15px; flex-wrap: wrap; }}
542
+
543
  .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; }}
544
  .search-box {{ flex: 2; min-width: 280px; position: relative; }}
545
  .search-icon {{ position: absolute; left: 16px; top: 14px; color: #8b949e; }}
546
  .search-input {{ width: 100%; padding: 14px 20px 14px 48px; background: #1a1f2c; border: 1px solid #2d3540; border-radius: 100px; color: #e4e6eb; font-size: 15px; }}
547
+ .filter-box {{ flex: 1; min-width: 150px; }}
548
  .filter-select {{ width: 100%; padding: 14px 20px; background: #1a1f2c; border: 1px solid #2d3540; border-radius: 100px; color: #e4e6eb; font-size: 15px; cursor: pointer; }}
549
+ .clear-btn {{ padding: 8px 16px; background: #2d3a4a; border: none; border-radius: 100px; color: white; font-size: 13px; cursor: pointer; text-decoration: none; }}
550
+
551
+ .otp-section {{ background: #0f131c; border-radius: 24px; padding: 28px; border: 1px solid #2d3540; overflow-x: auto; }}
552
+ table {{ width: 100%; border-collapse: collapse; min-width: 1100px; }}
 
 
553
  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; }}
554
  td {{ padding: 16px 12px; border-bottom: 1px solid #262c38; font-size: 14px; }}
555
  .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; }}
 
556
  .service-badge {{ background: #2d3a4a; padding: 6px 14px; border-radius: 100px; font-size: 12px; font-weight: 600; display: inline-block; }}
557
+ .account-badge {{ background: #4a3a2d; padding: 4px 10px; border-radius: 20px; font-size: 11px; display: inline-block; }}
558
+ .whatsapp {{ background: #25D36620; color: #25D366; border: 1px solid #25D36640; }}
559
+ .telegram {{ background: #26A5E420; color: #26A5E4; border: 1px solid #26A5E440; }}
 
 
 
 
560
  .number {{ font-family: monospace; }}
561
  .empty-state {{ text-align: center; padding: 60px; color: #8b949e; }}
562
  .highlight {{ background: #00f2fe30; border-radius: 4px; padding: 0 2px; }}
563
  .new-row {{ animation: fadeIn 0.3s ease; background: linear-gradient(90deg, #00f2fe10, transparent); }}
564
+ .toast {{ position: fixed; bottom: 24px; right: 24px; background: #00f2fe; color: #000; padding: 14px 28px; border-radius: 100px; font-weight: 600; z-index: 9999; }}
565
  @keyframes fadeIn {{ from {{ opacity: 0; transform: translateY(-10px); }} to {{ opacity: 1; transform: translateY(0); }} }}
 
566
  </style>
567
  </head>
568
  <body>
569
  <div class="container">
570
+ <!-- Navbar -->
571
+ <div class="navbar">
572
+ <div class="nav-brand">OTP MULTI ACCOUNT</div>
573
+ <div class="nav-menu">
574
+ <a href="/" class="nav-item active">🏠 Dashboard</a>
575
+ <a href="/accounts" class="nav-item">📋 My Accounts</a>
576
+ <a href="/logout_owner" class="nav-item">🚪 Logout</a>
577
+ <span class="owner-badge">👤 Owner: {owner_id}</span>
578
+ </div>
579
+ </div>
580
+
581
  <div class="header">
582
  <div class="header-top">
583
  <div class="title">
584
+ <h1>OTP MULTI ACCOUNT · FOURSTORE</h1>
585
  <p>{CUSTOM_DOMAIN}</p>
586
  </div>
587
+ <div class="status-badge">● ONLINE</div>
 
 
588
  </div>
589
  <div class="stats-grid">
590
+ <div class="stat-card"><div class="stat-label">Total Akun</div><div class="stat-value">{len(user_accounts)}</div></div>
591
+ <div class="stat-card"><div class="stat-label">Akun Online</div><div class="stat-value">{sum(1 for a in user_accounts.values() if a.get('status'))}</div></div>
592
+ <div class="stat-card"><div class="stat-label">Total OTP</div><div class="stat-value">{total_otp}</div></div>
593
+ <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>
594
  </div>
595
  </div>
596
 
597
+ <div class="add-account-form">
598
+ <h3 style="margin-bottom: 15px;">➕ Tambah Akun Baru (Owner ID: {owner_id})</h3>
599
+ <form action="/add_account" method="POST" class="form-grid">
600
+ <input type="text" name="username" placeholder="Username/Email" class="form-input" required>
601
+ <input type="password" name="password" placeholder="Password" class="form-input" required>
602
+ <input type="text" name="bot_token" placeholder="Bot Token (opsional)" class="form-input">
603
+ <input type="text" name="chat_id" placeholder="Chat ID (opsional)" class="form-input">
604
+ <input type="hidden" name="owner_id" value="{owner_id}">
605
+ <button type="submit" class="btn">Tambah & Login Otomatis</button>
606
+ </form>
607
+ </div>
608
+
609
+ <!-- Preview Akun (3 teratas) -->
610
+ <div style="margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
611
+ <h3>📋 Akun Terbaru</h3>
612
+ <a href="/accounts" class="btn btn-small">Lihat Semua Akun →</a>
613
+ </div>
614
+ <div class="account-grid">
615
+ {generate_preview_accounts(accounts_list[:3])}
616
+ </div>
617
+
618
  <div class="search-section">
619
  <div class="search-box">
620
  <span class="search-icon">🔍</span>
621
  <form action="/" method="get" id="searchForm">
622
+ <input type="text" class="search-input" name="q" placeholder="Cari OTP..." value="{request.args.get('q', '')}">
623
  </form>
624
  </div>
625
  <div class="filter-box">
626
+ <select class="filter-select" name="service" onchange="updateFilter('service', this.value)">
627
  <option value="">📋 Semua Service</option>
628
  {''.join([f'<option value="{s}" {"selected" if filter_service == s else ""}>📱 {s}</option>' for s in sorted(all_services)])}
629
  </select>
630
  </div>
631
+ <div class="filter-box">
632
+ <select class="filter-select" name="account" onchange="updateFilter('account', this.value)">
633
+ <option value="">👤 Semua Akun</option>
634
+ {''.join([f'<option value="{a["id"]}" {"selected" if filter_account == a["id"] else ""}>👤 {a["username"]}</option>' for a in accounts_list[:10]])}
635
+ </select>
636
+ </div>
637
+ <a href="/" class="clear-btn">✕ Reset</a>
638
+ <span class="result-count">📊 {len(all_logs)} hasil</span>
639
  </div>
640
 
641
  <div class="otp-section">
642
+ <h3 style="margin-bottom: 20px;">📨 OTP TERBARU <span style="background:#00f2fe20; padding:4px 12px; border-radius:100px; font-size:12px;">LIVE</span></h3>
643
+ <table>
644
+ <thead>
645
+ <tr>
646
+ <th>WIB</th>
647
+ <th>Akun</th>
648
+ <th>Country</th>
649
+ <th>Number</th>
650
+ <th>Service</th>
651
+ <th>OTP</th>
652
+ <th>Message</th>
653
+ </tr>
654
+ </thead>
655
+ <tbody id="otp-table-body">
656
+ {generate_otp_rows(all_logs, search_query)}
657
+ </tbody>
658
+ </table>
659
  </div>
660
  </div>
661
 
662
  <script>
663
  let eventSource;
664
+
665
  function connectSSE() {{
666
  eventSource = new EventSource('/stream');
667
  eventSource.onmessage = function(e) {{
668
  try {{
669
  const data = JSON.parse(e.data);
670
  if (data.otp) {{
671
+ location.reload();
 
672
  }}
673
  }} catch(err) {{}}
674
  }};
675
  eventSource.onerror = function() {{ setTimeout(connectSSE, 3000); }};
676
  }}
677
 
678
+ function updateFilter(key, value) {{
679
+ const url = new URL(window.location.href);
680
+ if (value) {{
681
+ url.searchParams.set(key, value);
682
+ }} else {{
683
+ url.searchParams.delete(key);
684
+ }}
685
+ window.location.href = url.toString();
686
+ }}
687
+
688
+ function loginAccount(accountId) {{
689
+ fetch('/login_account/' + accountId, {{method: 'POST'}})
690
+ .then(() => location.reload());
691
+ }}
692
+
693
+ function logoutAccount(accountId) {{
694
+ fetch('/logout_account/' + accountId, {{method: 'POST'}})
695
+ .then(() => location.reload());
696
  }}
697
 
698
  function copyOTP(otp) {{
699
  navigator.clipboard.writeText(otp).then(() => {{
700
+ const toast = document.createElement('div');
701
+ toast.className = 'toast';
702
+ toast.textContent = '✅ OTP copied!';
703
+ document.body.appendChild(toast);
704
+ setTimeout(() => toast.remove(), 2000);
705
  }});
706
  }}
707
 
 
 
 
 
 
 
 
708
  function updateTime() {{
709
+ const now = new Date();
710
+ now.setHours(now.getHours() + 7);
711
  const wibEl = document.getElementById('wib-time');
712
  if (wibEl) wibEl.textContent = now.toISOString().substr(11, 8);
713
  }}
 
720
  """
721
  return html
722
 
723
+ def generate_preview_accounts(accounts_list):
724
+ if not accounts_list:
725
+ return '<div style="grid-column:1/-1; text-align:center; padding:40px; color:#8b949e;">Belum ada akun. Tambah akun di atas!</div>'
726
+
727
+ html = ""
728
+ for acc in accounts_list:
729
+ status_class = "online" if acc["status"] else "offline"
730
+ html += f"""
731
+ <div class="account-card {status_class}">
732
+ <div class="account-status status-{status_class}"></div>
733
+ <h4>{acc['username']}</h4>
734
+ <p style="margin-top:8px; color:#8b949e; font-size:12px;">Bot: {acc['bot_token']} | Chat: {acc['chat_id']}</p>
735
+ <p style="color:#8b949e; font-size:12px;">Status: {'🟢 Online' if acc['status'] else '🔴 Offline'}</p>
736
+ </div>
737
+ """
738
+ return html
739
+
740
+ @app.route('/accounts')
741
+ @owner_required
742
+ def accounts_page():
743
+ owner_id = flask_session.get('owner_id')
744
+
745
+ # Filter accounts berdasarkan owner_id
746
+ user_accounts = {}
747
+ for acc_id, acc in accounts.items():
748
+ if acc.get("owner_id") == owner_id:
749
+ user_accounts[acc_id] = acc
750
+
751
+ accounts_list = []
752
+ for acc_id, acc in user_accounts.items():
753
+ accounts_list.append({
754
+ "id": acc_id,
755
+ "username": mask_email(acc.get("username", "Unknown")),
756
+ "status": acc.get("status", False),
757
+ "bot_token": acc.get("bot_token", "-")[:10] + "..." if len(acc.get("bot_token", "")) > 10 else acc.get("bot_token", "-"),
758
+ "chat_id": acc.get("chat_id", "-"),
759
+ "created_at": datetime.fromtimestamp(acc.get("created_at", time.time())).strftime("%Y-%m-%d %H:%M") if acc.get("created_at") else "-",
760
+ "last_login": datetime.fromtimestamp(acc.get("last_login", 0)).strftime("%Y-%m-%d %H:%M") if acc.get("last_login") else "-",
761
+ "otp_count": len(acc.get("sent_cache", []))
762
+ })
763
+
764
+ html = f"""
765
+ <!DOCTYPE html>
766
+ <html lang="en">
767
+ <head>
768
+ <meta charset="UTF-8">
769
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
770
+ <title>My Accounts · FOURSTORE</title>
771
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
772
+ <style>
773
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
774
+ body {{ font-family: 'Inter', sans-serif; background: #0a0c10; color: #e4e6eb; padding: 24px; }}
775
+ .container {{ max-width: 1400px; margin: 0 auto; }}
776
+
777
+ .navbar {{ background: #1a1f2c; border-radius: 16px; padding: 16px 24px; margin-bottom: 24px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #2d3540; }}
778
+ .nav-brand {{ font-size: 20px; font-weight: 700; color: #00f2fe; }}
779
+ .nav-menu {{ display: flex; gap: 20px; align-items: center; }}
780
+ .nav-item {{ color: #8b949e; text-decoration: none; padding: 8px 16px; border-radius: 8px; }}
781
+ .nav-item:hover {{ background: #2d3540; color: #00f2fe; }}
782
+ .nav-item.active {{ background: #00f2fe20; color: #00f2fe; border: 1px solid #00f2fe40; }}
783
+ .owner-badge {{ background: #4a3a2d; padding: 8px 16px; border-radius: 100px; font-size: 14px; }}
784
+
785
+ .header {{ background: linear-gradient(145deg, #1a1f2c, #0f131c); border-radius: 24px; padding: 28px; margin-bottom: 28px; border: 1px solid #2d3540; }}
786
+ .header-top {{ display: flex; justify-content: space-between; align-items: center; }}
787
+ .title h1 {{ font-size: 28px; font-weight: 700; background: linear-gradient(135deg, #00f2fe, #4facfe); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }}
788
+
789
+ .accounts-table {{ background: #0f131c; border-radius: 24px; padding: 28px; border: 1px solid #2d3540; overflow-x: auto; }}
790
+ table {{ width: 100%; border-collapse: collapse; min-width: 1000px; }}
791
+ 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; }}
792
+ td {{ padding: 16px 12px; border-bottom: 1px solid #262c38; font-size: 14px; }}
793
+ .status-badge {{ display: inline-block; padding: 4px 8px; border-radius: 100px; font-size: 12px; font-weight: 600; }}
794
+ .status-online {{ background: #0a4d3c; color: #a0f0d0; }}
795
+ .status-offline {{ background: #4a2c2c; color: #ffb3b3; }}
796
+ .btn {{ background: #00f2fe; color: #0a0c10; border: none; padding: 6px 12px; border-radius: 8px; font-size: 12px; font-weight: 600; cursor: pointer; text-decoration: none; display: inline-block; }}
797
+ .btn-danger {{ background: #ff4d4d; color: white; }}
798
+ .btn-small {{ padding: 4px 8px; font-size: 11px; }}
799
+ .action-group {{ display: flex; gap: 5px; }}
800
+ </style>
801
+ </head>
802
+ <body>
803
+ <div class="container">
804
+ <div class="navbar">
805
+ <div class="nav-brand">OTP MULTI ACCOUNT</div>
806
+ <div class="nav-menu">
807
+ <a href="/" class="nav-item">🏠 Dashboard</a>
808
+ <a href="/accounts" class="nav-item active">📋 My Accounts</a>
809
+ <a href="/logout_owner" class="nav-item">🚪 Logout</a>
810
+ <span class="owner-badge">👤 Owner: {owner_id}</span>
811
+ </div>
812
+ </div>
813
+
814
+ <div class="header">
815
+ <div class="header-top">
816
+ <div class="title">
817
+ <h1>📋 My Accounts</h1>
818
+ <p style="color:#8b949e; margin-top:8px;">Total: {len(accounts_list)} akun | Online: {sum(1 for a in accounts_list if a['status'])}</p>
819
+ </div>
820
+ <a href="/" class="btn">+ Tambah Akun Baru</a>
821
+ </div>
822
+ </div>
823
+
824
+ <div class="accounts-table">
825
+ <table>
826
+ <thead>
827
+ <tr>
828
+ <th>No</th>
829
+ <th>Username</th>
830
+ <th>Status</th>
831
+ <th>Bot Token</th>
832
+ <th>Chat ID</th>
833
+ <th>OTP Count</th>
834
+ <th>Created At</th>
835
+ <th>Last Login</th>
836
+ <th>Actions</th>
837
+ </tr>
838
+ </thead>
839
+ <tbody>
840
+ {generate_accounts_rows(accounts_list)}
841
+ </tbody>
842
+ </table>
843
+ </div>
844
+ </div>
845
+
846
+ <script>
847
+ function loginAccount(accountId) {{
848
+ fetch('/login_account/' + accountId, {{method: 'POST'}})
849
+ .then(() => location.reload());
850
+ }}
851
+
852
+ function logoutAccount(accountId) {{
853
+ fetch('/logout_account/' + accountId, {{method: 'POST'}})
854
+ .then(() => location.reload());
855
+ }}
856
+ </script>
857
+ </body>
858
+ </html>
859
+ """
860
+ return html
861
+
862
+ def generate_accounts_rows(accounts_list):
863
+ if not accounts_list:
864
+ return '<tr><td colspan="9" style="text-align:center; padding:60px; color:#8b949e;">Belum ada akun. Tambah akun di dashboard!</td></tr>'
865
+
866
+ rows = ""
867
+ for i, acc in enumerate(accounts_list, 1):
868
+ status_class = "status-online" if acc["status"] else "status-offline"
869
+ status_text = "🟢 Online" if acc["status"] else "🔴 Offline"
870
+
871
+ rows += f"""
872
+ <tr>
873
+ <td>{i}</td>
874
+ <td>{acc['username']}</td>
875
+ <td><span class="status-badge {status_class}">{status_text}</span></td>
876
+ <td>{acc['bot_token']}</td>
877
+ <td>{acc['chat_id']}</td>
878
+ <td>{acc['otp_count']}</td>
879
+ <td>{acc['created_at']}</td>
880
+ <td>{acc['last_login']}</td>
881
+ <td>
882
+ <div class="action-group">
883
+ <button onclick="loginAccount('{acc['id']}')" class="btn btn-small">Login</button>
884
+ <button onclick="logoutAccount('{acc['id']}')" class="btn btn-small btn-danger">Logout</button>
885
+ <a href="/delete_account/{acc['id']}" class="btn btn-small btn-danger" onclick="return confirm('Hapus akun ini?')">Hapus</a>
886
+ </div>
887
+ </td>
888
+ </tr>
889
+ """
890
+ return rows
891
+
892
+ def generate_otp_rows(logs, search_query):
893
+ if not logs:
894
+ return '<tr><td colspan="7" class="empty-state">🔍 Belum ada OTP</td></tr>'
895
 
896
  rows = ""
897
+ for log in logs[:100]:
898
+ country = log.get('country', '')
899
+ number = log.get('number', '')
900
+ otp = log.get('otp', '')
901
+ sms = log.get('sms', '')
902
+ service = log.get('service', 'UNKNOWN')
903
+ account = log.get('account_username', 'Unknown')
904
 
905
  if search_query:
906
  country = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', country, flags=re.I)
907
  number = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', number, flags=re.I)
908
  otp = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', otp, flags=re.I)
 
909
 
910
+ service_class = service.lower().replace(' ', '')
911
  rows += f'''
912
+ <tr class="new-row">
913
+ <td style="color:#00f2fe;">{log.get('time', '')}</td>
914
+ <td><span class="account-badge">{account}</span></td>
915
  <td>{country}</td>
916
  <td><span class="number">{number}</span></td>
917
+ <td><span class="service-badge {service_class}">{service}</span></td>
918
+ <td><span class="otp-badge" onclick="copyOTP('{otp}')">{otp}</span></td>
919
+ <td><div style="max-width:300px; overflow:hidden; text-overflow:ellipsis;" title="{log.get('sms', '')}">{log.get('sms', '')}</div></td>
920
  </tr>
921
  '''
922
  return rows
923
 
924
+ @app.route('/add_account', methods=['POST'])
925
+ def add_account_route():
926
+ account_id = str(uuid.uuid4())[:8]
927
+ username = request.form['username']
928
+ password = request.form['password']
929
+ bot_token = request.form.get('bot_token', '')
930
+ chat_id = request.form.get('chat_id', '')
931
+ owner_id = request.form.get('owner_id')
932
+
933
+ if not owner_id:
934
+ owner_id = flask_session.get('owner_id')
935
+
936
+ masked = mask_email(username)
937
+ print(f"\n➕ TAMBAH AKUN BARU: {masked} (ID: {account_id}) (Owner: {owner_id})")
938
+
939
+ accounts[account_id] = {
940
+ "id": account_id,
941
+ "username": username,
942
+ "password": password,
943
+ "bot_token": bot_token,
944
+ "chat_id": chat_id,
945
+ "owner_id": owner_id,
946
+ "session": None,
947
+ "csrf": None,
948
+ "status": False,
949
+ "otp_logs": [],
950
+ "sent_cache": [],
951
+ "sms_cache": {},
952
+ "sms_counter": {},
953
+ "range_counter": {},
954
+ "last_cleanup": time.time(),
955
+ "created_at": time.time()
956
+ }
957
+ save_accounts_to_mongodb(accounts)
958
+ print(f"✅ Akun ditambahkan: {masked}")
959
+
960
+ # OTOMATIS LOGIN SETELAH TAMBAH AKUN
961
+ print(f"🔄 Mencoba login otomatis untuk {masked}...")
962
+ success, msg = login_account(
963
+ account_id,
964
+ username,
965
+ password,
966
+ bot_token,
967
+ chat_id,
968
+ owner_id
969
+ )
970
+
971
+ if success:
972
+ print(f"✅✅✅ LOGIN OTOMATIS BERHASIL! Memulai thread scraper...")
973
+ thread = Thread(target=run_account_scraper, args=(account_id,), daemon=True)
974
+ thread.start()
975
+ print(f"✅ Thread scraper dimulai untuk {masked}")
976
+ else:
977
+ print(f"❌❌❌ LOGIN OTOMATIS GAGAL: {msg}")
978
+
979
+ return redirect('/')
980
+
981
+ @app.route('/delete_account/<account_id>')
982
+ @owner_required
983
+ def delete_account_route(account_id):
984
+ owner_id = flask_session.get('owner_id')
985
+
986
+ if account_id in accounts:
987
+ # Cek apakah akun ini milik owner yang sedang login
988
+ if accounts[account_id].get("owner_id") != owner_id:
989
+ return "Unauthorized", 403
990
+
991
+ username = accounts[account_id].get('username', 'Unknown')
992
+ masked = mask_email(username)
993
+ print(f"\n🗑️ HAPUS AKUN: {masked} (ID: {account_id})")
994
+ del accounts[account_id]
995
+ if mongo_client:
996
+ accounts_collection.delete_one({"_id": account_id})
997
+ print(f"✅ Akun dihapus")
998
+
999
+ return redirect(request.referrer or '/')
1000
+
1001
+ @app.route('/login_account/<account_id>', methods=['POST'])
1002
+ @owner_required
1003
+ def login_account_route(account_id):
1004
+ owner_id = flask_session.get('owner_id')
1005
+ print(f"\n🔔🔔🔔 LOGIN ACCOUNT DIPANGGIL: {account_id} 🔔🔔🔔")
1006
+
1007
+ if account_id in accounts:
1008
+ # Cek apakah akun ini milik owner yang sedang login
1009
+ if accounts[account_id].get("owner_id") != owner_id:
1010
+ return "Unauthorized", 403
1011
+
1012
+ acc = accounts[account_id]
1013
+ masked = mask_email(acc.get('username', 'Unknown'))
1014
+ print(f"📋 Data akun: {masked}")
1015
+ print(f"🔐 Mencoba login...")
1016
+
1017
+ success, msg = login_account(
1018
+ account_id,
1019
+ acc['username'],
1020
+ acc['password'],
1021
+ acc.get('bot_token', ''),
1022
+ acc.get('chat_id', ''),
1023
+ owner_id
1024
+ )
1025
+
1026
+ print(f"📝 Result: {success} - {msg}")
1027
+
1028
+ if success:
1029
+ print(f"✅✅✅ LOGIN BERHASIL! Memulai thread scraper...")
1030
+ thread = Thread(target=run_account_scraper, args=(account_id,), daemon=True)
1031
+ thread.start()
1032
+ print(f"✅ Thread scraper dimulai untuk {masked}")
1033
+ else:
1034
+ print(f"❌❌❌ LOGIN GAGAL: {msg}")
1035
+ else:
1036
+ print(f"❌ Account ID {account_id} tidak ditemukan!")
1037
+
1038
+ return redirect(request.referrer or '/')
1039
+
1040
+ @app.route('/logout_account/<account_id>', methods=['POST'])
1041
+ @owner_required
1042
+ def logout_account_route(account_id):
1043
+ owner_id = flask_session.get('owner_id')
1044
+
1045
+ if account_id in accounts:
1046
+ # Cek apakah akun ini milik owner yang sedang login
1047
+ if accounts[account_id].get("owner_id") != owner_id:
1048
+ return "Unauthorized", 403
1049
+
1050
+ username = accounts[account_id].get('username', 'Unknown')
1051
+ masked = mask_email(username)
1052
+ print(f"\n🔌 LOGOUT: {masked} (ID: {account_id})")
1053
+ accounts[account_id]["status"] = False
1054
+ accounts[account_id]["session"] = None
1055
+ accounts[account_id]["csrf"] = None
1056
+ save_accounts_to_mongodb(accounts)
1057
+ print(f"✅ Logout berhasil")
1058
+
1059
+ return redirect(request.referrer or '/')
1060
+
1061
  @app.route('/stream')
1062
  def stream():
1063
  def generate():
 
1071
  sse_clients.remove(q)
1072
  return Response(generate(), mimetype="text/event-stream")
1073
 
1074
+ def run_account_scraper(account_id):
1075
+ account = accounts.get(account_id)
1076
+ if not account:
 
 
 
 
 
 
 
 
 
 
 
 
 
1077
  return
1078
+
1079
+ username = account['username']
1080
+ masked = mask_email(username)
1081
+ print(f"\n🚀🚀🚀 STARTING SCRAPER FOR: {masked} 🚀🚀🚀")
1082
+ loop_count = 0
1083
+
1084
+ while account.get("status"):
1085
+ loop_count += 1
 
1086
  try:
1087
+ print(f"\n{'='*60}")
1088
+ print(f"🔄 [{masked}] LOOP #{loop_count}")
1089
+ print(f"{'='*60}")
1090
+
1091
+ if time.time() - account.get("last_cleanup", 0) > 300:
1092
+ account["sms_cache"] = {}
1093
+ account["sms_counter"] = {}
1094
+ account["range_counter"] = {}
1095
+ account["last_cleanup"] = time.time()
1096
+ print(f"🧹 [{masked}] Cache cleared")
1097
+
1098
+ ranges_data = get_ranges_with_count(account_id)
1099
+ print(f"📊 [{masked}] Total ranges: {len(ranges_data)}")
1100
+
 
1101
  for range_item in ranges_data:
1102
+ if not account.get("status"):
1103
+ break
1104
+
1105
  rng = range_item["name"]
1106
  current_count = range_item["count"]
1107
+ prev_count = account["range_counter"].get(rng, 0)
1108
+
1109
  if current_count > prev_count:
1110
  country = clean_country(rng)
1111
+ print(f"\n🔥 RANGE BERUBAH: {country} ({masked})")
1112
  print(f" 📊 {prev_count} → {current_count} SMS")
1113
+ account["range_counter"][rng] = current_count
1114
+
1115
+ numbers_data = get_numbers_with_count(account_id, rng)
1116
+ print(f" 📞 Total nomor: {len(numbers_data)}")
1117
+
1118
  for number_item in numbers_data:
1119
+ if not account.get("status"):
1120
+ break
1121
+
1122
  num = number_item["number"]
1123
  num_count = number_item["count"]
1124
  key = f"{rng}-{num}"
1125
+ prev_num_count = account["sms_counter"].get(key, 0)
1126
+
1127
  if num_count > prev_num_count:
1128
  print(f" 📱 Nomor: {mask_number(num)}")
1129
  print(f" 📨 {prev_num_count} → {num_count} SMS")
1130
+ account["sms_counter"][key] = num_count
1131
+
1132
+ all_sms = get_sms_fast(account_id, rng, num)
1133
  new_sms = all_sms[prev_num_count:]
1134
+
1135
  for service, sms, otp in new_sms:
1136
  if otp:
1137
  sms_id = f"{rng}-{num}-{otp}"
1138
+ if sms_id not in account["sent_cache"]:
1139
+ masked_num = mask_number(num)
1140
+ msg = f"🔔 *NEW OTP*\n🌍 {country}\n📞 `{masked_num}`\n💬 {service}\n🔐 `{otp}`\n\n{sms[:300]}"
1141
+ print(f" 📤 Mengirim OTP {otp} ke Telegram...")
1142
+
1143
+ if tg_send(account_id, msg):
1144
+ account["sent_cache"].append(sms_id)
1145
+ if len(account["sent_cache"]) > 1000:
1146
+ account["sent_cache"] = account["sent_cache"][-1000:]
1147
+ add_otp_log(account_id, country, masked_num, service, otp, sms)
1148
+ print(f" ✅ OTP: {otp} - {service} TERKIRIM!")
1149
+
1150
  time.sleep(0.5)
1151
+ else:
1152
+ print(f" ⏭️ Range {clean_country(rng)} tidak berubah (count: {current_count})")
1153
+
1154
+ print(f"\n⏳ [{masked}] Tidur 2 detik...")
1155
  time.sleep(2)
1156
+
1157
  except Exception as e:
1158
+ print(f"❌ ERROR in scraper for {masked}: {str(e)}")
1159
+ time.sleep(5)
1160
+
1161
+ print(f"\n🛑🛑🛑 SCRAPER STOPPED FOR: {masked} ����🛑🛑")
1162
+
1163
+ def run_server():
1164
+ app.run(host='0.0.0.0', port=7860, debug=False, threaded=True)
1165
+
1166
+ Thread(target=run_server, daemon=True).start()
1167
+
1168
+ def main():
1169
+ print("\n" + "="*60)
1170
+ print(" 🔥 OTP MULTI ACCOUNT - FOURSTORE")
1171
+ print(" ⚡ PORT: 7860")
1172
+ print(f" 🌐 DOMAIN: {CUSTOM_DOMAIN}")
1173
+ print(" 📁 Data tersimpan di MongoDB")
1174
+ print(" 📋 LOGGING: FULL DETAIL")
1175
+ print(" 🔒 EMAIL SENSOR: AKTIF")
1176
+ print(" 🤖 AUTO LOGIN: AKTIF (setelah tambah akun)")
1177
+ print(" 👑 OWNER SYSTEM: AKTIF")
1178
+ print("="*60 + "\n")
1179
+
1180
+ # Auto login untuk akun yang sudah login sebelumnya
1181
+ for acc_id, acc in accounts.items():
1182
+ if acc.get("status"):
1183
+ masked = mask_email(acc['username'])
1184
+ print(f"🔄 Auto-login untuk {masked}...")
1185
+ success, msg = login_account(
1186
+ acc_id,
1187
+ acc['username'],
1188
+ acc['password'],
1189
+ acc.get('bot_token', ''),
1190
+ acc.get('chat_id', ''),
1191
+ acc.get('owner_id', 'unknown')
1192
+ )
1193
+ if success:
1194
+ thread = Thread(target=run_account_scraper, args=(acc_id,), daemon=True)
1195
+ thread.start()
1196
+ print(f"✅ {masked} online")
1197
+ else:
1198
+ print(f"❌ {masked} offline: {msg}")
1199
+ acc["status"] = False
1200
+ save_accounts_to_mongodb(accounts)
1201
+
1202
+ print("\n✅ BOT SIAP! Dashboard: https://fourstore-otp.hf.space")
1203
+ print("🔐 Login dengan Owner ID untuk mengakses")
1204
+
1205
+ while True:
1206
+ time.sleep(60)
1207
 
1208
  if __name__ == "__main__":
1209
  try:
1210
  main()
1211
  except KeyboardInterrupt:
1212
+ print("\n🛑 BOT STOPPED")
1213
+ save_accounts_to_mongodb(accounts)