bep40 commited on
Commit
11a233f
·
verified ·
1 Parent(s): 888eb4e

Fix SyntaxError: bracket escaping in f-strings for ZGR logs page

Browse files
Files changed (1) hide show
  1. app.py +201 -233
app.py CHANGED
@@ -1,22 +1,6 @@
1
  import os, sys, json, secrets, logging, asyncio, re, time, threading, base64, tempfile, pathlib
2
  from html import escape
3
 
4
- try:
5
- import audioop
6
- except ImportError:
7
- try:
8
- import audioop_lts as _aol
9
- sys.modules["audioop"] = _aol
10
- except ImportError:
11
- import types
12
- _stub = types.ModuleType("audioop")
13
- for _n in ["lin2lin","ratecv","adpcm2lin","getsample",
14
- "mul","bias","add","mult","findmin","max","min","rms",
15
- "avg","avg_taps","tomono","tostereo"]:
16
- if not hasattr(_stub, _n):
17
- setattr(_stub, _n, lambda *a, **k: b"" if "lin" in _n or "sample" in _n else 0)
18
- sys.modules["audioop"] = _stub
19
-
20
  logging.basicConfig(level=logging.INFO, stream=sys.stdout)
21
  logger = logging.getLogger("zalo-bot")
22
 
@@ -165,6 +149,11 @@ def _get_user_dataset_id(user_id: str) -> str:
165
  return f"{NAMESPACE}/{safe_id}-zalo-data"
166
 
167
 
 
 
 
 
 
168
  def _ensure_user_dataset(user_id: str, token: str) -> tuple:
169
  if not HF_TOKEN:
170
  raise RuntimeError("HF_TOKEN chưa được cấu hình.")
