ci / lint-test-leaks (pull_request) Canceled after 0s
- calendar: one draft per theme/channel (idempotent), rotated schedule - human gate: approve() audited; AUTONOMOUS_MODE has no effect here by design - nostr (primary): in-repo NIP-01 client (D4) — canonical serialization, sha256 id, BIP-340 schnorr sign via coincurve with zero aux randomness (deterministic), pure-python reference verification before publish, per-relay publish with isolated errors, event id stored as post_ref - facebook/instagram adapters: exist but refuse to run unless explicitly enabled via env (default deployment never touches Meta) - images: Pillow brand templates (bh gold / hc orange), 1080x1350, template-driven typeset design (not AI-generated media) - failure isolation: config errors audited, item stays approved tests: 12 new (event roundtrip+tamper, determinism, key validation, calendar idempotence, approve gate, nostr happy path, future schedule, clean config failure, disabled meta adapters, image render) — 41 total
62 lines
2.4 KiB
Python
62 lines
2.4 KiB
Python
"""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", "")}
|