bsky mgm
This commit is contained in:
159
bsky/followback.py
Normal file
159
bsky/followback.py
Normal 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()
|
||||
Reference in New Issue
Block a user