├── .gitignore
├── logo.png
├── .env.sample
├── requirements.txt
├── LICENSE
├── .github
└── workflows
│ └── codeql-analysis.yml
├── bot_unfollow.py
├── bot_follow.py
├── README.md
└── GithubAPIBot.py
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | logs/
3 | .env
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/befoulers/github-follow-bot/HEAD/logo.png
--------------------------------------------------------------------------------
/.env.sample:
--------------------------------------------------------------------------------
1 | GITHUB_USER=YOUR_GITHUB_USERNAME
2 | TOKEN=YOUR_GITHUB_PERSONAL_ACCESS_TOKEN
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | tqdm==4.33.0
2 | urllib3==1.26.5
3 | requests==2.25.1
4 | python-dotenv==0.19.2
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 João PV Correia
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master ]
20 | schedule:
21 | - cron: '44 5 * * 6'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'python' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v2
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v1
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
52 |
53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
54 | # If this step fails, then you should remove it and run the build manually (see below)
55 | - name: Autobuild
56 | uses: github/codeql-action/autobuild@v1
57 |
58 | # ℹ️ Command-line programs to run using the OS shell.
59 | # 📚 https://git.io/JvXDl
60 |
61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
62 | # and modify them (or add more) to build your code if your project
63 | # uses a compiled language
64 |
65 | #- run: |
66 | # make bootstrap
67 | # make release
68 |
69 | - name: Perform CodeQL Analysis
70 | uses: github/codeql-action/analyze@v1
71 |
--------------------------------------------------------------------------------
/bot_unfollow.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | from datetime import datetime
3 | from dotenv import load_dotenv
4 | import json
5 | import os
6 | from GithubAPIBot import GithubAPIBot
7 |
8 | # Arguments
9 | parser = argparse.ArgumentParser()
10 | parser.add_argument("-a", "--all", help="Unfollow all your followings", action="store_true")
11 | parser.add_argument("-fo", "--followers", help="Only unfollow users who already follow you", action="store_true")
12 | parser.add_argument("-nf", "--non-followers", help="Only unfollow users who don't follow you back", action="store_true")
13 | parser.add_argument("-f", "--file", help="File with usernames to Unfollow")
14 | parser.add_argument("-m", "--max-unfollow", help="Max Number of People to Unfollow")
15 | parser.add_argument("-smin", "--sleep-min", help="Min Number of range to randomize sleep seconds between actions")
16 | parser.add_argument("-smax", "--sleep-max", help="Max Number of range to randomize sleep seconds between actions")
17 | parser.add_argument(
18 | "-slmin", "--sleep-min-limited", help="Min Number of range to randomize sleep seconds when account limited"
19 | )
20 | parser.add_argument(
21 | "-slmax", "--sleep-max-limited", help="Max Number of range to randomize sleep seconds when account limited"
22 | )
23 | parser.add_argument("-sh", "--sleep-hour", help="Hour for the bot to go to sleep")
24 | parser.add_argument("-sm", "--sleep-minute", help="Minute for the bot to go to sleep")
25 | parser.add_argument("-st", "--sleep-time", help="Total time (in hours) for the bot to sleep")
26 | args = parser.parse_args()
27 |
28 | sleepSecondsActionMin = int(args.sleep_min or 3)
29 | sleepSecondsActionMax = int(args.sleep_max or 9)
30 | sleepSecondsLimitedMin = int(args.sleep_min_limited or 90)
31 | sleepSecondsLimitedMax = int(args.sleep_max_limited or 300)
32 |
33 | load_dotenv()
34 | USER = os.getenv("GITHUB_USER")
35 | TOKEN = os.getenv("TOKEN")
36 |
37 |
38 | bot = GithubAPIBot(
39 | USER,
40 | TOKEN,
41 | sleepSecondsActionMin,
42 | sleepSecondsActionMax,
43 | sleepSecondsLimitedMin,
44 | sleepSecondsLimitedMax,
45 | args.sleep_hour,
46 | args.sleep_minute,
47 | args.sleep_time,
48 | args.max_unfollow,
49 | )
50 |
51 |
52 | # Grab all following users
53 | if args.all:
54 | bot.usersToAction.extend(bot.followings)
55 | else:
56 | # Grab following users from given file
57 | if args.file:
58 | with open(args.file, "r+") as file:
59 | try:
60 | fileUsers = json.load(file)
61 | except:
62 | raise ValueError("\n JSON file is in incorrect format.")
63 | followedFileUsers = [v for v in bot.followings if v in fileUsers]
64 | bot.usersToAction.extend(followedFileUsers)
65 |
66 | # Grab following users who are followers
67 | if args.followers:
68 | bot.getFollowers(following=True)
69 |
70 | # Grab following users who aren't followers
71 | if args.non_followers:
72 | bot.getFollowers(following=True)
73 | nonFollowersFollowings = [v for v in bot.followings if v not in bot.usersToAction]
74 | bot.usersToAction.extend(nonFollowersFollowings)
75 |
76 |
77 | # Write users to be unfollowed to file
78 | filename = (
79 | "./logs/"
80 | + str(datetime.now().strftime("%m-%d-%Y__%H-%M"))
81 | + "__"
82 | + str(len(bot.usersToAction))
83 | + "-unfollowed-users.json"
84 | )
85 | os.makedirs(os.path.dirname(filename), exist_ok=True)
86 | with open(filename, "w+") as f:
87 | json.dump(bot.usersToAction, f, indent=4)
88 |
89 | bot.unfollow()
90 |
--------------------------------------------------------------------------------
/bot_follow.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | from datetime import datetime
3 | from dotenv import load_dotenv
4 | import json
5 | import os
6 | import requests
7 | from GithubAPIBot import GithubAPIBot
8 |
9 | # Arguments
10 | parser = argparse.ArgumentParser()
11 | parser.add_argument("-t", "--user-target", help="Follow the followers of a target user")
12 | parser.add_argument("-f", "--file", help="Follow users from a pre-generated file")
13 | parser.add_argument("-p", "--popular", help="Follow the followers of the most popular users from a given country")
14 | parser.add_argument("-m", "--max-follow", help="Max number of users to follow")
15 | parser.add_argument("-smin", "--sleep-min", help="Min number of range to randomize sleep seconds between actions")
16 | parser.add_argument("-smax", "--sleep-max", help="Max number of range to randomize sleep seconds between actions")
17 | parser.add_argument(
18 | "-slmin", "--sleep-min-limited", help="Min number of range to randomize sleep seconds when account limited"
19 | )
20 | parser.add_argument(
21 | "-slmax", "--sleep-max-limited", help="Max number of range to randomize sleep seconds when account limited"
22 | )
23 | parser.add_argument("-sh", "--sleep-hour", help="Hour for the bot to go to sleep")
24 | parser.add_argument("-sm", "--sleep-minute", help="Minute for the bot to go to sleep")
25 | parser.add_argument("-st", "--sleep-time", help="Total time (in hours) for the bot to sleep")
26 | args = parser.parse_args()
27 |
28 | sleepSecondsActionMin = int(args.sleep_min or 20)
29 | sleepSecondsActionMax = int(args.sleep_max or 120)
30 | sleepSecondsLimitedMin = int(args.sleep_min_limited or 600)
31 | sleepSecondsLimitedMax = int(args.sleep_max_limited or 1500)
32 |
33 | load_dotenv()
34 | USER = os.getenv("GITHUB_USER")
35 | TOKEN = os.getenv("TOKEN")
36 |
37 |
38 | bot = GithubAPIBot(
39 | USER,
40 | TOKEN,
41 | sleepSecondsActionMin,
42 | sleepSecondsActionMax,
43 | sleepSecondsLimitedMin,
44 | sleepSecondsLimitedMax,
45 | args.sleep_hour,
46 | args.sleep_minute,
47 | args.sleep_time,
48 | args.max_follow,
49 | )
50 |
51 |
52 | # Grab users from the most popular users' followers list
53 | if args.popular:
54 | try:
55 | res = bot.session.get(
56 | "https://raw.githubusercontent.com/gayanvoice/top-github-users/main/cache/" + args.popular + ".json"
57 | )
58 | except requests.exceptions.RequestException as e:
59 | raise SystemExit(e)
60 |
61 | if res.status_code == 404:
62 | raise ValueError(f"\n\"{args.popular}\" is not a valid country. Check README for the valid countries.\n")
63 |
64 | popularUsers = res.json()
65 |
66 | print("\nGrabbing most popular users' followers.\n")
67 | for popularUser in popularUsers:
68 | # Check if we already have enough usernames
69 | if bot.maxAction != None:
70 | if len(bot.usersToAction) >= int(bot.maxAction):
71 | break
72 |
73 | bot.getFollowers(popularUser["login"])
74 |
75 |
76 | # Grab users to follow from given user's followers
77 | if args.user_target:
78 | bot.getFollowers(args.user_target)
79 |
80 |
81 | # Grab users from given file
82 | if args.file:
83 | with open(args.file, "r+") as file:
84 | try:
85 | fileUsers = json.load(file)
86 | except:
87 | raise ValueError("\n JSON file is in incorrect format.")
88 | fileUsersNotFollowed = [v for v in bot.followings if v not in fileUsers]
89 | bot.usersToAction.extend(fileUsersNotFollowed)
90 |
91 |
92 | # Write users to be followed to file
93 | filename = (
94 | "./logs/"
95 | + str(datetime.now().strftime("%m-%d-%Y__%H-%M"))
96 | + "__"
97 | + str(len(bot.usersToAction))
98 | + "-followed-users.json"
99 | )
100 | os.makedirs(os.path.dirname(filename), exist_ok=True)
101 | with open(filename, "w+") as f:
102 | json.dump(bot.usersToAction, f, indent=4)
103 |
104 | bot.follow()
105 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
GitHub Follow Bot
2 |
3 | Table of Contents
4 |
5 | - [Disclaimer](#disclaimer)
6 | - [Getting Started](#getting-started)
7 | - [Install Requirements](#install-requirements)
8 | - [Authenticate](#authenticate)
9 | - [Get a GitHub Personal Access Token](#get-a-github-personal-access-token)
10 | - [Add your GitHub username and PAT to `.env` file](#add-your-github-username-and-pat-to-env-file)
11 | - [How to Use](#how-to-use)
12 | - [Follow](#follow)
13 | - [Target user's followers](#target-users-followers)
14 | - [Followers of the most popular users from a country](#followers-of-the-most-popular-users-from-a-country)
15 | - [From a file](#from-a-file)
16 | - [Unfollow](#unfollow)
17 | - [All](#all)
18 | - [Followers](#followers)
19 | - [Non-followers](#non-followers)
20 | - [From a file](#from-a-file-1)
21 | - [Options](#options)
22 | - [Maximum follows/unfollows](#maximum-followsunfollows)
23 | - [Speed](#speed)
24 | - [Future Implementation](#future-implementation)
25 | - [Contributing](#contributing)
26 | - [Resources](#resources)
27 |
28 | ## Disclaimer
29 |
30 | **This is a PoC and was developed for educational purposes only. You may get your account banned. Use at your own risk.**
31 |
32 | > ### Spam and Inauthentic Activity on GitHub
33 | > Automated excessive bulk activity and coordinated inauthentic activity, such as spamming, are prohibited on GitHub. Prohibited activities include:
34 | > - (...)
35 | > - inauthentic interactions, such as fake accounts and automated inauthentic activity
36 | > - rank abuse, such as automated starring or following
37 |
38 | [From GitHub Acceptable Use Policies](https://docs.github.com/en/github/site-policy/github-acceptable-use-policies#4-spam-and-inauthentic-activity-on-github)
39 |
40 | ## Getting Started
41 |
42 | ### Install Requirements
43 |
44 | ```
45 | pip install -r requirements.txt
46 | ```
47 |
48 | ### Authenticate
49 |
50 | #### Get a GitHub Personal Access Token
51 |
52 | Make sure to enable the `user` scope and all subscopes inside of that permission.
53 |
54 | [How to get your GitHub PAT](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line)
55 |
56 | #### Add your GitHub username and PAT to `.env` file
57 |
58 | Create a `.env` file on the project's root directory or edit `.env.sample` (rename to `.env`) and add your username and PAT.
59 |
60 | ```
61 | GITHUB_USER=YOUR_GITHUB_USERNAME
62 | TOKEN=YOUR_GITHUB_PERSONAL_ACCESS_TOKEN
63 | ```
64 |
65 | ## How to Use
66 |
67 | ### Follow
68 |
69 | #### Target user's followers
70 | ```
71 | python bot_follow.py -t
72 | ```
73 | #### Followers of the most popular users from a country
74 | ([list of valid countries](https://github.com/gayanvoice/top-github-users#readme))
75 | ```
76 | python bot_follow.py -p
77 | ```
78 | #### From a file
79 | Follow users from a pre-generated file (JSON)
80 | ```
81 | python bot_follow.py -f
82 | ```
83 |
84 | ### Unfollow
85 |
86 | note: Unfollow order is FIFO, as in the most recently followed user will be the last to be unfollowed.
87 |
88 | #### All
89 | Unfollow all your followings
90 | ```
91 | python bot_unfollow.py -a
92 | ```
93 | #### Followers
94 | Only unfollow users who already follow you
95 | ```
96 | python bot_unfollow.py -fo
97 | ```
98 | #### Non-followers
99 | Only unfollow users who don't follow you back
100 | ```
101 | python bot_unfollow.py -nf
102 | ```
103 | #### From a file
104 | Unfollow users from a pre-generated file (JSON)
105 | ```
106 | python bot_unfollow.py -f
107 | ```
108 |
109 | ### Options
110 |
111 | #### Maximum follows/unfollows
112 | Set the maximum number of follow/unfollow actions
113 | ```
114 | -m 300
115 | ```
116 |
117 | #### Speed
118 |
119 | A random delay (in seconds) is performed after follow/unfollow actions or when the account is rate limited.
120 | You can change these delays to your liking with the following arguments:
121 |
122 | - Minimum delay between actions
123 | ```
124 | -smin 20
125 | ```
126 | - Maximum delay between actions
127 | ```
128 | -smax 120
129 | ```
130 | - Minimum delay when rate limited
131 | ```
132 | -slmin 600
133 | ```
134 | - Maximum delay when rate limited
135 | ```
136 | -slmin 1500
137 | ```
138 |
139 | ## Future Implementation
140 |
141 | - Schedule - Bot only performs actions between set time and sleeps after off-schedule
142 | - Max follow per source - Follow max `n` per popular user
143 | - Add follow source - Follow users per topic
144 | - Add follow source - Grab followers from users listed in a file
145 | - Email followed users - Send an email to followed users with templates (colaboration, follow back or custom)
146 | - Star `n` repositories of followed users
147 |
148 | ## Contributing
149 |
150 | Contributions are welcome! Read the [contribution guidelines](https://github.com/Correia-jpv/.github/blob/main/CONTRIBUTING.md#contributing) first.
151 |
152 | Wish there was another feature? Feel free to open an [feature request issue](/../../issues/new?assignees=Correia-jpv&labels=enhancement&template=feature-request.md&title=%5BREQUEST%5D) with your suggestion!
153 |
154 | If you find a bug, kindly open an [bug report issue](/../../issues/new?assignees=Correia-jpv&labels=bug&template=bug_report.md&title=%5BBUG%5D) as described in the contribution guidelines.
155 |
156 | ## Resources
157 |
158 | - [GitHub API](https://docs.github.com/en/rest)
159 | - [Top GitHub Users By Country](https://github.com/gayanvoice/top-github-users)
160 | - [GitHub-Follow-Bot](https://github.com/TheDarkAssassins/Github-Follow-Bot)
161 |
--------------------------------------------------------------------------------
/GithubAPIBot.py:
--------------------------------------------------------------------------------
1 | from base64 import b64encode
2 | import datetime
3 | import random
4 | import requests
5 | from requests.adapters import HTTPAdapter
6 | import time
7 | from tqdm import tqdm
8 | from urllib.parse import parse_qs
9 | from urllib.parse import urlparse
10 | from urllib3.util import Retry
11 |
12 |
13 | class GithubAPIBot:
14 | # Constructor
15 | def __init__(
16 | self,
17 | username: str,
18 | token: str,
19 | sleepSecondsActionMin: int,
20 | sleepSecondsActionMax: int,
21 | sleepSecondsLimitedMin: int,
22 | sleepSecondsLimitedMax: int,
23 | sleepHour=None,
24 | sleepMinute=None,
25 | sleepTime=None,
26 | maxAction=None,
27 | ):
28 | if not isinstance(username, str):
29 | raise TypeError("Missing/Incorrect username")
30 | if not isinstance(token, str):
31 | raise TypeError("Missing/Incorrect token")
32 |
33 | self.__username = username
34 | self.__token = token
35 | self.__sleepSecondsActionMin = sleepSecondsActionMin
36 | self.__sleepSecondsActionMax = sleepSecondsActionMax
37 | self.__sleepSecondsLimitedMin = sleepSecondsLimitedMin
38 | self.__sleepSecondsLimitedMax = sleepSecondsLimitedMax
39 | self.__sleepHour = sleepHour
40 | self.__sleepMinute = sleepMinute
41 | self.__sleepTime = sleepTime
42 | self.__maxAction = maxAction
43 | self.__usersToAction = []
44 | self.__followings = []
45 |
46 | # Requests' headers
47 | HEADERS = {
48 | "Authorization": "Basic " + b64encode(str(self.token + ":" + self.token).encode("utf-8")).decode("utf-8")
49 | }
50 |
51 | # Session
52 | self.session = requests.session()
53 | retries = Retry(total=5, backoff_factor=1, status_forcelist=[502, 503, 504])
54 | self.session.mount("https://", HTTPAdapter(max_retries=retries))
55 | self.session.headers.update(HEADERS)
56 |
57 | # Authenticate
58 | try:
59 | res = self.session.get("https://api.github.com/user")
60 | except requests.exceptions.RequestException as e:
61 | raise SystemExit(e)
62 |
63 | if res.status_code == 404:
64 | raise ValueError("\nFailure to Authenticate, please check Personal Access Token and Username!")
65 | else:
66 | print("\nSuccessful authentication.")
67 |
68 | self.getFollowings()
69 |
70 | # Getters & Setters
71 | @property
72 | def username(self):
73 | return self.__username
74 |
75 | @username.setter
76 | def username(self, value):
77 | self.__username = value
78 |
79 | @property
80 | def token(self):
81 | return self.__token
82 |
83 | @token.setter
84 | def token(self, value):
85 | self.__token = value
86 |
87 | @property
88 | def sleepSecondsActionMin(self):
89 | return self.__sleepSecondsActionMin
90 |
91 | @sleepSecondsActionMin.setter
92 | def sleepSecondsActionMin(self, value):
93 | self.__sleepSecondsActionMin = value
94 |
95 | @property
96 | def sleepSecondsActionMax(self):
97 | return self.__sleepSecondsActionMax
98 |
99 | @sleepSecondsActionMax.setter
100 | def sleepSecondsActionMax(self, value):
101 | self.__sleepSecondsActionMax = value
102 |
103 | @property
104 | def sleepSecondsLimitedMin(self):
105 | return self.__sleepSecondsLimitedMin
106 |
107 | @sleepSecondsLimitedMin.setter
108 | def sleepSecondsLimitedMin(self, value):
109 | self.__sleepSecondsLimitedMin = value
110 |
111 | @property
112 | def sleepSecondsLimitedMax(self):
113 | return self.__sleepSecondsLimitedMax
114 |
115 | @sleepSecondsLimitedMax.setter
116 | def sleepSecondsLimitedMax(self, value):
117 | self.__sleepSecondsLimitedMax = value
118 |
119 | @property
120 | def sleepHour(self):
121 | return self.__sleepHour
122 |
123 | @sleepHour.setter
124 | def sleepHour(self, value):
125 | self.__sleepHour = value
126 |
127 | @property
128 | def sleepMinute(self):
129 | return self.__sleepMinute
130 |
131 | @sleepMinute.setter
132 | def sleepMinute(self, value):
133 | self.__sleepMinute = value
134 |
135 | @property
136 | def sleepTime(self):
137 | return self.__sleepTime
138 |
139 | @sleepTime.setter
140 | def sleepTime(self, value):
141 | self.__sleepTime = value
142 |
143 | @property
144 | def maxAction(self):
145 | return self.__maxAction
146 |
147 | @maxAction.setter
148 | def maxAction(self, value):
149 | self.__maxAction = value
150 |
151 | @property
152 | def usersToAction(self):
153 | return self.__usersToAction
154 |
155 | @usersToAction.setter
156 | def usersToAction(self, value):
157 | self.__usersToAction = value
158 |
159 | @property
160 | def followings(self):
161 | return self.__followings
162 |
163 | @followings.setter
164 | def followings(self, value):
165 | self.__followings = value
166 |
167 | def getUsers(self, url="", maxAction=None, following=False):
168 | users = []
169 |
170 | try:
171 | res = self.session.get(url)
172 | except requests.exceptions.RequestException as e:
173 | raise SystemExit(e)
174 |
175 | # Get usernames from each page
176 | page = 1
177 | while True:
178 | try:
179 | res = self.session.get(url + "?page=" + str(page)).json()
180 | except requests.exceptions.RequestException as e:
181 | raise SystemExit(e)
182 |
183 | for user in res:
184 | # Check if we already have enough usernames
185 | if maxAction != None:
186 | if len(users) >= int(maxAction):
187 | break
188 |
189 | # Add username if it's not being followed already
190 | if (
191 | not following
192 | and not (user["login"] in self.followings)
193 | or following
194 | and (user["login"] in self.followings)
195 | ):
196 | users.append(user["login"])
197 |
198 | # Check if we already have enough usernames
199 | if maxAction != None:
200 | if len(users) >= int(maxAction):
201 | break
202 |
203 | if res == []:
204 | break
205 | else:
206 | page += 1
207 |
208 | return users
209 |
210 | def getFollowers(self, username=None, following=None):
211 | if username == None:
212 | username = self.username
213 | print(f"\nGrabbing {username}'s followers.\n")
214 | self.usersToAction.extend(
215 | self.getUsers(
216 | url=f"https://api.github.com/users/{username}/followers",
217 | maxAction=self.maxAction,
218 | following=following,
219 | )
220 | )
221 |
222 | def getFollowings(self, username=None):
223 | if username == None:
224 | username = self.username
225 | print(f"\nGrabbing {username}'s followings.\n")
226 | self.followings.extend(self.getUsers(url=f"https://api.github.com/users/{username}/following"))
227 |
228 | def run(self, action):
229 | if len(self.usersToAction) == 0:
230 | print(f"Nothing to {action}")
231 | else:
232 |
233 | # Users to follow/unfollow must not exceed the given max
234 | if self.maxAction != None:
235 | self.usersToAction = self.usersToAction[: min(len(self.usersToAction), int(self.maxAction))]
236 |
237 | # Time for the bot to go to sleep
238 | if self.sleepHour != None and self.sleepMinute != None and self.sleepTime != None:
239 | sleepTime = nextSleepTime(int(self.__sleepHour), int(self.sleepMinute))
240 |
241 | # Start follow/unfollow
242 | print(f"\nStarting to {action}.\n")
243 | users = tqdm(
244 | self.usersToAction,
245 | initial=1,
246 | dynamic_ncols=True,
247 | smoothing=True,
248 | bar_format="[PROGRESS] {n_fmt}/{total_fmt} |{l_bar}{bar}|",
249 | position=0,
250 | leave=False,
251 | )
252 | for user in users:
253 |
254 | # Set the bot to sleep at the set time
255 | if self.sleepHour != None and self.sleepMinute != None and self.sleepTime != None:
256 | timeNow = datetime.datetime.now()
257 | if timeNow.timestamp() > sleepTime.timestamp():
258 | sleepTime = nextSleepTime(int(self.__sleepHour), int(self.__sleepMinute))
259 | timeNow += datetime.timedelta(hours=int(self.__sleepTime))
260 | sleepUntil(timeNow.hour, random.randint(0, 59))
261 |
262 | # Follow/unfollow user
263 | try:
264 | if action == "follow":
265 | res = self.session.put(f"https://api.github.com/user/following/{user}")
266 | else:
267 | res = self.session.delete(f"https://api.github.com/user/following/{user}")
268 | except requests.exceptions.RequestException as e:
269 | raise SystemExit(e)
270 |
271 | # Unsuccessful
272 | if res.status_code != 204:
273 | sleepSeconds = random.randint(self.sleepSecondsLimitedMin, self.sleepSecondsLimitedMax)
274 | # Successful
275 | else:
276 | sleepSeconds = random.randint(self.sleepSecondsActionMin, self.sleepSecondsActionMax)
277 |
278 | # Sleep
279 | sleepSecondsObj = list(range(0, sleepSeconds))
280 | sleepSecondsBar = tqdm(
281 | sleepSecondsObj,
282 | dynamic_ncols=True,
283 | smoothing=True,
284 | bar_format="[SLEEPING] {n_fmt}s/{total_fmt}s |{l_bar}{bar}|",
285 | position=1,
286 | leave=False,
287 | )
288 | for second in sleepSecondsBar:
289 | time.sleep(1)
290 |
291 | print(f"\n\nFinished {action}ing!")
292 |
293 | def follow(self):
294 | self.run("follow")
295 |
296 | def unfollow(self):
297 | self.run("unfollow")
298 |
299 | def nextSleepTime(hour, minute):
300 | timeNow = datetime.datetime.now()
301 | future = datetime.datetime(timeNow.year, timeNow.month, timeNow.day, hour, minute)
302 |
303 | if timeNow.timestamp() > future.timestamp():
304 | future += datetime.timedelta(days=1)
305 | return future
306 |
307 | def sleepUntil(hour, minute):
308 | t = datetime.datetime.today()
309 | future = datetime.datetime(t.year, t.month, t.day, hour, minute)
310 |
311 | if t.timestamp() >= future.timestamp():
312 | future += datetime.timedelta(days=1)
313 |
314 | print(f'\nSleeping... Waking up at {future.hour}:{future.minute}')
315 |
316 | sleepSeconds = int((future-t).total_seconds())
317 | sleepSecondsObj = list(range(0, sleepSeconds))
318 | sleepSecondsBar = tqdm(
319 | sleepSecondsObj,
320 | dynamic_ncols=True,
321 | smoothing=True,
322 | bar_format="[SLEEPING] {n_fmt}s/{total_fmt}s |{l_bar}{bar}|",
323 | position=2,
324 | leave=False,
325 | )
326 | for second in sleepSecondsBar:
327 | time.sleep(1)
328 |
329 | print(f'\nWaking up...')
--------------------------------------------------------------------------------