|
|
import httpx |
|
|
from bs4 import BeautifulSoup |
|
|
import re |
|
|
from datetime import datetime, timedelta, timezone |
|
|
import time |
|
|
from flask import Flask, Response, request, redirect |
|
|
from threading import Thread |
|
|
from collections import deque |
|
|
import json |
|
|
from queue import Queue |
|
|
import os |
|
|
import uuid |
|
|
import sys |
|
|
from pymongo import MongoClient |
|
|
from pymongo.errors import ConnectionFailure |
|
|
|
|
|
MONGODB_URI = os.environ.get("MONGODB_URI") |
|
|
DB_NAME = "otp_bot" |
|
|
COLLECTION_NAME = "accounts" |
|
|
|
|
|
if MONGODB_URI: |
|
|
try: |
|
|
mongo_client = MongoClient(MONGODB_URI) |
|
|
db = mongo_client[DB_NAME] |
|
|
accounts_collection = db[COLLECTION_NAME] |
|
|
mongo_client.admin.command('ping') |
|
|
print("✅ MongoDB Connected!") |
|
|
|
|
|
count = accounts_collection.count_documents({}) |
|
|
print(f"📊 Total dokumen di MongoDB: {count}") |
|
|
except ConnectionFailure as e: |
|
|
print(f"❌ MongoDB Connection Failed: {e}") |
|
|
mongo_client = None |
|
|
else: |
|
|
print("⚠️ MONGODB_URI tidak di set, menggunakan penyimpanan lokal") |
|
|
mongo_client = None |
|
|
|
|
|
print = lambda *args, **kwargs: __builtins__.print(*args, **kwargs, flush=True) |
|
|
|
|
|
BASE = "http://159.69.3.189" |
|
|
LOGIN_URL = f"{BASE}/login" |
|
|
GET_RANGE_URL = f"{BASE}/portal/sms/received/getsms" |
|
|
GET_NUMBER_URL = f"{BASE}/portal/sms/received/getsms/number" |
|
|
GET_SMS_URL = f"{BASE}/portal/sms/received/getsms/number/sms" |
|
|
|
|
|
TELEGRAM_PROXY_URL = "https://danihitambangetjir.termai.cc/api/proxy" |
|
|
CUSTOM_DOMAIN = "https://fourstore-otp.hf.space" |
|
|
ACCOUNTS_FILE = "accounts.json" |
|
|
|
|
|
def mask_email(email): |
|
|
if not email or '@' not in email: |
|
|
return email |
|
|
parts = email.split('@') |
|
|
username = parts[0] |
|
|
domain = parts[1] |
|
|
if len(username) <= 3: |
|
|
masked_username = username[0] + '*' * (len(username) - 1) |
|
|
else: |
|
|
masked_username = username[:2] + '*' * (len(username) - 3) + username[-1] |
|
|
return f"{masked_username}@{domain}" |
|
|
|
|
|
def load_accounts_from_mongodb(): |
|
|
accounts_dict = {} |
|
|
try: |
|
|
if mongo_client: |
|
|
print("📥 Loading accounts from MongoDB...") |
|
|
cursor = accounts_collection.find({}) |
|
|
count = 0 |
|
|
for doc in cursor: |
|
|
count += 1 |
|
|
acc_id = doc.pop("_id") |
|
|
doc["session"] = None |
|
|
doc["csrf"] = None |
|
|
doc["status"] = False |
|
|
doc["otp_logs"] = doc.get("otp_logs", []) |
|
|
doc["sent_cache"] = [] |
|
|
doc["sms_cache"] = {} |
|
|
doc["sms_counter"] = {} |
|
|
doc["range_counter"] = {} |
|
|
doc["last_cleanup"] = time.time() |
|
|
accounts_dict[acc_id] = doc |
|
|
|
|
|
print(f"📊 Loaded {len(accounts_dict)} accounts from MongoDB") |
|
|
if count > 0: |
|
|
print(f"✅ Data MongoDB berhasil dimuat: {count} akun") |
|
|
|
|
|
for acc_id, acc in list(accounts_dict.items())[:2]: |
|
|
email_masked = mask_email(acc.get('username', 'Unknown')) |
|
|
log_count = len(acc.get('otp_logs', [])) |
|
|
print(f" - Akun: {email_masked} | OTP Logs: {log_count}") |
|
|
else: |
|
|
print("⚠️ Tidak ada data di MongoDB") |
|
|
except Exception as e: |
|
|
print(f"❌ Error load from MongoDB: {e}") |
|
|
return accounts_dict |
|
|
|
|
|
def save_accounts_to_mongodb(accounts_dict): |
|
|
try: |
|
|
if mongo_client: |
|
|
print("💾 Saving accounts to MongoDB...") |
|
|
saved_count = 0 |
|
|
for acc_id, acc in accounts_dict.items(): |
|
|
acc_copy = acc.copy() |
|
|
acc_copy.pop("session", None) |
|
|
acc_copy.pop("csrf", None) |
|
|
acc_copy.pop("sms_cache", None) |
|
|
acc_copy.pop("sms_counter", None) |
|
|
acc_copy.pop("range_counter", None) |
|
|
acc_copy.pop("last_cleanup", None) |
|
|
|
|
|
|
|
|
if "otp_logs" in acc_copy and len(acc_copy["otp_logs"]) > 100: |
|
|
acc_copy["otp_logs"] = acc_copy["otp_logs"][:100] |
|
|
|
|
|
result = accounts_collection.update_one( |
|
|
{"_id": acc_id}, |
|
|
{"$set": acc_copy}, |
|
|
upsert=True |
|
|
) |
|
|
if result.upserted_id or result.modified_count > 0: |
|
|
saved_count += 1 |
|
|
|
|
|
print(f"💾 Saved {saved_count} accounts to MongoDB") |
|
|
except Exception as e: |
|
|
print(f"❌ Error save to MongoDB: {e}") |
|
|
|
|
|
def load_accounts_from_file(): |
|
|
if os.path.exists(ACCOUNTS_FILE): |
|
|
try: |
|
|
with open(ACCOUNTS_FILE, 'r') as f: |
|
|
data = json.load(f) |
|
|
for acc_id in data: |
|
|
data[acc_id]["session"] = None |
|
|
data[acc_id]["csrf"] = None |
|
|
data[acc_id]["status"] = False |
|
|
if "otp_logs" not in data[acc_id]: |
|
|
data[acc_id]["otp_logs"] = [] |
|
|
data[acc_id]["sent_cache"] = [] |
|
|
data[acc_id]["sms_cache"] = {} |
|
|
data[acc_id]["sms_counter"] = {} |
|
|
data[acc_id]["range_counter"] = {} |
|
|
data[acc_id]["last_cleanup"] = time.time() |
|
|
print(f"📊 Loaded {len(data)} accounts from file") |
|
|
return data |
|
|
except: |
|
|
return {} |
|
|
return {} |
|
|
|
|
|
def save_accounts_to_file(accounts_dict): |
|
|
try: |
|
|
accounts_to_save = {} |
|
|
for acc_id, acc in accounts_dict.items(): |
|
|
acc_copy = acc.copy() |
|
|
acc_copy.pop("session", None) |
|
|
acc_copy.pop("csrf", None) |
|
|
if "otp_logs" in acc_copy and len(acc_copy["otp_logs"]) > 100: |
|
|
acc_copy["otp_logs"] = acc_copy["otp_logs"][:100] |
|
|
acc_copy.pop("sms_cache", None) |
|
|
acc_copy.pop("sms_counter", None) |
|
|
acc_copy.pop("range_counter", None) |
|
|
acc_copy.pop("last_cleanup", None) |
|
|
accounts_to_save[acc_id] = acc_copy |
|
|
|
|
|
with open(ACCOUNTS_FILE, 'w') as f: |
|
|
json.dump(accounts_to_save, f, indent=2) |
|
|
print(f"💾 Accounts saved to file") |
|
|
except Exception as e: |
|
|
print(f"❌ Error save to file: {e}") |
|
|
|
|
|
|
|
|
if mongo_client: |
|
|
accounts = load_accounts_from_mongodb() |
|
|
else: |
|
|
accounts = load_accounts_from_file() |
|
|
|
|
|
app = Flask('') |
|
|
app.secret_key = "fourstore-multi-account-secret" |
|
|
sse_clients = [] |
|
|
|
|
|
global_otp_logs = [] |
|
|
|
|
|
def get_utc_time(): |
|
|
return datetime.now(timezone.utc) |
|
|
|
|
|
def get_wib_time(): |
|
|
return datetime.now(timezone.utc) + timedelta(hours=7) |
|
|
|
|
|
def get_wib_time_str(): |
|
|
return get_wib_time().strftime("%H:%M:%S") |
|
|
|
|
|
def get_search_date(): |
|
|
now_utc = datetime.now(timezone.utc) |
|
|
return now_utc.strftime("%Y-%m-%d") |
|
|
|
|
|
def login_account(account_id, username, password, bot_token, chat_id): |
|
|
try: |
|
|
masked = mask_email(username) |
|
|
print(f"\n{'='*60}") |
|
|
print(f"🔐 PROSES LOGIN UNTUK: {masked} (ID: {account_id})") |
|
|
print(f"{'='*60}") |
|
|
|
|
|
session = httpx.Client(follow_redirects=True, timeout=30.0) |
|
|
r = session.get(LOGIN_URL, timeout=30) |
|
|
|
|
|
if r.status_code != 200: |
|
|
return False, f"HTTP {r.status_code}" |
|
|
|
|
|
soup = BeautifulSoup(r.text, "html.parser") |
|
|
token = soup.find("input", {"name": "_token"}) |
|
|
if not token: |
|
|
return False, "Token tidak ditemukan" |
|
|
|
|
|
csrf_token = token.get("value") |
|
|
r = session.post(LOGIN_URL, data={ |
|
|
"_token": csrf_token, |
|
|
"email": username, |
|
|
"password": password |
|
|
}, timeout=30) |
|
|
|
|
|
if "dashboard" in r.text.lower() or "logout" in r.text.lower(): |
|
|
accounts[account_id]["session"] = session |
|
|
accounts[account_id]["csrf"] = csrf_token |
|
|
accounts[account_id]["status"] = True |
|
|
accounts[account_id]["username"] = username |
|
|
accounts[account_id]["password"] = password |
|
|
accounts[account_id]["bot_token"] = bot_token |
|
|
accounts[account_id]["chat_id"] = chat_id |
|
|
accounts[account_id]["last_login"] = time.time() |
|
|
|
|
|
if mongo_client: |
|
|
save_accounts_to_mongodb(accounts) |
|
|
else: |
|
|
save_accounts_to_file(accounts) |
|
|
return True, "Login berhasil" |
|
|
else: |
|
|
return False, "Login gagal" |
|
|
|
|
|
except Exception as e: |
|
|
return False, str(e) |
|
|
|
|
|
def tg_send(account_id, msg): |
|
|
try: |
|
|
account = accounts.get(account_id) |
|
|
if not account or not account.get("bot_token") or not account.get("chat_id"): |
|
|
return False |
|
|
|
|
|
url = f"{TELEGRAM_PROXY_URL}?url=https://api.telegram.org/bot{account['bot_token']}/sendMessage" |
|
|
payload = { |
|
|
"chat_id": account['chat_id'], |
|
|
"text": msg, |
|
|
"parse_mode": "Markdown" |
|
|
} |
|
|
httpx.post(url, json=payload, timeout=30) |
|
|
print(f"✅ OTP terkirim ke chat {account['chat_id']}") |
|
|
return True |
|
|
except Exception as e: |
|
|
print(f"❌ Gagal kirim Telegram: {e}") |
|
|
return False |
|
|
|
|
|
def clean_country(rng): |
|
|
return re.sub(r"\s*\d+$", "", rng).strip() if rng else "UNKNOWN" |
|
|
|
|
|
def mask_number(number): |
|
|
if not number: return "UNKNOWN" |
|
|
clean = re.sub(r"[^\d+]", "", number) |
|
|
if len(clean) <= 6: return clean |
|
|
return f"{clean[:4]}****{clean[-3:]}" |
|
|
|
|
|
def map_service(raw): |
|
|
if not raw: return "UNKNOWN" |
|
|
s = raw.lower().strip() |
|
|
if 'whatsapp' in s: return "WHATSAPP" |
|
|
if 'telegram' in s: return "TELEGRAM" |
|
|
if 'google' in s or 'gmail' in s: return "GOOGLE" |
|
|
if 'facebook' in s or 'fb' in s: return "FACEBOOK" |
|
|
if 'instagram' in s or 'ig' in s: return "INSTAGRAM" |
|
|
if 'tiktok' in s: return "TIKTOK" |
|
|
if 'temu' in s: return "TEMU" |
|
|
if 'shopee' in s: return "SHOPEE" |
|
|
if 'tokopedia' in s: return "TOKOPEDIA" |
|
|
if 'grab' in s: return "GRAB" |
|
|
if 'gojek' in s or 'go-jek' in s: return "GOJEK" |
|
|
return raw.upper() |
|
|
|
|
|
def extract_otp(text): |
|
|
if not text: return None |
|
|
m = re.search(r"\b(\d{6})\b", text) |
|
|
if m: |
|
|
return m.group(0) |
|
|
m = re.search(r"\b(\d{4,5})\b", text) |
|
|
if m: |
|
|
return m.group(0) |
|
|
m = re.search(r"\b(\d{3}[- ]?\d{3})\b", text) |
|
|
if m: |
|
|
return m.group(0).replace("-", "").replace(" ", "") |
|
|
digits = re.findall(r'\d+', text) |
|
|
for d in digits: |
|
|
if 4 <= len(d) <= 6: |
|
|
return d |
|
|
return None |
|
|
|
|
|
def generate_otp_id(account_id, rng, number, otp, service): |
|
|
return f"{account_id}-{rng}-{number}-{otp}-{service}" |
|
|
|
|
|
def get_ranges_with_count(account_id): |
|
|
account = accounts.get(account_id) |
|
|
if not account or not account.get("session") or not account.get("csrf"): |
|
|
return [] |
|
|
|
|
|
try: |
|
|
date = get_search_date() |
|
|
r = account["session"].post(GET_RANGE_URL, data={ |
|
|
"_token": account["csrf"], |
|
|
"from": date, |
|
|
"to": date |
|
|
}, timeout=15) |
|
|
|
|
|
soup = BeautifulSoup(r.text, "html.parser") |
|
|
ranges_data = [] |
|
|
|
|
|
for item in soup.select(".item"): |
|
|
name_div = item.select_one(".col-sm-4") |
|
|
if not name_div: continue |
|
|
rng = name_div.get_text(strip=True) |
|
|
|
|
|
count_p = item.select_one(".col-3 .mb-0.pb-0") |
|
|
count = int(count_p.get_text(strip=True)) if count_p else 0 |
|
|
|
|
|
ranges_data.append({ |
|
|
"name": rng, |
|
|
"count": count |
|
|
}) |
|
|
|
|
|
return ranges_data |
|
|
except Exception as e: |
|
|
print(f"Error get_ranges: {e}") |
|
|
return [] |
|
|
|
|
|
def get_numbers_with_count(account_id, rng): |
|
|
account = accounts.get(account_id) |
|
|
if not account or not account.get("session") or not account.get("csrf"): |
|
|
return [] |
|
|
|
|
|
try: |
|
|
date = get_search_date() |
|
|
r = account["session"].post(GET_NUMBER_URL, data={ |
|
|
"_token": account["csrf"], |
|
|
"start": date, |
|
|
"end": date, |
|
|
"range": rng |
|
|
}, timeout=15) |
|
|
|
|
|
soup = BeautifulSoup(r.text, "html.parser") |
|
|
numbers_data = [] |
|
|
|
|
|
for div in soup.find_all("div", onclick=True): |
|
|
onclick = div.get("onclick", "") |
|
|
match = re.search(r"getDetialsNumber\w*\('?(\d+)'?", onclick) |
|
|
if not match: |
|
|
match = re.search(r"open_(\d+)", onclick) |
|
|
if not match: |
|
|
match = re.search(r"'(\d+)'", onclick) |
|
|
|
|
|
if match: |
|
|
num = match.group(1) |
|
|
if num and len(num) > 5: |
|
|
parent = div.find_parent("div", class_="card") |
|
|
count = 0 |
|
|
if parent: |
|
|
p_tag = parent.find("p", class_="mb-0 pb-0") |
|
|
if p_tag: |
|
|
try: |
|
|
count = int(p_tag.get_text(strip=True)) |
|
|
except: |
|
|
count = 0 |
|
|
|
|
|
numbers_data.append({ |
|
|
"number": num, |
|
|
"count": count |
|
|
}) |
|
|
|
|
|
if len(numbers_data) == 0: |
|
|
for div in soup.find_all("div", class_="col-sm-4"): |
|
|
text = div.get_text(strip=True) |
|
|
match = re.search(r'\b(\d{10,15})\b', text) |
|
|
if match: |
|
|
num = match.group(1) |
|
|
parent = div.find_parent("div", class_="card") |
|
|
count = 0 |
|
|
if parent: |
|
|
p_tag = parent.find("p", class_="mb-0 pb-0") |
|
|
if p_tag: |
|
|
try: |
|
|
count = int(p_tag.get_text(strip=True)) |
|
|
except: |
|
|
count = 0 |
|
|
|
|
|
numbers_data.append({ |
|
|
"number": num, |
|
|
"count": count |
|
|
}) |
|
|
|
|
|
return numbers_data |
|
|
except Exception as e: |
|
|
print(f"Error get_numbers: {e}") |
|
|
return [] |
|
|
|
|
|
def get_sms_fast(account_id, rng, number): |
|
|
account = accounts.get(account_id) |
|
|
if not account or not account.get("session") or not account.get("csrf"): |
|
|
return [] |
|
|
|
|
|
try: |
|
|
date = get_search_date() |
|
|
cache_key = f"{rng}-{number}" |
|
|
|
|
|
if cache_key in account["sms_cache"]: |
|
|
timestamp, results = account["sms_cache"][cache_key] |
|
|
if time.time() - timestamp < 5: |
|
|
return results |
|
|
|
|
|
r = account["session"].post(GET_SMS_URL, data={ |
|
|
"_token": account["csrf"], |
|
|
"start": date, |
|
|
"end": date, |
|
|
"Number": number, |
|
|
"Range": rng |
|
|
}, timeout=20) |
|
|
|
|
|
soup = BeautifulSoup(r.text, "html.parser") |
|
|
results = [] |
|
|
|
|
|
for card in soup.select("div.card.card-body"): |
|
|
try: |
|
|
service = "UNKNOWN" |
|
|
service_div = card.select_one("div.col-sm-4") |
|
|
if service_div: |
|
|
raw = service_div.get_text(strip=True) |
|
|
service = map_service(raw) |
|
|
|
|
|
msg_p = card.find("p", class_="mb-0 pb-0") |
|
|
if msg_p: |
|
|
sms = msg_p.get_text(strip=True) |
|
|
otp = extract_otp(sms) |
|
|
if otp: |
|
|
results.append((service, sms, otp)) |
|
|
except: |
|
|
continue |
|
|
|
|
|
account["sms_cache"][cache_key] = (time.time(), results) |
|
|
return results |
|
|
except Exception as e: |
|
|
print(f"Error get_sms: {e}") |
|
|
return [] |
|
|
|
|
|
def is_otp_sent(account_id, otp_id): |
|
|
account = accounts.get(account_id) |
|
|
if not account: |
|
|
return True |
|
|
return otp_id in account.get("sent_cache", []) |
|
|
|
|
|
def mark_otp_sent(account_id, otp_id): |
|
|
account = accounts.get(account_id) |
|
|
if not account: |
|
|
return |
|
|
|
|
|
if "sent_cache" not in account: |
|
|
account["sent_cache"] = [] |
|
|
|
|
|
account["sent_cache"].append(otp_id) |
|
|
if len(account["sent_cache"]) > 1000: |
|
|
account["sent_cache"] = account["sent_cache"][-1000:] |
|
|
|
|
|
def add_otp_log(account_id, country, number, service, otp, sms, otp_id): |
|
|
account = accounts.get(account_id) |
|
|
if not account: |
|
|
return |
|
|
|
|
|
wib = get_wib_time() |
|
|
wib_str = wib.strftime("%Y-%m-%d %H:%M:%S WIB") |
|
|
time_only = wib.strftime("%H:%M:%S") |
|
|
|
|
|
log_entry = { |
|
|
"time": time_only, |
|
|
"time_full": wib_str, |
|
|
"timestamp": time.time(), |
|
|
"country": country, |
|
|
"number": number, |
|
|
"service": service, |
|
|
"otp": otp, |
|
|
"sms": sms[:150] + "..." if len(sms) > 150 else sms, |
|
|
"account_id": account_id, |
|
|
"account_username": mask_email(account.get("username", "Unknown")), |
|
|
"otp_id": otp_id |
|
|
} |
|
|
|
|
|
if "otp_logs" not in account: |
|
|
account["otp_logs"] = [] |
|
|
|
|
|
account["otp_logs"].insert(0, log_entry) |
|
|
if len(account["otp_logs"]) > 100: |
|
|
account["otp_logs"] = account["otp_logs"][:100] |
|
|
|
|
|
global_otp_logs.insert(0, log_entry) |
|
|
if len(global_otp_logs) > 500: |
|
|
global_otp_logs[:] = global_otp_logs[:500] |
|
|
|
|
|
broadcast_sse(log_entry) |
|
|
|
|
|
if mongo_client: |
|
|
save_accounts_to_mongodb(accounts) |
|
|
else: |
|
|
save_accounts_to_file(accounts) |
|
|
|
|
|
print(f"✅ Log ditambahkan: {service} - {otp} - {time_only}") |
|
|
return log_entry |
|
|
|
|
|
def broadcast_sse(data): |
|
|
msg = f"data: {json.dumps(data)}\n\n" |
|
|
dead = [] |
|
|
for q in sse_clients: |
|
|
try: |
|
|
q.put(msg) |
|
|
except: |
|
|
dead.append(q) |
|
|
for q in dead: |
|
|
sse_clients.remove(q) |
|
|
|
|
|
@app.route('/') |
|
|
def home(): |
|
|
all_logs = global_otp_logs.copy() |
|
|
|
|
|
search_query = request.args.get('q', '').lower() |
|
|
filter_service = request.args.get('service', '') |
|
|
|
|
|
if search_query: |
|
|
all_logs = [log for log in all_logs if |
|
|
search_query in log.get('country', '').lower() or |
|
|
search_query in log.get('number', '').lower() or |
|
|
search_query in log.get('otp', '').lower() or |
|
|
search_query in log.get('sms', '').lower() or |
|
|
search_query in log.get('account_username', '').lower()] |
|
|
|
|
|
if filter_service: |
|
|
all_logs = [log for log in all_logs if log.get('service') == filter_service] |
|
|
|
|
|
all_services = list(set([log.get('service') for log in all_logs if log.get('service')])) |
|
|
|
|
|
total_otp = len(global_otp_logs) |
|
|
today_otp = len([l for l in global_otp_logs if l.get('time_full', '').startswith(get_wib_time().strftime("%Y-%m-%d"))]) |
|
|
|
|
|
service_stats = {} |
|
|
for log in global_otp_logs[:50]: |
|
|
service = log.get('service', 'UNKNOWN') |
|
|
service_stats[service] = service_stats.get(service, 0) + 1 |
|
|
|
|
|
html = f""" |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>OTP MULTI ACCOUNT · FOURSTORE</title> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
* {{ margin: 0; padding: 0; box-sizing: border-box; }} |
|
|
body {{ font-family: 'Inter', sans-serif; background: #0a0c10; color: #e4e6eb; padding: 24px; }} |
|
|
.container {{ max-width: 1600px; margin: 0 auto; }} |
|
|
|
|
|
.header {{ background: linear-gradient(145deg, #1a1f2c, #0f131c); border-radius: 24px; padding: 28px; margin-bottom: 28px; border: 1px solid #2d3540; }} |
|
|
.header-top {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 15px; }} |
|
|
.title h1 {{ font-size: 28px; font-weight: 700; background: linear-gradient(135deg, #00f2fe, #4facfe); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }} |
|
|
.title p {{ color: #8b949e; font-size: 14px; }} |
|
|
.status-badge {{ padding: 10px 24px; border-radius: 100px; font-weight: 600; font-size: 14px; |
|
|
background: #0a4d3c; color: #a0f0d0; border: 1px solid #1a6e5a; }} |
|
|
.stats-grid {{ display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-top: 20px; }} |
|
|
.stat-card {{ background: #1a1f2c; padding: 20px; border-radius: 20px; border: 1px solid #2d3540; }} |
|
|
.stat-label {{ color: #8b949e; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }} |
|
|
.stat-value {{ font-size: 32px; font-weight: 700; color: #00f2fe; }} |
|
|
|
|
|
.add-account-form {{ background: #1a1f2c; padding: 20px; border-radius: 16px; margin-bottom: 30px; border: 1px solid #2d3540; }} |
|
|
.form-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; }} |
|
|
.form-input {{ background: #0a0c10; border: 1px solid #2d3540; padding: 12px 16px; border-radius: 12px; color: #e4e6eb; width: 100%; }} |
|
|
.form-input:focus {{ outline: none; border-color: #00f2fe; }} |
|
|
.btn {{ background: #00f2fe; color: #0a0c10; border: none; padding: 12px 24px; border-radius: 12px; font-weight: 600; cursor: pointer; transition: all 0.2s; }} |
|
|
.btn:hover {{ background: #00d8e4; transform: translateY(-2px); }} |
|
|
|
|
|
.search-section {{ background: #0f131c; border-radius: 20px; padding: 20px; margin-bottom: 24px; border: 1px solid #2d3540; display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }} |
|
|
.search-box {{ flex: 2; min-width: 280px; position: relative; }} |
|
|
.search-icon {{ position: absolute; left: 16px; top: 14px; color: #8b949e; }} |
|
|
.search-input {{ width: 100%; padding: 14px 20px 14px 48px; background: #1a1f2c; border: 1px solid #2d3540; border-radius: 100px; color: #e4e6eb; font-size: 15px; }} |
|
|
.filter-box {{ flex: 1; min-width: 150px; }} |
|
|
.filter-select {{ width: 100%; padding: 14px 20px; background: #1a1f2c; border: 1px solid #2d3540; border-radius: 100px; color: #e4e6eb; font-size: 15px; cursor: pointer; }} |
|
|
.clear-btn {{ padding: 8px 16px; background: #2d3a4a; border: none; border-radius: 100px; color: white; font-size: 13px; cursor: pointer; text-decoration: none; }} |
|
|
|
|
|
.stats-mini {{ display: flex; gap: 12px; flex-wrap: wrap; margin: 10px 0; }} |
|
|
.service-tag {{ background: #1a1f2c; padding: 6px 14px; border-radius: 100px; font-size: 12px; border: 1px solid #2d3540; }} |
|
|
|
|
|
.otp-section {{ background: #0f131c; border-radius: 24px; padding: 28px; border: 1px solid #2d3540; overflow-x: auto; }} |
|
|
table {{ width: 100%; border-collapse: collapse; min-width: 1100px; }} |
|
|
th {{ text-align: left; padding: 16px 12px; background: #1a1f2c; color: #00f2fe; font-weight: 600; font-size: 13px; text-transform: uppercase; border-bottom: 2px solid #2d3540; }} |
|
|
td {{ padding: 16px 12px; border-bottom: 1px solid #262c38; font-size: 14px; }} |
|
|
.otp-badge {{ background: #002b36; color: #00f2fe; font-family: monospace; font-size: 16px; font-weight: 700; padding: 6px 14px; border-radius: 100px; border: 1px solid #00f2fe40; cursor: pointer; user-select: all; }} |
|
|
.service-badge {{ padding: 6px 14px; border-radius: 100px; font-size: 12px; font-weight: 600; display: inline-block; }} |
|
|
.whatsapp {{ background: #25D36620; color: #25D366; border: 1px solid #25D36640; }} |
|
|
.telegram {{ background: #26A5E420; color: #26A5E4; border: 1px solid #26A5E440; }} |
|
|
.google {{ background: #4285F420; color: #4285F4; border: 1px solid #4285F440; }} |
|
|
.facebook {{ background: #1877F220; color: #1877F2; border: 1px solid #1877F240; }} |
|
|
.instagram {{ background: #E4405F20; color: #E4405F; border: 1px solid #E4405F40; }} |
|
|
.tiktok {{ background: #00000020; color: #FFFFFF; border: 1px solid #FFFFFF40; }} |
|
|
.shopee {{ background: #EE4D2D20; color: #EE4D2D; border: 1px solid #EE4D2D40; }} |
|
|
.tokopedia {{ background: #42B54920; color: #42B549; border: 1px solid #42B54940; }} |
|
|
.grab {{ background: #00B14F20; color: #00B14F; border: 1px solid #00B14F40; }} |
|
|
.gojek {{ background: #00880A20; color: #00880A; border: 1px solid #00880A40; }} |
|
|
.number {{ font-family: monospace; }} |
|
|
.empty-state {{ text-align: center; padding: 60px; color: #8b949e; }} |
|
|
.highlight {{ background: #00f2fe30; border-radius: 4px; padding: 0 2px; }} |
|
|
.new-row {{ animation: fadeIn 0.3s ease; background: linear-gradient(90deg, #00f2fe10, transparent); }} |
|
|
.toast {{ position: fixed; bottom: 24px; right: 24px; background: #00f2fe; color: #000; padding: 14px 28px; border-radius: 100px; font-weight: 600; z-index: 9999; }} |
|
|
.account-badge {{ background: #2d3a4a; padding: 4px 12px; border-radius: 100px; font-size: 12px; }} |
|
|
@keyframes fadeIn {{ from {{ opacity: 0; transform: translateY(-10px); }} to {{ opacity: 1; transform: translateY(0); }} }} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="container"> |
|
|
<div class="header"> |
|
|
<div class="header-top"> |
|
|
<div class="title"> |
|
|
<h1>📱 OTP MULTI ACCOUNT · FOURSTORE</h1> |
|
|
<p>{CUSTOM_DOMAIN} · {get_wib_time().strftime('%d %B %Y')}</p> |
|
|
</div> |
|
|
<div class="status-badge">● ONLINE · 24 JAM</div> |
|
|
</div> |
|
|
<div class="stats-grid"> |
|
|
<div class="stat-card"><div class="stat-label">Total Akun</div><div class="stat-value">{len(accounts)}</div></div> |
|
|
<div class="stat-card"><div class="stat-label">Akun Online</div><div class="stat-value">{sum(1 for a in accounts.values() if a.get('status'))}</div></div> |
|
|
<div class="stat-card"><div class="stat-label">Total OTP</div><div class="stat-value">{total_otp}</div></div> |
|
|
<div class="stat-card"><div class="stat-label">WIB</div><div class="stat-value" id="wib-time">{get_wib_time().strftime('%H:%M:%S')}</div></div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="add-account-form"> |
|
|
<h3 style="margin-bottom: 15px;">➕ Tambah Akun Baru</h3> |
|
|
<form action="/add_account" method="POST" class="form-grid"> |
|
|
<input type="text" name="username" placeholder="Username/Email" class="form-input" required> |
|
|
<input type="password" name="password" placeholder="Password" class="form-input" required> |
|
|
<input type="text" name="bot_token" placeholder="Bot Token (opsional)" class="form-input"> |
|
|
<input type="text" name="chat_id" placeholder="Chat ID (opsional)" class="form-input"> |
|
|
<button type="submit" class="btn">Tambah & Login Otomatis</button> |
|
|
</form> |
|
|
</div> |
|
|
|
|
|
<div class="search-section"> |
|
|
<div class="search-box"> |
|
|
<span class="search-icon">🔍</span> |
|
|
<form action="/" method="get" id="searchForm"> |
|
|
<input type="text" class="search-input" name="q" placeholder="Cari country, nomor, OTP, akun..." value="{request.args.get('q', '')}"> |
|
|
</form> |
|
|
</div> |
|
|
<div class="filter-box"> |
|
|
<select class="filter-select" name="service" onchange="updateFilter('service', this.value)"> |
|
|
<option value="">📋 Semua Service</option> |
|
|
{''.join([f'<option value="{s}" {"selected" if filter_service == s else ""}>📱 {s}</option>' for s in sorted(all_services)])} |
|
|
</select> |
|
|
</div> |
|
|
<a href="/" class="clear-btn">✕ Reset</a> |
|
|
<span class="result-count">📊 {len(all_logs)} hasil</span> |
|
|
</div> |
|
|
|
|
|
<div class="stats-mini"> |
|
|
{''.join([f'<span class="service-tag">{service}: {count}</span>' for service, count in sorted(service_stats.items())][:8])} |
|
|
</div> |
|
|
|
|
|
<div class="otp-section"> |
|
|
<h3 style="margin-bottom: 20px;">📨 OTP TERBARU <span style="background:#00f2fe20; padding:4px 12px; border-radius:100px; font-size:12px;">LIVE · 24 JAM</span></h3> |
|
|
<table> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>WIB</th> |
|
|
<th>Akun</th> |
|
|
<th>Country</th> |
|
|
<th>Number</th> |
|
|
<th>Service</th> |
|
|
<th>OTP</th> |
|
|
<th>Message</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody id="otp-table-body"> |
|
|
{generate_otp_rows(all_logs, search_query)} |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
let eventSource; |
|
|
|
|
|
function connectSSE() {{ |
|
|
eventSource = new EventSource('/stream'); |
|
|
eventSource.onmessage = function(e) {{ |
|
|
try {{ |
|
|
const data = JSON.parse(e.data); |
|
|
if (data.otp) {{ |
|
|
location.reload(); |
|
|
}} |
|
|
}} catch(err) {{}} |
|
|
}}; |
|
|
eventSource.onerror = function() {{ |
|
|
setTimeout(connectSSE, 3000); |
|
|
}}; |
|
|
}} |
|
|
|
|
|
function updateFilter(key, value) {{ |
|
|
const url = new URL(window.location.href); |
|
|
if (value) {{ |
|
|
url.searchParams.set(key, value); |
|
|
}} else {{ |
|
|
url.searchParams.delete(key); |
|
|
}} |
|
|
window.location.href = url.toString(); |
|
|
}} |
|
|
|
|
|
function copyOTP(otp) {{ |
|
|
navigator.clipboard.writeText(otp).then(() => {{ |
|
|
const toast = document.createElement('div'); |
|
|
toast.className = 'toast'; |
|
|
toast.textContent = '✅ OTP copied: ' + otp; |
|
|
document.body.appendChild(toast); |
|
|
setTimeout(() => toast.remove(), 2000); |
|
|
}}); |
|
|
}} |
|
|
|
|
|
function updateTime() {{ |
|
|
const now = new Date(); |
|
|
now.setHours(now.getHours() + 7); |
|
|
const wibEl = document.getElementById('wib-time'); |
|
|
if (wibEl) wibEl.textContent = now.toISOString().substr(11, 8); |
|
|
}} |
|
|
|
|
|
setInterval(() => {{ |
|
|
location.reload(); |
|
|
}}, 30000); |
|
|
|
|
|
connectSSE(); |
|
|
setInterval(updateTime, 1000); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
""" |
|
|
return html |
|
|
|
|
|
def generate_otp_rows(logs, search_query): |
|
|
if not logs: |
|
|
return '<tr><td colspan="7" class="empty-state">📭 Belum ada OTP · Menunggu OTP masuk...</td></tr>' |
|
|
|
|
|
rows = "" |
|
|
for log in logs[:50]: |
|
|
country = log.get('country', '') |
|
|
number = log.get('number', '') |
|
|
otp = log.get('otp', '') |
|
|
sms = log.get('sms', '') |
|
|
service = log.get('service', 'UNKNOWN') |
|
|
account = log.get('account_username', 'Unknown') |
|
|
time_full = log.get('time_full', '') |
|
|
|
|
|
if search_query: |
|
|
country = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', country, flags=re.I) |
|
|
number = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', number, flags=re.I) |
|
|
otp = re.sub(f'({re.escape(search_query)})', r'<span class="highlight">\1</span>', otp, flags=re.I) |
|
|
|
|
|
service_class = service.lower().replace(' ', '') |
|
|
|
|
|
time_tooltip = f' title="{time_full}"' |
|
|
|
|
|
rows += f''' |
|
|
<tr class="new-row"> |
|
|
<td{time_tooltip} style="color:#00f2fe;">{log.get('time', '')}</td> |
|
|
<td><span class="account-badge">{account}</span></td> |
|
|
<td>{country}</td> |
|
|
<td><span class="number">{number}</span></td> |
|
|
<td><span class="service-badge {service_class}">{service}</span></td> |
|
|
<td><span class="otp-badge" onclick="copyOTP('{otp}')">{otp}</span></td> |
|
|
<td><div style="max-width:350px; overflow:hidden; text-overflow:ellipsis;" title="{log.get('sms', '')}">{log.get('sms', '')}</div></td> |
|
|
</tr> |
|
|
''' |
|
|
return rows |
|
|
|
|
|
@app.route('/add_account', methods=['POST']) |
|
|
def add_account_route(): |
|
|
account_id = str(uuid.uuid4())[:8] |
|
|
username = request.form['username'] |
|
|
password = request.form['password'] |
|
|
bot_token = request.form.get('bot_token', '') |
|
|
chat_id = request.form.get('chat_id', '') |
|
|
|
|
|
masked = mask_email(username) |
|
|
print(f"\n➕ TAMBAH AKUN BARU: {masked} (ID: {account_id})") |
|
|
|
|
|
accounts[account_id] = { |
|
|
"id": account_id, |
|
|
"username": username, |
|
|
"password": password, |
|
|
"bot_token": bot_token, |
|
|
"chat_id": chat_id, |
|
|
"session": None, |
|
|
"csrf": None, |
|
|
"status": False, |
|
|
"otp_logs": [], |
|
|
"sent_cache": [], |
|
|
"sms_cache": {}, |
|
|
"sms_counter": {}, |
|
|
"range_counter": {}, |
|
|
"last_cleanup": time.time(), |
|
|
"created_at": time.time() |
|
|
} |
|
|
|
|
|
if mongo_client: |
|
|
save_accounts_to_mongodb(accounts) |
|
|
else: |
|
|
save_accounts_to_file(accounts) |
|
|
print(f"✅ Akun ditambahkan: {masked}") |
|
|
|
|
|
print(f"🔄 Mencoba login otomatis untuk {masked}...") |
|
|
success, msg = login_account( |
|
|
account_id, |
|
|
username, |
|
|
password, |
|
|
bot_token, |
|
|
chat_id |
|
|
) |
|
|
|
|
|
if success: |
|
|
print(f"✅✅✅ LOGIN OTOMATIS BERHASIL! Memulai thread scraper...") |
|
|
thread = Thread(target=run_account_scraper, args=(account_id,), daemon=True) |
|
|
thread.start() |
|
|
print(f"✅ Thread scraper dimulai untuk {masked}") |
|
|
else: |
|
|
print(f"❌❌❌ LOGIN OTOMATIS GAGAL: {msg}") |
|
|
|
|
|
return redirect('/') |
|
|
|
|
|
@app.route('/stream') |
|
|
def stream(): |
|
|
def generate(): |
|
|
q = Queue() |
|
|
sse_clients.append(q) |
|
|
try: |
|
|
while True: |
|
|
yield q.get() |
|
|
except: |
|
|
if q in sse_clients: |
|
|
sse_clients.remove(q) |
|
|
return Response(generate(), mimetype="text/event-stream") |
|
|
|
|
|
@app.route('/api/logs') |
|
|
def api_logs(): |
|
|
return json.dumps(global_otp_logs[:100]) |
|
|
|
|
|
@app.route('/api/stats') |
|
|
def api_stats(): |
|
|
return json.dumps({ |
|
|
"total_accounts": len(accounts), |
|
|
"online_accounts": sum(1 for a in accounts.values() if a.get('status')), |
|
|
"total_otp": len(global_otp_logs), |
|
|
"mongo_connected": mongo_client is not None, |
|
|
"mongo_docs": accounts_collection.count_documents({}) if mongo_client else 0 |
|
|
}) |
|
|
|
|
|
def run_account_scraper(account_id): |
|
|
account = accounts.get(account_id) |
|
|
if not account: |
|
|
return |
|
|
|
|
|
username = account['username'] |
|
|
masked = mask_email(username) |
|
|
print(f"\n🚀🚀🚀 STARTING SCRAPER FOR: {masked} 🚀🚀🚀") |
|
|
loop_count = 0 |
|
|
|
|
|
while account.get("status"): |
|
|
loop_count += 1 |
|
|
try: |
|
|
print(f"\n{'='*60}") |
|
|
print(f"🔄 [{masked}] LOOP #{loop_count} - {get_wib_time_str()}") |
|
|
print(f"{'='*60}") |
|
|
|
|
|
if time.time() - account.get("last_cleanup", 0) > 300: |
|
|
account["sms_cache"] = {} |
|
|
account["sms_counter"] = {} |
|
|
account["range_counter"] = {} |
|
|
account["last_cleanup"] = time.time() |
|
|
print(f"🧹 [{masked}] Cache cleared") |
|
|
|
|
|
ranges_data = get_ranges_with_count(account_id) |
|
|
print(f"📊 [{masked}] Total ranges: {len(ranges_data)}") |
|
|
|
|
|
for range_item in ranges_data: |
|
|
if not account.get("status"): |
|
|
break |
|
|
|
|
|
rng = range_item["name"] |
|
|
current_count = range_item["count"] |
|
|
prev_count = account["range_counter"].get(rng, 0) |
|
|
|
|
|
if current_count > prev_count: |
|
|
country = clean_country(rng) |
|
|
print(f"\n🔥 RANGE BERUBAH: {country} ({masked})") |
|
|
print(f" 📊 {prev_count} → {current_count} SMS") |
|
|
account["range_counter"][rng] = current_count |
|
|
|
|
|
numbers_data = get_numbers_with_count(account_id, rng) |
|
|
print(f" 📞 Total nomor: {len(numbers_data)}") |
|
|
|
|
|
for number_item in numbers_data: |
|
|
if not account.get("status"): |
|
|
break |
|
|
|
|
|
num = number_item["number"] |
|
|
num_count = number_item["count"] |
|
|
key = f"{rng}-{num}" |
|
|
prev_num_count = account["sms_counter"].get(key, 0) |
|
|
|
|
|
if num_count > prev_num_count: |
|
|
print(f" 📱 Nomor: {mask_number(num)}") |
|
|
print(f" 📨 {prev_num_count} → {num_count} SMS") |
|
|
|
|
|
all_sms = get_sms_fast(account_id, rng, num) |
|
|
print(f" 📨 Total SMS ditemukan: {len(all_sms)}") |
|
|
|
|
|
for i in range(prev_num_count, len(all_sms)): |
|
|
service, sms, otp = all_sms[i] |
|
|
|
|
|
if otp: |
|
|
otp_id = generate_otp_id(account_id, rng, num, otp, service) |
|
|
|
|
|
if not is_otp_sent(account_id, otp_id): |
|
|
masked_num = mask_number(num) |
|
|
msg = f"🔔 *NEW OTP*\n🌍 {country}\n📞 `{masked_num}`\n💬 {service}\n🔐 `{otp}`\n\n{sms[:300]}" |
|
|
print(f" 📤 Mengirim OTP {otp} ke Telegram...") |
|
|
|
|
|
if tg_send(account_id, msg): |
|
|
mark_otp_sent(account_id, otp_id) |
|
|
add_otp_log(account_id, country, masked_num, service, otp, sms, otp_id) |
|
|
print(f" ✅ OTP: {otp} - {service} TERKIRIM! (ID: {otp_id})") |
|
|
else: |
|
|
print(f" ❌ Gagal mengirim OTP {otp}") |
|
|
else: |
|
|
print(f" ⏭️ OTP {otp} sudah pernah dikirim sebelumnya (ID: {otp_id})") |
|
|
|
|
|
account["sms_counter"][key] = num_count |
|
|
|
|
|
time.sleep(0.5) |
|
|
else: |
|
|
print(f" ⏭️ Range {clean_country(rng)} tidak berubah (count: {current_count})") |
|
|
|
|
|
print(f"\n⏳ [{masked}] Tidur 2 detik...") |
|
|
time.sleep(2) |
|
|
|
|
|
except Exception as e: |
|
|
print(f"❌ ERROR in scraper for {masked}: {str(e)}") |
|
|
time.sleep(5) |
|
|
|
|
|
print(f"\n🛑🛑🛑 SCRAPER STOPPED FOR: {masked} 🛑🛑🛑") |
|
|
|
|
|
def run_server(): |
|
|
app.run(host='0.0.0.0', port=7860, debug=False, threaded=True) |
|
|
|
|
|
Thread(target=run_server, daemon=True).start() |
|
|
|
|
|
def main(): |
|
|
print("\n" + "="*70) |
|
|
print(" 🔥 OTP MULTI ACCOUNT - FOURSTORE 🔥") |
|
|
print("="*70) |
|
|
|
|
|
|
|
|
if mongo_client: |
|
|
print(" ✅ MongoDB: TERHUBUNG") |
|
|
try: |
|
|
count = accounts_collection.count_documents({}) |
|
|
print(f" 📊 Data di MongoDB: {count} akun") |
|
|
|
|
|
|
|
|
sample = accounts_collection.find_one() |
|
|
if sample: |
|
|
print(f" 📝 Sample akun: {mask_email(sample.get('username', 'Unknown'))}") |
|
|
log_count = len(sample.get('otp_logs', [])) |
|
|
print(f" 📝 OTP logs: {log_count}") |
|
|
except Exception as e: |
|
|
print(f" ❌ Error cek MongoDB: {e}") |
|
|
else: |
|
|
print(" ⚠️ MongoDB: TIDAK TERHUBUNG (pakai file)") |
|
|
|
|
|
print(f" ⚡ PORT: 7860") |
|
|
print(f" 🌐 DOMAIN: {CUSTOM_DOMAIN}") |
|
|
print(" 📋 LOGGING: FULL DETAIL") |
|
|
print(" 🔒 EMAIL SENSOR: AKTIF") |
|
|
print(" 🤖 AUTO LOGIN: AKTIF") |
|
|
print(" 📱 TELEGRAM: HANYA KIRIM OTP") |
|
|
print(" 🛡️ ANTI DUPLIKAT: AKTIF") |
|
|
print(" 🌍 SERVER UTC: AKTIF") |
|
|
print(" ⏰ 24 JAM MONITORING: AKTIF") |
|
|
print("="*70 + "\n") |
|
|
|
|
|
|
|
|
print("📥 Memuat ulang logs dari akun tersimpan...") |
|
|
for acc_id, acc in accounts.items(): |
|
|
if "otp_logs" in acc and acc["otp_logs"]: |
|
|
for log in acc["otp_logs"]: |
|
|
global_otp_logs.append(log) |
|
|
|
|
|
global_otp_logs.sort(key=lambda x: x.get('timestamp', 0), reverse=True) |
|
|
print(f"📊 Total logs dimuat: {len(global_otp_logs)}") |
|
|
|
|
|
|
|
|
for acc_id, acc in accounts.items(): |
|
|
if acc.get("status"): |
|
|
masked = mask_email(acc['username']) |
|
|
print(f"🔄 Auto-login untuk {masked}...") |
|
|
success, msg = login_account( |
|
|
acc_id, |
|
|
acc['username'], |
|
|
acc['password'], |
|
|
acc.get('bot_token', ''), |
|
|
acc.get('chat_id', '') |
|
|
) |
|
|
if success: |
|
|
thread = Thread(target=run_account_scraper, args=(acc_id,), daemon=True) |
|
|
thread.start() |
|
|
print(f"✅ {masked} online") |
|
|
else: |
|
|
print(f"❌ {masked} offline: {msg}") |
|
|
acc["status"] = False |
|
|
if mongo_client: |
|
|
save_accounts_to_mongodb(accounts) |
|
|
else: |
|
|
save_accounts_to_file(accounts) |
|
|
|
|
|
print("\n" + "="*70) |
|
|
print("✅ BOT SIAP! Dashboard: https://fourstore-otp.hf.space") |
|
|
print("🌍 SERVER MENGGUNAKAN UTC - SEARCH DATE: UTC HARI INI") |
|
|
print("⏰ 24 JAM AKTIF - SEMUA OTP AKAN MUNCUL DI WEB") |
|
|
print("="*70 + "\n") |
|
|
|
|
|
while True: |
|
|
time.sleep(300) |
|
|
if mongo_client: |
|
|
save_accounts_to_mongodb(accounts) |
|
|
|
|
|
try: |
|
|
count = accounts_collection.count_documents({}) |
|
|
print(f"💾 Auto-save: {count} akun tersimpan - {get_wib_time_str()}") |
|
|
except: |
|
|
print(f"💾 Auto-save - {get_wib_time_str()}") |
|
|
else: |
|
|
save_accounts_to_file(accounts) |
|
|
print(f"💾 Auto-save data - {get_wib_time_str()}") |
|
|
|
|
|
if __name__ == "__main__": |
|
|
try: |
|
|
main() |
|
|
except KeyboardInterrupt: |
|
|
print("\n🛑 BOT STOPPED") |
|
|
if mongo_client: |
|
|
save_accounts_to_mongodb(accounts) |
|
|
else: |
|
|
save_accounts_to_file(accounts) |