Phoe2004 commited on
Commit
02f0dd4
·
verified ·
1 Parent(s): 796385c

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +12 -20
  2. app.py +0 -170
  3. recap_tg_bot.py +225 -0
  4. start.sh +48 -6
Dockerfile CHANGED
@@ -13,7 +13,7 @@ ENV PATH="$DENO_INSTALL/bin:$PATH"
13
 
14
  WORKDIR /app
15
 
16
- # numpy ကို ဦးစွာ install (torch မတိုင်ခင်)
17
  RUN pip install --no-cache-dir "numpy<2"
18
 
19
  # torch CPU only
@@ -24,33 +24,36 @@ RUN pip install --no-cache-dir \
24
  COPY requirements.txt .
25
  RUN pip install --no-cache-dir -r requirements.txt
26
 
27
- # yt-dlp + openai-whisper (app uses 'import whisper' = openai-whisper)
28
  RUN pip install --no-cache-dir -U "yt-dlp[default,curl-cffi]"
29
  RUN pip install --no-cache-dir openai-whisper
30
 
31
- # App files (bot polling app.py ထဲပါပြီ — သီးသန့် bot file မလို)
32
  COPY app.py .
 
33
  COPY index.html .
34
  COPY privacy.html .
35
  COPY terms.html .
36
  COPY NotoSansMyanmar-Bold.ttf .
37
  COPY manifest.json .
38
  COPY sw.js .
39
- COPY m_youtube_com_cookies.txt* ./
 
40
 
41
- # Optional static files (မရှိရင် build မပျက်)
42
  COPY payment.html* ./
43
  COPY payment_history.html* ./
 
44
 
45
  # Directories
46
  RUN mkdir -p outputs slips temp_prev temp_thumb temp_
47
 
48
- # Myanmar font install
49
  RUN mkdir -p /usr/local/share/fonts/myanmar \
50
  && cp /app/NotoSansMyanmar-Bold.ttf /usr/local/share/fonts/myanmar/ \
51
  && fc-cache -fv
52
 
53
- # Isolated fonts.conf (Myanmar font only)
54
  RUN mkdir -p /app/fc_conf && \
55
  printf '<?xml version="1.0"?>\n\
56
  <!DOCTYPE fontconfig SYSTEM "fonts.dtd">\n\
@@ -62,21 +65,10 @@ RUN mkdir -p /app/fc_conf && \
62
  </fontconfig>\n' > /app/fc_conf/fonts.conf
63
 
64
  # Pre-cache Whisper tiny model
65
- RUN python -c "import whisper; m=whisper.load_model('tiny', device='cpu'); print('Whisper tiny cached OK')"
66
 
67
  # Sanity check
68
  RUN deno --version && yt-dlp --version && python -c "import numpy; print('numpy', numpy.__version__)"
69
 
70
  EXPOSE 7860
71
-
72
- # Gunicorn တိုက်ရိုက် run — start.sh မလို
73
- CMD ["gunicorn", "app:app", \
74
- "--bind", "0.0.0.0:7860", \
75
- "--workers", "1", \
76
- "--threads", "8", \
77
- "--worker-class", "gthread", \
78
- "--timeout", "120", \
79
- "--keep-alive", "5", \
80
- "--log-level", "info", \
81
- "--access-logfile", "-", \
82
- "--error-logfile", "-"]
 
13
 
14
  WORKDIR /app
15
 
16
+ # numpy first (before torch)
17
  RUN pip install --no-cache-dir "numpy<2"
18
 
19
  # torch CPU only
 
24
  COPY requirements.txt .
25
  RUN pip install --no-cache-dir -r requirements.txt
26
 
27
+ # yt-dlp + openai-whisper
28
  RUN pip install --no-cache-dir -U "yt-dlp[default,curl-cffi]"
29
  RUN pip install --no-cache-dir openai-whisper
30
 
