Phoe2004 commited on
Commit
f5ec83f
ยท
verified ยท
1 Parent(s): 69b98c4

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +1 -0
  2. bot.py +928 -496
app.py CHANGED
@@ -1333,6 +1333,7 @@ def _build_video(vpath, cmb, mpath, ad, vd, crop, flip, col, wmk, out_file,
1333
  f'-filter_complex "{filter_complex}" '
1334
  f'-map "[v_final]" -map "[outa]" '
1335
  f'-c:v libx264 -crf 24 -preset ultrafast -pix_fmt yuv420p '
 
1336
  f'-c:a aac -ar 44100 -b:a 128k '
1337
  f'-t {ad:.3f} -movflags +faststart "{out_file}"'
1338
  )
 
1333
  f'-filter_complex "{filter_complex}" '
1334
  f'-map "[v_final]" -map "[outa]" '
1335
  f'-c:v libx264 -crf 24 -preset ultrafast -pix_fmt yuv420p '
1336
+ f'-threads 0 '
1337
  f'-c:a aac -ar 44100 -b:a 128k '
1338
  f'-t {ad:.3f} -movflags +faststart "{out_file}"'
1339
  )
bot.py CHANGED
@@ -1,536 +1,968 @@
1
  """
2
  Recap Studio โ€” Telegram Bot
3
- Features:
4
- โ€ข User: Login, Auto-process, Voice, Settings, Coins, Payment submit, Payment history
5
- โ€ข Admin: Pending payments, Approve/Reject, Add/Set coins, User list, Broadcast
6
  """
7
-
8
- import os, json, uuid, re, time, logging, threading
9
- import urllib.request, urllib.parse
10
- from datetime import datetime
11
  from pathlib import Path
12
 
13
- logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
14
- log = logging.getLogger(__name__)
15
-
16
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
17
- # CONFIG
18
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
19
- BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', '')
20
- ADMIN_CHAT_ID = os.getenv('ADMIN_TELEGRAM_CHAT_ID', '') # numeric chat id
21
- APP_BASE_URL = os.getenv('APP_BASE_URL', 'https://ai.psonline.shop')
22
- ADMIN_USERNAME = os.getenv('ADMIN_USERNAME', '')
23
- ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD', '')
24
-
25
- BASE_DIR = Path(__file__).parent
26
- DB_FILE = str(BASE_DIR / 'users_db.json')
27
- PAYMENTS_DB_FILE = str(BASE_DIR / 'payments_db.json')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- API = f'https://api.telegram.org/bot{BOT_TOKEN}'
 
 
 
 
 
30
 
31
  PACKAGES = [
32
- {'coins': 10, 'price': '10,000 MMK'},
33
- {'coins': 30, 'price': '28,000 MMK'},
34
- {'coins': 60, 'price': '58,000 MMK'},
35
- {'coins': 100, 'price': '95,000 MMK'},
36
  ]
37
- KBZ_NAME = 'PHOE SHAN'
38
- KBZ_NUMBER = '09679871352'
39
-
40
- # Per-user state: {'step': ..., 'data': {...}}
41
- _state: dict = {}
42
- _state_lock = threading.Lock()
43
 
44
- def get_state(cid):
45
- with _state_lock:
46
- return _state.get(str(cid), {})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
- def set_state(cid, step, **data):
49
- with _state_lock:
50
- _state[str(cid)] = {'step': step, 'data': data}
 
 
 
 
51
 
52
- def clear_state(cid):
53
- with _state_lock:
54
- _state.pop(str(cid), None)
55
 
56
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
57
- # DB HELPERS
58
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
59
- def load_db():
 
60
  try:
61
- with open(DB_FILE, encoding='utf-8') as f:
62
- return json.load(f)
63
- except:
64
- return {'users': {}}
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
- def save_db(db):
67
- with open(DB_FILE, 'w', encoding='utf-8') as f:
68
- json.dump(db, f, ensure_ascii=False, indent=2)
69
 
70
- def load_payments():
71
- try:
72
- with open(PAYMENTS_DB_FILE, encoding='utf-8') as f:
73
- return json.load(f)
74
- except:
75
- return {'payments': []}
76
-
77
- def save_payments(db):
78
- with open(PAYMENTS_DB_FILE, 'w', encoding='utf-8') as f:
79
- json.dump(db, f, ensure_ascii=False, indent=2)
80
-
81
- def get_tg_session(cid):
82
- """Return username linked to this telegram chat_id, or None."""
83
- db = load_db()
84
- cid_str = str(cid)
85
- for uname, udata in db['users'].items():
86
- if str(udata.get('tg_chat_id', '')) == cid_str:
87
- return uname
88
- return None
89
-
90
- def link_tg(username, cid):
91
- db = load_db()
92
- if username in db['users']:
93
- db['users'][username]['tg_chat_id'] = str(cid)
94
- save_db(db)
95
-
96
- def get_coins(username):
97
- db = load_db()
98
- return db['users'].get(username, {}).get('coins', 0)
99
-
100
- def add_coins(username, n):
101
- db = load_db()
102
- if username not in db['users']:
103
- return False, 0
104
- db['users'][username]['coins'] = db['users'][username].get('coins', 0) + int(n)
105
- save_db(db)
106
- return True, db['users'][username]['coins']
107
-
108
- def set_coins(username, n):
109
- db = load_db()
110
- if username not in db['users']:
111
- return False, 0
112
- db['users'][username]['coins'] = int(n)
113
- save_db(db)
114
- return True, int(n)
115
-
116
- import hashlib
117
- def hp(p): return hashlib.sha256(p.encode()).hexdigest()
118
-
119
- def verify_login(username, password):
120
- if username == ADMIN_USERNAME and password == ADMIN_PASSWORD:
121
- return True, True # ok, is_admin
122
- db = load_db()
123
- if username not in db['users']:
124
- return False, False
125
- stored = db['users'][username].get('password', '')
126
- if stored and stored != hp(password):
127
- return False, False
128
- return True, False
129
-
130
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
131
- # TELEGRAM API HELPERS
132
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
133
- def tg(method, **params):
134
- url = f'{API}/{method}'
135
- data = json.dumps(params).encode()
136
- req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'})
137
  try:
138
- with urllib.request.urlopen(req, timeout=15) as r:
139
- return json.loads(r.read())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  except Exception as e:
141
- log.error(f'tg({method}) error: {e}')
142
- return {}
143
-
144
- def send(cid, text, markup=None, parse_mode='HTML'):
145
- params = {'chat_id': cid, 'text': text, 'parse_mode': parse_mode}
146
- if markup:
147
- params['reply_markup'] = markup
148
- return tg('sendMessage', **params)
149
-
150
- def send_photo(cid, photo_bytes, caption='', markup=None):
151
- """Send photo via multipart upload."""
152
- boundary = uuid.uuid4().hex
153
- body = (
154
- f'--{boundary}\r\n'
155
- f'Content-Disposition: form-data; name="chat_id"\r\n\r\n'
156
- f'{cid}\r\n'
157
- f'--{boundary}\r\n'
158
- f'Content-Disposition: form-data; name="caption"\r\n\r\n'
159
- f'{caption}\r\n'
160
- f'--{boundary}\r\n'
161
- f'Content-Disposition: form-data; name="parse_mode"\r\n\r\nHTML\r\n'
162
- f'--{boundary}\r\n'
163
- f'Content-Disposition: form-data; name="photo"; filename="slip.jpg"\r\n'
164
- f'Content-Type: image/jpeg\r\n\r\n'
165
- ).encode() + photo_bytes + f'\r\n--{boundary}--\r\n'.encode()
166
- if markup:
167
- body = body[:-len(f'\r\n--{boundary}--\r\n'.encode())]
168
- body += (
169
- f'\r\n--{boundary}\r\n'
170
- f'Content-Disposition: form-data; name="reply_markup"\r\n\r\n'
171
- f'{json.dumps(markup)}\r\n'
172
- f'--{boundary}--\r\n'
173
- ).encode()
174
- req = urllib.request.Request(
175
- f'{API}/sendPhoto',
176
- data=body,
177
- headers={'Content-Type': f'multipart/form-data; boundary={boundary}'}
178
- )
179
  try:
180
- with urllib.request.urlopen(req, timeout=20) as r:
181
- return json.loads(r.read())
182
- except Exception as e:
183
- log.error(f'send_photo error: {e}')
184
-
185
- def edit(cid, mid, text, markup=None, parse_mode='HTML'):
186
- params = {'chat_id': cid, 'message_id': mid, 'text': text, 'parse_mode': parse_mode}
187
- if markup:
188
- params['reply_markup'] = markup
189
- tg('editMessageText', **params)
190
-
191
- def answer_cb(cb_id, text=''):
192
- tg('answerCallbackQuery', callback_query_id=cb_id, text=text)
193
-
194
- def kb(*rows):
195
- """Inline keyboard. Each row is list of (text, callback_data) tuples."""
196
- return {'inline_keyboard': [[{'text': t, 'callback_data': d} for t, d in row] for row in rows]}
197
-
198
- def rkb(*rows):
199
- """Reply keyboard."""
200
- return {'keyboard': [[{'text': t} for t in row] for row in rows],
201
- 'resize_keyboard': True, 'one_time_keyboard': True}
202
-
203
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
204
- # MENU BUILDERS
205
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
206
- def main_menu(cid):
207
- uname = get_tg_session(cid)
208
- is_admin = (uname == ADMIN_USERNAME)
209
- if not uname:
210
- send(cid, '๐Ÿ‘‹ <b>Recap Studio Bot</b>\n\nแ€žแ€„แ€ทแ€บ account แ€€แ€ญแ€ฏ แ€แ€ปแ€ญแ€แ€บแ€†แ€€แ€บแ€•แ€ซ:',
211
- rkb(['๐Ÿ”‘ Login'], ['โ„น๏ธ About']))
212
- return
213
- coins = get_coins(uname) if not is_admin else -1
214
- coin_txt = 'โˆž' if is_admin else str(coins)
215
- txt = (f'๐Ÿ‘‹ แ€™แ€„แ€บแ€นแ€‚แ€œแ€ฌแ€•แ€ซ <b>{uname}</b>!\n'
216
- f'๐Ÿช™ Coins: <b>{coin_txt}</b>\n\n'
217
- f'แ€˜แ€ฌแ€™แ€ปแ€ฌแ€ธ แ€€แ€ฐแ€Šแ€ฎแ€•แ€ฑแ€ธแ€›แ€™แ€œแ€ฒ?')
218
- rows = [['๐ŸŽฌ Video Process', '๐Ÿ’ฐ Coins แ€แ€šแ€บแ€›แ€”แ€บ'],
219
- ['๐Ÿ“‹ Payment แ€™แ€พแ€แ€บแ€แ€™แ€บแ€ธ', 'โš™๏ธ Settings'],
220
- ['๐Ÿšช Logout']]
221
- if is_admin:
222
- rows.insert(1, ['๐Ÿ‘‘ Admin Panel'])
223
- send(cid, txt, rkb(*rows))
224
-
225
- def admin_menu(cid):
226
- send(cid, '๐Ÿ‘‘ <b>Admin Panel</b>\n\nแ€˜แ€ฌแ€™แ€ปแ€ฌแ€ธ แ€œแ€ฏแ€•แ€บแ€™แ€œแ€ฒ?',
227
- rkb(['๐Ÿ’ณ Pending Payments', '๐Ÿ‘ฅ User List'],
228
- ['โž• Add Coins', '๐Ÿ“ข Broadcast'],
229
- ['๐Ÿ”™ Main Menu']))
230
-
231
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
232
- # PAYMENT HELPERS
233
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
234
- def fmt_payment(p, include_slip_hint=False):
235
- status_emoji = {'pending': 'โณ', 'approved': 'โœ…', 'rejected': 'โŒ'}.get(p['status'], 'โ“')
236
- txt = (f'{status_emoji} <b>{p["status"].upper()}</b>\n'
237
- f'๐Ÿ†” <code>{p["id"]}</code>\n'
238
- f'๐Ÿ‘ค <code>{p["username"]}</code>\n'
239
- f'๐Ÿช™ {p["coins"]} Coins โ€” {p["price"]}\n'
240
- f'๐Ÿ“… {p["created_at"][:16].replace("T"," ")}')
241
- if p.get('admin_note'):
242
- txt += f'\n๐Ÿ“ {p["admin_note"]}'
243
- return txt
244
-
245
- def notify_admin_payment(p, slip_bytes=None):
246
- """Send payment notification to admin."""
247
- if not ADMIN_CHAT_ID:
248
- return
249
- markup = kb(
250
- [(f'โœ… Approve {p["coins"]} Coins', f'apv:{p["id"]}')],
251
- [('โŒ Reject', f'rej:{p["id"]}')]
252
- )
253
- caption = (f'๐Ÿ’ฐ <b>New Payment Request</b>\n\n'
254
- f'๐Ÿ‘ค User: <code>{p["username"]}</code>\n'
255
- f'๐Ÿช™ {p["coins"]} Coins โ€” {p["price"]}\n'
256
- f'๐Ÿ†” <code>{p["id"]}</code>\n'
257
- f'๐Ÿ“… {p["created_at"][:16].replace("T"," ")}')
258
- if slip_bytes:
259
- send_photo(ADMIN_CHAT_ID, slip_bytes, caption=caption, markup=markup)
260
- else:
261
- send(ADMIN_CHAT_ID, caption, markup=markup)
262
-
263
- def notify_user_payment(p, approved):
264
- """Notify user when payment is approved/rejected."""
265
- db = load_db()
266
- udata = db['users'].get(p['username'], {})
267
- cid = udata.get('tg_chat_id')
268
- if not cid:
269
  return
