Files
scripts/bsky/followback.py
Guillem Hernandez Sola 621ff10fbb bsky mgm
2026-04-09 11:53:37 +02:00

159 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()