├── .deepsource.toml ├── .env.sample ├── .gitattributes ├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── GithubAPIBot.py ├── LICENSE ├── README.md ├── bot_follow.py ├── bot_unfollow.py ├── last-command-used.txt ├── logo.png └── requirements.txt /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "python" 5 | 6 | [analyzers.meta] 7 | runtime_version = "3.x.x" -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | GITHUB_USER=YOUR_GITHUB_USERNAME 2 | TOKEN=YOUR_GITHUB_PERSONAL_ACCESS_TOKEN -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.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 urllib3.util import Retry 9 | 10 | 11 | class GithubAPIBot: 12 | # Constructor 13 | def __init__( 14 | self, 15 | username: str, 16 | token: str, 17 | sleepSecondsActionMin: int, 18 | sleepSecondsActionMax: int, 19 | sleepSecondsLimitedMin: int, 20 | sleepSecondsLimitedMax: int, 21 | sleepHour=None, 22 | sleepMinute=None, 23 | sleepTime=None, 24 | maxAction=None, 25 | ): 26 | if not isinstance(username, str): 27 | raise TypeError("Missing/Incorrect username") 28 | if not isinstance(token, str): 29 | raise TypeError("Missing/Incorrect token") 30 | 31 | self.__username = username 32 | self.__token = token 33 | self.__sleepSecondsActionMin = sleepSecondsActionMin 34 | self.__sleepSecondsActionMax = sleepSecondsActionMax 35 | self.__sleepSecondsLimitedMin = sleepSecondsLimitedMin 36 | self.__sleepSecondsLimitedMax = sleepSecondsLimitedMax 37 | self.__sleepHour = sleepHour 38 | self.__sleepMinute = sleepMinute 39 | self.__sleepTime = sleepTime 40 | self.__maxAction = maxAction 41 | self.__usersToAction = [] 42 | self.__followings = [] 43 | 44 | # Requests' headers 45 | HEADERS = { 46 | "Authorization": "Basic " + b64encode(str(self.token + ":" + self.token).encode("utf-8")).decode("utf-8") 47 | } 48 | 49 | # Session 50 | self.session = requests.session() 51 | retries = Retry(total=5, backoff_factor=1, status_forcelist=[502, 503, 504]) 52 | self.session.mount("https://", HTTPAdapter(max_retries=retries)) 53 | self.session.headers.update(HEADERS) 54 | 55 | # Authenticate 56 | try: 57 | res = self.session.get("https://api.github.com/user") 58 | except requests.exceptions.RequestException as e: 59 | raise SystemExit(e) 60 | 61 | if res.status_code == 404: 62 | raise ValueError("\nFailure to Authenticate, please check Personal Access Token and Username!") 63 | print("\nSuccessful authentication.") 64 | 65 | self.getFollowings() 66 | 67 | # Getters & Setters 68 | @property 69 | def username(self): 70 | return self.__username 71 | 72 | @username.setter 73 | def username(self, value): 74 | self.__username = value 75 | 76 | @property 77 | def token(self): 78 | return self.__token 79 | 80 | @token.setter 81 | def token(self, value): 82 | self.__token = value 83 | 84 | @property 85 | def sleepSecondsActionMin(self): 86 | return self.__sleepSecondsActionMin 87 | 88 | @sleepSecondsActionMin.setter 89 | def sleepSecondsActionMin(self, value): 90 | self.__sleepSecondsActionMin = value 91 | 92 | @property 93 | def sleepSecondsActionMax(self): 94 | return self.__sleepSecondsActionMax 95 | 96 | @sleepSecondsActionMax.setter 97 | def sleepSecondsActionMax(self, value): 98 | self.__sleepSecondsActionMax = value 99 | 100 | @property 101 | def sleepSecondsLimitedMin(self): 102 | return self.__sleepSecondsLimitedMin 103 | 104 | @sleepSecondsLimitedMin.setter 105 | def sleepSecondsLimitedMin(self, value): 106 | self.__sleepSecondsLimitedMin = value 107 | 108 | @property 109 | def sleepSecondsLimitedMax(self): 110 | return self.__sleepSecondsLimitedMax 111 | 112 | @sleepSecondsLimitedMax.setter 113 | def sleepSecondsLimitedMax(self, value): 114 | self.__sleepSecondsLimitedMax = value 115 | 116 | @property 117 | def sleepHour(self): 118 | return self.__sleepHour 119 | 120 | @sleepHour.setter 121 | def sleepHour(self, value): 122 | self.__sleepHour = value 123 | 124 | @property 125 | def sleepMinute(self): 126 | return self.__sleepMinute 127 | 128 | @sleepMinute.setter 129 | def sleepMinute(self, value): 130 | self.__sleepMinute = value 131 | 132 | @property 133 | def sleepTime(self): 134 | return self.__sleepTime 135 | 136 | @sleepTime.setter 137 | def sleepTime(self, value): 138 | self.__sleepTime = value 139 | 140 | @property 141 | def maxAction(self): 142 | return self.__maxAction 143 | 144 | @maxAction.setter 145 | def maxAction(self, value): 146 | self.__maxAction = value 147 | 148 | @property 149 | def usersToAction(self): 150 | return self.__usersToAction 151 | 152 | @usersToAction.setter 153 | def usersToAction(self, value): 154 | self.__usersToAction = value 155 | 156 | @property 157 | def followings(self): 158 | return self.__followings 159 | 160 | @followings.setter 161 | def followings(self, value): 162 | self.__followings = value 163 | 164 | def getUsers(self, url="", maxAction=None, following=False): 165 | users = [] 166 | 167 | try: 168 | res = self.session.get(url) 169 | except requests.exceptions.RequestException as e: 170 | raise SystemExit(e) 171 | 172 | # Get usernames from each page 173 | page = 1 174 | while True: 175 | try: 176 | res = self.session.get(url + "?page=" + str(page)).json() 177 | except requests.exceptions.RequestException as e: 178 | raise SystemExit(e) 179 | 180 | for user in res: 181 | # Check if we already have enough usernames 182 | if maxAction is not None: 183 | if len(users) >= int(maxAction): 184 | break 185 | 186 | # Add username if it's not being followed already 187 | if ( 188 | not following 189 | and not (user["login"] in self.followings) 190 | or following 191 | and (user["login"] in self.followings) 192 | ): 193 | users.append(user["login"]) 194 | 195 | # Check if we already have enough usernames 196 | if maxAction is not None: 197 | if len(users) >= int(maxAction): 198 | break 199 | 200 | if res == []: 201 | break 202 | page += 1 203 | 204 | return users 205 | 206 | def getFollowers(self, username=None, following=None): 207 | if username is None: 208 | username = self.username 209 | print(f"\nGrabbing {username}'s followers.\n") 210 | self.usersToAction.extend( 211 | self.getUsers( 212 | url=f"https://api.github.com/users/{username}/followers", 213 | maxAction=self.maxAction, 214 | following=following, 215 | ) 216 | ) 217 | 218 | def getFollowings(self, username=None): 219 | if username is None: 220 | username = self.username 221 | print(f"\nGrabbing {username}'s followings.\n") 222 | self.followings.extend(self.getUsers(url=f"https://api.github.com/users/{username}/following")) 223 | 224 | def run(self, action): 225 | if len(self.usersToAction) == 0: 226 | print(f"Nothing to {action}") 227 | else: 228 | 229 | # Users to follow/unfollow must not exceed the given max 230 | if self.maxAction is not None: 231 | self.usersToAction = self.usersToAction[: min(len(self.usersToAction), int(self.maxAction))] 232 | 233 | # Time for the bot to go to sleep 234 | if self.sleepHour is not None and self.sleepMinute is not None and self.sleepTime is not None: 235 | sleepTime = nextSleepTime(int(self.__sleepHour), int(self.sleepMinute)) 236 | 237 | # Start follow/unfollow 238 | print(f"\nStarting to {action}.\n") 239 | users = tqdm( 240 | self.usersToAction, 241 | initial=1, 242 | dynamic_ncols=True, 243 | smoothing=True, 244 | bar_format="[PROGRESS] {n_fmt}/{total_fmt} |{l_bar}{bar}|", 245 | position=0, 246 | leave=False, 247 | ) 248 | for user in users: 249 | 250 | # Set the bot to sleep at the set time 251 | if self.sleepHour is not None and self.sleepMinute is not None and self.sleepTime is not None: 252 | timeNow = datetime.datetime.now() 253 | if timeNow.timestamp() > sleepTime.timestamp(): 254 | sleepTime = nextSleepTime(int(self.__sleepHour), int(self.__sleepMinute)) 255 | timeNow += datetime.timedelta(hours=int(self.__sleepTime)) 256 | sleepUntil(timeNow.hour, random.randint(0, 59)) 257 | 258 | # Follow/unfollow user 259 | try: 260 | if action == "follow": 261 | res = self.session.put(f"https://api.github.com/user/following/{user}") 262 | else: 263 | res = self.session.delete(f"https://api.github.com/user/following/{user}") 264 | except requests.exceptions.RequestException as e: 265 | raise SystemExit(e) 266 | 267 | # Unsuccessful 268 | if res.status_code != 204: 269 | sleepSeconds = random.randint(self.sleepSecondsLimitedMin, self.sleepSecondsLimitedMax) 270 | # Successful 271 | else: 272 | sleepSeconds = random.randint(self.sleepSecondsActionMin, self.sleepSecondsActionMax) 273 | 274 | # Sleep 275 | sleepSecondsObj = list(range(0, sleepSeconds)) 276 | sleepSecondsBar = tqdm( 277 | sleepSecondsObj, 278 | dynamic_ncols=True, 279 | smoothing=True, 280 | bar_format="[SLEEPING] {n_fmt}s/{total_fmt}s |{l_bar}{bar}|", 281 | position=1, 282 | leave=False, 283 | ) 284 | for second in sleepSecondsBar: 285 | time.sleep(1) 286 | 287 | print(f"\n\nFinished {action}ing!") 288 | 289 | def follow(self): 290 | self.run("follow") 291 | 292 | def unfollow(self): 293 | self.run("unfollow") 294 | 295 | def nextSleepTime(hour, minute): 296 | timeNow = datetime.datetime.now() 297 | future = datetime.datetime(timeNow.year, timeNow.month, timeNow.day, hour, minute) 298 | 299 | if timeNow.timestamp() > future.timestamp(): 300 | future += datetime.timedelta(days=1) 301 | return future 302 | 303 | def sleepUntil(hour, minute): 304 | t = datetime.datetime.today() 305 | future = datetime.datetime(t.year, t.month, t.day, hour, minute) 306 | 307 | if t.timestamp() >= future.timestamp(): 308 | future += datetime.timedelta(days=1) 309 | 310 | print(f'\nSleeping... Waking up at {future.hour}:{future.minute}') 311 | 312 | sleepSeconds = int((future-t).total_seconds()) 313 | sleepSecondsObj = list(range(0, sleepSeconds)) 314 | sleepSecondsBar = tqdm( 315 | sleepSecondsObj, 316 | dynamic_ncols=True, 317 | smoothing=True, 318 | bar_format="[SLEEPING] {n_fmt}s/{total_fmt}s |{l_bar}{bar}|", 319 | position=2, 320 | leave=False, 321 | ) 322 | for second in sleepSecondsBar: 323 | time.sleep(1) 324 | 325 | 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 | > 34 | > Automated excessive bulk activity and coordinated inauthentic activity, such as spamming, are prohibited on GitHub. Prohibited activities include: 35 | > 36 | > - (...) 37 | > - inauthentic interactions, such as fake accounts and automated inauthentic activity 38 | > - rank abuse, such as automated starring or following 39 | 40 | [From GitHub Acceptable Use Policies](https://docs.github.com/en/github/site-policy/github-acceptable-use-policies#4-spam-and-inauthentic-activity-on-github) 41 | 42 | ## Getting Started 43 | 44 | ### Install Requirements 45 | 46 | ``` 47 | pip install -r requirements.txt 48 | ``` 49 | 50 | ### Authenticate 51 | 52 | #### Get a GitHub Personal Access Token 53 | 54 | Make sure to enable the `user` scope and all subscopes inside of that permission. 55 | 56 | [How to get your GitHub PAT](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) 57 | 58 | #### Add your GitHub username and PAT to `.env` file 59 | 60 | Create a `.env` file on the project's root directory or edit `.env.sample` (rename to `.env`) and add your username and PAT. 61 | 62 | ``` 63 | GITHUB_USER=YOUR_GITHUB_USERNAME 64 | TOKEN=YOUR_GITHUB_PERSONAL_ACCESS_TOKEN 65 | ``` 66 | 67 | ## How to Use 68 | 69 | ### Follow 70 | 71 | #### Target user's followers 72 | 73 | ``` 74 | python bot_follow.py -t 75 | ``` 76 | 77 | #### Followers of the most popular users from a country 78 | 79 | ([list of valid countries](https://github.com/gayanvoice/top-github-users#readme)) 80 | 81 | ``` 82 | python bot_follow.py -p 83 | ``` 84 | 85 | #### From a file 86 | 87 | Follow users from a pre-generated file (JSON) 88 | 89 | ``` 90 | python bot_follow.py -f 91 | ``` 92 | 93 | ### Unfollow 94 | 95 | note: Unfollow order is FIFO, as in the most recently followed user will be the last to be unfollowed. 96 | 97 | #### All 98 | 99 | Unfollow all your followings 100 | 101 | ``` 102 | python bot_unfollow.py -a 103 | ``` 104 | 105 | #### Followers 106 | 107 | Only unfollow users who already follow you 108 | 109 | ``` 110 | python bot_unfollow.py -fo 111 | ``` 112 | 113 | #### Non-followers 114 | 115 | Only unfollow users who don't follow you back 116 | 117 | ``` 118 | python bot_unfollow.py -nf 119 | ``` 120 | 121 | #### From a file 122 | 123 | Unfollow users from a pre-generated file (JSON) 124 | 125 | ``` 126 | python bot_unfollow.py -f 127 | ``` 128 | 129 | ### Options 130 | 131 | #### Maximum follows/unfollows 132 | 133 | Set the maximum number of follow/unfollow actions 134 | 135 | ``` 136 | -m 300 137 | ``` 138 | 139 | #### Speed 140 | 141 | A random delay (in seconds) is performed after follow/unfollow actions or when the account is rate limited. 142 | You can change these delays to your liking with the following arguments: 143 | 144 | - Minimum delay between actions 145 | ``` 146 | -smin 20 147 | ``` 148 | - Maximum delay between actions 149 | ``` 150 | -smax 120 151 | ``` 152 | - Minimum delay when rate limited 153 | ``` 154 | -slmin 600 155 | ``` 156 | - Maximum delay when rate limited 157 | ``` 158 | -slmin 1500 159 | ``` 160 | 161 | ## Future Implementation 162 | 163 | - Schedule - Bot only performs actions between set time and sleeps after off-schedule 164 | - Max follow per source - Follow max `n` per popular user 165 | - Add follow source - Follow users per topic 166 | - Add follow source - Grab followers from users listed in a file 167 | - Email followed users - Send an email to followed users with templates (colaboration, follow back or custom) 168 | - Star `n` repositories of followed users 169 | 170 | ## Contributing 171 | 172 | Contributions are welcome! Read the [contribution guidelines](https://github.com/isyuricunha/github-follow-bot/blob/main/CONTRIBUTING.md#contributing) first. 173 | 174 | Wish there was another feature? Feel free to open an [feature request issue](/../../issues/new?assignees=isyuricunha&labels=enhancement&template=feature-request.md&title=%5BREQUEST%5D) with your suggestion! 175 | 176 | If you find a bug, kindly open an [bug report issue](/../../issues/new?assignees=isyuricunha&labels=bug&template=bug_report.md&title=%5BBUG%5D) as described in the contribution guidelines. 177 | 178 | ## Resources 179 | 180 | - [GitHub API](https://docs.github.com/en/rest) 181 | - [Top GitHub Users By Country](https://github.com/gayanvoice/top-github-users) 182 | - [GitHub-Follow-Bot](https://github.com/TheDarkAssassins/Github-Follow-Bot) 183 | -------------------------------------------------------------------------------- /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 is not 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 | -------------------------------------------------------------------------------- /last-command-used.txt: -------------------------------------------------------------------------------- 1 | - put your last command manually here 2 | 3 | follow: 4 | python bot_follow.py -t AYIDouble -smin 15 -smax 20 5 | python bot_follow.py -t filipedeschamps -smin 15 -smax 20 6 | python bot_follow.py -t MrXyfir -smin 15 -smax 20 7 | 8 | unfollow: 9 | python bot_unfollow.py -a -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isyuricunha/github-follow-bot/e51276710037f5dbdfde716091f21c9d469f169e/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 | --------------------------------------------------------------------------------