import logging import json import os import subprocess import time import asyncio import datetime import random import string import re import sys import html import zipfile from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, InputFile from telegram.constants import ParseMode from telegram.ext import ( ApplicationBuilder, ContextTypes, CommandHandler, CallbackQueryHandler, MessageHandler, filters, ConversationHandler ) from telegram.error import InvalidToken # --- Global Configuration & Paths (mirip ZIVPN) --- # Coba pakai /etc/upcloud, kalau gak bisa tulis pakai folder lokal DIR_PATH = "/etc/upcloud" try: os.makedirs(DIR_PATH, exist_ok=True) # test write with open(os.path.join(DIR_PATH, ".test"), "w") as f: f.write("test") os.remove(os.path.join(DIR_PATH, ".test")) except: DIR_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "upcloud_data") os.makedirs(DIR_PATH, exist_ok=True) STOCK_FILE = f"{DIR_PATH}/stok.txt" RESELLER_DB = f"{DIR_PATH}/resellers.json" MEMBER_DB = f"{DIR_PATH}/all_members.json" SETTINGS_DB = f"{DIR_PATH}/settings.json" QRIS_IMAGE = f"{DIR_PATH}/qris.jpg" QRIS_IMAGE_PNG = f"{DIR_PATH}/qris.png" QRIS_IMAGE_JPEG = f"{DIR_PATH}/qris.jpeg" def get_qris_path(): for path in [QRIS_IMAGE, QRIS_IMAGE_PNG, QRIS_IMAGE_JPEG, f"{DIR_PATH}/qris.JPG", f"{DIR_PATH}/qris.PNG"]: if os.path.exists(path): return path return None INCOME_DB = f"{DIR_PATH}/income_log.json" TRX_COUNTER_FILE = f"{DIR_PATH}/transaction_counter.txt" BOT_CONFIG = f"{DIR_PATH}/bot_config.sh" SOLD_DB = f"{DIR_PATH}/sold_log.json" ADMIN_DB = f"{DIR_PATH}/admins.json" # untuk multi admin # --- Backup Script --- BACKUP_SCRIPT_PATH = "/usr/local/bin/upcloud-autobackup.sh" # --- Global Variable for Uptime --- BOT_START_TIME = time.time() # --- Logging Setup --- logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) logger = logging.getLogger(__name__) # --- Conversation States --- ( SELECTING_ACTION, ADD_STOCK_INPUT, ADD_BALANCE_ID, ADD_BALANCE_AMOUNT, SET_PRICE_INPUT, SET_RESELLER_PRICE_INPUT, ADD_RESELLER, DEL_RESELLER, BROADCAST_MSG, BUY_CONFIRM ) = range(10) # --- Helper Functions (ala ZIVPN) --- def get_config_value(key): if not os.path.exists(BOT_CONFIG): # fallback ke .env if key == "BOT_TOKEN": return os.getenv("BOT_TOKEN") if key == "CHAT_ID": return os.getenv("CHAT_ID") if key == "CHANNEL_ID": return os.getenv("CHANNEL_ID") return None try: with open(BOT_CONFIG, 'r') as f: content = f.read() pattern = r'^\s*(?:export\s+)?' + re.escape(key) + r'\s*=\s*(["\']?)(.*?)\1\s*$' match = re.search(pattern, content, re.MULTILINE) if match: return match.group(2).strip() except: pass return os.getenv(key) def load_json(filepath, default=None): if default is None: default = {} if "resellers" in filepath or "settings" in filepath else [] if not os.path.exists(filepath): return default try: with open(filepath, 'r') as f: return json.load(f) except: return default def save_json(filepath, data): with open(filepath, 'w') as f: json.dump(data, f, indent=4) # --- LOGIKA DATABASE & ROLE --- def load_resellers_data(): if not os.path.exists(RESELLER_DB): return {} try: with open(RESELLER_DB, 'r') as f: data = json.load(f) if isinstance(data, list): new_data = {} for r_id in data: new_data[str(r_id)] = {"balance": 0, "role": "member"} save_json(RESELLER_DB, new_data) return new_data return data except: return {} def save_resellers_data(data): save_json(RESELLER_DB, data) def get_reseller_balance(user_id): data = load_resellers_data() return data.get(str(user_id), {}).get("balance", 0) def update_reseller_balance(user_id, amount): data = load_resellers_data() str_id = str(user_id) if str_id not in data: data[str_id] = {"balance": 0, "role": "member"} current = data[str_id].get("balance", 0) data[str_id]["balance"] = current + amount save_resellers_data(data) return data[str_id]["balance"] def check_is_reseller(user_id): data = load_resellers_data() user_data = data.get(str(user_id), {}) return user_data.get("role") == "reseller" def load_admins(): # multi admin support admins = set() # dari bot_config CHAT_ID bisa comma separated chat_id_raw = get_config_value("CHAT_ID") or "" if chat_id_raw: for cid in chat_id_raw.split(","): cid = cid.strip() if cid.isdigit(): admins.add(cid) # dari admins.json if os.path.exists(ADMIN_DB): try: with open(ADMIN_DB, 'r') as f: data = json.load(f) for a in data: admins.add(str(a)) except: pass # dari resellers yang role admin resellers = load_resellers_data() for uid, info in resellers.items(): if info.get("role") == "admin": admins.add(str(uid)) return admins def save_admins(admin_list): save_json(ADMIN_DB, list(admin_list)) def is_admin(user_id): return str(user_id) in load_admins() def check_is_admin_role(user_id): return is_admin(user_id) def register_member(user_id): if not os.path.exists(MEMBER_DB): save_json(MEMBER_DB, []) members = load_json(MEMBER_DB, []) if user_id not in members: members.append(user_id) save_json(MEMBER_DB, members) # --- HARGA --- def get_account_price(): data = load_json(SETTINGS_DB, {}) if not isinstance(data, dict): return 15000 return data.get("account_price", 15000) def get_reseller_price(): data = load_json(SETTINGS_DB, {}) if not isinstance(data, dict): return 10000 return data.get("reseller_price", 10000) def set_account_price(price): data = load_json(SETTINGS_DB, {}) if not isinstance(data, dict): data = {} data["account_price"] = int(price) save_json(SETTINGS_DB, data) # --- STOK UPCLOUD (INTI FITUR) --- def load_stock(): if not os.path.exists(STOCK_FILE): open(STOCK_FILE, 'w').close() return [] with open(STOCK_FILE, 'r', encoding='utf-8', errors='ignore') as f: return [line.strip() for line in f if line.strip()] def save_stock(stock_list): with open(STOCK_FILE, 'w', encoding='utf-8') as f: f.write("\n".join(stock_list)) def get_stock_count(): return len(load_stock()) def pop_stock(): stok = load_stock() if not stok: return None akun = stok.pop(0) save_stock(stok) return akun def add_stock_baru(list_akun: list): stok = load_stock() baru = [] ucat_pattern = re.compile(r'ucat_[A-Za-z0-9]+', re.IGNORECASE) for raw in list_akun: a = raw.strip() if not a: continue found = ucat_pattern.findall(a) if found: for clean in found: clean = clean.strip() if not clean: continue if clean in stok or clean in baru: continue if len(clean) > 10: baru.append(clean) continue else: clean = re.sub(r'^\s*\d+[\.\)\-\s]+', '', a).strip() clean = re.sub(r'^[\-\*\โ€ข\>]+\s*', '', clean).strip() if not clean: continue if clean in stok or clean in baru: continue if len(clean) > 10: baru.append(clean) stok.extend(baru) save_stock(stok) return len(baru) def remove_duplicate_stock(): stok = load_stock() unique = list(dict.fromkeys(stok)) removed = len(stok) - len(unique) save_stock(unique) return removed, len(unique) # --- WIB TIME --- def get_now_wib(): return datetime.datetime.utcnow() + datetime.timedelta(hours=7) def get_indo_date(): now = get_now_wib() return f"{now.strftime('%d')}.{now.strftime('%m')}.{now.strftime('%Y')}" def get_time_wib(): return get_now_wib().strftime("%H:%M:%S WIB") def get_uptime_str(): delta = int(time.time() - BOT_START_TIME) days = delta // 86400 hours = (delta % 86400) // 3600 minutes = (delta % 3600) // 60 seconds = delta % 60 return f"{days}d {hours}h {minutes}m {seconds}s" def format_rupiah(value): try: return f"Rp {int(value):,.0f}".replace(",", ".") except: return str(value) def get_public_ip(): try: return subprocess.getoutput("curl -s ifconfig.me") except: return "127.0.0.1" def get_domain(): return get_public_ip() def get_geo_info(): try: response = subprocess.getoutput("curl -s http://ip-api.com/json") data = json.loads(response) country_code = data.get('countryCode', 'ID') flags = {'ID': '๐Ÿ‡ฎ๐Ÿ‡ฉ', 'SG': '๐Ÿ‡ธ๐Ÿ‡ฌ', 'MY': '๐Ÿ‡ฒ๐Ÿ‡พ', 'US': '๐Ÿ‡บ๐Ÿ‡ธ'} return f"{country_code}-SERVER {flags.get(country_code, '๐Ÿณ๏ธ')}" except: return "ID-SERVER ๐Ÿ‡ฎ๐Ÿ‡ฉ" def mask_string(text): if not text or text == "-": return "-" text = str(text).strip() if len(text) > 5: return text[:3] + "xxx" + text[-3:] elif len(text) > 0: return text[0] + "xxx" return "xxx" # --- TRANSAKSI --- def get_next_transaction_id(): current = 1 if os.path.exists(TRX_COUNTER_FILE): try: with open(TRX_COUNTER_FILE, 'r') as f: current = int(f.read().strip()) except: current = 1 with open(TRX_COUNTER_FILE, 'w') as f: f.write(str(current + 1)) return current def record_income(user_id, username, amount, description=""): if not os.path.exists(INCOME_DB): save_json(INCOME_DB, []) logs = load_json(INCOME_DB, []) entry = { "timestamp": int(time.time()), "user_id": str(user_id), "username": str(username), "amount": int(amount), "description": description } logs.append(entry) save_json(INCOME_DB, logs) def get_income_summary(since_timestamp=0, user_id_filter=None): logs = load_json(INCOME_DB, []) if os.path.exists(INCOME_DB) else [] filtered_logs = [ log for log in logs if log["timestamp"] >= since_timestamp and (user_id_filter is None or log["user_id"] == str(user_id_filter)) ] if not filtered_logs: return {"total": 0, "count": 0, "avg": 0, "max": 0} amounts = [log["amount"] for log in filtered_logs] return { "total": sum(amounts), "count": len(amounts), "avg": sum(amounts) // len(amounts), "max": max(amounts) } def record_sold(user_id, username, akun, price): logs = load_json(SOLD_DB, []) logs.append({ "waktu": get_indo_date() + " " + get_time_wib(), "timestamp": int(time.time()), "user_id": str(user_id), "username": str(username), "akun": akun, "price": price }) save_json(SOLD_DB, logs) async def send_log_to_channel(context, user_data, akun, price, role_user, remaining_stock): channel_ids_raw = get_config_value("CHANNEL_ID") if not channel_ids_raw or channel_ids_raw.strip() == "": return chat_ids = [cid.strip() for cid in channel_ids_raw.split(",") if cid.strip()] if not chat_ids: return trx_id = f"#{get_next_transaction_id()}" saldo_keluar = "Gratis" if role_user == "Admin" else format_rupiah(price) icon_role = "๐Ÿ‘‘" if role_user == "Admin" else ("๐Ÿ†" if role_user == "Reseller" else "๐Ÿ‘ค") date_now = get_indo_date() time_now = get_time_wib() msg = ( "โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ\n" " ๐Ÿ“ฆ UPCLOUD TERJUAL ๐Ÿ“ฆ\n" "โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ\n\n" f"๐Ÿ“’ No Trx : {trx_id}\n" f"๐ŸŒ€ Status : {role_user} {icon_role}\n" f"๐Ÿ‘ค Username : @{html.escape(user_data['username']) if user_data['username'] else '-'}\n" f"๐Ÿ†” ID : {user_data['id']}\n\n" f"๐Ÿ”‘ Akun : {html.escape(akun)}\n" f"๐Ÿ’ณ Harga : {saldo_keluar}\n" f"๐Ÿ“ฆ Sisa Stok : {remaining_stock} akun\n\n" f"๐Ÿ“… Tanggal : {date_now}\n" f"โฐ Waktu : {time_now}\n" "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" ) for chat_id in chat_ids: try: await context.bot.send_message(chat_id=chat_id, text=msg, parse_mode=ParseMode.HTML) except Exception as e: logger.error(f"Gagal kirim notifikasi ke {chat_id}: {e}") # --- MAIN MENU --- async def start(update: Update, context: ContextTypes.DEFAULT_TYPE, force_new=False): register_member(update.effective_user.id) user = update.effective_user user_id = str(user.id) safe_username = f"@{user.username}" if user.username else html.escape(user.first_name) saldo = 0 status_user = "Member" if is_admin(user_id): status_user = "Admin" saldo = "โˆž (Unlimited)" else: balance = get_reseller_balance(user_id) if check_is_reseller(user_id): status_user = "Reseller" else: status_user = "Member" saldo = format_rupiah(balance) if balance > 0 else "Rp 0" total_stock = get_stock_count() sold_logs = load_json(SOLD_DB, []) total_sold = len(sold_logs) uptime = get_uptime_str() waktu_skr = get_time_wib() tanggal_skr = get_indo_date() current_price = format_rupiah(get_account_price()) reseller_price = format_rupiah(get_reseller_price()) now_wib = get_now_wib() today_wib_midnight = now_wib.replace(hour=0, minute=0, second=0, microsecond=0) start_of_day = (today_wib_midnight - datetime.timedelta(hours=7)).timestamp() start_week_wib = (now_wib - datetime.timedelta(days=now_wib.weekday())).replace(hour=0, minute=0, second=0, microsecond=0) start_of_week = (start_week_wib - datetime.timedelta(hours=7)).timestamp() stat_daily = 0 stat_weekly = 0 for s in sold_logs: created_at = s.get('timestamp', 0) if created_at >= start_of_day: stat_daily += 1 if created_at >= start_of_week: stat_weekly += 1 text = ( f"๐Ÿ“ฆโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”๐Ÿ“ฆ\n" f" โœจ UPCLOUD STORE โœจ\n" f"๐Ÿ“ฆโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”๐Ÿ“ฆ\n\n" f"selamat datang di UPCloud Auto Store ๐Ÿ’Ž\n" f"jual akun upcloud format ucat_... otomatis ๐Ÿš€\n\n" f"๐Ÿงญ informasi akun\n" f"โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“\n" f"โ”ƒ ๐Ÿ’ฐ saldo: {saldo}\n" f"โ”ƒ ๐Ÿ‘ค status: {status_user}\n" f"โ”ƒ ๐ŸŒ username: {safe_username}\n" f"โ”ƒ ๐Ÿ†” id pengguna: {user_id}\n" f"โ”ƒ โฑ๏ธ bot aktif: {uptime}\n" f"โ”—โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”›\n\n" f"๐ŸŽฏ harga & stok\n" f"โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“\n" f"โ”ƒ ๐Ÿท๏ธ harga member: {current_price} / akun\n" f"โ”ƒ ๐Ÿท๏ธ harga reseller: {reseller_price} / akun\n" f"โ”ƒ ๐Ÿ“ฆ sisa stok: {total_stock} akun\n" f"โ”ƒ ๐Ÿ” format: ucat_...\n" f"โ”—โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”›\n\n" f"๐ŸŒ statistik penjualan\n" f"โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“\n" f"โ”ƒ ๐Ÿ“† hari ini: {stat_daily} akun\n" f"โ”ƒ ๐Ÿ“… minggu ini: {stat_weekly} akun\n" f"โ”ƒ ๐Ÿ—“๏ธ total terjual: {total_sold} akun\n" f"โ”—โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”›\n\n" f"๐Ÿ•’ waktu & server\n" f"โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“\n" f"โ”ƒ ๐Ÿงญ waktu: {waktu_skr}\n" f"โ”ƒ ๐Ÿ“… tanggal: {tanggal_skr}\n" f"โ”ƒ ๐Ÿ–ฅ๏ธ stok ready: {total_stock}\n" f"โ”—โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”›\n\n" f"๐Ÿ“ฆโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”๐Ÿ“ฆ\n" f" ๐ŸŒ dikelola oleh Admin\n" f"๐Ÿ“ฆโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”๐Ÿ“ฆ" ) buttons = [ [InlineKeyboardButton("๐Ÿ›’ Beli Akun", callback_data='menu_buy'), InlineKeyboardButton("๐Ÿ“ฆ Cek Stok", callback_data='menu_stock')], [InlineKeyboardButton("๐Ÿ’ณ Topup Saldo", callback_data='menu_topup'), InlineKeyboardButton("๐Ÿ“Š Riwayat Beli", callback_data='menu_my_history')], ] if is_admin(user_id): buttons.append([InlineKeyboardButton("๐Ÿ“ฆ Kelola Stok", callback_data='menu_stock_manage')]) buttons.append([InlineKeyboardButton("๐Ÿ‘ฅ Manage Resellers & Saldo", callback_data='menu_resellers')]) buttons.append([InlineKeyboardButton("๐Ÿ“ข Broadcast", callback_data='menu_broadcast'), InlineKeyboardButton("๐Ÿ’พ Backup Data", callback_data='menu_backup')]) buttons.append([InlineKeyboardButton("๐Ÿ“ˆ Laporan Pendapatan", callback_data='menu_income')]) reply_markup = InlineKeyboardMarkup(buttons) if update.callback_query: await update.callback_query.answer() if force_new: await context.bot.send_message(chat_id=update.effective_chat.id, text=text, reply_markup=reply_markup, parse_mode=ParseMode.HTML) else: try: await update.callback_query.edit_message_text(text=text, reply_markup=reply_markup, parse_mode=ParseMode.HTML) except: await context.bot.send_message(chat_id=update.effective_chat.id, text=text, reply_markup=reply_markup, parse_mode=ParseMode.HTML) else: await update.message.reply_text(text, reply_markup=reply_markup, parse_mode=ParseMode.HTML) return SELECTING_ACTION # --- CEK STOK --- async def action_stock(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() count = get_stock_count() if count > 0: text = f"๐Ÿ“ฆ STOK UPCLOUD READY\n\nSisa stok: {count} akun\nFormat: ucat_...\n\nHarga Member: {format_rupiah(get_account_price())}\nHarga Reseller: {format_rupiah(get_reseller_price())}\n\nKlik Beli untuk mendapatkan akun otomatis." else: text = f"๐Ÿ˜ฅ STOK HABIS\n\nSisa stok: 0 akun\nHubungi admin untuk restok." keyboard = [[InlineKeyboardButton("๐Ÿ›’ Beli Sekarang", callback_data='menu_buy'), InlineKeyboardButton("๐Ÿ”™ Kembali", callback_data='menu_back')]] await query.edit_message_text(text, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard)) return SELECTING_ACTION # --- BELI AKUN --- async def action_buy(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() user_id = str(update.effective_user.id) stock = get_stock_count() if stock == 0: await query.edit_message_text("โŒ Stok Habis!\nTidak bisa membeli saat ini.", parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Kembali", callback_data='menu_back')]])) return SELECTING_ACTION # harga berdasarkan role if is_admin(user_id): price = 0 role = "Admin" elif check_is_reseller(user_id): price = get_reseller_price() role = "Reseller" else: price = get_account_price() role = "Member" if not is_admin(user_id): balance = get_reseller_balance(user_id) if balance < price: await query.edit_message_text( f"๐Ÿšซ Saldo Tidak Cukup!\n\nSaldo kamu: {format_rupiah(balance)}\nHarga akun ({role}): {format_rupiah(price)}\nSisa stok: {stock} akun\n\nSilakan Topup dulu.", parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ’ณ Topup Saldo", callback_data='menu_topup'), InlineKeyboardButton("๐Ÿ”™ Kembali", callback_data='menu_back')]]) ) return SELECTING_ACTION # Proses ambil stok akun = pop_stock() if not akun: await query.edit_message_text("โŒ Gagal ambil stok, coba lagi.", parse_mode=ParseMode.HTML) return SELECTING_ACTION # potong saldo jika bukan admin if not is_admin(user_id): update_reseller_balance(user_id, -price) record_income(user_id, update.effective_user.username or "-", price, f"Beli {akun}") record_sold(user_id, update.effective_user.username or "-", akun, price) remaining = get_stock_count() user_data_log = {"id": user_id, "username": update.effective_user.username or "-" } await send_log_to_channel(context, user_data_log, akun, price, role, remaining) # Kirim ke pembeli text = ( f"โœ… PEMBELIAN BERHASIL!\n\n" f"๐Ÿ”‘ Akun UpCloud kamu:\n{html.escape(akun)}\n\n" f"๐Ÿ’ฐ Harga: {format_rupiah(price) if price>0 else 'Gratis (Admin)'}\n" f"๐Ÿ“ฆ Sisa Stok: {remaining} akun\n" f"๐Ÿ“… Tanggal: {get_indo_date()} {get_time_wib()}\n\n" f"โš ๏ธ Simpan baik-baik, jangan share!\n" f"๐Ÿ“ Format: ucat_...\n\n" f"Terima kasih sudah membeli!" ) keyboard = [[InlineKeyboardButton("๐Ÿ›’ Beli Lagi", callback_data='menu_buy'), InlineKeyboardButton("๐Ÿ”™ Menu Utama", callback_data='menu_back_new')]] await query.edit_message_text(text, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup(keyboard)) # kirim juga sebagai pesan baru biar gampang copy await context.bot.send_message(chat_id=update.effective_chat.id, text=f"๐Ÿ“‹ Copy akun:\n{html.escape(akun)}", parse_mode=ParseMode.HTML) return SELECTING_ACTION # --- RIWAYAT BELI USER --- async def action_my_history(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() user_id = str(update.effective_user.id) logs = load_json(SOLD_DB, []) my_logs = [l for l in logs if l["user_id"] == user_id][-10:] if not my_logs: text = "๐Ÿ“Š Belum ada riwayat pembelian." else: text = f"๐Ÿ“Š 10 Pembelian Terakhir Kamu:\n\n" for l in reversed(my_logs): text += f"[{l['waktu']}] - {html.escape(l['akun'])} - {format_rupiah(l['price'])}\n" await query.edit_message_text(text, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Kembali", callback_data='menu_back')]])) return SELECTING_ACTION # --- TOPUP --- async def action_topup(update: Update, context: ContextTypes.DEFAULT_TYPE): q = update.callback_query await q.answer() price_member = format_rupiah(get_account_price()) price_reseller = format_rupiah(get_reseller_price()) uid = update.effective_user.id saldo = get_reseller_balance(uid) admin_uname = "@Nizwara" txt = ( "๐Ÿ’ณ TOPUP MANUAL - QRIS\n\n" "1. Scan QR dibawah ini\n" "2. Transfer sesuai nominal\n" f"3. Kirim bukti ke Admin: {admin_uname}\n" f"4. Sertakan ID: {uid}\n\n" f"Harga Member: {price_member}/akun\n" f"Harga Reseller: {price_reseller}/akun\n" f"Saldo kamu: {format_rupiah(saldo)}\n\n" "Admin akan add saldo manual" ) qris_path = get_qris_path() # GTN style - simple open if qris_path and os.path.exists(qris_path): try: # cek valid if os.path.getsize(qris_path) == 0: raise ValueError("File QRIS kosong") with open(qris_path, 'rb') as photo: await context.bot.send_photo(chat_id=update.effective_chat.id, photo=photo, caption=txt, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Kembali", callback_data='menu_back')]])) try: await q.delete_message() except: pass except Exception as e: logger.warning(f"send_photo QRIS gagal {qris_path}: {e}, coba document") try: with open(qris_path, 'rb') as doc: await context.bot.send_document(chat_id=update.effective_chat.id, document=doc, filename=os.path.basename(qris_path), caption=txt, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Kembali", callback_data='menu_back')]])) try: await q.delete_message() except: pass except Exception as e2: logger.error(f"Gagal kirim QRIS {qris_path}: {e2}") await q.edit_message_text(txt + f"\n\nโš ๏ธ QRIS error: {e2}\nFile: {qris_path}", parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Kembali", callback_data='menu_back')]])) else: await context.bot.send_message(chat_id=update.effective_chat.id, text=txt + "\n\nโš ๏ธ QR belum ada di /etc/upcloud/qris.jpg\nUpload file qris.jpg di folder tersebut atau copy dari /etc/gtnrdp/qris.jpg", parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Kembali", callback_data='menu_back')]])) try: await q.delete_message() except: pass return SELECTING_ACTION async def stock_manage_menu(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() if not is_admin(update.effective_user.id): await query.message.reply_text("โ›” Hanya admin!") return SELECTING_ACTION count = get_stock_count() text = ( f"๐Ÿ“ฆ KELOLA STOK UPCLOUD\n\n" f"Sisa stok: {count} akun\n" f"File: {STOCK_FILE}\n\n" f"Pilih aksi:" ) buttons = [ [InlineKeyboardButton("โž• Tambah Stok (Paste)", callback_data='stock_add_paste'), InlineKeyboardButton("๐Ÿ“Ž Upload File .txt", callback_data='stock_add_info')], [InlineKeyboardButton("๐Ÿ“‹ List 20 Stok", callback_data='stock_list'), InlineKeyboardButton(f"๐Ÿ“Š Jumlah: {count}", callback_data='stock_count')], [InlineKeyboardButton("๐Ÿงน Hapus Duplikat", callback_data='stock_dedup'), InlineKeyboardButton("๐Ÿ’พ Backup Stok", callback_data='stock_backup')], [InlineKeyboardButton("๐Ÿ”™ Menu Utama", callback_data='menu_back')] ] await query.edit_message_text(text, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup(buttons)) return SELECTING_ACTION async def stock_list_action(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() stok = load_stock() if not stok: text = "๐Ÿ“ฆ Stok kosong" else: preview = "\n".join([f"{i+1}. {s}" for i, s in enumerate(stok[:20])]) text = f"๐Ÿ“ฆ Total {len(stok)} akun. 20 teratas:\n
{html.escape(preview)}
\n...dan {max(0, len(stok)-20)} lainnya" await query.edit_message_text(text, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Kelola Stok", callback_data='menu_stock_manage')]])) return SELECTING_ACTION async def stock_count_action(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() count = get_stock_count() await query.edit_message_text(f"๐Ÿ“Š Jumlah stok: {count} akun", parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Kelola Stok", callback_data='menu_stock_manage')]])) return SELECTING_ACTION async def stock_dedup_action(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() removed, total = remove_duplicate_stock() await query.edit_message_text(f"๐Ÿงน Duplikat dihapus: {removed}\nTotal sekarang: {total}", parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Kelola Stok", callback_data='menu_stock_manage')]])) return SELECTING_ACTION async def stock_backup_action(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() if not os.path.exists(STOCK_FILE): await query.edit_message_text("โŒ stok.txt tidak ada") return SELECTING_ACTION try: await context.bot.send_document(chat_id=update.effective_chat.id, document=open(STOCK_FILE, 'rb'), filename="stok_backup.txt", caption=f"๐Ÿ“ฆ Backup stok {get_stock_count()} akun - {get_indo_date()}") await query.edit_message_text("โœ… Backup stok dikirim", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Kelola Stok", callback_data='menu_stock_manage')]]), parse_mode=ParseMode.HTML) except Exception as e: await query.edit_message_text(f"โŒ Gagal backup: {e}") return SELECTING_ACTION async def stock_add_paste_start(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() if not is_admin(update.effective_user.id): return SELECTING_ACTION await query.edit_message_text( "๐Ÿ“ฅ Tambah Stok UpCloud\n\nKirim list akun format:\nucat_01M4PVA1M32KRFB7MX9RXGCMVT\n\nSatu baris satu akun. Bisa paste banyak sekaligus.\n\nKetik /cancel untuk batal.", parse_mode=ParseMode.HTML ) return ADD_STOCK_INPUT async def stock_add_paste_input(update: Update, context: ContextTypes.DEFAULT_TYPE): text = update.message.text or "" list_akun = [line.strip() for line in text.splitlines() if line.strip()] if not list_akun: await update.message.reply_text("โŒ Tidak ada data, coba lagi") return ADD_STOCK_INPUT added = add_stock_baru(list_akun) await update.message.reply_text(f"โœ… Berhasil tambah {added} akun baru!\n๐Ÿ“ฆ Total stok sekarang: {get_stock_count()} akun", parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ“ฆ Kelola Stok", callback_data='menu_stock_manage')]])) return SELECTING_ACTION async def stock_add_file_info(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() await query.edit_message_text("๐Ÿ“Ž Kirim file .txt berisi list akun ucat_...\nBot akan otomatis menambahkannya.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Kelola Stok", callback_data='menu_stock_manage')]])) return SELECTING_ACTION async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE): if not is_admin(update.effective_user.id): await update.message.reply_text("โ›” Hanya admin bisa upload") return SELECTING_ACTION doc = update.message.document file_name = (doc.file_name or "").lower() # Jika upload QRIS (qris.jpg / qris.png / qris.jpeg atau gambar) if "qris" in file_name or file_name.endswith((".jpg", ".jpeg", ".png", ".webp")): if "qris" in file_name or (doc.mime_type and doc.mime_type.startswith("image/")): file = await doc.get_file() ext = ".jpg" if file_name.endswith(".png"): ext = ".png" elif file_name.endswith(".jpeg"): ext = ".jpeg" elif file_name.endswith(".webp"): ext = ".jpg" qris_target = f"{DIR_PATH}/qris{ext}" await file.download_to_drive(qris_target) if ext != ".jpg": try: import shutil shutil.copy(qris_target, QRIS_IMAGE) except: pass await update.message.reply_text(f"โœ… QRIS berhasil diupload!\n๐Ÿ“ File: {qris_target}\nSekarang tombol Topup akan muncul QRIS.", parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Menu", callback_data='menu_back_new')]])) return SELECTING_ACTION # Jika bukan gambar QRIS, anggap file stok txt if not file_name.endswith(".txt"): if not (doc.mime_type and "text" in doc.mime_type): await update.message.reply_text("โŒ Kirim file .txt untuk stok, atau file gambar qris.jpg/qris.png untuk QRIS.\nContoh: stok.txt atau qris.jpg") return SELECTING_ACTION file = await doc.get_file() temp_path = f"{DIR_PATH}/temp_upload.txt" await file.download_to_drive(temp_path) with open(temp_path, 'r', encoding='utf-8', errors='ignore') as f: akun_list = [line.strip() for line in f if line.strip()] added = add_stock_baru(akun_list) try: os.remove(temp_path) except: pass await update.message.reply_text(f"โœ… File diterima!\nBerhasil tambah {added} akun\nTotal stok: {get_stock_count()}", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ“ฆ Kelola Stok", callback_data='menu_stock_manage')]])) return SELECTING_ACTION async def reseller_menu(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() if not is_admin(update.effective_user.id): await query.message.reply_text("โ›” Hanya admin!") return SELECTING_ACTION text = ( "๐Ÿ‘ฅ MANAGE RESELLERS & SALDO\n\n" "Kelola member, reseller, saldo, dan harga akun." ) buttons = [ [InlineKeyboardButton("โž• Tambah Reseller", callback_data='reseller_add'), InlineKeyboardButton("โž– Hapus Reseller", callback_data='reseller_del')], [InlineKeyboardButton("๐Ÿ“‹ List Reseller & Saldo", callback_data='reseller_list')], [InlineKeyboardButton("๐Ÿ’ฐ Isi Saldo Member", callback_data='reseller_balance_add')], [InlineKeyboardButton("๐Ÿท๏ธ Set Harga Member", callback_data='reseller_set_price'), InlineKeyboardButton("๐Ÿท๏ธ Set Harga Reseller", callback_data='reseller_set_reseller_price')], [InlineKeyboardButton("๐Ÿ‘‘ Daftar Admin", callback_data='admin_list'), InlineKeyboardButton("โž• Tambah Admin", callback_data='admin_add')], [InlineKeyboardButton("๐Ÿ”™ Menu Utama", callback_data='menu_back')] ] await query.edit_message_text(text, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup(buttons)) return SELECTING_ACTION async def action_list_resellers(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() resellers = load_resellers_data() msg = f"๐Ÿ“‹ Daftar Member & Saldo ({len(resellers)})\n\n" if not resellers: msg += "Belum ada data." else: sorted_ids = sorted(resellers.keys(), key=lambda x: resellers[x].get('balance', 0), reverse=True) for r_id in sorted_ids[:50]: data = resellers[r_id] balance = format_rupiah(data.get('balance', 0)) role_type = data.get('role', 'member') role_tag = "๐Ÿ‘‘ Admin" if role_type == 'admin' else ("๐Ÿ† Reseller" if role_type == 'reseller' else "๐Ÿ‘ค Member") try: chat = await context.bot.get_chat(chat_id=r_id) name = html.escape(chat.first_name) user_display = f"@{chat.username}" if chat.username else "" msg += f"{role_tag} | {name} {user_display}\n๐Ÿ†” {r_id} | ๐Ÿ’ฐ {balance}\nโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”\n" except: msg += f"{role_tag} | ID {r_id}\n๐Ÿ’ฐ {balance}\nโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”\n" await asyncio.sleep(0.05) if len(msg) > 4000: msg = msg[:4000] + "\n...terpotong" await query.edit_message_text(msg, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Back", callback_data='menu_resellers')]])) return SELECTING_ACTION async def start_add_reseller(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() await query.edit_message_text("โž• Tambah Reseller\nMasukkan Telegram ID:", parse_mode=ParseMode.HTML) return ADD_RESELLER async def add_reseller_input(update: Update, context: ContextTypes.DEFAULT_TYPE): r_id = update.message.text.strip() data = load_resellers_data() if r_id not in data: data[r_id] = {"balance": 0, "role": "reseller"} save_resellers_data(data) await update.message.reply_text(f"โœ… Reseller {r_id} ditambahkan.", parse_mode=ParseMode.HTML) else: data[r_id]["role"] = "reseller" save_resellers_data(data) await update.message.reply_text(f"โœ… User {r_id} diupgrade jadi Reseller.", parse_mode=ParseMode.HTML) await update.message.reply_text("Kembali:", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Menu Reseller", callback_data='menu_resellers')]])) return SELECTING_ACTION async def start_del_reseller(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() await query.edit_message_text("โž– Hapus/Downgrade Reseller\nMasukkan Telegram ID:", parse_mode=ParseMode.HTML) return DEL_RESELLER async def del_reseller_input(update: Update, context: ContextTypes.DEFAULT_TYPE): r_id = update.message.text.strip() data = load_resellers_data() if r_id in data: data[r_id]["role"] = "member" save_resellers_data(data) await update.message.reply_text(f"โœ… Status Reseller {r_id} dicabut.", parse_mode=ParseMode.HTML) else: await update.message.reply_text("โŒ Tidak ditemukan.") await update.message.reply_text("Kembali:", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Menu Reseller", callback_data='menu_resellers')]])) return SELECTING_ACTION async def start_add_balance(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() await query.edit_message_text("๐Ÿ’ฐ Isi Saldo Member\nMasukkan ID Telegram Member/Reseller:", parse_mode=ParseMode.HTML) return ADD_BALANCE_ID async def add_balance_id_input(update: Update, context: ContextTypes.DEFAULT_TYPE): r_id = update.message.text.strip() context.user_data['bal_id'] = r_id await update.message.reply_text(f"โœ… Target ID: {r_id}\nMasukkan Nominal Saldo (angka saja, contoh 20000):", parse_mode=ParseMode.HTML) return ADD_BALANCE_AMOUNT async def add_balance_amount_input(update: Update, context: ContextTypes.DEFAULT_TYPE): try: amount = int(update.message.text.strip()) except ValueError: await update.message.reply_text("โŒ Harus angka!") return ADD_BALANCE_AMOUNT r_id = context.user_data['bal_id'] new_bal = update_reseller_balance(r_id, amount) await update.message.reply_text(f"โœ… Berhasil.\n๐Ÿ’ฐ Saldo Sekarang: {format_rupiah(new_bal)}", parse_mode=ParseMode.HTML) await update.message.reply_text("Kembali:", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Menu Reseller", callback_data='menu_resellers')]])) return SELECTING_ACTION async def start_set_price(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() curr = format_rupiah(get_account_price()) await query.edit_message_text(f"๐Ÿท๏ธ Set Harga Member per Akun\nHarga saat ini: {curr}\nMasukkan harga baru (angka saja):", parse_mode=ParseMode.HTML) return SET_PRICE_INPUT async def set_price_input(update: Update, context: ContextTypes.DEFAULT_TYPE): try: price = int(update.message.text.strip()) except ValueError: await update.message.reply_text("โŒ Harus angka!") return SET_PRICE_INPUT set_account_price(price) await update.message.reply_text(f"โœ… Harga member diubah menjadi: {format_rupiah(price)}", parse_mode=ParseMode.HTML) await update.message.reply_text("Kembali:", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Menu Reseller", callback_data='menu_resellers')]])) return SELECTING_ACTION async def start_set_reseller_price(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() curr = format_rupiah(get_reseller_price()) await query.edit_message_text(f"๐Ÿท๏ธ Set Harga Reseller per Akun\nHarga saat ini: {curr}\nMasukkan harga baru:", parse_mode=ParseMode.HTML) return SET_RESELLER_PRICE_INPUT async def set_reseller_price_input(update: Update, context: ContextTypes.DEFAULT_TYPE): try: price = int(update.message.text.strip()) except ValueError: return SET_RESELLER_PRICE_INPUT data = load_json(SETTINGS_DB, {}) if not isinstance(data, dict): data = {} data["reseller_price"] = int(price) save_json(SETTINGS_DB, data) await update.message.reply_text(f"โœ… Harga reseller diubah menjadi: {format_rupiah(price)}", parse_mode=ParseMode.HTML) await update.message.reply_text("Kembali:", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Menu Reseller", callback_data='menu_resellers')]])) return SELECTING_ACTION async def admin_list_action(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() admins = load_admins() text = f"๐Ÿ‘‘ Daftar Admin ({len(admins)})\n\n" + "\n".join([f"โ€ข {a}" for a in admins]) await query.edit_message_text(text, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Kembali", callback_data='menu_resellers')]])) return SELECTING_ACTION async def admin_add_start(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() await query.edit_message_text("๐Ÿ‘‘ Tambah Admin Baru\nMasukkan ID Telegram admin baru (bisa lebih dari 1, admin multi):", parse_mode=ParseMode.HTML) return ADD_RESELLER # reuse state but handle as admin # Broadcast & Income & Backup mirip ZIVPN async def start_broadcast(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() await query.edit_message_text("๐Ÿ“ข Broadcast Message\n\nSilakan kirim pesan (Teks/Gambar) yang ingin Anda sebarkan ke semua member:", parse_mode=ParseMode.HTML) return BROADCAST_MSG async def process_broadcast(update: Update, context: ContextTypes.DEFAULT_TYPE): members = load_json(MEMBER_DB, []) count = 0 await update.message.reply_text("โณ Sedang mengirim broadcast...") for uid in members: try: await update.message.copy(chat_id=uid) count += 1 except: pass await asyncio.sleep(0.1) await update.message.reply_text(f"โœ… Broadcast Selesai\nPesan terkirim ke: {count} member.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Menu", callback_data='menu_back_new')]]), parse_mode=ParseMode.HTML) return SELECTING_ACTION def ascii_bar(value, max_val, width=10): if max_val == 0: return "โ–‘" * width filled = int((value / max_val) * width) return "โ–ˆ" * filled + "โ–‘" * (width - filled) async def action_income_report(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() if not is_admin(update.effective_user.id): await query.message.reply_text("โ›” Hanya admin!") return SELECTING_ACTION now_wib = get_now_wib() today_wib = now_wib.replace(hour=0, minute=0, second=0, microsecond=0) today_start = (today_wib - datetime.timedelta(hours=7)).timestamp() week_wib = (now_wib - datetime.timedelta(days=now_wib.weekday())).replace(hour=0, minute=0, second=0, microsecond=0) week_start = (week_wib - datetime.timedelta(hours=7)).timestamp() month_wib = now_wib.replace(day=1, hour=0, minute=0, second=0, microsecond=0) month_start = (month_wib - datetime.timedelta(hours=7)).timestamp() all_data = get_income_summary(0) today_data = get_income_summary(today_start) week_data = get_income_summary(week_start) month_data = get_income_summary(month_start) max_income = max(week_data["total"], month_data["total"], 1) bar_week = ascii_bar(week_data["total"], max_income) bar_month = ascii_bar(month_data["total"], max_income) msg = ( "๐Ÿ“ˆ LAPORAN PENDAPATAN UPCLOUD (WIB)\n\n" f"๐Ÿ“† Hari Ini\n ๐Ÿ’ฐ Total: {format_rupiah(today_data['total'])}\n ๐Ÿงพ Transaksi: {today_data['count']}\n\n" f"๐Ÿ“… Minggu Ini\n ๐Ÿ’ฐ Total: {format_rupiah(week_data['total'])}\n ๐Ÿงพ Transaksi: {week_data['count']}\n ๐Ÿ“Š Grafik: {bar_week}\n\n" f"๐Ÿ—“๏ธ Bulan Ini\n ๐Ÿ’ฐ Total: {format_rupiah(month_data['total'])}\n ๐Ÿงพ Transaksi: {month_data['count']}\n ๐Ÿ“Š Grafik: {bar_month}\n\n" f"๐Ÿ’ผ Seumur Hidup\n ๐Ÿ’ฐ Total: {format_rupiah(all_data['total'])}\n ๐Ÿงพ Transaksi: {all_data['count']}\n ๐Ÿ“ˆ Rata-rata: {format_rupiah(all_data['avg'])}\n ๐Ÿฅ‡ Terbesar: {format_rupiah(all_data['max'])}\n\n" f"๐Ÿ“ฆ Sisa Stok: {get_stock_count()} akun\n" f"๐Ÿ—‚๏ธ Total Terjual: {len(load_json(SOLD_DB, []))} akun" ) await query.edit_message_text(msg, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Kembali", callback_data='menu_back')]])) return SELECTING_ACTION async def action_backup(update: Update, context: ContextTypes.DEFAULT_TYPE): query = update.callback_query await query.answer() if not is_admin(update.effective_user.id): await query.message.reply_text("โ›” Hanya admin!") return SELECTING_ACTION try: await query.edit_message_text("โณ Membuat backup...", parse_mode=ParseMode.HTML) zip_path = f"{DIR_PATH}/backup_upcloud_{int(time.time())}.zip" with zipfile.ZipFile(zip_path, 'w') as zf: for fp in [STOCK_FILE, RESELLER_DB, MEMBER_DB, SETTINGS_DB, INCOME_DB, SOLD_DB, ADMIN_DB]: if os.path.exists(fp): zf.write(fp, os.path.basename(fp)) await context.bot.send_document(chat_id=update.effective_chat.id, document=open(zip_path, 'rb'), filename=os.path.basename(zip_path), caption=f"๐Ÿ’พ Backup UpCloud - {get_indo_date()} {get_time_wib()}\nStok: {get_stock_count()} akun") os.remove(zip_path) await context.bot.send_message(chat_id=update.effective_chat.id, text="โœ… Backup selesai!", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Menu", callback_data='menu_back_new')]])) except Exception as e: await query.message.reply_text(f"โŒ Gagal backup: {e}") return SELECTING_ACTION # --- Navigation --- async def back_to_main(update: Update, context: ContextTypes.DEFAULT_TYPE): return await start(update, context) async def back_to_main_new(update: Update, context: ContextTypes.DEFAULT_TYPE): return await start(update, context, force_new=True) async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text("๐Ÿšซ Cancelled.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("๐Ÿ”™ Main Menu", callback_data='menu_back')]])) return SELECTING_ACTION # --- Main Function --- def main(): try: token = get_config_value("BOT_TOKEN") # load .env kalau ada try: from dotenv import load_dotenv load_dotenv() if not token: token = os.getenv("BOT_TOKEN") except: pass logger.info("--- Starting UPCLOUD Bot (ZIVPN Style) ---") if not token: logger.critical("BOT_TOKEN missing di /etc/upcloud/bot_config.sh atau .env - Buat file dengan isi BOT_TOKEN=... dan CHAT_ID=...") print("โŒ BOT_TOKEN belum di set! Buat file /etc/upcloud/bot_config.sh dengan:\nBOT_TOKEN=token_bot_kamu\nCHAT_ID=id_admin_kamu (bisa multi: 123,456,789)\n") return app = ApplicationBuilder().token(token).build() conv_handler = ConversationHandler( entry_points=[ CommandHandler("start", start), CommandHandler("menu", start), CallbackQueryHandler(back_to_main, pattern='^menu_back$'), CallbackQueryHandler(back_to_main_new, pattern='^menu_back_new$') ], states={ SELECTING_ACTION: [ CallbackQueryHandler(action_buy, pattern='^menu_buy$'), CallbackQueryHandler(action_stock, pattern='^menu_stock$'), CallbackQueryHandler(action_topup, pattern='^menu_topup$'), CallbackQueryHandler(action_my_history, pattern='^menu_my_history$'), CallbackQueryHandler(stock_manage_menu, pattern='^menu_stock_manage$'), CallbackQueryHandler(stock_list_action, pattern='^stock_list$'), CallbackQueryHandler(stock_count_action, pattern='^stock_count$'), CallbackQueryHandler(stock_dedup_action, pattern='^stock_dedup$'), CallbackQueryHandler(stock_backup_action, pattern='^stock_backup$'), CallbackQueryHandler(stock_add_paste_start, pattern='^stock_add_paste$'), CallbackQueryHandler(stock_add_file_info, pattern='^stock_add_info$'), CallbackQueryHandler(reseller_menu, pattern='^menu_resellers$'), CallbackQueryHandler(start_add_reseller, pattern='^reseller_add$'), CallbackQueryHandler(start_del_reseller, pattern='^reseller_del$'), CallbackQueryHandler(action_list_resellers, pattern='^reseller_list$'), CallbackQueryHandler(start_add_balance, pattern='^reseller_balance_add$'), CallbackQueryHandler(start_set_price, pattern='^reseller_set_price$'), CallbackQueryHandler(start_set_reseller_price, pattern='^reseller_set_reseller_price$'), CallbackQueryHandler(admin_list_action, pattern='^admin_list$'), CallbackQueryHandler(admin_add_start, pattern='^admin_add$'), CallbackQueryHandler(start_broadcast, pattern='^menu_broadcast$'), CallbackQueryHandler(action_income_report, pattern='^menu_income$'), CallbackQueryHandler(action_backup, pattern='^menu_backup$'), CallbackQueryHandler(back_to_main, pattern='^menu_back$'), CallbackQueryHandler(back_to_main_new, pattern='^menu_back_new$') ], ADD_STOCK_INPUT: [MessageHandler(filters.TEXT & ~filters.COMMAND, stock_add_paste_input)], ADD_BALANCE_ID: [MessageHandler(filters.TEXT & ~filters.COMMAND, add_balance_id_input)], ADD_BALANCE_AMOUNT: [MessageHandler(filters.TEXT & ~filters.COMMAND, add_balance_amount_input)], SET_PRICE_INPUT: [MessageHandler(filters.TEXT & ~filters.COMMAND, set_price_input)], SET_RESELLER_PRICE_INPUT: [MessageHandler(filters.TEXT & ~filters.COMMAND, set_reseller_price_input)], ADD_RESELLER: [MessageHandler(filters.TEXT & ~filters.COMMAND, add_reseller_input)], DEL_RESELLER: [MessageHandler(filters.TEXT & ~filters.COMMAND, del_reseller_input)], BROADCAST_MSG: [MessageHandler(filters.ALL & ~filters.COMMAND, process_broadcast)], }, fallbacks=[CommandHandler("cancel", cancel), CommandHandler("start", start), CommandHandler("menu", start)] ) app.add_handler(conv_handler) app.add_handler(MessageHandler(filters.Document.ALL, handle_document)) logger.info(f"Bot polling... DIR={DIR_PATH} Stok={get_stock_count()}") print(f"Bot jalan... DIR={DIR_PATH} Stok={get_stock_count()} akun") app.run_polling() except Exception as e: logger.critical(f"Error: {e}") import traceback; traceback.print_exc() time.sleep(5) if __name__ == '__main__': main()