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