diff --git a/social_posters/bluesky.py b/social_posters/bluesky.py index a727b3c..6a92e32 100644 --- a/social_posters/bluesky.py +++ b/social_posters/bluesky.py @@ -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. @@ -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", @@ -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"}, @@ -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"] = { @@ -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, @@ -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