"""Optional Facebook/Instagram adapters (Graph API). DISABLED BY DEFAULT. Each adapter refuses to run unless its enable flag is true (``CAYCE_OPS_ENABLE_FACEBOOK`` / ``CAYCE_OPS_ENABLE_INSTAGRAM``), so the default deployment never touches Meta endpoints. Tokens are env-only and are never logged. """ from __future__ import annotations import json import urllib.parse import urllib.request GRAPH = "https://graph.facebook.com/v21.0" def _post(url: str, payload: dict) -> dict: data = urllib.parse.urlencode(payload).encode() req = urllib.request.Request(url, data=data, method="POST") # noqa: S310 with urllib.request.urlopen(req, timeout=30) as resp: return json.loads(resp.read().decode()) def post_facebook(cfg, caption: str, media_path: str | None = None) -> dict: """Post to the page feed (text) or /photos (with local media). Gated.""" if not cfg.enable_facebook: raise PermissionError("facebook adapter disabled (CAYCE_OPS_ENABLE_FACEBOOK=false)") if not cfg.facebook_page_token: raise ValueError("CAYCE_OPS_FACEBOOK_PAGE_TOKEN missing") if media_path: # photo posts require a reachable URL or multipart upload; we use the # simple URL form — media must be hosted (out of scope for local files) result = _post( f"{GRAPH}/me/photos", {"url": media_path, "caption": caption, "access_token": cfg.facebook_page_token}, ) else: result = _post( f"{GRAPH}/me/feed", {"message": caption, "access_token": cfg.facebook_page_token}, ) return {"post_id": result.get("post_id") or result.get("id", "")} def post_instagram(cfg, caption: str, image_url: str) -> dict: """Two-step IG publish (container -> publish). Gated.""" if not cfg.enable_instagram: raise PermissionError("instagram adapter disabled (CAYCE_OPS_ENABLE_INSTAGRAM=false)") if not cfg.instagram_user_id: raise ValueError("CAYCE_OPS_INSTAGRAM_USER_ID missing") token = cfg.facebook_page_token # IG Graph publishing uses the page token container = _post( f"{GRAPH}/{cfg.instagram_user_id}/media", {"image_url": image_url, "caption": caption, "access_token": token}, ) published = _post( f"{GRAPH}/{cfg.instagram_user_id}/media_publish", {"creation_id": container["id"], "access_token": token}, ) return {"post_id": published.get("id", "")}