Fourstore commited on
Commit
c510d40
Β·
1 Parent(s): 99605e4
Files changed (1) hide show
  1. app.py +456 -361
app.py CHANGED
@@ -3,11 +3,13 @@ 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
 
12
  BASE = "http://159.69.3.189"
13
  LOGIN_URL = f"{BASE}/login"
@@ -15,20 +17,37 @@ GET_RANGE_URL = f"{BASE}/portal/sms/received/getsms"
15
  GET_NUMBER_URL = f"{BASE}/portal/sms/received/getsms/number"
16
  GET_SMS_URL = f"{BASE}/portal/sms/received/getsms/number/sms"
17
 
18
- USERNAME = "rezaciledug35@gmail.com"
19
- PASSWORD = "Ciledug577"
 
20
 
21
- session = httpx.Client(follow_redirects=True, timeout=30.0)
22
- sent_cache = set()
23
- csrf_token = None
24
- LOGIN_SUCCESS = False
 
 
 
 
25
 
26
- CUSTOM_DOMAIN = "https://fourstore-tele.hf.space"
 
 
27
 
28
- sms_cache = {}
29
- sms_counter = {}
30
- range_counter = {}
31
- otp_logs = deque(maxlen=100)
 
 
 
 
 
 
 
 
 
 
32
  sse_clients = []
33
 
34
  def get_wib_time():
@@ -38,153 +57,147 @@ def get_search_date():
38
  wib = get_wib_time()
39
  return (wib - timedelta(days=1)).strftime("%Y-%m-%d") if wib.hour < 7 else wib.strftime("%Y-%m-%d")
40
 
41
- def login():
42
- global csrf_token, LOGIN_SUCCESS
43
  try:
44
- print("\n" + "="*50)
45
- print("πŸ” PROSES LOGIN DIMULAI")
46
- print("="*50)
47
-
48
  r = session.get(LOGIN_URL, timeout=30)
49
- print(f"πŸ“Š Status code: {r.status_code}")
50
-
51
  soup = BeautifulSoup(r.text, "html.parser")
52
  token = soup.find("input", {"name": "_token"})
53
-
54
  if not token:
55
- print("❌ CSRF Token TIDAK DITEMUKAN!")
56
- return False
57
-
58
- csrf_token = token.get("value")
59
- print(f"βœ… CSRF Token: {csrf_token[:20]}...")
60
 
 
61
  r = session.post(LOGIN_URL, data={
62
  "_token": csrf_token,
63
- "email": USERNAME,
64
- "password": PASSWORD
65
  }, timeout=30)
66
 
67
  if "dashboard" in r.text.lower() or "logout" in r.text.lower():
68
- LOGIN_SUCCESS = True
69
- print("βœ…βœ…βœ… LOGIN BERHASIL! βœ…βœ…βœ…")
70
- return True
 
 
 
71
  else:
72
- LOGIN_SUCCESS = False
73
- print("❌❌❌ LOGIN GAGAL! ❌❌❌")
74
- return False
75
-
76
  except Exception as e:
