""" Recap Studio — Telegram Bot """ import os, sys, json, uuid, glob, shutil, logging, threading, time, re, subprocess from pathlib import Path logging.basicConfig(format='%(asctime)s [%(levelname)s] %(name)s: %(message)s', level=logging.INFO) logger = logging.getLogger(__name__) from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, KeyboardButton from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, ConversationHandler, filters, ContextTypes from telegram.constants import ParseMode sys.path.insert(0, str(Path(__file__).parent)) from app import ( login_user, get_coins, deduct, load_db, save_db, gen_uname, create_user_fn, add_coins_fn, set_coins_fn, call_api, parse_out, split_txt, dur, run_tts_sync, run_gemini_tts_sync, #ytdlp_download, cpu_queue_wait, upd_stat, _build_video, ADMIN_U, SYS_MOVIE, SYS_MED, BASE_DIR, OUTPUT_DIR, NUM_TO_MM_RULE, run_stage, ytdlp_download, upd_stat, load_payments_db, save_payments_db, ) ADMIN_TELEGRAM_CHAT_ID = os.getenv('ADMIN_TELEGRAM_CHAT_ID', '') pending_payment = {} # cid -> {'coins':int,'price':int} try: import whisper as whisper_mod except ImportError: whisper_mod = None BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', '') ADMIN_TG_USERNAME = os.getenv('ADMIN_TG_USERNAME', 'PhoeShan2001') WEB_BASE_URL = os.getenv('WEB_BASE_URL', 'https://recap.psonline.shop') TG_MAX_FILE_BYTES = 49 * 1024 * 1024 # 49 MB — Telegram bot upload limit (ST_MAIN, ST_LOGIN_USER, ST_LOGIN_PASS, ST_AWAIT_VIDEO) = range(4) sessions = {} _whisper_model = [None] _wm_lock = threading.Lock() cancel_flags = {} # ── Per-user busy flag (one job at a time) ── user_busy = {} # cid -> bool _busy_lock = threading.Lock() def is_processing(cid): with _busy_lock: return user_busy.get(cid, False) def set_processing(cid, val): with _busy_lock: user_busy[cid] = val def start_job(bot, cid, pending_url, pending_file_id, prog_msg_id): """Start processing in background thread.""" threading.Thread( target=_process_thread, args=(bot, cid, prog_msg_id, pending_url, pending_file_id), daemon=True ).start() MS_VOICES = [ ('Thiha (ကျား)', 'my-MM-ThihaNeural', 'ms'), ('Nilar (မိန်း)', 'my-MM-NilarNeural', 'ms'), ] GEMINI_VOICES = [ ('Kore','Kore','gemini'),('Charon','Charon','gemini'),('Fenrir','Fenrir','gemini'), ('Leda','Leda','gemini'),('Orus','Orus','gemini'),('Puck','Puck','gemini'), ('Aoede','Aoede','gemini'),('Zephyr','Zephyr','gemini'),('Achelois','Achelois','gemini'), ('Pegasus','Pegasus','gemini'),('Perseus','Perseus','gemini'),('Schedar','Schedar','gemini'), ] PACKAGES = [ (10, 10000, 90, 'Process 5 ကြိမ်'), (20, 18000, 160, 'Process 10 ကြိမ် — အကောင်းဆုံး'), (30, 27000, 240, 'Process 15 ကြိမ်'), (60, 54000, 450, 'Process 30 ကြိမ်'), ] # PromptPay number (same as app.py default) PROMPTPAY_NUM = os.getenv('PROMPTPAY_NUMBER', '0951266012') # ── HELPERS ── def sess(cid): if cid not in sessions: sessions[cid] = { 'username': None, 'coins': 0, 'is_admin': False, 'voice': 'my-MM-ThihaNeural', 'engine': 'ms', 'speed': 30, 'crop': 'original', 'flip': False, 'color': False, 'watermark': '', 'content_type': 'Movie Recap', 'ai_model': 'Gemini', 'pending_url': None, 'pending_file_id': None, 'music_file_id': None, } s = sessions[cid] # Auto-sync coins from DB so admin top-ups reflect without re-login if s.get('username'): try: fresh = get_coins(s['username']) if fresh is not None: s['coins'] = fresh except Exception: pass return s def is_logged(cid): return sess(cid).get('username') is not None def fmt_coins(c): return '∞' if c == -1 else str(c) def main_kb(cid): s = sess(cid) rows = [ [KeyboardButton('🎬 Auto Process'), KeyboardButton('🔊 အသံ')], [KeyboardButton('⚙️ Settings'), KeyboardButton('👤 အကောင့်')], [KeyboardButton('🛒 Coins ဝယ်ရန်'), KeyboardButton('🔄 Reset')], ] if s.get('is_admin'): rows.append([KeyboardButton('👑 Admin')]) rows.append([KeyboardButton('🚪 Logout')]) return ReplyKeyboardMarkup(rows, resize_keyboard=True) def ensure_whisper(): with _wm_lock: if _whisper_model[0] is None and whisper_mod: _whisper_model[0] = whisper_mod.load_model('tiny', device='cpu') return _whisper_model[0] def settings_kb(cid): s = sess(cid) crop_labels = {'original':'🎬 Original','9:16':'📱 9:16','16:9':'🖥️ 16:9','1:1':'⬛ 1:1'} return InlineKeyboardMarkup([ [InlineKeyboardButton(f"📐 Crop: {crop_labels.get(s['crop'],s['crop'])}", callback_data='set|crop')], [InlineKeyboardButton(f"🤖 AI: {s['ai_model']}", callback_data='set|ai'), InlineKeyboardButton(f"📺 {s['content_type'].split('/')[0]}", callback_data='set|ct')], [InlineKeyboardButton(f"Flip: {'ON' if s['flip'] else 'OFF'}", callback_data='set|flip'), InlineKeyboardButton(f"Color: {'ON' if s['color'] else 'OFF'}", callback_data='set|color')], [InlineKeyboardButton(f"Speed: {s['speed']}%", callback_data='set|speed')], [InlineKeyboardButton(f"Watermark: {s['watermark'] or 'မရှိ'}", callback_data='set|wmk')], [InlineKeyboardButton(f"🎵 BG Music: {'✅ ပါမည်' if s.get('music_file_id') else '❌ မပါ'}", callback_data='set|music'), InlineKeyboardButton('🗑️ Music ဖျက်', callback_data='set|music_del')], [InlineKeyboardButton('✅ သိမ်းမည်', callback_data='set|done')], ]) def voice_kb(): btns = [[InlineKeyboardButton('── Microsoft TTS ──', callback_data='noop')]] row = [] for name, vid, eng in MS_VOICES: row.append(InlineKeyboardButton(name, callback_data=f'voice|{vid}|{eng}')) btns.append(row) btns.append([InlineKeyboardButton('── Gemini TTS ──', callback_data='noop')]) row = [] for name, vid, eng in GEMINI_VOICES: row.append(InlineKeyboardButton(name, callback_data=f'voice|{vid}|{eng}')) if len(row) == 3: btns.append(row); row = [] if row: btns.append(row) return InlineKeyboardMarkup(btns) def package_kb(method='mmk'): btns = [] for coins, mmk, thb, desc in PACKAGES: if method == 'thb': label = f"🪙 {coins} Coins — {thb} ฿ ({desc})" btns.append([InlineKeyboardButton(label, callback_data=f'pkg|{coins}|{thb}|thb')]) else: label = f"🪙 {coins} Coins — {mmk:,} MMK ({desc})" btns.append([InlineKeyboardButton(label, callback_data=f'pkg|{coins}|{mmk}|mmk')]) # Method switch button if method == 'thb': btns.append([InlineKeyboardButton('🇲🇲 MMK ဖြင့်ကြည့်မည်', callback_data='pkg_method|mmk')]) else: btns.append([InlineKeyboardButton('🇹🇭 THB / PromptPay ဖြင့်ကြည့်မည်', callback_data='pkg_method|thb')]) return InlineKeyboardMarkup(btns) PAYMENT_INFO = ( "💳 *ငွေလွဲနည်း*\n\n" "📱 *KBZPay / Wave*\n" "👤 *Phoe Shan* 📞 *09679871352*\n\n" "⚡ *PromptPay* (Thai)\n" f"📞 *{PROMPTPAY_NUM}*\n\n" "ငွေလွဲပြီးနောက် slip ကို Admin ထံ ပို့ပေးပါ" ) def cancel_kb(): return InlineKeyboardMarkup([[InlineKeyboardButton('❌ ဖျက်သိမ်းမည်', callback_data='cancel|process')]]) # ── AUTH ── async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE): cid = update.effective_chat.id if is_logged(cid): s = sess(cid) await update.message.reply_text( f"👋 ပြန်လာသည့်အတွက် ကြိုဆိုပါတယ် *{s['username']}*!\n🪙 Coins: *{fmt_coins(s['coins'])}*", parse_mode=ParseMode.MARKDOWN, reply_markup=main_kb(cid)) return ST_MAIN await update.message.reply_text( "🎬 *Recap Studio Bot*\n\n" "AI-Powered Video Recap Tool\n\n" "━━━━━━━━━━━━━━━━━━━\n" "👨‍💻 *Developer* — @PhoeShan2001\n" "━━━━━━━━━━━━━━━━━━━\n\n" "သင့် *username* ထည့်ပါ —", parse_mode=ParseMode.MARKDOWN, reply_markup=ReplyKeyboardMarkup( [[KeyboardButton('/start')]], resize_keyboard=True )) return ST_LOGIN_USER async def recv_login_user(update: Update, ctx: ContextTypes.DEFAULT_TYPE): ctx.user_data['login_user'] = update.message.text.strip() await update.message.reply_text("🔑 *Password* ထည့်ပါ\n_(မရှိရင် `-` ပို့ပါ)_", parse_mode=ParseMode.MARKDOWN) return ST_LOGIN_PASS async def recv_login_pass(update: Update, ctx: ContextTypes.DEFAULT_TYPE): cid = update.effective_chat.id u = ctx.user_data.get('login_user', '') p = update.message.text.strip() if p == '-': p = '' ok, msg, coins = login_user(u, p) if not ok: await update.message.reply_text(f"❌ {msg}\n\n*Username* ထပ်ထည့်ပါ —", parse_mode=ParseMode.MARKDOWN) return ST_LOGIN_USER s = sess(cid) s['username'] = u; s['coins'] = coins; s['is_admin'] = (u == ADMIN_U) # Save tg_chat_id so approve/reject works via global handler try: db = load_db() if u in db['users']: db['users'][u]['tg_chat_id'] = cid save_db(db) except Exception: pass await update.message.reply_text( f"✅ *{u}* အနေဖြင့် ဝင်ရောက်ပြီးပါပြီ\n🪙 Coins: *{fmt_coins(coins)}*", parse_mode=ParseMode.MARKDOWN, reply_markup=main_kb(cid)) return ST_MAIN async def cmd_logout(update: Update, ctx: ContextTypes.DEFAULT_TYPE): sessions.pop(update.effective_chat.id, None) await update.message.reply_text("👋 Logout လုပ်ပြီးပါပြီ\n/start နှိပ်ပြီး ပြန်ဝင်ပါ", reply_markup=ReplyKeyboardMarkup([[]], resize_keyboard=True)) return ConversationHandler.END # ── MENU BUTTONS ── async def btn_auto_process(update: Update, ctx: ContextTypes.DEFAULT_TYPE): cid = update.effective_chat.id if not is_logged(cid): await update.message.reply_text("❌ /start နှိပ်ပြီး ဦးစွာ login ဝင်ပါ") return ST_MAIN s = sess(cid) if s['coins'] != -1 and s['coins'] < 1: await update.message.reply_text( f"❌ Coins မလုံလောက်ဘူး\nသင့်မှာ *{s['coins']}* ရှိတယ် — *1* လိုတယ်\n\n🛒 Coins ဝယ်ရန် ခလုတ်နှိပ်ပါ", parse_mode=ParseMode.MARKDOWN, reply_markup=main_kb(cid)) return ST_MAIN cancel_flags[cid] = False await update.message.reply_text( "📥 *Auto Process* — 1 Coin ကုန်မည်\n\n" "အောက်ပါ link တစ်ခုခု ပို့ပါ သို့မဟုတ် Video File တင်ပါ —\n\n" "• YouTube\n• TikTok\n• Facebook\n• Instagram\n\n" "_(ဖျက်သိမ်းလိုရင် ❌ နှိပ်ပါ)_", parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton('❌ ဖျက်သိမ်းမည်', callback_data='cancel|await')]])) return ST_AWAIT_VIDEO async def btn_voice(update: Update, ctx: ContextTypes.DEFAULT_TYPE): cid = update.effective_chat.id s = sess(cid) await update.message.reply_text( f"🔊 *အသံ ရွေးချယ်ရန်*\n\nလက်ရှိ — *{s['voice']}* ({s['engine'].upper()})", parse_mode=ParseMode.MARKDOWN, reply_markup=voice_kb()) return ST_MAIN async def btn_settings(update: Update, ctx: ContextTypes.DEFAULT_TYPE): cid = update.effective_chat.id await update.message.reply_text("⚙️ *Settings*", parse_mode=ParseMode.MARKDOWN, reply_markup=settings_kb(cid)) return ST_MAIN async def btn_account(update: Update, ctx: ContextTypes.DEFAULT_TYPE): cid = update.effective_chat.id if not is_logged(cid): await update.message.reply_text("❌ /start နှိပ်ပါ") return ST_MAIN s = sess(cid); db = load_db(); u = db['users'].get(s['username'], {}) await update.message.reply_text( f"👤 *ကိုယ့်အကောင့်*\n\nUsername: `{s['username']}`\n🪙 Coins: *{fmt_coins(s['coins'])}*\n" f"🎬 Video: {u.get('total_videos',0)} ကြိမ်\n📝 Transcript: {u.get('total_transcripts',0)} ကြိမ်\n" f"📅 စတင်သောနေ့: {u.get('created_at','')[:10]}", parse_mode=ParseMode.MARKDOWN, reply_markup=main_kb(cid)) return ST_MAIN async def btn_reset(update: Update, ctx: ContextTypes.DEFAULT_TYPE): cid = update.effective_chat.id # Cancel any running process if is_processing(cid): cancel_flags[cid] = True # Clear session state but keep login s = sess(cid) s['pending_url'] = None s['pending_file_id'] = None await update.message.reply_text( "🔄 *Reset လုပ်ပြီးပါပြီ*\n\nဘာလုပ်မလဲ?", parse_mode=ParseMode.MARKDOWN, reply_markup=main_kb(cid) ) return ST_MAIN async def btn_buy(update: Update, ctx: ContextTypes.DEFAULT_TYPE): cid = update.effective_chat.id s = sess(cid) await update.message.reply_text( f"🛒 *Coins ဝယ်ရန်*\n\n" f"လက်ရှိ Coins: *{fmt_coins(s['coins'])}*\n\n" f"{PAYMENT_INFO}\n\n" f"Package တစ်ခု ရွေးပါ —", parse_mode=ParseMode.MARKDOWN, reply_markup=package_kb()) return ST_MAIN async def btn_admin(update: Update, ctx: ContextTypes.DEFAULT_TYPE): cid = update.effective_chat.id s = sess(cid) if not s.get('is_admin'): await update.message.reply_text("❌ Admin သာ ဝင်ခွင့်ရှိသည်") return ST_MAIN db = load_db(); users = db.get('users', {}) lines = [f"👑 *Admin Panel* — {len(users)} ယောက်\n"] for uname, udata in list(users.items())[:20]: lines.append(f"• `{uname}` — 🪙{udata.get('coins',0)}") await update.message.reply_text('\n'.join(lines), parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup([ [InlineKeyboardButton('➕ User ဖန်တီး', callback_data='adm|create'), InlineKeyboardButton('🪙 Coins ထည့်', callback_data='adm|coins')], [InlineKeyboardButton('🗑️ User ဖျက်', callback_data='adm|delete'), InlineKeyboardButton('🔄 Refresh', callback_data='adm|refresh')], [InlineKeyboardButton('⏳ Pending Payments', callback_data='adm|pending')], ])) return ST_MAIN # ── VIDEO INPUT ── async def recv_video_input(update: Update, ctx: ContextTypes.DEFAULT_TYPE): cid = update.effective_chat.id s = sess(cid) if cancel_flags.get(cid): await update.message.reply_text("⛔ ဖျက်သိမ်းပြီးပါပြီ", reply_markup=main_kb(cid)) return ST_MAIN if update.message.text: url = update.message.text.strip() if not re.match(r'https?://', url): await update.message.reply_text("❌ URL မမှန်ဘူး\nYouTube / TikTok / Facebook / Instagram link ပို့ပါ") return ST_AWAIT_VIDEO s['pending_url'] = url; s['pending_file_id'] = None elif update.message.video or update.message.document: media = update.message.video or update.message.document s['pending_file_id'] = media.file_id; s['pending_url'] = None else: await update.message.reply_text("❌ URL သို့မဟုတ် Video File ပို့ပါ") return ST_AWAIT_VIDEO # ── Reject if already processing ── if is_processing(cid): await update.message.reply_text( "⏳ *လုပ်ဆောင်နေဆဲဖြစ်သည်*\n\nပြီးမှ ထပ်ပို့ပါ", parse_mode=ParseMode.MARKDOWN ) return ST_MAIN # ── Coin check before starting ── fresh_coins = get_coins(s['username']) if fresh_coins is not None: s['coins'] = fresh_coins if not s['is_admin'] and s['coins'] != -1 and s['coins'] < 1: await update.message.reply_text( f"❌ *Coins မလုံလောက်ဘူး*\n\n" f"သင့်မှာ *{s['coins']}* ရှိတယ် — *1* လိုတယ်\n\n" f"🛒 Coins ဝယ်ရန် ခလုတ်နှိပ်ပါ", parse_mode=ParseMode.MARKDOWN, reply_markup=main_kb(cid) ) return ST_MAIN cancel_flags[cid] = False prog_msg = await update.message.reply_text( "⏳ *စတင်နေပါသည်…* ခနစောင့်ပါ", parse_mode=ParseMode.MARKDOWN, reply_markup=cancel_kb() ) start_job(ctx.bot, cid, s.get('pending_url'), s.get('pending_file_id'), prog_msg.message_id) return ST_MAIN # ── PROCESSING ── def _process_thread(bot, cid, prog_msg_id, pending_url=None, pending_file_id=None): set_processing(cid, True) import asyncio as _asyncio loop = _asyncio.new_event_loop() _asyncio.set_event_loop(loop) try: loop.run_until_complete(_do_process(bot, cid, prog_msg_id, pending_url, pending_file_id)) finally: loop.close() set_processing(cid, False) async def _do_process(bot, cid, prog_msg_id, pending_url=None, pending_file_id=None): s = sess(cid) tid = uuid.uuid4().hex[:8] tmp_dir = str(BASE_DIR / f'temp_{tid}') out_file = str(OUTPUT_DIR / f'final_{tid}.mp4') os.makedirs(tmp_dir, exist_ok=True) async def prog(text, show_cancel=False): try: await bot.edit_message_text(chat_id=cid, message_id=prog_msg_id, text=text, parse_mode=ParseMode.MARKDOWN, reply_markup=cancel_kb() if show_cancel else None) except Exception: pass def is_cancelled(): return cancel_flags.get(cid, False) try: # ── Coin check at start of each job (important for queued jobs) ── if not s['is_admin']: fresh = get_coins(s['username']) if fresh is not None: s['coins'] = fresh if s['coins'] != -1 and s['coins'] < 1: await prog( f"❌ *Coins မလုံလောက်ဘူး*\n\n" f"သင့်မှာ *{fmt_coins(s['coins'])}* ရှိတယ် — *1* လိုတယ်\n" f"🛒 Coins ဝယ်ပြီးနောက် ထပ်ကြိုးစားပါ" ) return await prog("📥 *အဆင့် 1/5* — Video ဒေါင်းလုပ် လုပ်နေသည်…", show_cancel=True) vpath = None if pending_file_id: file_obj = await bot.get_file(pending_file_id) vpath = f'{tmp_dir}/input.mp4' await file_obj.download_to_drive(vpath) elif pending_url: # cpu_queue_wait() if is_cancelled(): await prog("⛔ ဖျက်သိမ်းပြီးပါပြီ"); return ytdlp_download(f'{tmp_dir}/input.%(ext)s', pending_url) found = glob.glob(f'{tmp_dir}/input.*') if found: vpath = found[0] if not vpath or not os.path.exists(vpath): await prog("❌ Video ဒေါင်းလုပ် မအောင်မြင်ပါ"); return if is_cancelled(): await prog("⛔ ဖျက်သိမ်းပြီးပါပြီ"); return # Download background music if set mpath = None if s.get('music_file_id'): try: mfile = await bot.get_file(s['music_file_id']) mpath = f'{tmp_dir}/bgmusic.mp3' await mfile.download_to_drive(mpath) except Exception: mpath = None await prog("🎙️ *အဆင့် 2/5* — Whisper ဖြင့် ကူးယူနေသည်…", show_cancel=True) if whisper_mod is None: await prog("❌ Whisper မ install ရသေးပါ"); return model = ensure_whisper() res = run_stage('whisper', model.transcribe, 'bot', lambda p,m: None, '⏳ Whisper စောင့်နေသည်', '🎙️ Transcribing…', vpath, fp16=False) tr = res['text']; src_lang = res.get('language', 'en') if is_cancelled(): await prog("⛔ ဖျက်သိမ်းပြီးပါပြီ"); return await prog(f"🤖 *အဆင့် 3/5* — {s['ai_model']} ဖြင့် Script ရေးနေသည်…", show_cancel=True) sys_p = SYS_MED if s['content_type'] == 'Medical/Health' else SYS_MOVIE sys_p = sys_p + '\n' + NUM_TO_MM_RULE out_txt, _ = run_stage('ai', call_api, 'bot', lambda p,m: None, '⏳ AI စောင့်နေသည်', '🤖 AI Script…', [{'role':'system','content':sys_p},{'role':'user','content':f'Language:{src_lang}\n\n{tr}'}], api=s['ai_model']) sc, caption_text, hashtags = parse_out(out_txt) if is_cancelled(): await prog("⛔ ဖျက်သိမ်းပြီးပါပြီ"); return await prog("🔊 *အဆင့် 4/5* — အသံ ထုတ်နေသည်…", show_cancel=True) sentences = split_txt(sc) import asyncio as _aio, functools as _ft loop = _aio.get_running_loop() if s['engine'] == 'gemini': parts = await loop.run_in_executor(None, _ft.partial( run_stage, 'tts', run_gemini_tts_sync, 'bot', lambda p,m: None, '⏳ TTS စောင့်နေသည်', '🔊 TTS…', sentences, s['voice'], tmp_dir, speed=s['speed'])) else: parts = await loop.run_in_executor(None, _ft.partial( run_stage, 'tts', run_tts_sync, 'bot', lambda p,m: None, '⏳ TTS စောင့်နေသည်', '🔊 TTS…', sentences, s['voice'], f'+{s["speed"]}%', tmp_dir)) cmb = f'{tmp_dir}/combined.mp3'; lst = f'{tmp_dir}/list.txt' with open(lst, 'w') as lf: for a in parts: lf.write(f"file '{os.path.abspath(a)}'\n") subprocess.run(f'ffmpeg -y -f concat -safe 0 -i "{lst}" -af "silenceremove=start_periods=1:stop_periods=-1:stop_duration=0.1:stop_threshold=-50dB" -c:a libmp3lame -q:a 2 "{cmb}"', shell=True, check=True) if is_cancelled(): await prog("⛔ ဖျက်သိမ်းပြီးပါပြီ"); return await prog("🎬 *အဆင့် 5/5* — Video ပေါင်းစပ်နေသည်…") vd = dur(vpath); ad = dur(cmb) if vd <= 0: await prog("❌ Video duration ဖတ်မရပါ"); return if ad <= 0: await prog("❌ Audio duration ဖတ်မရပါ"); return _build_video(vpath, cmb, mpath, ad, vd, s['crop'], s['flip'], s['color'], s['watermark'], out_file) if not s['is_admin']: ok2, rem = deduct(s['username'], 1) if ok2: s['coins'] = rem; upd_stat(s['username'], 'tr'); upd_stat(s['username'], 'vd') await prog("✅ ပြီးပါပြီ! ပို့နေသည်…") file_size = os.path.getsize(out_file) caption = ( f"🎬 *{caption_text}*\n\n{hashtags}\n\n" f"🪙 ကျန် Coins: *{fmt_coins(s['coins'])}*" ) send_file = out_file # file to actually send if file_size > TG_MAX_FILE_BYTES: size_mb = file_size / (1024 * 1024) await prog(f"📦 File ကြီးနေသည် ({size_mb:.1f} MB) — Compress လုပ်နေသည်…") compressed_file = out_file.replace('.mp4', '_compressed.mp4') try: # Two-pass target: aim for 45 MB to stay safely under 49 MB limit target_size_kb = 45 * 1024 # Get video duration for bitrate calculation probe = subprocess.run( ['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', out_file], capture_output=True, text=True ) video_duration = float(probe.stdout.strip()) if probe.stdout.strip() else 60.0 # Target total bitrate (kbps), reserve 128kbps for audio target_total_kbps = int((target_size_kb * 8) / video_duration) video_kbps = max(target_total_kbps - 128, 200) compress_cmd = ( f'ffmpeg -y -i "{out_file}" ' f'-c:v libx264 -b:v {video_kbps}k -preset fast -maxrate {video_kbps*2}k -bufsize {video_kbps*4}k ' f'-c:a aac -b:a 128k ' f'"{compressed_file}"' ) subprocess.run(compress_cmd, shell=True, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) compressed_size = os.path.getsize(compressed_file) if compressed_size <= TG_MAX_FILE_BYTES: send_file = compressed_file logger.info(f"[{tid}] Compressed {size_mb:.1f}MB → {compressed_size/1024/1024:.1f}MB") else: logger.warning(f"[{tid}] Compress မအောင်မြင် — {compressed_size/1024/1024:.1f}MB ကျန်နေသေးသည်") send_file = None # fall through to link except Exception as ce: logger.warning(f"[{tid}] Compress error: {ce}") send_file = None if send_file is None: # Compression failed or still too large — send link file_name = Path(out_file).name dl_url = f"{WEB_BASE_URL}/outputs/{file_name}" await bot.send_message( chat_id=cid, text=( f"🎬 *{caption_text}*\n\n" f"{hashtags}\n\n" f"📦 File ကြီးသောကြောင့် ({size_mb:.1f} MB) တိုက်ရိုက် ပို့၍မရပါ\n\n" f"🔗 *Download Link:*\n`{dl_url}`\n\n" f"🪙 ကျန် Coins: *{fmt_coins(s['coins'])}*" ), parse_mode=ParseMode.MARKDOWN, ) send_file = None # skip video send below if send_file is not None: with open(send_file, 'rb') as vf: await bot.send_video( chat_id=cid, video=vf, caption=caption, parse_mode=ParseMode.MARKDOWN, supports_streaming=True, read_timeout=900, write_timeout=900, ) await bot.delete_message(chat_id=cid, message_id=prog_msg_id) except Exception as e: logger.exception(f'[{tid}] Error: {e}') await prog(f"❌ Error: {e}") finally: shutil.rmtree(tmp_dir, ignore_errors=True) s['pending_url'] = None; s['pending_file_id'] = None cancel_flags.pop(cid, None) # ── CALLBACKS ── async def on_callback(update: Update, ctx: ContextTypes.DEFAULT_TYPE): q = update.callback_query cid = q.message.chat_id s = sess(cid) try: await q.answer() except Exception: pass data = q.data if data == 'noop': return # ── Admin: pending payment approve/reject ── if data.startswith('adm_pay|'): # Check admin — session, ADMIN_TELEGRAM_CHAT_ID env, or DB tg_chat_id if not s.get('is_admin') and s.get('username') != ADMIN_U: is_real_admin = False # 1) ADMIN_TELEGRAM_CHAT_ID env var (most reliable, no login needed) if ADMIN_TELEGRAM_CHAT_ID and str(cid) == str(ADMIN_TELEGRAM_CHAT_ID): is_real_admin = True s['username'] = ADMIN_U s['is_admin'] = True else: # 2) Fallback: DB tg_chat_id db = load_db() for uname, udata in db.get('users', {}).items(): if udata.get('tg_chat_id') == cid and uname == ADMIN_U: is_real_admin = True s['username'] = uname s['is_admin'] = True break if not is_real_admin: await q.answer("❌ Admin only", show_alert=True); return parts = data.split('|') action = parts[1] # approve / reject / slip payment_id = parts[2] username = parts[3] from datetime import datetime as _dt pdb = load_payments_db() pay = next((p for p in pdb['payments'] if p['id'] == payment_id), None) if not pay: await q.answer("❌ Payment မတွေ့ပါ", show_alert=True); return if action == 'slip': # Show slip image slip = pay.get('slip_image','') if slip and ',' in slip: import base64, io b64 = slip.split(',',1)[1] buf = io.BytesIO(base64.b64decode(b64)) buf.name = 'slip.jpg' await ctx.bot.send_photo( chat_id=cid, photo=buf, caption=f"🖼 Slip — ID: {payment_id}\n👤 {username}\n🪙 {pay['coins']} Coins — {pay['price']} MMK", reply_markup=InlineKeyboardMarkup([ [InlineKeyboardButton(f"✅ Approve +{pay['coins']} coins", callback_data=f"adm_pay|approve|{payment_id}|{username}|{pay['coins']}")], [InlineKeyboardButton("❌ Reject", callback_data=f"adm_pay|reject|{payment_id}|{username}")], ]) ) else: await q.answer("❌ Slip ပုံ မတွေ့ပါ", show_alert=True) return if pay['status'] != 'pending': await q.answer(f"⚠️ ဒီ payment {pay['status']} ပြီးပြီ", show_alert=True); return if action == 'approve': coins_add = int(parts[4]) if len(parts) > 4 else pay['coins'] # Let Flask API handle status update + coins + HF sync (don't pre-save here) try: import urllib.request as _ur, json as _json _payload = _json.dumps({ 'caller': os.getenv('ADMIN_USERNAME', ''), 'payment_id': payment_id, }).encode() _req = _ur.Request( 'http://localhost:7860/api/admin/payment/approve', data=_payload, headers={'Content-Type': 'application/json'}) res = _ur.urlopen(_req, timeout=10) res_data = _json.loads(res.read()) if not res_data.get('ok'): await q.answer(f"❌ {res_data.get('msg','Approve failed')}", show_alert=True) return except Exception as _e: print(f'[approve api] {_e}') # Fallback: direct update pay['status'] = 'approved' pay['updated_at'] = _dt.now().isoformat() save_payments_db(pdb) add_coins_fn(username, coins_add, 'admin_bot') # Edit original message to show approved status + remove buttons new_cap = (q.message.caption or q.message.text or '') + f"\n\n✅ Approved! +{coins_add} coins → {username}" try: await q.edit_message_caption(caption=new_cap, parse_mode=ParseMode.HTML, reply_markup=None) except: try: await q.edit_message_text(text=new_cap, parse_mode=ParseMode.HTML, reply_markup=None) except: pass await q.answer(f"✅ Approved +{coins_add} coins → {username}", show_alert=False) # Notify user with new balance db = load_db() tg_id = db['users'].get(username, {}).get('tg_chat_id') new_bal = db['users'].get(username, {}).get('coins', 0) if tg_id: try: await ctx.bot.send_message(tg_id, f"🎉 *Coins ထည့်ပြီးပါပြီ!*\n" f"🪙 *+{coins_add} Coins* ရောက်ပြီ\n" f"💰 လက်ကျန် — *{new_bal} Coins*\n" f"🆔 `{payment_id}`", parse_mode=ParseMode.MARKDOWN) except: pass else: # reject pay['status'] = 'rejected' pay['updated_at'] = _dt.now().isoformat() save_payments_db(pdb) new_cap = (q.message.caption or q.message.text or '') + "\n\n❌ Rejected" try: await q.edit_message_caption(caption=new_cap, parse_mode=ParseMode.HTML, reply_markup=None) except: try: await q.edit_message_text(text=new_cap, parse_mode=ParseMode.HTML, reply_markup=None) except: pass await q.answer("❌ Rejected", show_alert=False) db = load_db() tg_id = db['users'].get(username, {}).get('tg_chat_id') if tg_id: try: await ctx.bot.send_message(tg_id, f"❌ *Payment ငြင်းပယ်ခြင်းခံရသည်*\n" f"🆔 `{payment_id}`\n" f"မေးမြန်းရန် — @{ADMIN_TG_USERNAME}", parse_mode=ParseMode.MARKDOWN) except: pass return if data.startswith('cancel|'): cancel_flags[cid] = True await q.edit_message_text("⛔ ဖျက်သိမ်းနေသည်…" if data == 'cancel|process' else "⛔ ဖျက်သိမ်းပြီးပါပြီ") return if data == 'pkg|back': await q.edit_message_text("🛒 *Coins ဝယ်ရန်*\n\nPackage ရွေးပါ —", parse_mode=ParseMode.MARKDOWN, reply_markup=package_kb()) return if data.startswith('pkg_method|'): method = data.split('|')[1] # 'mmk' or 'thb' await q.edit_message_reply_markup(reply_markup=package_kb(method)) return if data.startswith('pkg|'): parts = data.split('|') # parts: pkg | coins | price | method(mmk/thb) coins = int(parts[1]); price = int(parts[2]) method = parts[3] if len(parts) > 3 else 'mmk' pending_payment[cid] = {'coins': coins, 'price': price, 'method': method} ctx.user_data['await_slip'] = True if method == 'thb': # Send PromptPay QR code image try: import urllib.request as _ur, io as _io qr_url = f"{WEB_BASE_URL}/api/payment/promptpay_qr?amount={price}" with _ur.urlopen(qr_url, timeout=10) as resp: img_bytes = resp.read() caption = ( f"⚡ *PromptPay QR Code*\n\n" f"🪙 *{coins} Coins* — *{price} ฿*\n" f"📞 PromptPay: `{PROMPTPAY_NUM}`\n\n" f"QR scan လုပ်ပြီးနောက် *Slip ဓာတ်ပုံ* ဒီ chat ထဲ တိုက်ရိုက်ပို့ပါ" ) await q.message.reply_photo( photo=_io.BytesIO(img_bytes), caption=caption, parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton('🔙 ပြန်သွားမည်', callback_data='pkg|back')]])) await q.delete_message() except Exception as qr_e: logger.warning(f"QR gen failed: {qr_e}") await q.edit_message_text( f"⚡ *PromptPay*\n\n" f"🪙 *{coins} Coins* — *{price} ฿*\n" f"📞 PromptPay: `{PROMPTPAY_NUM}`\n\n" f"📸 ငွေလွဲပြီးနောက် *Slip ဓာတ်ပုံ* ပို့ပါ", parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton('🔙 ပြန်သွားမည်', callback_data='pkg|back')]])) else: price_str = f"{price:,} MMK" await q.edit_message_text( f"🪙 *{coins} Coins* — {price_str}\n\n" f"{PAYMENT_INFO}\n\n" f"📸 ငွေလွဲပြီးနောက် *Slip ဓာတ်ပုံ* ဒီ chat ထဲ တိုက်ရိုက်ပို့ပါ", parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton('🔙 ပြန်သွားမည်', callback_data='pkg|back')]])) return if data.startswith('voice|'): _, vid, eng = data.split('|') s['voice'] = vid; s['engine'] = eng await q.edit_message_text(f"✅ အသံ သတ်မှတ်ပြီး — *{vid}* ({'Microsoft' if eng=='ms' else 'Gemini'})", parse_mode=ParseMode.MARKDOWN) return if data.startswith('set|'): key = data.split('|')[1] if key == 'flip': s['flip'] = not s['flip'] elif key == 'color': s['color'] = not s['color'] elif key == 'crop': crops = ['original','9:16','16:9','1:1'] s['crop'] = crops[(crops.index(s['crop'])+1) % len(crops)] if s['crop'] in crops else 'original' elif key == 'ai': s['ai_model'] = 'DeepSeek' if s['ai_model'] == 'Gemini' else 'Gemini' elif key == 'ct': s['content_type'] = 'Medical/Health' if s['content_type'] == 'Movie Recap' else 'Movie Recap' elif key == 'speed': speeds = [0,10,20,30,40,50,60,80] cur = s['speed'] s['speed'] = speeds[(speeds.index(cur)+1) % len(speeds)] if cur in speeds else 30 elif key == 'wmk': ctx.user_data['await_wmk'] = True await q.edit_message_text("💧 Watermark စာသားပို့ပါ (ဥပမာ — @username):") return elif key == 'music': ctx.user_data['await_music'] = True await q.edit_message_text( "🎵 *Background Music*\n\n" "MP3 / Audio file တင်ပါ\n" "_(ဖိုင်တင်ပြီးနောက် auto သိမ်းမည်)_", parse_mode=ParseMode.MARKDOWN) return elif key == 'music_del': s['music_file_id'] = None await q.answer("🗑️ Music ဖျက်ပြီး", show_alert=False) elif key == 'done': await q.edit_message_text("✅ Settings သိမ်းပြီးပါပြီ"); return await q.edit_message_reply_markup(reply_markup=settings_kb(cid)) return if data.startswith('adm|'): if not s.get('is_admin'): await q.edit_message_text("❌ Admin only"); return action = data.split('|')[1] if action == 'refresh': db = load_db(); users = db.get('users', {}) lines = [f"👑 *Admin Panel* — {len(users)} ယောက်\n"] for uname, udata in list(users.items())[:20]: lines.append(f"• `{uname}` — 🪙{udata.get('coins',0)}") await q.edit_message_text('\n'.join(lines), parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup([ [InlineKeyboardButton('➕ User ဖန်တီး', callback_data='adm|create'), InlineKeyboardButton('🪙 Coins ထည့်', callback_data='adm|coins')], [InlineKeyboardButton('🗑️ User ဖျက်', callback_data='adm|delete'), InlineKeyboardButton('🔄 Refresh', callback_data='adm|refresh')], [InlineKeyboardButton('⏳ Pending Payments', callback_data='adm|pending')], ])) elif action == 'pending': pdb = load_payments_db() pending = [p for p in pdb['payments'] if p['status'] == 'pending'] if not pending: await q.answer("📭 Pending payment မရှိပါ", show_alert=True); return await q.answer() for p in pending[-10:]: coins = p['coins']; price = p.get('price', 0) pid = p['id']; uname = p['username'] slip_exists = bool(p.get('slip_image')) created = p['created_at'][:16].replace('T', ' ') text = ( f"💰 Pending Payment\n" f"👤 {uname}\n" f"🪙 {coins} Coins — {price:,} MMK\n" f"🆔 {pid}\n" f"⏰ {created}" ) kb_rows = [] if slip_exists: kb_rows.append([InlineKeyboardButton("🖼 Slip ကြည့်", callback_data=f"adm_pay|slip|{pid}|{uname}")]) kb_rows.append([ InlineKeyboardButton(f"✅ Approve +{coins}", callback_data=f"adm_pay|approve|{pid}|{uname}|{coins}"), InlineKeyboardButton("❌ Reject", callback_data=f"adm_pay|reject|{pid}|{uname}"), ]) await q.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=InlineKeyboardMarkup(kb_rows)) return elif action == 'create': ctx.user_data['adm_action'] = 'create' await q.edit_message_text("➕ *User ဖန်တီးရန်*\n\n`username coins` ပို့ပါ\nဥပမာ — `CoolUser 10`", parse_mode=ParseMode.MARKDOWN) elif action == 'coins': ctx.user_data['adm_action'] = 'coins' await q.edit_message_text("🪙 *Coins စီမံရန်*\n\n`username amount add|set` ပို့ပါ\nဥပမာ — `CoolUser 20 add`", parse_mode=ParseMode.MARKDOWN) elif action == 'delete': ctx.user_data['adm_action'] = 'delete' await q.edit_message_text("🗑️ ဖျက်မည့် *username* ပို့ပါ —", parse_mode=ParseMode.MARKDOWN) return # ── TEXT HANDLER ── async def error_handler(update: object, ctx: ContextTypes.DEFAULT_TYPE) -> None: """Handle all telegram errors gracefully without crashing.""" import traceback err = ctx.error tb = ''.join(traceback.format_exception(type(err), err, err.__traceback__)) logger.warning(f'[ErrorHandler] {err}\n{tb[:400]}') async def cmd_broadcast(update: Update, ctx: ContextTypes.DEFAULT_TYPE): """Admin: /broadcast — send to all users with tg_chat_id.""" cid = update.effective_chat.id s = sess(cid) if not s.get('is_admin'): await update.message.reply_text("❌ Admin only"); return text = ' '.join(ctx.args) if ctx.args else '' if not text: await update.message.reply_text( "📢 *Broadcast*\\n\\nUsage: `/broadcast `", parse_mode=ParseMode.MARKDOWN); return db = load_db() sent = 0; fail = 0 for uname, udata in db.get('users', {}).items(): tg_id = udata.get('tg_chat_id') if not tg_id: continue try: await ctx.bot.send_message( chat_id=tg_id, text=f"📢 *ကြေငြာချက်*\\n\\n{text}", parse_mode=ParseMode.MARKDOWN) sent += 1 except Exception as e: logger.warning(f'[broadcast] {uname}: {e}') fail += 1 await update.message.reply_text( f"✅ Broadcast ပြီးပါပြီ\\n📨 ပေးပို့: {sent} ယောက်\\n❌ မအောင်မြင်: {fail} ယောက်") async def cmd_pending(update: Update, ctx: ContextTypes.DEFAULT_TYPE): """Admin: list pending payments.""" cid = update.effective_chat.id s = sess(cid) if not s.get('is_admin'): await update.message.reply_text("❌ Admin only"); return pdb = load_payments_db() pending = [p for p in pdb['payments'] if p['status'] == 'pending'] if not pending: await update.message.reply_text("📭 Pending payment မရှိပါ"); return for p in pending[-10:]: # last 10 coins = p['coins']; price = p.get('price', 0) pid = p['id']; uname = p['username'] slip_exists = bool(p.get('slip_image')) created = p['created_at'][:16].replace('T',' ') text = ( f"💰 Pending Payment\n" f"👤 {uname}\n" f"🪙 {coins} Coins — {price:,} MMK\n" f"🆔 {pid}\n" f"⏰ {created}" ) kb = InlineKeyboardMarkup([ ([InlineKeyboardButton("🖼 Slip ကြည့်", callback_data=f"adm_pay|slip|{pid}|{uname}")] if slip_exists else []), [InlineKeyboardButton(f"✅ Approve +{coins}", callback_data=f"adm_pay|approve|{pid}|{uname}|{coins}"), InlineKeyboardButton("❌ Reject", callback_data=f"adm_pay|reject|{pid}|{uname}")], ]) await update.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=kb) # ── MUSIC INPUT HANDLER ── async def recv_music_input(update: Update, ctx: ContextTypes.DEFAULT_TYPE): cid = update.effective_chat.id s = sess(cid) if not ctx.user_data.get('await_music'): # Not expecting music — ignore audio uploads return ST_MAIN media = update.message.audio or update.message.document if not media: await update.message.reply_text("❌ Audio file မဟုတ်ပါ") return ST_MAIN s['music_file_id'] = media.file_id ctx.user_data['await_music'] = False await update.message.reply_text( "✅ Background music သိမ်းပြီးပါပြီ\n" "⚙️ Settings ထဲမှာ ပြနေမည်", reply_markup=main_kb(cid) ) return ST_MAIN async def recv_slip_photo(update: Update, ctx: ContextTypes.DEFAULT_TYPE): """User sends slip photo after selecting package.""" cid = update.effective_chat.id s = sess(cid) if not ctx.user_data.get('await_slip'): await update.message.reply_text("❌ Package အရင်ရွေးပါ 🛒", reply_markup=main_kb(cid)) return ST_MAIN if not update.message.photo: await update.message.reply_text("❌ ဓာတ်ပုံ (Photo) ပို့ပါ", reply_markup=main_kb(cid)) return ST_MAIN pkg = pending_payment.get(cid) if not pkg: await update.message.reply_text("❌ Package မရွေးရသေးပါ", reply_markup=main_kb(cid)) ctx.user_data['await_slip'] = False; return ST_MAIN coins = pkg['coins']; price = pkg['price'] method = pkg.get('method', 'mmk') price_label = f"{price} ฿" if method == 'thb' else f"{price:,} MMK" username = s.get('username','') # Download largest photo photo_obj = update.message.photo[-1] tg_file = await ctx.bot.get_file(photo_obj.file_id) import io, base64 as _b64, uuid as _uuid from datetime import datetime as _dt buf = io.BytesIO() await tg_file.download_to_memory(buf) slip_data_url = "data:image/jpeg;base64," + _b64.b64encode(buf.getvalue()).decode() # Save to payments_db payment_id = _uuid.uuid4().hex[:10] now = _dt.now().isoformat() pdb = load_payments_db() pdb['payments'].append({ 'id': payment_id, 'username': username, 'coins': coins, 'price': price_label, 'status': 'pending', 'created_at': now, 'updated_at': now, 'slip_image': slip_data_url, 'admin_note': '', }) save_payments_db(pdb) # Save tg_chat_id try: db = load_db(); db['users'][username]['tg_chat_id'] = cid; save_db(db) except: pass ctx.user_data['await_slip'] = False pending_payment.pop(cid, None) # Notify admin if ADMIN_TELEGRAM_CHAT_ID: cap = ( f"💰 New Payment Request\n" f"👤 {username}\n" f"🪙 {coins} Coins — {price_label}\n" f"{'⚡ PromptPay (THB)' if method=='thb' else '📱 KBZ/Wave (MMK)'}\n" f"🆔 {payment_id}\n⏰ {now[:19]}" ) kb = InlineKeyboardMarkup([ [InlineKeyboardButton("🖼 Slip ကြည့်", callback_data=f"adm_pay|slip|{payment_id}|{username}")], [InlineKeyboardButton(f"✅ Approve +{coins} coins", callback_data=f"adm_pay|approve|{payment_id}|{username}|{coins}"), InlineKeyboardButton("❌ Reject", callback_data=f"adm_pay|reject|{payment_id}|{username}")], ]) try: buf.seek(0) await ctx.bot.send_photo(chat_id=ADMIN_TELEGRAM_CHAT_ID, photo=buf, caption=cap, parse_mode=ParseMode.HTML, reply_markup=kb) except Exception as e: logger.error(f"Admin notify err: {e}") await update.message.reply_text( f"✅ *Slip လက်ခံပြီးပါပြီ!*\n🪙 *{coins} Coins* — {price_label}\n🆔 `{payment_id}`\n\nAdmin စစ်ဆေးပြီး Coins ထည့်ပေးပါမည် ⏳", parse_mode=ParseMode.MARKDOWN, reply_markup=main_kb(cid)) return ST_MAIN async def recv_text(update: Update, ctx: ContextTypes.DEFAULT_TYPE): cid = update.effective_chat.id text = update.message.text.strip() s = sess(cid) # Music file handled via recv_music_input — text fallback if ctx.user_data.get('await_wmk'): s['watermark'] = text; ctx.user_data['await_wmk'] = False await update.message.reply_text(f"✅ Watermark — `{text}`", parse_mode=ParseMode.MARKDOWN, reply_markup=main_kb(cid)) return ST_MAIN if ctx.user_data.get('adm_action') == 'create' and s.get('is_admin'): parts = text.split() uname = parts[0] if parts else '' coins = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else 10 msg, created = create_user_fn(uname, coins, s['username']) ctx.user_data.pop('adm_action', None) await update.message.reply_text( f"✅ User ဖန်တီးပြီး!\nUsername: `{created}`\nCoins: {coins}" if created else f"❌ {msg}", parse_mode=ParseMode.MARKDOWN, reply_markup=main_kb(cid)) return ST_MAIN if ctx.user_data.get('adm_action') == 'coins' and s.get('is_admin'): parts = text.split() if len(parts) >= 3: msg = set_coins_fn(parts[0], int(parts[1])) if parts[2] == 'set' else add_coins_fn(parts[0], int(parts[1])) ctx.user_data.pop('adm_action', None) await update.message.reply_text(f"✅ {msg}", reply_markup=main_kb(cid)) else: await update.message.reply_text("❌ Format: `username amount add|set`", parse_mode=ParseMode.MARKDOWN) return ST_MAIN if ctx.user_data.get('adm_action') == 'delete' and s.get('is_admin'): db = load_db() if text in db['users']: del db['users'][text]; save_db(db) await update.message.reply_text(f"✅ `{text}` ဖျက်ပြီးပါပြီ", parse_mode=ParseMode.MARKDOWN, reply_markup=main_kb(cid)) else: await update.message.reply_text(f"❌ `{text}` မတွေ့ပါ", parse_mode=ParseMode.MARKDOWN, reply_markup=main_kb(cid)) ctx.user_data.pop('adm_action', None) return ST_MAIN if re.match(r'https?://', text): s['pending_url'] = text; s['pending_file_id'] = None if s['coins'] != -1 and s['coins'] < 1: await update.message.reply_text(f"❌ Coins မလုံလောက်ဘူး ({s['coins']}) — 1 လိုသည်") return ST_MAIN # Busy check if is_processing(cid): await update.message.reply_text( "⛔ *Process လုပ်နေဆဲဖြစ်သည်*\n\n" "လက်ရှိ video ပြီးမှ နောက်တစ်ခု ပို့ပါ\n" "_(ဖျက်သိမ်းလိုရင် ❌ နှိပ်ပါ)_", parse_mode=ParseMode.MARKDOWN, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton('❌ ဖျက်သိမ်းမည်', callback_data='cancel|process')]]) ) return ST_MAIN # Coin check fresh_coins2 = get_coins(s['username']) if fresh_coins2 is not None: s['coins'] = fresh_coins2 if not s['is_admin'] and s['coins'] != -1 and s['coins'] < 1: await update.message.reply_text( f"❌ Coins မလုံလောက်ဘူး ({s['coins']}) — 1 လိုသည်" ) return ST_MAIN cancel_flags[cid] = False prog_msg = await update.message.reply_text( "⏳ *စတင်နေပါသည်…*", parse_mode=ParseMode.MARKDOWN, reply_markup=cancel_kb() ) start_job(ctx.bot, cid, text, None, prog_msg.message_id) return ST_MAIN await update.message.reply_text("ဘာလုပ်မလဲ?", reply_markup=main_kb(cid)) return ST_MAIN # ── MAIN ── def main(): if not BOT_TOKEN: logger.error('❌ TELEGRAM_BOT_TOKEN မသတ်မှတ်ရသေးပါ!'); return application = Application.builder().token(BOT_TOKEN).connect_timeout(60).read_timeout(900).write_timeout(900).pool_timeout(900).build() conv = ConversationHandler( entry_points=[CommandHandler('start', cmd_start)], states={ ST_LOGIN_USER: [MessageHandler(filters.TEXT & ~filters.COMMAND, recv_login_user)], ST_LOGIN_PASS: [MessageHandler(filters.TEXT & ~filters.COMMAND, recv_login_pass)], ST_MAIN: [ MessageHandler(filters.Regex('^🎬 Auto Process$'), btn_auto_process), MessageHandler(filters.Regex('^🔊 အသံ$'), btn_voice), MessageHandler(filters.Regex('^⚙️ Settings$'), btn_settings), MessageHandler(filters.Regex('^👤 အကောင့်$'), btn_account), MessageHandler(filters.Regex('^🛒 Coins ဝယ်ရန်$'), btn_buy), MessageHandler(filters.Regex('^🔄 Reset$'), btn_reset), MessageHandler(filters.Regex('^👑 Admin$'), btn_admin), MessageHandler(filters.Regex('^🚪 Logout$'), cmd_logout), MessageHandler(filters.VIDEO | filters.Document.VIDEO, recv_video_input), MessageHandler(filters.AUDIO | filters.Document.AUDIO, recv_music_input), MessageHandler(filters.PHOTO, recv_slip_photo), MessageHandler(filters.TEXT & ~filters.COMMAND, recv_text), CallbackQueryHandler(on_callback), ], ST_AWAIT_VIDEO: [ MessageHandler(filters.TEXT & ~filters.COMMAND, recv_video_input), MessageHandler(filters.VIDEO | filters.Document.VIDEO, recv_video_input), CallbackQueryHandler(on_callback), ], }, fallbacks=[CommandHandler('start', cmd_start), CommandHandler('logout', cmd_logout)], per_chat=True, allow_reentry=True, ) application.add_handler(conv) application.add_handler(CommandHandler('pending', cmd_pending)) application.add_handler(CommandHandler('broadcast', cmd_broadcast)) # Global handler for adm_pay callbacks — works even outside conversation state application.add_handler(CallbackQueryHandler(on_callback, pattern='^adm_pay[|]')) application.add_error_handler(error_handler) logger.info('🤖 Recap Studio Bot အသင့်ဖြစ်ပြီ!') application.run_polling( drop_pending_updates=True, allowed_updates=Update.ALL_TYPES, ) if __name__ == '__main__': main()