Spaces:
Running
Running
| 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 = "<image>\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 += "<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>" | |
| 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>" | |
| 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: <bot_token> để 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") | |
| async def root(): | |
| return RedirectResponse(url="/gradio/") | |
| async def health(): | |
| return {"status": "ok", "service": "zalo-bot-webhook", "main_dataset": MAIN_DATASET_ID, "zgr_sender_id": ZGR_SENDER_ID} | |
| async def webhooks(request: Request): | |
| return await handle_webhook(request) | |
| 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( | |
| "<div style='margin:8px 0;padding:10px;background:" + bg + ";border-radius:6px;border-left:3px solid " + header_color + "'>" | |
| + "<div style='display:flex;gap:6px;align-items:center;flex-wrap:wrap'>" | |
| + "<b style='color:" + header_color + "'>" + status_badge + " [" + escape(str(log.get("event",""))) + "]</b>" | |
| + "<span style='color:" + header_color + "'>👤 " + escape(str(sender_name)) + "</span>" | |
| + "<span style='color:#666'>🆔 " + escape(str(sender_id)) + "</span>" | |
| + "<span style='color:#666'>💬 " + escape(str(log.get("chat_id",""))) + "</span>" | |
| + "<span style='color:#888'>[" + escape(str(log.get("chat_type",""))) + "]</span>" | |
| + "<span style='color:#4CAF50'>" + zgr_badge + "</span>" | |
| + "</div>" | |
| + "<div style='margin-top:4px;color:#333;font-family:monospace;font-size:13px;word-break:break-word'>" | |
| + escape(str(log.get("text","")[:300])) | |
| + "</div>" | |
| + "<div style='margin-top:2px;color:#999;font-size:11px'>⏰ " + escape(str(log.get("time","")) or time.strftime('%Y-%m-%d %H:%M:%S')) + " | <a href='/logs/zgr-b7e1e71cf5701c2e4561'>zgr logs</a></div>" | |
| + "</div>" | |
| ) | |
| 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 '<p style="color:#999">Chưa có sự kiện</p>' | |
| html_content = ( | |
| '<!DOCTYPE html><html><head><title>Zalo Bot Logs</title>' | |
| '<meta http-equiv="refresh" content="5">' | |
| '<meta name="viewport" content="width=device-width, initial-scale=1">' | |
| '<style>body { font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto; padding: 16px; background:#fafafa; }' | |
| 'h1 { color: #1a73e8; margin-bottom: 4px; }' | |
| '.subtitle { color: #5f6368; font-size: 14px; margin-bottom: 16px; }' | |
| '.stats { display: flex; gap: 16px; margin: 16px 0; flex-wrap: wrap; }' | |
| '.stat-box { background: #e8f0fe; padding: 12px 24px; border-radius: 10px; min-width: 140px; }' | |
| '.stat-value { font-size: 26px; font-weight: bold; color: #1a73e8; }' | |
| '.stat-label { font-size: 12px; color: #5f6368; }' | |
| '.log-container { max-height: 650px; overflow-y: auto; background:#fff; border-radius:8px; padding:8px; }' | |
| '</style></head><body>' | |
| '<h1>Zalo Bot Logs</h1>' | |
| '<p class="subtitle">Event details</p>' | |
| '<p>Links: <a href="/gradio/">Main UI</a> | <a href="/proxy-spaces">Proxy spaces</a></p>' | |
| '<div class="stats">' | |
| '<div class="stat-box"><div class="stat-value">' + str(total_logs) + '</div><div class="stat-label">Total Events</div></div>' | |
| '<div class="stat-box"><div class="stat-value">' + str(total_proxies) + '</div><div class="stat-label">Proxies</div></div>' | |
| '<div class="stat-box"><div class="stat-value">' + str(zgr_count) + '</div><div class="stat-label">ZGR Events</div></div>' | |
| '<div class="stat-box"><div class="stat-value">' + connected_status + '</div><div class="stat-label">Bot Status</div></div>' | |
| '<div class="stat-box"><div class="stat-value">' + last_sender + '</div><div class="stat-label">Last Sender</div></div>' | |
| '</div>' | |
| '<h2>Events (' + str(total_logs) + ')</h2>' | |
| '<div class="log-container">' + rows_html + '</div>' | |
| '<p><a href="/logs/zgr-b7e1e71cf5701c2e4561">Xem logs riêng cho nhóm ZGR</a></p>' | |
| '</body></html>' | |
| ) | |
| return Response(content=html_content, media_type="text/html") | |
| 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( | |
| "<div style='margin:8px 0;padding:10px;background:" + bg + ";border-radius:6px;border-left:3px solid " + status_color + "'>" | |
| "<div style='display:flex;gap:6px;align-items:center;flex-wrap:wrap'>" | |
| "<b style='color:" + status_color + "'>" + status_icon + " [" + event_val + "]</b>" | |
| "<span style='color:#1a73e8;font-weight:bold'>👤 " + sender_val + "</span>" | |
| "<span style='color:#666'>🆔 " + sid_short + "</span>" | |
| "<span style='color:#666'>💬 " + cid_short + "</span>" | |
| "<span style='color:#666'>[" + chat_type_val + "]</span>" | |
| "</div>" | |
| "<div style='margin-top:4px;color:#333;font-family:monospace;font-size:13px;word-break:break-word'>" | |
| + text_val | |
| + "</div>" | |
| "<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>" | |
| "</div>" | |
| ) | |
| 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 '<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>' | |
| html_content = ( | |
| '<!DOCTYPE html><html><head><title>Zalo Logs - ZGR Group</title>' | |
| '<meta http-equiv="refresh" content="5">' | |
| '<meta name="viewport" content="width=device-width, initial-scale=1">' | |
| '<style>' | |
| 'body { font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto; padding: 16px; background:#fafafa; }' | |
| 'h1 { color: #1a73e8; margin-bottom: 4px; }' | |
| '.subtitle { color: #5f6368; font-size: 14px; margin-bottom: 16px; }' | |
| '.stats { display: flex; gap: 16px; margin: 16px 0; flex-wrap: wrap; }' | |
| '.stat-box { padding: 12px 24px; border-radius: 10px; min-width: 140px; }' | |
| '.stat-value { font-size: 26px; font-weight: bold; }' | |
| '.stat-saved { background: #e8f5e9; } .stat-saved .stat-value { color: #4CAF50; }' | |
| '.stat-error { background: #ffebee; } .stat-error .stat-value { color: #f44336; }' | |
| '.stat-total { background: #e8f0fe; } .stat-total .stat-value { color: #1a73e8; }' | |
| '.log-container { max-height: 650px; overflow-y: auto; background:#fff; border-radius:8px; padding:8px; }' | |
| '</style></head><body>' | |
| '<h1>Zalo Logs - ZGR Group (zgr-b7e1e71cf5701c2e4561)</h1>' | |
| '<p class="subtitle">All webhook events from this group</p>' | |
| '<p>Links: <a href="/logs">All logs</a> | <a href="/gradio/">Main UI</a> | <a href="/proxy-spaces">Proxies</a></p>' | |
| '<div class="stats">' | |
| '<div class="stat-box stat-total"><div class="stat-value">' + str(total_zgr_logs) + '</div><div class="stat-label">Total Events</div></div>' | |
| '<div class="stat-box stat-saved"><div class="stat-value">' + str(saved_count) + '</div><div class="stat-label">Saved to Dataset</div></div>' | |
| '<div class="stat-box stat-error"><div class="stat-value">' + str(error_count) + '</div><div class="stat-label">Errors</div></div>' | |
| '<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>' | |
| '</div>' | |
| '<h2>Events (' + str(total_zgr_logs) + ')</h2>' | |
| '<div class="log-container">' + rows_html + '</div>' | |
| '<p style="color:#5f6368;font-size:13px;margin-top:12px">Send image + "Tên sp: ..., Giá: ..., Chuyên mục: ..." to test.</p>' | |
| '</body></html>' | |
| ) | |
| return Response(content=html_content, media_type="text/html") | |
| 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 = "<a href='https://huggingface.co/datasets/" + escape(dataset_id_val) + "' target='_blank'>💾 dataset</a>" if dataset_id_val else "" | |
| rows.append( | |
| "<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)'>" | |
| "<div style='display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:space-between'>" | |
| "<div>" | |
| "<b style='color:#1a73e8'>👤 " + escape(str(s.get("sender_name", ""))) + "</b>" | |
| "<span style='color:#666'>🆔 " + escape(str(s.get("user_id", ""))) + "</span>" | |
| "<span style='color:#4CAF50;font-weight:bold'>[" + escape(str(s.get("status", ""))) + "]</span>" | |
| "</div>" | |
| "<div style='display:flex;gap:6px;flex-wrap:wrap'>" | |
| "<a href='https://huggingface.co/spaces/bep40/" + repo_name + "' target='_blank'>Space</a>" | |
| "<a href='" + escape(str(s.get("proxy_url", ""))) + "' target='_blank'>webhook</a>" | |
| "<a href='" + escape(str(s.get("proxy_url", "")).replace("/webhooks", "/logs")) + "' target='_blank'>📊 logs</a>" | |
| + dataset_link + | |
| "</div>" | |
| "</div>" | |
| "<div style='margin-top:6px'><span style='color:#5f6368'>Repo:</span> <code>" + escape(str(s.get("repo_id", ""))) + "</code></div>" | |
| "<div style='margin-top:2px;color:#999;font-size:11px'>⏰ " + time.strftime('%Y-%m-%d %H:%M:%S') + "</div>" | |
| "</div>" | |
| ) | |
| total_proxies = len(BOT_STATE.get("api_spaces", [])) | |
| connected_status = "✅" if BOT_STATE.get("connected") else "❌" | |
| rows_html = "".join(rows) if rows else '<p style="color:#999">Chưa có proxy space nào</p>' | |
| html_content = ( | |
| '<!DOCTYPE html><html><head><title>Proxy Spaces</title>' | |
| '<meta http-equiv="refresh" content="5">' | |
| '<meta name="viewport" content="width=device-width, initial-scale=1">' | |
| '<style>' | |
| 'body { font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto; padding: 16px; background:#fafafa; }' | |
| 'h1 { color: #1a73e8; } .subtitle { color: #5f6368; }' | |
| '.stats { display: flex; gap: 16px; margin: 16px 0; flex-wrap: wrap; }' | |
| '.stat-box { background: #e8f0fe; padding: 12px 24px; border-radius: 10px; min-width: 140px; }' | |
| '.stat-value { font-size: 26px; font-weight: bold; color: #1a73e8; }' | |
| '.stat-label { font-size: 12px; color: #5f6368; }' | |
| '.container { background:#fff; border-radius:8px; padding:12px; }' | |
| 'a { color: #1a73e8; text-decoration: none; cursor: pointer; }' | |
| 'a:hover { text-decoration: underline; }' | |
| '</style></head><body>' | |
| '<h1>Quan ly Proxy Spaces</h1>' | |
| '<p class="subtitle">Danh sach cac space proxy da tao.</p>' | |
| '<p>Links: <a href="/logs">Logs</a> | <a href="/gradio/">Main UI</a></p>' | |
| '<div class="stats">' | |
| '<div class="stat-box"><div class="stat-value">' + str(total_proxies) + '</div><div class="stat-label">Proxies</div></div>' | |
| '<div class="stat-box"><div class="stat-value">' + connected_status + '</div><div class="stat-label">Bot Status</div></div>' | |
| '</div>' | |
| '<div class="container">' | |
| + rows_html + | |
| '</div>' | |
| '</body></html>' | |
| ) | |
| return Response(content=html_content, media_type="text/html") | |
| 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) | |