diff --git a/README.md b/README.md index 36dc3ac..88f0679 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,9 @@ A FastAPI application with Docker and docker-compose support. 2. **Configure BoxAPI (for Instagram scraping):** ```bash # Add your BoxAPI credentials to .env - BOXAPI_KEY=your_boxapi_bearer_token - BOXAPI_BASE=https://boxapi.ir + BOXAPI_USERNAME=your_boxapi_username + BOXAPI_PASSWORD=your_boxapi_password + BOXAPI_ENDPOINT=https://boxapi.ir/api/instagram/user/get_media_by_username BOXAPI_LOG_DIR=/app/logs/boxapi ``` @@ -176,8 +177,9 @@ FastApi-ISS/ The application uses environment variables for configuration. Copy `env.example` to `.env` and customize the values: ### BoxAPI Configuration (Instagram Scraping) -- `BOXAPI_KEY`: Your BoxAPI authentication key (Bearer token) -- `BOXAPI_BASE`: BoxAPI base URL (default: `https://boxapi.ir`) +- `BOXAPI_USERNAME`: Your BoxAPI username +- `BOXAPI_PASSWORD`: Your BoxAPI password +- `BOXAPI_ENDPOINT`: BoxAPI endpoint URL (default: `https://boxapi.ir/api/instagram/user/get_media_by_username`) - `BOXAPI_LOG_DIR`: Directory for BoxAPI response logs (default: `/app/logs/boxapi`) ### Database Configuration diff --git a/utils/boxapi_client.py b/utils/boxapi_client.py index 4bc331a..849dc77 100644 --- a/utils/boxapi_client.py +++ b/utils/boxapi_client.py @@ -2,60 +2,81 @@ import os, requests from typing import Any, Dict, List, Tuple, Optional from utils.boxapi_log import log_boxapi_response -BOXAPI_BASE = os.environ.get("BOXAPI_BASE", "https://boxapi.ir") -BOXAPI_KEY = os.environ.get("BOXAPI_KEY", "") # put your key in .env +BOXAPI_USERNAME = os.environ.get("BOXAPI_USERNAME", "") +BOXAPI_PASSWORD = os.environ.get("BOXAPI_PASSWORD", "") +# Your .env can override to /get_media; default matches your cURL (/get_media_by_username) +BOXAPI_ENDPOINT = os.environ.get( + "BOXAPI_ENDPOINT", + "https://boxapi.ir/api/instagram/user/get_media_by_username" +) class BoxAPIError(Exception): pass def get_media(username: str, count: int = 5) -> Tuple[List[Dict[str, Any]], str]: """ - Returns (items, log_path) - items is the array under response.body.items + Calls BoxAPI and returns (items, log_path). """ - url = f"{BOXAPI_BASE}/api/instagram/user/get_media" - payload = {"username": username, "count": count} - headers = {"Authorization": f"Bearer {BOXAPI_KEY}"} if BOXAPI_KEY else {} + if not BOXAPI_USERNAME or not BOXAPI_PASSWORD: + raise BoxAPIError("Missing BOXAPI_USERNAME/BOXAPI_PASSWORD in environment.") + payload = {"username": username, "count": count} resp = None resp_json: Optional[Dict[str, Any]] = None + resp_text: Optional[str] = None status_code: Optional[int] = None try: - resp = requests.post(url, json=payload, headers=headers, timeout=60) + resp = requests.post( + BOXAPI_ENDPOINT, + json=payload, + auth=(BOXAPI_USERNAME, BOXAPI_PASSWORD), + timeout=60, + ) status_code = resp.status_code - resp.raise_for_status() - resp_json = resp.json() - except Exception as e: - log_path = log_boxapi_response(username, payload, resp_json, status_code, note=f"HTTP/parse error: {e}") - raise BoxAPIError(f"BoxAPI request failed ({e}). See log: {log_path}") + resp_text = resp.text + # Try to parse JSON; if it fails, we'll log text and raise + try: + resp_json = resp.json() + except Exception: + log_path = log_boxapi_response( + username, payload, status_code, response_text=resp_text, + note="Non-JSON response" + ) + raise BoxAPIError(f"BoxAPI non-JSON response. See log: {log_path}") - # ✅ Real structure according to your sample - # { - # "status": "done", - # "response": { - # "status_code": 200, - # "content_type": "application/json", - # "body": { - # "items": [ ... ] - # } - # } - # } - try: + # Expected shape: status -> response -> status_code + body.items top_status = resp_json.get("status") inner = resp_json.get("response") or {} inner_code = inner.get("status_code") body = inner.get("body") or {} items = body.get("items") or [] - log_path = log_boxapi_response(username, payload, resp_json, inner_code, note="OK") + log_path = log_boxapi_response( + username, payload, inner_code, response_json=resp_json, note="OK" + ) if top_status != "done" or inner_code != 200 or not isinstance(items, list): - raise ValueError(f"Unexpected shape: status={top_status}, inner_code={inner_code}, items_type={type(items)}") + # Include a concise snapshot of keys to help debugging + snapshot = { + "top_status": top_status, + "inner_code": inner_code, + "has_body": isinstance(body, dict), + "items_type": str(type(items)), + "items_len": len(items) if isinstance(items, list) else None, + } + raise BoxAPIError(f"Unexpected response format from BoxAPI ({snapshot}). See log: {log_path}") + return items, log_path + + except BoxAPIError: + raise except Exception as e: - log_path = log_boxapi_response(username, payload, resp_json, status_code, note=f"schema error: {e}") - raise BoxAPIError(f"Unexpected response format from BoxAPI. See log: {log_path}") + log_path = log_boxapi_response( + username, payload, status_code, response_json=resp_json, response_text=resp_text, + note=f"HTTP/parse error: {e}" + ) + raise BoxAPIError(f"BoxAPI request failed ({e}). See log: {log_path}") # Legacy function for backward compatibility def fetch_boxapi_posts(username: str, count: int = 5) -> List[Dict]: diff --git a/utils/boxapi_log.py b/utils/boxapi_log.py index b9300bb..6dbba44 100644 --- a/utils/boxapi_log.py +++ b/utils/boxapi_log.py @@ -10,8 +10,9 @@ def _ensure_dir(path: str) -> None: def log_boxapi_response( username: str, request_payload: Dict[str, Any], - response_json: Optional[Dict[str, Any]], status_code: Optional[int], + response_json: Optional[Dict[str, Any]] = None, + response_text: Optional[str] = None, note: str = "" ) -> str: ts = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") @@ -25,8 +26,9 @@ def log_boxapi_response( "username": username, "status_code": status_code, "note": note, - "request_payload": request_payload, - "response": response_json, + "request_payload": request_payload, # does NOT include API credentials + "response_json": response_json, + "response_text": response_text if response_json is None else None, } with open(filepath, "w", encoding="utf-8") as f: json.dump(doc, f, ensure_ascii=False, indent=2)