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!") # Cek isi database 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") # Tampilkan sample 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) # Batasi logs 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}") # Load accounts 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"""
{CUSTOM_DOMAIN} ยท {get_wib_time().strftime('%d %B %Y')}
| WIB | Akun | Country | Number | Service | OTP | Message |
|---|