Spaces:
Running
Running
| """ | |
| 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โ <b>Approved!</b> +{coins_add} coins โ <code>{username}</code>" | |
| 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โ <b>Rejected</b>" | |
| 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"๐ฐ <b>Pending Payment</b>\n" | |
| f"๐ค <code>{uname}</code>\n" | |
| f"๐ช {coins} Coins โ {price:,} MMK\n" | |
| f"๐ <code>{pid}</code>\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 <message> โ 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 <message>`", | |
| 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"๐ฐ <b>Pending Payment</b>\n" | |
| f"๐ค <code>{uname}</code>\n" | |
| f"๐ช {coins} Coins โ {price:,} MMK\n" | |
| f"๐ <code>{pid}</code>\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"๐ฐ <b>New Payment Request</b>\n" | |
| f"๐ค <code>{username}</code>\n" | |
| f"๐ช {coins} Coins โ {price_label}\n" | |
| f"{'โก PromptPay (THB)' if method=='thb' else '๐ฑ KBZ/Wave (MMK)'}\n" | |
| f"๐ <code>{payment_id}</code>\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() | |