270
- if approved:
271
- coins = get_coins(p['username'])
272
- send(cid, f'โœ… <b>Payment Approved!</b>\n\n'
273
- f'๐Ÿช™ {p["coins"]} Coins แ€‘แ€Šแ€ทแ€บแ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎแ‹\n'
274
- f'๐Ÿ’ผ แ€œแ€€แ€บแ€€แ€ปแ€”แ€บ Coins: <b>{coins}</b>\n\n'
275
- f'แ€€แ€ปแ€ฑแ€ธแ€‡แ€ฐแ€ธแ€แ€„แ€บแ€•แ€ซแ€žแ€Šแ€บ ๐Ÿ™')
276
- else:
277
- note = p.get('admin_note', '')
278
- send(cid, f'โŒ <b>Payment Rejected</b>\n\n'
279
- f'๐Ÿช™ {p["coins"]} Coins โ€” {p["price"]}\n'
280
- + (f'๐Ÿ“ แ€กแ€€แ€ผแ€ฑแ€ฌแ€„แ€บแ€ธแ€•แ€ผแ€แ€ปแ€€แ€บ: {note}\n' if note else '') +
281
- f'\nแ€‘แ€•แ€บแ€™แ€ถ แ€†แ€€แ€บแ€žแ€ฝแ€šแ€บแ€•แ€ซ ๐Ÿ™')
282
-
283
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
284
-
285
- def is_admin_chat(cid):
286
- return str(cid) == str(ADMIN_CHAT_ID)
287
-
288
- def show_pending(cid):
289
- pdb = load_payments()
290
- pending = [p for p in pdb['payments'] if p['status'] == 'pending']
291
- if not pending:
292
- send(cid, 'โœ… Pending payments แ€™แ€›แ€พแ€ญแ€•แ€ซ'); admin_menu(cid); return
293
- send(cid, f'โณ <b>Pending Payments ({len(pending)})</b>')
294
- for p in pending[:10]:
295
- markup = kb(
296
- [(f'โœ… Approve {p["coins"]} Coins', f'apv:{p["id"]}')],
297
- [('โŒ Reject', f'rej:{p["id"]}'), ('๐Ÿ–ผ Slip', f'slip:{p["id"]}')]
298
- )
299
- send(cid, fmt_payment(p), markup)
300
-
301
- def show_payments(cid, status='all'):
302
- pdb = load_payments()
303
- pmts = pdb['payments'] if status == 'all' else [p for p in pdb['payments'] if p['status'] == status]
304
- pmts = sorted(pmts, key=lambda x: x['created_at'], reverse=True)[:10]
305
- if not pmts:
306
- send(cid, f'๐Ÿ“ญ {status} payments แ€™แ€›แ€พแ€ญแ€•แ€ซ'); admin_menu(cid); return
307
- for p in pmts:
308
- send(cid, fmt_payment(p))
309
-
310
- def show_users(cid):
311
- db = load_db()
312
- users = sorted(db['users'].items(), key=lambda x: x[1].get('coins', 0), reverse=True)[:20]
313
- lines = ['๐Ÿ‘ฅ <b>User List</b>\n']
314
- for uname_, udata in users:
315
- c = udata.get('coins', 0); v = udata.get('total_videos', 0)
316
- lines.append(f'โ€ข <code>{uname_}</code> โ€” ๐Ÿช™{c} | ๐ŸŽฌ{v}')
317
- send(cid, '\n'.join(lines))
318
-
319
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
320
- # ADMIN-ONLY MESSAGE HANDLER
321
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
322
- def handle_message(msg):
323
- cid = msg['chat']['id']
324
- text = msg.get('text', '').strip()
325
-
326
- if not is_admin_chat(cid):
327
- send(cid, '๐Ÿ”’ แ€ค Bot แ€žแ€Šแ€บ Admin แ€กแ€แ€ฝแ€€แ€บแ€žแ€ฌ แ€–แ€ผแ€…แ€บแ€•แ€ซแ€žแ€Šแ€บ')
328
  return
329
 
330
- state = get_state(cid)
331
- step = state.get('step', '')
332
- sdata = state.get('data', {})
333
-
334
- # โ”€โ”€ /start โ”€โ”€
335
- if text in ('/start', '/menu', '๐Ÿ”™ Main Menu'):
336
- clear_state(cid); admin_menu(cid); return
337
-
338
- if text in ('๐Ÿ’ณ Pending Payments', '/pending'):
339
- show_pending(cid); return
340
-
341
- if text in ('๐Ÿ“‹ All Payments', '/all'):
342
- show_payments(cid, 'all'); return
343
-
344
- if text in ('๐Ÿ‘ฅ User List', '/users'):
345
- show_users(cid); return
346
-
347
- # โ”€โ”€ Add Coins โ”€โ”€
348
- if text in ('โž• Add Coins', '/addcoins'):
349
- set_state(cid, 'adm_add_user')
350
- send(cid, 'โž• <b>Add Coins</b>\n\nUsername แ€‘แ€Šแ€ทแ€บแ€•แ€ซ:', rkb(['โŒ Cancel'])); return
351
-
352
- if step == 'adm_add_user':
353
- if text == 'โŒ Cancel': clear_state(cid); admin_menu(cid); return
354
- set_state(cid, 'adm_add_n', target=text)
355
- send(cid, f'๐Ÿ‘ค <code>{text}</code>\n\nCoins แ€กแ€›แ€ฑแ€กแ€แ€ฝแ€€แ€บ:', rkb(['โŒ Cancel'])); return
356
-
357
- if step == 'adm_add_n':
358
- if text == 'โŒ Cancel': clear_state(cid); admin_menu(cid); return
359
- target = sdata.get('target', '')
360
- try: n = int(text)
361
- except: send(cid, 'โŒ แ€‚แ€แ€”แ€บแ€ธ แ€‘แ€Šแ€ทแ€บแ€•แ€ซ'); return
362
- ok, total = add_coins(target, n)
363
- clear_state(cid)
364
- send(cid, f'โœ… <code>{target}</code> โ€” +{n} ๐Ÿช™\n๐Ÿ’ผ แ€œแ€€แ€บแ€€แ€ปแ€”แ€บ: <b>{total}</b>' if ok else 'โŒ User แ€™แ€แ€ฝแ€ฑแ€ทแ€•แ€ซ')
365
- admin_menu(cid); return
366
-
367
- # โ”€โ”€ Set Coins โ”€โ”€
368
- if text in ('๐Ÿ”ง Set Coins', '/setcoins'):
369
- set_state(cid, 'adm_set_user')
370
- send(cid, '๐Ÿ”ง <b>Set Coins</b>\n\nUsername แ€‘แ€Šแ€ทแ€บแ€•แ€ซ:', rkb(['โŒ Cancel'])); return
371
-
372
- if step == 'adm_set_user':
373
- if text == 'โŒ Cancel': clear_state(cid); admin_menu(cid); return
374
- set_state(cid, 'adm_set_n', target=text)
375
- send(cid, f'๐Ÿ‘ค <code>{text}</code>\n\nCoins แ€แ€”แ€บแ€–แ€ญแ€ฏแ€ธ (แ€€แ€”แ€ทแ€บแ€žแ€แ€บแ€แ€ป):', rkb(['โŒ Cancel'])); return
376
-
377
- if step == 'adm_set_n':
378
- if text == 'โŒ Cancel': clear_state(cid); admin_menu(cid); return
379
- target = sdata.get('target', '')
380
- try: n = int(text)
381
- except: send(cid, 'โŒ แ€‚แ€แ€”แ€บแ€ธ แ€‘แ€Šแ€ทแ€บแ€•แ€ซ'); return
382
- ok, total = set_coins(target, n)
383
- clear_state(cid)
384
- send(cid, f'โœ… <code>{target}</code> Coins = <b>{total}</b>' if ok else 'โŒ User แ€™แ€แ€ฝแ€ฑแ€ทแ€•แ€ซ')
385
- admin_menu(cid); return
386
-
387
- # โ”€โ”€ Delete User โ”€โ”€
388
- if text in ('๐Ÿ—‘ Delete User', '/deluser'):
389
- set_state(cid, 'adm_del_user')
390
- send(cid, '๐Ÿ—‘ <b>Delete User</b>\n\nUsername แ€‘แ€Šแ€ทแ€บแ€•แ€ซ:', rkb(['โŒ Cancel'])); return
391
-
392
- if step == 'adm_del_user':
393
- if text == 'โŒ Cancel': clear_state(cid); admin_menu(cid); return
394
- db = load_db()
395
- if text not in db['users']: send(cid, 'โŒ User แ€™แ€แ€ฝแ€ฑแ€ทแ€•แ€ซ'); clear_state(cid); return
396
- set_state(cid, 'adm_del_confirm', target=text)
397
- send(cid, f'โš ๏ธ <code>{text}</code> แ€€แ€ญแ€ฏ แ€–แ€ปแ€€แ€บแ€™แ€พแ€ฌ แ€žแ€ฑแ€แ€ปแ€ฌแ€•แ€ซแ€žแ€œแ€ฌแ€ธ?',
398
- rkb([f'โœ… Yes, Delete', 'โŒ Cancel'])); return
399
-
400
- if step == 'adm_del_confirm':
401
- if text == 'โŒ Cancel': clear_state(cid); admin_menu(cid); return
402
- target = sdata.get('target', '')
403
- db = load_db()
404
- if target in db['users']:
405
- del db['users'][target]; save_db(db)
406
- send(cid, f'โœ… <code>{target}</code> แ€–แ€ปแ€€แ€บแ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ')
407
- else:
408
- send(cid, 'โŒ User แ€™แ€แ€ฝแ€ฑแ€ทแ€•แ€ซ')
409
- clear_state(cid); admin_menu(cid); return
410
-
411
- # โ”€โ”€ Broadcast โ”€โ”€
412
- if text in ('๐Ÿ“ข Broadcast', '/broadcast'):
413
- set_state(cid, 'adm_broadcast')
414
- send(cid, '๐Ÿ“ข <b>Broadcast</b>\n\nMessage แ€›แ€ญแ€ฏแ€€แ€บแ€•แ€ซ:', rkb(['โŒ Cancel'])); return
415
-
416
- if step == 'adm_broadcast':
417
- if text == 'โŒ Cancel': clear_state(cid); admin_menu(cid); return
418
- clear_state(cid)
419
- db = load_db(); sent = 0
420
- for udata in db['users'].values():
421
- tcid = udata.get('tg_chat_id')
422
- if tcid:
423
- try: send(tcid, f'๐Ÿ“ข <b>Recap Studio</b>\n\n{text}'); sent += 1; time.sleep(0.1)
424
- except: pass
425
- send(cid, f'โœ… {sent} users แ€‘แ€ถ แ€•แ€ญแ€ฏแ€ทแ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ')
426
- admin_menu(cid); return
427
-
428
- # โ”€โ”€ Approve/Reject note flows โ”€โ”€
429
- if step == 'adm_approve_note':
430
- pid = sdata.get('pid', '')
431
- _do_approve(cid, pid, text if text.lower() != 'skip' else ''); return
432
-
433
- if step == 'adm_reject_note':
434
- pid = sdata.get('pid', '')
435
- _do_reject(cid, pid, text if text.lower() != 'skip' else ''); return
436
-
437
- admin_menu(cid)
438
-
439
-
440
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
441
- # CALLBACK QUERY HANDLER
442
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
443
- def handle_callback(cb):
444
- cid = cb['message']['chat']['id']
445
- mid = cb['message']['message_id']
446
- cb_id = cb['id']
447
- data = cb.get('data', '')
448
-
449
- answer_cb(cb_id)
450
-
451
- if not is_admin_chat(cid):
452
  return
