Skip to content

Instantly share code, notes, and snippets.

@tatsumoto-ren
Created December 14, 2023 03:13
Show Gist options
  • Save tatsumoto-ren/37de04595f4454ad5665bf1d7c3840de to your computer and use it in GitHub Desktop.
Save tatsumoto-ren/37de04595f4454ad5665bf1d7c3840de to your computer and use it in GitHub Desktop.
#!/usr/bin/python3
# This program takes your GitHub username and auth token and prints two lists of usernames:
# ・you should unfollow = people who aren't following you.
# ・you should follow = people who are following you.
# If you choose to proceed, the program follows or unfollows the users on your behalf.
import argparse
import json
from enum import Enum, auto
from pprint import pprint
from typing import Collection, Any, NamedTuple
import requests
RED = '\033[0;31m'
GREEN = '\033[0;32m'
NC = '\033[0m'
class Auth(NamedTuple):
user: str
access_token: str
class Action(Enum):
unfollow = auto()
follow = auto()
def __str__(self):
return f"{RED if self.name == 'unfollow' else GREEN}{self.name}{NC}"
def format_num(idx: int, length: int) -> str:
return f'{" " * (len(str(length)) - len(str(idx)))}{idx}'
def parse_args():
parser = argparse.ArgumentParser(description="Synchronize followers and following on GitHub.")
parser.add_argument('-u', '--user', type=str, required=True)
parser.add_argument('-t', '--token', type=str, required=True)
parser.add_argument(
'-n', '--noconfirm', required=False, default=False, action='store_true',
help="Don't ask for confirmation.",
)
return parser.parse_args()
class GitHubClient:
def __init__(self):
args = parse_args()
self.auth = Auth(args.user, args.token)
self.noconfirm = args.noconfirm
with requests.session() as self.session:
self.session.auth = self.auth
self.session.headers.update({'Accept': 'application/vnd.github.v3+json'})
self.run()
def requests_users(self, tab: str) -> set[str]:
def get_page() -> list[dict[str, Any]]:
return self.session.get(
f'https://api.github.com/users/{self.auth.user}/{tab}',
params={'per_page': '100', 'page': page_n},
).json()
page_n = 1
total_users = []
while users := get_page():
if type(users) != list:
break
total_users.extend(users)
page_n += 1
return {user['login'] for user in total_users}
def unfollow(self, to_unfollow: set[str]):
for user in to_unfollow:
resp = self.session.delete(f'https://api.github.com/user/following/{user}')
print(f"Unfollow {user}: {'Ok' if resp.status_code == 204 else 'Failed'}")
def follow(self, to_follow: set[str]):
for user in to_follow:
resp = self.session.put(
f'https://api.github.com/user/following/{user}',
headers={"Content-Length": "0"},
)
print(f"Follow {user}: {'Ok' if resp.status_code == 204 else 'Failed'}")
def print_users(self, users: Collection[str], action: Action) -> None:
"""
Print the program's result.
:param users: an iterable of usernames
:param action: either follow or unfollow
:return: None
"""
if users:
print(f"{self.auth.user} should {action} {len(users)} users:")
for i, name in enumerate(users):
print(format_num(i + 1, len(users)), name)
else:
print(f"{self.auth.user} doesn't have any people to {action} at the moment.")
def run(self):
followers = self.requests_users('followers')
following = self.requests_users('following')
to_unfollow = following - followers
to_follow = followers - following
print(f"Following: {len(following)}, Followers: {len(followers)}")
self.print_users(to_unfollow, Action.unfollow)
self.print_users(to_follow, Action.follow)
if (to_unfollow or to_follow) and (self.noconfirm or input("Proceed? [y/N] ").lower() == 'y'):
self.unfollow(to_unfollow)
self.follow(to_follow)
if __name__ == '__main__':
GitHubClient()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment