| """ |
| 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, |
| |
| _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 = {} |
|
|
| 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 |
|
|
| (ST_MAIN, ST_LOGIN_USER, ST_LOGIN_PASS, ST_AWAIT_VIDEO) = range(4) |
|
|
| sessions = {} |
| _whisper_model = [None] |
| _wm_lock = threading.Lock() |
| cancel_flags = {} |
|
|
| |
| user_busy = {} |
| _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, 'Process 5 แแผแญแแบ'), |
| (20, 18000, 'Process 10 แแผแญแแบ โ แกแแฑแฌแแบแธแแฏแถแธ'), |
| (30, 28000, 'Process 15 แแผแญแแบ'), |
| ] |
|
|
| |
|
|
| 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] |
| |
| 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(): |
| btns = [] |
| for coins, price, desc in PACKAGES: |
| btns.append([InlineKeyboardButton( |
| f"๐ช {coins} Coins โ {price:,} MMK ({desc})", |
| callback_data=f'pkg|{coins}|{price}' |
| )]) |
| return InlineKeyboardMarkup(btns) |
|
|
| PAYMENT_INFO = ( |
| "๐ณ *แแฝแฑแแฝแฒแแแบแธ*\n\n" |
| "๐ฑ KBZPay / Wave\n\n" |
| "๐ค *Phoe Shan*\n" |
| "๐ *09679871352* (Kapy)\n\n" |
| "แแฝแฑแแฝแฒแแผแฎแธแแฑแฌแแบ slip แแญแฏ Admin แแถ แแญแฏแทแแฑแธแแซ" |
| ) |
|
|
| def cancel_kb(): |
| return InlineKeyboardMarkup([[InlineKeyboardButton('โ แแปแแบแแญแแบแธแแแบ', callback_data='cancel|process')]]) |
|
|
| |
|
|
| 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) |
| |
| 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 |
|
|
| |
|
|
| 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 |
| |
| if is_processing(cid): |
| cancel_flags[cid] = True |
| |
| 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')], |
| ])) |
| return ST_MAIN |
|
|
| |
|
|
| 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 |
| |
| if is_processing(cid): |
| await update.message.reply_text( |
| "โณ *แแฏแแบแแฑแฌแแบแแฑแแฒแแผแ
แบแแแบ*\n\nแแผแฎแธแแพ แแแบแแญแฏแทแแซ", |
| parse_mode=ParseMode.MARKDOWN |
| ) |
| return ST_MAIN |
|
|
| |
| 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 |
|
|
| |
|
|
| 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: |
| |
| 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: |
| |
| 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 |
|
|
| |
| 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 |
|
|
| 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: |
| |
| target_size_kb = 45 * 1024 |
| |
| 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_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 |
| except Exception as ce: |
| logger.warning(f"[{tid}] Compress error: {ce}") |
| send_file = None |
|
|
| if send_file is None: |
| |
| 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 |
|
|
| 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=300, write_timeout=300, |
| ) |
| 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) |
|
|
| |
|
|
| 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 |
|
|
| |
| if data.startswith('adm_pay|'): |
| |
| already_admin = s.get('is_admin') or s.get('username') == ADMIN_U |
| if not already_admin: |
| |
| if ADMIN_TELEGRAM_CHAT_ID and str(cid) == str(ADMIN_TELEGRAM_CHAT_ID): |
| s['username'] = ADMIN_U |
| s['is_admin'] = True |
| already_admin = True |
| else: |
| |
| db = load_db() |
| for uname, udata in db.get('users', {}).items(): |
| if udata.get('tg_chat_id') == cid and uname == ADMIN_U: |
| s['username'] = uname |
| s['is_admin'] = True |
| already_admin = True |
| break |
| if not already_admin: |
| await q.answer("โ Admin only", show_alert=True); return |
| parts = data.split('|') |
| action = parts[1] |
| 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': |
| |
| 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'] |
| pay['status'] = 'approved' |
| pay['updated_at'] = _dt.now().isoformat() |
| save_payments_db(pdb) |
| add_coins_fn(username, coins_add, 'admin_bot') |
| |
| 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) |
| |
| 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: |
| 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.startswith('pkg|'): |
| if data == 'pkg|back': |
| await q.edit_message_text(f"๐ *Coins แแแบแแแบ*\n\nPackage แแฝแฑแธแแซ โ", parse_mode=ParseMode.MARKDOWN, reply_markup=package_kb()) |
| return |
| _, coins, price = data.split('|') |
| coins = int(coins); price = int(price) |
| pending_payment[cid] = {'coins': coins, 'price': price} |
| ctx.user_data['await_slip'] = True |
| await q.edit_message_text( |
| f"๐ช *{coins} Coins* โ {price:,} MMK\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')], |
| ])) |
| 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 |
|
|
| |
|
|
|
|
| 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:]: |
| 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) |
|
|
| |
|
|
| 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'): |
| |
| 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'] |
| username = s.get('username','') |
|
|
| |
| 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() |
|
|
| |
| 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, |
| 'status': 'pending', 'created_at': now, 'updated_at': now, |
| 'slip_image': slip_data_url, 'admin_note': '', |
| }) |
| save_payments_db(pdb) |
|
|
| |
| 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) |
|
|
| |
| if ADMIN_TELEGRAM_CHAT_ID: |
| cap = ( |
| f"๐ฐ <b>New Payment Request</b>\n" |
| f"๐ค <code>{username}</code>\n" |
| f"๐ช {coins} Coins โ {price:,} 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:,} MMK\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) |
|
|
| |
| 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 |
| |
| 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 |
| |
| 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 |
|
|
| |
|
|
| def get_application(): |
| """Build and return the PTB Application (used by Flask webhook route).""" |
| if not BOT_TOKEN: |
| logger.error('โ TELEGRAM_BOT_TOKEN แแแแบแแพแแบแแแฑแธแแซ!') |
| return None |
|
|
| application = Application.builder().token(BOT_TOKEN).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(CallbackQueryHandler(on_callback, pattern='^adm_pay[|]')) |
| return application |
|
|
|
|
| def main(): |
| """Legacy entry point โ kept for local testing only.""" |
| if not BOT_TOKEN: |
| logger.error('โ TELEGRAM_BOT_TOKEN แแแแบแแพแแบแแแฑแธแแซ!'); return |
| application = get_application() |
| logger.info('๐ค Recap Studio Bot แกแแแทแบแแผแ
แบแแผแฎ! (polling mode โ local only)') |
| application.run_polling(drop_pending_updates=True, allowed_updates=Update.ALL_TYPES) |
|
|
| if __name__ == '__main__': |
| main() |
|
|