├── .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 |