Spaces:
Running
Running
Upload 3 files
Browse files- app.py +42 -0
- bot.py +37 -0
- index.html +23 -0
app.py
CHANGED
|
@@ -2599,6 +2599,48 @@ def api_admin_payment_slip(payment_id):
|
|
| 2599 |
return jsonify(ok=False, msg=str(e))
|
| 2600 |
|
| 2601 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2602 |
@app.route('/api/payment/kbz_qr')
|
| 2603 |
def api_kbz_qr():
|
| 2604 |
"""
|
|
|
|
| 2599 |
return jsonify(ok=False, msg=str(e))
|
| 2600 |
|
| 2601 |
|
| 2602 |
+
@app.route('/api/admin/broadcast', methods=['POST'])
|
| 2603 |
+
def api_admin_broadcast():
|
| 2604 |
+
"""Send broadcast message to all users via Telegram bot."""
|
| 2605 |
+
try:
|
| 2606 |
+
data = request.get_json(force=True)
|
| 2607 |
+
caller = data.get('caller', '')
|
| 2608 |
+
if caller != ADMIN_U:
|
| 2609 |
+
return jsonify(ok=False, msg='❌ Admin only'), 403
|
| 2610 |
+
message = data.get('message', '').strip()
|
| 2611 |
+
if not message:
|
| 2612 |
+
return jsonify(ok=False, msg='❌ Message မထည့်ရသေးပါ')
|
| 2613 |
+
db = load_db()
|
| 2614 |
+
token = os.getenv('TELEGRAM_BOT_TOKEN', '')
|
| 2615 |
+
if not token:
|
| 2616 |
+
return jsonify(ok=False, msg='❌ BOT_TOKEN မသတ်မှတ်ရသေးပါ')
|
| 2617 |
+
import urllib.request as _ur, json as _json, threading as _th
|
| 2618 |
+
sent = 0; fail = 0
|
| 2619 |
+
for uname, udata in db.get('users', {}).items():
|
| 2620 |
+
tg_id = udata.get('tg_chat_id')
|
| 2621 |
+
if not tg_id:
|
| 2622 |
+
continue
|
| 2623 |
+
try:
|
| 2624 |
+
payload = _json.dumps({
|
| 2625 |
+
'chat_id': tg_id,
|
| 2626 |
+
'text': f'📢 *ကြေငြာချက်*\n\n{message}',
|
| 2627 |
+
'parse_mode': 'Markdown',
|
| 2628 |
+
}).encode()
|
| 2629 |
+
req = _ur.Request(
|
| 2630 |
+
f'https://api.telegram.org/bot{token}/sendMessage',
|
| 2631 |
+
data=payload,
|
| 2632 |
+
headers={'Content-Type': 'application/json'})
|
| 2633 |
+
_ur.urlopen(req, timeout=8)
|
| 2634 |
+
sent += 1
|
| 2635 |
+
except Exception as e:
|
| 2636 |
+
logger.warning(f'[broadcast] {uname}: {e}')
|
| 2637 |
+
fail += 1
|
| 2638 |
+
return jsonify(ok=True, sent=sent, fail=fail,
|
| 2639 |
+
msg=f'✅ {sent} ယောက် ပို့ပြီး — {fail} ကျ')
|
| 2640 |
+
except Exception as e:
|
| 2641 |
+
return jsonify(ok=False, msg=str(e))
|
| 2642 |
+
|
| 2643 |
+
|
| 2644 |
@app.route('/api/payment/kbz_qr')
|
| 2645 |
def api_kbz_qr():
|
| 2646 |
"""
|
bot.py
CHANGED
|
@@ -876,6 +876,41 @@ async def on_callback(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
| 876 |
# ── TEXT HANDLER ──
|
| 877 |
|
| 878 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 879 |
async def cmd_pending(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
| 880 |
"""Admin: list pending payments."""
|
| 881 |
cid = update.effective_chat.id
|
|
@@ -1119,8 +1154,10 @@ def main():
|
|
| 1119 |
)
|
| 1120 |
application.add_handler(conv)
|
| 1121 |
application.add_handler(CommandHandler('pending', cmd_pending))
|
|
|
|
| 1122 |
# Global handler for adm_pay callbacks — works even outside conversation state
|
| 1123 |
application.add_handler(CallbackQueryHandler(on_callback, pattern='^adm_pay[|]'))
|
|
|
|
| 1124 |
logger.info('🤖 Recap Studio Bot အသင့်ဖြစ်ပြီ!')
|
| 1125 |
application.run_polling(
|
| 1126 |
drop_pending_updates=True,
|
|
|
|
| 876 |
# ── TEXT HANDLER ──
|
| 877 |
|
| 878 |
|
| 879 |
+
async def error_handler(update: object, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
| 880 |
+
"""Handle all telegram errors gracefully without crashing."""
|
| 881 |
+
import traceback
|
| 882 |
+
err = ctx.error
|
| 883 |
+
tb = ''.join(traceback.format_exception(type(err), err, err.__traceback__))
|
| 884 |
+
logger.warning(f'[ErrorHandler] {err}\n{tb[:400]}')
|
| 885 |
+
|
| 886 |
+
async def cmd_broadcast(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
| 887 |
+
"""Admin: /broadcast <message> — send to all users with tg_chat_id."""
|
| 888 |
+
cid = update.effective_chat.id
|
| 889 |
+
s = sess(cid)
|
| 890 |
+
if not s.get('is_admin'):
|
| 891 |
+
await update.message.reply_text("❌ Admin only"); return
|
| 892 |
+
text = ' '.join(ctx.args) if ctx.args else ''
|
| 893 |
+
if not text:
|
| 894 |
+
await update.message.reply_text(
|
| 895 |
+
"📢 *Broadcast*\\n\\nUsage: `/broadcast <message>`",
|
| 896 |
+
parse_mode=ParseMode.MARKDOWN); return
|
| 897 |
+
db = load_db()
|
| 898 |
+
sent = 0; fail = 0
|
| 899 |
+
for uname, udata in db.get('users', {}).items():
|
| 900 |
+
tg_id = udata.get('tg_chat_id')
|
| 901 |
+
if not tg_id: continue
|
| 902 |
+
try:
|
| 903 |
+
await ctx.bot.send_message(
|
| 904 |
+
chat_id=tg_id,
|
| 905 |
+
text=f"📢 *ကြေငြာချက်*\\n\\n{text}",
|
| 906 |
+
parse_mode=ParseMode.MARKDOWN)
|
| 907 |
+
sent += 1
|
| 908 |
+
except Exception as e:
|
| 909 |
+
logger.warning(f'[broadcast] {uname}: {e}')
|
| 910 |
+
fail += 1
|
| 911 |
+
await update.message.reply_text(
|
| 912 |
+
f"✅ Broadcast ပြီးပါပြီ\\n📨 ပေးပို့: {sent} ယောက်\\n❌ မအောင်မြင်: {fail} ယောက်")
|
| 913 |
+
|
| 914 |
async def cmd_pending(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
| 915 |
"""Admin: list pending payments."""
|
| 916 |
cid = update.effective_chat.id
|
|
|
|
| 1154 |
)
|
| 1155 |
application.add_handler(conv)
|
| 1156 |
application.add_handler(CommandHandler('pending', cmd_pending))
|
| 1157 |
+
application.add_handler(CommandHandler('broadcast', cmd_broadcast))
|
| 1158 |
# Global handler for adm_pay callbacks — works even outside conversation state
|
| 1159 |
application.add_handler(CallbackQueryHandler(on_callback, pattern='^adm_pay[|]'))
|
| 1160 |
+
application.add_error_handler(error_handler)
|
| 1161 |
logger.info('🤖 Recap Studio Bot အသင့်ဖြစ်ပြီ!')
|
| 1162 |
application.run_polling(
|
| 1163 |
drop_pending_updates=True,
|
index.html
CHANGED
|
@@ -1003,6 +1003,17 @@ body{background:var(--bg);color:var(--text);font-family:var(--F);min-height:100v
|
|
| 1003 |
<div style="display:flex;gap:8px;margin-top:8px;flex-wrap:wrap">
|
| 1004 |
<button class="adm-act-btn" onclick="loadUsers()"><i class="fas fa-users"></i> Users</button>
|
| 1005 |
<button class="adm-act-btn" style="background:rgba(239,68,68,.1);border-color:rgba(239,68,68,.3);color:#ef4444" onclick="loadPendingPayments()"><i class="fas fa-clock"></i> Pending Payments <span id="adm-pending-count" style="background:#ef4444;color:#fff;border-radius:20px;padding:1px 7px;font-size:.62rem;margin-left:4px;display:none">0</span></button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1006 |
</div>
|
| 1007 |
<div id="utw" style="overflow-x:auto;margin-top:8px"></div>
|
| 1008 |
<!-- Pending Payments -->
|
|
@@ -1821,6 +1832,18 @@ async function qDel(u){if(!confirm('Delete '+u+'?'))return;const r=await fetch('
|
|
| 1821 |
async function qBan(u,ban){const label=ban?'Ban':'Unban';if(!confirm(label+' '+u+'?'))return;const r=await fetch('/api/admin/ban_user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({caller:U,username:u,ban})});const d=await r.json();toast(d.msg);if(d.ok)loadUsers();}
|
| 1822 |
|
| 1823 |
/* ══ PENDING PAYMENTS ══ */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1824 |
async function loadPendingPayments(){
|
| 1825 |
const wrap=document.getElementById('adm-pending-wrap');
|
| 1826 |
const list=document.getElementById('adm-pending-list');
|
|
|
|
| 1003 |
<div style="display:flex;gap:8px;margin-top:8px;flex-wrap:wrap">
|
| 1004 |
<button class="adm-act-btn" onclick="loadUsers()"><i class="fas fa-users"></i> Users</button>
|
| 1005 |
<button class="adm-act-btn" style="background:rgba(239,68,68,.1);border-color:rgba(239,68,68,.3);color:#ef4444" onclick="loadPendingPayments()"><i class="fas fa-clock"></i> Pending Payments <span id="adm-pending-count" style="background:#ef4444;color:#fff;border-radius:20px;padding:1px 7px;font-size:.62rem;margin-left:4px;display:none">0</span></button>
|
| 1006 |
+
<button class="adm-act-btn" style="background:rgba(99,102,241,.1);border-color:rgba(99,102,241,.3);color:#6366f1" onclick="document.getElementById('adm-bc-wrap').style.display=document.getElementById('adm-bc-wrap').style.display==='none'?'block':'none'"><i class="fas fa-bullhorn"></i> Broadcast</button>
|
| 1007 |
+
</div>
|
| 1008 |
+
<!-- Broadcast Section -->
|
| 1009 |
+
<div id="adm-bc-wrap" style="display:none;margin-top:10px;background:rgba(99,102,241,.06);border:1px solid rgba(99,102,241,.2);border-radius:12px;padding:12px">
|
| 1010 |
+
<div style="font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#6366f1;margin-bottom:8px"><i class="fas fa-bullhorn"></i> Broadcast Message</div>
|
| 1011 |
+
<textarea id="bc-msg" placeholder="ကြေငြာချက် ရေးပါ…" style="width:100%;box-sizing:border-box;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);border-radius:8px;color:#fff;padding:10px;font-size:.82rem;font-family:var(--F);resize:vertical;min-height:80px;outline:none" rows="3"></textarea>
|
| 1012 |
+
<div style="display:flex;gap:8px;margin-top:8px;align-items:center">
|
| 1013 |
+
<button class="abtn ab-g" onclick="sendBroadcast()" style="flex:1"><i class="fas fa-paper-plane"></i> ပို့မည်</button>
|
| 1014 |
+
<button class="abtn" onclick="document.getElementById('bc-msg').value=''" style="background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.1);color:rgba(255,255,255,.6)"><i class="fas fa-times"></i></button>
|
| 1015 |
+
</div>
|
| 1016 |
+
<div id="bc-res" style="font-size:.72rem;margin-top:6px;color:var(--green);font-weight:600"></div>
|
| 1017 |
</div>
|
| 1018 |
<div id="utw" style="overflow-x:auto;margin-top:8px"></div>
|
| 1019 |
<!-- Pending Payments -->
|
|
|
|
| 1832 |
async function qBan(u,ban){const label=ban?'Ban':'Unban';if(!confirm(label+' '+u+'?'))return;const r=await fetch('/api/admin/ban_user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({caller:U,username:u,ban})});const d=await r.json();toast(d.msg);if(d.ok)loadUsers();}
|
| 1833 |
|
| 1834 |
/* ══ PENDING PAYMENTS ══ */
|
| 1835 |
+
async function sendBroadcast(){
|
| 1836 |
+
const msg=document.getElementById('bc-msg').value.trim();
|
| 1837 |
+
const res=document.getElementById('bc-res');
|
| 1838 |
+
if(!msg){res.style.color='#ef4444';res.textContent='❌ Message ထည့်ပါ';return;}
|
| 1839 |
+
res.style.color='rgba(255,255,255,.5)';res.textContent='📨 ပို့နေသည်…';
|
| 1840 |
+
try{
|
| 1841 |
+
const r=await fetch('/api/admin/broadcast',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({caller:U,message:msg})});
|
| 1842 |
+
const d=await r.json();
|
| 1843 |
+
if(d.ok){res.style.color='var(--green)';res.textContent=d.msg;document.getElementById('bc-msg').value='';}
|
| 1844 |
+
else{res.style.color='#ef4444';res.textContent=d.msg||'❌ Error';}
|
| 1845 |
+
}catch(e){res.style.color='#ef4444';res.textContent='❌ Network error';}
|
| 1846 |
+
}
|
| 1847 |
async function loadPendingPayments(){
|
| 1848 |
const wrap=document.getElementById('adm-pending-wrap');
|
| 1849 |
const list=document.getElementById('adm-pending-list');
|