Recap-shan / bot.py
Phoe2004's picture
Upload 3 files
cc95c85 verified
raw
history blame
57.2 kB
"""
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()