├── .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) 2025 C. Bernard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /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 | ## Progress Update // (02/15/2025) 12 | 13 | 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. 14 | 15 | ## Progress Update // (02/19/2025) 16 | 17 | ### Features Implemented (app.py): 18 | 19 | 1. Error Checking and Halt on Failure: Stops the process if data retrieval fails. 20 | 2. Request Security: Monitors API limits and halts requests when approaching limits. 21 | 3. Dry Run Mode: Displays planned actions without executing them, with user confirmation. 22 | 4. Automatic Follow/Unfollow: Enables or disables follow/unfollow actions. 23 | 5. Statistics Display: Shows follower/following stats and remaining requests. 24 | 6. Blacklist Management: Allows management of a blacklist to ignore certain users. 25 | 26 | - GUI Development: Planned as the next major step, focusing on user interface and experience. 27 | 28 | --- 29 | 30 | # GitHub Follower Management Script 31 | 32 | [**Download the latest version here**](https://github.com/cfrBernard/GitHub-Follower-Management/releases) 33 | 34 | ![Version](https://img.shields.io/badge/version-v2.2.0-blue) 35 | ![License](https://img.shields.io/github/license/cfrBernard/MaskMapWizard) 36 | 37 | ## Features: 38 | - **Follow Back Followers**: Automatically follow back users who follow you. 39 | - **Unfollow Non-Followers**: Unfollow users who don't follow you back. 40 | - **Blacklist Management**: Manage a list of users to ignore for follow/unfollow actions. 41 | - **Rate Limit Monitoring**: Track and manage API rate limits to avoid hitting them. 42 | 43 | --- 44 | 45 | ## 🛠 Development Setup 46 | 47 | ### Prerequisites: 48 | - Python 3.x installed on your machine. 49 | - A personal access token from GitHub with the necessary permissions to manage your subscriptions. 50 | 51 | ### Installation: 52 | 1. Clone this repository or download the script: 53 | ```bash 54 | git clone https://github.com/cfrBernard/GitHub-Follower-Management.git 55 | cd GitHub-Follower-Management 56 | ``` 57 | 2. Install the required libraries: 58 | ```bash 59 | pip install requests 60 | ``` 61 | **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. 62 | 63 | ### Configuration: 64 | 1. Create a `config.txt` file in the same directory as the script. 65 | 2. Add the following lines to `config.txt`: 66 | ```text 67 | GITHUB_TOKEN=your_personal_access_token 68 | GITHUB_USERNAME=your_github_username 69 | BLACKLIST=user1,user2,user3 # comma-separated list of usernames to ignore 70 | ``` 71 | 3. Run the script: 72 | ```bash 73 | python github_followback.py 74 | ``` 75 | 4. The GUI will appear, and you can adjust settings as needed. 76 | 77 | ## Usage: 78 | 1. Enter your GitHub username and token in the provided fields. 79 | 2. Enter any usernames you want to ignore in the designated text area. Each username should be on a new line. 80 | 3. After entering or changing any settings, click the **Update Config** button to save the changes. 81 | 4. After configuring your settings, click the **Start** button. 82 | - The application will display output messages in the text area, informing you about the actions taken and requests limit. 83 | 84 | ## API Rate Limits: 85 | - **Authenticated users**: 5000 requests/hour 86 | - **Unauthenticated users**: 60 requests/hour 87 | 88 | > **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. 89 | 90 | --- 91 | 92 | ## License: 93 | This project is licensed under the MIT License. See the [LICENSE](./LICENSE.md) file for details. 94 | 95 | --- 96 | 97 | > **Note**: A MacOS version will be released in the future. Maybe.. 98 | -------------------------------------------------------------------------------- /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/cfrBernard/GitHub-Follower-Management/a0aeb741869324715c4de58a16deae37b079f9af/icon.ico --------------------------------------------------------------------------------