├── .gitignore ├── LICENSE.md ├── README.md ├── github_followback.py ├── github_followback.spec └── icon.ico /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | 4 | config.txt -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## MIT License 2 | 3 | Copyright (c) [2024] [C. Bernard] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated files (the "Software"), to use, reproduce, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the Software or the use or other dealings in the Software. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | ## ⚠️ Important Notice // (01/31/2025) 4 | 5 | Please be aware that all versions of this tool currently have a **major issue** that affects users with several thousand followers/following. 🚨 6 | 7 | If your account has **thousands of followers or following**, **I strongly advise against using this tool at the moment**. There are known issues with pagination and the handling of large user lists, which may result in incomplete or incorrect data being processed. 8 | 9 | 💡 **I am actively working on fixing this issue**, and a more stable version will be available soon. Thank you for your patience and understanding! 10 | 11 | Stay tuned for updates! 🔧🚀 12 | 13 | ## Update // (02/15/2025) 🛠 14 | 15 | I have made progress on the refactoring and created a test script (`app.py` in the `dev` branch) that focuses solely on data collection. The script is highly robust, and everything appears to be functioning correctly. The next step is to rebuild the application around this script. 16 | 17 | --- 18 | 19 | # GitHub Follower Management Script 20 | 21 | ## Features: 22 | - **Follow Back Followers**: Automatically follow back users who follow you. 23 | - **Unfollow Non-Followers**: Unfollow users who don't follow you back. 24 | - **Blacklist Management**: Manage a list of users to ignore for follow/unfollow actions. 25 | - **Rate Limit Monitoring**: Track and manage API rate limits to avoid hitting them. 26 | 27 | --- 28 | 29 | ## Quick Download 30 | For the latest '.exe' version, go to the [Releases](https://github.com/cfrBernard/GitHub-Follower-Management/releases) page. 31 | 32 | --- 33 | 34 | ## Prerequisites: 35 | - Python 3.x installed on your machine. 36 | - A personal access token from GitHub with the necessary permissions to manage your subscriptions. 37 | 38 | ## Installation: 39 | 1. Clone this repository or download the script: 40 | ```bash 41 | git clone https://github.com/cfrBernard/GitHub-Follower-Management.git 42 | cd GitHub-Follower-Management 43 | ``` 44 | 2. Install the required libraries: 45 | ```bash 46 | pip install requests 47 | ``` 48 | **Note**: `tkinter` comes pre-installed with Python on most systems, but you can check if it's available by trying to import it in a Python shell. 49 | 50 | ## Configuration: 51 | 1. Create a `config.txt` file in the same directory as the script. 52 | 2. Add the following lines to `config.txt`: 53 | ```text 54 | GITHUB_TOKEN=your_personal_access_token 55 | GITHUB_USERNAME=your_github_username 56 | BLACKLIST=user1,user2,user3 # comma-separated list of usernames to ignore 57 | ``` 58 | 3. Run the script: 59 | ```bash 60 | python github_followback.py 61 | ``` 62 | 4. The GUI will appear, and you can adjust settings as needed. 63 | 64 | ## Usage: 65 | 1. Enter your GitHub username and token in the provided fields. 66 | 2. Enter any usernames you want to ignore in the designated text area. Each username should be on a new line. 67 | 3. After entering or changing any settings, click the **Update Config** button to save the changes. 68 | 4. After configuring your settings, click the **Start** button. 69 | - The application will display output messages in the text area, informing you about the actions taken and requests limit. 70 | 71 | ## API Rate Limits: 72 | - **Authenticated users**: 5000 requests/hour 73 | - **Unauthenticated users**: 60 requests/hour 74 | 75 | **Example**: For 150 followers, 120 following, and actions like following 10 new users and unfollowing 5, only 24 requests are used—well within the authenticated limit. 76 | 77 | ## License: 78 | This project is licensed under the MIT License. See the LICENSE file for details. 79 | 80 | --- 81 | 82 | **Note**: A MacOS version will be released in the future. Maybe.. 83 | -------------------------------------------------------------------------------- /github_followback.py: -------------------------------------------------------------------------------- 1 | import requests # Importing the requests library to handle HTTP requests 2 | import tkinter as tk # Importing the tkinter library for the GUI 3 | from tkinter import ttk # Importing ttk module for themed widgets 4 | import time # Importing time for managing time-related functions 5 | import os # Importing os for file path operations 6 | 7 | class GitHubManager: 8 | def __init__(self): 9 | """Initialize the GitHubManager class and load configuration.""" 10 | self.requests_made = 0 # Counter for API requests made 11 | self.blacklist = set() # Set of usernames to be ignored 12 | self.github_token = "" # GitHub token for authentication 13 | self.github_username = "" # GitHub username for API calls 14 | self.headers = {} # HTTP headers for API requests 15 | self.load_config() # Load configuration from file 16 | self.validate_token() # Validate the GitHub token 17 | 18 | def load_config(self): 19 | """Load GitHub token, username, and blacklist from config.txt.""" 20 | if os.path.exists('config.txt'): # Check if the config file exists 21 | with open('config.txt', 'r') as file: # Open the file for reading 22 | for line in file: 23 | line = line.strip() # Remove leading and trailing whitespace 24 | if "=" in line: # Ensure the line is a key-value pair 25 | key, value = line.split('=', 1) # Split into key and value 26 | if key == "GITHUB_TOKEN": 27 | self.github_token = value.strip() # Set GitHub token 28 | self.headers = {"Authorization": f"token {self.github_token}"} # Set headers 29 | elif key == "GITHUB_USERNAME": 30 | self.github_username = value.strip() # Set GitHub username 31 | elif key == "BLACKLIST": 32 | self.blacklist = set(value.strip().split(',')) # Load blacklist from config 33 | 34 | def validate_token(self): 35 | """Validate the GitHub token by making a simple API request.""" 36 | if self.github_token: # Check if the token is set 37 | response = requests.get("https://api.github.com/user", headers=self.headers) # Make a request to GitHub API 38 | if response.status_code != 200: # Check if the response is not OK 39 | self.github_token = "" # Reset token if invalid 40 | return "Invalid token." # Return an error message 41 | return None # Return None if the token is valid 42 | 43 | def save_config(self): 44 | """Save the current configuration (token, username, and blacklist) to config.txt.""" 45 | with open('config.txt', 'w') as file: # Open the file for writing 46 | file.write(f"GITHUB_TOKEN={self.github_token}\n") # Write token 47 | file.write(f"GITHUB_USERNAME={self.github_username}\n") # Write username 48 | file.write(f"BLACKLIST={','.join(self.blacklist)}\n") # Write blacklist 49 | 50 | def api_request(self, url): 51 | """Generic API request handler.""" 52 | if not self.github_token: # Check if the token is provided 53 | return {"error": "No token provided."} # Return an error message 54 | try: 55 | response = requests.get(url, headers=self.headers) # Make the API request 56 | self.requests_made += 1 # Increment request counter 57 | response.raise_for_status() # Raise an error for bad responses 58 | return response.json() # Return the JSON response 59 | except requests.RequestException as e: # Handle any request exceptions 60 | return {"error": f"API Error: {str(e)}"} # Return the error message 61 | 62 | def get_users(self, username, action): 63 | """Retrieve users (followers or following) and apply blacklist.""" 64 | url = f"https://api.github.com/users/{username}/{action}" # Construct the API URL 65 | users = [] # Initialize a list to store usernames 66 | page = 1 # Start from the first page 67 | 68 | while True: 69 | full_url = f"{url}?per_page=100&page={page}" # URL for paginated requests 70 | page_users = self.api_request(full_url) # Get users from API 71 | 72 | if "error" in page_users: # Check for errors 73 | break 74 | 75 | if not page_users: # If no more users, exit the loop 76 | break 77 | 78 | # Add users to the list if they are not in the blacklist 79 | users.extend(user['login'] for user in page_users if user['login'] not in self.blacklist) 80 | page += 1 # Move to the next page 81 | 82 | return set(users) # Return a set of usernames to avoid duplicates 83 | 84 | def follow_user(self, username_to_follow): 85 | """Follow a user.""" 86 | return self._manage_following("PUT", username_to_follow) # Call the private method to follow 87 | 88 | def unfollow_user(self, username_to_unfollow): 89 | """Unfollow a user.""" 90 | return self._manage_following("DELETE", username_to_unfollow) # Call the private method to unfollow 91 | 92 | def _manage_following(self, method, username): 93 | """Manage following/unfollowing users.""" 94 | url = f"https://api.github.com/user/following/{username}" # Construct the API URL 95 | response = requests.request(method, url, headers=self.headers) # Make the request (PUT/DELETE) 96 | self.requests_made += 1 # Increment request counter 97 | return response.status_code == 204 # Return True if the action was successful 98 | 99 | def display_rate_limits(self): 100 | """Display the current rate limits.""" 101 | response = requests.get("https://api.github.com/user", headers=self.headers) # Make a request to check rate limits 102 | if response.status_code == 200: # If the response is OK 103 | limits = response.headers # Get response headers 104 | reset_in_seconds = int(limits['X-RateLimit-Reset']) - int(time.time()) # Calculate time until reset 105 | return (limits['X-RateLimit-Limit'], limits['X-RateLimit-Remaining'], reset_in_seconds) # Return limits and reset time 106 | else: 107 | raise Exception(f"Rate Limit Error: {response.status_code}") # Raise an error for failed request 108 | 109 | class App: 110 | def __init__(self, root): 111 | """Initialize the App class and set up the user interface.""" 112 | self.github_manager = GitHubManager() # Create an instance of GitHubManager 113 | self.root = root # Save the root window reference 114 | self.setup_ui() # Set up the user interface 115 | 116 | def setup_ui(self): 117 | """Set up the user interface.""" 118 | self.root.title("GitHub Follower Management") # Set the window title 119 | self.root.geometry("412x500") # Set the window size 120 | self.root.configure(bg="#e0e0e0") # Set the background color 121 | 122 | # Frame for user input 123 | frame_user = tk.Frame(self.root, bg="#e0e0e0", padx=20, pady=0) # Create a frame 124 | frame_user.pack(pady=0) # Add the frame to the window 125 | 126 | # Create input fields for GitHub username and token 127 | tk.Label(frame_user, text="GitHub Username:", bg="#e0e0e0").grid(row=0, column=0, sticky="e", padx=5, pady=5) 128 | self.entry_username = tk.Entry(frame_user, width=25) # Entry for username 129 | self.entry_username.insert(0, self.github_manager.github_username) # Pre-fill with saved username 130 | self.entry_username.grid(row=0, column=1, padx=5, pady=5) 131 | 132 | tk.Label(frame_user, text="GitHub Token:", bg="#e0e0e0").grid(row=1, column=0, sticky="e", padx=5, pady=5) 133 | self.entry_token = tk.Entry(frame_user, width=25, show="*") # Entry for token 134 | self.entry_token.insert(0, self.github_manager.github_token) # Pre-fill with saved token 135 | self.entry_token.grid(row=1, column=1, padx=5, pady=5) 136 | 137 | # Boolean variables for checkboxes 138 | self.var_follow_back = tk.BooleanVar() # Follow back followers checkbox 139 | self.var_unfollow_non_followers = tk.BooleanVar() # Unfollow non-followers checkbox 140 | 141 | # Create checkboxes for following/unfollowing options 142 | tk.Checkbutton(frame_user, text="Follow Back Followers", variable=self.var_follow_back, bg="#e0e0e0").grid(row=2, column=0, sticky="w", padx=5, pady=5) 143 | tk.Checkbutton(frame_user, text="Unfollow Non-Followers", variable=self.var_unfollow_non_followers, bg="#e0e0e0").grid(row=3, column=0, sticky="w", padx=5, pady=5) 144 | 145 | # Create a text area for blacklist usernames 146 | tk.Label(frame_user, text="Blacklist Usernames (one per line):", bg="#e0e0e0").grid(row=4, column=0, sticky="e", padx=5, pady=5) 147 | self.blacklist_entry = tk.Text(frame_user, height=5, width=20) # Text area for blacklist 148 | self.blacklist_entry.insert(tk.END, "\n".join(self.github_manager.blacklist)) # Pre-fill with saved blacklist 149 | self.blacklist_entry.grid(row=4, column=1, padx=5, pady=5) 150 | 151 | # Create buttons for saving configuration and starting actions 152 | ttk.Button(frame_user, text="Save", command=self.update_config).grid(row=5, column=0, columnspan=2, pady=10) 153 | ttk.Button(frame_user, text="Start", command=self.start_actions).grid(row=6, column=0, columnspan=2, pady=10) 154 | 155 | self.progress = ttk.Progressbar(self.root, orient="horizontal", length=412, mode="determinate") # Progress bar for actions 156 | self.progress.pack(pady=0) # Add progress bar to the window 157 | 158 | # Output area for messages 159 | self.text_output = tk.Text(self.root, height=15, width=58, bg="#e0e0e0", font=("Arial", 10)) # Text area for output 160 | self.scrollbar = tk.Scrollbar(self.root, command=self.text_output.yview) # Scrollbar for output area 161 | self.text_output.config(yscrollcommand=self.scrollbar.set) # Link scrollbar to text area 162 | 163 | self.text_output.pack(pady=0) # Add text output area to the window 164 | 165 | def start_actions(self): 166 | """Start following and unfollowing actions based on user input.""" 167 | self.github_manager.requests_made = 0 # Reset request counter 168 | self.text_output.delete(1.0, tk.END) # Clear previous output 169 | username = self.entry_username.get().strip() # Get username from entry 170 | token = self.entry_token.get().strip() # Get token from entry 171 | 172 | self.github_manager.github_username = username # Update manager with new username 173 | self.github_manager.github_token = token # Update manager with new token 174 | 175 | if token: # If token is provided 176 | self.github_manager.headers = {"Authorization": f"token {token}"} # Set authorization header 177 | token_error = self.github_manager.validate_token() # Validate the token 178 | if token_error: # If token validation fails 179 | self.text_output.insert(tk.END, f"Error: {token_error}\n") # Display error 180 | return 181 | else: 182 | self.text_output.insert(tk.END, "Error: No token provided.\n") # Display error if no token 183 | return 184 | 185 | if not username: # Check if username is provided 186 | self.text_output.insert(tk.END, "Error: Please enter a username.\n") # Display error if no username 187 | return 188 | 189 | # Retrieve followers and following lists 190 | followers_response = self.github_manager.get_users(username, "followers") 191 | if "error" in followers_response: # Handle error in followers retrieval 192 | self.text_output.insert(tk.END, f"Error: {followers_response['error']}\n") 193 | return 194 | 195 | following_response = self.github_manager.get_users(username, "following") 196 | if "error" in following_response: # Handle error in following retrieval 197 | self.text_output.insert(tk.END, f"Error: {following_response['error']}\n") 198 | return 199 | 200 | total_users = 0 # Initialize counter for total actions 201 | if self.var_follow_back.get(): # If follow back option is selected 202 | to_follow = followers_response - following_response # Determine users to follow 203 | total_users += len(to_follow) # Count total users to follow 204 | 205 | if self.var_unfollow_non_followers.get(): # If unfollow non-followers option is selected 206 | to_unfollow = following_response - followers_response # Determine users to unfollow 207 | total_users += len(to_unfollow) # Count total users to unfollow 208 | 209 | self.progress["maximum"] = total_users # Set maximum for progress bar 210 | self.progress["value"] = 0 # Reset progress bar value 211 | 212 | if self.var_follow_back.get(): # Follow back users if option is selected 213 | to_follow = followers_response - following_response # Get users to follow back 214 | for user in to_follow: # Iterate over each user 215 | if self.github_manager.follow_user(user): # Follow user 216 | self.text_output.insert(tk.END, f"Followed {user}\n") # Output success message 217 | self.progress["value"] += 1 # Update progress bar 218 | self.root.update_idletasks() # Update UI 219 | 220 | if self.var_unfollow_non_followers.get(): # Unfollow users if option is selected 221 | to_unfollow = following_response - followers_response # Get users to unfollow 222 | for user in to_unfollow: # Iterate over each user 223 | if self.github_manager.unfollow_user(user): # Unfollow user 224 | self.text_output.insert(tk.END, f"Unfollowed {user}\n") # Output success message 225 | self.progress["value"] += 1 # Update progress bar 226 | self.root.update_idletasks() # Update UI 227 | 228 | self.display_rate_limits() # Display rate limits after actions 229 | 230 | def display_rate_limits(self): 231 | """Display the current rate limits after actions are performed.""" 232 | try: 233 | rate_limit, remaining, reset_in_seconds = self.github_manager.display_rate_limits() # Get rate limits 234 | 235 | output = ( 236 | f"Total requests made: {self.github_manager.requests_made} | " 237 | f"Rate Limit: {rate_limit} | " 238 | f"Requests Remaining: {remaining} | " 239 | f"Rate Limit Reset in: {reset_in_seconds // 60} minutes {reset_in_seconds % 60} seconds\n" 240 | ) # Format output string 241 | 242 | self.text_output.insert(tk.END, output) # Display rate limit information 243 | except Exception as e: # Handle any exceptions 244 | self.text_output.insert(tk.END, str(e) + "\n") # Display error message 245 | 246 | def update_config(self): 247 | """Update the configuration based on user input and save to config.txt.""" 248 | users = self.blacklist_entry.get("1.0", tk.END).strip().splitlines() # Get blacklist from text area 249 | self.github_manager.blacklist = {user.strip() for user in users if user.strip()} # Update blacklist 250 | self.github_manager.github_username = self.entry_username.get().strip() # Update username 251 | self.github_manager.github_token = self.entry_token.get().strip() # Update token 252 | self.github_manager.save_config() # Save updated configuration 253 | self.text_output.insert(tk.END, "Configuration has been updated successfully.\n") # Display success message 254 | 255 | if __name__ == "__main__": 256 | root = tk.Tk() # Create the main window 257 | app = App(root) # Create an instance of the App class 258 | root.mainloop() # Start the GUI event loop 259 | -------------------------------------------------------------------------------- /github_followback.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | a = Analysis( 5 | ['github_followback.py'], 6 | pathex=[], 7 | binaries=[], 8 | datas=[], 9 | hiddenimports=[], 10 | hookspath=[], 11 | hooksconfig={}, 12 | runtime_hooks=[], 13 | excludes=[], 14 | noarchive=False, 15 | optimize=0, 16 | ) 17 | pyz = PYZ(a.pure) 18 | 19 | exe = EXE( 20 | pyz, 21 | a.scripts, 22 | a.binaries, 23 | a.datas, 24 | [], 25 | name='github_followback', 26 | debug=False, 27 | bootloader_ignore_signals=False, 28 | strip=False, 29 | upx=True, 30 | upx_exclude=[], 31 | runtime_tmpdir=None, 32 | console=False, 33 | disable_windowed_traceback=False, 34 | argv_emulation=False, 35 | target_arch=None, 36 | codesign_identity=None, 37 | entitlements_file=None, 38 | icon=['icon.ico'], 39 | ) 40 | -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/light-hat/GitHub-Follower-Management/e6d989d33afe6bd21c6c8ad797953a3a49be0b65/icon.ico --------------------------------------------------------------------------------