This commit is contained in:
Guillem Hernandez Sola
2026-04-09 11:53:37 +02:00
parent 53dddb1f13
commit 621ff10fbb
4 changed files with 693 additions and 0 deletions

159
bsky/followback.py Normal file
View File

@@ -0,0 +1,159 @@
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()