├── .env.sample ├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── GithubAPIBot.py ├── LICENSE ├── README.md ├── bot_follow.py ├── bot_unfollow.py ├── logo.png └── requirements.txt /.env.sample: -------------------------------------------------------------------------------- 1 | GITHUB_USER=YOUR_GITHUB_USERNAME 2 | TOKEN=YOUR_GITHUB_PERSONAL_ACCESS_TOKEN -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '44 5 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | logs/ 3 | .env -------------------------------------------------------------------------------- /GithubAPIBot.py: -------------------------------------------------------------------------------- 1 | from base64 import b64encode 2 | import datetime 3 | import random 4 | import requests 5 | from requests.adapters import HTTPAdapter 6 | import time 7 | from tqdm import tqdm 8 | from urllib.parse import parse_qs 9 | from urllib.parse import urlparse 10 | from urllib3.util import Retry 11 | 12 | 13 | class GithubAPIBot: 14 | # Constructor 15 | def __init__( 16 | self, 17 | username: str, 18 | token: str, 19 | sleepSecondsActionMin: int, 20 | sleepSecondsActionMax: int, 21 | sleepSecondsLimitedMin: int, 22 | sleepSecondsLimitedMax: int, 23 | sleepHour=None, 24 | sleepMinute=None, 25 | sleepTime=None, 26 | maxAction=None, 27 | ): 28 | if not isinstance(username, str): 29 | raise TypeError("Missing/Incorrect username") 30 | if not isinstance(token, str): 31 | raise TypeError("Missing/Incorrect token") 32 | 33 | self.__username = username 34 | self.__token = token 35 | self.__sleepSecondsActionMin = sleepSecondsActionMin 36 | self.__sleepSecondsActionMax = sleepSecondsActionMax 37 | self.__sleepSecondsLimitedMin = sleepSecondsLimitedMin 38 | self.__sleepSecondsLimitedMax = sleepSecondsLimitedMax 39 | self.__sleepHour = sleepHour 40 | self.__sleepMinute = sleepMinute 41 | self.__sleepTime = sleepTime 42 | self.__maxAction = maxAction 43 | self.__usersToAction = [] 44 | self.__followings = [] 45 | 46 | # Requests' headers 47 | HEADERS = { 48 | "Authorization": "Basic " + b64encode(str(self.token + ":" + self.token).encode("utf-8")).decode("utf-8") 49 | } 50 | 51 | # Session 52 | self.session = requests.session() 53 | retries = Retry(total=5, backoff_factor=1, status_forcelist=[502, 503, 504]) 54 | self.session.mount("https://", HTTPAdapter(max_retries=retries)) 55 | self.session.headers.update(HEADERS) 56 | 57 | # Authenticate 58 | try: 59 | res = self.session.get("https://api.github.com/user") 60 | except requests.exceptions.RequestException as e: 61 | raise SystemExit(e) 62 | 63 | if res.status_code == 404: 64 | raise ValueError("\nFailure to Authenticate, please check Personal Access Token and Username!") 65 | else: 66 | print("\nSuccessful authentication.") 67 | 68 | self.getFollowings() 69 | 70 | # Getters & Setters 71 | @property 72 | def username(self): 73 | return self.__username 74 | 75 | @username.setter 76 | def username(self, value): 77 | self.__username = value 78 | 79 | @property 80 | def token(self): 81 | return self.__token 82 | 83 | @token.setter 84 | def token(self, value): 85 | self.__token = value 86 | 87 | @property 88 | def sleepSecondsActionMin(self): 89 | return self.__sleepSecondsActionMin 90 | 91 | @sleepSecondsActionMin.setter 92 | def sleepSecondsActionMin(self, value): 93 | self.__sleepSecondsActionMin = value 94 | 95 | @property 96 | def sleepSecondsActionMax(self): 97 | return self.__sleepSecondsActionMax 98 | 99 | @sleepSecondsActionMax.setter 100 | def sleepSecondsActionMax(self, value): 101 | self.__sleepSecondsActionMax = value 102 | 103 | @property 104 | def sleepSecondsLimitedMin(self): 105 | return self.__sleepSecondsLimitedMin 106 | 107 | @sleepSecondsLimitedMin.setter 108 | def sleepSecondsLimitedMin(self, value): 109 | self.__sleepSecondsLimitedMin = value 110 | 111 | @property 112 | def sleepSecondsLimitedMax(self): 113 | return self.__sleepSecondsLimitedMax 114 | 115 | @sleepSecondsLimitedMax.setter 116 | def sleepSecondsLimitedMax(self, value): 117 | self.__sleepSecondsLimitedMax = value 118 | 119 | @property 120 | def sleepHour(self): 121 | return self.__sleepHour 122 | 123 | @sleepHour.setter 124 | def sleepHour(self, value): 125 | self.__sleepHour = value 126 | 127 | @property 128 | def sleepMinute(self): 129 | return self.__sleepMinute 130 | 131 | @sleepMinute.setter 132 | def sleepMinute(self, value): 133 | self.__sleepMinute = value 134 | 135 | @property 136 | def sleepTime(self): 137 | return self.__sleepTime 138 | 139 | @sleepTime.setter 140 | def sleepTime(self, value): 141 | self.__sleepTime = value 142 | 143 | @property 144 | def maxAction(self): 145 | return self.__maxAction 146 | 147 | @maxAction.setter 148 | def maxAction(self, value): 149 | self.__maxAction = value 150 | 151 | @property 152 | def usersToAction(self): 153 | return self.__usersToAction 154 | 155 | @usersToAction.setter 156 | def usersToAction(self, value): 157 | self.__usersToAction = value 158 | 159 | @property 160 | def followings(self): 161 | return self.__followings 162 | 163 | @followings.setter 164 | def followings(self, value): 165 | self.__followings = value 166 | 167 | def getUsers(self, url="", maxAction=None, following=False): 168 | users = [] 169 | 170 | try: 171 | res = self.session.get(url) 172 | except requests.exceptions.RequestException as e: 173 | raise SystemExit(e) 174 | 175 | # Get usernames from each page 176 | page = 1 177 | while True: 178 | try: 179 | res = self.session.get(url + "?page=" + str(page)).json() 180 | except requests.exceptions.RequestException as e: 181 | raise SystemExit(e) 182 | 183 | for user in res: 184 | # Check if we already have enough usernames 185 | if maxAction != None: 186 | if len(users) >= int(maxAction): 187 | break 188 | 189 | # Add username if it's not being followed already 190 | if ( 191 | not following 192 | and not (user["login"] in self.followings) 193 | or following 194 | and (user["login"] in self.followings) 195 | ): 196 | users.append(user["login"]) 197 | 198 | # Check if we already have enough usernames 199 | if maxAction != None: 200 | if len(users) >= int(maxAction): 201 | break 202 | 203 | if res == []: 204 | break 205 | else: 206 | page += 1 207 | 208 | return users 209 | 210 | def getFollowers(self, username=None, following=None): 211 | if username == None: 212 | username = self.username 213 | print(f"\nGrabbing {username}'s followers.\n") 214 | self.usersToAction.extend( 215 | self.getUsers( 216 | url=f"https://api.github.com/users/{username}/followers", 217 | maxAction=self.maxAction, 218 | following=following, 219 | ) 220 | ) 221 | 222 | def getFollowings(self, username=None): 223 | if username == None: 224 | username = self.username 225 | print(f"\nGrabbing {username}'s followings.\n") 226 | self.followings.extend(self.getUsers(url=f"https://api.github.com/users/{username}/following")) 227 | 228 | def run(self, action): 229 | if len(self.usersToAction) == 0: 230 | print(f"Nothing to {action}") 231 | else: 232 | 233 | # Users to follow/unfollow must not exceed the given max 234 | if self.maxAction != None: 235 | self.usersToAction = self.usersToAction[: min(len(self.usersToAction), int(self.maxAction))] 236 | 237 | # Time for the bot to go to sleep 238 | if self.sleepHour != None and self.sleepMinute != None and self.sleepTime != None: 239 | sleepTime = nextSleepTime(int(self.__sleepHour), int(self.sleepMinute)) 240 | 241 | # Start follow/unfollow 242 | print(f"\nStarting to {action}.\n") 243 | users = tqdm( 244 | self.usersToAction, 245 | initial=1, 246 | dynamic_ncols=True, 247 | smoothing=True, 248 | bar_format="[PROGRESS] {n_fmt}/{total_fmt} |{l_bar}{bar}|", 249 | position=0, 250 | leave=False, 251 | ) 252 | for user in users: 253 | 254 | # Set the bot to sleep at the set time 255 | if self.sleepHour != None and self.sleepMinute != None and self.sleepTime != None: 256 | timeNow = datetime.datetime.now() 257 | if timeNow.timestamp() > sleepTime.timestamp(): 258 | sleepTime = nextSleepTime(int(self.__sleepHour), int(self.__sleepMinute)) 259 | timeNow += datetime.timedelta(hours=int(self.__sleepTime)) 260 | sleepUntil(timeNow.hour, random.randint(0, 59)) 261 | 262 | # Follow/unfollow user 263 | try: 264 | if action == "follow": 265 | res = self.session.put(f"https://api.github.com/user/following/{user}") 266 | else: 267 | res = self.session.delete(f"https://api.github.com/user/following/{user}") 268 | except requests.exceptions.RequestException as e: 269 | raise SystemExit(e) 270 | 271 | # Unsuccessful 272 | if res.status_code != 204: 273 | sleepSeconds = random.randint(self.sleepSecondsLimitedMin, self.sleepSecondsLimitedMax) 274 | # Successful 275 | else: 276 | sleepSeconds = random.randint(self.sleepSecondsActionMin, self.sleepSecondsActionMax) 277 | 278 | # Sleep 279 | sleepSecondsObj = list(range(0, sleepSeconds)) 280 | sleepSecondsBar = tqdm( 281 | sleepSecondsObj, 282 | dynamic_ncols=True, 283 | smoothing=True, 284 | bar_format="[SLEEPING] {n_fmt}s/{total_fmt}s |{l_bar}{bar}|", 285 | position=1, 286 | leave=False, 287 | ) 288 | for second in sleepSecondsBar: 289 | time.sleep(1) 290 | 291 | print(f"\n\nFinished {action}ing!") 292 | 293 | def follow(self): 294 | self.run("follow") 295 | 296 | def unfollow(self): 297 | self.run("unfollow") 298 | 299 | def nextSleepTime(hour, minute): 300 | timeNow = datetime.datetime.now() 301 | future = datetime.datetime(timeNow.year, timeNow.month, timeNow.day, hour, minute) 302 | 303 | if timeNow.timestamp() > future.timestamp(): 304 | future += datetime.timedelta(days=1) 305 | return future 306 | 307 | def sleepUntil(hour, minute): 308 | t = datetime.datetime.today() 309 | future = datetime.datetime(t.year, t.month, t.day, hour, minute) 310 | 311 | if t.timestamp() >= future.timestamp(): 312 | future += datetime.timedelta(days=1) 313 | 314 | print(f'\nSleeping... Waking up at {future.hour}:{future.minute}') 315 | 316 | sleepSeconds = int((future-t).total_seconds()) 317 | sleepSecondsObj = list(range(0, sleepSeconds)) 318 | sleepSecondsBar = tqdm( 319 | sleepSecondsObj, 320 | dynamic_ncols=True, 321 | smoothing=True, 322 | bar_format="[SLEEPING] {n_fmt}s/{total_fmt}s |{l_bar}{bar}|", 323 | position=2, 324 | leave=False, 325 | ) 326 | for second in sleepSecondsBar: 327 | time.sleep(1) 328 | 329 | print(f'\nWaking up...') -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 João PV Correia 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 |

