import os, sys, json, secrets, logging, asyncio, re, time, threading, base64, tempfile, pathlib from html import escape logging.basicConfig(level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("zalo-bot") import requests import gradio as gr from fastapi import FastAPI, Request, Response from starlette.responses import RedirectResponse from huggingface_hub import HfApi, SpaceStage, hf_hub_download, upload_file DEFAULT_BOT_TOKEN = os.getenv( "DEFAULT_BOT_TOKEN", "4179413508988279245:DmcFvOoFHHGiISQtmInHFchHwqfmAsaNWxhENixtvawrerrMGALunAbfhBvOzUcc", ) WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET", "") or secrets.token_urlsafe(32)[:128] SPACE_ID = os.getenv("SPACE_ID", "") HF_TOKEN = os.getenv("HF_TOKEN", os.getenv("HF_API_TOKEN", "")) if not HF_TOKEN: try: from huggingface_hub import get_token HF_TOKEN = get_token() or "" except Exception: HF_TOKEN = "" NAMESPACE = os.getenv("HF_NAMESPACE", "bep40") MAIN_DATASET_ID = os.getenv("MAIN_DATASET_ID", f"{NAMESPACE}/zalo-products-all") ZGR_SENDER_ID = "zgr-b7e1e71cf5701c2e4561" OCR_MODEL_ID = os.getenv("OCR_MODEL_ID", "5CD-AI/Vintern-1B-v3_5") if not HF_TOKEN: logger.warning("[startup] HF_TOKEN not found — Space creation features will fail until HF_TOKEN secret is set") logger.info("[startup] SPACE_ID=%s NAMESPACE=%s OCR_MODEL=%s", SPACE_ID or "(local)", NAMESPACE, OCR_MODEL_ID) BOT_STATE = { "bot_token": DEFAULT_BOT_TOKEN, "webhook_url": "", "webhook_secret": WEBHOOK_SECRET, "connected": False, "bot_info": {}, "logs": [], "api_spaces": [], "last_chat_id": "", "last_sender_id": "", } def _load_proxy_spaces(): if not SPACE_ID or not HF_TOKEN: return try: file_path = hf_hub_download( repo_id=SPACE_ID, filename="proxy_spaces.json", repo_type="space", token=HF_TOKEN, ) with open(file_path) as f: data = json.load(f) BOT_STATE["api_spaces"] = data.get("api_spaces", []) logger.info("Loaded %d proxy spaces from disk", len(BOT_STATE["api_spaces"])) except Exception as e: logger.info("No persisted proxy data yet: %s", e) def _save_proxy_spaces(): if not SPACE_ID or not HF_TOKEN: return try: data = {"api_spaces": BOT_STATE.get("api_spaces", [])} with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp: json.dump(data, tmp, indent=2, default=str) tmp_path = tmp.name upload_file( path_or_fileobj=tmp_path, path_in_repo="proxy_spaces.json", repo_id=SPACE_ID, repo_type="space", token=HF_TOKEN, commit_message="Update proxy spaces list", ) pathlib.Path(tmp_path).unlink(missing_ok=True) logger.info("Saved %d proxy spaces to disk", len(BOT_STATE.get("api_spaces", []))) except Exception as e: logger.error("Failed to save proxy spaces: %s", e) _load_proxy_spaces() def _save_chat_id(cid: str, sender_id: str): if cid: BOT_STATE["last_chat_id"] = cid if sender_id: BOT_STATE["last_sender_id"] = sender_id class ZaloBotAPI: BASE_URL = "https://bot-api.zaloplatforms.com" def __init__(self, bt: str): self.bt = bt assert ":" in bt, "FAIL-FAST: sai dinh dang token" self.api_base = f"{self.BASE_URL}/bot{bt}" self.headers = {"Content-Type": "application/json"} def get_me(self): return requests.post(f"{self.api_base}/getMe", headers=self.headers, timeout=15).json() def set_webhook(self, url: str, secret: str): return requests.post( f"{self.api_base}/setWebhook", json={"url": url, "secret_token": secret}, headers=self.headers, timeout=15, ).json() def send_message(self, cid: str, text: str): return requests.post( f"{self.api_base}/sendMessage", json={"chat_id": cid, "text": text, "parse_mode": "markdown"}, headers=self.headers, timeout=15, ).json() def get_webhook_url(): if SPACE_ID: slug = SPACE_ID.replace("/", "-").replace("_", "-") return f"https://{slug}.hf.space/webhooks" port = os.getenv("PORT", "7860") return f"http://localhost:{port}/webhooks" def _extract_user_token(text: str): m = re.search(r'HTTP\s*API\s*:\s*(\S+:\S+)', text, re.IGNORECASE) if m: return m.group(1).strip() m = re.search(r'(\d+:[A-Za-z0-9_-]+)', text) if m: return m.group(1) return None def _safe_space_name(user_id: str) -> str: return re.sub(r'[^a-zA-Z0-9-]', '', str(user_id))[:40] def _get_user_dataset_id(user_id: str) -> str: safe_id = _safe_space_name(user_id) if not safe_id: safe_id = "default" return f"{NAMESPACE}/{safe_id}-zalo-data" def _is_zgr_sender_local(sender_id): sid = str(sender_id) return ZGR_SENDER_ID in sid or sid == ZGR_SENDER_ID def _ensure_user_dataset(user_id: str, token: str) -> tuple: if not HF_TOKEN: raise RuntimeError("HF_TOKEN chưa được cấu hình.") api = HfApi(token=HF_TOKEN) dataset_id = _get_user_dataset_id(user_id) created = False try: api.dataset_info(dataset_id) logger.info("Dataset %s already exists", dataset_id) except Exception: with tempfile.TemporaryDirectory() as tmp: readme_path = pathlib.Path(tmp, "README.md") readme_path.write_text( f"# Zalo Product Data — {user_id}\n\n" f"Dữ liệu sản phẩm thu thập từ Zalo chat.\n\n" f"## Cấu trúc (schema)\n" f"| image | ảnh (binary/URL) | Hình ảnh sản phẩm |\n" f"| product_name | text | Tên sản phẩm |\n" f"| description | text | Nội dung mô tả |\n" f"| price | number | Giá sản phẩm |\n" f"| category | text | Chuyên mục |\n" f"| sender_id | text | ID người gửi |\n" f"| sender_name | text | Tên người gửi |\n" f"| timestamp | text | Thời gian ghi nhận |\n" ) try: api.create_repo(repo_id=dataset_id, repo_type="dataset", exist_ok=True) api.upload_file( path_or_fileobj=str(readme_path), path_in_repo="README.md", repo_id=dataset_id, repo_type="dataset", token=HF_TOKEN, commit_message="Initial dataset readme", ) created = True logger.info("Created dataset %s", dataset_id) except Exception as e: err_msg = str(e) if "429" in err_msg or "rate limit" in err_msg: raise RuntimeError("⚠️ Đã đạt giới hạn tạo repo (20/ngày). Vui lòng thử lại sau 24h.") if "already exist" in err_msg or "conflict" in err_msg: logger.info("Dataset %s already exists, skipping create", dataset_id) else: raise return dataset_id, created def _save_product_to_main_dataset(image_url, image_data_b64, description, price, category, sender_id, sender_name, product_name="", chat_id="", timestamp="", img_bytes=None): if not HF_TOKEN or not MAIN_DATASET_ID: logger.warning("MAIN_DATASET_ID or HF_TOKEN not configured") return None try: api = HfApi(token=HF_TOKEN) file_ts = timestamp or time.strftime("%Y%m%d_%H%M%S") rec_ts = time.strftime("%Y-%m-%d %H:%M:%S") safe_sender = _safe_space_name(sender_id) or "unknown" img_filename = f"images/{file_ts}_{safe_sender}.jpg" meta_filename = f"data/{file_ts}_{safe_sender}.json" if img_bytes is None: img_bytes = None if image_data_b64: try: img_bytes = base64.b64decode(image_data_b64) except Exception: img_bytes = None elif image_url: try: r = requests.get(image_url, timeout=15) img_bytes = r.content except Exception as e: logger.error("Image download failed: %s", e) uploaded_img = None if img_bytes: with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp: tmp.write(img_bytes) tmp_path = tmp.name try: 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}", ) uploaded_img = img_filename except Exception as e: logger.error("Image upload to main dataset failed: %s", e) finally: pathlib.Path(tmp_path).unlink(missing_ok=True) 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), "timestamp": rec_ts, } with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp: json.dump(record, tmp, indent=2, ensure_ascii=False) tmp_path = tmp.name try: 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}", ) finally: pathlib.Path(tmp_path).unlink(missing_ok=True) logger.info("Saved product to main dataset: %s", MAIN_DATASET_ID) return MAIN_DATASET_ID except Exception as e: logger.error("Failed to save to main dataset: %s", e) return None def _save_text_message_to_dataset(text, description, price, category, sender_id, sender_name, chat_id): """Save a text-only message to the main Zalo products dataset.""" try: from huggingface_hub import HfApi api = HfApi() filename_ts = time.strftime("%Y%m%d_%H%M%S") safe_name = re.sub(r'[^a-zA-Z0-9_\-]', '_', str(sender_name)[:30]) if sender_name else "" file_name = f"{filename_ts}_{safe_name}_text.json" upload_path = "data/" + file_name record = { "text": text, "description": description[:500] if description else text[:500], "price": price or "", "category": category or "", "sender_id": str(sender_id), "sender_name": str(sender_name), "chat_id": str(chat_id), "image": "", "product_name": "", "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), "message_type": "text", } json_content = json.dumps(record, ensure_ascii=False, indent=2) api.upload_file( path=upload_path, path_in_repo=upload_path, repo_id=MAIN_DATASET_ID, repo_type="dataset", token=HF_TOKEN, commit_message=f"Add text message from {sender_name}", ) logger.info("Text message saved to dataset: %s", file_name) return upload_path except Exception as e: logger.error("Failed to save text to dataset: %s", e) return None def _ocr_extract_text(image_bytes): """Use HF Inference API with Vietnamese OCR model (Vintern-1B) to extract text from image.""" try: from huggingface_hub import InferenceClient client = InferenceClient(model=OCR_MODEL_ID, token=HF_TOKEN) # Convert bytes to PIL image from PIL import Image import io img = Image.open(io.BytesIO(image_bytes)).convert("RGB") prompt = "\nTrích xuất toàn bộ văn bản trong hình ảnh và trả về dưới dạng markdown." result = client.chat_completion( messages=[{"role": "user", "content": [{"type": "text", "text": prompt}, {"type": "image_url", "image_url": img}]}], max_tokens=2048, ) ocr_text = result.choices[0].message.content.strip() logger.info("OCR extracted %d chars of text", len(ocr_text)) return ocr_text except Exception as e: logger.error("OCR extraction failed: %s", e) return "" def _parse_ocr_product_info(ocr_text): """Parse OCR-extracted text to extract product fields.""" product_name, description, price, category = "", "", "", "" # Try to extract price (Vietnamese đồng format: số, số, hoặc số vnđ) price_match = re.search(r'(\d{1,3}(?:[.,]\d{3})*(?:[.,]\d{2,3})?(?:\s*(?:đ|vnđ|VND|dong))?)', ocr_text, re.IGNORECASE) if price_match: price = price_match.group(1) # Try to extract category from common keywords for kw in ["Chuyên mục", "Danh mục", "Loại", "Category"]: m = re.search(kw + r'[:\s]*([^\n]+)', ocr_text, re.IGNORECASE) if m: category = m.group(1).strip() break # Try to extract product name from common keywords for kw in ["Tên sp", "Tên sản phẩm", "Product", "Tên hàng"]: m = re.search(kw + r'[:\s]*([^\n]+)', ocr_text, re.IGNORECASE) if m: product_name = m.group(1).strip() break # Use remaining text as description desc_text = ocr_text for kw in ["Chuyên mục", "Danh mục", "Loại", "Category", "Tên sp", "Tên sản phẩm", "Product", "Tên hàng", "Giá", "gia", "Price"]: desc_text = re.sub(kw + r'[:\s]*[^\n]+', '', desc_text, flags=re.IGNORECASE) description = desc_text.strip()[:500] if desc_text.strip() else "" return product_name, description, price, category def _create_api_proxy_space(token, user_id, sender_display): safe_id = _safe_space_name(user_id) token_suffix = token.split(":")[-1][:12] if ":" in token else re.sub(r'\W', '', token[:12]) unique_key = safe_id if safe_id else "u" + token_suffix space_name = f"zalo-proxy-{unique_key}" repo_id = f"{NAMESPACE}/{space_name}" dataset_id = f"{NAMESPACE}/{unique_key}-zalo-data" logger.info("Creating proxy space %s for user %s", repo_id, sender_display) if not HF_TOKEN: raise RuntimeError("HF_TOKEN secret chưa được cấu hình cho Space.") api = HfApi(token=HF_TOKEN) dockerfile = """FROM python:3.12-slim RUN useradd -m -u 1000 user USER user ENV HOME=/home/user ENV PATH=/home/user/.local/bin:$PATH WORKDIR $HOME/app COPY --chown=user requirements.txt . RUN pip install --user --no-cache-dir -r requirements.txt COPY --chown=user app.py . EXPOSE 7860 CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"] """ requirements = "fastapi>=0.111.0\nuvicorn[standard]>=0.30.0\nrequests>=2.32.0\nhuggingface_hub>=0.30.0\n" app_py = '''import os, json, requests, time, re, base64, tempfile, pathlib, sys from html import escape as _escape class _StderrLogger: def __init__(self): self._log = [] def write(self, s): if s.strip(): self._log.append(s) sys.__stderr__.write(s) def flush(self): pass sys.stderr = _StderrLogger() from fastapi import FastAPI, Request, Response app = FastAPI(title="Zalo Proxy Space") BOT_TOKEN = "''' + token + '''" TARGET_API = "https://bot-api.zaloplatforms.com" PROXY_NAME = "''' + sender_display + '''" HF_TOKEN = os.getenv("HF_TOKEN", "") DATASET_ID = "''' + dataset_id + '''" MAIN_DATASET_ID = "''' + MAIN_DATASET_ID + '''" MAIN_SPACE_URL = "''' + SPACE_ID.replace("/", "-") + '''.hf.space" ZGR_SENDER_ID = "zgr-b7e1e71cf5701c2e4561" _logs = [] def _send(cid, text): headers = {"Content-Type": "application/json"} url = TARGET_API + "/bot" + BOT_TOKEN + "/sendMessage" return requests.post(url, json={"chat_id": cid, "text": text}, headers=headers) def _safe_name(name): return re.sub(r'[^a-zA-Z0-9]', '_', str(name))[:30] def _is_zgr_sender(sender_id): sid = str(sender_id) return ZGR_SENDER_ID in sid or sid == ZGR_SENDER_ID def _log(event, sender_id, chat_id, text, sender_name="", chat_type=""): 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")} _logs.append(entry) 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) if len(_logs) > 200: del _logs[:100] _log("startup", "system", "SYSTEM", "Proxy space initialized. PROXY_NAME=" + PROXY_NAME) def _save_to_main_dataset(image_url, image_data_b64, description, price, category, sender_id, sender_name, product_name="", chat_id="", text_message=None): _log("main_dataset_save_start", sender_id, chat_id, "product_name=" + str(product_name) + " price=" + str(price)) if not HF_TOKEN or not MAIN_DATASET_ID: _log("main_dataset_skip", sender_id, chat_id, "HF_TOKEN or MAIN_DATASET_ID missing") return None try: from huggingface_hub import HfApi api = HfApi(token=HF_TOKEN) file_ts = time.strftime("%Y%m%d_%H%M%S") rec_ts = time.strftime("%Y-%m-%d %H:%M:%S") safe_sender = _safe_name(sender_id) or "unknown" if text_message: img_filename = "" meta_filename = "data/" + file_ts + "_" + safe_sender + "_text.json" else: img_filename = "images/" + file_ts + "_" + safe_sender + ".jpg" meta_filename = "data/" + file_ts + "_" + safe_sender + ".json" img_bytes = None if image_data_b64: try: img_bytes = base64.b64decode(image_data_b64) except Exception: img_bytes = None elif image_url: try: r = requests.get(image_url, timeout=15) img_bytes = r.content _log("image_downloaded_from_url", sender_id, chat_id, image_url[:100]) except Exception as e: _log("image_download_fail", sender_id, chat_id, str(e)) uploaded_img = None if img_bytes: with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp: tmp.write(img_bytes) tmp_path = tmp.name try: 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) uploaded_img = img_filename _log("image_uploaded", sender_id, chat_id, img_filename) except Exception as e: _log("image_upload_fail", sender_id, chat_id, str(e)) finally: pathlib.Path(tmp_path).unlink(missing_ok=True) if text_message: record = {"image": "", "product_name": str(product_name)[:200] if product_name else "", "category": str(category) if category else "", "description": str(description)[:500] if description else str(text_message)[:500], "price": str(price) if price 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": rec_ts, "text": str(text_message)[:1000], "message_type": "text"} else: record = {"image": uploaded_img, "product_name": str(product_name)[:200] if product_name else "", "category": str(category) if category else "", "description": str(description)[:500] if description else "", "price": str(price) if price 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": rec_ts} with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp: json.dump(record, tmp, indent=2, ensure_ascii=False) tmp_path = tmp.name try: 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) _log("dataset_save_to_main", sender_id, chat_id, "OK") except Exception as e: _log("dataset_save_fail", sender_id, chat_id, str(e)) finally: pathlib.Path(tmp_path).unlink(missing_ok=True) return MAIN_DATASET_ID except Exception as e: _log("main_dataset_error", sender_id, chat_id, str(e)) return None def _save_to_dataset(image_url, image_data_b64, description, price, category, sender_id, sender_name): if not HF_TOKEN or not DATASET_ID: _log("dataset_skip", sender_id, "N/A", "HF_TOKEN or DATASET_ID missing") return None try: from huggingface_hub import HfApi api = HfApi(token=HF_TOKEN) file_ts = time.strftime("%Y%m%d_%H%M%S") rec_ts = time.strftime("%Y-%m-%d %H:%M:%S") safe_sender = _safe_name(sender_id) or "unknown" img_filename = "images/" + file_ts + "_" + safe_sender + ".jpg" meta_filename = "data/" + file_ts + "_" + safe_sender + ".json" img_bytes = None if image_data_b64: try: img_bytes = base64.b64decode(image_data_b64) except Exception: img_bytes = None elif image_url: try: r = requests.get(image_url, timeout=15) img_bytes = r.content except Exception as e: _log("image_download_fail", sender_id, "N/A", str(e)) uploaded_img = None if img_bytes: with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp: tmp.write(img_bytes) tmp_path = tmp.name try: 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) uploaded_img = img_filename except Exception as e: _log("image_upload_fail", sender_id, "N/A", str(e)) finally: pathlib.Path(tmp_path).unlink(missing_ok=True) record = {"image": uploaded_img, "product_name": "", "category": str(category) if category else "", "description": str(description)[:500] if description else "", "price": str(price) if price else "", "sender_id": str(sender_id), "sender_name": str(sender_name), "timestamp": rec_ts} with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp: json.dump(record, tmp, indent=2, ensure_ascii=False) tmp_path = tmp.name try: 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) finally: pathlib.Path(tmp_path).unlink(missing_ok=True) _log("dataset_saved", sender_id, "N/A", "Saved to " + DATASET_ID) return DATASET_ID except Exception as e: _log("dataset_error", sender_id, "N/A", str(e)) return None @app.get("/") async def root(): return {"status": "ok"} @app.get("/health") async def health(): return {"status": "ok", "dataset": DATASET_ID, "zgr_sender": ZGR_SENDER_ID, "is_zgr": _is_zgr_sender(ZGR_SENDER_ID)} @app.get("/webhooks") async def webhooks_get(): return Response(content=json.dumps({"message": "Success"}), media_type="application/json", status_code=200) @app.post("/webhooks") async def webhooks(request: Request): body = await request.body() body_str = body.decode("utf-8") if body else "" _log("webhook_received", "N/A", "N/A", "Body length: " + str(len(body_str))) try: data = json.loads(body_str) except Exception as e: _log("parse_error", "N/A", "N/A", "Bad JSON: " + str(e) + " | body=" + body_str[:200]) return Response(content=json.dumps({"message": "Bad JSON"}), media_type="application/json", status_code=400) result = data.get("result", data) event = result.get("event_name", "unknown") msg = result.get("message", {}) sender = msg.get("from", {}) chat = msg.get("chat", {}) text = msg.get("text", "") sender_id = str(sender.get("id", "")) sender_name = sender.get("display_name") or sender.get("name") or sender_id chat_id = str(chat.get("id", "")) chat_type = str(chat.get("chat_type", "")) attachments = msg.get("attachment", {}) image_url = "" image_data_b64 = "" if attachments: payload = attachments.get("payload", {}) if isinstance(payload, str): try: payload = json.loads(payload) except Exception: payload = {} image_url = payload.get("url", "") or msg.get("photo", "") or msg.get("photo_url", "") or msg.get("image_url", "") image_data_b64 = payload.get("data", "") or msg.get("image", "") else: image_url = msg.get("photo", "") or msg.get("photo_url", "") or msg.get("image_url", "") image_data_b64 = msg.get("image", "") is_zgr = _is_zgr_sender(sender_id) _log(event, sender_id, chat_id, text, sender_name, chat_type) _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(image_url[:100])) if event == "message.text.received" and chat_id: description, price, category, product_name = "", "", "", "" desc_match = re.search(r'(?:mo ta|description|desc|mota)[:\\\\s]*([^|\\n]+)', text, re.IGNORECASE) price_match = re.search(r'(?:gia|price|don gia|donggia)[:\\\\s]*([\\d,.]+)', text, re.IGNORECASE) cat_match = re.search(r'(?:chuyen muc|category|danh muc|loai)[:\\s]*([^|\\n]+?)(?:$|\\n)', text, re.IGNORECASE) name_match = re.search(r'(?:ten sp|ten san pham|product name|name)[:\\s]*([^|\\n]+)', text, re.IGNORECASE) if name_match: product_name = name_match.group(1).strip() if desc_match: description = desc_match.group(1).strip() if price_match: price = price_match.group(1).strip() if cat_match: category = cat_match.group(1).strip() if image_url or image_data_b64 or product_name or description or price or category: 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) _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) if dataset_id: reply = "GOT IT! Product saved!" else: reply = "GOT IT! Saved to main dataset!" elif is_zgr and text: # Save all text messages from ZGR group to main dataset _save_to_main_dataset( image_url=image_url, image_data_b64=image_data_b64, description=text[:200], price=price, category=category, sender_id=sender_id, sender_name=sender_name, product_name=product_name, chat_id=chat_id, text_message=text, ) reply = "👋 Xin chào " + str(sender_name) + " (Zalo ID: " + str(sender_id) + ")!\n\n" + HELP_INSTRUCTIONS else: reply = "Hi! Send image + product info to save." try: _send(chat_id, reply) except Exception as e: _log("send_reply_fail", sender_id, chat_id, str(e)) elif event == "message.image.received" and chat_id: description, price, category, product_name = "", "", "", "" desc_match = re.search(r'(?:mo ta|description|desc|mota)[:\\\\s]*([^|\\n]+)', text, re.IGNORECASE) price_match = re.search(r'(?:gia|price|don gia|donggia)[:\\\\s]*([\\d,.]+)', text, re.IGNORECASE) cat_match = re.search(r'(?:chuyen muc|category|danh muc|loai)[:\\s]*([^|\\n]+?)(?:$|\\n)', text, re.IGNORECASE) name_match = re.search(r'(?:ten sp|ten san pham|product name|name)[:\\s]*([^|\\n]+)', text, re.IGNORECASE) if name_match: product_name = name_match.group(1).strip() if desc_match: description = desc_match.group(1).strip() if price_match: price = price_match.group(1).strip() if cat_match: category = cat_match.group(1).strip() log_text = "photo_url=" + str(image_url[:100]) if image_url else "No photo_url in message" if text: log_text += " | caption=" + str(text[:200]) if image_url: 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) saved = _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) if dataset_id: reply = "GOT IT! Product saved!" else: reply = "GOT IT! Saved to main dataset!" _log("image_saved", sender_id, chat_id, log_text) try: _send(chat_id, reply) except Exception as e: _log("send_reply_fail", sender_id, chat_id, str(e)) else: _log("image_no_url", sender_id, chat_id, log_text) return Response(content=json.dumps({"message": "Success"}), media_type="application/json", status_code=200) @app.get("/logs") async def proxy_logs(): rows = "" for log in reversed(_logs[-100:]): is_zgr = _is_zgr_sender(log.get("sender_id", "")) bg = "#e8f5e9" if is_zgr else "#ffffff" zgr_badge = "[ZGR] " if is_zgr else "" chat_type_val = _escape(str(log.get("chat_type", ""))) rows += "
" + zgr_badge + "[" + _escape(str(log["event"])) + "] " + _escape(str(log.get("sender_name",""))) + " ID:" + _escape(str(log.get("sender_id",""))) + " chat:" + _escape(str(log.get("chat_id",""))) + " type:[" + chat_type_val + "]
" + _escape(str(log.get("text",""))[:300]) + "
" + _escape(str(log.get("time",""))) + "
" html_content = "Proxy Logs

Proxy Logs - " + _escape(PROXY_NAME) + "

" + rows + "
" return Response(content=html_content, media_type="text/html") ''' readme = """--- title: Zalo Proxy colorFrom: blue colorTo: purple sdk: docker app_port: 7860 --- Zalo Webhook Proxy Space """ with tempfile.TemporaryDirectory() as tmp: pathlib.Path(tmp, "Dockerfile").write_text(dockerfile) pathlib.Path(tmp, "app.py").write_text(app_py) pathlib.Path(tmp, "requirements.txt").write_text(requirements) pathlib.Path(tmp, "README.md").write_text(readme) try: existing = api.space_info(repo_id=repo_id) logger.info("Proxy space %s already exists (stage=%s)", repo_id, existing.stage) except Exception: try: api.create_repo(repo_id=repo_id, repo_type="space", space_sdk="docker", exist_ok=True) logger.info("Created new proxy space repo: %s", repo_id) except Exception as e: err_msg = str(e).lower() if "429" in err_msg or "rate limit" in err_msg: raise RuntimeError("Rate limit. Try again later.") if "already exist" in err_msg or "conflict" in err_msg: logger.info("Proxy space %s already exists, skipping create_repo", repo_id) else: raise try: api.upload_folder(folder_path=tmp, repo_id=repo_id, repo_type="space", commit_message="Initial proxy space") except Exception as e: err_msg = str(e) if "404" in err_msg or "Repository Not Found" in err_msg: raise RuntimeError("Proxy repo error") raise try: api.wait_for_space(repo_id=repo_id, expected_stage=SpaceStage.RUNNING, timeout=180) status = "RUNNING" except Exception as e: logger.warning("wait_for_space timeout: %s", e) try: rt = api.get_space_runtime(repo_id=repo_id) status = str(rt.stage) except Exception: status = "UNKNOWN" proxy_url = "https://" + repo_id.replace('/', '-') + ".hf.space/webhooks" logger.info("Proxy space ready: %s -> %s (status=%s)", repo_id, proxy_url, status) return repo_id, proxy_url, status def _set_user_webhook(user_token, proxy_url, secret): try: zapi = ZaloBotAPI(user_token) result = zapi.set_webhook(proxy_url, secret) logger.info("setWebhook result: %s", result) return result except Exception as e: logger.error("setWebhook failed: %s", e) return {"ok": False, "message": str(e)} def connect_bot(token: str): if not token: return "Nhap Bot Token" BOT_STATE["bot_token"] = token BOT_STATE["connected"] = False BOT_STATE["bot_info"] = {} try: zapi = ZaloBotAPI(token) except AssertionError as e: return "Token sai: " + str(e) try: me = zapi.get_me() if not me.get("ok"): return "That bai: " + str(me.get('message', '')) BOT_STATE["bot_info"] = me.get("result", {}) except Exception as e: return "Loi: " + str(e) wh = get_webhook_url() sc = BOT_STATE["webhook_secret"] try: sw = zapi.set_webhook(wh, sc) if sw.get("ok"): BOT_STATE["webhook_url"] = wh BOT_STATE["connected"] = True return "Ket noi thanh cong! Webhook: " + wh return "setWebhook that bai: " + str(sw.get('message', '')) except Exception as e: return "Loi: " + str(e) def send_msg(cid: str, text: str): if not BOT_STATE["connected"]: return "Chua ket noi bot." if not cid or not text: return "Nhap Chat ID va Noi dung" try: result = ZaloBotAPI(BOT_STATE["bot_token"]).send_message(cid, text) return json.dumps(result, indent=2, ensure_ascii=False) except Exception as e: return "Loi: " + str(e) def get_botinfo(): if BOT_STATE["bot_info"]: info = BOT_STATE["bot_info"] lines = ["Ten bot: " + str(info.get('name', '?')), "ID: " + str(info.get('id', ''))] if BOT_STATE.get("webhook_url"): lines.append("Webhook: " + BOT_STATE["webhook_url"]) lines.append("Ket noi: " + str(BOT_STATE.get("connected", False))) return "\n".join(lines) return "Chua ket noi" def get_events(): if not BOT_STATE["logs"]: return "Chua co su kien" lines = [] for i, l in enumerate(BOT_STATE["logs"][-20:][::-1], 1): is_zgr = _is_zgr_sender_local(l.get("sender_id", "")) zgr_tag = " [ZGR]" if is_zgr else "" lines.append("{}. [{}] {} Zalo:{} ID:{} chat:{} | {}".format( i, l.get('event',''), zgr_tag, l.get('sender_name',''), l.get('sender_id',''), l.get('chat_id',''), str(l.get('text','')[:50]) )) return "\n".join(lines) def get_proxy_spaces(): spaces = BOT_STATE.get("api_spaces", []) if not spaces: return "Chua co proxy space nao" lines = [] for i, s in enumerate(spaces[-10:][::-1], 1): lines.append("{}. {} ID:{} Space:{} Webhook:{} Status:{}".format( i, s.get('sender_name',''), s.get('user_id',''), s.get('repo_id',''), s.get('proxy_url',''), s.get('status','') )) return "\n".join(lines) HELP_INSTRUCTIONS = ( "🎓 HƯỚNG DẪN CẤU HÌNH ZALO BOT CHI TIẾT\n\n" "1️⃣ Cách đặt tên Zalobot (QUAN TRỌNG):\n" "• Tên bot không được chứa 'Zalo' hoặc 'bot'\n" "• Ví dụ đúng: Shop, ChămSóc, HỗTrợ247, CSKH-TựĐộng ✅\n" "• Ví dụ sai: Zalo Support, ShopBot, ZaloBot ❌\n\n" "2️⃣ Cách lấy HTTP API:\n" "• Truy cập https://zalo.me/s/botcreator\n" "• Chọn bot → Cài đặt → API/HTTP API\n" "• Copy Bot token: 4179413508988279245:XXXXXXXXXXXXXXXXXXXXXX\n\n" "3️⃣ Cách dùng:\n" "• Gửi HTTP API: để tạo proxy tự động\n" "• Gửi ảnh + mô tả sản phẩm để lưu vào dataset\n" "• Mọi tin nhắn trong nhóm sẽ được lưu tự động" ) async def handle_webhook(request: Request): body = await request.body() body_str = body.decode("utf-8") if body else "" logger.info("webhook received: body=%s", body_str[:500]) try: data = json.loads(body_str) except Exception as e: logger.error("JSON parse error: %s", e) BOT_STATE["logs"].append({"event": "parse_error", "sender_id": "N/A", "chat_id": "N/A", "sender_name": "N/A", "chat_type": "N/A", "text": body_str[:200]}) return Response(content=json.dumps({"message": "Bad JSON", "error": str(e)}), media_type="application/json", status_code=400) result = data.get("result", data) event = result.get("event_name", "unknown") msg = result.get("message", {}) sender = msg.get("from", {}) chat = msg.get("chat", {}) text = msg.get("text", "") sender_id = str(sender.get("id", "")) sender_name = sender.get("display_name") or sender.get("name") or sender_id chat_id = str(chat.get("id", "")) chat_type = str(chat.get("chat_type", "")) BOT_STATE["logs"].append({ "event": str(event), "sender_id": sender_id, "chat_id": chat_id, "sender_name": sender_name, "chat_type": chat_type, "text": str(text)[:500], "is_zgr": _is_zgr_sender_local(sender_id), "time": time.strftime("%Y-%m-%d %H:%M:%S"), }) if len(BOT_STATE["logs"]) > 100: BOT_STATE["logs"] = BOT_STATE["logs"][-100:] logger.info("EVENT=%s SENDER_ID=%s CHAT_ID=%s SENDER_NAME=%s", event, sender_id, chat_id, sender_name) if event == "message.text.received": cid = chat.get("id") or sender.get("id") or "" _save_chat_id(cid, sender_id) user_token = _extract_user_token(text) if user_token: zapi = ZaloBotAPI(BOT_STATE["bot_token"]) try: zapi.send_message(cid, "Processing your HTTP API...") except Exception: pass def _create_and_setup(): try: _r, proxy_url, status = _create_api_proxy_space(user_token, sender_id, sender_name) dataset_id = None try: dataset_id, _ = _ensure_user_dataset(sender_id, user_token) except Exception as de: logger.error("Dataset setup failed: %s", de) BOT_STATE["api_spaces"].append({ "repo_id": _r, "proxy_url": proxy_url, "user_token": user_token, "user_id": sender_id, "sender_name": sender_name, "status": status, "dataset_id": dataset_id, }) _save_proxy_spaces() sw = _set_user_webhook(user_token, proxy_url, BOT_STATE["webhook_secret"]) user_bot_id = user_token.split(":")[0] if ":" in user_token else "" user_bot_link = "https://zalo.me/s/" + user_bot_id if user_bot_id else "https://zalo.me/s/botcreator" dataset_url = "https://huggingface.co/datasets/" + NAMESPACE + "/" + _safe_space_name(sender_id or "user") + "-zalo-data" if sender_id else "" instructions = "BOT OK! Proxy: " + proxy_url + "\\nDataset: " + (dataset_url if dataset_url else "N/A") + "\\nManage bot at: " + user_bot_link try: zapi.send_message(cid, instructions) except Exception: pass BOT_STATE["logs"].append({ "event": "proxy_created", "sender_id": sender_id, "chat_id": chat_id, "sender_name": sender_name, "chat_type": chat_type, "text": "Proxy created: " + proxy_url, "time": time.strftime("%Y-%m-%d %H:%M:%S"), }) except Exception as e: logger.error("Failed: %s", e) try: ZaloBotAPI(BOT_STATE["bot_token"]).send_message(cid, "Loi tao proxy: " + str(e)) except Exception: pass threading.Thread(target=_create_and_setup, daemon=True).start() return Response(content=json.dumps({"message": "Processing", "proxy_url": "pending"}), media_type="application/json") # ─── Regular message ─── if cid: zapi = ZaloBotAPI(BOT_STATE["bot_token"]) product_name, description, price, category = "", "", "", "" image_url, image_data_b64 = "", "" attachments = msg.get("attachment", {}) if attachments: payload = attachments.get("payload", {}) if isinstance(payload, str): try: payload = json.loads(payload) except Exception: payload = {} image_url = payload.get("url", "") or msg.get("photo", "") or msg.get("photo_url", "") or msg.get("image_url", "") image_data_b64 = payload.get("data", "") or msg.get("image", "") else: image_url = msg.get("photo", "") or msg.get("photo_url", "") or msg.get("image_url", "") image_data_b64 = msg.get("image", "") name_match = re.search(r'(?:ten sp|ten san pham|product name|name)[:\s]*([^|\n]+)', text, re.IGNORECASE) desc_match = re.search(r'(?:mo ta|description|desc|mota)[:\\s]*([^|\n]+)', text, re.IGNORECASE) price_match = re.search(r'(?:gia|price|don gia|donggia)[:\\s]*([\\d,.]+)', text, re.IGNORECASE) cat_match = re.search(r'(?:chuyen muc|category|danh muc|loai)[:\\s]*([^|\n]+?)(?:$|\n)', text, re.IGNORECASE) if name_match: product_name = name_match.group(1).strip() if desc_match: description = desc_match.group(1).strip() if price_match: price = price_match.group(1).strip() if cat_match: category = cat_match.group(1).strip() if image_url or image_data_b64 or product_name or description or price or category: _save_product_to_main_dataset( image_url=image_url, image_data_b64=image_data_b64, description=description, price=price, category=category, sender_id=sender_id, sender_name=sender_name, product_name=product_name, chat_id=chat_id, ) BOT_STATE["logs"].append({ "event": "product_saved", "sender_id": sender_id, "chat_id": chat_id, "sender_name": sender_name, "chat_type": chat_type, "text": "Saved! " + str(product_name)[:50], "time": time.strftime("%Y-%m-%d %H:%M:%S"), }) elif _is_zgr_sender_local(sender_id) and text: # Save all text messages from ZGR group to dataset _save_text_message_to_dataset( text=text, description=text[:200], price=price, category=category, sender_id=sender_id, sender_name=sender_name, chat_id=chat_id, ) BOT_STATE["logs"].append({ "event": "text_saved", "sender_id": sender_id, "chat_id": chat_id, "sender_name": sender_name, "chat_type": chat_type, "text": "Text saved: " + str(text[:100]), "time": time.strftime("%Y-%m-%d %H:%M:%S"), }) reply = "👋 Xin chào " + str(sender_name) + " (Zalo ID: " + str(sender_id) + ")!\n\n" + HELP_INSTRUCTIONS asyncio.create_task(asyncio.to_thread(zapi.send_message, cid, reply)) elif event == "message.image.received" and chat_id: cid = chat.get("id") or sender.get("id") or "" _save_chat_id(cid, sender_id) image_url = msg.get("photo", "") or msg.get("photo_url", "") or msg.get("image_url", "") image_data_b64 = msg.get("image", "") attachments = msg.get("attachment", {}) if attachments and not image_url: payload = attachments.get("payload", {}) if isinstance(payload, str): try: payload = json.loads(payload) except Exception: payload = {} image_url = payload.get("url", "") image_data_b64 = payload.get("data", "") product_name, description, price, category = "", "", "", "" text = msg.get("caption", "") or text name_match = re.search(r'(?:ten sp|ten san pham|product name|name)[:\s]*([^|\n]+)', text, re.IGNORECASE) desc_match = re.search(r'(?:mo ta|description|desc|mota)[:\\s]*([^|\n]+)', text, re.IGNORECASE) price_match = re.search(r'(?:gia|price|don gia|donggia)[:\\s]*([\\d,.]+)', text, re.IGNORECASE) cat_match = re.search(r'(?:chuyen muc|category|danh muc|loai)[:\\s]*([^|\n]+?)(?:$|\n)', text, re.IGNORECASE) if name_match: product_name = name_match.group(1).strip() if desc_match: description = desc_match.group(1).strip() if price_match: price = price_match.group(1).strip() if cat_match: category = cat_match.group(1).strip() log_text = "photo_url=" + str(image_url[:100]) if image_url else "No photo_url in message" if text: log_text += " | caption=" + str(text[:200]) if image_url: ts = time.strftime("%Y%m%d_%H%M%S") log_text += " | image_url=" + str(image_url[:100]) logger.info("image.received from %s, url=%s", sender_id, image_url[:100]) # Download image bytes first (needed for both OCR and saving) img_bytes = None if image_data_b64: try: img_bytes = base64.b64decode(image_data_b64) except Exception: img_bytes = None if not img_bytes and image_url: try: r = requests.get(image_url, timeout=30) img_bytes = r.content logger.info("Downloaded image (%d bytes) from %s", len(img_bytes), image_url[:80]) except Exception as e: logger.error("Image download failed: %s", e) # Run OCR if we have image bytes and no manual caption if img_bytes and not text: logger.info("Running OCR extraction on image from %s", sender_id) ocr_text = _ocr_extract_text(img_bytes) if ocr_text: # Parse OCR text for product fields ocr_name, ocr_desc, ocr_price, ocr_cat = _parse_ocr_product_info(ocr_text) if not product_name: product_name = ocr_name if not description: description = ocr_desc if not price: price = ocr_price if not category: category = ocr_cat log_text += " | OCR: " + str(ocr_text[:200]) else: log_text += " | OCR failed" # Now save with OCR-extracted data saved = _save_product_to_main_dataset( image_url=image_url, image_data_b64=image_data_b64, img_bytes=img_bytes, 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, timestamp=ts, ) if saved: BOT_STATE["logs"].append({ "event": "product_saved", "sender_id": sender_id, "chat_id": chat_id, "sender_name": sender_name, "chat_type": chat_type, "text": log_text + " | Image saved to dataset! " + str(product_name)[:50], "time": time.strftime("%Y-%m-%d %H:%M:%S"), }) try: zapi = ZaloBotAPI(BOT_STATE["bot_token"]) asyncio.create_task(asyncio.to_thread(zapi.send_message, cid, "GOT IT! Image saved to dataset!")) except Exception as e: logger.error("Reply failed: %s", e) else: BOT_STATE["logs"].append({ "event": "main_dataset_error", "sender_id": sender_id, "chat_id": chat_id, "sender_name": sender_name, "chat_type": chat_type, "text": log_text + " | FAILED to save image to dataset (download or upload error)", "time": time.strftime("%Y-%m-%d %H:%M:%S"), }) else: logger.warning("image.received but no photo_url found: %s", json.dumps(msg)[:300]) BOT_STATE["logs"].append({ "event": "image_no_url", "sender_id": sender_id, "chat_id": chat_id, "sender_name": sender_name, "chat_type": chat_type, "text": log_text, "time": time.strftime("%Y-%m-%d %H:%M:%S"), }) return Response(content=json.dumps({"message": "Success"}), media_type="application/json", status_code=200) app = FastAPI(title="Zalo Bot Webhook") @app.get("/") async def root(): return RedirectResponse(url="/gradio/") @app.get("/health") async def health(): return {"status": "ok", "service": "zalo-bot-webhook", "main_dataset": MAIN_DATASET_ID, "zgr_sender_id": ZGR_SENDER_ID} @app.post("/webhooks") async def webhooks(request: Request): return await handle_webhook(request) @app.get("/logs") async def logs_page(): log_lines = [] for idx, log in enumerate(reversed(BOT_STATE.get("logs", [])[-50:])): sender_id = log.get("sender_id", "") sender_name = log.get("sender_name", sender_id) is_zgr = _is_zgr_sender_local(sender_id) is_saved = log.get("event") in ("dataset_saved", "main_dataset_saved", "proxy_created", "product_saved", "image_saved") is_error = log.get("event") in ("dataset_error", "main_dataset_error", "parse_error", "image_upload_fail", "image_no_url") bg = "#e8f5e9" if is_zgr else "#ffffff" header_color = "#4CAF50" if is_zgr else "#1a73e8" zgr_badge = " [ZGR]" if is_zgr else "" status_badge = "SUCCESS" if is_saved else ("ERROR" if is_error else "INFO") log_lines.append( "
" + "
" + "" + status_badge + " [" + escape(str(log.get("event",""))) + "]" + "👤 " + escape(str(sender_name)) + "" + "🆔 " + escape(str(sender_id)) + "" + "💬 " + escape(str(log.get("chat_id",""))) + "" + "[" + escape(str(log.get("chat_type",""))) + "]" + "" + zgr_badge + "" + "
" + "
" + escape(str(log.get("text","")[:300])) + "
" + "
⏰ " + escape(str(log.get("time","")) or time.strftime('%Y-%m-%d %H:%M:%S')) + " | zgr logs
" + "
" ) total_logs = len(BOT_STATE.get("logs", [])) total_proxies = len(BOT_STATE.get("api_spaces", [])) connected_status = "✅" if BOT_STATE.get("connected") else "❌" last_sender = escape(str(BOT_STATE.get("last_sender_id", "")[:8]) or "—") zgr_count = sum(1 for l in BOT_STATE.get("logs", []) if _is_zgr_sender_local(l.get("sender_id", ""))) rows_html = "".join(log_lines) if log_lines else '

Chưa có sự kiện

' html_content = ( 'Zalo Bot Logs' '' '' '' '

Zalo Bot Logs

' '

Event details

' '

Links: Main UI | Proxy spaces

' '
' '
' + str(total_logs) + '
Total Events
' '
' + str(total_proxies) + '
Proxies
' '
' + str(zgr_count) + '
ZGR Events
' '
' + connected_status + '
Bot Status
' '
' + last_sender + '
Last Sender
' '
' '

Events (' + str(total_logs) + ')

' '
' + rows_html + '
' '

Xem logs riêng cho nhóm ZGR

' '' ) return Response(content=html_content, media_type="text/html") @app.get("/logs/zgr-b7e1e71cf5701c2e4561") async def zgr_logs_page(): zgr_logs = [] for log in BOT_STATE.get("logs", []): sid = str(log.get("sender_id", "")) # Match any sender_id that contains or equals the ZGR identifier if ZGR_SENDER_ID in sid or sid.endswith(ZGR_SENDER_ID[-12:]) or sid == ZGR_SENDER_ID: zgr_logs.append(log) # Also include logs explicitly tagged as ZGR if log.get("is_zgr", False) and not log.get("sender_id"): zgr_logs.append(log) log_lines = [] for idx, log in enumerate(reversed(zgr_logs[-50:])): sender_id = log.get("sender_id", "") sender_name = log.get("sender_name", sender_id) is_saved = log.get("event") in ("dataset_saved", "main_dataset_saved", "proxy_created", "product_saved", "image_saved") is_error = log.get("event") in ("dataset_error", "main_dataset_error", "parse_error", "image_upload_fail", "image_no_url") bg = "#ffffff" if idx % 2 == 0 else "#fafafa" status_color = "#4CAF50" if is_saved else ("#f44336" if is_error else "#1a73e8") status_icon = "SUCCESS" if is_saved else ("ERROR" if is_error else "INFO") chat_type_val = escape(str(log.get("chat_type", ""))) text_val = escape(str(log.get("text", "")[:300])) event_val = escape(str(log.get("event", ""))) sender_val = escape(str(sender_name)) sid_short = escape(str(sender_id)[:20]) cid_short = escape(str(log.get("chat_id", ""))[:20]) time_val = escape(str(log.get("time", ""))) log_lines.append( "
" "
" "" + status_icon + " [" + event_val + "]" "👤 " + sender_val + "" "🆔 " + sid_short + "" "💬 " + cid_short + "" "[" + chat_type_val + "]" "
" "
" + text_val + "
" "
⏰ " + time_val + " | Dataset
" "
" ) saved_count = sum(1 for l in zgr_logs if l.get("event") in ("dataset_saved", "main_dataset_saved", "product_saved")) error_count = sum(1 for l in zgr_logs if l.get("event") in ("dataset_error", "main_dataset_error", "image_upload_fail")) total_zgr_logs = len(zgr_logs) rows_html = "".join(log_lines) if log_lines else '

Chưa có sự kiện cho nhóm này. Gửi ảnh + thông tin sản phẩm để kiểm tra.

' html_content = ( 'Zalo Logs - ZGR Group' '' '' '' '

Zalo Logs - ZGR Group (zgr-b7e1e71cf5701c2e4561)

' '

All webhook events from this group

' '

Links: All logs | Main UI | Proxies

' '
' '
' + str(total_zgr_logs) + '
Total Events
' '
' + str(saved_count) + '
Saved to Dataset
' '
' + str(error_count) + '
Errors
' '
Main Dataset
' '
' '

Events (' + str(total_zgr_logs) + ')

' '
' + rows_html + '
' '

Send image + "Tên sp: ..., Giá: ..., Chuyên mục: ..." to test.

' '' ) return Response(content=html_content, media_type="text/html") @app.get("/proxy-spaces") async def proxy_spaces_page(): rows = [] for s in reversed(BOT_STATE.get("api_spaces", [])[-20:]): repo_name = escape(str(s.get("repo_id", "").split("/")[-1])) dataset_id_val = s.get("dataset_id", "") dataset_link = "💾 dataset" if dataset_id_val else "" rows.append( "
" "
" "
" "👤 " + escape(str(s.get("sender_name", ""))) + "" "🆔 " + escape(str(s.get("user_id", ""))) + "" "[" + escape(str(s.get("status", ""))) + "]" "
" "
" "Space" "webhook" "📊 logs" + dataset_link + "
" "
" "
Repo: " + escape(str(s.get("repo_id", ""))) + "
" "
⏰ " + time.strftime('%Y-%m-%d %H:%M:%S') + "
" "
" ) total_proxies = len(BOT_STATE.get("api_spaces", [])) connected_status = "✅" if BOT_STATE.get("connected") else "❌" rows_html = "".join(rows) if rows else '

Chưa có proxy space nào

' html_content = ( 'Proxy Spaces' '' '' '' '

Quan ly Proxy Spaces

' '

Danh sach cac space proxy da tao.

' '

Links: Logs | Main UI

' '
' '
' + str(total_proxies) + '
Proxies
' '
' + connected_status + '
Bot Status
' '
' '
' + rows_html + '
' '' ) return Response(content=html_content, media_type="text/html") @app.post("/api/delete-proxy/{repo_name}") async def delete_proxy(repo_name: str): repo_id = NAMESPACE + "/" + repo_name try: api = HfApi(token=HF_TOKEN) try: api.delete_repo(repo_id=repo_id, repo_type="space", token=HF_TOKEN) except Exception as e: logger.warning("Could not delete HF Space %s: %s", repo_id, e) for s in BOT_STATE.get("api_spaces", []): if s.get("repo_id", "").split("/")[-1] == repo_name: dataset_id = s.get("dataset_id", "") if dataset_id: try: api.delete_repo(repo_id=dataset_id, repo_type="dataset", token=HF_TOKEN) except Exception as e: logger.warning("Could not delete dataset %s: %s", dataset_id, e) BOT_STATE["api_spaces"] = [s for s in BOT_STATE.get("api_spaces", []) if s.get("repo_id", "").split("/")[-1] != repo_name] _save_proxy_spaces() return {"ok": True, "message": "Da xoa proxy " + repo_id} except Exception as e: logger.error("Delete proxy failed: %s", e) return {"ok": False, "message": "Loi xoa: " + str(e)} with gr.Blocks(title="Zalo Bot Webhook") as demo: gr.Markdown("# Zalo Bot Webhook Setup") with gr.Tabs(): with gr.Tab("Ket noi Bot"): tok = gr.Textbox(DEFAULT_BOT_TOKEN, label="Bot Token", type="password") btn = gr.Button("Ket noi") res = gr.Markdown("") btn.click(fn=connect_bot, inputs=[tok], outputs=res) gr.Textbox(value=get_botinfo, label="Thong tin bot", interactive=False, lines=8) with gr.Tab("Gui tin"): with gr.Row(): cid = gr.Textbox(label="Chat ID") txt = gr.Textbox("HTTP API: 4179413508988279245:abc123", label="Noi dung") b = gr.Button("Gui") b.click(fn=send_msg, inputs=[cid, txt], outputs=gr.Textbox(label="Ket qua")) gr.Textbox(value=get_events, label="Su kien nhan duoc", interactive=False, lines=20) gr.Markdown( "Links: [Proxy spaces](/proxy-spaces)\n\n" "Each user sends HTTP API token to create their own proxy space." ) with gr.Tab("Huong dan"): gr.Markdown("1. Go to https://zalo.me/s/botcreator\n2. Copy HTTP API token\n3. Send to bot to auto-create proxy") demo.queue() app = gr.mount_gradio_app(app, demo, path="/gradio") print("[startup] FastAPI app ready: /health, /webhooks, /logs, /logs/zgr-b7e1e71cf5701c2e4561, /proxy-spaces, /gradio/", flush=True) if __name__ == "__main__": port = int(os.getenv("PORT", "7860")) server_name = os.getenv("GRADIO_SERVER_NAME", "0.0.0.0") print("[launch] uvicorn on " + server_name + ":" + str(port), flush=True) import uvicorn uvicorn.run(app, host=server_name, port=port)