publisher: themed calendar, NIP-01 nostr adapter, gated brand images
ci / lint-test-leaks (pull_request) Canceled after 0s
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
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
"""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", "")}
|
||||
Reference in New Issue
Block a user