77
- LOGIN_SUCCESS = False
78
- print(f"❌ EXCEPTION: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  return False
80
 
81
- def get_ranges_with_count():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  try:
83
- if not LOGIN_SUCCESS or not csrf_token:
84
- return []
85
-
86
  date = get_search_date()
87
- print(f"\nπŸ” AMBIL RANGE - Tanggal: {date}")
88
-
89
- r = session.post(GET_RANGE_URL, data={
90
- "_token": csrf_token,
91
  "from": date,
92
  "to": date
93
  }, timeout=15)
94
-
95
  soup = BeautifulSoup(r.text, "html.parser")
96
  ranges_data = []
97
-
98
- items = soup.select(".item")
99
- print(f"πŸ“¦ Total items: {len(items)}")
100
-
101
- for item in items:
102
- try:
103
- card = item.select_one(".card[onclick]")
104
- if card:
105
- onclick = card.get("onclick", "")
106
- match = re.search(r"getDetials\('(.+?)'\)", onclick)
107
- if match:
108
- rng = match.group(1)
109
- else:
110
- name_div = item.select_one(".col-sm-4")
111
- rng = name_div.get_text(strip=True) if name_div else "UNKNOWN"
112
- else:
113
- name_div = item.select_one(".col-sm-4")
114
- rng = name_div.get_text(strip=True) if name_div else "UNKNOWN"
115
-
116
- count_p = item.select_one(".col-3 .mb-0.pb-0")
117
- count = 0
118
- if count_p:
119
- try:
120
- count = int(count_p.get_text(strip=True))
121
- except:
122
- count = 0
123
-
124
- ranges_data.append({
125
- "name": rng,
126
- "count": count
127
- })
128
- print(f" βœ… {rng} - {count} SMS")
129
-
130
- except Exception as e:
131
- print(f" ❌ Error parse: {e}")
132
- continue
133
-
134
  return ranges_data
135
-
136
- except Exception as e:
137
- print(f"❌ Error get_ranges: {e}")
138
  return []
139
 
140
- def get_numbers_with_count(rng):
 
 
 
 
141
  try:
142
- if not LOGIN_SUCCESS or not csrf_token:
143
- return []
144
-
145
  date = get_search_date()
146
- print(f"\n πŸ“ž AMBIL NOMOR: {rng}")
147
-
148
- r = session.post(GET_NUMBER_URL, data={
149
- "_token": csrf_token,
150
  "start": date,
151
  "end": date,
152
  "range": rng
153
  }, timeout=15)
154
-
155
  soup = BeautifulSoup(r.text, "html.parser")
156
  numbers_data = []
157
-
158
- # METHOD 1: Extract dari onclick attribute
159
  for div in soup.find_all("div", onclick=True):
160
  onclick = div.get("onclick", "")
161
- match = re.search(r"getDetialsNumber\w+\('(\d+)',\s*'(\d+)'\)", onclick)
 
 
 
 
 
162
  if match:
163
  num = match.group(1)
164
- id_number = match.group(2)
165
-
166
- parent = div.find_parent("div", class_="card")
167
- count = 0
168
- if parent:
169
- p_tag = parent.find("p", class_="mb-0 pb-0")
170
- if p_tag:
171
- try:
172
- count = int(p_tag.get_text(strip=True))
173
- except:
174
- count = 0
175
-
176
- numbers_data.append({
177
- "number": num,
178
- "count": count
179
- })
180
- print(f" βœ… {mask_number(num)} - {count} SMS")
181
-
182
- # METHOD 2: Extract dari teks div
183
- if len(numbers_data) == 0:
184
- for div in soup.find_all("div", class_="col-sm-4"):
185
- text = div.get_text(strip=True)
186
- if re.match(r'^\d{10,}$', text):
187
- num = text
188
  parent = div.find_parent("div", class_="card")
189
  count = 0
190
  if parent:
@@ -194,109 +207,68 @@ def get_numbers_with_count(rng):
194
  count = int(p_tag.get_text(strip=True))
195
  except:
196
  count = 0
197
-
198
  numbers_data.append({
199
  "number": num,
200
  "count": count
201
  })
202
- print(f" βœ… {mask_number(num)} - {count} SMS")
203
-
204
  return numbers_data
205
-
206
- except Exception as e:
207
- print(f" ❌ Error get_numbers: {e}")
208
  return []
209
 
210
- def get_sms_fast(rng, number):
 
 
 
 
211
  try:
212
- if not LOGIN_SUCCESS or not csrf_token:
213
- return []
214
-
215
  date = get_search_date()
216
  cache_key = f"{rng}-{number}"
217
 
218
- if cache_key in sms_cache:
219
- timestamp, results = sms_cache[cache_key]
220
  if time.time() - timestamp < 5:
221
  return results
222
-
223
- print(f" πŸ“¨ AMBIL SMS: {mask_number(number)}")
224
- print(f" πŸ“€ Range: '{rng}'")
225
-
226
- r = session.post(GET_SMS_URL, data={
227
- "_token": csrf_token,
228
  "start": date,
229
  "end": date,
230
  "Number": number,
231
  "Range": rng
232
- }, timeout=30)
233
-
234
  soup = BeautifulSoup(r.text, "html.parser")
235
  results = []
236
-
237
- cards = soup.select("div.card.card-body")
238
-
239
- for card in cards:
240
  try:
241
  service = "UNKNOWN"
242
  service_div = card.select_one("div.col-sm-4")
243
  if service_div:
244
  raw = service_div.get_text(strip=True)
245
  service = map_service(raw)
246
-
247
  msg_p = card.find("p", class_="mb-0 pb-0")
248
  if msg_p:
249
  sms = msg_p.get_text(strip=True)
250
  otp = extract_otp(sms)
251
  if otp:
252
  results.append((service, sms, otp))
253
- print(f" βœ… OTP: {otp} - {service}")
254
  except:
255
  continue
256
-
257
- sms_cache[cache_key] = (time.time(), results)
258
  return results
259
-
260
- except Exception as e:
261
- print(f" ❌ Error get_sms: {e}")
262
  return []
263
 
264
- def extract_otp(text):
265
- if not text: return None
266
-
267
- # Cari pola 3 digit - 3 digit (530-736)
268
- m = re.search(r"\b(\d{3}[- ]?\d{3})\b", text)
269
- if m:
270
- return m.group(0).replace("-", "").replace(" ", "")
271
 
272
- # Cari pola 4-6 digit biasa
273
- m = re.search(r"\b(\d{4,6})\b", text)
274
- if m:
275
- return m.group(0)
276
-
277
- return None
278
-
279
- def clean_country(rng):
280
- return re.sub(r"\s*\d+$", "", rng).strip() if rng else "UNKNOWN"
281
-
282
- def mask_number(number):
283
- if not number: return "UNKNOWN"
284
- clean = re.sub(r"[^\d+]", "", number)
285
- if len(clean) <= 6: return clean
286
- return f"{clean[:4]}****{clean[-3:]}"
287
-
288
- def map_service(raw):
289
- if not raw: return "UNKNOWN"
290
- s = raw.lower().strip()
291
- if 'whatsapp' in s: return "WHATSAPP"
292
- if 'telegram' in s: return "TELEGRAM"
293
- if 'google' in s or 'gmail' in s: return "GOOGLE"
294
- if 'facebook' in s or 'fb' in s: return "FACEBOOK"
295
- if 'instagram' in s or 'ig' in s: return "INSTAGRAM"
296
- if 'tiktok' in s: return "TIKTOK"
297
- return raw.upper()
298
-
299
- def add_otp_log(country, number, service, otp, sms):
300
  wib = get_wib_time()
301
  log_entry = {
302
  "time": wib.strftime("%H:%M:%S"),
@@ -304,10 +276,20 @@ def add_otp_log(country, number, service, otp, sms):
304
  "number": number,
305
  "service": service,
306
  "otp": otp,
307
- "sms": sms[:80] + "..." if len(sms) > 80 else sms
 
 
308
  }
309
- otp_logs.appendleft(log_entry)
 
 
 
 
 
 
 
310
  broadcast_sse(log_entry)
 
311
  return log_entry
312
 
313
  def broadcast_sse(data):
@@ -321,27 +303,46 @@ def broadcast_sse(data):
321
  for q in dead:
322
  sse_clients.remove(q)
323
 
324
- app = Flask('')
325
-
326
  @app.route('/')
327
  def home():
328
- wib = get_wib_time()
329
- search_date = get_search_date()
 
 
 
 
 
 
 
 
330
  search_query = request.args.get('q', '').lower()
331
  filter_service = request.args.get('service', '')
 
332
 
333
- filtered_logs = list(otp_logs)
334
  if search_query:
335
- filtered_logs = [log for log in filtered_logs if
336
- search_query in log['country'].lower() or
337
- search_query in log['number'].lower() or
338
- search_query in log['otp'].lower() or
339
- search_query in log['sms'].lower()]
340
-
341
  if filter_service:
342
- filtered_logs = [log for log in filtered_logs if log['service'] == filter_service]
 
 
 
 
 
343
 
344
- all_services = list(set([log['service'] for log in otp_logs]))
 
 
 
 
 
 
 
 
345
 
346
  html = f"""
347
  <!DOCTYPE html>
@@ -349,56 +350,60 @@ def home():
349
  <head>
350
  <meta charset="UTF-8">
351
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
352
- <title>OTP DASHBOARD Β· FOURSTORE</title>
353
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
354
  <style>
355
  * {{ margin: 0; padding: 0; box-sizing: border-box; }}
356
  body {{ font-family: 'Inter', sans-serif; background: #0a0c10; color: #e4e6eb; padding: 24px; }}
357
- .container {{ max-width: 1400px; margin: 0 auto; }}
358
  .header {{ background: linear-gradient(145deg, #1a1f2c, #0f131c); border-radius: 24px; padding: 28px; margin-bottom: 28px; border: 1px solid #2d3540; }}
359
- .header-top {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }}
360
  .title h1 {{ font-size: 28px; font-weight: 700; background: linear-gradient(135deg, #00f2fe, #4facfe); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }}
361
  .title p {{ color: #8b949e; font-size: 14px; }}
362
- .status-badge {{ padding: 10px 24px; border-radius: 100px; font-weight: 600; font-size: 14px;
363
- background: {'#0a4d3c' if LOGIN_SUCCESS else '#4a2c2c'};
364
- color: {'#a0f0d0' if LOGIN_SUCCESS else '#ffb3b3'};
365
- border: 1px solid {'#1a6e5a' if LOGIN_SUCCESS else '#6e3a3a'}; }}
366
  .stats-grid {{ display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-top: 20px; }}
367
  .stat-card {{ background: #1a1f2c; padding: 20px; border-radius: 20px; border: 1px solid #2d3540; }}
368
  .stat-label {{ color: #8b949e; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }}
369
  .stat-value {{ font-size: 32px; font-weight: 700; color: #00f2fe; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  .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; }}
371
  .search-box {{ flex: 2; min-width: 280px; position: relative; }}
372
  .search-icon {{ position: absolute; left: 16px; top: 14px; color: #8b949e; }}
373
  .search-input {{ width: 100%; padding: 14px 20px 14px 48px; background: #1a1f2c; border: 1px solid #2d3540; border-radius: 100px; color: #e4e6eb; font-size: 15px; }}
374
- .search-input:focus {{ outline: none; border-color: #00f2fe; background: #1e2433; box-shadow: 0 0 0 3px #00f2fe20; }}
375
- .filter-box {{ flex: 1; min-width: 180px; }}
376
  .filter-select {{ width: 100%; padding: 14px 20px; background: #1a1f2c; border: 1px solid #2d3540; border-radius: 100px; color: #e4e6eb; font-size: 15px; cursor: pointer; }}
377
- .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; display: inline-block; }}
378
- .clear-btn:hover {{ background: #3d4a5a; }}
379
- .result-count {{ color: #8b949e; font-size: 13px; margin-left: 8px; }}
380
- .otp-section {{ background: #0f131c; border-radius: 24px; padding: 28px; border: 1px solid #2d3540; }}
381
- .section-title {{ font-size: 18px; font-weight: 600; color: #e4e6eb; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; }}
382
- .section-title span {{ background: #00f2fe20; padding: 6px 12px; border-radius: 100px; font-size: 12px; color: #00f2fe; }}
383
- table {{ width: 100%; border-collapse: collapse; }}
384
  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; }}
385
  td {{ padding: 16px 12px; border-bottom: 1px solid #262c38; font-size: 14px; }}
386
- .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; display: inline-block; user-select: all; cursor: pointer; }}
387
- .otp-badge:hover {{ background: #003d4a; border-color: #00f2fe; transform: scale(1.05); }}
388
  .service-badge {{ background: #2d3a4a; padding: 6px 14px; border-radius: 100px; font-size: 12px; font-weight: 600; display: inline-block; }}
389
- .service-badge.whatsapp {{ background: #25D36620; color: #25D366; border: 1px solid #25D36640; }}
390
- .service-badge.telegram {{ background: #26A5E420; color: #26A5E4; border: 1px solid #26A5E440; }}
391
- .service-badge.google {{ background: #EA433520; color: #EA4335; border: 1px solid #EA433540; }}
392
- .service-badge.facebook {{ background: #4267B220; color: #4267B2; border: 1px solid #4267B240; }}
393
- .service-badge.instagram {{ background: #E4405F20; color: #E4405F; border: 1px solid #E4405F40; }}
394
- .service-badge.tiktok {{ background: #00000020; color: #ffffff; border: 1px solid #ffffff40; }}
395
- .number {{ font-family: monospace; color: #e4e6eb; }}
396
- .empty-state {{ text-align: center; padding: 60px; color: #8b949e; }}
397
- .highlight {{ background: #00f2fe30; border-radius: 4px; padding: 0 2px; }}
398
  .new-row {{ animation: fadeIn 0.3s ease; background: linear-gradient(90deg, #00f2fe10, transparent); }}
399
- .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; box-shadow: 0 8px 20px rgba(0,242,254,0.3); }}
400
  @keyframes fadeIn {{ from {{ opacity: 0; transform: translateY(-10px); }} to {{ opacity: 1; transform: translateY(0); }} }}
401
- @keyframes slideIn {{ from {{ opacity: 0; transform: translateX(30px); }} to {{ opacity: 1; transform: translateX(0); }} }}
402
  </style>
403
  </head>
404
  <body>
@@ -406,46 +411,64 @@ def home():
406
  <div class="header">
407
  <div class="header-top">
408
  <div class="title">
409
- <h1>OTP MONITOR Β· FOURSTORE</h1>
410
  <p>{CUSTOM_DOMAIN}</p>
411
  </div>
412
- <div class="status-badge">
413
- {'● ONLINE' if LOGIN_SUCCESS else 'β—‹ OFFLINE'}
 
 
 
414
  </div>
415
  </div>
416
- <div class="stats-grid">
417
- <div class="stat-card"><div class="stat-label">Total OTP</div><div class="stat-value" id="total-otp">{len(sent_cache)}</div></div>
418
- <div class="stat-card"><div class="stat-label">Today</div><div class="stat-value" id="today-otp">{len(otp_logs)}</div></div>
419
- <div class="stat-card"><div class="stat-label">WIB</div><div class="stat-value" id="wib-time">{wib.strftime('%H:%M:%S')}</div></div>
420
- <div class="stat-card"><div class="stat-label">Date</div><div class="stat-value">{search_date}</div></div>
421
- </div>
 
 
 
 
 
 
 
 
 
422
  </div>
423
 
424
  <div class="search-section">
425
  <div class="search-box">
426
  <span class="search-icon">πŸ”</span>
427
- <form action="/" method="get" style="margin: 0;" id="searchForm">
428
- <input type="text" class="search-input" name="q" placeholder="Cari negara, nomor, OTP, atau SMS..." value="{request.args.get('q', '')}" autocomplete="off">
429
  </form>
430
  </div>
431
  <div class="filter-box">
432
- <select class="filter-select" name="service" onchange="window.location.href='/?' + (document.getElementById('searchForm').q.value ? 'q=' + document.getElementById('searchForm').q.value + '&' : '') + 'service=' + this.value">
433
  <option value="">πŸ“‹ Semua Service</option>
434
  {''.join([f'<option value="{s}" {"selected" if filter_service == s else ""}>πŸ“± {s}</option>' for s in sorted(all_services)])}
435
  </select>
436
  </div>
437
- <a href="/" class="clear-btn">βœ• Clear</a>
438
- <span class="result-count">πŸ“Š {len(filtered_logs)} results</span>
 
 
 
 
 
 
439
  </div>
440
 
441
  <div class="otp-section">
442
- <div class="section-title">πŸ“¨ OTP TERBARU <span>LIVE</span></div>
443
- <div style="overflow-x: auto;">
444
- <table>
445
- <thead><tr><th>WIB</th><th>Country</th><th>Number</th><th>Service</th><th>OTP</th><th>Message</th></tr></thead>
446
- <tbody id="otp-table-body">{generate_filtered_rows(filtered_logs, search_query)}</tbody>
447
- </table>
448
- </div>
449
  </div>
450
  </div>
451
 
@@ -456,49 +479,46 @@ def home():
456
  eventSource.onmessage = function(e) {{
457
  try {{
458
  const data = JSON.parse(e.data);
459
- if (data.otp) {{
460
- addRow(data);
461
- updateStats();
462
- }}
463
- }} catch (err) {{}}
464
  }};
465
  eventSource.onerror = function() {{ setTimeout(connectSSE, 3000); }};
466
  }}
467
 
468
- function addRow(data) {{
469
- const tbody = document.getElementById('otp-table-body');
470
- if (tbody.children.length === 1 && tbody.children[0].innerHTML.includes('Tidak ada hasil')) tbody.innerHTML = '';
471
- const row = tbody.insertRow(0);
472
- row.className = 'new-row';
473
- const serviceClass = data.service.toLowerCase().replace(' ', '');
474
- 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 style="max-width: 350px; overflow: hidden; text-overflow: ellipsis;" title="${{data.sms}}">${{data.sms}}</div></td>`;
475
- while (tbody.children.length > 50) tbody.removeChild(tbody.lastChild);
 
 
 
 
 
 
 
476
  }}
477
 
478
  function copyOTP(otp) {{
479
  navigator.clipboard.writeText(otp).then(() => {{
480
- const toast = document.createElement('div'); toast.className = 'toast'; toast.textContent = 'βœ… OTP copied!';
481
- document.body.appendChild(toast); setTimeout(() => toast.remove(), 2000);
 
 
 
482
  }});
483
  }}
484
 
485
- function updateStats() {{
486
- const totalEl = document.getElementById('total-otp');
487
- const todayEl = document.getElementById('today-otp');
488
- if (totalEl) totalEl.textContent = parseInt(totalEl.textContent || 0) + 1;
489
- if (todayEl) todayEl.textContent = parseInt(todayEl.textContent || 0) + 1;
490
- }}
491
-
492
  function updateTime() {{
493
- const now = new Date(); now.setHours(now.getHours() + 7);
 
494
  const wibEl = document.getElementById('wib-time');
495
  if (wibEl) wibEl.textContent = now.toISOString().substr(11, 8);
496
  }}
497
 
498
- document.querySelector('.search-input')?.addEventListener('keypress', function(e) {{
499
- if (e.key === 'Enter') document.getElementById('searchForm').submit();
500
- }});
501
-
502
  connectSSE();
503
  setInterval(updateTime, 1000);
504
  </script>
@@ -507,36 +527,108 @@ def home():
507
  """
508
  return html
509
 
510
- def generate_filtered_rows(filtered_logs, search_query=""):
511
- if not filtered_logs:
512
- return '<tr><td colspan="6" class="empty-state"><h3>πŸ” Tidak ada hasil</h3><p>Coba kata kunci lain atau reset filter</p></td></tr>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
 
514
  rows = ""
515
- for entry in filtered_logs[:50]:
516
- country = entry["country"]
517
- number = entry["number"]
518
- otp = entry["otp"]
519
- sms = entry["sms"]
 
 
520
 
521
  if search_query:
522
- country = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', country, flags=re.I)
523
  number = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', number, flags=re.I)
524
  otp = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', otp, flags=re.I)
525
- sms = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', sms, flags=re.I)
526
 
527
- service_class = entry["service"].lower().replace(' ', '')
528
  rows += f'''
529
- <tr>
530
- <td style="color: #00f2fe;">{entry["time"]}</td>
 
531
  <td>{country}</td>
532
  <td><span class="number">{number}</span></td>
533
- <td><span class="service-badge {service_class}">{entry["service"]}</span></td>
534
- <td><span class="otp-badge" onclick="copyOTP('{entry["otp"]}')">{otp}</span></td>
535
- <td><div style="max-width: 350px; overflow: hidden; text-overflow: ellipsis;" title="{entry["sms"]}">{sms}</div></td>
536
  </tr>
537
  '''
538
  return rows
539
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
  @app.route('/stream')
541
  def stream():
542
  def generate():
@@ -550,105 +642,108 @@ def stream():
550
  sse_clients.remove(q)
551
  return Response(generate(), mimetype="text/event-stream")
552
 
553
- def run_server():
554
- app.run(host='0.0.0.0', port=7860, debug=False, threaded=True)
555
-
556
- Thread(target=run_server, daemon=True).start()
557
-
558
- def main():
559
- print("\n" + "="*60)
560
- print(" πŸ”₯ OTP BOT - FOURSTORE HUGGING FACE")
561
- print(" ⚑ PORT: 7860")
562
- print(f" 🌐 DOMAIN: {CUSTOM_DOMAIN}")
563
- print(" πŸ“‹ TANPA INTEGRASI TELEGRAM")
564
- print("="*60 + "\n")
565
-
566
- login_attempts = 0
567
- while login_attempts < 5:
568
- if login():
569
- break
570
- login_attempts += 1
571
- print(f"⏳ Login gagal, percobaan ke-{login_attempts}/5...")
572
- time.sleep(3)
573
 
574
- last_cleanup = time.time()
575
- loop_count = 0
576
 
577
- while True:
578
  try:
579
- loop_count += 1
580
- print(f"\n{'='*50}")
581
- print(f"πŸ”„ LOOP KE-{loop_count}")
582
- print(f"{'='*50}")
583
-
584
- if not LOGIN_SUCCESS or not csrf_token:
585
- print("⚠️ Session expired, login ulang...")
586
- login()
587
- time.sleep(2)
588
- continue
589
 
590
- if time.time() - last_cleanup > 300:
591
- sms_cache.clear()
592
- sms_counter.clear()
593
- range_counter.clear()
594
- print("🧹 Cache cleared (5 menit)")
595
- last_cleanup = time.time()
596
-
597
- ranges_data = get_ranges_with_count()
598
-
599
- if not ranges_data:
600
- print("⚠️ TIDAK ADA DATA RANGE!")
601
- time.sleep(5)
602
- continue
603
 
604
  for range_item in ranges_data:
 
 
 
605
  rng = range_item["name"]
606
  current_count = range_item["count"]
607
- prev_count = range_counter.get(rng, 0)
608
 
609
  if current_count > prev_count:
610
  country = clean_country(rng)
611
- print(f"\nπŸ”₯ RANGE BERUBAH: {country}")
612
- print(f" πŸ“Š {prev_count} β†’ {current_count} SMS (baru: {current_count - prev_count})")
613
- range_counter[rng] = current_count
614
 
615
- numbers_data = get_numbers_with_count(rng)
616
 
617
  for number_item in numbers_data:
 
 
 
618
  num = number_item["number"]
619
  num_count = number_item["count"]
620
  key = f"{rng}-{num}"
621
- prev_num_count = sms_counter.get(key, 0)
622
 
623
  if num_count > prev_num_count:
624
- print(f" πŸ“± Nomor: {mask_number(num)}")
625
- print(f" πŸ“¨ {prev_num_count} β†’ {num_count} SMS (baru: {num_count - prev_num_count})")
626
- sms_counter[key] = num_count
627
 
628
- all_sms = get_sms_fast(rng, num)
629
  new_sms = all_sms[prev_num_count:]
630
 
631
  for service, sms, otp in new_sms:
632
  if otp:
633
  sms_id = f"{rng}-{num}-{otp}"
634
- if sms_id not in sent_cache:
635
  masked = mask_number(num)
636
- sent_cache.add(sms_id)
637
- add_otp_log(country, masked, service, otp, sms)
638
- print(f" βœ… OTP: {otp} - {service}")
 
 
 
 
 
639
 
640
- print(f" βœ… Selesai proses {country}")
641
  time.sleep(0.5)
642
 
643
- print("\n⏳ Tidur 2 detik...")
644
  time.sleep(2)
645
 
646
  except Exception as e:
647
- print(f"❌ Error: {e}")
648
- time.sleep(3)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
649
 
650
  if __name__ == "__main__":
651
  try:
652
  main()
653
  except KeyboardInterrupt:
654
- 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
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
 
14
  BASE = "http://159.69.3.189"
15
  LOGIN_URL = f"{BASE}/login"
 
17
  GET_NUMBER_URL = f"{BASE}/portal/sms/received/getsms/number"
18
  GET_SMS_URL = f"{BASE}/portal/sms/received/getsms/number/sms"
19
 
20
+ TELEGRAM_PROXY_URL = "https://danihitambangetjir.termai.cc/api/proxy"
21
+ CUSTOM_DOMAIN = "https://fourstore-tele.hf.space"
22
+ ACCOUNTS_FILE = "accounts.json"
23
 
24
+ def load_accounts():
25
+ if os.path.exists(ACCOUNTS_FILE):
26
+ try:
27
+ with open(ACCOUNTS_FILE, 'r') as f:
28
+ return json.load(f)
29
+ except:
30
+ return {}
31
+ return {}
32
 
33
+ def save_accounts(accounts):
34
+ with open(ACCOUNTS_FILE, 'w') as f:
35
+ json.dump(accounts, f, indent=2)
36
 
37
+ accounts = load_accounts()
38
+ for acc_id in accounts:
39
+ accounts[acc_id]["session"] = None
40
+ accounts[acc_id]["csrf"] = None
41
+ accounts[acc_id]["status"] = False
42
+ accounts[acc_id]["otp_logs"] = []
43
+ accounts[acc_id]["sent_cache"] = []
44
+ accounts[acc_id]["sms_cache"] = {}
45
+ accounts[acc_id]["sms_counter"] = {}
46
+ accounts[acc_id]["range_counter"] = {}
47
+ accounts[acc_id]["last_cleanup"] = time.time()
48
+
49
+ app = Flask('')
50
+ app.secret_key = "fourstore-secret-key-123"
51
  sse_clients = []
52
 
53
  def get_wib_time():
 
57
  wib = get_wib_time()
58
  return (wib - timedelta(days=1)).strftime("%Y-%m-%d") if wib.hour < 7 else wib.strftime("%Y-%m-%d")
59
 
60
+ def login_account(account_id, username, password):
 
61
  try:
62
+ session = httpx.Client(follow_redirects=True, timeout=30.0)
 
 
 
63
  r = session.get(LOGIN_URL, timeout=30)
 
 
64
  soup = BeautifulSoup(r.text, "html.parser")
65
  token = soup.find("input", {"name": "_token"})
 
66
  if not token:
67
+ return False, "Token tidak ditemukan"
 
 
 
 
68
 
69
+ csrf_token = token.get("value")
70
  r = session.post(LOGIN_URL, data={
71
  "_token": csrf_token,
72
+ "email": username,
73
+ "password": password
74
  }, timeout=30)
75
 
76
  if "dashboard" in r.text.lower() or "logout" in r.text.lower():
77
+ accounts[account_id]["session"] = session
78
+ accounts[account_id]["csrf"] = csrf_token
79
+ accounts[account_id]["status"] = True
80
+ accounts[account_id]["last_login"] = time.time()
81
+ save_accounts(accounts)
82
+ return True, "Login berhasil"
83
  else:
84
+ return False, "Login gagal"
 
 
 
85
  except Exception as e:
86
+ return False, str(e)
87
+
88
+ def tg_send(account_id, msg):
89
+ try:
90
+ account = accounts.get(account_id)
91
+ if not account or not account.get("bot_token") or not account.get("chat_id"):
92
+ return False
93
+
94
+ url = f"{TELEGRAM_PROXY_URL}?url=https://api.telegram.org/bot{account['bot_token']}/sendMessage"
95
+ payload = {
96
+ "chat_id": account['chat_id'],
97
+ "text": msg,
98
+ "parse_mode": "Markdown"
99
+ }
100
+ httpx.post(url, json=payload, timeout=30)
101
+ return True
102
+ except:
103
  return False
104
 
105
+ def clean_country(rng):
106
+ return re.sub(r"\s*\d+$", "", rng).strip() if rng else "UNKNOWN"
107
+
108
+ def mask_number(number):
109
+ if not number: return "UNKNOWN"
110
+ clean = re.sub(r"[^\d+]", "", number)
111
+ if len(clean) <= 6: return clean
112
+ return f"{clean[:4]}****{clean[-3:]}"
113
+
114
+ def map_service(raw):
115
+ if not raw: return "UNKNOWN"
116
+ s = raw.lower().strip()
117
+ if 'whatsapp' in s: return "WHATSAPP"
118
+ if 'telegram' in s: return "TELEGRAM"
119
+ if 'google' in s or 'gmail' in s: return "GOOGLE"
120
+ if 'facebook' in s or 'fb' in s: return "FACEBOOK"
121
+ if 'instagram' in s or 'ig' in s: return "INSTAGRAM"
122
+ if 'tiktok' in s: return "TIKTOK"
123
+ if 'temu' in s: return "TEMU"
124
+ return raw.upper()
125
+
126
+ def extract_otp(text):
127
+ if not text: return None
128
+ m = re.search(r"\b(\d{3}[- ]?\d{3})\b", text)
129
+ if m:
130
+ return m.group(0).replace("-", "").replace(" ", "")
131
+ m = re.search(r"\b(\d{4,6})\b", text)
132
+ if m:
133
+ return m.group(0)
134
+ digits = re.findall(r'\d+', text)
135
+ for d in digits:
136
+ if 4 <= len(d) <= 6:
137
+ return d
138
+ return None
139
+
140
+ def get_ranges_with_count(account_id):
141
+ account = accounts.get(account_id)
142
+ if not account or not account.get("session") or not account.get("csrf"):
143
+ return []
144
+
145
  try:
 
 
 
146
  date = get_search_date()
147
+ r = account["session"].post(GET_RANGE_URL, data={
148
+ "_token": account["csrf"],
 
 
149
  "from": date,
150
  "to": date
151
  }, timeout=15)
152
+
153
  soup = BeautifulSoup(r.text, "html.parser")
154
  ranges_data = []
155
+
156
+ for item in soup.select(".item"):
157
+ name_div = item.select_one(".col-sm-4")
158
+ if not name_div: continue
159
+ rng = name_div.get_text(strip=True)
160
+
161
+ count_p = item.select_one(".col-3 .mb-0.pb-0")
162
+ count = int(count_p.get_text(strip=True)) if count_p else 0
163
+
164
+ ranges_data.append({
165
+ "name": rng,
166
+ "count": count
167
+ })
168
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  return ranges_data
170
+ except:
 
 
171
  return []
172
 
173
+ def get_numbers_with_count(account_id, rng):
174
+ account = accounts.get(account_id)
175
+ if not account or not account.get("session") or not account.get("csrf"):
176
+ return []
177
+
178
  try:
 
 
 
179
  date = get_search_date()
180
+ r = account["session"].post(GET_NUMBER_URL, data={
181
+ "_token": account["csrf"],
 
 
182
  "start": date,
183
  "end": date,
184
  "range": rng
185
  }, timeout=15)
186
+
187
  soup = BeautifulSoup(r.text, "html.parser")
188
  numbers_data = []
189
+
 
190
  for div in soup.find_all("div", onclick=True):
191
  onclick = div.get("onclick", "")
192
+ match = re.search(r"getDetialsNumber\w*\('?(\d+)'?", onclick)
193
+ if not match:
194
+ match = re.search(r"open_(\d+)", onclick)
195
+ if not match:
196
+ match = re.search(r"'(\d+)'", onclick)
197
+
198
  if match:
199
  num = match.group(1)
200
+ if num and len(num) > 5:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  parent = div.find_parent("div", class_="card")
202
  count = 0
203
  if parent:
 
207
  count = int(p_tag.get_text(strip=True))
208
  except:
209
  count = 0
210
+
211
  numbers_data.append({
212
  "number": num,
213
  "count": count
214
  })
215
+
 
216
  return numbers_data
217
+ except:
 
 
218
  return []
219
 
220
+ def get_sms_fast(account_id, rng, number):
221
+ account = accounts.get(account_id)
222
+ if not account or not account.get("session") or not account.get("csrf"):
223
+ return []
224
+
225
  try:
 
 
 
226
  date = get_search_date()
227
  cache_key = f"{rng}-{number}"
228
 
229
+ if cache_key in account["sms_cache"]:
230
+ timestamp, results = account["sms_cache"][cache_key]
231
  if time.time() - timestamp < 5:
232
  return results
233
+
234
+ r = account["session"].post(GET_SMS_URL, data={
235
+ "_token": account["csrf"],
 
 
 
236
  "start": date,
237
  "end": date,
238
  "Number": number,
239
  "Range": rng
240
+ }, timeout=20)
241
+
242
  soup = BeautifulSoup(r.text, "html.parser")
243
  results = []
244
+
245
+ for card in soup.select("div.card.card-body"):
 
 
246
  try:
247
  service = "UNKNOWN"
248
  service_div = card.select_one("div.col-sm-4")
249
  if service_div:
250
  raw = service_div.get_text(strip=True)
251
  service = map_service(raw)
252
+
253
  msg_p = card.find("p", class_="mb-0 pb-0")
254
  if msg_p:
255
  sms = msg_p.get_text(strip=True)
256
  otp = extract_otp(sms)
257
  if otp:
258
  results.append((service, sms, otp))
 
259
  except:
260
  continue
261
+
262
+ account["sms_cache"][cache_key] = (time.time(), results)
263
  return results
264
+ except:
 
 
265
  return []
266
 
267
+ def add_otp_log(account_id, country, number, service, otp, sms):
268
+ account = accounts.get(account_id)
269
+ if not account:
270
+ return
 
 
 
271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  wib = get_wib_time()
273
  log_entry = {
274
  "time": wib.strftime("%H:%M:%S"),
 
276
  "number": number,
277
  "service": service,
278
  "otp": otp,
279
+ "sms": sms[:80] + "..." if len(sms) > 80 else sms,
280
+ "account_id": account_id,
281
+ "account_username": account.get("username", "Unknown")
282
  }
283
+
284
+ if "otp_logs" not in account:
285
+ account["otp_logs"] = []
286
+
287
+ account["otp_logs"].insert(0, log_entry)
288
+ if len(account["otp_logs"]) > 100:
289
+ account["otp_logs"] = account["otp_logs"][:100]
290
+
291
  broadcast_sse(log_entry)
292
+ save_accounts(accounts)
293
  return log_entry
294
 
295
  def broadcast_sse(data):
 
303
  for q in dead:
304
  sse_clients.remove(q)
305
 
 
 
306
  @app.route('/')
307
  def home():
308
+ all_logs = []
309
+ for acc_id, acc in accounts.items():
310
+ if acc.get("otp_logs"):
311
+ for log in acc["otp_logs"]:
312
+ log_copy = log.copy()
313
+ log_copy["account_username"] = acc.get("username", "Unknown")
314
+ all_logs.append(log_copy)
315
+
316
+ all_logs = sorted(all_logs, key=lambda x: x.get("time", ""), reverse=True)
317
+
318
  search_query = request.args.get('q', '').lower()
319
  filter_service = request.args.get('service', '')
320
+ filter_account = request.args.get('account', '')
321
 
 
322
  if search_query:
323
+ all_logs = [log for log in all_logs if
324
+ search_query in log.get('country', '').lower() or
325
+ search_query in log.get('number', '').lower() or
326
+ search_query in log.get('otp', '').lower() or
327
+ search_query in log.get('sms', '').lower()]
328
+
329
  if filter_service:
330
+ all_logs = [log for log in all_logs if log.get('service') == filter_service]
331
+
332
+ if filter_account:
333
+ all_logs = [log for log in all_logs if log.get('account_id') == filter_account]
334
+
335
+ all_services = list(set([log.get('service') for log in all_logs if log.get('service')]))
336
 
337
+ accounts_list = []
338
+ for acc_id, acc in accounts.items():
339
+ accounts_list.append({
340
+ "id": acc_id,
341
+ "username": acc.get("username", "Unknown"),
342
+ "status": acc.get("status", False),
343
+ "bot": "βœ…" if acc.get("bot_token") else "❌",
344
+ "chat": acc.get("chat_id", "-")[:5] + "..." if acc.get("chat_id") and len(acc.get("chat_id")) > 5 else acc.get("chat_id", "-")
345
+ })
346
 
347
  html = f"""
348
  <!DOCTYPE html>
 
350
  <head>
351
  <meta charset="UTF-8">
352
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
353
+ <title>OTP MULTI ACCOUNT Β· FOURSTORE</title>
354
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
355
  <style>
356
  * {{ margin: 0; padding: 0; box-sizing: border-box; }}
357
  body {{ font-family: 'Inter', sans-serif; background: #0a0c10; color: #e4e6eb; padding: 24px; }}
358
+ .container {{ max-width: 1600px; margin: 0 auto; }}
359
  .header {{ background: linear-gradient(145deg, #1a1f2c, #0f131c); border-radius: 24px; padding: 28px; margin-bottom: 28px; border: 1px solid #2d3540; }}
360
+ .header-top {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 15px; }}
361
  .title h1 {{ font-size: 28px; font-weight: 700; background: linear-gradient(135deg, #00f2fe, #4facfe); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }}
362
  .title p {{ color: #8b949e; font-size: 14px; }}
 
 
 
 
363
  .stats-grid {{ display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-top: 20px; }}
364
  .stat-card {{ background: #1a1f2c; padding: 20px; border-radius: 20px; border: 1px solid #2d3540; }}
365
  .stat-label {{ color: #8b949e; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }}
366
  .stat-value {{ font-size: 32px; font-weight: 700; color: #00f2fe; }}
367
+
368
+ .add-account-form {{ background: #1a1f2c; padding: 20px; border-radius: 16px; margin-bottom: 30px; border: 1px solid #2d3540; }}
369
+ .form-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; }}
370
+ .form-input {{ background: #0a0c10; border: 1px solid #2d3540; padding: 12px 16px; border-radius: 12px; color: #e4e6eb; width: 100%; }}
371
+ .form-input:focus {{ outline: none; border-color: #00f2fe; }}
372
+ .btn {{ background: #00f2fe; color: #0a0c10; border: none; padding: 12px 24px; border-radius: 12px; font-weight: 600; cursor: pointer; transition: all 0.2s; }}
373
+ .btn:hover {{ background: #00d8e4; transform: translateY(-2px); }}
374
+ .btn-danger {{ background: #ff4d4d; color: white; }}
375
+ .btn-small {{ padding: 6px 12px; font-size: 12px; }}
376
+
377
+ .account-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 30px; }}
378
+ .account-card {{ background: #1a1f2c; border-radius: 16px; padding: 20px; border: 1px solid #2d3540; position: relative; }}
379
+ .account-card.online {{ border-color: #00f2fe; box-shadow: 0 0 15px #00f2fe20; }}
380
+ .account-status {{ position: absolute; top: 20px; right: 20px; width: 12px; height: 12px; border-radius: 50%; }}
381
+ .status-online {{ background: #00f2fe; box-shadow: 0 0 10px #00f2fe; }}
382
+ .status-offline {{ background: #ff4d4d; }}
383
+ .account-actions {{ display: flex; gap: 8px; margin-top: 15px; flex-wrap: wrap; }}
384
+
385
  .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; }}
386
  .search-box {{ flex: 2; min-width: 280px; position: relative; }}
387
  .search-icon {{ position: absolute; left: 16px; top: 14px; color: #8b949e; }}
388
  .search-input {{ width: 100%; padding: 14px 20px 14px 48px; background: #1a1f2c; border: 1px solid #2d3540; border-radius: 100px; color: #e4e6eb; font-size: 15px; }}
389
+ .filter-box {{ flex: 1; min-width: 150px; }}
 
390
  .filter-select {{ width: 100%; padding: 14px 20px; background: #1a1f2c; border: 1px solid #2d3540; border-radius: 100px; color: #e4e6eb; font-size: 15px; cursor: pointer; }}
391
+
392
+ .otp-section {{ background: #0f131c; border-radius: 24px; padding: 28px; border: 1px solid #2d3540; overflow-x: auto; }}
393
+ table {{ width: 100%; border-collapse: collapse; min-width: 1000px; }}
 
 
 
 
394
  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; }}
395
  td {{ padding: 16px 12px; border-bottom: 1px solid #262c38; font-size: 14px; }}
396
+ .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; }}
 
397
  .service-badge {{ background: #2d3a4a; padding: 6px 14px; border-radius: 100px; font-size: 12px; font-weight: 600; display: inline-block; }}
398
+ .account-badge {{ background: #4a3a2d; padding: 4px 10px; border-radius: 20px; font-size: 11px; display: inline-block; }}
399
+ .whatsapp {{ background: #25D36620; color: #25D366; border: 1px solid #25D36640; }}
400
+ .telegram {{ background: #26A5E420; color: #26A5E4; border: 1px solid #26A5E440; }}
401
+ .google {{ background: #EA433520; color: #EA4335; border: 1px solid #EA433540; }}
402
+ .facebook {{ background: #4267B220; color: #4267B2; border: 1px solid #4267B240; }}
403
+ .temu {{ background: #FF6B6B20; color: #FF6B6B; border: 1px solid #FF6B6B40; }}
 
 
 
404
  .new-row {{ animation: fadeIn 0.3s ease; background: linear-gradient(90deg, #00f2fe10, transparent); }}
 
405
  @keyframes fadeIn {{ from {{ opacity: 0; transform: translateY(-10px); }} to {{ opacity: 1; transform: translateY(0); }} }}
406
+ .toast {{ position: fixed; bottom: 24px; right: 24px; background: #00f2fe; color: #000; padding: 14px 28px; border-radius: 100px; font-weight: 600; z-index: 9999; }}
407
  </style>
408
  </head>
409
  <body>
 
411
  <div class="header">
412
  <div class="header-top">
413
  <div class="title">
414
+ <h1>OTP MULTI ACCOUNT Β· FOURSTORE</h1>
415
  <p>{CUSTOM_DOMAIN}</p>
416
  </div>
417
+ <div class="stats-grid">
418
+ <div class="stat-card"><div class="stat-label">Total Akun</div><div class="stat-value">{len(accounts)}</div></div>
419
+ <div class="stat-card"><div class="stat-label">Akun Online</div><div class="stat-value">{sum(1 for a in accounts.values() if a.get('status'))}</div></div>
420
+ <div class="stat-card"><div class="stat-label">Total OTP</div><div class="stat-value">{sum(len(a.get('sent_cache', [])) for a in accounts.values())}</div></div>
421
+ <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>
422
  </div>
423
  </div>
424
+ </div>
425
+
426
+ <div class="add-account-form">
427
+ <h3 style="margin-bottom: 15px;">βž• Tambah Akun Baru</h3>
428
+ <form action="/add_account" method="POST" class="form-grid">
429
+ <input type="text" name="username" placeholder="Username/Email" class="form-input" required>
430
+ <input type="password" name="password" placeholder="Password" class="form-input" required>
431
+ <input type="text" name="bot_token" placeholder="Bot Token (opsional)" class="form-input">
432
+ <input type="text" name="chat_id" placeholder="Chat ID (opsional)" class="form-input">
433
+ <button type="submit" class="btn">Tambah Akun</button>
434
+ </form>
435
+ </div>
436
+
437
+ <div class="account-grid">
438
+ {generate_account_cards(accounts_list)}
439
  </div>
440
 
441
  <div class="search-section">
442
  <div class="search-box">
443
  <span class="search-icon">πŸ”</span>
444
+ <form action="/" method="get" id="searchForm">
445
+ <input type="text" class="search-input" name="q" placeholder="Cari OTP..." value="{request.args.get('q', '')}">
446
  </form>
447
  </div>
448
  <div class="filter-box">
449
+ <select class="filter-select" name="service" onchange="window.location.href=updateQuery('service', this.value)">
450
  <option value="">πŸ“‹ Semua Service</option>
451
  {''.join([f'<option value="{s}" {"selected" if filter_service == s else ""}>πŸ“± {s}</option>' for s in sorted(all_services)])}
452
  </select>
453
  </div>
454
+ <div class="filter-box">
455
+ <select class="filter-select" name="account" onchange="window.location.href=updateQuery('account', this.value)">
456
+ <option value="">πŸ‘€ Semua Akun</option>
457
+ {''.join([f'<option value="{a["id"]}" {"selected" if filter_account == a["id"] else ""}>πŸ‘€ {a["username"][:15]}</option>' for a in accounts_list])}
458
+ </select>
459
+ </div>
460
+ <a href="/" class="btn btn-small">Reset</a>
461
+ <span class="result-count">πŸ“Š {len(all_logs)} hasil</span>
462
  </div>
463
 
464
  <div class="otp-section">
465
+ <h3 style="margin-bottom: 20px;">πŸ“¨ OTP TERBARU <span style="background:#00f2fe20; padding:4px 12px; border-radius:100px; font-size:12px;">LIVE</span></h3>
466
+ <table>
467
+ <thead><tr><th>WIB</th><th>Akun</th><th>Country</th><th>Number</th><th>Service</th><th>OTP</th><th>Message</th></tr></thead>
468
+ <tbody id="otp-table-body">
469
+ {generate_otp_rows(all_logs, search_query)}
470
+ </tbody>
471
+ </table>
472
  </div>
473
  </div>
474
 
 
479
  eventSource.onmessage = function(e) {{
480
  try {{
481
  const data = JSON.parse(e.data);
482
+ if (data.otp) location.reload();
483
+ }} catch(err) {{}}
 
 
 
484
  }};
485
  eventSource.onerror = function() {{ setTimeout(connectSSE, 3000); }};
486
  }}
487
 
488
+ function updateQuery(key, value) {{
489
+ const url = new URL(window.location.href);
490
+ if (value) url.searchParams.set(key, value);
491
+ else url.searchParams.delete(key);
492
+ return url.toString();
493
+ }}
494
+
495
+ function loginAccount(accountId) {{
496
+ fetch('/login_account/' + accountId, {{method: 'POST'}})
497
+ .then(() => location.reload());
498
+ }}
499
+
500
+ function logoutAccount(accountId) {{
501
+ fetch('/logout_account/' + accountId, {{method: 'POST'}})
502
+ .then(() => location.reload());
503
  }}
504
 
505
  function copyOTP(otp) {{
506
  navigator.clipboard.writeText(otp).then(() => {{
507
+ const toast = document.createElement('div');
508
+ toast.className = 'toast';
509
+ toast.textContent = 'βœ… OTP copied!';
510
+ document.body.appendChild(toast);
511
+ setTimeout(() => toast.remove(), 2000);
512
  }});
513
  }}
514
 
 
 
 
 
 
 
 
515
  function updateTime() {{
516
+ const now = new Date();
517
+ now.setHours(now.getHours() + 7);
518
  const wibEl = document.getElementById('wib-time');
519
  if (wibEl) wibEl.textContent = now.toISOString().substr(11, 8);
520
  }}
521
 
 
 
 
 
522
  connectSSE();
523
  setInterval(updateTime, 1000);
524
  </script>
 
527
  """
528
  return html
529
 
530
+ def generate_account_cards(accounts_list):
531
+ html = ""
532
+ for acc in accounts_list:
533
+ status_class = "online" if acc["status"] else "offline"
534
+ html += f"""
535
+ <div class="account-card {status_class}">
536
+ <div class="account-status status-{status_class}"></div>
537
+ <h4>{acc['username'][:20]}{'...' if len(acc['username']) > 20 else ''}</h4>
538
+ <p style="margin-top:8px; color:#8b949e;">Bot: {acc['bot']} | Chat: {acc['chat']}</p>
539
+ <div class="account-actions">
540
+ <button onclick="loginAccount('{acc['id']}')" class="btn btn-small">Login</button>
541
+ <button onclick="logoutAccount('{acc['id']}')" class="btn btn-small btn-danger">Logout</button>
542
+ <a href="/delete_account/{acc['id']}" class="btn btn-small btn-danger" onclick="return confirm('Hapus akun?')">Hapus</a>
543
+ </div>
544
+ </div>
545
+ """
546
+ if not accounts_list:
547
+ html = "<div style='grid-column:1/-1; text-align:center; padding:40px; color:#8b949e;'>Belum ada akun. Tambah akun di atas!</div>"
548
+ return html
549
+
550
+ def generate_otp_rows(logs, search_query):
551
+ if not logs:
552
+ return '<tr><td colspan="7" style="text-align:center; padding:60px; color:#8b949e;">πŸ” Belum ada OTP</td></tr>'
553
 
554
  rows = ""
555
+ for log in logs[:100]:
556
+ country = log.get('country', '')
557
+ number = log.get('number', '')
558
+ otp = log.get('otp', '')
559
+ sms = log.get('sms', '')
560
+ service = log.get('service', 'UNKNOWN')
561
+ account = log.get('account_username', 'Unknown')[:15]
562
 
563
  if search_query:
564
+ country = re.sub(f'({re.escape(search_query)})', r'<span class="highlight" style="background:#00f2fe30; border-radius:4px;">\1</span>', country, flags=re.I)
565
  number = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', number, flags=re.I)
566
  otp = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', otp, flags=re.I)
 
567
 
568
+ service_class = service.lower().replace(' ', '')
569
  rows += f'''
570
+ <tr class="new-row">
571
+ <td style="color:#00f2fe;">{log.get('time', '')}</td>
572
+ <td><span class="account-badge">{account}</span></td>
573
  <td>{country}</td>
574
  <td><span class="number">{number}</span></td>
575
+ <td><span class="service-badge {service_class}">{service}</span></td>
576
+ <td><span class="otp-badge" onclick="copyOTP('{otp}')">{otp}</span></td>
577
+ <td><div style="max-width:300px; overflow:hidden; text-overflow:ellipsis;" title="{log.get('sms', '')}">{log.get('sms', '')}</div></td>
578
  </tr>
579
  '''
580
  return rows
581
 
582
+ @app.route('/add_account', methods=['POST'])
583
+ def add_account_route():
584
+ account_id = str(uuid.uuid4())[:8]
585
+ accounts[account_id] = {
586
+ "id": account_id,
587
+ "username": request.form['username'],
588
+ "password": request.form['password'],
589
+ "bot_token": request.form.get('bot_token', ''),
590
+ "chat_id": request.form.get('chat_id', ''),
591
+ "session": None,
592
+ "csrf": None,
593
+ "status": False,
594
+ "otp_logs": [],
595
+ "sent_cache": [],
596
+ "sms_cache": {},
597
+ "sms_counter": {},
598
+ "range_counter": {},
599
+ "last_cleanup": time.time(),
600
+ "created_at": time.time()
601
+ }
602
+ save_accounts(accounts)
603
+ return redirect('/')
604
+
605
+ @app.route('/delete_account/<account_id>')
606
+ def delete_account_route(account_id):
607
+ if account_id in accounts:
608
+ del accounts[account_id]
609
+ save_accounts(accounts)
610
+ return redirect('/')
611
+
612
+ @app.route('/login_account/<account_id>', methods=['POST'])
613
+ def login_account_route(account_id):
614
+ if account_id in accounts:
615
+ acc = accounts[account_id]
616
+ success, msg = login_account(account_id, acc['username'], acc['password'])
617
+ if success:
618
+ # Start thread untuk akun ini
619
+ thread = Thread(target=run_account_scraper, args=(account_id,), daemon=True)
620
+ thread.start()
621
+ return redirect('/')
622
+
623
+ @app.route('/logout_account/<account_id>', methods=['POST'])
624
+ def logout_account_route(account_id):
625
+ if account_id in accounts:
626
+ accounts[account_id]["status"] = False
627
+ accounts[account_id]["session"] = None
628
+ accounts[account_id]["csrf"] = None
629
+ save_accounts(accounts)
630
+ return redirect('/')
631
+
632
  @app.route('/stream')
633
  def stream():
634
  def generate():
 
642
  sse_clients.remove(q)
643
  return Response(generate(), mimetype="text/event-stream")
644
 
645
+ def run_account_scraper(account_id):
646
+ account = accounts.get(account_id)
647
+ if not account:
648
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
649
 
650
+ print(f"πŸš€ Starting scraper for {account['username']}")
 
651
 
652
+ while account.get("status"):
653
  try:
654
+ if time.time() - account.get("last_cleanup", 0) > 300:
655
+ account["sms_cache"] = {}
656
+ account["sms_counter"] = {}
657
+ account["range_counter"] = {}
658
+ account["last_cleanup"] = time.time()
659
+ print(f"🧹 Cache cleared for {account['username']}")
 
 
 
 
660
 
661
+ ranges_data = get_ranges_with_count(account_id)
 
 
 
 
 
 
 
 
 
 
 
 
662
 
663
  for range_item in ranges_data:
664
+ if not account.get("status"):
665
+ break
666
+
667
  rng = range_item["name"]
668
  current_count = range_item["count"]
669
+ prev_count = account["range_counter"].get(rng, 0)
670
 
671
  if current_count > prev_count:
672
  country = clean_country(rng)
673
+ account["range_counter"][rng] = current_count
 
 
674
 
675
+ numbers_data = get_numbers_with_count(account_id, rng)
676
 
677
  for number_item in numbers_data:
678
+ if not account.get("status"):
679
+ break
680
+
681
  num = number_item["number"]
682
  num_count = number_item["count"]
683
  key = f"{rng}-{num}"
684
+ prev_num_count = account["sms_counter"].get(key, 0)
685
 
686
  if num_count > prev_num_count:
687
+ account["sms_counter"][key] = num_count
 
 
688
 
689
+ all_sms = get_sms_fast(account_id, rng, num)
690
  new_sms = all_sms[prev_num_count:]
691
 
692
  for service, sms, otp in new_sms:
693
  if otp:
694
  sms_id = f"{rng}-{num}-{otp}"
695
+ if sms_id not in account["sent_cache"]:
696
  masked = mask_number(num)
697
+ msg = f"πŸ”” *NEW OTP*\n🌍 {country}\nπŸ“ž `{masked}`\nπŸ’¬ {service}\nπŸ” `{otp}`\n\n{sms[:300]}"
698
+ if tg_send(account_id, msg):
699
+ if "sent_cache" not in account:
700
+ account["sent_cache"] = []
701
+ account["sent_cache"].append(sms_id)
702
+ if len(account["sent_cache"]) > 1000:
703
+ account["sent_cache"] = account["sent_cache"][-1000:]
704
+ add_otp_log(account_id, country, masked, service, otp, sms)
705
 
 
706
  time.sleep(0.5)
707
 
 
708
  time.sleep(2)
709
 
710
  except Exception as e:
711
+ print(f"❌ Error in scraper for {account['username']}: {e}")
712
+ time.sleep(5)
713
+
714
+ def run_server():
715
+ app.run(host='0.0.0.0', port=7860, debug=False, threaded=True)
716
+
717
+ # Start server thread
718
+ Thread(target=run_server, daemon=True).start()
719
+
720
+ # Start scraper untuk semua akun yang sudah login
721
+ def main():
722
+ print("\n" + "="*60)
723
+ print(" πŸ”₯ OTP MULTI ACCOUNT - FOURSTORE")
724
+ print(" ⚑ PORT: 7860")
725
+ print(f" 🌐 DOMAIN: {CUSTOM_DOMAIN}")
726
+ print(" πŸ“ Data tersimpan di accounts.json")
727
+ print("="*60 + "\n")
728
+
729
+ # Login semua akun yang punya status True
730
+ for acc_id, acc in accounts.items():
731
+ if acc.get("status"):
732
+ success, msg = login_account(acc_id, acc['username'], acc['password'])
733
+ if success:
734
+ thread = Thread(target=run_account_scraper, args=(acc_id,), daemon=True)
735
+ thread.start()
736
+ print(f"βœ… {acc['username']} online")
737
+ else:
738
+ print(f"❌ {acc['username']} offline: {msg}")
739
+
740
+ # Keep main thread alive
741
+ while True:
742
+ time.sleep(60)
743
 
744
  if __name__ == "__main__":
745
  try:
746
  main()
747
  except KeyboardInterrupt:
748
+ print("\nπŸ›‘ BOT STOPPED")
749
+ save_accounts(accounts)