@@ -293,7 +282,7 @@ def _save_product_to_main_dataset(image_url, image_data_b64, description, price,
293
  def _create_api_proxy_space(token, user_id, sender_display):
294
  safe_id = _safe_space_name(user_id)
295
  token_suffix = token.split(":")[-1][:12] if ":" in token else re.sub(r'\W', '', token[:12])
296
- unique_key = safe_id if safe_id else f"u{token_suffix}"
297
  space_name = f"zalo-proxy-{unique_key}"
298
  repo_id = f"{NAMESPACE}/{space_name}"
299
  dataset_id = f"{NAMESPACE}/{unique_key}-zalo-data"
@@ -316,12 +305,11 @@ EXPOSE 7860
316
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
317
  """
318
  requirements = "fastapi>=0.111.0\nuvicorn[standard]>=0.30.0\nrequests>=2.32.0\nhuggingface_hub>=0.30.0\n"
319
- app_py = f'''import os, json, requests, time, re, base64, tempfile, pathlib, sys
320
  from html import escape as _escape
321
 
322
- # Redirect print to stderr so uvicorn logs go to stdout
323
  class _StderrLogger:
324
- def __init__(self):
325
  self._log = []
326
  def write(self, s):
327
  if s.strip():
@@ -334,20 +322,20 @@ sys.stderr = _StderrLogger()
334
  from fastapi import FastAPI, Request, Response
335
 
336
  app = FastAPI(title="Zalo Proxy Space")
337
- BOT_TOKEN = "{token}"
338
  TARGET_API = "https://bot-api.zaloplatforms.com"
339
- PROXY_NAME = "{sender_display}"
340
  HF_TOKEN = os.getenv("HF_TOKEN", "")
341
- DATASET_ID = "{dataset_id}"
342
- MAIN_DATASET_ID = "{MAIN_DATASET_ID}"
343
- MAIN_SPACE_URL = "{SPACE_ID.replace("/", "-")}.hf.space"
344
- ZGR_SENDER_ID = "{ZGR_SENDER_ID}"
345
  _logs = []
346
 
347
  def _send(cid, text):
348
- headers = {{"Content-Type": "application/json"}}
349
- url = f"{{TARGET_API}}/bot{{BOT_TOKEN}}/sendMessage"
350
- return requests.post(url, json={{"chat_id": cid, "text": text}}, headers=headers)
351
 
352
  def _safe_name(name):
353
  return re.sub(r'[^a-zA-Z0-9]', '_', str(name))[:30]
@@ -356,9 +344,17 @@ def _is_zgr_sender(sender_id):
356
  sid = str(sender_id)
357
  return ZGR_SENDER_ID in sid or sid == ZGR_SENDER_ID
358
 
359
- @_log("startup", "system", "SYSTEM", f"Proxy space initialized. PROXY_NAME={{PROXY_NAME}}")
 
 
 
 
 
 
 
 
360
  def _save_to_main_dataset(image_url, image_data_b64, description, price, category, sender_id, sender_name, product_name="", chat_id=""):
361
- _log("main_dataset_save_start", sender_id, chat_id, f"product_name={{product_name}} price={{price}}")
362
  if not HF_TOKEN or not MAIN_DATASET_ID:
363
  _log("main_dataset_skip", sender_id, chat_id, "HF_TOKEN or MAIN_DATASET_ID missing")
364
  return None
@@ -367,8 +363,8 @@ def _save_to_main_dataset(image_url, image_data_b64, description, price, categor
367
  api = HfApi(token=HF_TOKEN)
368
  ts = time.strftime("%Y%m%d_%H%M%S")
369
  safe_sender = _safe_name(sender_id) or "unknown"
370
- img_filename = f"images/{{ts}}_{{safe_sender}}.jpg"
371
- meta_filename = f"data/{{ts}}_{{safe_sender}}.json"
372
  img_bytes = None
373
  if image_data_b64:
374
  try:
@@ -388,19 +384,19 @@ def _save_to_main_dataset(image_url, image_data_b64, description, price, categor
388
  tmp.write(img_bytes)
389
  tmp_path = tmp.name
390
  try:
391
- api.upload_file(path_or_fileobj=tmp_path, path_in_repo=img_filename, repo_id=MAIN_DATASET_ID, repo_type="dataset", token=HF_TOKEN, commit_message=f"Add product image from {{sender_name}}")
392
  uploaded_img = img_filename
393
  _log("image_uploaded", sender_id, chat_id, img_filename)
394
  except Exception as e:
395
  _log("image_upload_fail", sender_id, chat_id, str(e))
396
  finally:
397
  pathlib.Path(tmp_path).unlink(missing_ok=True)
398
- record = {{"image": uploaded_img, "product_name": str(product_name)[:200] if product_name else "", "description": str(description)[:500] if description else "", "price": str(price) if price else "", "category": str(category) if category else "", "sender_id": str(sender_id), "sender_name": str(sender_name), "chat_id": str(chat_id), "is_zgr_group": _is_zgr_sender(sender_id), "timestamp": time.strftime("%Y-%m-%d %H:%M:%S")}}
399
  with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
400
  json.dump(record, tmp, indent=2, ensure_ascii=False)
401
  tmp_path = tmp.name
402
  try:
403
- api.upload_file(path_or_fileobj=tmp_path, path_in_repo=meta_filename, repo_id=MAIN_DATASET_ID, repo_type="dataset", token=HF_TOKEN, commit_message=f"Add product metadata from {{sender_name}}")
404
  _log("dataset_save_to_main", sender_id, chat_id, "OK")
405
  except Exception as e:
406
  _log("dataset_save_fail", sender_id, chat_id, str(e))
@@ -420,8 +416,8 @@ def _save_to_dataset(image_url, image_data_b64, description, price, category, se
420
  api = HfApi(token=HF_TOKEN)
421
  ts = time.strftime("%Y%m%d_%H%M%S")
422
  safe_sender = _safe_name(sender_id) or "unknown"
423
- img_filename = f"images/{{ts}}_{{safe_sender}}.jpg"
424
- meta_filename = f"data/{{ts}}_{{safe_sender}}.json"
425
  img_bytes = None
426
  if image_data_b64:
427
  try:
@@ -440,21 +436,21 @@ def _save_to_dataset(image_url, image_data_b64, description, price, category, se
440
  tmp.write(img_bytes)
441
  tmp_path = tmp.name
442
  try:
443
- api.upload_file(path_or_fileobj=tmp_path, path_in_repo=img_filename, repo_id=DATASET_ID, repo_type="dataset", token=HF_TOKEN, commit_message=f"Add product image from {{sender_name}}")
444
  uploaded_img = img_filename
445
  except Exception as e:
446
  _log("image_upload_fail", sender_id, "N/A", str(e))
447
  finally:
448
  pathlib.Path(tmp_path).unlink(missing_ok=True)
449
- record = {{"image": uploaded_img, "product_name": "", "description": str(description)[:500] if description else "", "price": str(price) if price else "", "category": str(category) if category else "", "sender_id": str(sender_id), "sender_name": str(sender_name), "timestamp": time.strftime("%Y-%m-%d %H:%M:%S")}}
450
  with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
451
  json.dump(record, tmp, indent=2, ensure_ascii=False)
452
  tmp_path = tmp.name
453
  try:
454
- api.upload_file(path_or_fileobj=tmp_path, path_in_repo=meta_filename, repo_id=DATASET_ID, repo_type="dataset", token=HF_TOKEN, commit_message=f"Add product metadata from {{sender_name}}")
455
  finally:
456
  pathlib.Path(tmp_path).unlink(missing_ok=True)
457
- _log("dataset_saved", sender_id, "N/A", f"Saved to {{DATASET_ID}}")
458
  return DATASET_ID
459
  except Exception as e:
460
  _log("dataset_error", sender_id, "N/A", str(e))
@@ -462,15 +458,15 @@ def _save_to_dataset(image_url, image_data_b64, description, price, category, se
462
 
463
  @app.get("/")
464
  async def root():
465
- return {{"status": "ok"}}
466
 
467
  @app.get("/health")
468
  async def health():
469
- return {{"status": "ok", "dataset": DATASET_ID, "zgr_sender": ZGR_SENDER_ID, "is_zgr": _is_zgr_sender(ZGR_SENDER_ID)}}
470
 
471
  @app.get("/webhooks")
472
  async def webhooks_get():
473
- return Response(content=json.dumps({{"message": "Success"}}), media_type="application/json", status_code=200)
474
 
475
  @app.post("/webhooks")
476
  async def webhooks(request: Request):
@@ -481,32 +477,32 @@ async def webhooks(request: Request):
481
  data = json.loads(body_str)
482
  except Exception as e:
483
  _log("parse_error", "N/A", "N/A", "Bad JSON: " + str(e) + " | body=" + body_str[:200])
484
- return Response(content=json.dumps({{"message": "Bad JSON"}}), media_type="application/json", status_code=400)
485
  result = data.get("result", data)
486
  event = result.get("event_name", "unknown")
487
- msg = result.get("message", {{}})
488
- sender = msg.get("from", {{}})
489
- chat = msg.get("chat", {{}})
490
  text = msg.get("text", "")
491
  sender_id = str(sender.get("id", ""))
492
  sender_name = sender.get("display_name") or sender.get("name") or sender_id
493
  chat_id = str(chat.get("id", ""))
494
  chat_type = str(chat.get("chat_type", ""))
495
- attachments = msg.get("attachment", {{}})
496
  image_url = ""
497
  image_data_b64 = ""
498
  if attachments:
499
- payload = attachments.get("payload", {{}})
500
  if isinstance(payload, str):
501
  try:
502
  payload = json.loads(payload)
503
  except Exception:
504
- payload = {{}}
505
  image_url = payload.get("url", "")
506
  image_data_b64 = payload.get("data", "") or msg.get("image", "")
507
  is_zgr = _is_zgr_sender(sender_id)
508
  _log(event, sender_id, chat_id, text, sender_name, chat_type)
509
- _log("debug_info", sender_id, chat_id, f"chat_type={{chat_type}} is_zgr={{is_zgr}} sender_id={{sender_id}} sender_name={{sender_name}} text_len={{len(text)}} has_attachment={{bool(attachments)}} image_url={{bool(image_url)}}")
510
 
511
  if event == "message.text.received" and chat_id:
512
  description, price, category, product_name = "", "", "", ""
@@ -526,48 +522,39 @@ async def webhooks(request: Request):
526
  dataset_id = _save_to_dataset(image_url=image_url, image_data_b64=image_data_b64, description=description or text[:200], price=price, category=category, sender_id=sender_id, sender_name=sender_name)
527
  _save_to_main_dataset(image_url=image_url, image_data_b64=image_data_b64, description=description or text[:200], price=price, category=category, sender_id=sender_id, sender_name=sender_name, product_name=product_name, chat_id=chat_id)
528
  if dataset_id:
529
- reply = f"🎉 **Bot đã lưu sản phẩm của bạn!**\\n\\n✅ Đã lưu vào: https://huggingface.co/datasets/{{DATASET_ID}}\\n✅ Đồng thời lưu về dataset trung tâm: {{MAIN_DATASET_ID}}\\n\\n👉 Bạn có thể quản lý bot của mình tại https://zalo.me/s/botcreator"
530
  else:
531
- reply = f"🎉 **Bot đã lưu sản phẩm của bạn!**\\n✅ Đã lưu về dataset trung tâm: {{MAIN_DATASET_ID}}"
532
  else:
533
- reply = f"👋 Xin chào! Gửi ảnh kèm thông tin sản phẩm (tên, giá, mô tả) để mình tự động lưu nhé."
534
  try:
535
  _send(chat_id, reply)
536
  except Exception as e:
537
  _log("send_reply_fail", sender_id, chat_id, str(e))
538
- return Response(content=json.dumps({{"message": "Success"}}), media_type="application/json", status_code=200)
539
-
540
- def _log(event, sender_id, chat_id, text, sender_name="", chat_type=""):
541
- entry = {{"event": str(event), "sender_id": str(sender_id), "sender_name": str(sender_name), "chat_id": str(chat_id), "chat_type": str(chat_type), "text": str(text)[:500], "is_zgr": _is_zgr_sender(sender_id), "time": time.strftime("%Y-%m-%d %H:%M:%S")}}
542
- _logs.append(entry)
543
- print(f"[WEBHOOK] event={{event}} sender={{sender_id}} chat={{chat_id}} type={{chat_type}} is_zgr={{entry['is_zgr']}} text={{str(text)[:100]}}", flush=True)
544
- if len(_logs) > 200:
545
- del _logs[:100]
546
 
547
  @app.get("/logs")
548
  async def proxy_logs():
549
  rows = ""
550
  for log in reversed(_logs[-100:]):
551
  is_zgr = _is_zgr_sender(log.get("sender_id", ""))
552
- bg = "#e8f5e9" if is_zgr else ("#ffffff" if len(_logs) % 2 == 0 else "#fafafa")
553
- zgr_badge = "<span style='color:#4CAF50;font-weight:bold'>[ZGR]</span> " if is_zgr else ""
554
- rows += f"<div style='margin:6px 0;padding:8px;background:{bg};border-radius:4px;border-left:3px solid {'#4CAF50' if is_zgr else '#1a73e8'}'><b>{zgr_badge}[{{log['event']}}]</b> 👤{{_escape(str(log['sender_name']))}} 🆔<code>{{_escape(str(log['sender_id']))}}</code> 💬<code>{{_escape(str(log['chat_id']))}}</code> [{_escape(str(log['chat_type']))}]<br><span style='font-family:monospace;font-size:12px;color:#333'>{{_escape(str(log['text'][:300]))}}</span><br><small style='color:#999'>⏰ {{log['time']}} {' + ' if log.get('is_zgr') else ''}</small></div>"
555
- return Response(content=f"<!DOCTYPE html><html><head><title>Proxy Logs</title><meta http-equiv='refresh' content='5'><style>body{{font-family:Arial,sans-serif;max-width:1000px;margin:0 auto;padding:16px;}} h1{{color:#1a73e8;}} .log-c{{max-height:600px;overflow-y:auto;background:#fff;border-radius:8px;padding:8px;}}</style></head><body><h1>📊 Proxy Logs {_escape(PROXY_NAME)}</h1><p>Webhook proxy cho: <b>{_escape(PROXY_NAME)}</b> | ZGR Sender: <code>{_escape(ZGR_SENDER_ID)}</code></p><div class='log-c'>{rows if rows else '<p style=\"color:#999\">Chưa sự kiện</p>'} </div></body></html>", media_type="text/html")
 
 
556
  '''
557
 
558
- readme = f"""---
559
- title: Zalo Proxy ({sender_display})
560
  colorFrom: blue
561
  colorTo: purple
562
  sdk: docker
563
  app_port: 7860
564
  ---
565
 
566
- 🚀 **Zalo Webhook Proxy** (dành riêng cho {sender_display})
567
-
568
- Webhook URL: `https://{repo_id.replace("/", "-")}.hf.space/webhooks`
569
-
570
- Tự động trả lời tin nhắn từ bot của bạn. Quản lý bot tại [https://zalo.me/s/botcreator](https://zalo.me/s/botcreator)
571
  """
572
 
573
  with tempfile.TemporaryDirectory() as tmp:
@@ -585,7 +572,7 @@ Tự động trả lời tin nhắn từ bot của bạn. Quản lý bot tại [
585
  except Exception as e:
586
  err_msg = str(e).lower()
587
  if "429" in err_msg or "rate limit" in err_msg:
588
- raise RuntimeError("⚠️ Đã đạt giới hạn tạo Space. Vui lòng thử lại sau 24h.")
589
  if "already exist" in err_msg or "conflict" in err_msg:
590
  logger.info("Proxy space %s already exists, skipping create_repo", repo_id)
591
  else:
@@ -595,7 +582,7 @@ Tự động trả lời tin nhắn từ bot của bạn. Quản lý bot tại [
595
  except Exception as e:
596
  err_msg = str(e)
597
  if "404" in err_msg or "Repository Not Found" in err_msg:
598
- raise RuntimeError(f"Proxy repo bị lỗi. Tạo thủ công: https://huggingface.co/new (tên: {repo_id}, SDK: Docker)")
599
  raise
600
 
601
  try:
@@ -609,7 +596,7 @@ Tự động trả lời tin nhắn từ bot của bạn. Quản lý bot tại [
609
  except Exception:
610
  status = "UNKNOWN"
611
 
612
- proxy_url = f"https://{repo_id.replace('/', '-')}.hf.space/webhooks"
613
  logger.info("Proxy space ready: %s -> %s (status=%s)", repo_id, proxy_url, status)
614
  return repo_id, proxy_url, status
615
 
@@ -634,15 +621,14 @@ def connect_bot(token: str):
634
  try:
635
  zapi = ZaloBotAPI(token)
636
  except AssertionError as e:
637
- return f"Token sai: {e}"
638
  try:
639
  me = zapi.get_me()
640
  if not me.get("ok"):
641
- return f"That bai: {me.get('message', '')}"
642
  BOT_STATE["bot_info"] = me.get("result", {})
643
- bot_name = BOT_STATE["bot_info"].get("name", "?")
644
  except Exception as e:
645
- return f"Loi: {e}"
646
  wh = get_webhook_url()
647
  sc = BOT_STATE["webhook_secret"]
648
  try:
@@ -650,76 +636,65 @@ def connect_bot(token: str):
650
  if sw.get("ok"):
651
  BOT_STATE["webhook_url"] = wh
652
  BOT_STATE["connected"] = True
653
- return f" Kết nối thành công!\nWebhook: {wh}\nSecret: `{sc}`"
654
- return f"setWebhook thất bại: {sw.get('message', '')}"
655
  except Exception as e:
656
- return f"Lỗi: {e}"
657
 
658
 
659
  def send_msg(cid: str, text: str):
660
  if not BOT_STATE["connected"]:
661
- return "⚠️ Chưa kết nối bot."
662
  if not cid or not text:
663
- return "Vui lòng nhập Chat ID Nội dung"
664
  try:
665
  result = ZaloBotAPI(BOT_STATE["bot_token"]).send_message(cid, text)
666
  return json.dumps(result, indent=2, ensure_ascii=False)
667
  except Exception as e:
668
- return f"Lỗi: {e}"
669
 
670
 
671
  def get_botinfo():
672
  if BOT_STATE["bot_info"]:
673
  info = BOT_STATE["bot_info"]
674
- lines = [f"Tên bot: {info.get('name', '?')}", f"ID: {info.get('id', '')}"]
675
  if BOT_STATE.get("webhook_url"):
676
- lines.append(f"Webhook: {BOT_STATE['webhook_url']}")
677
- lines.append(f"Kết nối: {BOT_STATE.get('connected', False)}")
678
- lines.append(f"Chat ID lưu: `{BOT_STATE.get('last_chat_id', '')}`")
679
  return "\n".join(lines)
680
- return "Chưa kết nối"
681
 
682
 
683
  def get_events():
684
  if not BOT_STATE["logs"]:
685
- return "Chưa sự kiện"
686
  lines = []
687
  for i, l in enumerate(BOT_STATE["logs"][-20:][::-1], 1):
688
- is_zgr = _is_zgr_sender(l.get("sender_id", ""))
689
  zgr_tag = " [ZGR]" if is_zgr else ""
690
- lines.append(f"{i}. [{l.get('event','')}] {zgr_tag} Zalo:{l.get('sender_name','')} ID:{l.get('sender_id','')} chat:{l.get('chat_id','')} [{l.get('chat_type','')}] | {l.get('text', '')[:50]}")
 
 
691
  return "\n".join(lines)
692
 
693
 
694
  def get_proxy_spaces():
695
  spaces = BOT_STATE.get("api_spaces", [])
696
  if not spaces:
697
- return "Chưa proxy space nào được tạo"
698
  lines = []
699
  for i, s in enumerate(spaces[-10:][::-1], 1):
700
- lines.append(f"{i}. 👤 {escape(s.get('sender_name',''))} | ID: {s.get('user_id','')} | Space: {s.get('repo_id','')} | Webhook: {s.get('proxy_url','')} | Dataset: {s.get('dataset_id','')} | Token: {s.get('user_token','')[:10]}... | Status: {s.get('status','')}")
 
 
701
  return "\n".join(lines)
702
 
703
 
704
- def _is_zgr_sender_local(sender_id):
705
- sid = str(sender_id)
706
- return ZGR_SENDER_ID in sid or sid == ZGR_SENDER_ID
707
-
708
-
709
  HELP_INSTRUCTIONS = (
710
- "🎓 **HƯỚNG DẪN CẤU HÌNH ZALO BOT CHI TIẾT**\n\n"
711
- "1️⃣ Cách đặt tên Zalobot (QUAN TRỌNG):\n"
712
- " Tên bot không được chứa 'Zalo' hoặc 'bot'\n"
713
- " dụ đúng: Shop, ChămSóc, HỗTrợ247, CSKH-TựĐộng ✅\n"
714
- " • Ví dụ sai: Zalo Support, ShopBot, ZaloBot ❌\n\n"
715
- "2️⃣ Cách lấy HTTP API:\n"
716
- " • Truy cập https://zalo.me/s/botcreator\n"
717
- " • Chọn bot → Cài đặt → API/HTTP API\n"
718
- " • Copy Bot token: `4179413508988279245:XXXXXXXXXXXXXXXXXXXXXX`\n\n"
719
- "3️⃣ Cách dùng:\n"
720
- " • Gửi ảnh + mô tả sản phẩm trực tiếp cho bot hoặc qua webhook\n"
721
- " • Bot sẽ tự động lưu vào dataset\n\n"
722
- "✅ Hệ thống đang theo dõi nhóm ZGR: " + ZGR_SENDER_ID
723
  )
724
 
725
 
@@ -763,7 +738,7 @@ async def handle_webhook(request: Request):
763
  if user_token:
764
  zapi = ZaloBotAPI(BOT_STATE["bot_token"])
765
  try:
766
- zapi.send_message(cid, "🤖 Bot đang xử lý HTTP API của bạn...\nVui lòng chờ 1-2 phút")
767
  except Exception:
768
  pass
769
 
@@ -783,22 +758,10 @@ async def handle_webhook(request: Request):
783
  _save_proxy_spaces()
784
 
785
  sw = _set_user_webhook(user_token, proxy_url, BOT_STATE["webhook_secret"])
786
- webhook_ok = sw.get("ok", False)
787
-
788
  user_bot_id = user_token.split(":")[0] if ":" in user_token else ""
789
- user_bot_link = f"https://zalo.me/s/{user_bot_id}" if user_bot_id else "https://zalo.me/s/botcreator"
790
- dataset_url = f"https://huggingface.co/datasets/{NAMESPACE}/{_safe_space_name(sender_id or 'user')}-zalo-data" if sender_id else ""
791
- instructions = (
792
- f"🎉 **Bot Zalo của bạn đã được AUTOMATION SALE thiết lập thành công!**\n\n"
793
- f"✅ Mọi cấu hình đã tự động hoàn tất.\n\n"
794
- f"👉 Bạn có thể vào bot của mình tại {user_bot_link} để tiếp tục cài đặt dữ liệu cho bot của mình.\n\n"
795
- f"🔗 **Webhook URL:** `{proxy_url}`\n"
796
- f"📊 **Logs:** {proxy_url.replace('/webhooks','/logs')}\n"
797
- f"🗂️ **Quản lý proxy:** https://bep40-zalo-bot-webhook.hf.space/proxy-spaces\n"
798
- )
799
- if dataset_url:
800
- instructions += f"💾 **Dataset sản phẩm:** {dataset_url}\n→ Gửi ảnh kèm mô tả/giá/chuyên mục để tự động lưu!\n"
801
- instructions += f"\n⚙️ Bot của bạn sẽ tự động trả lời khi có người nhắn tin."
802
  try:
803
  zapi.send_message(cid, instructions)
804
  except Exception:
@@ -807,19 +770,19 @@ async def handle_webhook(request: Request):
807
  BOT_STATE["logs"].append({
808
  "event": "proxy_created", "sender_id": sender_id, "chat_id": chat_id,
809
  "sender_name": sender_name, "chat_type": chat_type,
810
- "text": f"Proxy: {proxy_url} | Webhook: {webhook_ok}",
811
  })
812
  except Exception as e:
813
  logger.error("Failed: %s", e)
814
  try:
815
- ZaloBotAPI(BOT_STATE["bot_token"]).send_message(cid, f" Lỗi tạo proxy: {e}")
816
  except Exception:
817
  pass
818
 
819
  threading.Thread(target=_create_and_setup, daemon=True).start()
820
  return Response(content=json.dumps({"message": "Processing", "proxy_url": "pending"}), media_type="application/json")
821
 
822
- # ─── Regular message: extract product data → save to MAIN dataset ───
823
  if cid:
824
  zapi = ZaloBotAPI(BOT_STATE["bot_token"])
825
  product_name, description, price, category = "", "", "", ""
@@ -828,14 +791,16 @@ async def handle_webhook(request: Request):
828
  if attachments:
829
  payload = attachments.get("payload", {})
830
  if isinstance(payload, str):
831
- try: payload = json.loads(payload)
832
- except Exception: payload = {}
 
 
833
  image_url = payload.get("url", "")
834
  image_data_b64 = payload.get("data", "") or msg.get("image", "")
835
 
836
  name_match = re.search(r'(?:ten sp|ten san pham|product name|name)[:\s]*([^|\n]+)', text, re.IGNORECASE)
837
  desc_match = re.search(r'(?:mo ta|description|desc|mota)[:\\s]*([^|\n]+)', text, re.IGNORECASE)
838
- price_match = re.search(r'(?:gia|price|don gia|donggia)[:\\s]*([\d,.]+)', text, re.IGNORECASE)
839
  cat_match = re.search(r'(?:chuyen muc|category|danh muc|loai)[:\\s]*([^|\n]+?)(?:$|\n)', text, re.IGNORECASE)
840
  if name_match: product_name = name_match.group(1).strip()
841
  if desc_match: description = desc_match.group(1).strip()
@@ -852,10 +817,10 @@ async def handle_webhook(request: Request):
852
  BOT_STATE["logs"].append({
853
  "event": "product_saved", "sender_id": sender_id, "chat_id": chat_id,
854
  "sender_name": sender_name, "chat_type": chat_type,
855
- "text": f"Product saved! name={product_name} price={price} category={category} ds={MAIN_DATASET_ID}",
856
  })
857
 
858
- reply = f"👋 Xin chào **{sender_name}** (ID: `{sender_id}`)!\n\n{HELP_INSTRUCTIONS}"
859
  asyncio.create_task(asyncio.to_thread(zapi.send_message, cid, reply))
860
 
861
  return Response(content=json.dumps({"message": "Success"}), media_type="application/json", status_code=200)
@@ -888,36 +853,33 @@ async def logs_page():
888
  is_zgr = _is_zgr_sender_local(sender_id)
889
  is_saved = log.get("event") in ("dataset_saved", "main_dataset_saved", "proxy_created", "product_saved")
890
  is_error = log.get("event") in ("dataset_error", "main_dataset_error", "parse_error", "image_upload_fail")
891
- bg = "#e8f5e9" if is_zgr else ("#ffffff" if idx % 2 == 0 else "#fafafa")
892
  header_color = "#4CAF50" if is_zgr else "#1a73e8"
893
  zgr_badge = " [ZGR]" if is_zgr else ""
894
- status_badge = "" if is_saved else ("" if is_error else "ℹ️")
895
- weight = "bold" if is_zgr else "normal"
896
  log_lines.append(
897
- f"<div style='margin:8px 0;padding:10px;background:{bg};border-radius:6px;border-left:3px solid {header_color}'>"
898
- f"<div style='display:flex;gap:6px;align-items:center;flex-wrap:wrap'>"
899
- f"<b style='color:{header_color}'>{status_badge} [{escape(str(log.get('event','')))}]</b>"
900
- f"<span style='color:{header_color};font-weight:{weight}'>"
901
- f"👤 {escape(str(sender_name))}</span>"
902
- f"<span style='color:#666'>🆔 <code>{escape(str(sender_id))}</code></span>"
903
- f"<span style='color:#666'>💬 <code>{escape(str(log.get('chat_id','')))}</code></span>"
904
- f"<span style='color:#888'>[{escape(str(log.get('chat_type','')))}]</span>"
905
- f"<span style='color:{'#4CAF50' if is_zgr else '#999'}'>{zgr_badge}</span>"
906
- f"</div>"
907
- f"<div style='margin-top:4px;color:#333;font-family:monospace;font-size:13px;word-break:break-word'>"
908
- f"{escape(str(log.get('text','')[:200]))}"
909
- f"</div>"
910
- f"<div style='margin-top:2px;color:#999;font-size:11px'>⏰ {time.strftime('%Y-%m-%d %H:%M:%S')}"
911
- f" | 📊 <a href='/logs/zgr-b7e1e71cf5701c2e4561'>xem riêng zgr logs</a>"
912
- f"</div>"
913
- f"</div>"
914
  )
915
  total_logs = len(BOT_STATE.get("logs", []))
916
  total_proxies = len(BOT_STATE.get("api_spaces", []))
917
  connected_status = "✅" if BOT_STATE.get("connected") else "❌"
918
  last_sender = escape(str(BOT_STATE.get("last_sender_id", "")[:8]) or "—")
919
  zgr_count = sum(1 for l in BOT_STATE.get("logs", []) if _is_zgr_sender_local(l.get("sender_id", "")))
920
- log_html = "".join(log_lines) if log_lines else '<p style="color:#999">Chưa có sự kiện</p>'
 
921
  html_content = (
922
  '<!DOCTYPE html><html><head><title>Zalo Bot Logs</title>'
923
  '<meta http-equiv="refresh" content="5">'
@@ -931,19 +893,19 @@ async def logs_page():
931
  '.stat-label { font-size: 12px; color: #5f6368; }'
932
  '.log-container { max-height: 650px; overflow-y: auto; background:#fff; border-radius:8px; padding:8px; }'
933
  '</style></head><body>'
934
- '<h1>📊 Zalo Bot Logs</h1>'
935
- '<p class="subtitle">Chi tiết từng sự kiện — tên Zalo, ID, chat_id, loại chat</p>'
936
- '<p>🔗 <a href="/gradio/"> Trở về giao diện chính</a> | 📊 <a href="/proxy-spaces">Danh sách proxy</a></p>'
937
  '<div class="stats">'
938
- f'<div class="stat-box"><div class="stat-value">{total_logs}</div><div class="stat-label">Tổng sự kiện</div></div>'
939
- f'<div class="stat-box"><div class="stat-value">{total_proxies}</div><div class="stat-label">Proxy đã tạo</div></div>'
940
- f'<div class="stat-box"><div class="stat-value">{zgr_count}</div><div class="stat-label">ZGR Events</div></div>'
941
- f'<div class="stat-box"><div class="stat-value">{connected_status}</div><div class="stat-label">Trạng thái bot</div></div>'
942
- f'<div class="stat-box"><div class="stat-value">{last_sender}</div><div class="stat-label">Sender ID (last)</div></div>'
943
  '</div>'
944
- f'<h2>📋 Sự kiện ({total_logs})</h2>'
945
- f'<div class="log-container">{log_html}</div>'
946
- '<p><a href="/logs/zgr-b7e1e71cf5701c2e4561">📊 Xem logs riêng cho nhóm zgr-b7e1e71cf5701c2e4561</a></p>'
947
  '</body></html>'
948
  )
949
  return Response(content=html_content, media_type="text/html")
@@ -951,11 +913,14 @@ async def logs_page():
951
 
952
  @app.get("/logs/zgr-b7e1e71cf5701c2e4561")
953
  async def zgr_logs_page():
954
- """Tab riêng cho nhóm zgr-b7e1e71cf5701c2e4561."""
955
  zgr_logs = []
956
  for log in BOT_STATE.get("logs", []):
957
  sid = str(log.get("sender_id", ""))
958
- if ZGR_SENDER_ID in sid or sid == ZGR_SENDER_ID:
 
 
 
 
959
  zgr_logs.append(log)
960
  log_lines = []
961
  for idx, log in enumerate(reversed(zgr_logs[-50:])):
@@ -965,32 +930,36 @@ async def zgr_logs_page():
965
  is_error = log.get("event") in ("dataset_error", "main_dataset_error", "parse_error", "image_upload_fail")
966
  bg = "#ffffff" if idx % 2 == 0 else "#fafafa"
967
  status_color = "#4CAF50" if is_saved else ("#f44336" if is_error else "#1a73e8")
968
- status_icon = "" if is_saved else ("" if is_error else "ℹ️")
 
 
 
 
 
 
 
969
  log_lines.append(
970
- f"<div style='margin:8px 0;padding:10px;background:{bg};border-radius:6px;border-left:3px solid {status_color}'>"
971
- f"<div style='display:flex;gap:6px;align-items:center;flex-wrap:wrap'>"
972
- f"<b style='color:{status_color}'>{status_icon} [{escape(str(log.get('event','')))}]</b>"
973
- f"<span style='color:#1a73e8;font-weight:bold'>👤 {escape(str(sender_name))}</span>"
974
- f"<span style='color:#666'>🆔 <code>{escape(str(sender_id[:12]))}</code></span>"
975
- f"<span style='color:#666'>💬 <code>{escape(str(log.get('chat_id','')[:12]))}</code></span>"
976
- f"[{escape(str(log.get('chat_type','')))]"
977
- f"</div>"
978
- f"<div style='margin-top:4px;color:#333;font-family:monospace;font-size:13px;word-break:break-word'>"
979
- f"{escape(str(log.get('text','')[:300]))}"
980
- f"</div>"
981
- f"<div style='margin-top:2px;color:#999;font-size:11px'>⏰ {log.get('time','')}"
982
- f" | 📊 <a href='https://huggingface.co/datasets/{escape(NAMESPACE)}/zalo-products-all' target='_blank'>Main Dataset</a>"
983
- f" | 📁 <a href='/proxy-spaces'>Quản lý proxy</a>"
984
- f"</div>"
985
- f"</div>"
986
  )
987
  saved_count = sum(1 for l in zgr_logs if l.get("event") in ("dataset_saved", "main_dataset_saved", "product_saved"))
988
  error_count = sum(1 for l in zgr_logs if l.get("event") in ("dataset_error", "main_dataset_error", "image_upload_fail"))
989
  total_zgr_logs = len(zgr_logs)
990
- zgr_log_html = "".join(log_lines) if log_lines else '<p style="color:#999">Chưa có sự kiện cho nhóm này. Gửi ảnh + thông tin sản phẩm để kiểm tra.</p>'
991
 
992
  html_content = (
993
- '<!DOCTYPE html><html><head><title>Zalo Logs - zgr-b7e1e71cf5701c2e4561</title>'
994
  '<meta http-equiv="refresh" content="5">'
995
  '<meta name="viewport" content="width=device-width, initial-scale=1">'
996
  '<style>'
@@ -1005,18 +974,18 @@ async def zgr_logs_page():
1005
  '.stat-total { background: #e8f0fe; } .stat-total .stat-value { color: #1a73e8; }'
1006
  '.log-container { max-height: 650px; overflow-y: auto; background:#fff; border-radius:8px; padding:8px; }'
1007
  '</style></head><body>'
1008
- '<h1>📊 Logs nhóm zgr-b7e1e71cf5701c2e4561</h1>'
1009
- '<p class="subtitle">Kiểm tra dữ liệu ảnh và thông tin sản phẩm được lưu vào dataset</p>'
1010
- '<p>🔗 <a href="/logs"> Tất cả logs</a> | 📊 <a href="/gradio/"> Giao diện chính</a> | <a href="/proxy-spaces">Danh sách proxy</a></p>'
1011
  '<div class="stats">'
1012
- f'<div class="stat-box stat-total"><div class="stat-value">{total_zgr_logs}</div><div class="stat-label">Tổng sự kiện</div></div>'
1013
- f'<div class="stat-box stat-saved"><div class="stat-value">{saved_count}</div><div class="stat-label">Đã lưu dataset</div></div>'
1014
- f'<div class="stat-box stat-error"><div class="stat-value">{error_count}</div><div class="stat-label">Lỗi</div></div>'
1015
- '<div class="stat-box stat-total"><div class="stat-value"><a href="https://huggingface.co/datasets/bep40/zalo-products-all" target="_blank">🔗 Main DS</a></div><div class="stat-label">Dataset chính</div></div>'
1016
  '</div>'
1017
- f'<h2>📋 Sự kiện ({total_zgr_logs})</h2>'
1018
- f'<div class="log-container">{zgr_log_html}</div>'
1019
- '<p style="color:#5f6368;font-size:13px;margin-top:12px">💡 Gửi ảnh sản phẩm kèm: "Tên sp: áo thun, Mô tả: áo thun cotton, Giá: 150000, Chuyên mục: quần áo"</p>'
1020
  '</body></html>'
1021
  )
1022
  return Response(content=html_content, media_type="text/html")
@@ -1026,25 +995,25 @@ async def zgr_logs_page():
1026
  async def proxy_spaces_page():
1027
  rows = []
1028
  for s in reversed(BOT_STATE.get("api_spaces", [])[-20:]):
1029
- repo_name = escape(str(s.get('repo_id', '').split('/')[-1]))
1030
- dataset_id_val = s.get('dataset_id', '')
1031
  dataset_link = "<a href='https://huggingface.co/datasets/" + escape(dataset_id_val) + "' target='_blank'>💾 dataset</a>" if dataset_id_val else ""
1032
  rows.append(
1033
  "<div style='margin:8px 0;padding:12px;background:#fff;border-radius:8px;border-left:4px solid #4CAF50;box-shadow:0 1px 3px rgba(0,0,0,0.1)'>"
1034
  "<div style='display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:space-between'>"
1035
  "<div>"
1036
- "<b style='color:#1a73e8'>👤 " + escape(str(s.get('sender_name', ''))) + "</b>"
1037
- "<span style='color:#666'>🆔 <code>" + escape(str(s.get('user_id', ''))) + "</code></span>"
1038
- "<span style='color:#4CAF50;font-weight:bold'>[" + escape(str(s.get('status', ''))) + "]</span>"
1039
  "</div>"
1040
  "<div style='display:flex;gap:6px;flex-wrap:wrap'>"
1041
  "<a href='https://huggingface.co/spaces/bep40/" + repo_name + "' target='_blank'>Space</a>"
1042
- "<a href='" + escape(str(s.get('proxy_url', ''))) + "' target='_blank'>webhook</a>"
1043
- "<a href='" + escape(str(s.get('proxy_url', '')).replace('/webhooks', '/logs')) + "' target='_blank'>📊 logs</a>"
1044
  + dataset_link +
1045
  "</div>"
1046
  "</div>"
1047
- "<div style='margin-top:6px'><span style='color:#5f6368'>Repo:</span> <code>" + escape(str(s.get('repo_id', ''))) + "</code></div>"
1048
  "<div style='margin-top:2px;color:#999;font-size:11px'>⏰ " + time.strftime('%Y-%m-%d %H:%M:%S') + "</div>"
1049
  "</div>"
1050
  )
@@ -1066,12 +1035,12 @@ async def proxy_spaces_page():
1066
  'a { color: #1a73e8; text-decoration: none; cursor: pointer; }'
1067
  'a:hover { text-decoration: underline; }'
1068
  '</style></head><body>'
1069
- '<h1>📊 Quản Proxy Spaces</h1>'
1070
- '<p class="subtitle">Danh sách các space proxy đã tạo (mỗi user có 1 proxy riêng).</p>'
1071
- '<p>🔗 <a href="/logs">Logs chính</a> | <a href="/gradio/"> Giao diện chính</a></p>'
1072
  '<div class="stats">'
1073
- f'<div class="stat-box"><div class="stat-value">{total_proxies}</div><div class="stat-label">Proxy đã tạo</div></div>'
1074
- f'<div class="stat-box"><div class="stat-value">{connected_status}</div><div class="stat-label">Bot chính</div></div>'
1075
  '</div>'
1076
  '<div class="container">'
1077
  + rows_html +
@@ -1083,7 +1052,7 @@ async def proxy_spaces_page():
1083
 
1084
  @app.post("/api/delete-proxy/{repo_name}")
1085
  async def delete_proxy(repo_name: str):
1086
- repo_id = f"{NAMESPACE}/{repo_name}"
1087
  try:
1088
  api = HfApi(token=HF_TOKEN)
1089
  try:
@@ -1100,35 +1069,34 @@ async def delete_proxy(repo_name: str):
1100
  logger.warning("Could not delete dataset %s: %s", dataset_id, e)
1101
  BOT_STATE["api_spaces"] = [s for s in BOT_STATE.get("api_spaces", []) if s.get("repo_id", "").split("/")[-1] != repo_name]
1102
  _save_proxy_spaces()
1103
- return {"ok": True, "message": f"Đã xóa proxy {repo_id}!"}
1104
  except Exception as e:
1105
  logger.error("Delete proxy failed: %s", e)
1106
- return {"ok": False, "message": f"Lỗi xóa: {e}"}
1107
 
1108
 
1109
  with gr.Blocks(title="Zalo Bot Webhook") as demo:
1110
  gr.Markdown("# Zalo Bot Webhook Setup")
1111
  with gr.Tabs():
1112
- with gr.Tab("Kết nối Bot"):
1113
  tok = gr.Textbox(DEFAULT_BOT_TOKEN, label="Bot Token", type="password")
1114
- btn = gr.Button("Kết nối")
1115
  res = gr.Markdown("")
1116
  btn.click(fn=connect_bot, inputs=[tok], outputs=res)
1117
- gr.Textbox(value=get_botinfo, label="Thông tin bot", interactive=False, lines=8)
1118
- with gr.Tab("Gửi tin"):
1119
  with gr.Row():
1120
  cid = gr.Textbox(label="Chat ID")
1121
- txt = gr.Textbox("HTTP API: 4179413508988279245:abc123", label="Nội dung")
1122
- b = gr.Button("Gửi")
1123
- b.click(fn=send_msg, inputs=[cid, txt], outputs=gr.Textbox(label="Kết quả"))
1124
- gr.Textbox(value=get_events, label="Sự kiện nhận được", interactive=False, lines=20)
1125
  gr.Markdown(
1126
- "🔗 [Xem trang quản lý chi tiết](/proxy-spaces)\n\n"
1127
- "📖 Mỗi user gửi `HTTP API: <token>` bot tự động tạo proxy space riêng, "
1128
- "setup webhook + auto trả lời tin nhắn."
1129
  )
1130
- with gr.Tab("Hướng dẫn"):
1131
- gr.Markdown("📖 1. Tạo bot https://zalo.me/s/botcreator\n2. Copy HTTP API → nhắn cho bot\n3. Bot tự động tạo proxy + hướng dẫn + setup webhook")
1132
 
1133
  demo.queue()
1134
  app = gr.mount_gradio_app(app, demo, path="/gradio")
@@ -1138,6 +1106,6 @@ print("[startup] FastAPI app ready: /health, /webhooks, /logs, /logs/zgr-b7e1e71
1138
  if __name__ == "__main__":
1139
  port = int(os.getenv("PORT", "7860"))
1140
  server_name = os.getenv("GRADIO_SERVER_NAME", "0.0.0.0")
1141
- print(f"[launch] uvicorn on {server_name}:{port}", flush=True)
1142
  import uvicorn
1143
  uvicorn.run(app, host=server_name, port=port)
 
1
  import os, sys, json, secrets, logging, asyncio, re, time, threading, base64, tempfile, pathlib
2
  from html import escape
3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  logging.basicConfig(level=logging.INFO, stream=sys.stdout)
5
  logger = logging.getLogger("zalo-bot")
6
 
 
149
  return f"{NAMESPACE}/{safe_id}-zalo-data"
150
 
151
 
152
+ def _is_zgr_sender_local(sender_id):
153
+ sid = str(sender_id)
154
+ return ZGR_SENDER_ID in sid or sid == ZGR_SENDER_ID
155
+
156
+
157
  def _ensure_user_dataset(user_id: str, token: str) -> tuple:
158
  if not HF_TOKEN:
159
  raise RuntimeError("HF_TOKEN chưa được cấu hình.")
 
282
  def _create_api_proxy_space(token, user_id, sender_display):
283
  safe_id = _safe_space_name(user_id)
284
  token_suffix = token.split(":")[-1][:12] if ":" in token else re.sub(r'\W', '', token[:12])
285
+ unique_key = safe_id if safe_id else "u" + token_suffix
286
  space_name = f"zalo-proxy-{unique_key}"
287
  repo_id = f"{NAMESPACE}/{space_name}"
288
  dataset_id = f"{NAMESPACE}/{unique_key}-zalo-data"
 
305
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
306
  """
307
  requirements = "fastapi>=0.111.0\nuvicorn[standard]>=0.30.0\nrequests>=2.32.0\nhuggingface_hub>=0.30.0\n"
308
+ app_py = '''import os, json, requests, time, re, base64, tempfile, pathlib, sys
309
  from html import escape as _escape
310
 
 
311
  class _StderrLogger:
312
+ def __init__(self):
313
  self._log = []
314
  def write(self, s):
315
  if s.strip():
 
322
  from fastapi import FastAPI, Request, Response
323
 
324
  app = FastAPI(title="Zalo Proxy Space")
325
+ BOT_TOKEN = "''' + token + '''"
326
  TARGET_API = "https://bot-api.zaloplatforms.com"
327
+ PROXY_NAME = "''' + sender_display + '''"
328
  HF_TOKEN = os.getenv("HF_TOKEN", "")
329
+ DATASET_ID = "''' + dataset_id + '''"
330
+ MAIN_DATASET_ID = "''' + MAIN_DATASET_ID + '''"
331
+ MAIN_SPACE_URL = "''' + SPACE_ID.replace("/", "-") + '''.hf.space"
332
+ ZGR_SENDER_ID = "zgr-b7e1e71cf5701c2e4561"
333
  _logs = []
334
 
335
  def _send(cid, text):
336
+ headers = {"Content-Type": "application/json"}
337
+ url = TARGET_API + "/bot" + BOT_TOKEN + "/sendMessage"
338
+ return requests.post(url, json={"chat_id": cid, "text": text}, headers=headers)
339
 
340
  def _safe_name(name):
341
  return re.sub(r'[^a-zA-Z0-9]', '_', str(name))[:30]
 
344
  sid = str(sender_id)
345
  return ZGR_SENDER_ID in sid or sid == ZGR_SENDER_ID
346
 
347
+ def _log(event, sender_id, chat_id, text, sender_name="", chat_type=""):
348
+ entry = {"event": str(event), "sender_id": str(sender_id), "sender_name": str(sender_name), "chat_id": str(chat_id), "chat_type": str(chat_type), "text": str(text)[:500], "is_zgr": _is_zgr_sender(sender_id), "time": time.strftime("%Y-%m-%d %H:%M:%S")}
349
+ _logs.append(entry)
350
+ print("[WEBHOOK] event=" + str(event) + " sender=" + str(sender_id) + " chat=" + str(chat_id) + " is_zgr=" + str(entry["is_zgr"]) + " text=" + str(text)[:100], flush=True)
351
+ if len(_logs) > 200:
352
+ del _logs[:100]
353
+
354
+ _log("startup", "system", "SYSTEM", "Proxy space initialized. PROXY_NAME=" + PROXY_NAME)
355
+
356
  def _save_to_main_dataset(image_url, image_data_b64, description, price, category, sender_id, sender_name, product_name="", chat_id=""):
357
+ _log("main_dataset_save_start", sender_id, chat_id, "product_name=" + str(product_name) + " price=" + str(price))
358
  if not HF_TOKEN or not MAIN_DATASET_ID:
359
  _log("main_dataset_skip", sender_id, chat_id, "HF_TOKEN or MAIN_DATASET_ID missing")
360
  return None
 
363
  api = HfApi(token=HF_TOKEN)
364
  ts = time.strftime("%Y%m%d_%H%M%S")
365
  safe_sender = _safe_name(sender_id) or "unknown"
366
+ img_filename = "images/" + ts + "_" + safe_sender + ".jpg"
367
+ meta_filename = "data/" + ts + "_" + safe_sender + ".json"
368
  img_bytes = None
369
  if image_data_b64:
370
  try:
 
384
  tmp.write(img_bytes)
385
  tmp_path = tmp.name
386
  try:
387
+ api.upload_file(path_or_fileobj=tmp_path, path_in_repo=img_filename, repo_id=MAIN_DATASET_ID, repo_type="dataset", token=HF_TOKEN, commit_message="Add product image from " + sender_name)
388
  uploaded_img = img_filename
389
  _log("image_uploaded", sender_id, chat_id, img_filename)
390
  except Exception as e:
391
  _log("image_upload_fail", sender_id, chat_id, str(e))
392
  finally:
393
  pathlib.Path(tmp_path).unlink(missing_ok=True)
394
+ record = {"image": uploaded_img, "product_name": str(product_name)[:200] if product_name else "", "description": str(description)[:500] if description else "", "price": str(price) if price else "", "category": str(category) if category else "", "sender_id": str(sender_id), "sender_name": str(sender_name), "chat_id": str(chat_id), "is_zgr_group": _is_zgr_sender(sender_id), "timestamp": time.strftime("%Y-%m-%d %H:%M:%S")}
395
  with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
396
  json.dump(record, tmp, indent=2, ensure_ascii=False)
397
  tmp_path = tmp.name
398
  try:
399
+ api.upload_file(path_or_fileobj=tmp_path, path_in_repo=meta_filename, repo_id=MAIN_DATASET_ID, repo_type="dataset", token=HF_TOKEN, commit_message="Add product metadata from " + sender_name)
400
  _log("dataset_save_to_main", sender_id, chat_id, "OK")
401
  except Exception as e:
402
  _log("dataset_save_fail", sender_id, chat_id, str(e))
 
416
  api = HfApi(token=HF_TOKEN)
417
  ts = time.strftime("%Y%m%d_%H%M%S")
418
  safe_sender = _safe_name(sender_id) or "unknown"
419
+ img_filename = "images/" + ts + "_" + safe_sender + ".jpg"
420
+ meta_filename = "data/" + ts + "_" + safe_sender + ".json"
421
  img_bytes = None
422
  if image_data_b64:
423
  try:
 
436
  tmp.write(img_bytes)
437
  tmp_path = tmp.name
438
  try:
439
+ api.upload_file(path_or_fileobj=tmp_path, path_in_repo=img_filename, repo_id=DATASET_ID, repo_type="dataset", token=HF_TOKEN, commit_message="Add product image from " + sender_name)
440
  uploaded_img = img_filename
441
  except Exception as e:
442
  _log("image_upload_fail", sender_id, "N/A", str(e))
443
  finally:
444
  pathlib.Path(tmp_path).unlink(missing_ok=True)
445
+ record = {"image": uploaded_img, "product_name": "", "description": str(description)[:500] if description else "", "price": str(price) if price else "", "category": str(category) if category else "", "sender_id": str(sender_id), "sender_name": str(sender_name), "timestamp": time.strftime("%Y-%m-%d %H:%M:%S")}
446
  with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
447
  json.dump(record, tmp, indent=2, ensure_ascii=False)
448
  tmp_path = tmp.name
449
  try:
450
+ api.upload_file(path_or_fileobj=tmp_path, path_in_repo=meta_filename, repo_id=DATASET_ID, repo_type="dataset", token=HF_TOKEN, commit_message="Add product metadata from " + sender_name)
451
  finally:
452
  pathlib.Path(tmp_path).unlink(missing_ok=True)
453
+ _log("dataset_saved", sender_id, "N/A", "Saved to " + DATASET_ID)
454
  return DATASET_ID
455
  except Exception as e:
456
  _log("dataset_error", sender_id, "N/A", str(e))
 
458
 
459
  @app.get("/")
460
  async def root():
461
+ return {"status": "ok"}
462
 
463
  @app.get("/health")
464
  async def health():
465
+ return {"status": "ok", "dataset": DATASET_ID, "zgr_sender": ZGR_SENDER_ID, "is_zgr": _is_zgr_sender(ZGR_SENDER_ID)}
466
 
467
  @app.get("/webhooks")
468
  async def webhooks_get():
469
+ return Response(content=json.dumps({"message": "Success"}), media_type="application/json", status_code=200)
470
 
471
  @app.post("/webhooks")
472
  async def webhooks(request: Request):
 
477
  data = json.loads(body_str)
478
  except Exception as e:
479
  _log("parse_error", "N/A", "N/A", "Bad JSON: " + str(e) + " | body=" + body_str[:200])
480
+ return Response(content=json.dumps({"message": "Bad JSON"}), media_type="application/json", status_code=400)
481
  result = data.get("result", data)
482
  event = result.get("event_name", "unknown")
483
+ msg = result.get("message", {})
484
+ sender = msg.get("from", {})
485
+ chat = msg.get("chat", {})
486
  text = msg.get("text", "")
487
  sender_id = str(sender.get("id", ""))
488
  sender_name = sender.get("display_name") or sender.get("name") or sender_id
489
  chat_id = str(chat.get("id", ""))
490
  chat_type = str(chat.get("chat_type", ""))
491
+ attachments = msg.get("attachment", {})
492
  image_url = ""
493
  image_data_b64 = ""
494
  if attachments:
495
+ payload = attachments.get("payload", {})
496
  if isinstance(payload, str):
497
  try:
498
  payload = json.loads(payload)
499
  except Exception:
500
+ payload = {}
501
  image_url = payload.get("url", "")
502
  image_data_b64 = payload.get("data", "") or msg.get("image", "")
503
  is_zgr = _is_zgr_sender(sender_id)
504
  _log(event, sender_id, chat_id, text, sender_name, chat_type)
505
+ _log("debug_info", sender_id, chat_id, "chat_type=" + str(chat_type) + " is_zgr=" + str(is_zgr) + " sender_id=" + str(sender_id) + " sender_name=" + str(sender_name) + " text_len=" + str(len(text)) + " has_attachment=" + str(bool(attachments)) + " image_url=" + str(bool(image_url)))
506
 
507
  if event == "message.text.received" and chat_id:
508
  description, price, category, product_name = "", "", "", ""
 
522
  dataset_id = _save_to_dataset(image_url=image_url, image_data_b64=image_data_b64, description=description or text[:200], price=price, category=category, sender_id=sender_id, sender_name=sender_name)
523
  _save_to_main_dataset(image_url=image_url, image_data_b64=image_data_b64, description=description or text[:200], price=price, category=category, sender_id=sender_id, sender_name=sender_name, product_name=product_name, chat_id=chat_id)
524
  if dataset_id:
525
+ reply = "GOT IT! Product saved!"
526
  else:
527
+ reply = "GOT IT! Saved to main dataset!"
528
  else:
529
+ reply = "Hi! Send image + product info to save."
530
  try:
531
  _send(chat_id, reply)
532
  except Exception as e:
533
  _log("send_reply_fail", sender_id, chat_id, str(e))
534
+ return Response(content=json.dumps({"message": "Success"}), media_type="application/json", status_code=200)
 
 
 
 
 
 
 
535
 
536
  @app.get("/logs")
537
  async def proxy_logs():
538
  rows = ""
539
  for log in reversed(_logs[-100:]):
540
  is_zgr = _is_zgr_sender(log.get("sender_id", ""))
541
+ bg = "#e8f5e9" if is_zgr else "#ffffff"
542
+ zgr_badge = "[ZGR] " if is_zgr else ""
543
+ chat_type_val = _escape(str(log.get("chat_type", "")))
544
+ rows += "<div style='margin:6px 0;padding:8px;background:" + bg + ";border-radius:4px;border-left:3px solid #4CAF50'><b>" + zgr_badge + "[" + _escape(str(log["event"])) + "]</b> " + _escape(str(log.get("sender_name",""))) + " ID:<code>" + _escape(str(log.get("sender_id",""))) + "</code> chat:<code>" + _escape(str(log.get("chat_id",""))) + "</code> type:[" + chat_type_val + "]<br><span style='font-family:monospace;font-size:12px;color:#333'>" + _escape(str(log.get("text",""))[:300]) + "</span><br><small style='color:#999'>" + _escape(str(log.get("time",""))) + "</small></div>"
545
+ html_content = "<!DOCTYPE html><html><head><title>Proxy Logs</title><meta http-equiv='refresh' content='5'><style>body{font-family:Arial,sans-serif;max-width:1000px;margin:0 auto;padding:16px;}h1{color:#1a73e8;}.log-c{max-height:600px;overflow-y:auto;background:#fff;border-radius:8px;padding:8px;}</style></head><body><h1>Proxy Logs - " + _escape(PROXY_NAME) + "</h1><div class='log-c'>" + rows + "</div></body></html>"
546
+ return Response(content=html_content, media_type="text/html")
547
  '''
548
 
549
+ readme = """---
550
+ title: Zalo Proxy
551
  colorFrom: blue
552
  colorTo: purple
553
  sdk: docker
554
  app_port: 7860
555
  ---
556
 
557
+ Zalo Webhook Proxy Space
 
 
 
 
558
  """
559
 
560
  with tempfile.TemporaryDirectory() as tmp:
 
572
  except Exception as e:
573
  err_msg = str(e).lower()
574
  if "429" in err_msg or "rate limit" in err_msg:
575
+ raise RuntimeError("Rate limit. Try again later.")
576
  if "already exist" in err_msg or "conflict" in err_msg:
577
  logger.info("Proxy space %s already exists, skipping create_repo", repo_id)
578
  else:
 
582
  except Exception as e:
583
  err_msg = str(e)
584
  if "404" in err_msg or "Repository Not Found" in err_msg:
585
+ raise RuntimeError("Proxy repo error")
586
  raise
587
 
588
  try:
 
596
  except Exception:
597
  status = "UNKNOWN"
598
 
599
+ proxy_url = "https://" + repo_id.replace('/', '-') + ".hf.space/webhooks"
600
  logger.info("Proxy space ready: %s -> %s (status=%s)", repo_id, proxy_url, status)
601
  return repo_id, proxy_url, status
602
 
 
621
  try:
622
  zapi = ZaloBotAPI(token)
623
  except AssertionError as e:
624
+ return "Token sai: " + str(e)
625
  try:
626
  me = zapi.get_me()
627
  if not me.get("ok"):
628
+ return "That bai: " + str(me.get('message', ''))
629
  BOT_STATE["bot_info"] = me.get("result", {})
 
630
  except Exception as e:
631
+ return "Loi: " + str(e)
632
  wh = get_webhook_url()
633
  sc = BOT_STATE["webhook_secret"]
634
  try:
 
636
  if sw.get("ok"):
637
  BOT_STATE["webhook_url"] = wh
638
  BOT_STATE["connected"] = True
639
+ return "Ket noi thanh cong! Webhook: " + wh
640
+ return "setWebhook that bai: " + str(sw.get('message', ''))
641
  except Exception as e:
642
+ return "Loi: " + str(e)
643
 
644
 
645
  def send_msg(cid: str, text: str):
646
  if not BOT_STATE["connected"]:
647
+ return "Chua ket noi bot."
648
  if not cid or not text:
649
+ return "Nhap Chat ID va Noi dung"
650
  try:
651
  result = ZaloBotAPI(BOT_STATE["bot_token"]).send_message(cid, text)
652
  return json.dumps(result, indent=2, ensure_ascii=False)
653
  except Exception as e:
654
+ return "Loi: " + str(e)
655
 
656
 
657
  def get_botinfo():
658
  if BOT_STATE["bot_info"]:
659
  info = BOT_STATE["bot_info"]
660
+ lines = ["Ten bot: " + str(info.get('name', '?')), "ID: " + str(info.get('id', ''))]
661
  if BOT_STATE.get("webhook_url"):
662
+ lines.append("Webhook: " + BOT_STATE["webhook_url"])
663
+ lines.append("Ket noi: " + str(BOT_STATE.get("connected", False)))
 
664
  return "\n".join(lines)
665
+ return "Chua ket noi"
666
 
667
 
668
  def get_events():
669
  if not BOT_STATE["logs"]:
670
+ return "Chua co su kien"
671
  lines = []
672
  for i, l in enumerate(BOT_STATE["logs"][-20:][::-1], 1):
673
+ is_zgr = _is_zgr_sender_local(l.get("sender_id", ""))
674
  zgr_tag = " [ZGR]" if is_zgr else ""
675
+ lines.append("{}. [{}] {} Zalo:{} ID:{} chat:{} | {}".format(
676
+ i, l.get('event',''), zgr_tag, l.get('sender_name',''), l.get('sender_id',''), l.get('chat_id',''), str(l.get('text','')[:50])
677
+ ))
678
  return "\n".join(lines)
679
 
680
 
681
  def get_proxy_spaces():
682
  spaces = BOT_STATE.get("api_spaces", [])
683
  if not spaces:
684
+ return "Chua co proxy space nao"
685
  lines = []
686
  for i, s in enumerate(spaces[-10:][::-1], 1):
687
+ lines.append("{}. {} ID:{} Space:{} Webhook:{} Status:{}".format(
688
+ i, s.get('sender_name',''), s.get('user_id',''), s.get('repo_id',''), s.get('proxy_url',''), s.get('status','')
689
+ ))
690
  return "\n".join(lines)
691
 
692
 
 
 
 
 
 
693
  HELP_INSTRUCTIONS = (
694
+ "Cach cau hinh Zalo bot:\n"
695
+ "1. Ten bot khong duoc chua 'Zalo' hoac 'bot'\n"
696
+ "2. Lay HTTP API tu https://zalo.me/s/botcreator\n"
697
+ "3. Gui anh + mo ta san pham de luu"
 
 
 
 
 
 
 
 
 
698
  )
699
 
700
 
 
738
  if user_token:
739
  zapi = ZaloBotAPI(BOT_STATE["bot_token"])
740
  try:
741
+ zapi.send_message(cid, "Processing your HTTP API...")
742
  except Exception:
743
  pass
744
 
 
758
  _save_proxy_spaces()
759
 
760
  sw = _set_user_webhook(user_token, proxy_url, BOT_STATE["webhook_secret"])
 
 
761
  user_bot_id = user_token.split(":")[0] if ":" in user_token else ""
762
+ user_bot_link = "https://zalo.me/s/" + user_bot_id if user_bot_id else "https://zalo.me/s/botcreator"
763
+ dataset_url = "https://huggingface.co/datasets/" + NAMESPACE + "/" + _safe_space_name(sender_id or "user") + "-zalo-data" if sender_id else ""
764
+ instructions = "BOT OK! Proxy: " + proxy_url + "\\nDataset: " + (dataset_url if dataset_url else "N/A") + "\\nManage bot at: " + user_bot_link
 
 
 
 
 
 
 
 
 
 
765
  try:
766
  zapi.send_message(cid, instructions)
767
  except Exception:
 
770
  BOT_STATE["logs"].append({
771
  "event": "proxy_created", "sender_id": sender_id, "chat_id": chat_id,
772
  "sender_name": sender_name, "chat_type": chat_type,
773
+ "text": "Proxy created: " + proxy_url,
774
  })
775
  except Exception as e:
776
  logger.error("Failed: %s", e)
777
  try:
778
+ ZaloBotAPI(BOT_STATE["bot_token"]).send_message(cid, "Loi tao proxy: " + str(e))
779
  except Exception:
780
  pass
781
 
782
  threading.Thread(target=_create_and_setup, daemon=True).start()
783
  return Response(content=json.dumps({"message": "Processing", "proxy_url": "pending"}), media_type="application/json")
784
 
785
+ # ─── Regular message ───
786
  if cid:
787
  zapi = ZaloBotAPI(BOT_STATE["bot_token"])
788
  product_name, description, price, category = "", "", "", ""
 
791
  if attachments:
792
  payload = attachments.get("payload", {})
793
  if isinstance(payload, str):
794
+ try:
795
+ payload = json.loads(payload)
796
+ except Exception:
797
+ payload = {}
798
  image_url = payload.get("url", "")
799
  image_data_b64 = payload.get("data", "") or msg.get("image", "")
800
 
801
  name_match = re.search(r'(?:ten sp|ten san pham|product name|name)[:\s]*([^|\n]+)', text, re.IGNORECASE)
802
  desc_match = re.search(r'(?:mo ta|description|desc|mota)[:\\s]*([^|\n]+)', text, re.IGNORECASE)
803
+ price_match = re.search(r'(?:gia|price|don gia|donggia)[:\\s]*([\\d,.]+)', text, re.IGNORECASE)
804
  cat_match = re.search(r'(?:chuyen muc|category|danh muc|loai)[:\\s]*([^|\n]+?)(?:$|\n)', text, re.IGNORECASE)
805
  if name_match: product_name = name_match.group(1).strip()
806
  if desc_match: description = desc_match.group(1).strip()
 
817
  BOT_STATE["logs"].append({
818
  "event": "product_saved", "sender_id": sender_id, "chat_id": chat_id,
819
  "sender_name": sender_name, "chat_type": chat_type,
820
+ "text": "Saved! " + str(product_name)[:50],
821
  })
822
 
823
+ reply = "Hi " + str(sender_name) + "! " + HELP_INSTRUCTIONS
824
  asyncio.create_task(asyncio.to_thread(zapi.send_message, cid, reply))
825
 
826
  return Response(content=json.dumps({"message": "Success"}), media_type="application/json", status_code=200)
 
853
  is_zgr = _is_zgr_sender_local(sender_id)
854
  is_saved = log.get("event") in ("dataset_saved", "main_dataset_saved", "proxy_created", "product_saved")
855
  is_error = log.get("event") in ("dataset_error", "main_dataset_error", "parse_error", "image_upload_fail")
856
+ bg = "#e8f5e9" if is_zgr else "#ffffff"
857
  header_color = "#4CAF50" if is_zgr else "#1a73e8"
858
  zgr_badge = " [ZGR]" if is_zgr else ""
859
+ status_badge = "SUCCESS" if is_saved else ("ERROR" if is_error else "INFO")
 
860
  log_lines.append(
861
+ "<div style='margin:8px 0;padding:10px;background:" + bg + ";border-radius:6px;border-left:3px solid " + header_color + "'>"
862
+ + "<div style='display:flex;gap:6px;align-items:center;flex-wrap:wrap'>"
863
+ + "<b style='color:" + header_color + "'>" + status_badge + " [" + escape(str(log.get("event",""))) + "]</b>"
864
+ + "<span style='color:" + header_color + "'>👤 " + escape(str(sender_name)) + "</span>"
865
+ + "<span style='color:#666'>🆔 " + escape(str(sender_id)) + "</span>"
866
+ + "<span style='color:#666'>💬 " + escape(str(log.get("chat_id",""))) + "</span>"
867
+ + "<span style='color:#888'>[" + escape(str(log.get("chat_type",""))) + "]</span>"
868
+ + "<span style='color:#4CAF50'>" + zgr_badge + "</span>"
869
+ + "</div>"
870
+ + "<div style='margin-top:4px;color:#333;font-family:monospace;font-size:13px;word-break:break-word'>"
871
+ + escape(str(log.get("text","")[:300]))
872
+ + "</div>"
873
+ + "<div style='margin-top:2px;color:#999;font-size:11px'>⏰ " + time.strftime('%Y-%m-%d %H:%M:%S') + " | <a href='/logs/zgr-b7e1e71cf5701c2e4561'>zgr logs</a></div>"
874
+ + "</div>"
 
 
 
875
  )
876
  total_logs = len(BOT_STATE.get("logs", []))
877
  total_proxies = len(BOT_STATE.get("api_spaces", []))
878
  connected_status = "✅" if BOT_STATE.get("connected") else "❌"
879
  last_sender = escape(str(BOT_STATE.get("last_sender_id", "")[:8]) or "—")
880
  zgr_count = sum(1 for l in BOT_STATE.get("logs", []) if _is_zgr_sender_local(l.get("sender_id", "")))
881
+ rows_html = "".join(log_lines) if log_lines else '<p style="color:#999">Chưa có sự kiện</p>'
882
+
883
  html_content = (
884
  '<!DOCTYPE html><html><head><title>Zalo Bot Logs</title>'
885
  '<meta http-equiv="refresh" content="5">'
 
893
  '.stat-label { font-size: 12px; color: #5f6368; }'
894
  '.log-container { max-height: 650px; overflow-y: auto; background:#fff; border-radius:8px; padding:8px; }'
895
  '</style></head><body>'
896
+ '<h1>Zalo Bot Logs</h1>'
897
+ '<p class="subtitle">Event details</p>'
898
+ '<p>Links: <a href="/gradio/">Main UI</a> | <a href="/proxy-spaces">Proxy spaces</a></p>'
899
  '<div class="stats">'
900
+ '<div class="stat-box"><div class="stat-value">' + str(total_logs) + '</div><div class="stat-label">Total Events</div></div>'
901
+ '<div class="stat-box"><div class="stat-value">' + str(total_proxies) + '</div><div class="stat-label">Proxies</div></div>'
902
+ '<div class="stat-box"><div class="stat-value">' + str(zgr_count) + '</div><div class="stat-label">ZGR Events</div></div>'
903
+ '<div class="stat-box"><div class="stat-value">' + connected_status + '</div><div class="stat-label">Bot Status</div></div>'
904
+ '<div class="stat-box"><div class="stat-value">' + last_sender + '</div><div class="stat-label">Last Sender</div></div>'
905
  '</div>'
906
+ '<h2>Events (' + str(total_logs) + ')</h2>'
907
+ '<div class="log-container">' + rows_html + '</div>'
908
+ '<p><a href="/logs/zgr-b7e1e71cf5701c2e4561">Xem logs riêng cho nhóm ZGR</a></p>'
909
  '</body></html>'
910
  )
911
  return Response(content=html_content, media_type="text/html")
 
913
 
914
  @app.get("/logs/zgr-b7e1e71cf5701c2e4561")
915
  async def zgr_logs_page():
 
916
  zgr_logs = []
917
  for log in BOT_STATE.get("logs", []):
918
  sid = str(log.get("sender_id", ""))
919
+ # Match any sender_id that contains or equals the ZGR identifier
920
+ if ZGR_SENDER_ID in sid or sid.endswith(ZGR_SENDER_ID[-12:]) or sid == ZGR_SENDER_ID:
921
+ zgr_logs.append(log)
922
+ # Also include logs explicitly tagged as ZGR
923
+ if log.get("is_zgr", False) and not log.get("sender_id"):
924
  zgr_logs.append(log)
925
  log_lines = []
926
  for idx, log in enumerate(reversed(zgr_logs[-50:])):
 
930
  is_error = log.get("event") in ("dataset_error", "main_dataset_error", "parse_error", "image_upload_fail")
931
  bg = "#ffffff" if idx % 2 == 0 else "#fafafa"
932
  status_color = "#4CAF50" if is_saved else ("#f44336" if is_error else "#1a73e8")
933
+ status_icon = "SUCCESS" if is_saved else ("ERROR" if is_error else "INFO")
934
+ chat_type_val = escape(str(log.get("chat_type", "")))
935
+ text_val = escape(str(log.get("text", "")[:300]))
936
+ event_val = escape(str(log.get("event", "")))
937
+ sender_val = escape(str(sender_name))
938
+ sid_short = escape(str(sender_id)[:20])
939
+ cid_short = escape(str(log.get("chat_id", ""))[:20])
940
+ time_val = escape(str(log.get("time", "")))
941
  log_lines.append(
942
+ "<div style='margin:8px 0;padding:10px;background:" + bg + ";border-radius:6px;border-left:3px solid " + status_color + "'>"
943
+ "<div style='display:flex;gap:6px;align-items:center;flex-wrap:wrap'>"
944
+ "<b style='color:" + status_color + "'>" + status_icon + " [" + event_val + "]</b>"
945
+ "<span style='color:#1a73e8;font-weight:bold'>👤 " + sender_val + "</span>"
946
+ "<span style='color:#666'>🆔 " + sid_short + "</span>"
947
+ "<span style='color:#666'>💬 " + cid_short + "</span>"
948
+ "<span style='color:#666'>[" + chat_type_val + "]</span>"
949
+ "</div>"
950
+ "<div style='margin-top:4px;color:#333;font-family:monospace;font-size:13px;word-break:break-word'>"
951
+ + text_val
952
+ + "</div>"
953
+ "<div style='margin-top:2px;color:#999;font-size:11px'>⏰ " + time_val + " | <a href='https://huggingface.co/datasets/bep40/zalo-products-all' target='_blank'>Dataset</a></div>"
954
+ "</div>"
 
 
 
955
  )
956
  saved_count = sum(1 for l in zgr_logs if l.get("event") in ("dataset_saved", "main_dataset_saved", "product_saved"))
957
  error_count = sum(1 for l in zgr_logs if l.get("event") in ("dataset_error", "main_dataset_error", "image_upload_fail"))
958
  total_zgr_logs = len(zgr_logs)
959
+ rows_html = "".join(log_lines) if log_lines else '<p style="color:#999">Chưa có sự kiện cho nhóm này. Gửi ảnh + thông tin sản phẩm để kiểm tra.</p>'
960
 
961
  html_content = (
962
+ '<!DOCTYPE html><html><head><title>Zalo Logs - ZGR Group</title>'
963
  '<meta http-equiv="refresh" content="5">'
964
  '<meta name="viewport" content="width=device-width, initial-scale=1">'
965
  '<style>'
 
974
  '.stat-total { background: #e8f0fe; } .stat-total .stat-value { color: #1a73e8; }'
975
  '.log-container { max-height: 650px; overflow-y: auto; background:#fff; border-radius:8px; padding:8px; }'
976
  '</style></head><body>'
977
+ '<h1>Zalo Logs - ZGR Group (zgr-b7e1e71cf5701c2e4561)</h1>'
978
+ '<p class="subtitle">All webhook events from this group</p>'
979
+ '<p>Links: <a href="/logs">All logs</a> | <a href="/gradio/">Main UI</a> | <a href="/proxy-spaces">Proxies</a></p>'
980
  '<div class="stats">'
981
+ '<div class="stat-box stat-total"><div class="stat-value">' + str(total_zgr_logs) + '</div><div class="stat-label">Total Events</div></div>'
982
+ '<div class="stat-box stat-saved"><div class="stat-value">' + str(saved_count) + '</div><div class="stat-label">Saved to Dataset</div></div>'
983
+ '<div class="stat-box stat-error"><div class="stat-value">' + str(error_count) + '</div><div class="stat-label">Errors</div></div>'
984
+ '<div class="stat-box stat-total"><div class="stat-value"><a href="https://huggingface.co/datasets/bep40/zalo-products-all" target="_blank">Dataset</a></div><div class="stat-label">Main Dataset</div></div>'
985
  '</div>'
986
+ '<h2>Events (' + str(total_zgr_logs) + ')</h2>'
987
+ '<div class="log-container">' + rows_html + '</div>'
988
+ '<p style="color:#5f6368;font-size:13px;margin-top:12px">Send image + "Tên sp: ..., Giá: ..., Chuyên mục: ..." to test.</p>'
989
  '</body></html>'
990
  )
991
  return Response(content=html_content, media_type="text/html")
 
995
  async def proxy_spaces_page():
996
  rows = []
997
  for s in reversed(BOT_STATE.get("api_spaces", [])[-20:]):
998
+ repo_name = escape(str(s.get("repo_id", "").split("/")[-1]))
999
+ dataset_id_val = s.get("dataset_id", "")
1000
  dataset_link = "<a href='https://huggingface.co/datasets/" + escape(dataset_id_val) + "' target='_blank'>💾 dataset</a>" if dataset_id_val else ""
1001
  rows.append(
1002
  "<div style='margin:8px 0;padding:12px;background:#fff;border-radius:8px;border-left:4px solid #4CAF50;box-shadow:0 1px 3px rgba(0,0,0,0.1)'>"
1003
  "<div style='display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:space-between'>"
1004
  "<div>"
1005
+ "<b style='color:#1a73e8'>👤 " + escape(str(s.get("sender_name", ""))) + "</b>"
1006
+ "<span style='color:#666'>🆔 " + escape(str(s.get("user_id", ""))) + "</span>"
1007
+ "<span style='color:#4CAF50;font-weight:bold'>[" + escape(str(s.get("status", ""))) + "]</span>"
1008
  "</div>"
1009
  "<div style='display:flex;gap:6px;flex-wrap:wrap'>"
1010
  "<a href='https://huggingface.co/spaces/bep40/" + repo_name + "' target='_blank'>Space</a>"
1011
+ "<a href='" + escape(str(s.get("proxy_url", ""))) + "' target='_blank'>webhook</a>"
1012
+ "<a href='" + escape(str(s.get("proxy_url", "")).replace("/webhooks", "/logs")) + "' target='_blank'>📊 logs</a>"
1013
  + dataset_link +
1014
  "</div>"
1015
  "</div>"
1016
+ "<div style='margin-top:6px'><span style='color:#5f6368'>Repo:</span> <code>" + escape(str(s.get("repo_id", ""))) + "</code></div>"
1017
  "<div style='margin-top:2px;color:#999;font-size:11px'>⏰ " + time.strftime('%Y-%m-%d %H:%M:%S') + "</div>"
1018
  "</div>"
1019
  )
 
1035
  'a { color: #1a73e8; text-decoration: none; cursor: pointer; }'
1036
  'a:hover { text-decoration: underline; }'
1037
  '</style></head><body>'
1038
+ '<h1>Quan ly Proxy Spaces</h1>'
1039
+ '<p class="subtitle">Danh sach cac space proxy da tao.</p>'
1040
+ '<p>Links: <a href="/logs">Logs</a> | <a href="/gradio/">Main UI</a></p>'
1041
  '<div class="stats">'
1042
+ '<div class="stat-box"><div class="stat-value">' + str(total_proxies) + '</div><div class="stat-label">Proxies</div></div>'
1043
+ '<div class="stat-box"><div class="stat-value">' + connected_status + '</div><div class="stat-label">Bot Status</div></div>'
1044
  '</div>'
1045
  '<div class="container">'
1046
  + rows_html +
 
1052
 
1053
  @app.post("/api/delete-proxy/{repo_name}")
1054
  async def delete_proxy(repo_name: str):
1055
+ repo_id = NAMESPACE + "/" + repo_name
1056
  try:
1057
  api = HfApi(token=HF_TOKEN)
1058
  try:
 
1069
  logger.warning("Could not delete dataset %s: %s", dataset_id, e)
1070
  BOT_STATE["api_spaces"] = [s for s in BOT_STATE.get("api_spaces", []) if s.get("repo_id", "").split("/")[-1] != repo_name]
1071
  _save_proxy_spaces()
1072
+ return {"ok": True, "message": "Da xoa proxy " + repo_id}
1073
  except Exception as e:
1074
  logger.error("Delete proxy failed: %s", e)
1075
+ return {"ok": False, "message": "Loi xoa: " + str(e)}
1076
 
1077
 
1078
  with gr.Blocks(title="Zalo Bot Webhook") as demo:
1079
  gr.Markdown("# Zalo Bot Webhook Setup")
1080
  with gr.Tabs():
1081
+ with gr.Tab("Ket noi Bot"):
1082
  tok = gr.Textbox(DEFAULT_BOT_TOKEN, label="Bot Token", type="password")
1083
+ btn = gr.Button("Ket noi")
1084
  res = gr.Markdown("")
1085
  btn.click(fn=connect_bot, inputs=[tok], outputs=res)
1086
+ gr.Textbox(value=get_botinfo, label="Thong tin bot", interactive=False, lines=8)
1087
+ with gr.Tab("Gui tin"):
1088
  with gr.Row():
1089
  cid = gr.Textbox(label="Chat ID")
1090
+ txt = gr.Textbox("HTTP API: 4179413508988279245:abc123", label="Noi dung")
1091
+ b = gr.Button("Gui")
1092
+ b.click(fn=send_msg, inputs=[cid, txt], outputs=gr.Textbox(label="Ket qua"))
1093
+ gr.Textbox(value=get_events, label="Su kien nhan duoc", interactive=False, lines=20)
1094
  gr.Markdown(
1095
+ "Links: [Proxy spaces](/proxy-spaces)\n\n"
1096
+ "Each user sends HTTP API token to create their own proxy space."
 
1097
  )
1098
+ with gr.Tab("Huong dan"):
1099
+ gr.Markdown("1. Go to https://zalo.me/s/botcreator\n2. Copy HTTP API token\n3. Send to bot to auto-create proxy")
1100
 
1101
  demo.queue()
1102
  app = gr.mount_gradio_app(app, demo, path="/gradio")
 
1106
  if __name__ == "__main__":
1107
  port = int(os.getenv("PORT", "7860"))
1108
  server_name = os.getenv("GRADIO_SERVER_NAME", "0.0.0.0")
1109
+ print("[launch] uvicorn on " + server_name + ":" + str(port), flush=True)
1110
  import uvicorn
1111
  uvicorn.run(app, host=server_name, port=port)