Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 53 additions & 7 deletions social_posters/bluesky.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
from datetime import datetime
import logging
import os
import pprint

import requests

from abstractions import Post, PostResult, SocialPoster
from config import CITY_HASHTAGS, CITY_NAME, CITY_STATE

logger = logging.getLogger(__name__)


class PosterBluesky(SocialPoster):
def __init__(self):
# Handle environment variable validation internally
self.username = os.environ.get("BLUESKY_HANDLE")
self.username = os.environ.get("BLUESKY_HANDLE")
self.password = os.environ.get("BLUESKY_PASSWORD")
self._access_token = None
self._did = None # Decentralized identifier from the Bluesky session.
Expand All @@ -21,6 +25,7 @@ def platform_name(self) -> str:
return "Bluesky"

def authenticate(self) -> bool:
logger.info("Start authenticating to Bluesky")
try:
response = requests.post(
"https://bsky.social/xrpc/com.atproto.server.createSession",
Expand All @@ -31,32 +36,50 @@ def authenticate(self) -> bool:
session = response.json()
self._access_token = session.get("accessJwt")
self._did = session.get("did")
return bool(self._access_token and self._did)
ok = bool(self._access_token and self._did)
if ok:
logger.info("Bluesky authentication succeeded (did present=%s)", bool(self._did))
else:
logger.warning("Bluesky auth response missing accessJwt or did")
return ok
except Exception:
logger.exception("Bluesky authentication failed")
self._access_token = None
self._did = None
return False

def publish(self, post: Post) -> PostResult:
logger.info("Start publishing to Bluesky")
logger.info("Bluesky post input: %s", pprint.pformat(post))

if not self._is_available:
return PostResult(
logger.warning("Bluesky credentials not available.")
result = PostResult(
success=False,
error_message="Bluesky credentials not available."
)
logger.info("Bluesky publish result: %s", pprint.pformat(result))
return result

if not self._access_token or not self._did:
logger.info("No active Bluesky session; authenticating now")
if not self.authenticate():
return PostResult(
result = PostResult(
success=False, error_message="Bluesky authentication failed."
)
logger.info("Bluesky publish result: %s", pprint.pformat(result))
return result

headers = {"Authorization": f"Bearer {self._access_token}"}
image_blob = None

if post.image_url:
logger.info("Bluesky image URL found; starting download/upload")
try:
img_response = requests.get(post.image_url, timeout=20)
img_response.raise_for_status()
logger.info("Bluesky image downloaded (%d bytes)", len(img_response.content))

upload = requests.post(
"https://bsky.social/xrpc/com.atproto.repo.uploadBlob",
headers={**headers, "Content-Type": "image/jpeg"},
Expand All @@ -65,18 +88,31 @@ def publish(self, post: Post) -> PostResult:
)
upload.raise_for_status()
image_blob = upload.json().get("blob")
logger.info("Bluesky image uploaded (blob present=%s)", bool(image_blob))
except Exception as exc:
return PostResult(success=False, error_message=str(exc))
logger.exception("Bluesky image download/upload failed")
result = PostResult(success=False, error_message=str(exc))
logger.info("Bluesky publish result: %s", pprint.pformat(result))
return result
else:
logger.info("Bluesky post has no image URL; publishing text-only post")

logger.info("Building Bluesky text and facets")
text, facets = self._build_text_and_facets(post)
logger.info("Built text/facets (text_len=%d, facets_count=%d)", len(text), len(facets))
logger.debug("Bluesky text preview: %s", text[:280])
logger.debug("Bluesky facets: %s", pprint.pformat(facets))

record = {
"$type": "app.bsky.feed.post",
"text": text,
"createdAt": datetime.utcnow().isoformat() + "Z",
}
logger.debug("Bluesky record base: %s", pprint.pformat(record))

if facets:
record["facets"] = facets
logger.info("Attached facets to Bluesky record")

if image_blob:
record["embed"] = {
Expand All @@ -88,8 +124,12 @@ def publish(self, post: Post) -> PostResult:
}
],
}
logger.info("Attached image embed to Bluesky record")

logger.debug("Final Bluesky record payload: %s", pprint.pformat(record))

try:
logger.info("Sending Bluesky createRecord request")
response = requests.post(
"https://bsky.social/xrpc/com.atproto.repo.createRecord",
headers=headers,
Expand All @@ -102,13 +142,19 @@ def publish(self, post: Post) -> PostResult:
)
response.raise_for_status()
data = response.json()
return PostResult(
result = PostResult(
success=True,
post_id=data.get("cid"),
post_url=data.get("uri"),
)
logger.info("Bluesky createRecord succeeded")
logger.info("Bluesky publish result: %s", pprint.pformat(result))
return result
except Exception as exc:
return PostResult(success=False, error_message=str(exc))
logger.exception("Bluesky createRecord failed")
result = PostResult(success=False, error_message=str(exc))
logger.info("Bluesky publish result: %s", pprint.pformat(result))
return result

def format_post(self, pet):
from abstractions import Post
Expand Down
Loading