GitHub Follow Bot

  2 | 3 |

Table of Contents

4 | 5 | - [Disclaimer](#disclaimer) 6 | - [Getting Started](#getting-started) 7 | - [Install Requirements](#install-requirements) 8 | - [Authenticate](#authenticate) 9 | - [Get a GitHub Personal Access Token](#get-a-github-personal-access-token) 10 | - [Add your GitHub username and PAT to `.env` file](#add-your-github-username-and-pat-to-env-file) 11 | - [How to Use](#how-to-use) 12 | - [Follow](#follow) 13 | - [Target user's followers](#target-users-followers) 14 | - [Followers of the most popular users from a country](#followers-of-the-most-popular-users-from-a-country) 15 | - [From a file](#from-a-file) 16 | - [Unfollow](#unfollow) 17 | - [All](#all) 18 | - [Followers](#followers) 19 | - [Non-followers](#non-followers) 20 | - [From a file](#from-a-file-1) 21 | - [Options](#options) 22 | - [Maximum follows/unfollows](#maximum-followsunfollows) 23 | - [Speed](#speed) 24 | - [Future Implementation](#future-implementation) 25 | - [Contributing](#contributing) 26 | - [Resources](#resources) 27 | 28 | ## Disclaimer 29 | 30 | **This is a PoC and was developed for educational purposes only. You may get your account banned. Use at your own risk.** 31 | 32 | > ### Spam and Inauthentic Activity on GitHub 33 | > Automated excessive bulk activity and coordinated inauthentic activity, such as spamming, are prohibited on GitHub. Prohibited activities include: 34 | > - (...) 35 | > - inauthentic interactions, such as fake accounts and automated inauthentic activity 36 | > - rank abuse, such as automated starring or following 37 | 38 | [From GitHub Acceptable Use Policies](https://docs.github.com/en/github/site-policy/github-acceptable-use-policies#4-spam-and-inauthentic-activity-on-github) 39 | 40 | ## Getting Started 41 | 42 | ### Install Requirements 43 | 44 | ``` 45 | pip install -r requirements.txt 46 | ``` 47 | 48 | ### Authenticate 49 | 50 | #### Get a GitHub Personal Access Token 51 | 52 | Make sure to enable the `user` scope and all subscopes inside of that permission. 53 | 54 | [How to get your GitHub PAT](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) 55 | 56 | #### Add your GitHub username and PAT to `.env` file 57 | 58 | Create a `.env` file on the project's root directory or edit `.env.sample` (rename to `.env`) and add your username and PAT. 59 | 60 | ``` 61 | GITHUB_USER=YOUR_GITHUB_USERNAME 62 | TOKEN=YOUR_GITHUB_PERSONAL_ACCESS_TOKEN 63 | ``` 64 | 65 | ## How to Use 66 | 67 | ### Follow 68 | 69 | #### Target user's followers 70 | ``` 71 | python bot_follow.py -t 72 | ``` 73 | #### Followers of the most popular users from a country 74 | ([list of valid countries](https://github.com/gayanvoice/top-github-users#readme)) 75 | ``` 76 | python bot_follow.py -p 77 | ``` 78 | #### From a file 79 | Follow users from a pre-generated file (JSON) 80 | ``` 81 | python bot_follow.py -f 82 | ``` 83 | 84 | ### Unfollow 85 | 86 | note: Unfollow order is FIFO, as in the most recently followed user will be the last to be unfollowed. 87 | 88 | #### All 89 | Unfollow all your followings 90 | ``` 91 | python bot_unfollow.py -a 92 | ``` 93 | #### Followers 94 | Only unfollow users who already follow you 95 | ``` 96 | python bot_unfollow.py -fo 97 | ``` 98 | #### Non-followers 99 | Only unfollow users who don't follow you back 100 | ``` 101 | python bot_unfollow.py -nf 102 | ``` 103 | #### From a file 104 | Unfollow users from a pre-generated file (JSON) 105 | ``` 106 | python bot_unfollow.py -f 107 | ``` 108 | 109 | ### Options 110 | 111 | #### Maximum follows/unfollows 112 | Set the maximum number of follow/unfollow actions 113 | ``` 114 | -m 300 115 | ``` 116 | 117 | #### Speed 118 | 119 | A random delay (in seconds) is performed after follow/unfollow actions or when the account is rate limited. 120 | You can change these delays to your liking with the following arguments: 121 | 122 | - Minimum delay between actions 123 | ``` 124 | -smin 20 125 | ``` 126 | - Maximum delay between actions 127 | ``` 128 | -smax 120 129 | ``` 130 | - Minimum delay when rate limited 131 | ``` 132 | -slmin 600 133 | ``` 134 | - Maximum delay when rate limited 135 | ``` 136 | -slmin 1500 137 | ``` 138 | 139 | ## Future Implementation 140 | 141 | - Schedule - Bot only performs actions between set time and sleeps after off-schedule 142 | - Max follow per source - Follow max `n` per popular user 143 | - Add follow source - Follow users per topic 144 | - Add follow source - Grab followers from users listed in a file 145 | - Email followed users - Send an email to followed users with templates (colaboration, follow back or custom) 146 | - Star `n` repositories of followed users 147 | 148 | ## Contributing 149 | 150 | Contributions are welcome! Read the [contribution guidelines](https://github.com/Correia-jpv/.github/blob/main/CONTRIBUTING.md#contributing) first. 151 | 152 | Wish there was another feature? Feel free to open an [feature request issue](/../../issues/new?assignees=Correia-jpv&labels=enhancement&template=feature-request.md&title=%5BREQUEST%5D) with your suggestion! 153 | 154 | If you find a bug, kindly open an [bug report issue](/../../issues/new?assignees=Correia-jpv&labels=bug&template=bug_report.md&title=%5BBUG%5D) as described in the contribution guidelines. 155 | 156 | ## Resources 157 | 158 | - [GitHub API](https://docs.github.com/en/rest) 159 | - [Top GitHub Users By Country](https://github.com/gayanvoice/top-github-users) 160 | - [GitHub-Follow-Bot](https://github.com/TheDarkAssassins/Github-Follow-Bot) 161 | -------------------------------------------------------------------------------- /bot_follow.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from datetime import datetime 3 | from dotenv import load_dotenv 4 | import json 5 | import os 6 | import requests 7 | from GithubAPIBot import GithubAPIBot 8 | 9 | # Arguments 10 | parser = argparse.ArgumentParser() 11 | parser.add_argument("-t", "--user-target", help="Follow the followers of a target user") 12 | parser.add_argument("-f", "--file", help="Follow users from a pre-generated file") 13 | parser.add_argument("-p", "--popular", help="Follow the followers of the most popular users from a given country") 14 | parser.add_argument("-m", "--max-follow", help="Max number of users to follow") 15 | parser.add_argument("-smin", "--sleep-min", help="Min number of range to randomize sleep seconds between actions") 16 | parser.add_argument("-smax", "--sleep-max", help="Max number of range to randomize sleep seconds between actions") 17 | parser.add_argument( 18 | "-slmin", "--sleep-min-limited", help="Min number of range to randomize sleep seconds when account limited" 19 | ) 20 | parser.add_argument( 21 | "-slmax", "--sleep-max-limited", help="Max number of range to randomize sleep seconds when account limited" 22 | ) 23 | parser.add_argument("-sh", "--sleep-hour", help="Hour for the bot to go to sleep") 24 | parser.add_argument("-sm", "--sleep-minute", help="Minute for the bot to go to sleep") 25 | parser.add_argument("-st", "--sleep-time", help="Total time (in hours) for the bot to sleep") 26 | args = parser.parse_args() 27 | 28 | sleepSecondsActionMin = int(args.sleep_min or 20) 29 | sleepSecondsActionMax = int(args.sleep_max or 120) 30 | sleepSecondsLimitedMin = int(args.sleep_min_limited or 600) 31 | sleepSecondsLimitedMax = int(args.sleep_max_limited or 1500) 32 | 33 | load_dotenv() 34 | USER = os.getenv("GITHUB_USER") 35 | TOKEN = os.getenv("TOKEN") 36 | 37 | 38 | bot = GithubAPIBot( 39 | USER, 40 | TOKEN, 41 | sleepSecondsActionMin, 42 | sleepSecondsActionMax, 43 | sleepSecondsLimitedMin, 44 | sleepSecondsLimitedMax, 45 | args.sleep_hour, 46 | args.sleep_minute, 47 | args.sleep_time, 48 | args.max_follow, 49 | ) 50 | 51 | 52 | # Grab users from the most popular users' followers list 53 | if args.popular: 54 | try: 55 | res = bot.session.get( 56 | "https://raw.githubusercontent.com/gayanvoice/top-github-users/main/cache/" + args.popular + ".json" 57 | ) 58 | except requests.exceptions.RequestException as e: 59 | raise SystemExit(e) 60 | 61 | if res.status_code == 404: 62 | raise ValueError(f"\n\"{args.popular}\" is not a valid country. Check README for the valid countries.\n") 63 | 64 | popularUsers = res.json() 65 | 66 | print("\nGrabbing most popular users' followers.\n") 67 | for popularUser in popularUsers: 68 | # Check if we already have enough usernames 69 | if bot.maxAction != None: 70 | if len(bot.usersToAction) >= int(bot.maxAction): 71 | break 72 | 73 | bot.getFollowers(popularUser["login"]) 74 | 75 | 76 | # Grab users to follow from given user's followers 77 | if args.user_target: 78 | bot.getFollowers(args.user_target) 79 | 80 | 81 | # Grab users from given file 82 | if args.file: 83 | with open(args.file, "r+") as file: 84 | try: 85 | fileUsers = json.load(file) 86 | except: 87 | raise ValueError("\n JSON file is in incorrect format.") 88 | fileUsersNotFollowed = [v for v in bot.followings if v not in fileUsers] 89 | bot.usersToAction.extend(fileUsersNotFollowed) 90 | 91 | 92 | # Write users to be followed to file 93 | filename = ( 94 | "./logs/" 95 | + str(datetime.now().strftime("%m-%d-%Y__%H-%M")) 96 | + "__" 97 | + str(len(bot.usersToAction)) 98 | + "-followed-users.json" 99 | ) 100 | os.makedirs(os.path.dirname(filename), exist_ok=True) 101 | with open(filename, "w+") as f: 102 | json.dump(bot.usersToAction, f, indent=4) 103 | 104 | bot.follow() 105 | -------------------------------------------------------------------------------- /bot_unfollow.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from datetime import datetime 3 | from dotenv import load_dotenv 4 | import json 5 | import os 6 | from GithubAPIBot import GithubAPIBot 7 | 8 | # Arguments 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument("-a", "--all", help="Unfollow all your followings", action="store_true") 11 | parser.add_argument("-fo", "--followers", help="Only unfollow users who already follow you", action="store_true") 12 | parser.add_argument("-nf", "--non-followers", help="Only unfollow users who don't follow you back", action="store_true") 13 | parser.add_argument("-f", "--file", help="File with usernames to Unfollow") 14 | parser.add_argument("-m", "--max-unfollow", help="Max Number of People to Unfollow") 15 | parser.add_argument("-smin", "--sleep-min", help="Min Number of range to randomize sleep seconds between actions") 16 | parser.add_argument("-smax", "--sleep-max", help="Max Number of range to randomize sleep seconds between actions") 17 | parser.add_argument( 18 | "-slmin", "--sleep-min-limited", help="Min Number of range to randomize sleep seconds when account limited" 19 | ) 20 | parser.add_argument( 21 | "-slmax", "--sleep-max-limited", help="Max Number of range to randomize sleep seconds when account limited" 22 | ) 23 | parser.add_argument("-sh", "--sleep-hour", help="Hour for the bot to go to sleep") 24 | parser.add_argument("-sm", "--sleep-minute", help="Minute for the bot to go to sleep") 25 | parser.add_argument("-st", "--sleep-time", help="Total time (in hours) for the bot to sleep") 26 | args = parser.parse_args() 27 | 28 | sleepSecondsActionMin = int(args.sleep_min or 3) 29 | sleepSecondsActionMax = int(args.sleep_max or 9) 30 | sleepSecondsLimitedMin = int(args.sleep_min_limited or 90) 31 | sleepSecondsLimitedMax = int(args.sleep_max_limited or 300) 32 | 33 | load_dotenv() 34 | USER = os.getenv("GITHUB_USER") 35 | TOKEN = os.getenv("TOKEN") 36 | 37 | 38 | bot = GithubAPIBot( 39 | USER, 40 | TOKEN, 41 | sleepSecondsActionMin, 42 | sleepSecondsActionMax, 43 | sleepSecondsLimitedMin, 44 | sleepSecondsLimitedMax, 45 | args.sleep_hour, 46 | args.sleep_minute, 47 | args.sleep_time, 48 | args.max_unfollow, 49 | ) 50 | 51 | 52 | # Grab all following users 53 | if args.all: 54 | bot.usersToAction.extend(bot.followings) 55 | else: 56 | # Grab following users from given file 57 | if args.file: 58 | with open(args.file, "r+") as file: 59 | try: 60 | fileUsers = json.load(file) 61 | except: 62 | raise ValueError("\n JSON file is in incorrect format.") 63 | followedFileUsers = [v for v in bot.followings if v in fileUsers] 64 | bot.usersToAction.extend(followedFileUsers) 65 | 66 | # Grab following users who are followers 67 | if args.followers: 68 | bot.getFollowers(following=True) 69 | 70 | # Grab following users who aren't followers 71 | if args.non_followers: 72 | bot.getFollowers(following=True) 73 | nonFollowersFollowings = [v for v in bot.followings if v not in bot.usersToAction] 74 | bot.usersToAction.extend(nonFollowersFollowings) 75 | 76 | 77 | # Write users to be unfollowed to file 78 | filename = ( 79 | "./logs/" 80 | + str(datetime.now().strftime("%m-%d-%Y__%H-%M")) 81 | + "__" 82 | + str(len(bot.usersToAction)) 83 | + "-unfollowed-users.json" 84 | ) 85 | os.makedirs(os.path.dirname(filename), exist_ok=True) 86 | with open(filename, "w+") as f: 87 | json.dump(bot.usersToAction, f, indent=4) 88 | 89 | bot.unfollow() 90 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/befoulers/github-follow-bot/4a7709912bae0c3142b34545a88a93ad92555280/logo.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tqdm==4.33.0 2 | urllib3==1.26.5 3 | requests==2.25.1 4 | python-dotenv==0.19.2 5 | --------------------------------------------------------------------------------