import json
import os
from urllib.request import Request, urlopen
API_URL = "https://api.buffer.com"
class BufferError(RuntimeError):
pass
def _gql(token, query, variables=None):
payload = {"query": query}
if variables is not None:
payload["variables"] = variables
req = Request(API_URL, data=json.dumps(payload).encode("utf-8"), headers={
"Authorization": "Bearer " + token,
"Content-Type": "application/json",
})
with urlopen(req, timeout=20) as response:
data = json.load(response)
if data.get("errors"):
raise BufferError(data["errors"][0].get("message", "Buffer GraphQL error"))
return data.get("data") or {}
def _account(token):
return _gql(token, "{ account { organizations { id name } } }")
def channels(token, organization_id=None):
if not organization_id:
orgs = _account(token).get("account", {}).get("organizations", [])
if len(orgs) != 1:
raise BufferError("Buffer account must expose exactly one organization")
organization_id = orgs[0]["id"]
query = '''query Channels($org: OrganizationId!) {
channels(input: {organizationId: $org}) { id name displayName service isQueuePaused }
}'''
return _gql(token, query, {"org": organization_id}).get("channels", [])
def create_draft(token, platform, text, organization_id=None):
service = "instagram" if platform == "reels" else platform
matches = [c for c in channels(token, organization_id) if c.get("service") == service]
if len(matches) != 1:
raise BufferError(f"Expected exactly one Buffer channel for {service}, found {len(matches)}")
channel = matches[0]
mutation = '''mutation CreateDraft($input: CreatePostInput!) {
createPost(input: $input) {
... on PostActionSuccess { post { id text } }
... on MutationError { message }
}
}'''
variables = {"input": {
"text": text,
"channelId": channel["id"],
"schedulingType": "automatic",
"mode": "addToQueue",
"saveToDraft": True,
}}
result = _gql(token, mutation, variables).get("createPost") or {}
if result.get("message"):
raise BufferError(result["message"])
post = result.get("post")
if not post or not post.get("id"):
raise BufferError("Buffer returned no post id")
return {"post_id": post["id"], "channel_id": channel["id"], "channel": channel.get("displayName") or channel.get("name"), "service": service}
def token_from_env(name):
token = os.environ.get(name, "").strip()
if not token:
raise BufferError(f"Missing {name}")
return token