453
 
454
- if data.startswith('apv:'):
455
- pid = data.split(':', 1)[1]
456
- set_state(cid, 'adm_approve_note', pid=pid)
457
- send(cid, f'โœ… Approve note แ€‘แ€Šแ€ทแ€บแ€•แ€ซ (<code>skip</code> = แ€™แ€œแ€ญแ€ฏแ€•แ€ซ):\n๐Ÿ†” <code>{pid}</code>')
458
  return
459
 
460
- if data.startswith('rej:'):
461
- pid = data.split(':', 1)[1]
462
- set_state(cid, 'adm_reject_note', pid=pid)
463
- send(cid, f'โŒ Reject reason แ€‘แ€Šแ€ทแ€บแ€•แ€ซ (<code>skip</code> = แ€™แ€œแ€ญแ€ฏแ€•แ€ซ):\n๐Ÿ†” <code>{pid}</code>')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
464
  return
465
 
466
- if data.startswith('slip:'):
467
- pid = data.split(':', 1)[1]
468
- pdb = load_payments()
469
- for p in pdb['payments']:
470
- if p['id'] == pid:
471
- slip_b64 = p.get('slip_image', '')
472
- if not slip_b64:
473
- send(cid, 'โŒ Slip แ€™แ€›แ€พแ€ญแ€•แ€ซ'); return
474
- import base64
475
- slip_bytes = base64.b64decode(slip_b64.split(',')[-1])
476
- markup = kb(
477
- [(f'โœ… Approve {p["coins"]} Coins', f'apv:{pid}')],
478
- [('โŒ Reject', f'rej:{pid}')]
479
- )
480
- send_photo(cid, slip_bytes,
481
- caption=f'๐Ÿ–ผ {p["username"]} โ€” {p["coins"]} Coins\n๐Ÿ†” <code>{pid}</code>',
482
- markup=markup)
483
- return
484
- send(cid, 'โŒ Payment not found')
 
 
 
 
 
 
485
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
486
 
487
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
488
- # POLLING LOOP (409 Conflict safe)
489
- # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
490
- def poll():
491
- for attempt in range(5):
492
  try:
493
- tg('deleteWebhook', drop_pending_updates=True)
494
- log.info('โœ… Webhook cleared')
495
- break
 
 
 
 
 
 
 
 
496
  except Exception as e:
497
- log.warning(f'deleteWebhook attempt {attempt+1}: {e}')
498
- time.sleep(3)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
 
