"""
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()