31
+ # App files
32
  COPY app.py .
33
+ COPY recap_tg_bot.py .
34
  COPY index.html .
35
  COPY privacy.html .
36
  COPY terms.html .
37
  COPY NotoSansMyanmar-Bold.ttf .
38
  COPY manifest.json .
39
  COPY sw.js .
40
+ COPY start.sh .
41
+ RUN chmod +x start.sh
42
 
43
+ # Optional files (build won't fail if missing)
44
  COPY payment.html* ./
45
  COPY payment_history.html* ./
46
+ COPY m_youtube_com_cookies.txt* ./
47
 
48
  # Directories
49
  RUN mkdir -p outputs slips temp_prev temp_thumb temp_
50
 
51
+ # Myanmar font
52
  RUN mkdir -p /usr/local/share/fonts/myanmar \
53
  && cp /app/NotoSansMyanmar-Bold.ttf /usr/local/share/fonts/myanmar/ \
54
  && fc-cache -fv
55
 
56
+ # Isolated fonts.conf
57
  RUN mkdir -p /app/fc_conf && \
58
  printf '<?xml version="1.0"?>\n\
59
  <!DOCTYPE fontconfig SYSTEM "fonts.dtd">\n\
 
65
  </fontconfig>\n' > /app/fc_conf/fonts.conf
66
 
67
  # Pre-cache Whisper tiny model
68
+ RUN python -c "import whisper; whisper.load_model('tiny', device='cpu'); print('Whisper cached')"
69
 
70
  # Sanity check
71
  RUN deno --version && yt-dlp --version && python -c "import numpy; print('numpy', numpy.__version__)"
72
 
73
  EXPOSE 7860
74
+ CMD ["./start.sh"]
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -1093,178 +1093,8 @@ def run_gemini_preview(voice_name, out_path):
1093
  # ═════════════════════════════════════════════════════════════
1094
  # TELEGRAM BOT — inline polling (runs as daemon thread inside app.py)
1095
  # ═════════════════════════════════════════════════════════════
