Some scripts
This commit is contained in:
196
bsky/unfollow_non_unfollowers.py
Normal file
196
bsky/unfollow_non_unfollowers.py
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# AUTH
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def login(email: str, password: str) -> Client:
|
||||||
|
client = Client()
|
||||||
|
try:
|
||||||
|
logging.info(f"Attempting login: {email}")
|
||||||
|
client.login(email, password)
|
||||||
|
logging.info("✅ Login successful!")
|
||||||
|
return client
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"❌ Login failed: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# FETCH DATA
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def get_all_followers(client, handle: str) -> set:
|
||||||
|
"""Returns a set of DIDs of everyone who follows you."""
|
||||||
|
logging.info("📋 Fetching your followers...")
|
||||||
|
followers = set()
|
||||||
|
cursor = None
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
params = {'actor': handle, 'cursor': cursor, 'limit': 100}
|
||||||
|
res = client.app.bsky.graph.get_followers(params)
|
||||||
|
for user in res.followers:
|
||||||
|
followers.add(user.did)
|
||||||
|
if not res.cursor:
|
||||||
|
break
|
||||||
|
cursor = res.cursor
|
||||||
|
time.sleep(0.3)
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"⚠️ Error fetching followers: {e}")
|
||||||
|
break
|
||||||
|
logging.info(f"👥 You have {len(followers)} followers.")
|
||||||
|
return followers
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_following(client, handle: str) -> dict:
|
||||||
|
"""Returns {did: user_object} of everyone you follow."""
|
||||||
|
logging.info("📋 Fetching accounts you follow...")
|
||||||
|
following = {}
|
||||||
|
cursor = None
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
params = {'actor': handle, 'cursor': cursor, 'limit': 100}
|
||||||
|
res = client.app.bsky.graph.get_follows(params)
|
||||||
|
for user in res.follows:
|
||||||
|
following[user.did] = user
|
||||||
|
if not res.cursor:
|
||||||
|
break
|
||||||
|
cursor = res.cursor
|
||||||
|
time.sleep(0.3)
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"⚠️ Error fetching following: {e}")
|
||||||
|
break
|
||||||
|
logging.info(f"➡️ You follow {len(following)} accounts.")
|
||||||
|
return following
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# DISPLAY
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def print_user_list(users: dict):
|
||||||
|
"""Prints a numbered list of users to unfollow."""
|
||||||
|
print(f"\n📤 YOU FOLLOW — THEY DON'T FOLLOW BACK ({len(users)} total):")
|
||||||
|
print("-" * 60)
|
||||||
|
for i, user in enumerate(users.values(), start=1):
|
||||||
|
display = f" ({user.display_name})" if user.display_name else ""
|
||||||
|
print(f" {i:>4}. @{user.handle}{display}")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# UNFOLLOW
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def unfollow_non_followers(client, to_unfollow: dict, dry_run: bool):
|
||||||
|
"""Prints the list, prompts for confirmation, then unfollows."""
|
||||||
|
|
||||||
|
if not to_unfollow:
|
||||||
|
logging.info("✅ Everyone you follow also follows you back!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Always print the full list first
|
||||||
|
print_user_list(to_unfollow)
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
logging.info("🔍 Dry run — no changes made.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Confirmation prompt
|
||||||
|
answer = input(f"\n❓ Unfollow these {len(to_unfollow)} accounts? (y/N): ").strip().lower()
|
||||||
|
if answer != 'y':
|
||||||
|
logging.info("❌ Cancelled — no one was unfollowed.")
|
||||||
|
return
|
||||||
|
|
||||||
|
logging.info("➖ Unfollowing...")
|
||||||
|
success = 0
|
||||||
|
failed = 0
|
||||||
|
|
||||||
|
for did, user in to_unfollow.items():
|
||||||
|
try:
|
||||||
|
if user.viewer and user.viewer.following:
|
||||||
|
rkey = user.viewer.following.split("/")[-1]
|
||||||
|
repo = user.viewer.following.split("/")[2]
|
||||||
|
client.com.atproto.repo.delete_record({
|
||||||
|
"repo": repo,
|
||||||
|
"collection": "app.bsky.graph.follow",
|
||||||
|
"rkey": rkey
|
||||||
|
})
|
||||||
|
logging.info(f" ✅ Unfollowed: @{user.handle}")
|
||||||
|
success += 1
|
||||||
|
else:
|
||||||
|
logging.warning(f" ⚠️ No follow record found for @{user.handle}")
|
||||||
|
failed += 1
|
||||||
|
time.sleep(0.5)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f" ❌ Failed to unfollow @{user.handle}: {e}")
|
||||||
|
failed += 1
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(f" ➖ Unfollowed : {success}")
|
||||||
|
print(f" ❌ Failed : {failed}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# MAIN
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Unfollow everyone on Bluesky who doesn't follow you back."
|
||||||
|
)
|
||||||
|
parser.add_argument("bsky_email", help="Bluesky login email or handle")
|
||||||
|
parser.add_argument("bsky_app_password", help="Bluesky app password (Settings > App Passwords)")
|
||||||
|
parser.add_argument(
|
||||||
|
"--dry_run",
|
||||||
|
action="store_true",
|
||||||
|
help="Print the list without unfollowing anyone"
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# --- Login ---
|
||||||
|
client = login(args.bsky_email, args.bsky_app_password)
|
||||||
|
my_handle = client.me.handle
|
||||||
|
logging.info(f"👤 Logged in as: @{my_handle}")
|
||||||
|
|
||||||
|
# --- Fetch both lists ---
|
||||||
|
followers = get_all_followers(client, my_handle)
|
||||||
|
following = get_all_following(client, my_handle)
|
||||||
|
|
||||||
|
# --- Compute who doesn't follow back ---
|
||||||
|
to_unfollow = {
|
||||||
|
did: user
|
||||||
|
for did, user in following.items()
|
||||||
|
if did not in followers
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Summary ---
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("📊 SUMMARY")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f" ➡️ Total following : {len(following)}")
|
||||||
|
print(f" 👥 Total followers : {len(followers)}")
|
||||||
|
print(f" 📤 Don't follow you back : {len(to_unfollow)}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# --- Unfollow ---
|
||||||
|
unfollow_non_followers(client, to_unfollow, dry_run=args.dry_run)
|
||||||
|
|
||||||
|
logging.info("🎉 All done!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user