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

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 |
--------------------------------------------------------------------------------