1096
- import os, json, time, threading
1097
- import requests as _rq
1098
-
1099
- TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', '')
1100
- ADMIN_TELEGRAM_CHAT_ID = os.getenv('ADMIN_TELEGRAM_CHAT_ID', '')
1101
-
1102
- def _bot_tg(method, **kw):
1103
- try:
1104
- r = _rq.post(
1105
- 'https://api.telegram.org/bot' + TELEGRAM_BOT_TOKEN + '/' + method,
1106
- json=kw, timeout=30
1107
- )
1108
- return r.json()
1109
- except Exception as e:
1110
- print('[BOT] ' + method + ' error: ' + str(e))
1111
- return {'ok': False}
1112
-
1113
- def _bot_send(chat_id, text, reply_markup=None, parse_mode='HTML'):
1114
- kw = {'chat_id': chat_id, 'text': text, 'parse_mode': parse_mode,
1115
- 'disable_web_page_preview': True}
1116
- if reply_markup:
1117
- kw['reply_markup'] = reply_markup
1118
- r = _bot_tg('sendMessage', **kw)
1119
- return r.get('result', {}).get('message_id')
1120
-
1121
- def _bot_inline_kb():
1122
- wu = os.getenv('RECAP_WEBAPP_URL', 'https://recap.psonline.shop')
1123
- return {'inline_keyboard': [[
1124
- {'text': '\U0001f3ac Recap Studio \u1016\u103d\u1004\u103a\u1019\u100a\u103a', 'web_app': {'url': wu}}
1125
- ]]}
1126
-
1127
- def _bot_reply_kb():
1128
- wu = os.getenv('RECAP_WEBAPP_URL', 'https://recap.psonline.shop')
1129
- return {
1130
- 'keyboard': [[{'text': '\U0001f3ac Recap Studio', 'web_app': {'url': wu}}]],
1131
- 'resize_keyboard': True, 'persistent': True,
1132
- }
1133
-
1134
- def _bot_dispatch(update, load_db_fn):
1135
- msg = update.get('message')
1136
- if not msg:
1137
- return
1138
- text = (msg.get('text') or '').strip()
1139
- chat_id = msg['chat']['id']
1140
- fname = msg.get('from', {}).get('first_name', 'User')
1141
- tg_id = str(msg.get('from', {}).get('id', ''))
1142
- if not text:
1143
- return
1144
- cmd = text.split()[0].split('@')[0].lower() if text.startswith('/') else ''
1145
- print('[BOT] chat=' + str(chat_id) + ' cmd=' + repr(cmd))
1146
-
1147
- if cmd == '/start':
1148
- body = (
1149
- '\U0001f44b \u1019\u1004\u103a\u1039\u1002\u101c\u102c\u1015\u102b <b>' + fname + '</b>!\n\n'
1150
- '\U0001f3ac <b>Recap Studio</b> \u1019\u103e \u1000\u103c\u102d\u102f\u1006\u102d\u102f\u1015\u102b\u101e\u100a\u103a\u1015\u103e\u1004\u103a\u101e\u100a\u103a\u104b\n\n'
1151
- 'AI \u1016\u103c\u1004\u103a\u103d \u1019\u103c\u1014\u103a\u1019\u102c\u1018\u102c\u101e Movie Recap \u1017\u102e\u1012\u102e\u101a\u102c\u1038\u1019\u103b\u102c\u1038 '
1152
- '\u1021\u101c\u102d\u102f\u1021\u101c\u103b\u1031\u102c\u1000\u103a \u1011\u102f\u1010\u103a\u101c\u102f\u1015\u103a\u1015\u1031\u1038\u101e\u1031\u102c app \u1016\u103c\u1005\u103a\u101e\u100a\u103a\u104b\n\n'
1153
- '\u2b07\ufe0f \u1021\u1031\u102c\u1000\u103a\u1015\u102b\u1038 button \u1014\u103e\u102d\u1015\u103a\u1014\u103e\u102d\u1015\u103a \u1016\u103d\u1004\u103a\u1015\u102b\u104b'
1154
- )
1155
- _bot_send(chat_id, body, reply_markup=_bot_reply_kb())
1156
-
1157
- elif cmd == '/help':
1158
- body = (
1159
- '<b>\U0001f4d6 Recap Studio \u2014 \u101e\u102f\u1038\u1014\u100a\u103a\u1038</b>\n\n'
1160
- '1\ufe0f\u20e3 /start \u1014\u103e\u102d\u1015\u103a\u1015\u102b\n'
1161
- '2\ufe0f\u20e3 <b>\U0001f3ac Recap Studio</b> button \u1014\u103e\u102d\u1015\u103a\u1015\u102b\n'
1162
- '3\ufe0f\u20e3 Video URL \u1011\u100a\u103a\u103e\u1015\u102b\n'
1163
- '4\ufe0f\u20e3 Settings \u1001\u103b\u1031\u1014\u103e\u102d\u1015\u103a\u1015\u103c\u102e\u1038 <b>Auto Process</b> \u1014\u103e\u102d\u1015\u103a\u1015\u102b\n'
1164
- '5\ufe0f\u20e3 App \u1015\u102d\u1010\u103a\u1004\u103a progress + video \u1023 chat \u1011\u1032 \u101b\u1031\u102c\u1000\u103a\u1019\u100a\u103a\n\n'
1165
- '<b>\U0001f4b0 Coins:</b>\n'
1166
- '• Process \u1010\u1005\u103a\u1001\u102f = 1 Coin\n'
1167
- '• App \u1011\u1032 Buy Coins \u1014\u103e\u102d\u1015\u103a\u1010\u100a\u103a\u101d\u101a\u1015\u102b\n\n'
1168
- '<b>Commands:</b>\n'
1169
- '/start \u2014 App \u1016\u103d\u1004\u103a\u1019\u100a\u103a\n'
1170
- '/coins \u2014 Coin \u101c\u1000\u103a\u1000\u103b\u1014\u103a \u1005\u1005\u103a\u1019\u100a\u103a\n'
1171
- '/help \u2014 \u1023 message'
1172
- )
1173
- _bot_send(chat_id, body, reply_markup=_bot_inline_kb())
1174
-
1175
- elif cmd == '/coins':
1176
- ukey = 'user_' + tg_id
1177
- db = load_db_fn()
1178
- udata = db['users'].get(ukey, {})
1179
- if udata:
1180
- body = (
1181
- '\U0001fa99 <b>' + ukey + '</b>\n\n'
1182
- '\u101c\u1000\u103a\u1000\u103b\u1014\u103a Coins: <b>' + str(udata.get('coins', 0)) + '</b>\n\n'
1183
- 'Coins \u1000\u103a\u101d\u101a\u1000\u103a App \u1016\u103d\u1004\u103a\u1015\u102b \U0001f447'
1184
- )
1185
- else:
1186
- body = '\u274c Account \u1019\u1010\u103d\u1031\u1037\u1015\u102b\u104b\n/start \u1014\u103e\u102d\u1015\u103a\u1014\u103e\u102d\u1015\u103a app \u1000\u1014\u1031 login \u1000\u103d\u1004\u103a\u1015\u102b\u104b'
1187
- _bot_send(chat_id, body, reply_markup=_bot_inline_kb())
1188
-
1189
- elif cmd == '/broadcast' and str(chat_id) == str(ADMIN_TELEGRAM_CHAT_ID):
1190
- parts = text.split(None, 1)
1191
- if len(parts) < 2:
1192
- _bot_send(chat_id, '\u274c Usage: /broadcast <message>')
1193
- return
1194
- btext = parts[1]
1195
- db = load_db_fn()
1196
- tids = [str(v.get('tg_chat_id') or v.get('telegram_id',''))
1197
- for v in db['users'].values()
1198
- if v.get('tg_chat_id') or v.get('telegram_id')]
1199
- sent = 0
1200
- for tid in tids:
1201
- if tid and _bot_send(tid, '\U0001f4e2 <b>Recap Studio</b>\n\n' + btext):
1202
- sent += 1
1203
- time.sleep(0.05)
1204
- _bot_send(chat_id, '\u2705 Sent to ' + str(sent) + '/' + str(len(tids)) + ' users.')
1205
-
1206
- else:
1207
- _bot_send(chat_id, '\U0001f3ac Recap Studio \u1016\u103d\u1004\u103a\u101b\u1014\u103a:',
1208
- reply_markup=_bot_inline_kb())
1209
-
1210
- def _bot_polling_loop(load_db_fn):
1211
- if not TELEGRAM_BOT_TOKEN:
1212
- print('[BOT] No TELEGRAM_BOT_TOKEN — bot disabled')
1213
- return
1214
-
1215
- # Clear webhook — use requests (urllib SSL fails on HuggingFace)
1216
- for attempt in range(1, 4):
1217
- try:
1218
- r = _rq.post(
1219
- 'https://api.telegram.org/bot' + TELEGRAM_BOT_TOKEN + '/deleteWebhook',
1220
- json={'drop_pending_updates': True}, timeout=15
1221
- )
1222
- data = r.json()
1223
- if data.get('ok'):
1224
- print('[BOT] Webhook cleared (attempt ' + str(attempt) + ')')
1225
- break
1226
- print('[BOT] deleteWebhook not ok: ' + str(data))
1227
- except Exception as e:
1228
- print('[BOT] deleteWebhook attempt ' + str(attempt) + ' failed: ' + str(e))
1229
- time.sleep(3)
1230
-
1231
- time.sleep(2)
1232
- print('[BOT] Polling loop started')
1233
- offset = 0
1234
-
1235
- while True:
1236
- try:
1237
- r = _rq.get(
1238
- 'https://api.telegram.org/bot' + TELEGRAM_BOT_TOKEN + '/getUpdates',
1239
- params={'offset': offset, 'timeout': 25,
1240
- 'allowed_updates': ['message']},
1241
- timeout=30
1242
- )
1243
- data = r.json()
1244
-
1245
- if not data.get('ok'):
1246
- desc = data.get('description', '')
1247
- print('[BOT] getUpdates not ok: ' + desc)
1248
- time.sleep(15 if 'conflict' in desc.lower() else 5)
1249
- continue
1250
-
1251
- for upd in data.get('result', []):
1252
- offset = upd['update_id'] + 1
1253
- try:
1254
- _bot_dispatch(upd, load_db_fn)
1255
- except Exception as e:
1256
- print('[BOT] dispatch error: ' + str(e))
1257
-
1258
- except Exception as e:
1259
- err = str(e)
1260
- if 'timed out' not in err.lower() and 'timeout' not in err.lower():
1261
- print('[BOT] polling error: ' + err)
1262
- time.sleep(5)
1263
-
1264
-
1265
  # ── PULL DB ON START ──
