├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── art └── bot.png ├── chromedriver_linux64 └── .gitkeep ├── followbot ├── __init__.py ├── bot.py └── config.py ├── requirements-dev.txt └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Redowan Delowar 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # REDNAFI 2 | # This only works with embedded venv not virtualenv 3 | # Install venv: python3.8 -m venv venv 4 | # Activate venv: source venv/bin/activate 5 | 6 | # Usage (line =black line length, path = action path) 7 | # ------ 8 | # make pylinter [make pylinter line=88 path=.] 9 | # make pyupgrade 10 | 11 | path := . 12 | line := 88 13 | 14 | all: 15 | @echo 16 | 17 | .PHONY: checkvenv 18 | checkvenv: 19 | # raises error if environment is not active 20 | ifeq ("$(VIRTUAL_ENV)","") 21 | @echo "Venv is not activated!" 22 | @echo "Activate venv first." 23 | @echo 24 | exit 1 25 | endif 26 | 27 | .PHONY: pyupgrade 28 | pyupgrade: checkvenv 29 | # checks if pip-tools is installed 30 | ifeq ("$(wildcard venv/bin/pip-compile)","") 31 | @echo "Installing Pip-tools..." 32 | @pip install pip-tools 33 | endif 34 | 35 | ifeq ("$(wildcard venv/bin/pip-sync)","") 36 | @echo "Installing Pip-tools..." 37 | @pip install pip-tools 38 | endif 39 | 40 | # pip-tools 41 | @pip-compile --upgrade requirements-dev.txt 42 | @pip-compile --upgrade requirements.txt 43 | @pip-sync requirements-dev.txt requirements.txt 44 | 45 | 46 | .PHONY: pylinter 47 | pylinter: checkvenv 48 | # checks if black is installed 49 | ifeq ("$(wildcard venv/bin/black)","") 50 | @echo "Installing Black..." 51 | @pip install black 52 | endif 53 | 54 | # checks if isort is installed 55 | ifeq ("$(wildcard venv/bin/isort)","") 56 | @echo "Installing Isort..." 57 | @pip install isort 58 | endif 59 | 60 | # checks if flake8 is installed 61 | ifeq ("$(wildcard venv/bin/flake8)","") 62 | @echo -e "Installing flake8..." 63 | @pip install flake8 64 | @echo 65 | endif 66 | 67 | # black 68 | @echo "Applying Black" 69 | @echo "----------------\n" 70 | @black -l $(line) $(path) 71 | @echo 72 | 73 | # isort 74 | @echo "Applying Isort" 75 | @echo "----------------\n" 76 | @isort --atomic --profile black $(path) 77 | @echo 78 | 79 | # flake8 80 | @echo "Applying Flake8" 81 | @echo "----------------\n" 82 | @flake8 --max-line-length "$(line)" \ 83 | --max-complexity "18" \ 84 | --select "B,C,E,F,W,T4,B9" \ 85 | --ignore "E203,E266,E501,W503,F403,F401,E402" \ 86 | --exclude ".git,__pycache__,old, build, \ 87 | dist, venv" 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | Bot logo 5 | 6 | # Github Follow Bot 7 | 8 |
9 | 10 | ## What Does it Do? 11 | 12 | Follows Github users and also their followers from a user provided list of Github handles. 13 | 14 | ## Installation 15 | 16 | * Clone the repo. 17 | * Create a Python virtual environment. 18 | * Install the dependencies: 19 | 20 | ``` 21 | pip install -r requirements.txt 22 | ``` 23 | 24 | * Download compatible [Chrome Driver](https://chromedriver.chromium.org/downloads) for your OS. 25 | 26 | * Provide your Chrome Driver path, credentials and the target users' Github usernames: 27 | 28 | ```python 29 | CHROME_DRIVER_PATH = "./chromedriver_linux64/chromedriver" 30 | 31 | # Put your github username and password here 32 | YOUR_NAME = "yourname" 33 | YOUR_PASS = "yourpass" 34 | 35 | # List of people's github usernames whose followers you want to follow 36 | # This could be even yourself 37 | TARGET_NAMES_LIST = ["rednafi", "gvanrossum"] 38 | ``` 39 | 40 | * Run the bot and let it do its work: 41 | 42 | ``` 43 | python -m followbot.bot 44 | ``` 45 | 46 | ## Development & Contribution 47 | 48 | * After cloning the repo, activate python environment and run: 49 | 50 | ``` 51 | pip install -r requirements-dev.txt && pip install -r requirements.txt 52 | ``` 53 | 54 | * Make your proposed changes 55 | * Before sending a PR, lint the code with: 56 | 57 | ``` 58 | make pylinter 59 | ``` 60 | 61 | * To upgrade the dependencies and sync your environment, run: 62 | 63 | ``` 64 | make pyupgrade 65 | ``` 66 | 67 | ## Disclaimer 68 | 69 | I created this just to fiddle with some [selenium](https://selenium-python.readthedocs.io/). The code could be polished. Moreover, it's debatable whether you should use a bot to follow people or not. Also, there is a high chance that Github will flag you as a spam user and ban your public profile. Use at your own risk 🤷‍♂️ 70 | -------------------------------------------------------------------------------- /art/bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rednafi/github-follow-bot/ea197331cb717112a775bd5fa5f108b4d0cd3a02/art/bot.png -------------------------------------------------------------------------------- /chromedriver_linux64/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rednafi/github-follow-bot/ea197331cb717112a775bd5fa5f108b4d0cd3a02/chromedriver_linux64/.gitkeep -------------------------------------------------------------------------------- /followbot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rednafi/github-follow-bot/ea197331cb717112a775bd5fa5f108b4d0cd3a02/followbot/__init__.py -------------------------------------------------------------------------------- /followbot/bot.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Tuple 3 | 4 | from selenium import webdriver 5 | from selenium.webdriver.common.by import By 6 | from selenium.webdriver.remote.webelement import WebElement 7 | from selenium.webdriver.support import expected_conditions as EC 8 | from selenium.webdriver.support.ui import WebDriverWait 9 | 10 | from followbot import config 11 | 12 | 13 | class GHFollow: 14 | """Bot that follows everyone who follows the target user(s).""" 15 | 16 | def __init__( 17 | self, yourname: str, yourpass: str, targetname: str, driverpath: str, 18 | ) -> None: 19 | 20 | # Initializing the headless chrome 21 | self.yourname = yourname 22 | self.yourpass = yourpass 23 | self.targetname = targetname 24 | 25 | # I'm on Linux, so the default chrome driver is for Linux 26 | self.driver = webdriver.Chrome(driverpath) 27 | self.driver.get("https://github.com/login") 28 | self.wait = WebDriverWait(self.driver, 10) 29 | 30 | def _locate_userpass_fields(self) -> Tuple[WebElement, WebElement]: 31 | """Finds out the username and the password field in the UI.""" 32 | 33 | username = self.wait.until( 34 | EC.presence_of_element_located((By.ID, "login_field")) 35 | ) 36 | password = self.wait.until(EC.presence_of_element_located((By.ID, "password"))) 37 | return username, password 38 | 39 | def _put_username_password(self) -> None: 40 | """Fills in the username and the password fields with their 41 | corresponding values.""" 42 | 43 | username, password = self._locate_userpass_fields() 44 | username.send_keys(self.yourname) 45 | password.send_keys(self.yourpass) 46 | 47 | def _click_signin_button(self) -> None: 48 | """Locates and clicks the sign in button.""" 49 | 50 | login_form = self.wait.until( 51 | EC.presence_of_element_located((By.XPATH, "//input[@value='Sign in']")) 52 | ) 53 | login_form.click() 54 | 55 | def _goto_followers_tab(self) -> None: 56 | """Takes the browser to the followers tab.""" 57 | 58 | # Your target user's Github handle goes here 59 | targetname = self.targetname 60 | 61 | # The function name doesn't reflect it but this also fetches 62 | # followers' data 63 | self.driver.get(f"https://github.com/{targetname}?tab=followers") 64 | time.sleep(2) 65 | 66 | def _find_and_follow(self) -> None: 67 | # Finds and gathers the followers in a list 68 | 69 | # Get the users elements 70 | users = self.driver.find_elements_by_xpath("//a[@data-hovercard-type='user']") 71 | 72 | # Getting the links of the followers 73 | followers = [follower.get_attribute("href") for follower in users] 74 | 75 | # Unique follower links 76 | followers = list(set(followers)) 77 | 78 | # Follow everyone who is following the targetuser 79 | for user in followers: 80 | 81 | # I know hardcoding the page range is lame but just being 82 | # lazy for now. Scope for improvement. 83 | for page in range(1, 5): 84 | link = f"{user}?page={page}&tab=following" 85 | self.driver.get(link) 86 | 87 | # Gotta sleep to give it some organic vibe! 88 | time.sleep(2) 89 | 90 | follow_buttons = self.driver.find_elements_by_xpath( 91 | "//input[@aria-label='Follow this person']" 92 | ) 93 | 94 | # I know I'm going to hell for supressing all the errors here. 95 | # But I've zero ideas what might be the potential exceptions. 96 | # Care to explore?? 97 | # Will add proper logging here. 98 | try: 99 | for button in follow_buttons: 100 | button.submit() 101 | except Exception: 102 | pass 103 | 104 | self.driver.close() 105 | 106 | def assimilate(self) -> None: 107 | """Assembles and executes all the private methods defined above.""" 108 | 109 | self._put_username_password() 110 | self._click_signin_button() 111 | self._goto_followers_tab() 112 | self._find_and_follow() 113 | 114 | 115 | if __name__ == "__main__": 116 | 117 | # Here goes the list of the Github handles of the users that you want 118 | # to target 119 | for targetname in config.TARGET_NAMES_LIST: 120 | # Instantiate the GHFollow class with your username & password 121 | # Dont' forget the chrome driver 122 | bot = GHFollow( 123 | config.YOUR_NAME, config.YOUR_PASS, targetname, config.CHROME_DRIVER_PATH 124 | ) # driverpath=default here 125 | bot.assimilate() 126 | -------------------------------------------------------------------------------- /followbot/config.py: -------------------------------------------------------------------------------- 1 | # Path of your chrome driver 2 | # You'll need to download the Chrome driver compatible with 3 | # your OS and provide the path here. Find the driver: 4 | # https://chromedriver.chromium.org/downloads 5 | CHROME_DRIVER_PATH = "./chromedriver_linux64/chromedriver" 6 | 7 | # Put your github user name here 8 | YOUR_NAME = "yourname" 9 | YOUR_PASS = "yourpass" 10 | 11 | # List of people's github usernames whose followers you want to follow 12 | # This could be even yourself 13 | TARGET_NAMES_LIST = ["rednafi", "gvanrossum"] 14 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile requirements-dev.txt 6 | # 7 | appdirs==1.4.4 # via -r requirements-dev.txt, black 8 | attrs==19.3.0 # via -r requirements-dev.txt, black 9 | black==19.10b0 # via -r requirements-dev.txt 10 | click==7.1.2 # via -r requirements-dev.txt, black, pip-tools 11 | flake8==3.8.3 # via -r requirements-dev.txt 12 | isort==5.1.2 # via -r requirements-dev.txt 13 | mccabe==0.6.1 # via -r requirements-dev.txt, flake8 14 | pathspec==0.8.0 # via -r requirements-dev.txt, black 15 | pip-tools==5.2.1 # via -r requirements-dev.txt 16 | pycodestyle==2.6.0 # via -r requirements-dev.txt, flake8 17 | pyflakes==2.2.0 # via -r requirements-dev.txt, flake8 18 | regex==2020.7.14 # via -r requirements-dev.txt, black 19 | six==1.15.0 # via -r requirements-dev.txt, pip-tools 20 | toml==0.10.1 # via -r requirements-dev.txt, black 21 | typed-ast==1.4.1 # via -r requirements-dev.txt, black 22 | 23 | # The following packages are considered to be unsafe in a requirements file: 24 | # pip 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile requirements.txt 6 | # 7 | selenium==3.141.0 # via -r requirements.txt 8 | urllib3==1.25.9 # via -r requirements.txt, selenium 9 | --------------------------------------------------------------------------------