Spaces:
Running
Running
Fix SyntaxError: bracket escaping in f-strings for ZGR logs page
Browse files
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
|
| 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 =
|
| 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 = "
|
| 338 |
TARGET_API = "https://bot-api.zaloplatforms.com"
|
| 339 |
-
PROXY_NAME = "
|
| 340 |
HF_TOKEN = os.getenv("HF_TOKEN", "")
|
| 341 |
-
DATASET_ID = "
|
| 342 |
-
MAIN_DATASET_ID = "
|
| 343 |
-
MAIN_SPACE_URL = "
|
| 344 |
-
ZGR_SENDER_ID = "
|
| 345 |
_logs = []
|
| 346 |
|
| 347 |
def _send(cid, text):
|
| 348 |
-
headers = {
|
| 349 |
-
url =
|
| 350 |
-
return requests.post(url, json={
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 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 =
|
| 371 |
-
meta_filename =
|
| 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=
|
| 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 = {
|
| 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=
|
| 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 =
|
| 424 |
-
meta_filename =
|
| 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=
|
| 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 = {
|
| 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=
|
| 455 |
finally:
|
| 456 |
pathlib.Path(tmp_path).unlink(missing_ok=True)
|
| 457 |
-
_log("dataset_saved", sender_id, "N/A",
|
| 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 {
|
| 466 |
|
| 467 |
@app.get("/health")
|
| 468 |
async def health():
|
| 469 |
-
return {
|
| 470 |
|
| 471 |
@app.get("/webhooks")
|
| 472 |
async def webhooks_get():
|
| 473 |
-
return Response(content=json.dumps({
|
| 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({
|
| 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,
|
| 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 =
|
| 530 |
else:
|
| 531 |
-
reply =
|
| 532 |
else:
|
| 533 |
-
reply =
|
| 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({
|
| 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
|
| 553 |
-
zgr_badge = "
|
| 554 |
-
|
| 555 |
-
|
|
|
|
|
|
|
| 556 |
'''
|
| 557 |
|
| 558 |
-
readme =
|
| 559 |
-
title: Zalo Proxy
|
| 560 |
colorFrom: blue
|
| 561 |
colorTo: purple
|
| 562 |
sdk: docker
|
| 563 |
app_port: 7860
|
| 564 |
---
|
| 565 |
|
| 566 |
-
|
| 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("
|
| 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(
|
| 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 =
|
| 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
|
| 638 |
try:
|
| 639 |
me = zapi.get_me()
|
| 640 |
if not me.get("ok"):
|
| 641 |
-
return
|
| 642 |
BOT_STATE["bot_info"] = me.get("result", {})
|
| 643 |
-
bot_name = BOT_STATE["bot_info"].get("name", "?")
|
| 644 |
except Exception as e:
|
| 645 |
-
return
|
| 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
|
| 654 |
-
return
|
| 655 |
except Exception as e:
|
| 656 |
-
return
|
| 657 |
|
| 658 |
|
| 659 |
def send_msg(cid: str, text: str):
|
| 660 |
if not BOT_STATE["connected"]:
|
| 661 |
-
return "
|
| 662 |
if not cid or not text:
|
| 663 |
-
return "
|
| 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
|
| 669 |
|
| 670 |
|
| 671 |
def get_botinfo():
|
| 672 |
if BOT_STATE["bot_info"]:
|
| 673 |
info = BOT_STATE["bot_info"]
|
| 674 |
-
lines = [
|
| 675 |
if BOT_STATE.get("webhook_url"):
|
| 676 |
-
lines.append(
|
| 677 |
-
lines.append(
|
| 678 |
-
lines.append(f"Chat ID lưu: `{BOT_STATE.get('last_chat_id', '')}`")
|
| 679 |
return "\n".join(lines)
|
| 680 |
-
return "
|
| 681 |
|
| 682 |
|
| 683 |
def get_events():
|
| 684 |
if not BOT_STATE["logs"]:
|
| 685 |
-
return "
|
| 686 |
lines = []
|
| 687 |
for i, l in enumerate(BOT_STATE["logs"][-20:][::-1], 1):
|
| 688 |
-
is_zgr =
|
| 689 |
zgr_tag = " [ZGR]" if is_zgr else ""
|
| 690 |
-
lines.append(
|
|
|
|
|
|
|
| 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 "
|
| 698 |
lines = []
|
| 699 |
for i, s in enumerate(spaces[-10:][::-1], 1):
|
| 700 |
-
lines.append(
|
|
|
|
|
|
|
| 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 |
-
"
|
| 711 |
-
"1
|
| 712 |
-
"
|
| 713 |
-
"
|
| 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, "
|
| 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 =
|
| 790 |
-
dataset_url =
|
| 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":
|
| 811 |
})
|
| 812 |
except Exception as e:
|
| 813 |
logger.error("Failed: %s", e)
|
| 814 |
try:
|
| 815 |
-
ZaloBotAPI(BOT_STATE["bot_token"]).send_message(cid,
|
| 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
|
| 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:
|
| 832 |
-
|
|
|
|
|
|
|
| 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":
|
| 856 |
})
|
| 857 |
|
| 858 |
-
reply =
|
| 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
|
| 892 |
header_color = "#4CAF50" if is_zgr else "#1a73e8"
|
| 893 |
zgr_badge = " [ZGR]" if is_zgr else ""
|
| 894 |
-
status_badge = "
|
| 895 |
-
weight = "bold" if is_zgr else "normal"
|
| 896 |
log_lines.append(
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 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 |
-
|
|
|
|
| 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>
|
| 935 |
-
'<p class="subtitle">
|
| 936 |
-
'<p>
|
| 937 |
'<div class="stats">'
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
'</div>'
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
'<p><a href="/logs/zgr-b7e1e71cf5701c2e4561">
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 969 |
log_lines.append(
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 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 |
-
|
| 991 |
|
| 992 |
html_content = (
|
| 993 |
-
'<!DOCTYPE html><html><head><title>Zalo Logs -
|
| 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>
|
| 1009 |
-
'<p class="subtitle">
|
| 1010 |
-
'<p>
|
| 1011 |
'<div class="stats">'
|
| 1012 |
-
|
| 1013 |
-
|
| 1014 |
-
|
| 1015 |
-
'<div class="stat-box stat-total"><div class="stat-value"><a href="https://huggingface.co/datasets/bep40/zalo-products-all" target="_blank">
|
| 1016 |
'</div>'
|
| 1017 |
-
|
| 1018 |
-
|
| 1019 |
-
'<p style="color:#5f6368;font-size:13px;margin-top:12px">
|
| 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(
|
| 1030 |
-
dataset_id_val = s.get(
|
| 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(
|
| 1037 |
-
"<span style='color:#666'>🆔
|
| 1038 |
-
"<span style='color:#4CAF50;font-weight:bold'>[" + escape(str(s.get(
|
| 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(
|
| 1043 |
-
"<a href='" + escape(str(s.get(
|
| 1044 |
+ dataset_link +
|
| 1045 |
"</div>"
|
| 1046 |
"</div>"
|
| 1047 |
-
"<div style='margin-top:6px'><span style='color:#5f6368'>Repo:</span> <code>" + escape(str(s.get(
|
| 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>
|
| 1070 |
-
'<p class="subtitle">Danh
|
| 1071 |
-
'<p>
|
| 1072 |
'<div class="stats">'
|
| 1073 |
-
|
| 1074 |
-
|
| 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 =
|
| 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":
|
| 1104 |
except Exception as e:
|
| 1105 |
logger.error("Delete proxy failed: %s", e)
|
| 1106 |
-
return {"ok": False, "message":
|
| 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("
|
| 1113 |
tok = gr.Textbox(DEFAULT_BOT_TOKEN, label="Bot Token", type="password")
|
| 1114 |
-
btn = gr.Button("
|
| 1115 |
res = gr.Markdown("")
|
| 1116 |
btn.click(fn=connect_bot, inputs=[tok], outputs=res)
|
| 1117 |
-
gr.Textbox(value=get_botinfo, label="
|
| 1118 |
-
with gr.Tab("
|
| 1119 |
with gr.Row():
|
| 1120 |
cid = gr.Textbox(label="Chat ID")
|
| 1121 |
-
txt = gr.Textbox("HTTP API: 4179413508988279245:abc123", label="
|
| 1122 |
-
b = gr.Button("
|
| 1123 |
-
b.click(fn=send_msg, inputs=[cid, txt], outputs=gr.Textbox(label="
|
| 1124 |
-
gr.Textbox(value=get_events, label="
|
| 1125 |
gr.Markdown(
|
| 1126 |
-
"
|
| 1127 |
-
"
|
| 1128 |
-
"setup webhook + auto trả lời tin nhắn."
|
| 1129 |
)
|
| 1130 |
-
with gr.Tab("
|
| 1131 |
-
gr.Markdown("
|
| 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(
|
| 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)
|