500
- offset = 0
501
- log.info('๐Ÿค– Admin-only bot polling started')
502
- while True:
503
- try:
504
- r = tg('getUpdates', offset=offset, timeout=30,
505
- allowed_updates=['message', 'callback_query'])
506
- for upd in r.get('result', []):
507
- offset = upd['update_id'] + 1
508
- try:
509
- if 'message' in upd:
510
- handle_message(upd['message'])
511
- elif 'callback_query' in upd:
512
- handle_callback(upd['callback_query'])
513
- except Exception as e:
514
- log.error(f'Handler error: {e}', exc_info=True)
515
- except Exception as e:
516
- err = str(e)
517
- if '409' in err:
518
- log.warning('โš ๏ธ 409 Conflict โ€” waiting 15s then retryingโ€ฆ')
519
- time.sleep(15)
520
- try: tg('deleteWebhook', drop_pending_updates=True)
521
- except: pass
522
- else:
523
- log.error(f'Poll error: {e}')
524
- time.sleep(5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
525
 
 
526
 
527
  def main():
528
- """Entry point โ€” called by run_bot.py"""
529
  if not BOT_TOKEN:
530
- log.error('โŒ TELEGRAM_BOT_TOKEN not set'); return
531
- if not ADMIN_CHAT_ID:
532
- log.warning('โš ๏ธ ADMIN_TELEGRAM_CHAT_ID not set โ€” bot will reject all messages')
533
- poll()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
 
535
  if __name__ == '__main__':
536
  main()
 
1
  """
2
  Recap Studio โ€” Telegram Bot
 
 
 
3
  """
4
+ import os, sys, json, uuid, glob, shutil, logging, threading, time, re, subprocess
 
 
 
5
  from pathlib import Path
6
 
7
+ logging.basicConfig(format='%(asctime)s [%(levelname)s] %(name)s: %(message)s', level=logging.INFO)
8
+ logger = logging.getLogger(__name__)
9
+
10
+ from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, KeyboardButton
11
+ from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, ConversationHandler, filters, ContextTypes
12
+ from telegram.constants import ParseMode
13
+
14
+ sys.path.insert(0, str(Path(__file__).parent))
15
+ from app import (
16
+ login_user, get_coins, deduct, load_db, save_db,
17
+ gen_uname, create_user_fn, add_coins_fn, set_coins_fn,
18
+ call_api, parse_out, split_txt, dur,
19
+ run_tts_sync, run_gemini_tts_sync,
20
+ #ytdlp_download, cpu_queue_wait, upd_stat,
21
+ _build_video, ADMIN_U, SYS_MOVIE, SYS_MED, BASE_DIR, OUTPUT_DIR,
22
+ NUM_TO_MM_RULE, run_stage, ytdlp_download, upd_stat,
23
+ load_payments_db, save_payments_db,
24
+ TELEGRAM_BOT_TOKEN, ADMIN_TELEGRAM_CHAT_ID,
25
+ )
26
+
27
+ try:
28
+ import whisper as whisper_mod
29
+ except ImportError:
30
+ whisper_mod = None
31
+
32
+ BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', '')
33
+ ADMIN_TG_USERNAME = os.getenv('ADMIN_TG_USERNAME', 'PhoeShan2001')
34
+ WEB_BASE_URL = os.getenv('WEB_BASE_URL', 'https://recap.psonline.shop')
35
+ TG_MAX_FILE_BYTES = 49 * 1024 * 1024 # 49 MB โ€” Telegram bot upload limit
36
+
37
+ (ST_MAIN, ST_LOGIN_USER, ST_LOGIN_PASS, ST_AWAIT_VIDEO) = range(4)
38
+
39
+ sessions = {}
40
+ _whisper_model = [None]
41
+ _wm_lock = threading.Lock()
42
+ cancel_flags = {}
43
+
44
+ # โ”€โ”€ Per-user busy flag (one job at a time) โ”€โ”€
45
+ user_busy = {} # cid -> bool
46
+ _busy_lock = threading.Lock()
47
+
48
+ def is_processing(cid):
49
+ with _busy_lock:
50
+ return user_busy.get(cid, False)
51
+
52
+ def set_processing(cid, val):
53
+ with _busy_lock:
54
+ user_busy[cid] = val
55
+
56
+ def start_job(bot, cid, pending_url, pending_file_id, prog_msg_id):
57
+ """Start processing in background thread."""
58
+ threading.Thread(
59
+ target=_process_thread,
60
+ args=(bot, cid, prog_msg_id, pending_url, pending_file_id),
61
+ daemon=True
62
+ ).start()
63
+
64
+ MS_VOICES = [
65
+ ('Thiha (แ€€แ€ปแ€ฌแ€ธ)', 'my-MM-ThihaNeural', 'ms'),
66
+ ('Nilar (แ€™แ€ญแ€”แ€บแ€ธ)', 'my-MM-NilarNeural', 'ms'),
67
+ ]
68
 
69
+ GEMINI_VOICES = [
70
+ ('Kore','Kore','gemini'),('Charon','Charon','gemini'),('Fenrir','Fenrir','gemini'),
71
+ ('Leda','Leda','gemini'),('Orus','Orus','gemini'),('Puck','Puck','gemini'),
72
+ ('Aoede','Aoede','gemini'),('Zephyr','Zephyr','gemini'),('Achelois','Achelois','gemini'),
73
+ ('Pegasus','Pegasus','gemini'),('Perseus','Perseus','gemini'),('Schedar','Schedar','gemini'),
74
+ ]
75
 
76
  PACKAGES = [
77
+ (10, 10000, 'Process 5 แ€€แ€ผแ€ญแ€™แ€บ'),
78
+ (20, 18000, 'Process 10 แ€€แ€ผแ€ญแ€™แ€บ โ€” แ€กแ€€แ€ฑแ€ฌแ€„แ€บแ€ธแ€†แ€ฏแ€ถแ€ธ'),
79
+ (30, 28000, 'Process 15 แ€€แ€ผแ€ญแ€™แ€บ'),
 
80
  ]
 
 
 
 
 
 
81
 
82
+ # pending_payment[cid] = {'coins': int, 'price': int} โ€” slip แ€…แ€ฑแ€ฌแ€„แ€ทแ€บแ€”แ€ฑแ€…แ€‰แ€บ
83
+ pending_payment = {}
84
+
85
+ # โ”€โ”€ HELPERS โ”€โ”€
86
+
87
+ def sess(cid):
88
+ if cid not in sessions:
89
+ sessions[cid] = {
90
+ 'username': None, 'coins': 0, 'is_admin': False,
91
+ 'voice': 'my-MM-ThihaNeural', 'engine': 'ms',
92
+ 'speed': 30, 'crop': 'original',
93
+ 'flip': False, 'color': False,
94
+ 'watermark': '', 'content_type': 'Movie Recap',
95
+ 'ai_model': 'Gemini',
96
+ 'pending_url': None, 'pending_file_id': None,
97
+ 'music_file_id': None,
98
+ }
99
+ s = sessions[cid]
100
+ # Auto-sync coins from DB so admin top-ups reflect without re-login
101
+ if s.get('username'):
102
+ try:
103
+ fresh = get_coins(s['username'])
104
+ if fresh is not None:
105
+ s['coins'] = fresh
106
+ except Exception:
107
+ pass
108
+ return s
109
+
110
+ def is_logged(cid): return sess(cid).get('username') is not None
111
+ def fmt_coins(c): return 'โˆž' if c == -1 else str(c)
112
+
113
+ def main_kb(cid):
114
+ s = sess(cid)
115
+ rows = [
116
+ [KeyboardButton('๐ŸŽฌ Auto Process'), KeyboardButton('๐Ÿ”Š แ€กแ€žแ€ถ')],
117
+ [KeyboardButton('โš™๏ธ Settings'), KeyboardButton('๐Ÿ‘ค แ€กแ€€แ€ฑแ€ฌแ€„แ€ทแ€บ')],
118
+ [KeyboardButton('๐Ÿ›’ Coins แ€แ€šแ€บแ€›แ€”แ€บ'), KeyboardButton('๐Ÿ”„ Reset')],
119
+ ]
120
+ if s.get('is_admin'):
121
+ rows.append([KeyboardButton('๐Ÿ‘‘ Admin')])
122
+ rows.append([KeyboardButton('๐Ÿšช Logout')])
123
+ return ReplyKeyboardMarkup(rows, resize_keyboard=True)
124
+
125
+ def ensure_whisper():
126
+ with _wm_lock:
127
+ if _whisper_model[0] is None and whisper_mod:
128
+ _whisper_model[0] = whisper_mod.load_model('tiny', device='cpu')
129
+ return _whisper_model[0]
130
+
131
+ def settings_kb(cid):
132
+ s = sess(cid)
133
+ crop_labels = {'original':'๐ŸŽฌ Original','9:16':'๐Ÿ“ฑ 9:16','16:9':'๐Ÿ–ฅ๏ธ 16:9','1:1':'โฌ› 1:1'}
134
+ return InlineKeyboardMarkup([
135
+ [InlineKeyboardButton(f"๐Ÿ“ Crop: {crop_labels.get(s['crop'],s['crop'])}", callback_data='set|crop')],
136
+ [InlineKeyboardButton(f"๐Ÿค– AI: {s['ai_model']}", callback_data='set|ai'),
137
+ InlineKeyboardButton(f"๐Ÿ“บ {s['content_type'].split('/')[0]}", callback_data='set|ct')],
138
+ [InlineKeyboardButton(f"Flip: {'ON' if s['flip'] else 'OFF'}", callback_data='set|flip'),
139
+ InlineKeyboardButton(f"Color: {'ON' if s['color'] else 'OFF'}", callback_data='set|color')],
140
+ [InlineKeyboardButton(f"Speed: {s['speed']}%", callback_data='set|speed')],
141
+ [InlineKeyboardButton(f"Watermark: {s['watermark'] or 'แ€™แ€›แ€พแ€ญ'}", callback_data='set|wmk')],
142
+ [InlineKeyboardButton(f"๐ŸŽต BG Music: {'โœ… แ€•แ€ซแ€™แ€Šแ€บ' if s.get('music_file_id') else 'โŒ แ€™แ€•แ€ซ'}", callback_data='set|music'),
143
+ InlineKeyboardButton('๐Ÿ—‘๏ธ Music แ€–แ€ปแ€€แ€บ', callback_data='set|music_del')],
144
+ [InlineKeyboardButton('โœ… แ€žแ€ญแ€™แ€บแ€ธแ€™แ€Šแ€บ', callback_data='set|done')],
145
+ ])
146
+
147
+ def voice_kb():
148
+ btns = [[InlineKeyboardButton('โ”€โ”€ Microsoft TTS โ”€โ”€', callback_data='noop')]]
149
+ row = []
150
+ for name, vid, eng in MS_VOICES:
151
+ row.append(InlineKeyboardButton(name, callback_data=f'voice|{vid}|{eng}'))
152
+ btns.append(row)
153
+ btns.append([InlineKeyboardButton('โ”€โ”€ Gemini TTS โ”€โ”€', callback_data='noop')])
154
+ row = []
155
+ for name, vid, eng in GEMINI_VOICES:
156
+ row.append(InlineKeyboardButton(name, callback_data=f'voice|{vid}|{eng}'))
157
+ if len(row) == 3:
158
+ btns.append(row); row = []
159
+ if row: btns.append(row)
160
+ return InlineKeyboardMarkup(btns)
161
+
162
+ def package_kb():
163
+ btns = []
164
+ for coins, price, desc in PACKAGES:
165
+ btns.append([InlineKeyboardButton(
166
+ f"๐Ÿช™ {coins} Coins โ€” {price:,} MMK ({desc})",
167
+ callback_data=f'pkg|{coins}|{price}'
168
+ )])
169
+ return InlineKeyboardMarkup(btns)
170
+
171
+ PAYMENT_INFO = (
172
+ "๐Ÿ’ณ *แ€„แ€ฝแ€ฑแ€œแ€ฝแ€ฒแ€”แ€Šแ€บแ€ธ*\n\n"
173
+ "๐Ÿ“ฑ KBZPay / Wave / AYA Pay\n\n"
174
+ "๐Ÿ‘ค *Phoe Shan*\n"
175
+ "๐Ÿ“ž *09679871352* (Kapy)\n\n"
176
+ "แ€„แ€ฝแ€ฑแ€œแ€ฝแ€ฒแ€•แ€ผแ€ฎแ€ธแ€”แ€ฑแ€ฌแ€€แ€บ slip แ€€แ€ญแ€ฏ Admin แ€‘แ€ถ แ€•แ€ญแ€ฏแ€ทแ€•แ€ฑแ€ธแ€•แ€ซ"
177
+ )
178
+
179
+ def cancel_kb():
180
+ return InlineKeyboardMarkup([[InlineKeyboardButton('โŒ แ€–แ€ปแ€€แ€บแ€žแ€ญแ€™แ€บแ€ธแ€™แ€Šแ€บ', callback_data='cancel|process')]])
181
+
182
+ # โ”€โ”€ AUTH โ”€โ”€
183
+
184
+ async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
185
+ cid = update.effective_chat.id
186
+ if is_logged(cid):
187
+ s = sess(cid)
188
+ await update.message.reply_text(
189
+ f"๐Ÿ‘‹ แ€•แ€ผแ€”แ€บแ€œแ€ฌแ€žแ€Šแ€ทแ€บแ€กแ€แ€ฝแ€€แ€บ แ€€แ€ผแ€ญแ€ฏแ€†แ€ญแ€ฏแ€•แ€ซแ€แ€šแ€บ *{s['username']}*!\n๐Ÿช™ Coins: *{fmt_coins(s['coins'])}*",
190
+ parse_mode=ParseMode.MARKDOWN, reply_markup=main_kb(cid))
191
+ return ST_MAIN
192
+ await update.message.reply_text(
193
+ "๐ŸŽฌ *Recap Studio Bot*\n\n"
194
+ "AI-Powered Video Recap Tool\n\n"
195
+ "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”\n"
196
+ "๐Ÿ‘จโ€๐Ÿ’ป *Developer* โ€” @PhoeShan2001\n"
197
+ "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”\n\n"
198
+ "แ€žแ€„แ€ทแ€บ *username* แ€‘แ€Šแ€ทแ€บแ€•แ€ซ โ€”",
199
+ parse_mode=ParseMode.MARKDOWN,
200
+ reply_markup=ReplyKeyboardMarkup(
201
+ [[KeyboardButton('/start')]],
202
+ resize_keyboard=True
203
+ ))
204
+ return ST_LOGIN_USER
205
+
206
+ async def recv_login_user(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
207
+ ctx.user_data['login_user'] = update.message.text.strip()
208
+ await update.message.reply_text("๐Ÿ”‘ *Password* แ€‘แ€Šแ€ทแ€บแ€•แ€ซ\n_(แ€™แ€›แ€พแ€ญแ€›แ€„แ€บ `-` แ€•แ€ญแ€ฏแ€ทแ€•แ€ซ)_", parse_mode=ParseMode.MARKDOWN)
209
+ return ST_LOGIN_PASS
210
+
211
+ async def recv_login_pass(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
212
+ cid = update.effective_chat.id
213
+ u = ctx.user_data.get('login_user', '')
214
+ p = update.message.text.strip()
215
+ if p == '-': p = ''
216
+ ok, msg, coins = login_user(u, p)
217
+ if not ok:
218
+ await update.message.reply_text(f"โŒ {msg}\n\n*Username* แ€‘แ€•แ€บแ€‘แ€Šแ€ทแ€บแ€•แ€ซ โ€”", parse_mode=ParseMode.MARKDOWN)
219
+ return ST_LOGIN_USER
220
+ s = sess(cid)
221
+ s['username'] = u; s['coins'] = coins; s['is_admin'] = (u == ADMIN_U)
222
+ # Save Telegram chat ID for payment approve notifications
223
+ try:
224
+ db = load_db(); db['users'][u]['tg_chat_id'] = cid; save_db(db)
225
+ except: pass
226
+ await update.message.reply_text(
227
+ f"โœ… *{u}* แ€กแ€”แ€ฑแ€–แ€ผแ€„แ€ทแ€บ แ€แ€„แ€บแ€›แ€ฑแ€ฌแ€€แ€บแ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ\n๐Ÿช™ Coins: *{fmt_coins(coins)}*",
228
+ parse_mode=ParseMode.MARKDOWN, reply_markup=main_kb(cid))
229
+ return ST_MAIN
230
+
231
+ async def cmd_logout(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
232
+ sessions.pop(update.effective_chat.id, None)
233
+ await update.message.reply_text("๐Ÿ‘‹ Logout แ€œแ€ฏแ€•แ€บแ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ\n/start แ€”แ€พแ€ญแ€•แ€บแ€•แ€ผแ€ฎแ€ธ แ€•แ€ผแ€”แ€บแ€แ€„แ€บแ€•แ€ซ",
234
+ reply_markup=ReplyKeyboardMarkup([[]], resize_keyboard=True))
235
+ return ConversationHandler.END
236
+
237
+ # โ”€โ”€ MENU BUTTONS โ”€โ”€
238
+
239
+ async def btn_auto_process(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
240
+ cid = update.effective_chat.id
241
+ if not is_logged(cid):
242
+ await update.message.reply_text("โŒ /start แ€”แ€พแ€ญแ€•แ€บแ€•แ€ผแ€ฎแ€ธ แ€ฆแ€ธแ€…แ€ฝแ€ฌ login แ€แ€„แ€บแ€•แ€ซ")
243
+ return ST_MAIN
244
+ s = sess(cid)
245
+ if s['coins'] != -1 and s['coins'] < 2:
246
+ await update.message.reply_text(
247
+ f"โŒ Coins แ€™แ€œแ€ฏแ€ถแ€œแ€ฑแ€ฌแ€€แ€บแ€˜แ€ฐแ€ธ\nแ€žแ€„แ€ทแ€บแ€™แ€พแ€ฌ *{s['coins']}* แ€›แ€พแ€ญแ€แ€šแ€บ โ€” *2* แ€œแ€ญแ€ฏแ€แ€šแ€บ\n\n๐Ÿ›’ Coins แ€แ€šแ€บแ€›แ€”แ€บ แ€แ€œแ€ฏแ€แ€บแ€”แ€พแ€ญแ€•แ€บแ€•แ€ซ",
248
+ parse_mode=ParseMode.MARKDOWN, reply_markup=main_kb(cid))
249
+ return ST_MAIN
250
+ cancel_flags[cid] = False
251
+ await update.message.reply_text(
252
+ "๐Ÿ“ฅ *Auto Process* โ€” 2 Coins แ€€แ€ฏแ€”แ€บแ€™แ€Šแ€บ\n\n"
253
+ "แ€กแ€ฑแ€ฌแ€€แ€บแ€•แ€ซ link แ€แ€…แ€บแ€แ€ฏแ€แ€ฏ แ€•แ€ญแ€ฏแ€ทแ€•แ€ซ แ€žแ€ญแ€ฏแ€ทแ€™แ€Ÿแ€ฏแ€แ€บ Video File แ€แ€„แ€บแ€•แ€ซ โ€”\n\n"
254
+ "โ€ข YouTube\nโ€ข TikTok\nโ€ข Facebook\nโ€ข Instagram\n\n"
255
+ "_(แ€–แ€ปแ€€แ€บแ€žแ€ญแ€™แ€บแ€ธแ€œแ€ญแ€ฏแ€›แ€„แ€บ โŒ แ€”แ€พแ€ญแ€•แ€บแ€•แ€ซ)_",
256
+ parse_mode=ParseMode.MARKDOWN,
257
+ reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton('โŒ แ€–แ€ปแ€€แ€บแ€žแ€ญแ€™แ€บแ€ธแ€™แ€Šแ€บ', callback_data='cancel|await')]]))
258
+ return ST_AWAIT_VIDEO
259
+
260
+ async def btn_voice(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
261
+ cid = update.effective_chat.id
262
+ s = sess(cid)
263
+ await update.message.reply_text(
264
+ f"๐Ÿ”Š *แ€กแ€žแ€ถ แ€›แ€ฝแ€ฑแ€ธแ€แ€ปแ€šแ€บแ€›แ€”แ€บ*\n\nแ€œแ€€แ€บแ€›แ€พแ€ญ โ€” *{s['voice']}* ({s['engine'].upper()})",
265
+ parse_mode=ParseMode.MARKDOWN, reply_markup=voice_kb())
266
+ return ST_MAIN
267
+
268
+ async def btn_settings(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
269
+ cid = update.effective_chat.id
270
+ await update.message.reply_text("โš™๏ธ *Settings*", parse_mode=ParseMode.MARKDOWN, reply_markup=settings_kb(cid))
271
+ return ST_MAIN
272
+
273
+ async def btn_account(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
274
+ cid = update.effective_chat.id
275
+ if not is_logged(cid):
276
+ await update.message.reply_text("โŒ /start แ€”แ€พแ€ญแ€•แ€บแ€•แ€ซ")
277
+ return ST_MAIN
278
+ s = sess(cid); db = load_db(); u = db['users'].get(s['username'], {})
279
+ await update.message.reply_text(
280
+ f"๐Ÿ‘ค *แ€€แ€ญแ€ฏแ€šแ€ทแ€บแ€กแ€€แ€ฑแ€ฌแ€„แ€ทแ€บ*\n\nUsername: `{s['username']}`\n๐Ÿช™ Coins: *{fmt_coins(s['coins'])}*\n"
281
+ f"๐ŸŽฌ Video: {u.get('total_videos',0)} แ€€แ€ผแ€ญแ€™แ€บ\n๐Ÿ“ Transcript: {u.get('total_transcripts',0)} แ€€แ€ผแ€ญแ€™แ€บ\n"
282
+ f"๐Ÿ“… แ€…แ€แ€„แ€บแ€žแ€ฑแ€ฌแ€”แ€ฑแ€ท: {u.get('created_at','')[:10]}",
283
+ parse_mode=ParseMode.MARKDOWN, reply_markup=main_kb(cid))
284
+ return ST_MAIN
285
+
286
+ async def btn_reset(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
287
+ cid = update.effective_chat.id
288
+ # Cancel any running process
289
+ if is_processing(cid):
290
+ cancel_flags[cid] = True
291
+ # Clear session state but keep login
292
+ s = sess(cid)
293
+ s['pending_url'] = None
294
+ s['pending_file_id'] = None
295
+ await update.message.reply_text(
296
+ "๐Ÿ”„ *Reset แ€œแ€ฏแ€•แ€บแ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ*\n\nแ€˜แ€ฌแ€œแ€ฏแ€•แ€บแ€™แ€œแ€ฒ?",
297
+ parse_mode=ParseMode.MARKDOWN,
298
+ reply_markup=main_kb(cid)
299
+ )
300
+ return ST_MAIN
301
+
302
+ async def btn_buy(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
303
+ cid = update.effective_chat.id
304
+ s = sess(cid)
305
+ await update.message.reply_text(
306
+ f"๐Ÿ›’ *Coins แ€แ€šแ€บแ€›แ€”แ€บ*\n\n"
307
+ f"แ€œแ€€แ€บแ€›แ€พแ€ญ Coins: *{fmt_coins(s['coins'])}*\n\n"
308
+ f"{PAYMENT_INFO}\n\n"
309
+ f"Package แ€แ€…แ€บแ€แ€ฏ แ€›แ€ฝแ€ฑแ€ธแ€•แ€ซ โ€”",
310
+ parse_mode=ParseMode.MARKDOWN, reply_markup=package_kb())
311
+ return ST_MAIN
312
+
313
+ async def btn_admin(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
314
+ cid = update.effective_chat.id
315
+ s = sess(cid)
316
+ if not s.get('is_admin'):
317
+ await update.message.reply_text("โŒ Admin แ€žแ€ฌ แ€แ€„แ€บแ€แ€ฝแ€„แ€ทแ€บแ€›แ€พแ€ญแ€žแ€Šแ€บ")
318
+ return ST_MAIN
319
+ db = load_db(); users = db.get('users', {})
320
+ lines = [f"๐Ÿ‘‘ *Admin Panel* โ€” {len(users)} แ€šแ€ฑแ€ฌแ€€แ€บ\n"]
321
+ for uname, udata in list(users.items())[:20]:
322
+ lines.append(f"โ€ข `{uname}` โ€” ๐Ÿช™{udata.get('coins',0)}")
323
+ await update.message.reply_text('\n'.join(lines), parse_mode=ParseMode.MARKDOWN,
324
+ reply_markup=InlineKeyboardMarkup([
325
+ [InlineKeyboardButton('โž• User แ€–แ€”แ€บแ€แ€ฎแ€ธ', callback_data='adm|create'),
326
+ InlineKeyboardButton('๐Ÿช™ Coins แ€‘แ€Šแ€ทแ€บ', callback_data='adm|coins')],
327
+ [InlineKeyboardButton('๐Ÿ—‘๏ธ User แ€–แ€ปแ€€แ€บ', callback_data='adm|delete'),
328
+ InlineKeyboardButton('๐Ÿ”„ Refresh', callback_data='adm|refresh')],
329
+ ]))
330
+ return ST_MAIN
331
+
332
+ # โ”€โ”€ VIDEO INPUT โ”€โ”€
333
+
334
+ async def recv_video_input(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
335
+ cid = update.effective_chat.id
336
+ s = sess(cid)
337
+ if cancel_flags.get(cid):
338
+ await update.message.reply_text("โ›” แ€–แ€ปแ€€แ€บแ€žแ€ญแ€™แ€บแ€ธแ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ", reply_markup=main_kb(cid))
339
+ return ST_MAIN
340
+ if update.message.text:
341
+ url = update.message.text.strip()
342
+ if not re.match(r'https?://', url):
343
+ await update.message.reply_text("โŒ URL แ€™แ€™แ€พแ€”แ€บแ€˜แ€ฐแ€ธ\nYouTube / TikTok / Facebook / Instagram link แ€•แ€ญแ€ฏแ€ทแ€•แ€ซ")
344
+ return ST_AWAIT_VIDEO
345
+ s['pending_url'] = url; s['pending_file_id'] = None
346
+ elif update.message.video or update.message.document:
347
+ media = update.message.video or update.message.document
348
+ s['pending_file_id'] = media.file_id; s['pending_url'] = None
349
+ else:
350
+ await update.message.reply_text("โŒ URL แ€žแ€ญแ€ฏแ€ทแ€™แ€Ÿแ€ฏแ€แ€บ Video File แ€•แ€ญแ€ฏแ€ทแ€•แ€ซ")
351
+ return ST_AWAIT_VIDEO
352
+ # โ”€โ”€ Reject if already processing โ”€โ”€
353
+ if is_processing(cid):
354
+ await update.message.reply_text(
355
+ "โณ *แ€œแ€ฏแ€•แ€บแ€†แ€ฑแ€ฌแ€„แ€บแ€”แ€ฑแ€†แ€ฒแ€–แ€ผแ€…แ€บแ€žแ€Šแ€บ*\n\nแ€•แ€ผแ€ฎแ€ธแ€™แ€พ แ€‘แ€•แ€บแ€•แ€ญแ€ฏแ€ทแ€•แ€ซ",
356
+ parse_mode=ParseMode.MARKDOWN
357
+ )
358
+ return ST_MAIN
359
+
360
+ # โ”€โ”€ Coin check before starting โ”€โ”€
361
+ fresh_coins = get_coins(s['username'])
362
+ if fresh_coins is not None:
363
+ s['coins'] = fresh_coins
364
+ if not s['is_admin'] and s['coins'] != -1 and s['coins'] < 2:
365
+ await update.message.reply_text(
366
+ f"โŒ *Coins แ€™แ€œแ€ฏแ€ถแ€œแ€ฑแ€ฌแ€€แ€บแ€˜แ€ฐแ€ธ*\n\n"
367
+ f"แ€žแ€„แ€ทแ€บแ€™แ€พแ€ฌ *{s['coins']}* แ€›แ€พแ€ญแ€แ€šแ€บ โ€” *2* แ€œแ€ญแ€ฏแ€แ€šแ€บ\n\n"
368
+ f"๐Ÿ›’ Coins แ€แ€šแ€บแ€›แ€”แ€บ แ€แ€œแ€ฏแ€แ€บแ€”แ€พแ€ญแ€•แ€บแ€•แ€ซ",
369
+ parse_mode=ParseMode.MARKDOWN, reply_markup=main_kb(cid)
370
+ )
371
+ return ST_MAIN
372
 
373
+ cancel_flags[cid] = False
374
+ prog_msg = await update.message.reply_text(
375
+ "โณ *แ€…แ€แ€„แ€บแ€”แ€ฑแ€•แ€ซแ€žแ€Šแ€บโ€ฆ* แ€แ€”แ€…แ€ฑแ€ฌแ€„แ€ทแ€บแ€•แ€ซ",
376
+ parse_mode=ParseMode.MARKDOWN, reply_markup=cancel_kb()
377
+ )
378
+ start_job(ctx.bot, cid, s.get('pending_url'), s.get('pending_file_id'), prog_msg.message_id)
379
+ return ST_MAIN
380
 
381
+ # โ”€โ”€ PROCESSING โ”€โ”€
 
 
382
 
383
+ def _process_thread(bot, cid, prog_msg_id, pending_url=None, pending_file_id=None):
384
+ set_processing(cid, True)
385
+ import asyncio as _asyncio
386
+ loop = _asyncio.new_event_loop()
387
+ _asyncio.set_event_loop(loop)
388
  try:
389
+ loop.run_until_complete(_do_process(bot, cid, prog_msg_id, pending_url, pending_file_id))
390
+ finally:
391
+ loop.close()
392
+ set_processing(cid, False)
393
+
394
+ async def _do_process(bot, cid, prog_msg_id, pending_url=None, pending_file_id=None):
395
+ s = sess(cid)
396
+ tid = uuid.uuid4().hex[:8]
397
+ tmp_dir = str(BASE_DIR / f'temp_{tid}')
398
+ out_file = str(OUTPUT_DIR / f'final_{tid}.mp4')
399
+ os.makedirs(tmp_dir, exist_ok=True)
400
+
401
+ async def prog(text, show_cancel=False):
402
+ try:
403
+ await bot.edit_message_text(chat_id=cid, message_id=prog_msg_id, text=text,
404
+ parse_mode=ParseMode.MARKDOWN, reply_markup=cancel_kb() if show_cancel else None)
405
+ except Exception: pass
406
 
407
+ def is_cancelled(): return cancel_flags.get(cid, False)
 
 
408
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  try:
410
+ # โ”€โ”€ Coin check at start of each job (important for queued jobs) โ”€โ”€
411
+ if not s['is_admin']:
412
+ fresh = get_coins(s['username'])
413
+ if fresh is not None:
414
+ s['coins'] = fresh
415
+ if s['coins'] != -1 and s['coins'] < 2:
416
+ await prog(
417
+ f"โŒ *Coins แ€™แ€œแ€ฏแ€ถแ€œแ€ฑแ€ฌแ€€แ€บแ€˜แ€ฐแ€ธ*\n\n"
418
+ f"แ€žแ€„แ€ทแ€บแ€™แ€พแ€ฌ *{fmt_coins(s['coins'])}* แ€›แ€พแ€ญแ€แ€šแ€บ โ€” *2* แ€œแ€ญแ€ฏแ€แ€šแ€บ\n"
419
+ f"๐Ÿ›’ Coins แ€แ€šแ€บแ€•แ€ผแ€ฎแ€ธแ€”แ€ฑแ€ฌแ€€แ€บ แ€‘แ€•แ€บแ€€แ€ผแ€ญแ€ฏแ€ธแ€…แ€ฌแ€ธแ€•แ€ซ"
420
+ )
421
+ return
422
+
423
+ await prog("๐Ÿ“ฅ *แ€กแ€†แ€„แ€ทแ€บ 1/5* โ€” Video แ€’แ€ฑแ€ซแ€„แ€บแ€ธแ€œแ€ฏแ€•แ€บ แ€œแ€ฏแ€•แ€บแ€”แ€ฑแ€žแ€Šแ€บโ€ฆ", show_cancel=True)
424
+ vpath = None
425
+ if pending_file_id:
426
+ file_obj = await bot.get_file(pending_file_id)
427
+ vpath = f'{tmp_dir}/input.mp4'
428
+ await file_obj.download_to_drive(vpath)
429
+ elif pending_url:
430
+ # cpu_queue_wait()
431
+ if is_cancelled(): await prog("โ›” แ€–แ€ปแ€€แ€บแ€žแ€ญแ€™แ€บแ€ธแ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ"); return
432
+ ytdlp_download(f'{tmp_dir}/input.%(ext)s', pending_url)
433
+ found = glob.glob(f'{tmp_dir}/input.*')
434
+ if found: vpath = found[0]
435
+ if not vpath or not os.path.exists(vpath):
436
+ await prog("โŒ Video แ€’แ€ฑแ€ซแ€„แ€บแ€ธแ€œแ€ฏแ€•แ€บ แ€™แ€กแ€ฑแ€ฌแ€„แ€บแ€™แ€ผแ€„แ€บแ€•แ€ซ"); return
437
+ if is_cancelled(): await prog("โ›” แ€–แ€ปแ€€แ€บแ€žแ€ญแ€™แ€บแ€ธแ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ"); return
438
+
439
+ # Download background music if set
440
+ mpath = None
441
+ if s.get('music_file_id'):
442
+ try:
443
+ mfile = await bot.get_file(s['music_file_id'])
444
+ mpath = f'{tmp_dir}/bgmusic.mp3'
445
+ await mfile.download_to_drive(mpath)
446
+ except Exception:
447
+ mpath = None
448
+
449
+ await prog("๐ŸŽ™๏ธ *แ€กแ€†แ€„แ€ทแ€บ 2/5* โ€” Whisper แ€–แ€ผแ€„แ€ทแ€บ แ€€แ€ฐแ€ธแ€šแ€ฐแ€”แ€ฑแ€žแ€Šแ€บโ€ฆ", show_cancel=True)
450
+ if whisper_mod is None: await prog("โŒ Whisper แ€™ install แ€›แ€žแ€ฑแ€ธแ€•แ€ซ"); return
451
+ model = ensure_whisper()
452
+ res = run_stage('whisper', model.transcribe, 'bot', lambda p,m: None, 'โณ Whisper แ€…แ€ฑแ€ฌแ€„แ€ทแ€บแ€”แ€ฑแ€žแ€Šแ€บ', '๐ŸŽ™๏ธ Transcribingโ€ฆ', vpath, fp16=False)
453
+ tr = res['text']; src_lang = res.get('language', 'en')
454
+ if is_cancelled(): await prog("โ›” แ€–แ€ปแ€€แ€บแ€žแ€ญแ€™แ€บแ€ธแ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ"); return
455
+
456
+ await prog(f"๐Ÿค– *แ€กแ€†แ€„แ€ทแ€บ 3/5* โ€” {s['ai_model']} แ€–แ€ผแ€„แ€ทแ€บ Script แ€›แ€ฑแ€ธแ€”แ€ฑแ€žแ€Šแ€บโ€ฆ", show_cancel=True)
457
+ sys_p = SYS_MED if s['content_type'] == 'Medical/Health' else SYS_MOVIE
458
+ sys_p = sys_p + '\n' + NUM_TO_MM_RULE
459
+ 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'])
460
+ sc, caption_text, hashtags = parse_out(out_txt)
461
+ if is_cancelled(): await prog("โ›” แ€–แ€ปแ€€แ€บแ€žแ€ญแ€™แ€บแ€ธแ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ"); return
462
+
463
+ await prog("๐Ÿ”Š *แ€กแ€†แ€„แ€ทแ€บ 4/5* โ€” แ€กแ€žแ€ถ แ€‘แ€ฏแ€แ€บแ€”แ€ฑแ€žแ€Šแ€บโ€ฆ", show_cancel=True)
464
+ sentences = split_txt(sc)
465
+ import asyncio as _aio, functools as _ft
466
+ loop = _aio.get_running_loop()
467
+ if s['engine'] == 'gemini':
468
+ parts = await loop.run_in_executor(None, _ft.partial(
469
+ run_stage, 'tts', run_gemini_tts_sync, 'bot', lambda p,m: None, 'โณ TTS แ€…แ€ฑแ€ฌแ€„แ€ทแ€บแ€”แ€ฑแ€žแ€Šแ€บ', '๐Ÿ”Š TTSโ€ฆ',
470
+ sentences, s['voice'], tmp_dir, speed=s['speed']))
471
+ else:
472
+ parts = await loop.run_in_executor(None, _ft.partial(
473
+ run_stage, 'tts', run_tts_sync, 'bot', lambda p,m: None, 'โณ TTS แ€…แ€ฑแ€ฌแ€„แ€ทแ€บแ€”แ€ฑแ€žแ€Šแ€บ', '๐Ÿ”Š TTSโ€ฆ',
474
+ sentences, s['voice'], f'+{s["speed"]}%', tmp_dir))
475
+ cmb = f'{tmp_dir}/combined.mp3'; lst = f'{tmp_dir}/list.txt'
476
+ with open(lst, 'w') as lf:
477
+ for a in parts: lf.write(f"file '{os.path.abspath(a)}'\n")
478
+ 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)
479
+ if is_cancelled(): await prog("โ›” แ€–แ€ปแ€€แ€บแ€žแ€ญแ€™แ€บแ€ธแ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ"); return
480
+
481
+ await prog("๐ŸŽฌ *แ€กแ€†แ€„แ€ทแ€บ 5/5* โ€” Video แ€•แ€ฑแ€ซแ€„แ€บแ€ธแ€…แ€•แ€บแ€”แ€ฑแ€žแ€Šแ€บโ€ฆ")
482
+ vd = dur(vpath); ad = dur(cmb)
483
+ if vd <= 0: await prog("โŒ Video duration แ€–แ€แ€บแ€™แ€›แ€•แ€ซ"); return
484
+ if ad <= 0: await prog("โŒ Audio duration แ€–แ€แ€บแ€™แ€›แ€•แ€ซ"); return
485
+ _build_video(vpath, cmb, mpath, ad, vd, s['crop'], s['flip'], s['color'], s['watermark'], out_file)
486
+
487
+ if not s['is_admin']:
488
+ ok2, rem = deduct(s['username'], 2)
489
+ if ok2:
490
+ s['coins'] = rem; upd_stat(s['username'], 'tr'); upd_stat(s['username'], 'vd')
491
+
492
+ await prog("โœ… แ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ! แ€•แ€ญแ€ฏแ€ทแ€”แ€ฑแ€žแ€Šแ€บโ€ฆ")
493
+ file_size = os.path.getsize(out_file)
494
+ caption = (
495
+ f"๐ŸŽฌ *{caption_text}*\n\n{hashtags}\n\n"
496
+ f"๐Ÿช™ แ€€แ€ปแ€”แ€บ Coins: *{fmt_coins(s['coins'])}*"
497
+ )
498
+
499
+ send_file = out_file # file to actually send
500
+
501
+ if file_size > TG_MAX_FILE_BYTES:
502
+ size_mb = file_size / (1024 * 1024)
503
+ await prog(f"๐Ÿ“ฆ File แ€€แ€ผแ€ฎแ€ธแ€”แ€ฑแ€žแ€Šแ€บ ({size_mb:.1f} MB) โ€” Compress แ€œแ€ฏแ€•แ€บแ€”แ€ฑแ€žแ€Šแ€บโ€ฆ")
504
+ compressed_file = out_file.replace('.mp4', '_compressed.mp4')
505
+ try:
506
+ # Two-pass target: aim for 45 MB to stay safely under 49 MB limit
507
+ target_size_kb = 45 * 1024
508
+ # Get video duration for bitrate calculation
509
+ probe = subprocess.run(
510
+ ['ffprobe', '-v', 'error', '-show_entries', 'format=duration',
511
+ '-of', 'default=noprint_wrappers=1:nokey=1', out_file],
512
+ capture_output=True, text=True
513
+ )
514
+ video_duration = float(probe.stdout.strip()) if probe.stdout.strip() else 60.0
515
+ # Target total bitrate (kbps), reserve 128kbps for audio
516
+ target_total_kbps = int((target_size_kb * 8) / video_duration)
517
+ video_kbps = max(target_total_kbps - 128, 200)
518
+ compress_cmd = (
519
+ f'ffmpeg -y -i "{out_file}" '
520
+ f'-c:v libx264 -b:v {video_kbps}k -preset fast -maxrate {video_kbps*2}k -bufsize {video_kbps*4}k '
521
+ f'-c:a aac -b:a 128k '
522
+ f'"{compressed_file}"'
523
+ )
524
+ subprocess.run(compress_cmd, shell=True, check=True,
525
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
526
+ compressed_size = os.path.getsize(compressed_file)
527
+ if compressed_size <= TG_MAX_FILE_BYTES:
528
+ send_file = compressed_file
529
+ logger.info(f"[{tid}] Compressed {size_mb:.1f}MB โ†’ {compressed_size/1024/1024:.1f}MB")
530
+ else:
531
+ logger.warning(f"[{tid}] Compress แ€™๏ฟฝ๏ฟฝแ€ฑแ€ฌแ€„แ€บแ€™แ€ผแ€„แ€บ โ€” {compressed_size/1024/1024:.1f}MB แ€€แ€ปแ€”แ€บแ€”แ€ฑแ€žแ€ฑแ€ธแ€žแ€Šแ€บ")
532
+ send_file = None # fall through to link
533
+ except Exception as ce:
534
+ logger.warning(f"[{tid}] Compress error: {ce}")
535
+ send_file = None
536
+
537
+ if send_file is None:
538
+ # Compression failed or still too large โ€” send link
539
+ file_name = Path(out_file).name
540
+ dl_url = f"{WEB_BASE_URL}/outputs/{file_name}"
541
+ await bot.send_message(
542
+ chat_id=cid,
543
+ text=(
544
+ f"๐ŸŽฌ *{caption_text}*\n\n"
545
+ f"{hashtags}\n\n"
546
+ f"๐Ÿ“ฆ File แ€€แ€ผแ€ฎแ€ธแ€žแ€ฑแ€ฌแ€€แ€ผแ€ฑแ€ฌแ€„แ€ทแ€บ ({size_mb:.1f} MB) แ€แ€ญแ€ฏแ€€แ€บแ€›แ€ญแ€ฏแ€€แ€บ แ€•แ€ญแ€ฏแ€ทแแ€™แ€›แ€•แ€ซ\n\n"
547
+ f"๐Ÿ”— *Download Link:*\n`{dl_url}`\n\n"
548
+ f"๐Ÿช™ แ€€แ€ปแ€”แ€บ Coins: *{fmt_coins(s['coins'])}*"
549
+ ),
550
+ parse_mode=ParseMode.MARKDOWN,
551
+ )
552
+ send_file = None # skip video send below
553
+
554
+ if send_file is not None:
555
+ with open(send_file, 'rb') as vf:
556
+ await bot.send_video(
557
+ chat_id=cid, video=vf, caption=caption,
558
+ parse_mode=ParseMode.MARKDOWN, supports_streaming=True,
559
+ read_timeout=300, write_timeout=300,
560
+ )
561
+ await bot.delete_message(chat_id=cid, message_id=prog_msg_id)
562
  except Exception as e:
563
+ logger.exception(f'[{tid}] Error: {e}')
564
+ await prog(f"โŒ Error: {e}")
565
+ finally:
566
+ shutil.rmtree(tmp_dir, ignore_errors=True)
567
+ s['pending_url'] = None; s['pending_file_id'] = None
568
+ cancel_flags.pop(cid, None)
569
+
570
+ # โ”€โ”€ CALLBACKS โ”€โ”€
571
+
572
+ async def on_callback(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
573
+ q = update.callback_query
574
+ cid = q.message.chat_id
575
+ s = sess(cid)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
  try:
577
+ await q.answer()
578
+ except Exception:
579
+ pass
580
+ data = q.data
581
+
582
+ if data == 'noop': return
583
+
584
+ # โ”€โ”€ Admin payment approve / reject (from admin chat) โ”€โ”€
585
+ if data.startswith('adm_pay|'):
586
+ parts = data.split('|')
587
+ action = parts[1] # approve / reject
588
+ payment_id = parts[2]
589
+ username = parts[3]
590
+ from datetime import datetime as _dt
591
+ pdb = load_payments_db()
592
+ pay = next((p for p in pdb['payments'] if p['id'] == payment_id), None)
593
+ if not pay:
594
+ await q.answer("โŒ Payment แ€™แ€แ€ฝแ€ฑแ€ทแ€•แ€ซ", show_alert=True)
595
+ return
596
+ if pay['status'] != 'pending':
597
+ await q.answer(f"โš ๏ธ แ€’แ€ฎ payment {pay['status']} แ€–แ€ผแ€…แ€บแ€•แ€ผแ€ฎแ€ธแ€•แ€ผแ€ฎ", show_alert=True)
598
+ return
599
+ if action == 'approve':
600
+ coins = int(parts[4])
601
+ pay['status'] = 'approved'
602
+ pay['updated_at'] = _dt.now().isoformat()
603
+ save_payments_db(pdb)
604
+ # Add coins to user
605
+ add_coins_fn(username, coins, 'bot_admin')
606
+ await q.edit_message_caption(
607
+ caption=q.message.caption + f"\n\nโœ… <b>Approved!</b> +{coins} coins โ†’ {username}",
608
+ parse_mode=ParseMode.HTML
609
+ )
610
+ # Notify user if online (best effort)
611
+ try:
612
+ db = load_db()
613
+ tg_cid = db['users'].get(username, {}).get('tg_chat_id')
614
+ if tg_cid:
615
+ await ctx.bot.send_message(
616
+ chat_id=tg_cid,
617
+ text=f"โœ… *Coins แ€‘แ€Šแ€ทแ€บแ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ!*\n\n๐Ÿช™ *+{coins} Coins* แ€žแ€„แ€ทแ€บแ€กแ€€แ€ฑแ€ฌแ€„แ€ทแ€บแ€‘แ€ฒ แ€›แ€ฑแ€ฌแ€€แ€บแ€•แ€ผแ€ฎ\n๐Ÿ†” ID: `{payment_id}`",
618
+ parse_mode=ParseMode.MARKDOWN
619
+ )
620
+ except: pass
621
+ else:
622
+ pay['status'] = 'rejected'
623
+ pay['updated_at'] = _dt.now().isoformat()
624
+ save_payments_db(pdb)
625
+ await q.edit_message_caption(
626
+ caption=q.message.caption + f"\n\nโŒ <b>Rejected</b>",
627
+ parse_mode=ParseMode.HTML
628
+ )
629
+ try:
630
+ db = load_db()
631
+ tg_cid = db['users'].get(username, {}).get('tg_chat_id')
632
+ if tg_cid:
633
+ await ctx.bot.send_message(
634
+ chat_id=tg_cid,
635
+ text=f"โŒ *Payment แ€„แ€ผแ€„แ€บแ€ธแ€•แ€šแ€บแ€แ€ผแ€„แ€บแ€ธแ€แ€ถแ€›แ€žแ€Šแ€บ*\n\n๐Ÿ†” ID: `{payment_id}`\n\nแ€™แ€ฑแ€ธแ€™แ€ผแ€”แ€บแ€ธแ€›แ€”แ€บ โ€” @{ADMIN_TG_USERNAME}",
636
+ parse_mode=ParseMode.MARKDOWN
637
+ )
638
+ except: pass
639
+ await q.answer()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
640
  return
641
+
642
+ if data.startswith('cancel|'):
643
+ cancel_flags[cid] = True
644
+ await q.edit_message_text("โ›” แ€–แ€ปแ€€แ€บแ€žแ€ญแ€™แ€บแ€ธแ€”แ€ฑแ€žแ€Šแ€บโ€ฆ" if data == 'cancel|process' else "โ›” แ€–แ€ปแ€€แ€บแ€žแ€ญแ€™แ€บแ€ธแ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
645
  return
646
 
647
+ if data.startswith('pkg|'):
648
+ if data == 'pkg|back':
649
+ await q.edit_message_text(f"๐Ÿ›’ *Coins แ€แ€šแ€บแ€›แ€”แ€บ*\n\nPackage แ€›แ€ฝแ€ฑแ€ธแ€•แ€ซ โ€”", parse_mode=ParseMode.MARKDOWN, reply_markup=package_kb())
650
+ return
651
+ _, coins, price = data.split('|')
652
+ coins = int(coins); price = int(price)
653
+ # Save selected package for slip upload
654
+ pending_payment[cid] = {'coins': coins, 'price': price}
655
+ ctx.user_data['await_slip'] = True
656
+ await q.edit_message_text(
657
+ f"๐Ÿช™ *{coins} Coins* โ€” {price:,} MMK\n\n"
658
+ f"{PAYMENT_INFO}\n\n"
659
+ f"๐Ÿ“ธ แ€„แ€ฝแ€ฑแ€œแ€ฝแ€ฒแ€•แ€ผแ€ฎแ€ธแ€”แ€ฑแ€ฌแ€€แ€บ *Slip แ€“แ€ฌแ€แ€บแ€•แ€ฏแ€ถ* แ€’แ€ฎ chat แ€‘แ€ฒ แ€แ€ญแ€ฏแ€€แ€บแ€›แ€ญแ€ฏแ€€แ€บแ€•แ€ญแ€ฏแ€ทแ€•แ€ซ",
660
+ parse_mode=ParseMode.MARKDOWN,
661
+ reply_markup=InlineKeyboardMarkup([
662
+ [InlineKeyboardButton('๐Ÿ”™ แ€•แ€ผแ€”แ€บแ€žแ€ฝแ€ฌแ€ธแ€™แ€Šแ€บ', callback_data='pkg|back')],
663
+ ]))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
664
  return
665
 
666
+ if data.startswith('voice|'):
667
+ _, vid, eng = data.split('|')
668
+ s['voice'] = vid; s['engine'] = eng
669
+ await q.edit_message_text(f"โœ… แ€กแ€žแ€ถ แ€žแ€แ€บแ€™แ€พแ€แ€บแ€•แ€ผแ€ฎแ€ธ โ€” *{vid}* ({'Microsoft' if eng=='ms' else 'Gemini'})", parse_mode=ParseMode.MARKDOWN)
670
  return
671
 
672
+ if data.startswith('set|'):
673
+ key = data.split('|')[1]
674
+ if key == 'flip': s['flip'] = not s['flip']
675
+ elif key == 'color': s['color'] = not s['color']
676
+ elif key == 'crop':
677
+ crops = ['original','9:16','16:9','1:1']
678
+ s['crop'] = crops[(crops.index(s['crop'])+1) % len(crops)] if s['crop'] in crops else 'original'
679
+ elif key == 'ai': s['ai_model'] = 'DeepSeek' if s['ai_model'] == 'Gemini' else 'Gemini'
680
+ elif key == 'ct': s['content_type'] = 'Medical/Health' if s['content_type'] == 'Movie Recap' else 'Movie Recap'
681
+ elif key == 'speed':
682
+ speeds = [0,10,20,30,40,50,60,80]
683
+ cur = s['speed']
684
+ s['speed'] = speeds[(speeds.index(cur)+1) % len(speeds)] if cur in speeds else 30
685
+ elif key == 'wmk':
686
+ ctx.user_data['await_wmk'] = True
687
+ await q.edit_message_text("๐Ÿ’ง Watermark แ€…แ€ฌแ€žแ€ฌแ€ธแ€•แ€ญแ€ฏแ€ทแ€•แ€ซ (แ€ฅแ€•แ€™แ€ฌ โ€” @username):")
688
+ return
689
+ elif key == 'music':
690
+ ctx.user_data['await_music'] = True
691
+ await q.edit_message_text(
692
+ "๐ŸŽต *Background Music*\n\n"
693
+ "MP3 / Audio file แ€แ€„แ€บแ€•แ€ซ\n"
694
+ "_(แ€–แ€ญแ€ฏแ€„แ€บแ€แ€„แ€บแ€•แ€ผแ€ฎแ€ธแ€”แ€ฑแ€ฌแ€€แ€บ auto แ€žแ€ญแ€™แ€บแ€ธแ€™แ€Šแ€บ)_",
695
+ parse_mode=ParseMode.MARKDOWN)
696
+ return
697
+ elif key == 'music_del':
698
+ s['music_file_id'] = None
699
+ await q.answer("๐Ÿ—‘๏ธ Music แ€–แ€ปแ€€แ€บแ€•แ€ผแ€ฎแ€ธ", show_alert=False)
700
+ elif key == 'done':
701
+ await q.edit_message_text("โœ… Settings แ€žแ€ญแ€™แ€บแ€ธแ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ"); return
702
+ await q.edit_message_reply_markup(reply_markup=settings_kb(cid))
703
  return
704
 
705
+ if data.startswith('adm|'):
706
+ if not s.get('is_admin'): await q.edit_message_text("โŒ Admin only"); return
707
+ action = data.split('|')[1]
708
+ if action == 'refresh':
709
+ db = load_db(); users = db.get('users', {})
710
+ lines = [f"๐Ÿ‘‘ *Admin Panel* โ€” {len(users)} แ€šแ€ฑแ€ฌแ€€แ€บ\n"]
711
+ for uname, udata in list(users.items())[:20]:
712
+ lines.append(f"โ€ข `{uname}` โ€” ๐Ÿช™{udata.get('coins',0)}")
713
+ await q.edit_message_text('\n'.join(lines), parse_mode=ParseMode.MARKDOWN,
714
+ reply_markup=InlineKeyboardMarkup([
715
+ [InlineKeyboardButton('โž• User แ€–แ€”แ€บแ€แ€ฎแ€ธ', callback_data='adm|create'),
716
+ InlineKeyboardButton('๐Ÿช™ Coins แ€‘แ€Šแ€ทแ€บ', callback_data='adm|coins')],
717
+ [InlineKeyboardButton('๐Ÿ—‘๏ธ User แ€–แ€ปแ€€แ€บ', callback_data='adm|delete'),
718
+ InlineKeyboardButton('๐Ÿ”„ Refresh', callback_data='adm|refresh')],
719
+ ]))
720
+ elif action == 'create':
721
+ ctx.user_data['adm_action'] = 'create'
722
+ await q.edit_message_text("โž• *User แ€–แ€”แ€บแ€แ€ฎแ€ธแ€›แ€”แ€บ*\n\n`username coins` แ€•แ€ญแ€ฏแ€ทแ€•แ€ซ\nแ€ฅแ€•แ€™แ€ฌ โ€” `CoolUser 10`", parse_mode=ParseMode.MARKDOWN)
723
+ elif action == 'coins':
724
+ ctx.user_data['adm_action'] = 'coins'
725
+ await q.edit_message_text("๐Ÿช™ *Coins แ€…แ€ฎแ€™แ€ถแ€›แ€”แ€บ*\n\n`username amount add|set` แ€•แ€ญแ€ฏแ€ทแ€•แ€ซ\nแ€ฅแ€•แ€™แ€ฌ โ€” `CoolUser 20 add`", parse_mode=ParseMode.MARKDOWN)
726
+ elif action == 'delete':
727
+ ctx.user_data['adm_action'] = 'delete'
728
+ await q.edit_message_text("๐Ÿ—‘๏ธ แ€–แ€ปแ€€แ€บแ€™แ€Šแ€ทแ€บ *username* แ€•แ€ญแ€ฏแ€ทแ€•แ€ซ โ€”", parse_mode=ParseMode.MARKDOWN)
729
+ return
730
 
731
+ # โ”€โ”€ TEXT HANDLER โ”€โ”€
732
+
733
+ # โ”€โ”€ MUSIC INPUT HANDLER โ”€โ”€
734
+
735
+ async def recv_music_input(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
736
+ cid = update.effective_chat.id
737
+ s = sess(cid)
738
+ if not ctx.user_data.get('await_music'):
739
+ # Not expecting music โ€” ignore audio uploads
740
+ return ST_MAIN
741
+ media = update.message.audio or update.message.document
742
+ if not media:
743
+ await update.message.reply_text("โŒ Audio file แ€™แ€Ÿแ€ฏแ€แ€บแ€•แ€ซ")
744
+ return ST_MAIN
745
+ s['music_file_id'] = media.file_id
746
+ ctx.user_data['await_music'] = False
747
+ await update.message.reply_text(
748
+ "โœ… Background music แ€žแ€ญแ€™แ€บแ€ธแ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ\n"
749
+ "โš™๏ธ Settings แ€‘แ€ฒแ€™แ€พแ€ฌ แ€•แ€ผแ€”แ€ฑแ€™แ€Šแ€บ",
750
+ reply_markup=main_kb(cid)
751
+ )
752
+ return ST_MAIN
753
+
754
+
755
+ async def recv_slip_photo(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
756
+ """Handle payment slip photo upload โ†’ save pending payment โ†’ notify admin."""
757
+ cid = update.effective_chat.id
758
+ s = sess(cid)
759
+
760
+ if not ctx.user_data.get('await_slip'):
761
+ # Not expecting slip โ€” treat as video if it's a photo? Just ignore.
762
+ await update.message.reply_text("โŒ Slip แ€™แ€™แ€ปแ€พแ€ฑแ€ฌแ€บแ€œแ€„แ€ทแ€บแ€‘แ€ฌแ€ธแ€•แ€ซแ‹ Package แ€กแ€›แ€„แ€บแ€›แ€ฝแ€ฑแ€ธแ€•แ€ซ ๐Ÿ›’", reply_markup=main_kb(cid))
763
+ return ST_MAIN
764
+
765
+ if not update.message.photo:
766
+ await update.message.reply_text("โŒ แ€“แ€ฌแ€แ€บแ€•แ€ฏแ€ถ (Photo) แ€•แ€ญแ€ฏแ€ทแ€•แ€ซ โ€” Document แ€™แ€Ÿแ€ฏแ€แ€บแ€•แ€ซแ€”แ€ฒแ€ท", reply_markup=main_kb(cid))
767
+ return ST_MAIN
768
+
769
+ pkg = pending_payment.get(cid)
770
+ if not pkg:
771
+ await update.message.reply_text("โŒ Package แ€™แ€›แ€ฝแ€ฑแ€ธแ€›แ€žแ€ฑแ€ธแ€•แ€ซ โ€” Coins แ€แ€šแ€บแ€›แ€”แ€บ แ€€แ€ญแ€ฏแ€”แ€พแ€ญแ€•แ€บแ€•แ€ซ", reply_markup=main_kb(cid))
772
+ ctx.user_data['await_slip'] = False
773
+ return ST_MAIN
774
+
775
+ coins = pkg['coins']
776
+ price = pkg['price']
777
+ username = s.get('username', '')
778
+
779
+ # Download slip photo (largest size)
780
+ photo = update.message.photo[-1]
781
+ file = await ctx.bot.get_file(photo.file_id)
782
+
783
+ import io, base64, urllib.request as _ur
784
+ buf = io.BytesIO()
785
+ await file.download_to_memory(buf)
786
+ slip_b64 = base64.b64encode(buf.getvalue()).decode()
787
+ slip_data_url = f"data:image/jpeg;base64,{slip_b64}"
788
+
789
+ # Save to payments_db
790
+ import uuid as _uuid
791
+ from datetime import datetime as _dt
792
+ payment_id = _uuid.uuid4().hex[:10]
793
+ now = _dt.now().isoformat()
794
+ pdb = load_payments_db()
795
+ pdb['payments'].append({
796
+ 'id': payment_id,
797
+ 'username': username,
798
+ 'coins': coins,
799
+ 'price': price,
800
+ 'status': 'pending',
801
+ 'created_at': now,
802
+ 'updated_at': now,
803
+ 'slip_image': slip_data_url,
804
+ 'admin_note': '',
805
+ })
806
+ save_payments_db(pdb)
807
+
808
+ ctx.user_data['await_slip'] = False
809
+ pending_payment.pop(cid, None)
810
+
811
+ # Notify admin via Telegram (send slip photo directly)
812
+ caption = (
813
+ f"๐Ÿ’ฐ <b>Payment Request</b>\n"
814
+ f"๐Ÿ‘ค User: <code>{username}</code>\n"
815
+ f"๐Ÿช™ Coins: {coins}\n"
816
+ f"๐Ÿ’ต Price: {price:,} MMK\n"
817
+ f"๐Ÿ†” ID: <code>{payment_id}</code>\n"
818
+ f"โฐ {now[:19]}\n\n"
819
+ f"โœ… Approve:\n<code>/approve {payment_id} {username} {coins}</code>\n"
820
+ f"โŒ Reject:\n<code>/reject {payment_id}</code>"
821
+ )
822
 
823
+ if ADMIN_TELEGRAM_CHAT_ID and TELEGRAM_BOT_TOKEN:
 
 
 
 
824
  try:
825
+ buf.seek(0)
826
+ await ctx.bot.send_photo(
827
+ chat_id=ADMIN_TELEGRAM_CHAT_ID,
828
+ photo=buf,
829
+ caption=caption,
830
+ parse_mode=ParseMode.HTML,
831
+ reply_markup=InlineKeyboardMarkup([
832
+ [InlineKeyboardButton(f"โœ… Approve {coins} coins", callback_data=f"adm_pay|approve|{payment_id}|{username}|{coins}")],
833
+ [InlineKeyboardButton("โŒ Reject", callback_data=f"adm_pay|reject|{payment_id}|{username}")],
834
+ ])
835
+ )
836
  except Exception as e:
837
+ logger.error(f"Admin notify failed: {e}")
838
+
839
+ await update.message.reply_text(
840
+ f"โœ… *Slip แ€œแ€€แ€บแ€แ€ถแ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ!*\n\n"
841
+ f"๐Ÿช™ Package: *{coins} Coins* โ€” {price:,} MMK\n"
842
+ f"๐Ÿ†” ID: `{payment_id}`\n\n"
843
+ f"Admin แ€…แ€…แ€บแ€†แ€ฑแ€ธแ€•แ€ผแ€ฎแ€ธ Coins แ€‘แ€Šแ€ทแ€บแ€•แ€ฑแ€ธแ€•แ€ซแ€™แ€Šแ€บ โณ",
844
+ parse_mode=ParseMode.MARKDOWN,
845
+ reply_markup=main_kb(cid)
846
+ )
847
+ return ST_MAIN
848
+
849
+
850
+ async def recv_text(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
851
+ cid = update.effective_chat.id
852
+ text = update.message.text.strip()
853
+ s = sess(cid)
854
+
855
+ # Music file handled via recv_music_input โ€” text fallback
856
+ if ctx.user_data.get('await_wmk'):
857
+ s['watermark'] = text; ctx.user_data['await_wmk'] = False
858
+ await update.message.reply_text(f"โœ… Watermark โ€” `{text}`", parse_mode=ParseMode.MARKDOWN, reply_markup=main_kb(cid))
859
+ return ST_MAIN
860
+
861
+ if ctx.user_data.get('adm_action') == 'create' and s.get('is_admin'):
862
+ parts = text.split()
863
+ uname = parts[0] if parts else ''
864
+ coins = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else 10
865
+ msg, created = create_user_fn(uname, coins, s['username'])
866
+ ctx.user_data.pop('adm_action', None)
867
+ await update.message.reply_text(
868
+ f"โœ… User แ€–แ€”แ€บแ€แ€ฎแ€ธแ€•แ€ผแ€ฎแ€ธ!\nUsername: `{created}`\nCoins: {coins}" if created else f"โŒ {msg}",
869
+ parse_mode=ParseMode.MARKDOWN, reply_markup=main_kb(cid))
870
+ return ST_MAIN
871
+
872
+ if ctx.user_data.get('adm_action') == 'coins' and s.get('is_admin'):
873
+ parts = text.split()
874
+ if len(parts) >= 3:
875
+ msg = set_coins_fn(parts[0], int(parts[1])) if parts[2] == 'set' else add_coins_fn(parts[0], int(parts[1]))
876
+ ctx.user_data.pop('adm_action', None)
877
+ await update.message.reply_text(f"โœ… {msg}", reply_markup=main_kb(cid))
878
+ else:
879
+ await update.message.reply_text("โŒ Format: `username amount add|set`", parse_mode=ParseMode.MARKDOWN)
880
+ return ST_MAIN
881
 
882
+ if ctx.user_data.get('adm_action') == 'delete' and s.get('is_admin'):
883
+ db = load_db()
884
+ if text in db['users']:
885
+ del db['users'][text]; save_db(db)
886
+ await update.message.reply_text(f"โœ… `{text}` แ€–แ€ปแ€€แ€บแ€•แ€ผแ€ฎแ€ธแ€•แ€ซแ€•แ€ผแ€ฎ", parse_mode=ParseMode.MARKDOWN, reply_markup=main_kb(cid))
887
+ else:
888
+ await update.message.reply_text(f"โŒ `{text}` แ€™แ€แ€ฝแ€ฑแ€ทแ€•แ€ซ", parse_mode=ParseMode.MARKDOWN, reply_markup=main_kb(cid))
889
+ ctx.user_data.pop('adm_action', None)
890
+ return ST_MAIN
891
+
892
+ if re.match(r'https?://', text):
893
+ s['pending_url'] = text; s['pending_file_id'] = None
894
+ if s['coins'] != -1 and s['coins'] < 2:
895
+ await update.message.reply_text(f"โŒ Coins แ€™แ€œแ€ฏแ€ถแ€œแ€ฑแ€ฌแ€€แ€บแ€˜แ€ฐแ€ธ ({s['coins']}) โ€” 2 แ€œแ€ญแ€ฏแ€žแ€Šแ€บ")
896
+ return ST_MAIN
897
+ # Busy check
898
+ if is_processing(cid):
899
+ await update.message.reply_text(
900
+ "โ›” *Process แ€œแ€ฏแ€•แ€บแ€”แ€ฑแ€†แ€ฒแ€–แ€ผแ€…แ€บแ€žแ€Šแ€บ*\n\n"
901
+ "แ€œแ€€แ€บแ€›แ€พแ€ญ video แ€•แ€ผแ€ฎแ€ธแ€™แ€พ แ€”แ€ฑแ€ฌแ€€แ€บแ€แ€…แ€บแ€แ€ฏ แ€•แ€ญแ€ฏแ€ทแ€•แ€ซ\n"
902
+ "_(แ€–แ€ปแ€€แ€บแ€žแ€ญแ€™แ€บแ€ธแ€œแ€ญแ€ฏแ€›แ€„แ€บ โŒ แ€”แ€พแ€ญแ€•แ€บแ€•แ€ซ)_",
903
+ parse_mode=ParseMode.MARKDOWN,
904
+ reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton('โŒ แ€–แ€ปแ€€แ€บแ€žแ€ญแ€™แ€บแ€ธแ€™แ€Šแ€บ', callback_data='cancel|process')]])
905
+ )
906
+ return ST_MAIN
907
+ # Coin check
908
+ fresh_coins2 = get_coins(s['username'])
909
+ if fresh_coins2 is not None:
910
+ s['coins'] = fresh_coins2
911
+ if not s['is_admin'] and s['coins'] != -1 and s['coins'] < 2:
912
+ await update.message.reply_text(
913
+ f"โŒ Coins แ€™แ€œแ€ฏแ€ถแ€œแ€ฑแ€ฌแ€€แ€บแ€˜แ€ฐแ€ธ ({s['coins']}) โ€” 2 แ€œแ€ญแ€ฏแ€žแ€Šแ€บ"
914
+ )
915
+ return ST_MAIN
916
+ cancel_flags[cid] = False
917
+ prog_msg = await update.message.reply_text(
918
+ "โณ *แ€…แ€แ€„แ€บแ€”แ€ฑแ€•แ€ซแ€žแ€Šแ€บโ€ฆ*",
919
+ parse_mode=ParseMode.MARKDOWN, reply_markup=cancel_kb()
920
+ )
921
+ start_job(ctx.bot, cid, text, None, prog_msg.message_id)
922
+ return ST_MAIN
923
+
924
+ await update.message.reply_text("แ€˜แ€ฌแ€œแ€ฏแ€•แ€บแ€™แ€œแ€ฒ?", reply_markup=main_kb(cid))
925
+ return ST_MAIN
926
 
927
+ # โ”€โ”€ MAIN โ”€โ”€
928
 
929
  def main():
 
930
  if not BOT_TOKEN:
931
+ logger.error('โŒ TELEGRAM_BOT_TOKEN แ€™แ€žแ€แ€บ๏ฟฝ๏ฟฝแ€พแ€แ€บแ€›แ€žแ€ฑแ€ธแ€•แ€ซ!'); return
932
+
933
+ application = Application.builder().token(BOT_TOKEN).build()
934
+ conv = ConversationHandler(
935
+ entry_points=[CommandHandler('start', cmd_start)],
936
+ states={
937
+ ST_LOGIN_USER: [MessageHandler(filters.TEXT & ~filters.COMMAND, recv_login_user)],
938
+ ST_LOGIN_PASS: [MessageHandler(filters.TEXT & ~filters.COMMAND, recv_login_pass)],
939
+ ST_MAIN: [
940
+ MessageHandler(filters.Regex('^๐ŸŽฌ Auto Process$'), btn_auto_process),
941
+ MessageHandler(filters.Regex('^๐Ÿ”Š แ€กแ€žแ€ถ$'), btn_voice),
942
+ MessageHandler(filters.Regex('^โš™๏ธ Settings$'), btn_settings),
943
+ MessageHandler(filters.Regex('^๐Ÿ‘ค แ€กแ€€แ€ฑแ€ฌแ€„แ€ทแ€บ$'), btn_account),
944
+ MessageHandler(filters.Regex('^๐Ÿ›’ Coins แ€แ€šแ€บแ€›แ€”แ€บ$'), btn_buy),
945
+ MessageHandler(filters.Regex('^๐Ÿ”„ Reset$'), btn_reset),
946
+ MessageHandler(filters.Regex('^๐Ÿ‘‘ Admin$'), btn_admin),
947
+ MessageHandler(filters.Regex('^๐Ÿšช Logout$'), cmd_logout),
948
+ MessageHandler(filters.VIDEO | filters.Document.VIDEO, recv_video_input),
949
+ MessageHandler(filters.AUDIO | filters.Document.AUDIO, recv_music_input),
950
+ MessageHandler(filters.PHOTO, recv_slip_photo),
951
+ MessageHandler(filters.TEXT & ~filters.COMMAND, recv_text),
952
+ CallbackQueryHandler(on_callback),
953
+ ],
954
+ ST_AWAIT_VIDEO: [
955
+ MessageHandler(filters.TEXT & ~filters.COMMAND, recv_video_input),
956
+ MessageHandler(filters.VIDEO | filters.Document.VIDEO, recv_video_input),
957
+ CallbackQueryHandler(on_callback),
958
+ ],
959
+ },
960
+ fallbacks=[CommandHandler('start', cmd_start), CommandHandler('logout', cmd_logout)],
961
+ per_chat=True, allow_reentry=True,
962
+ )
963
+ application.add_handler(conv)
964
+ logger.info('๐Ÿค– Recap Studio Bot แ€กแ€žแ€„แ€ทแ€บแ€–แ€ผแ€…แ€บแ€•แ€ผแ€ฎ!')
965
+ application.run_polling(drop_pending_updates=True)
966
 
967
  if __name__ == '__main__':
968
  main()