1266
  threading.Thread(target=pull_db, daemon=True).start()
1267
- threading.Thread(target=_bot_polling_loop, args=(load_db,), daemon=True).start()
1268
  whisper_model = None
1269
 
1270
  # ════════════════════════════════════════
 
1093
  # ═════════════════════════════════════════════════════════════
1094
  # TELEGRAM BOT — inline polling (runs as daemon thread inside app.py)
1095
  # ═════════════════════════════════════════════════════════════
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1096
  # ── PULL DB ON START ──
1097
  threading.Thread(target=pull_db, daemon=True).start()
 
1098
  whisper_model = None
1099
 
1100
  # ════════════════════════════════════════
recap_tg_bot.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ recap_tg_bot.py — Recap Studio Telegram Bot
3
+ Uses requests library (urllib SSL fails on HuggingFace)
4
+
5
+ Env vars:
6
+ TELEGRAM_BOT_TOKEN — @BotFather
7
+ RECAP_WEBAPP_URL — https://recap.psonline.shop
8
+ ADMIN_TELEGRAM_CHAT_ID — numeric Telegram ID
9
+ ADMIN_USERNAME — backend admin username
10
+ """
11
+
12
+ import os, json, time, logging, requests
13
+
14
+ logging.basicConfig(level=logging.INFO,
15
+ format='%(asctime)s [%(levelname)s] %(message)s')
16
+ log = logging.getLogger(__name__)
17
+
18
+ BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', '')
19
+ WEBAPP_URL = os.getenv('RECAP_WEBAPP_URL', 'https://recap.psonline.shop')
20
+ ADMIN_ID = os.getenv('ADMIN_TELEGRAM_CHAT_ID', '')
21
+ ADMIN_U = os.getenv('ADMIN_USERNAME', '')
22
+ API_BASE = f'https://api.telegram.org/bot{BOT_TOKEN}'
23
+
24
+ if not BOT_TOKEN:
25
+ raise RuntimeError('TELEGRAM_BOT_TOKEN not set!')
26
+
27
+
28
+ # ── API helpers ───────────────────────────────────────────────────────────────
29
+ def _tg(method, **kw):
30
+ try:
31
+ r = requests.post(f'{API_BASE}/{method}', json=kw, timeout=30)
32
+ data = r.json()
33
+ if not data.get('ok'):
34
+ log.warning('[BOT] %s not ok: %s', method, data.get('description', ''))
35
+ return data
36
+ except Exception as e:
37
+ log.error('[BOT] %s error: %s', method, e)
38
+ return {'ok': False}
39
+
40
+ def send_msg(chat_id, text, markup=None, parse_mode='HTML'):
41
+ kw = {'chat_id': chat_id, 'text': text, 'parse_mode': parse_mode,
42
+ 'disable_web_page_preview': True}
43
+ if markup:
44
+ kw['reply_markup'] = markup
45
+ r = _tg('sendMessage', **kw)
46
+ return r.get('result', {}).get('message_id')
47
+
48
+ def edit_msg(chat_id, msg_id, text, parse_mode='HTML'):
49
+ _tg('editMessageText', chat_id=chat_id, message_id=msg_id,
50
+ text=text, parse_mode=parse_mode)
51
+
52
+
53
+ # ── Keyboards ─────────────────────────────────────────────────────────────────
54
+ def inline_kb():
55
+ return {'inline_keyboard': [[
56
+ {'text': '🎬 Recap Studio ဖွင့်မည်', 'web_app': {'url': WEBAPP_URL}}
57
+ ]]}
58
+
59
+ def reply_kb():
60
+ return {
61
+ 'keyboard': [[{'text': '🎬 Recap Studio', 'web_app': {'url': WEBAPP_URL}}]],
62
+ 'resize_keyboard': True, 'persistent': True,
63
+ }
64
+
65
+
66
+ # ── Handlers ──────────────────────────────────────────────────────────────────
67
+ def on_start(msg):
68
+ chat_id = msg['chat']['id']
69
+ fname = msg['from'].get('first_name', 'User')
70
+ send_msg(chat_id,
71
+ f'👋 မင်္ဂလာပါ <b>{fname}</b>!\n\n'
72
+ '🎬 <b>Recap Studio</b> မှ ကြိုဆိုပါသည်။\n\n'
73
+ 'AI ဖြင့် မြန်မာဘာသာ Movie Recap ဗီဒီယိုများ '
74
+ 'အလိုအလျောက် ထုတ်လုပ်ပေးသော app ဖြစ်သည်။\n\n'
75
+ '⬇️ Button နှိပ်၍ ဖွင့်ပါ။',
76
+ markup=reply_kb()
77
+ )
78
+ log.info('/start chat=%s user=%s', chat_id, msg['from'].get('username', fname))
79
+
80
+ def on_help(msg):
81
+ send_msg(msg['chat']['id'],
82
+ '<b>📖 Recap Studio — သုံးနည်း</b>\n\n'
83
+ '1️⃣ /start နှိပ်ပါ\n'
84
+ '2️⃣ <b>🎬 Recap Studio</b> button နှိပ်ပါ\n'
85
+ '3️⃣ Video URL ထည့်ပါ\n'
86
+ '4️⃣ Settings ချိန်ညှိပြီး <b>Auto Process</b> နှိပ်ပါ\n'
87
+ '5️⃣ App ပိတ်၍ progress + video ဤ chat ထဲ ရောက်မည်\n\n'
88
+ '<b>💰 Coins:</b>\n'
89
+ '• Process တစ်ခု = 1 Coin\n'
90
+ '• App ထဲ Buy Coins နှိပ်ဝယ်ပါ\n\n'
91
+ '<b>Commands:</b>\n'
92
+ '/start — App ဖွင့်မည်\n'
93
+ '/coins — Coin လက်ကျန် စစ်မည်\n'
94
+ '/help — ဤ message',
95
+ markup=inline_kb()
96
+ )
97
+
98
+ def on_coins(msg):
99
+ chat_id = msg['chat']['id']
100
+ tg_id = str(msg['from']['id'])
101
+ try:
102
+ r = requests.get(f'{WEBAPP_URL}/api/coins_by_tgid',
103
+ params={'tg_id': tg_id}, timeout=10)
104
+ d = r.json()
105
+ except Exception:
106
+ d = {'ok': False}
107
+
108
+ if d.get('ok'):
109
+ send_msg(chat_id,
110
+ f"🪙 <b>{d.get('username','')}</b>\n\n"
111
+ f"လက်ကျန် Coins: <b>{d.get('coins', 0)}</b>\n\n"
112
+ 'Coins ဝယ်ရန် App ဖွင့်ပါ 👇',
113
+ markup=inline_kb()
114
+ )
115
+ else:
116
+ send_msg(chat_id,
117
+ '❌ Account မတွေ့ပါ။\n/start နှိပ်၍ app ကနေ login ဝင်ပါ။',
118
+ markup=inline_kb()
119
+ )
120
+
121
+ def on_broadcast(msg):
122
+ chat_id = msg['chat']['id']
123
+ if str(chat_id) != str(ADMIN_ID):
124
+ send_msg(chat_id, '❌ Admin only.')
125
+ return
126
+ parts = (msg.get('text') or '').split(None, 1)
127
+ if len(parts) < 2:
128
+ send_msg(chat_id, '❌ Usage: /broadcast <message>')
129
+ return
130
+ try:
131
+ r = requests.get(f'{WEBAPP_URL}/api/admin/tg_users',
132
+ params={'caller': ADMIN_U}, timeout=15)
133
+ tg_ids = r.json().get('tg_ids', [])
134
+ except Exception:
135
+ tg_ids = []
136
+ sent = 0
137
+ for tid in tg_ids:
138
+ if send_msg(tid, f'📢 <b>Recap Studio</b>\n\n{parts[1]}'):
139
+ sent += 1
140
+ time.sleep(0.05)
141
+ send_msg(chat_id, f'✅ Sent to {sent}/{len(tg_ids)} users.')
142
+
143
+ def on_unknown(msg):
144
+ send_msg(msg['chat']['id'],
145
+ '🎬 Recap Studio ဖွင့်ရန်:',
146
+ markup=inline_kb())
147
+
148
+
149
+ # ── Dispatcher ────────────────────────────────────────────────────────────────
150
+ def dispatch(update):
151
+ msg = update.get('message')
152
+ if not msg:
153
+ return
154
+ text = (msg.get('text') or '').strip()
155
+ if not text:
156
+ return
157
+ cmd = text.split()[0].split('@')[0].lower() if text.startswith('/') else ''
158
+ log.info('MSG chat=%s cmd=%r', msg['chat']['id'], cmd or text[:20])
159
+
160
+ if cmd == '/start': on_start(msg)
161
+ elif cmd == '/help': on_help(msg)
162
+ elif cmd == '/coins': on_coins(msg)
163
+ elif cmd == '/broadcast': on_broadcast(msg)
164
+ else: on_unknown(msg)
165
+
166
+
167
+ # ── Startup: clear webhook ────────────────────────────────────────────────────
168
+ def clear_webhook():
169
+ for attempt in range(1, 4):
170
+ try:
171
+ r = requests.post(f'{API_BASE}/deleteWebhook',
172
+ json={'drop_pending_updates': True}, timeout=15)
173
+ if r.json().get('ok'):
174
+ log.info('Webhook cleared (attempt %d)', attempt)
175
+ return
176
+ log.warning('deleteWebhook not ok: %s', r.json())
177
+ except Exception as e:
178
+ log.warning('deleteWebhook attempt %d failed: %s', attempt, e)
179
+ time.sleep(3)
180
+ log.error('Could not clear webhook — polling may conflict')
181
+
182
+
183
+ # ── Polling loop ──────────────────────────────────────────────────────────────
184
+ def run_polling():
185
+ log.info('Recap Studio Bot — clearing webhook...')
186
+ clear_webhook()
187
+ time.sleep(2)
188
+ log.info('Recap Studio Bot — polling started')
189
+
190
+ offset = 0
191
+ while True:
192
+ try:
193
+ r = requests.get(
194
+ f'{API_BASE}/getUpdates',
195
+ params={'offset': offset, 'timeout': 25,
196
+ 'allowed_updates': ['message']},
197
+ timeout=30
198
+ )
199
+ data = r.json()
200
+
201
+ if not data.get('ok'):
202
+ desc = data.get('description', '')
203
+ log.warning('getUpdates not ok: %s', desc)
204
+ time.sleep(15 if 'conflict' in desc.lower() else 5)
205
+ continue
206
+
207
+ for upd in data.get('result', []):
208
+ offset = upd['update_id'] + 1
209
+ try:
210
+ dispatch(upd)
211
+ except Exception as e:
212
+ log.error('dispatch error: %s', e)
213
+
214
+ except requests.exceptions.ReadTimeout:
215
+ pass
216
+ except requests.exceptions.ConnectionError as e:
217
+ log.error('Connection error: %s — retry 10s', e)
218
+ time.sleep(10)
219
+ except Exception as e:
220
+ log.error('Polling error: %s', e)
221
+ time.sleep(5)
222
+
223
+
224
+ if __name__ == '__main__':
225
+ run_polling()
start.sh CHANGED
@@ -1,11 +1,53 @@
1
  #!/bin/bash
 
2
 
3
- echo "Starting Flask Web App on port 7860..."
4
- python app.py &
5
 
6
- sleep 3
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
- echo "Starting YouTube Auto Poster..."
9
- python yt_to_tiktok.py &
 
10
 
11
- wait
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  #!/bin/bash
2
+ # Recap Studio — start Gunicorn + Telegram Bot
3
 
4
+ PORT=${PORT:-7860}
 
5
 
6
+ # Auto-detect RECAP_WEBAPP_URL
7
+ if [ -z "$RECAP_WEBAPP_URL" ]; then
8
+ if [ -n "$RENDER_EXTERNAL_URL" ]; then
9
+ export RECAP_WEBAPP_URL="$RENDER_EXTERNAL_URL"
10
+ elif [ -n "$RAILWAY_PUBLIC_DOMAIN" ]; then
11
+ export RECAP_WEBAPP_URL="https://$RAILWAY_PUBLIC_DOMAIN"
12
+ elif [ -n "$SPACE_HOST" ]; then
13
+ export RECAP_WEBAPP_URL="https://$SPACE_HOST"
14
+ else
15
+ export RECAP_WEBAPP_URL="https://recap.psonline.shop"
16
+ fi
17
+ echo "RECAP_WEBAPP_URL=$RECAP_WEBAPP_URL"
18
+ fi
19
 
20
+ # Kill stale bot processes
21
+ pkill -f "python.*recap_tg_bot.py" 2>/dev/null || true
22
+ sleep 1
23
 
24
+ # Start Telegram bot (background)
25
+ if [ -n "$TELEGRAM_BOT_TOKEN" ]; then
26
+ echo "🤖 Starting Telegram bot..."
27
+ python recap_tg_bot.py &
28
+ BOT_PID=$!
29
+ echo " Bot PID: $BOT_PID"
30
+ else
31
+ echo "⚠️ TELEGRAM_BOT_TOKEN not set — bot skipped"
32
+ fi
33
+
34
+ # Trap for clean shutdown
35
+ cleanup() {
36
+ echo "🛑 Shutting down..."
37
+ [ -n "$BOT_PID" ] && kill $BOT_PID 2>/dev/null
38
+ exit 0
39
+ }
40
+ trap cleanup SIGTERM SIGINT
41
+
42
+ # Start Gunicorn (foreground — main process)
43
+ echo "🌐 Starting Gunicorn on port $PORT..."
44
+ exec gunicorn app:app \
45
+ --bind "0.0.0.0:$PORT" \
46
+ --workers 1 \
47
+ --threads 8 \
48
+ --worker-class gthread \
49
+ --timeout 120 \
50
+ --keep-alive 5 \
51
+ --log-level info \
52
+ --access-logfile - \
53
+ --error-logfile -