import argparse import logging import sys import time from atproto import Client # --- Logging --- logging.basicConfig( format="%(asctime)s [%(levelname)s] %(message)s", level=logging.INFO, stream=sys.stdout ) def login(email: str, password: str) -> Client: """Login to Bluesky using email and app password.""" client = Client() try: logging.info(f"Attempting login with email: {email}") client.login(email, password) logging.info("✅ Login successful!") return client except Exception as e: logging.error(f"❌ Login failed: {e}") sys.exit(1) def get_my_handle(client) -> str: """Retrieve the authenticated user's handle.""" return client.me.handle def get_followers(client, handle: str) -> dict: """Fetches all accounts that follow the user. Returns {handle: did}.""" logging.info("📋 Fetching your followers...") followers = [] cursor = None while True: try: params = {'actor': handle, 'cursor': cursor, 'limit': 100} res = client.app.bsky.graph.get_followers(params) followers.extend(res.followers) if not res.cursor: break cursor = res.cursor time.sleep(0.3) except Exception as e: logging.warning(f"⚠️ Error fetching followers: {e}") break result = {user.handle: user.did for user in followers} logging.info(f"👥 You have {len(result)} followers.") return result def get_following(client, handle: str) -> set: """Fetches all accounts the user follows. Returns a set of handles.""" logging.info("📋 Fetching accounts you follow...") follows = [] cursor = None while True: try: params = {'actor': handle, 'cursor': cursor, 'limit': 100} res = client.app.bsky.graph.get_follows(params) follows.extend(res.follows) if not res.cursor: break cursor = res.cursor time.sleep(0.3) except Exception as e: logging.warning(f"⚠️ Error fetching following: {e}") break result = {user.handle for user in follows} logging.info(f"➡️ You are following {len(result)} accounts.") return result def follow_back(client, followers: dict, following: set) -> None: """Follows back anyone who follows you but you don't follow back.""" not_followed_back = { handle: did for handle, did in followers.items() if handle not in following } if not not_followed_back: logging.info("🎉 You already follow back everyone who follows you!") return logging.info(f"➕ Found {len(not_followed_back)} followers to follow back...") follow_count = 0 fail_count = 0 for handle, did in not_followed_back.items(): try: client.app.bsky.graph.follow.create( repo=client.me.did, record={ "$type": "app.bsky.graph.follow", "subject": did, "createdAt": client.get_current_time_iso() } ) logging.info(f" ✅ Followed back: @{handle}") follow_count += 1 time.sleep(0.5) except Exception as e: logging.error(f" ❌ Failed to follow back @{handle}: {e}") fail_count += 1 logging.info(f"✅ Followed back {follow_count} accounts. ({fail_count} failed)") def main(): parser = argparse.ArgumentParser( description="Follow back all Bluesky users who follow you but you don't follow back." ) parser.add_argument("bsky_email", help="Bluesky login email (e.g., you@email.com)") parser.add_argument("bsky_app_password", help="Bluesky app password (from Settings > App Passwords)") parser.add_argument("--dry_run", action="store_true", help="Preview who would be followed without actually following") args = parser.parse_args() # --- Login --- client = login(args.bsky_email, args.bsky_app_password) my_handle = get_my_handle(client) logging.info(f"👤 Logged in as: @{my_handle}") # --- Fetch data --- followers = get_followers(client, my_handle) following = get_following(client, my_handle) # --- Diff --- not_followed_back = { handle: did for handle, did in followers.items() if handle not in following } logging.info(f"📊 Summary:") logging.info(f" Followers : {len(followers)}") logging.info(f" Following : {len(following)}") logging.info(f" To follow back : {len(not_followed_back)}") # --- Dry run preview --- if args.dry_run: logging.info("🔍 Dry run — no follows will be made. Accounts to follow back:") for handle in not_followed_back: print(f" → @{handle}") return # --- Follow back --- follow_back(client, followers, following) logging.info("🎉 All done!") if __name__ == "__main__": main()