diff --git a/twitter2bsky_daemon.py b/twitter2bsky_daemon.py index 41d342d..4409764 100644 --- a/twitter2bsky_daemon.py +++ b/twitter2bsky_daemon.py @@ -21,11 +21,13 @@ SCRAPE_TWEET_LIMIT = 30 DEDUPE_BSKY_LIMIT = 30 TWEET_MAX_AGE_DAYS = 3 VIDEO_MAX_DURATION_SECONDS = 179 +BSKY_TEXT_MAX_LENGTH = 275 BSKY_BLOB_UPLOAD_MAX_RETRIES = 5 BSKY_BLOB_UPLOAD_BASE_DELAY = 10 BSKY_BLOB_UPLOAD_MAX_DELAY = 300 MEDIA_DOWNLOAD_TIMEOUT = 30 +DEFAULT_BSKY_BASE_URL = "https://bsky.social" # --- Logging Setup --- logging.basicConfig( @@ -158,7 +160,7 @@ def get_rate_limit_wait_seconds(error_obj, default_delay): now_ts = int(time.time()) reset_ts = int(reset_value) wait_seconds = max(reset_ts - now_ts + 1, default_delay) - return wait_seconds + return min(wait_seconds, BSKY_BLOB_UPLOAD_MAX_DELAY) except Exception: pass @@ -244,11 +246,12 @@ def prepare_post_text(text): """ Prepare the final public text exactly as it should be posted to Bluesky. Does NOT append the source X URL. + Enforces the Bluesky text limit. """ raw_text = (text or "").strip() - if len(raw_text) > 295: - truncated = raw_text[:290] + if len(raw_text) > BSKY_TEXT_MAX_LENGTH: + truncated = raw_text[:BSKY_TEXT_MAX_LENGTH - 3] last_space = truncated.rfind(" ") if last_space > 0: raw_text = truncated[:last_space] + "..." @@ -342,6 +345,31 @@ def build_text_media_key(normalized_text, media_fingerprint): return hashlib.sha256(f"{normalized_text}||{media_fingerprint}".encode("utf-8")).hexdigest() +def create_bsky_client(base_url, handle, password): + """ + Create a Bluesky/ATProto client pointed at the desired PDS or service host. + Supports custom hosts like eurosky.social. + """ + normalized_base_url = (base_url or DEFAULT_BSKY_BASE_URL).strip().rstrip("/") + logging.info(f"🔐 Connecting Bluesky client via base URL: {normalized_base_url}") + + try: + client = Client(base_url=normalized_base_url) + except TypeError: + logging.warning("⚠️ Your atproto Client does not accept base_url in constructor. Falling back.") + client = Client() + try: + if hasattr(client, "base_url"): + client.base_url = normalized_base_url + elif hasattr(client, "_base_url"): + client._base_url = normalized_base_url + except Exception as e: + logging.warning(f"⚠️ Could not apply custom base URL cleanly: {e}") + + client.login(handle, password) + return client + + # --- Local State Management --- def default_state(): return { @@ -1031,8 +1059,11 @@ def sync_feeds(args): logging.warning("⚠️ No tweets found or failed to fetch. Skipping Bluesky sync for this cycle.") return - bsky_client = Client() - bsky_client.login(args.bsky_handle, args.bsky_password) + bsky_client = create_bsky_client( + args.bsky_base_url, + args.bsky_handle, + args.bsky_password + ) recent_bsky_posts = get_recent_bsky_posts( bsky_client, @@ -1258,6 +1289,7 @@ def main(): parser.add_argument("--twitter-handle", help="The Twitter account to scrape") parser.add_argument("--bsky-handle", help="Your Bluesky handle") parser.add_argument("--bsky-password", help="Your Bluesky app password") + parser.add_argument("--bsky-base-url", help="Bluesky/ATProto PDS base URL, e.g. https://eurosky.social") args = parser.parse_args() @@ -1267,6 +1299,7 @@ def main(): args.bsky_handle = args.bsky_handle or os.getenv("BSKY_HANDLE") args.bsky_password = args.bsky_password or os.getenv("BSKY_APP_PASSWORD") args.twitter_handle = args.twitter_handle or os.getenv("TWITTER_HANDLE") or args.twitter_username + args.bsky_base_url = args.bsky_base_url if args.bsky_base_url else DEFAULT_BSKY_BASE_URL missing_args = [] if not args.twitter_username: @@ -1283,9 +1316,10 @@ def main(): return logging.info(f"🤖 Bot started. Will check @{args.twitter_handle}") + logging.info(f"🌍 Posting destination base URL: {args.bsky_base_url}") sync_feeds(args) logging.info("🤖 Bot finished.") if __name__ == "__main__": - main() + main() \ No newline at end of file