8000 🎉 Initial Commit · Correia-jpv/github-follow-bot@f8998f0 · GitHub
[go: up one dir, main page]

Skip to content

Commit f8998f0

Browse files
author
João PV Correia
committed
🎉 Initial Commit
0 parents  commit f8998f0

File tree

8 files changed

+590
-0
lines changed

8 files changed

+590
-0
lines changed

‎.env.sample

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
USER=YOUR_GITHUB_USERNAME
2+
TOKEN=YOUR_GITHUB_PERSONAL_ACCESS_TOKEN

‎.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__pycache__/
2+
logs/
3+
.env

‎GithubAPIBot.py

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
from base64 import b64encode
2+
import random
3+
import requests
4+
from requests.adapters import HTTPAdapter
5+
import time
6+
from tqdm import tqdm
7+
from urllib.parse import parse_qs
8+
from urllib.parse import urlparse
9+
from urllib3.util import Retry
10+
11+
12+
class GithubAPIBot:
13+
# Constructor
14+
def __init__(
15+
self,
16+
username: str,
17+
token: str,
18+
sleepSecondsActionMin: int,
19+
sleepSecondsActionMax: int,
20+
sleepSecondsLimitedMin: int,
21+
sleepSecondsLimitedMax: int,
22+
maxAction=None,
23+
):
24+
if not isinstance(username, str):
25+
raise TypeError("Missing/Incorrect username")
26+
if not isinstance(token, str):
27+
raise TypeError("Missing/Incorrect token")
28+
29+
self.__username = username
30+
self.__token = token
31+
self.__sleepSecondsActionMin = sleepSecondsActionMin
32+
self.__sleepSecondsActionMax = sleepSecondsActionMax
33+
self.__sleepSecondsLimitedMin = sleepSecondsLimitedMin
34+
self.__sleepSecondsLimitedMax = sleepSecondsLimitedMax
35+
self.__maxAction = maxAction
36+
self.__usersToAction = []
37+
self.__followings = []
38+
39+
# Requests' headers
40+
HEADERS = {
41+
"Authorization": "Basic " + b64encode(str(self.token + ":" + self.token).encode("utf-8")).decode("utf-8")
42+
}
43+
44+
# Session
45+
self.session = requests.session()
46+
retries = Retry(total=5, backoff_factor=1, status_forcelist=[502, 503, 504])
47+
self.session.mount("https://", HTTPAdapter(max_retries=retries))
48+
self.session.headers.update(HEADERS)
49+
50+
# Authenticate
51+
try:
52+
res = self.session.get("https://api.github.com/user")
53+
except requests.exceptions.RequestException as e:
54+
raise SystemExit(e)
55+
56+
if res.status_code == 404:
57+
raise ValueError("\nFailure to Authenticate, please check Personal Access Token and Username!")
58+
else:
59+
print("\nSuccessful authentication.")
60+
61+
self.getFollowings()
62+
63+
# Getters & Setters
64+
@property
65+
def username(self):
66+
return self.__username
67+
68+
@username.setter
69+
def username(self, value):
70+
self.__username = value
71+
72+
@property
73+
def token(self):
74+
return self.__token
75+
76+
@token.setter
77+
def token(self, value):
78+
self.__token = value
79+
80+
@property
81+
def sleepSecondsActionMin(self):
82+
return self.__sleepSecondsActionMin
83+
84+
@sleepSecondsActionMin.setter
85+
def sleepSecondsActionMin(self, value):
86+
self.__sleepSecondsActionMin = value
87+
88+
@property
89+
def sleepSecondsActionMax(self):
90+
return self.__sleepSecondsActionMax
91+
92+
@sleepSecondsActionMax.setter
93+
def sleepSecondsActionMax(self, value):
94+
self.__sleepSecondsActionMax = value
95+
96+
@property
97+
def sleepSecondsLimitedMin(self):
98+
return self.__sleepSecondsLimitedMin
99+
100+
@sleepSecondsLimitedMin.setter
101+
def sleepSecondsLimitedMin(self, value):
102+
self.__sleepSecondsLimitedMin = value
103+
104+
@property
105+
def sleepSecondsLimitedMax(self):
106+
return self.__sleepSecondsLimitedMax
107+
108+
@sleepSecondsLimitedMax.setter
109+
def sleepSecondsLimitedMax(self, value):
110+
self.__sleepSecondsLimitedMax = value
111+
112+
@property
113+
def maxAction(self):
114+
return self.__maxAction
115+
116+
@maxAction.setter
117+
def maxAction(self, value):
118+
self.__maxAction = value
119+
120+
@property
121+
def usersToAction(self):
122+
return self.__usersToAction
123+
124+
@usersToAction.setter
125+
def usersToAction(self, value):
126+
self.__usersToAction = value
127+
128+
@property
129+
def followings(self):
130+
return self.__followings
131+
132+
@followings.setter
133+
def followings(self, value):
134+
self.__followings = value
135+
136+
def getUsers(self, url="", maxAction=None):
137+
users = []
138+
139+
try:
140+
res = self.session.get(url)
141+
except requests.exceptions.RequestException as e:
142+
raise SystemExit(e)
143+
144+
# Get pages of users
145+
pagesURLs = requests.utils.parse_header_links(res.headers["Link"])
146+
lastPageURL = pagesURLs[1]["url"]
147+
lastPage = parse_qs(urlparse(lastPageURL).query)["page"][0]
148+
149+
# Get usernames from each page
150+
pages = tqdm(
151+
range(int(lastPage), 1 + 1, -1),
152+
dynamic_ncols=True,
153+
smoothing=True,
154+
bar_format="[PROGRESS] {l_bar}{bar}|",
155+
leave=False,
156+
)
157+
for page in pages:
158+
try:
159+
res = self.session.get(url + "?page=" + str(page)).json()
160+
except requests.exceptions.RequestException as e:
161+
raise SystemExit(e)
162+
163+
for user in res:
164+
# Check if we already have enough usernames
165+
if maxAction != None:
166+
if len(users) >= int(maxAction):
167+
break
168+
169+
# Add username if it's not being followed already
170+
if not (user["login"] in self.followings):
171+
users.append(user["login"])
172+
173+
# Check if we already have enough usernames
174+
if maxAction != None:
175+
if len(users) >= int(maxAction):
176+
break
177+
178+
return users
179+
180+
def getFollowers(self, username=None):
181+
if username == None:
182+
username = self.username
183+
print("\nGrabbing " + username + "'s followers\n")
184+
self.usersToAction.extend(
185+
self.getUsers("https://api.github.com/users/" + username + "/followers", self.maxAction)
186+
)
187+
188+
def getFollowings(self, username=None, maxAction=None):
189+
if username == None:
190+
username = self.username
191+
print("\nGrabbing " + username + "'s followings.\n")
192+
self.followings.extend(self.getUsers("https://api.github.com/users/" + username + "/following", maxAction))
193+
194+
def run(self, action):
195+
# Users to follow/unfollow must not exceed the given max
196+
if self.maxAction != None:
197+
self.usersToAction = self.usersToAction[: min(len(self.usersToAction), int(self.maxAction))]
198+
199+
# Start follow/unfollow
200+
print("\nStarting to " + action + ".\n")
201+
users = tqdm(
202+
self.usersToAction,
203+
initial=1,
204+
dynamic_ncols=True,
205+
smoothing=True,
206+
bar_format="[PROGRESS] {n_fmt}/{total_fmt} |{l_bar}{bar}|",
207+
leave=False,
208+
)
209+
for user in users:
210+
211+
# Follow/unfollow user
212+
try:
213+
if action == "follow":
214+
res = self.session.put("https://api.github.com/user/following/" + user)
215+
else:
216+
res = self.session.delete("https://api.github.com/user/following/" + user)
217+
except requests.exceptions.RequestException as e:
218+
raise SystemExit(e)
219+
220+
# Unsuccessful
221+
if res.status_code != 204:
222+
sleepSeconds = random.randint(self.sleepSecondsLimitedMin, self.sleepSecondsLimitedMax)
223+
# Successful
224+
else:
225+
sleepSeconds = random.randint(self.sleepSecondsActionMin, self.sleepSecondsActionMax)
226+
227+
# Sleep
228+
sleepSecondsObj = list(range(0, sleepSeconds))
229+
sleepSecondsBar = tqdm(
230+
sleepSecondsObj,
231+
dynamic_ncols=True,
232+
smoothing=True,
233+
bar_format="[SLEEPING] {n_fmt}s/{total_fmt}s |{l_bar}{bar}|",
234+
)
235+
for second in sleepSecondsBar:
236+
time.sleep(1)
237+
238+
print("\n\nFinished " + action + "ing!")
239+
240+
def follow(self):
241+
self.run("follow")
242+
243+
def unfollow(self):
244+
self.run("unfollow")

‎LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.

0 commit comments

Comments
 (0)
0