├── .gitignore ├── LICENSE.md ├── README.md └── src └── github_follower ├── back_bone ├── __init__.py └── parser.py ├── gf ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_following_purged_alter_user_parent.py │ ├── 0003_seeder_time_seeded_alter_setting_key.py │ ├── 0004_rename_seeded_user_needs_to_seed_and_more.py │ ├── 0005_alter_target_user_options_remove_user_last_updated.py │ ├── 0006_alter_user_last_parsed.py │ ├── 0007_target_user_allow_follow_target_user_allow_unfollow.py │ ├── 0008_user_cur_page.py │ ├── 0009_user_needs_parsing_follower_follower-target-user_and_more.py │ ├── 0010_remove_user_gid.py │ └── __init__.py ├── models.py ├── tests.py └── views.py ├── github_api ├── __init__.py └── api.py ├── github_follower ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py └── misc ├── __init__.py └── misc.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | db.sqlite3 -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Christian Deacon 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 | ## 11-28-22 3 | I'm not sure when I'll get around to finishing this tool due to other projects I'm working on. However, I wanted to note that mass following users is against GitHub's TOS (not stated below). Therefore, please use at your own risk! 4 | 5 | ## Description 6 | This is a GitHub Follow Bot made inside of a Django application. Management of the bot is done inside of Django's default admin center (`/admin`). The bot itself runs in the background of the Django application. 7 | 8 | The bot works as the following. 9 | 10 | * Runs as a background task in the Django application. 11 | * Management of bot is done in the Django application's web admin center. 12 | * After installing, you must add a super user via Django (e.g. `python3 manage.py createsuperuser`). 13 | * Navigate to the admin web center and add your target user (the user who will be following others) and seeders (users that start out the follow spread). 14 | * After adding the users, add them to the target and seed user list. 15 | * New/least updated users are parsed first up to the max users setting value followed by a random range wait scan time. 16 | * A task is ran in the background for parsed users to make sure they're being followed by target users. 17 | * Another task is ran in the background to retrieve target user's followers and if the Remove Following setting is on, it will automatically unfollow these specific users for the target users. 18 | * Another task is ran that checks all users a target user is following and unfollows the user after *x* days (0 = doesn't unfollow). 19 | * Each follow and unfollow is followed by a random range wait time which may be configured. 20 | 21 | ## To Do 22 | * Develop a more randomized timing system including most likely active hours of the day. 23 | * See if I can use something better in Django to alter general settings instead of relying on a table in the SQLite database. There are also issues with synchronization due to limitations with Django at this moment. 24 | 25 | ## Requirements 26 | The following Python models are required and I'd recommend Python version 3.8 or above since that's what I've tested with. 27 | 28 | ``` 29 | django 30 | aiohttp 31 | ``` 32 | 33 | You can install them like the below. 34 | 35 | ```bash 36 | # Python < 3 37 | python -m pip install django 38 | python -m pip install aiohttp 39 | 40 | pip install django 41 | pip install aiohttp 42 | 43 | # Python >= 3 44 | python3 -m pip install django 45 | python3 -m pip install aiohttp 46 | 47 | pip3 install django 48 | pip3 install aiohttp 49 | ``` 50 | 51 | ## My Motives 52 | A few months ago, I discovered a few GitHub users following over 100K users who were obviously using bots. At first I was shocked because I thought GitHub was against massive following users, but after reading more into it, it appears they don't mind. This had me thinking what if I started following random users as well. Some of these users had a single GitHub.io project that received a lot of attention and I'd assume it's from all the users they were following. I decided to try this. I wanted to see if it'd help me connect with other developers and it certainly did/has! Personally, I haven't used a bot to achieve this, I was actually going through lists of followers from other accounts and following random users. As you'd expect, this completely cluttered my home page, but it also allowed me to discover new projects which was neat in my opinion. 53 | 54 | While this is technically 'spam', the good thing I've noticed is it certainly doesn't impact the user I'm following much other than adding a single line in their home page stating I'm following them (or them receiving an email stating this if they have that on). Though, I could see this becoming annoying if many people/bots started doing it (perhaps GitHub could add a user setting that has a maximum following count of a user who can follow them or receive notifications when the user follows). 55 | 56 | I actually think it's neat this is allowed so far because it allows others to discover your projects. Since I have quite a few networking projects on this account, I've had some people reach out who I followed stating they found my projects neat because they aren't into that field. 57 | 58 | I also wouldn't support empty profiles made just for the purpose of mass following. 59 | 60 | ## USE AT YOUR OWN RISK 61 | Even though it appears GitHub doesn't mind users massive following others (which I again, support), this is still considered a spam tactic. Therefore, please use this tool at your own risk. I'm not even going to be using it myself because I do enjoy manually following users. I made this project to learn more about Python. 62 | 63 | ## Settings 64 | Inside of the web interface, a settings model should be visible. The following settings should be inserted. 65 | 66 | * **enabled** - Whether to enable the bot or not (should be "1" or "0"). 67 | * **max_scan_users** - The maximum users to parse at once before waiting for scan time. 68 | * **wait_time_follow_min** - The minimum number of seconds to wait after following or unfollowing a user. 69 | * **wait_time_follow_max** - The maximum number of seconds to wait after following or unfollowing a user. 70 | * **wait_time_list_min** - The minimum number of seconds to wait after parsing a user's followers page. 71 | * **wait_time_list_max** - The maximum number of seconds to wait after parsing a user's followers page. 72 | * **scan_time_min** - The minimum number of seconds to wait after parsing a batch of users. 73 | * **scan_time_max** - The maximum number of seconds to wait after parsing a batch of users. 74 | * **verbose** - Verbose level for stdout (see levels below). 75 | 1. \+ Notification when a target user follows another user. 76 | 1. \+ Notification when a target user unfollows a user due to being on the follower list or purge. 77 | 1. \+ Notification when users are automatically created from follow spread. 78 | * **user_agent** - The User Agent used to connect to the GitHub API. 79 | * **seed** - Whether to seed (add any existing user's followers to the user list). 80 | * **seed_min_free** - If above 0 and seeding is enabled, seeding will only occur when the amount of new users (users who haven't been followed by any target users) is below this value. 81 | * **max_api_fails** - The max amount of GitHub API fails before stopping the bot for a period of time based off of below (0 = disable). 82 | * **lockout_wait_min** - When the amount of fails exceeds max API fails, it will wait this time minimum in minutes until starting up again. 83 | * **lockout_wait_max** - When the amount of fails exceeds max API fails, it will wait this time maximum in minutes until starting up again. 84 | * **seed_max_pages** - The max amount of pages to seed from with each user parse when looking for new users (seeding). 85 | 86 | ## Installation 87 | Installation should be performed like a regular Django application. This application uses SQLite as the database. You can read more about Django [here](https://docs.djangoproject.com/en/4.0/intro/tutorial01/). I would recommend the following commands. 88 | 89 | ```bash 90 | # Make sure Django and aiohttp are installed for this user. 91 | 92 | # Clone repository. 93 | git clone https://github.com/gamemann/GitHub-Follower-Bot.git 94 | 95 | # Change directory to Django application. 96 | cd GitHub-Follower-Bot/src/github_follower 97 | 98 | # Migrate database. 99 | python3 manage.py migrate 100 | 101 | # Run the development server on any IP (0.0.0.0) as port 8000. 102 | # NOTE - If you don't want to expose the application publicly, bind it to a LAN IP instead (e.g. 10.50.0.4:8000 instead 0f 0.0.0.0:8000). 103 | python3 manage.py runserver 0.0.0.0:8000 104 | 105 | # Create super user for admin web interface. 106 | python3 manage.py createsuperuser 107 | ``` 108 | 109 | The web interface should be located at `http://:`. For example. 110 | 111 | http://localhost:8000 112 | 113 | While you could technically run the Django application's development server for this bot since only the settings are configured through there, Django recommends reading [this](https://docs.djangoproject.com/en/3.2/howto/deployment/) for production use. 114 | 115 | ## FAQ 116 | **Why did you choose Django to use as an interface?** 117 | 118 | While settings could have been configured on the host itself, I wanted an interface that was easily accessible from anywhere. The best thing for this would be a website in my opinion. Most of my experience is with Django which is why I chose that project. 119 | 120 | ## Credits 121 | * [Christian Deacon](https://github.com/gamemann) 122 | -------------------------------------------------------------------------------- /src/github_follower/back_bone/__init__.py: -------------------------------------------------------------------------------- 1 | __name__ = "Back Bone" 2 | __version__ = "1.0.0" 3 | 4 | from .parser import * -------------------------------------------------------------------------------- /src/github_follower/back_bone/parser.py: -------------------------------------------------------------------------------- 1 | import github_api as ga 2 | import json 3 | import asyncio 4 | 5 | import threading 6 | import datetime 7 | from django.conf import settings 8 | from django.utils.timezone import make_aware 9 | from django.db.models import F 10 | 11 | import random 12 | 13 | from asgiref.sync import sync_to_async 14 | 15 | class Parser(threading.Thread): 16 | def __init__(self): 17 | # Initialize thread. 18 | super().__init__() 19 | 20 | # Set daemon to true. 21 | self.daemon = True 22 | 23 | self.running = False 24 | self.locked = False 25 | 26 | self.api = None 27 | 28 | self.global_token = None 29 | self.global_username = None 30 | 31 | self.parse_users_task = None 32 | self.retrieve_followers_task = None 33 | self.purge_following_task = None 34 | 35 | self.retrieve_and_save_task = None 36 | 37 | def run(self): 38 | print("Parser is running...") 39 | 40 | self.running = True 41 | 42 | # Start the back-end parser. 43 | asyncio.run(self.work()) 44 | 45 | @sync_to_async 46 | def get_users(self, gnames, need_parse = True): 47 | import gf.models as mdl 48 | 49 | res = mdl.User.objects.all().exclude(username__in = gnames).order_by('needs_to_seed', F('last_parsed').asc(nulls_first = True)) 50 | 51 | if need_parse: 52 | res.filter(needs_parsing = True) 53 | 54 | return list(res) 55 | 56 | @sync_to_async 57 | def get_target_users(self): 58 | import gf.models as mdl 59 | 60 | return list(mdl.Target_User.objects.all().select_related('user')) 61 | 62 | @sync_to_async 63 | def get_setting(self, key): 64 | import gf.models as mdl 65 | 66 | val = mdl.Setting.get(key = key) 67 | 68 | return val 69 | 70 | @sync_to_async 71 | def get_filtered(self, otype, params = {}, related = [], sort = []): 72 | if len(params) < 1: 73 | return list(otype.objects.all().distinct()) 74 | else: 75 | items = otype.objects.filter(**params) 76 | 77 | if len(related) > 0: 78 | items = items.select_related(related) 79 | 80 | if len(sort) > 0: 81 | items = items.order_by(sort) 82 | 83 | return list(items) 84 | 85 | 86 | async def do_fail(self): 87 | if self.api is None: 88 | return 89 | 90 | # Increase fail count. 91 | self.api.add_fail() 92 | 93 | try: 94 | max_fails = int(await self.get_setting("max_api_fails")) 95 | except Exception as e: 96 | print("[ERR] parser.do_fail() :: Failed to receive max fails.") 97 | print(e) 98 | 99 | return 100 | 101 | if max_fails < 1: 102 | return 103 | 104 | if int(await self.get_setting("verbose")) >= 3: 105 | print("[VVV] Adding fail (" + str(self.api.fails) + " > " + str(max_fails) + ").") 106 | 107 | # If fail count exceeds max fails setting, set locked to True and stop everything. 108 | if self.api.fails >= max_fails: 109 | self.running = False 110 | self.locked = True 111 | 112 | if int(await self.get_setting("verbose")) >= 1: 113 | print("[V] Bot stopped due to fail count exceeding. Waiting specified time frame until starting again.") 114 | 115 | # Run lockout task in background. 116 | try: 117 | await self.run_locked_task() 118 | except Exception as e: 119 | print("[ERR] parser.do_fail() :: Failed to lock bot.") 120 | 121 | return 122 | 123 | async def retrieve_and_save_followers(self, user): 124 | import gf.models as mdl 125 | 126 | # Ignore targeted users. 127 | targeted = True 128 | 129 | try: 130 | tmp = await self.get_filtered(mdl.Target_User, {"user": user}) 131 | tmp = tmp[0] 132 | except Exception: 133 | targeted = False 134 | 135 | if targeted and tmp is None: 136 | targeted = False 137 | 138 | if targeted: 139 | return 140 | 141 | # Make sure we don't have enough free users (users who aren't following anybody).. 142 | free_users = int(await self.get_setting("seed_min_free")) 143 | 144 | if free_users > 0: 145 | target_users = await self.get_target_users() 146 | 147 | # Loop for target GitHub usernames to exclude from parsing list. 148 | gnames = [] 149 | 150 | for user in target_users: 151 | gnames.append(user.user.username) 152 | 153 | users = await self.get_users(gnames) 154 | user_cnt = 0 155 | 156 | for user in users: 157 | if len(await self.get_filtered(mdl.Following, {"user": user})) < 1: 158 | user_cnt = user_cnt + 1 159 | 160 | # If we have enough free users, 161 | if user_cnt > free_users: 162 | return 163 | 164 | page = user.cur_page 165 | 166 | # Create a loop and go through. 167 | while True: 168 | # Make new connection got GitHub API and set user agent. 169 | if self.api is None: 170 | self.api = ga.GH_API() 171 | 172 | # Authenticate globally. 173 | if self.global_username is not None and self.global_token is not None: 174 | self.api.authenticate(self.global_username, self.global_token) 175 | 176 | res = None 177 | 178 | # Try sending request to GitHub API. 179 | try: 180 | res = await self.api.send("GET", '/users/' + user.username + '/followers?page=' + str(page)) 181 | except Exception as e: 182 | print("[ERR] Failed to retrieve user's following list for " + user.username + " (request failure).") 183 | print(e) 184 | 185 | await self.do_fail() 186 | 187 | break 188 | 189 | # Check status code. 190 | if res[1] != 200 and res[1] != 204: 191 | await self.do_fail() 192 | 193 | break 194 | 195 | # Decode JSON. 196 | try: 197 | data = json.loads(res[0]) 198 | except json.JSONDecodeError as e: 199 | print("[ERR] Failed to retrieve user's following list for " + self.username + " (JSON decode failure).") 200 | print(e) 201 | 202 | break 203 | 204 | # Make sure we have data, if not, break the loop. 205 | if len(data) < 1 or page >= int(await self.get_setting("seed_max_pages")): 206 | # Save page and user. 207 | user.cur_page = page 208 | 209 | await sync_to_async(user.save)() 210 | 211 | break 212 | 213 | for nuser in data: 214 | if "id" not in nuser: 215 | print("[ERR] ID field not found in JSON data.") 216 | 217 | continue 218 | 219 | if "login" not in nuser: 220 | print("[ERR] ID field not found in JSON data.") 221 | 222 | continue 223 | 224 | # Check if user exists already. 225 | exists = True 226 | 227 | try: 228 | new_user = await self.get_filtered(mdl.User, {"username": nuser["login"]}) 229 | new_user = new_user[0] 230 | except Exception as e: 231 | exists = False 232 | 233 | if exists and new_user is None: 234 | exists = False 235 | 236 | if not exists: 237 | # Create new user by username. 238 | await sync_to_async(mdl.User.objects.create)(username = nuser["login"], parent = user.id, auto_added = True) 239 | 240 | if int(await self.get_setting("verbose")) >= 3: 241 | print("[V] Adding user " + nuser["login"] + " (parent " + user.username + ")") 242 | 243 | # Increment page 244 | page = page + 1 245 | 246 | await asyncio.sleep(float(random.randint(int(await self.get_setting("wait_time_list_min")), int(await self.get_setting("wait_time_list_max"))))) 247 | 248 | async def loop_and_follow_targets(self, user): 249 | import gf.models as mdl 250 | 251 | # First, we should make sure we're following the target users. 252 | target_users = await self.get_target_users() 253 | 254 | for tuser in target_users: 255 | # Check if user exists already. 256 | exists = True 257 | 258 | try: 259 | fuser = await self.get_filtered(mdl.Following, {"target_user": tuser, "user": user}) 260 | fuser = fuser[0] 261 | except Exception: 262 | exists = False 263 | 264 | if exists and fuser is None: 265 | exists = False 266 | 267 | # Check if we exist in the following list. 268 | if exists: 269 | continue 270 | 271 | # Follow target user. 272 | await tuser.follow_user(user) 273 | 274 | await asyncio.sleep(float(random.randint(int(await self.get_setting("wait_time_follow_min")), int(await self.get_setting("wait_time_follow_max"))))) 275 | 276 | async def parse_user(self, user): 277 | if bool(int(await self.get_setting("seed"))) and not self.locked: 278 | if self.retrieve_and_save_task is None or self.retrieve_and_save_task.done(): 279 | self.retrieve_and_save_task = asyncio.create_task(self.retrieve_and_save_followers(user)) 280 | else: 281 | if self.retrieve_and_save_task is not None and self.retrieve_and_save_task in asyncio.all_tasks(): 282 | self.retrieve_and_save_task.cancel() 283 | self.retrieve_and_save_task = None 284 | 285 | follow_targets_task = asyncio.create_task(self.loop_and_follow_targets(user)) 286 | 287 | await asyncio.gather(follow_targets_task) 288 | 289 | async def purge_following(self): 290 | import gf.models as mdl 291 | 292 | secs_in_day = 86400 293 | 294 | while True: 295 | # Retrieve target users. 296 | target_users = await self.get_target_users() 297 | 298 | # Loop through target users. 299 | for tuser in target_users: 300 | # Make sure cleanup days is above 0 (enabled). 301 | if tuser.cleanup_days < 1: 302 | continue 303 | 304 | # Retrieve the target user's following list. 305 | users = None 306 | 307 | try: 308 | users = await self.get_filtered(mdl.Following, {"target_user": tuser, "purged": False}, related = ('user'), sort = ('time_added')) 309 | except Exception: 310 | users = None 311 | 312 | # Make sure we have users and loop. 313 | if users is not None: 314 | for user in users: 315 | now = datetime.datetime.now().timestamp() 316 | expired = user.time_added.timestamp() + (tuser.cleanup_days * secs_in_day) 317 | 318 | # Check if we're expired. 319 | if now > expired: 320 | if int(await self.get_setting("verbose")) >= 3: 321 | print("[VVV] " + user.user.username + " has expired.") 322 | 323 | # Unfollow used and mark them as purged. 324 | await tuser.unfollow_user(user.user) 325 | 326 | # Set purged to true. 327 | user.purged = True 328 | 329 | # Save user. 330 | await sync_to_async(user.save)() 331 | 332 | # Wait follow time. 333 | await asyncio.sleep(float(random.randint(int(await self.get_setting("wait_time_follow_min")), int(await self.get_setting("wait_time_follow_max"))))) 334 | 335 | await asyncio.sleep(float(random.randint(int(await self.get_setting("wait_time_list_min")), int(await self.get_setting("wait_time_list_max"))))) 336 | 337 | async def retrieve_followers(self): 338 | import gf.models as mdl 339 | 340 | while True: 341 | await asyncio.sleep(1) 342 | 343 | tusers = await self.get_target_users() 344 | 345 | for user in tusers: 346 | # Use GitHub API. 347 | if self.api is None: 348 | self.api = ga.GH_API() 349 | 350 | # Authenticate. 351 | self.api.authenticate(user.user.username, user.token) 352 | 353 | page = 1 354 | 355 | # We'll want to create a loop through of the target user's followers. 356 | while True: 357 | res = None 358 | 359 | # Make connection. 360 | try: 361 | res = await self.api.send("GET", '/user/followers?page=' + str(page)) 362 | except Exception as e: 363 | print("[ERR] Failed to retrieve target user's followers list for " + user.user.username + " (request failure).") 364 | print(e) 365 | 366 | await self.do_fail() 367 | 368 | break 369 | 370 | # Check status code. 371 | if res[1] != 200 and res[1] != 204: 372 | await self.do_fail() 373 | 374 | break 375 | 376 | # Decode JSON. 377 | try: 378 | data = json.loads(res[0]) 379 | except json.JSONDecodeError as e: 380 | print("[ERR] Failed to retrieve target user's followers list for " + user.user.username + " (JSON decode failure).") 381 | print(e) 382 | 383 | break 384 | 385 | # Make sure we have data, if not, break the loop. 386 | if len(data) < 1: 387 | break 388 | 389 | for fuser in data: 390 | if "id" not in fuser: 391 | continue 392 | 393 | # Make sure user exists. 394 | exists = True 395 | 396 | muser = None 397 | 398 | try: 399 | muser = await self.get_filtered(mdl.User, {"username": fuser["login"]}) 400 | muser = muser[0] 401 | except Exception: 402 | exists = False 403 | 404 | if exists and muser is None: 405 | exists = False 406 | 407 | if not exists: 408 | muser = await sync_to_async(mdl.User.objects.create)(username = fuser["login"], needs_parsing = False) 409 | 410 | # Add to follower list if not already on it. 411 | exists = True 412 | 413 | try: 414 | tmp = await self.get_filtered(mdl.Follower, {"target_user": user, "user": muser}) 415 | tmp = tmp[0] 416 | except Exception: 417 | exists = False 418 | 419 | if exists and tmp is None: 420 | exists = False 421 | 422 | await sync_to_async(user.save)() 423 | 424 | # Make a new follower entry. 425 | if not exists: 426 | await sync_to_async(mdl.Follower.objects.create)(target_user = user, user = muser) 427 | 428 | # Check if the same user is following our target. 429 | exists = True 430 | 431 | try: 432 | tmp = await self.get_filtered(mdl.Following, {"target_user": user, "user": muser, "purged": False}) 433 | tmp = tmp[0] 434 | except Exception: 435 | exists = False 436 | 437 | if exists and tmp is None: 438 | exists = False 439 | 440 | # Check if target user is following this user. 441 | if exists: 442 | # Check for remove following setting. If enabled, unfollow user. 443 | if user.remove_following: 444 | await user.unfollow_user(muser) 445 | 446 | # We'll want to wait the follow period. 447 | await asyncio.sleep(float(random.randint(int(await self.get_setting("wait_time_follow_min")), int(await self.get_setting("wait_time_follow_max"))))) 448 | 449 | # Increment page 450 | page = page + 1 451 | 452 | await asyncio.sleep(float(random.randint(int(await self.get_setting("wait_time_list_min")), int(await self.get_setting("wait_time_list_max"))))) 453 | 454 | async def parse_users(self): 455 | import gf.models as mdl 456 | 457 | while True: 458 | # Retrieve users. 459 | target_users = await self.get_target_users() 460 | max_users = int(await self.get_setting("max_scan_users")) 461 | 462 | # Loop for target GitHub usernames to exclude from parsing list. 463 | gnames = [] 464 | 465 | for user in target_users: 466 | gnames.append(user.user.username) 467 | 468 | # Retrieve users excluding target users. 469 | users = await self.get_users(gnames) 470 | 471 | for user in users[:max_users]: 472 | # Update last parsed. 473 | user.last_parsed = make_aware(datetime.datetime.now()) 474 | 475 | # Check if this user needed to seed. 476 | if user.needs_to_seed: 477 | user.needs_to_seed = False 478 | 479 | # Save user. 480 | await sync_to_async(user.save)() 481 | 482 | # Parse user. 483 | await self.parse_user(user) 484 | 485 | # Wait scan time. 486 | await asyncio.sleep(float(random.randint(int(await self.get_setting("scan_time_min")), int(await self.get_setting("scan_time_max"))))) 487 | 488 | async def work(self): 489 | # Retrieve all target users 490 | tusers = await self.get_target_users() 491 | 492 | # Set global username and token.s 493 | for user in tusers: 494 | if user.global_user: 495 | self.global_username = user.user.username 496 | self.global_token = user.token 497 | 498 | # Create a loop until the program ends. 499 | while True: 500 | # Check if we're enabled. 501 | if bool(int(await self.get_setting("enabled"))) and not self.locked: 502 | # Run parse users task. 503 | if self.parse_users_task is None or self.parse_users_task.done(): 504 | self.parse_users_task = asyncio.create_task(self.parse_users()) 505 | 506 | # Create tasks to check followers/following for target users. 507 | if self.retrieve_followers_task is None or self.retrieve_followers_task.done(): 508 | self.retrieve_followers_task = asyncio.create_task(self.retrieve_followers()) 509 | 510 | if self.purge_following_task is None or self.purge_following_task.done(): 511 | self.purge_following_task = asyncio.create_task(self.purge_following()) 512 | else: 513 | # Check tasks and make sure they're closed. 514 | if self.parse_users_task in asyncio.all_tasks(): 515 | self.parse_users_task.cancel() 516 | self.parse_users_task = None 517 | 518 | if self.retrieve_and_save_task in asyncio.all_tasks(): 519 | self.retrieve_and_save_task.cancel() 520 | self.retrieve_and_save_task = None 521 | 522 | if self.purge_following_task in asyncio.all_tasks(): 523 | self.purge_following_task.cancel() 524 | self.purge_following_task = None 525 | 526 | if self.retrieve_followers_task is not None and self.retrieve_followers_task in asyncio.all_tasks(): 527 | self.retrieve_followers_task.cancel() 528 | self.retrieve_followers_task = None 529 | 530 | # Sleep for a second to avoid CPU consumption. 531 | await asyncio.sleep(1) 532 | 533 | async def run_locked(self): 534 | wait_time = float(random.randint(int(await self.get_setting("lockout_wait_min")), int(await self.get_setting("lockout_wait_max"))) * 60) 535 | 536 | await asyncio.sleep(wait_time) 537 | 538 | self.locked = False 539 | self.running = True 540 | self.api.fails = 0 541 | 542 | async def run_locked_task(self): 543 | asyncio.create_task(self.run_locked()) 544 | 545 | parser = Parser() -------------------------------------------------------------------------------- /src/github_follower/gf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamemann/GitHub-Follow-Bot/509ffa2ccfa597b400ad8405b77e05a4252c6ff7/src/github_follower/gf/__init__.py -------------------------------------------------------------------------------- /src/github_follower/gf/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import User, Seeder, Setting, Target_User, Follower, Following 4 | 5 | admin.site.register(User) 6 | admin.site.register(Target_User) 7 | admin.site.register(Seeder) 8 | admin.site.register(Follower) 9 | admin.site.register(Following) 10 | admin.site.register(Setting) 11 | 12 | admin.site.site_header = 'GitHub FB Administration' -------------------------------------------------------------------------------- /src/github_follower/gf/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | import back_bone as bb 4 | import os 5 | 6 | class GfConfig(AppConfig): 7 | default_auto_field = 'django.db.models.BigAutoField' 8 | name = 'gf' 9 | verbose_name = 'GitHub Follower' 10 | 11 | 12 | def ready(self): 13 | env = os.environ.get("FIRST_THREAD") 14 | 15 | # Check if first thread has started since we want to spin it up on the second thread in development. 16 | if env is not None: 17 | from .models import Setting 18 | 19 | # Set settings. defaults. 20 | Setting.create("enabled", "0", False) 21 | Setting.create("max_scan_users", "10", False) 22 | Setting.create("wait_time_follow_min", "10", False) 23 | Setting.create("wait_time_follow_max", "30", False) 24 | Setting.create("wait_time_list_min", "5", False) 25 | Setting.create("wait_time_list_max", "30", False) 26 | Setting.create("scan_time_min", "5", False) 27 | Setting.create("scan_time_max", "60", False) 28 | Setting.create("verbose", "1", False) 29 | Setting.create("user_agent", "GitHub-Follower", False) 30 | Setting.create("seed", "1", False) 31 | Setting.create("seed_min_free", "64", False) 32 | Setting.create("max_api_fails", "5", False) 33 | Setting.create("lockout_wait_min", "1", False) 34 | Setting.create("lockout_wait_max", "10", False) 35 | Setting.create("seed_max_pages", "5", False) 36 | 37 | bb.parser.start() 38 | else: 39 | os.environ["FIRST_THREAD"] = 'True' -------------------------------------------------------------------------------- /src/github_follower/gf/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.1 on 2022-02-01 05:55 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Setting', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('key', models.CharField(help_text='The setting key.', max_length=64, verbose_name='Key')), 20 | ('val', models.CharField(help_text='The setting value.', max_length=64, verbose_name='Value')), 21 | ], 22 | ), 23 | migrations.CreateModel( 24 | name='User', 25 | fields=[ 26 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 27 | ('gid', models.IntegerField(editable=False, null=True)), 28 | ('parent', models.IntegerField(default=0, editable=False)), 29 | ('username', models.CharField(help_text='The GitHub username.', max_length=64, unique=True, verbose_name='Username')), 30 | ('last_updated', models.DateTimeField(auto_now_add=True, null=True)), 31 | ('last_parsed', models.DateTimeField(auto_now_add=True, null=True)), 32 | ('seeded', models.BooleanField(default=False, editable=False)), 33 | ('auto_added', models.BooleanField(default=False, editable=False)), 34 | ], 35 | ), 36 | migrations.CreateModel( 37 | name='Target_User', 38 | fields=[ 39 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='gf.user')), 40 | ('remove_following', models.BooleanField(default=True, help_text='Whether to remove a user that follows from the following list.', verbose_name='Remove Following')), 41 | ('cleanup_days', models.IntegerField(help_text='Automatically purges uses from the following list after this many days.', verbose_name='Cleanup Days')), 42 | ('token', models.CharField(help_text="GitHub's personal token for authentication.", max_length=128, verbose_name='Personal Token')), 43 | ('global_user', models.BooleanField(default=False, help_text="If true, this user's token and username will be used for authentication in general.", verbose_name='Global User')), 44 | ], 45 | ), 46 | migrations.CreateModel( 47 | name='Seeder', 48 | fields=[ 49 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 50 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gf.user')), 51 | ], 52 | ), 53 | migrations.CreateModel( 54 | name='Following', 55 | fields=[ 56 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 57 | ('time_added', models.DateTimeField(auto_now_add=True)), 58 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gf.user')), 59 | ('target_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gf.target_user')), 60 | ], 61 | ), 62 | migrations.CreateModel( 63 | name='Follower', 64 | fields=[ 65 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 66 | ('time_added', models.DateTimeField(auto_now_add=True)), 67 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gf.user')), 68 | ('target_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gf.target_user')), 69 | ], 70 | ), 71 | ] 72 | -------------------------------------------------------------------------------- /src/github_follower/gf/migrations/0002_following_purged_alter_user_parent.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.1 on 2022-02-01 23:42 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('gf', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='following', 15 | name='purged', 16 | field=models.BooleanField(default=False, editable=False), 17 | ), 18 | migrations.AlterField( 19 | model_name='user', 20 | name='parent', 21 | field=models.IntegerField(default=0, editable=False, null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /src/github_follower/gf/migrations/0003_seeder_time_seeded_alter_setting_key.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.1 on 2022-02-04 02:53 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('gf', '0002_following_purged_alter_user_parent'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='seeder', 15 | name='time_seeded', 16 | field=models.DateTimeField(editable=False, null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='setting', 20 | name='key', 21 | field=models.CharField(help_text='The setting key.', max_length=64, unique=True, verbose_name='Key'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /src/github_follower/gf/migrations/0004_rename_seeded_user_needs_to_seed_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.1 on 2022-02-04 03:11 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('gf', '0003_seeder_time_seeded_alter_setting_key'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='user', 15 | old_name='seeded', 16 | new_name='needs_to_seed', 17 | ), 18 | migrations.RemoveField( 19 | model_name='seeder', 20 | name='time_seeded', 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /src/github_follower/gf/migrations/0005_alter_target_user_options_remove_user_last_updated.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.1 on 2022-02-04 05:28 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('gf', '0004_rename_seeded_user_needs_to_seed_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='target_user', 15 | options={'verbose_name': 'Target User'}, 16 | ), 17 | migrations.RemoveField( 18 | model_name='user', 19 | name='last_updated', 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /src/github_follower/gf/migrations/0006_alter_user_last_parsed.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.1 on 2022-02-04 05:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('gf', '0005_alter_target_user_options_remove_user_last_updated'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='user', 15 | name='last_parsed', 16 | field=models.DateTimeField(editable=False, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/github_follower/gf/migrations/0007_target_user_allow_follow_target_user_allow_unfollow.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.1 on 2022-02-04 05:48 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('gf', '0006_alter_user_last_parsed'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='target_user', 15 | name='allow_follow', 16 | field=models.BooleanField(default=True, help_text='If true, this user will start following parsed users.', verbose_name='Allow Following'), 17 | ), 18 | migrations.AddField( 19 | model_name='target_user', 20 | name='allow_unfollow', 21 | field=models.BooleanField(default=True, help_text='If true, the bot will unfollow users for this target user.', verbose_name='Allow Unfollowing'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /src/github_follower/gf/migrations/0008_user_cur_page.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.1 on 2022-02-04 22:39 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('gf', '0007_target_user_allow_follow_target_user_allow_unfollow'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='user', 15 | name='cur_page', 16 | field=models.IntegerField(default=1, editable=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/github_follower/gf/migrations/0009_user_needs_parsing_follower_follower-target-user_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.1 on 2022-02-06 19:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('gf', '0008_user_cur_page'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='user', 15 | name='needs_parsing', 16 | field=models.BooleanField(default=True, editable=False), 17 | ), 18 | migrations.AddConstraint( 19 | model_name='follower', 20 | constraint=models.UniqueConstraint(fields=('target_user', 'user'), name='follower-target-user'), 21 | ), 22 | migrations.AddConstraint( 23 | model_name='following', 24 | constraint=models.UniqueConstraint(fields=('target_user', 'user'), name='following-target-user'), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /src/github_follower/gf/migrations/0010_remove_user_gid.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.1 on 2022-02-06 22:49 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('gf', '0009_user_needs_parsing_follower_follower-target-user_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='user', 15 | name='gid', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /src/github_follower/gf/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamemann/GitHub-Follow-Bot/509ffa2ccfa597b400ad8405b77e05a4252c6ff7/src/github_follower/gf/migrations/__init__.py -------------------------------------------------------------------------------- /src/github_follower/gf/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | import json 3 | import http.client 4 | 5 | import back_bone 6 | 7 | import asyncio 8 | 9 | import github_api as ga 10 | 11 | from asgiref.sync import sync_to_async 12 | 13 | class Setting(models.Model): 14 | key = models.CharField(verbose_name = "Key", help_text="The setting key.", max_length = 64, unique = True) 15 | val = models.CharField(verbose_name = "Value", help_text="The setting value.", max_length = 64) 16 | 17 | def create(key, val, override): 18 | item = None 19 | 20 | exists = True 21 | 22 | try: 23 | item = Setting.objects.filter(key = key)[0] 24 | except Exception: 25 | exists = False 26 | 27 | if item is None: 28 | exists = False 29 | 30 | if exists: 31 | # Make sure we want to override. 32 | if not override: 33 | return 34 | else: 35 | item = Setting(key = key) 36 | 37 | # Set value and save. 38 | item.val = val 39 | item.save() 40 | 41 | def get(key): 42 | val = None 43 | exists = True 44 | 45 | try: 46 | item = Setting.objects.filter(key = key)[0] 47 | except Exception: 48 | exists = False 49 | 50 | if exists and item is None: 51 | exists = False 52 | 53 | if exists: 54 | val = str(item.val) 55 | 56 | return val 57 | 58 | def __str__(self): 59 | return self.key 60 | 61 | class User(models.Model): 62 | parent = models.IntegerField(editable = False, default = 0, null = True) 63 | 64 | username = models.CharField(verbose_name = "Username", help_text = "The GitHub username.", max_length = 64, unique = True) 65 | 66 | last_parsed = models.DateTimeField(editable = False, auto_now_add = False, null = True) 67 | needs_parsing = models.BooleanField(editable = False, default = True) 68 | 69 | needs_to_seed = models.BooleanField(editable = False, default = False) 70 | auto_added = models.BooleanField(editable = False, default = False) 71 | 72 | cur_page = models.IntegerField(editable = False, default = 1) 73 | 74 | def save(self, *args, **kwargs): 75 | try: 76 | super().save(*args, **kwargs) 77 | except Exception as e: 78 | print("[ERR] Error saving user (" + self.username + ").") 79 | print(e) 80 | 81 | return 82 | 83 | def __str__(self): 84 | return self.username 85 | 86 | class Target_User(models.Model): 87 | user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) 88 | remove_following = models.BooleanField(verbose_name = "Remove Following", help_text = "Whether to remove a user that follows from the following list.", default = True) 89 | cleanup_days = models.IntegerField(verbose_name = "Cleanup Days", help_text = "Automatically purges uses from the following list after this many days.") 90 | token = models.CharField(verbose_name = "Personal Token", help_text = "GitHub's personal token for authentication.", max_length=128) 91 | global_user = models.BooleanField(verbose_name = "Global User", help_text = "If true, this user's token and username will be used for authentication in general.", default=False) 92 | allow_follow = models.BooleanField(verbose_name = "Allow Following", help_text = "If true, this user will start following parsed users.", default = True) 93 | allow_unfollow = models.BooleanField(verbose_name = "Allow Unfollowing", help_text = "If true, the bot will unfollow users for this target user.", default = True) 94 | 95 | async def follow_user(self, user): 96 | # Check if we should follow. 97 | if not self.allow_follow: 98 | return 99 | 100 | # Make connection GitHub's API. 101 | if back_bone.parser.api is None: 102 | back_bone.parser.api = ga.GH_API() 103 | 104 | # Authenticate 105 | back_bone.parser.api.authenticate(self.user.username, self.token) 106 | 107 | res = None 108 | 109 | # Send request. 110 | try: 111 | res = await back_bone.parser.api.send('PUT', '/user/following/' + user.username) 112 | except Exception as e: 113 | print("[ERR] Failed to follow GitHub user " + user.username + " for " + self.user.username + " (request failure).") 114 | print(e) 115 | 116 | await back_bone.parser.do_fail() 117 | 118 | return 119 | 120 | # Check status code. 121 | if res[1] != 200 and res[1] != 204: 122 | await back_bone.parser.do_fail() 123 | 124 | return 125 | 126 | # Save to following. 127 | await sync_to_async(Following.objects.create)(target_user = self, user = user) 128 | 129 | if int(await sync_to_async(Setting.get)(key = "verbose")) >= 1: 130 | print("[V] Following user " + user.username + " for " + self.user.username + ".") 131 | 132 | @sync_to_async 133 | def get_following(self, user): 134 | res = None 135 | 136 | try: 137 | res = list(Following.objects.filter(target_user = self, user = user)) 138 | res = res[0] 139 | except Exception as e: 140 | res = None 141 | 142 | return res 143 | 144 | async def unfollow_user(self, user): 145 | # Check if we should unfollow. 146 | if not self.allow_unfollow: 147 | return 148 | 149 | # Make connection GitHub's API. 150 | if back_bone.parser.api is None: 151 | back_bone.parser.api = ga.GH_API() 152 | 153 | # Authenticate 154 | back_bone.parser.api.authenticate(self.user.username, self.token) 155 | 156 | res = None 157 | 158 | # Send request. 159 | try: 160 | res = await back_bone.parser.api.send('DELETE', '/user/following/' + user.username) 161 | except Exception as e: 162 | print("[ERR] Failed to unfollow GitHub user " + user.username + " for " + self.user.username + " (request failure).") 163 | print(e) 164 | 165 | await back_bone.parser.do_fail() 166 | 167 | return 168 | 169 | # Check status code. 170 | if res[1] != 200 and res[1] != 204: 171 | await back_bone.parser.do_fail() 172 | 173 | return 174 | 175 | following = await self.get_following(user) 176 | 177 | # Set user as purged. 178 | if following is not None: 179 | following.purged = True 180 | 181 | # Save. 182 | await sync_to_async(following.save)() 183 | 184 | if int(await sync_to_async(Setting.get)(key = "verbose")) >= 2: 185 | print("[VV] Unfollowing user " + user.username + " from " + self.user.username + ".") 186 | 187 | class Meta: 188 | verbose_name = "Target User" 189 | 190 | def __str__(self): 191 | return self.user.username 192 | 193 | class Follower(models.Model): 194 | target_user = models.ForeignKey(Target_User, on_delete = models.CASCADE) 195 | user = models.ForeignKey(User, on_delete = models.CASCADE) 196 | 197 | time_added = models.DateTimeField(editable = False, auto_now_add = True) 198 | 199 | class Meta: 200 | constraints = [ 201 | models.UniqueConstraint(fields = ['target_user', 'user'], name = "follower-target-user") 202 | ] 203 | 204 | def __str__(self): 205 | return self.user.username 206 | 207 | class Following(models.Model): 208 | target_user = models.ForeignKey(Target_User, on_delete = models.CASCADE) 209 | user = models.ForeignKey(User, on_delete = models.CASCADE) 210 | purged = models.BooleanField(editable = False, default = False) 211 | 212 | time_added = models.DateTimeField(editable = False, auto_now_add = True) 213 | 214 | class Meta: 215 | constraints = [ 216 | models.UniqueConstraint(fields = ['target_user', 'user'], name="following-target-user") 217 | ] 218 | 219 | def __str__(self): 220 | return self.user.username 221 | 222 | class Seeder(models.Model): 223 | user = models.ForeignKey(User, on_delete = models.CASCADE) 224 | 225 | def save(self, *args, **kwargs): 226 | # We need to seed user. 227 | self.user.needs_to_seed = True 228 | 229 | try: 230 | super().save(*args, **kwargs) 231 | except Exception as e: 232 | print("[ERR] Error saving seed user.") 233 | print(e) 234 | 235 | return 236 | 237 | def __str__(self): 238 | return self.user.username -------------------------------------------------------------------------------- /src/github_follower/gf/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /src/github_follower/gf/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /src/github_follower/github_api/__init__.py: -------------------------------------------------------------------------------- 1 | __name__ = "GitHub API" 2 | __version__ = "1.0.0" 3 | 4 | from .api import * -------------------------------------------------------------------------------- /src/github_follower/github_api/api.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import asyncio 3 | import base64 4 | 5 | class GH_API(): 6 | def __init__(self): 7 | import gf.models as mdl 8 | self.conn = None 9 | self.headers = {} 10 | 11 | self.endpoint = 'https://api.github.com' 12 | 13 | self.method = "GET" 14 | self.url = "/" 15 | 16 | self.response = None 17 | self.response_code = 0 18 | self.fails = 0 19 | 20 | # Default user agent. 21 | user_agent = mdl.Setting.get("user_agent") 22 | 23 | if user_agent is None: 24 | user_agent = "GitHub-Follower" 25 | 26 | self.add_header("User-Agent", user_agent) 27 | 28 | def add_header(self, key, val): 29 | self.headers[key] = val 30 | def add_fail(self): 31 | self.fails = self.fails + 1 32 | 33 | def authenticate(self, user, token): 34 | mix = user + ":" + token 35 | 36 | r = base64.b64encode(bytes(mix, encoding='utf8')) 37 | 38 | self.add_header("Authorization", "Basic " + r.decode('ascii')) 39 | 40 | async def send(self, method = "GET", url = "/", headers = {}): 41 | # Make connection. 42 | conn = aiohttp.ClientSession() 43 | 44 | # Insert additional headers. 45 | if self.headers is not None: 46 | for k, v in self.headers.items(): 47 | headers[k] = v 48 | 49 | res = None 50 | status = None 51 | failed = False 52 | 53 | # Send request. 54 | try: 55 | if method == "POST": 56 | res = await conn.post(self.endpoint + url, headers = headers) 57 | elif method == "PUT": 58 | res = await conn.put(self.endpoint + url, headers = headers) 59 | elif method == "DELETE": 60 | res = await conn.delete(self.endpoint + url, headers = headers) 61 | else: 62 | res = await conn.get(self.endpoint + url, headers = headers) 63 | except Exception as e: 64 | print(e) 65 | 66 | failed = True 67 | 68 | if not failed and res is not None: 69 | status = res.status 70 | res = await res.text() 71 | 72 | # Close connection. 73 | try: 74 | await conn.close() 75 | except Exception as e: 76 | print("[ERR] HTTP close error.") 77 | print(e) 78 | 79 | return [None, 0] 80 | else: 81 | # Set fails to 0 indicating we closed the connection. 82 | if not failed and res is not None: 83 | self.fails = 0 84 | 85 | # Return list (response, response code) 86 | return [res, status] -------------------------------------------------------------------------------- /src/github_follower/github_follower/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamemann/GitHub-Follow-Bot/509ffa2ccfa597b400ad8405b77e05a4252c6ff7/src/github_follower/github_follower/__init__.py -------------------------------------------------------------------------------- /src/github_follower/github_follower/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for github_follower project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'github_follower.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /src/github_follower/github_follower/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for github_follower project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.0.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.0/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'django-insecure-$-%g3fsw+$80j7998ysxsti-9i60h7vw5ztr^yf(*)=ra*5hx*' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['pb-dev01.deacon.internal'] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'gf.apps.GfConfig', 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'github_follower.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'github_follower.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': BASE_DIR / 'db.sqlite3', 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 106 | 107 | LANGUAGE_CODE = 'en-us' 108 | 109 | TIME_ZONE = 'UTC' 110 | 111 | USE_I18N = True 112 | 113 | USE_TZ = True 114 | 115 | 116 | # Static files (CSS, JavaScript, Images) 117 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 118 | 119 | STATIC_URL = 'static/' 120 | 121 | # Default primary key field type 122 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 123 | 124 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 125 | 126 | # GitHub Follower Settings. 127 | MAX_USERS = 30 128 | SCAN_TIME = 15 129 | WAIT_TIME = 15 -------------------------------------------------------------------------------- /src/github_follower/github_follower/urls.py: -------------------------------------------------------------------------------- 1 | """github_follower URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/4.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path 18 | 19 | urlpatterns = [ 20 | path('', admin.site.urls), 21 | ] 22 | -------------------------------------------------------------------------------- /src/github_follower/github_follower/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for github_follower project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'github_follower.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /src/github_follower/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'github_follower.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /src/github_follower/misc/__init__.py: -------------------------------------------------------------------------------- 1 | __title__ = "Misc" 2 | __version__ = "1.0.0" 3 | 4 | -------------------------------------------------------------------------------- /src/github_follower/misc/misc.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamemann/GitHub-Follow-Bot/509ffa2ccfa597b400ad8405b77e05a4252c6ff7/src/github_follower/misc/misc.py --------------------------------------------------------------------------------