├── LICENSE
├── README.md
├── ghbot
└── install
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 x3ric
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 |
2 |
3 | # OctoBot
4 |
5 | Tired of OctoCat hogging the spotlight?
6 |
7 | ### Installation
8 |
9 |
10 | curl -s https://raw.githubusercontent.com/X3ric/octobot/main/install | bash
11 |
12 |
13 | ### Usage
14 |
15 |
16 | ghbot <command> [username]
17 |
18 |
19 |
20 | follow [username] [blacklist] — Follow all followers of the specified user.
21 | unfollow [blacklist] — Unfollow who do not follow back.
22 | following — Shows count of users you follow.
23 | followers — Show count of your followers.
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/ghbot:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Bash OctoBot
3 |
4 | if [[ -z "$personal_github_token" ]]; then
5 | cat << EOF
6 | Error: 'personal_github_token' environment variable not set.
7 |
8 | To resolve this:
9 | 1. Generate a GitHub personal access token with the 'user:follow' and 'read:user' scopes at https://github.com/settings/tokens.
10 | 2. Set the token in your environment with:
11 | export personal_github_token="your_token_here"
12 | 3. To make this change permanent, add it to your '~/.bashrc' with:
13 | echo 'export personal_github_token="your_token_here"' >> ~/.bashrc
14 | source ~/.bashrc
15 |
16 | After setting up the token, you can run OctoBot commands with:
17 | ghbot [username]
18 |
19 | For more details, visit the GitHub repository.
20 | EOF
21 | exit 1
22 | fi
23 |
24 | github_user=$(curl -s -H "Authorization: token $personal_github_token" https://api.github.com/user | jq -r '.login')
25 | rate_limit_count_file=$(mktemp /tmp/rate_limit_count.XXXXXX)
26 |
27 | handle_rate_limit() {
28 | local count=$(cat "$rate_limit_count_file" 2>/dev/null || echo 0)
29 | count=$((count + 1))
30 | local delay=$((60 * count))
31 | echo "Rate limit exceeded. Waiting for ${delay}s ..."
32 | sleep "$delay"
33 | echo "$count" > "$rate_limit_count_file"
34 | }
35 |
36 | rate_reset() {
37 | rm -f "$rate_limit_count_file"
38 | }
39 |
40 | fetch_data() {
41 | local url="$1"
42 | local all_data=()
43 | while [[ -n "$url" ]]; do
44 | local response=$(curl -s -H "Authorization: token $personal_github_token" "$url")
45 | local status_code=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $personal_github_token" "$url")
46 | if [[ "$status_code" -eq 429 ]]; then
47 | handle_rate_limit
48 | continue
49 | fi
50 | rate_reset
51 | all_data+=($(echo "$response" | jq -r '.[] | .login'))
52 | url=$(curl -sI -H "Authorization: token $personal_github_token" "$url" | grep -i '^Link:' | sed -e 's/.*<\(https:\/\/api.github.com\/[^\"]*\)>; rel="next".*/\1/')
53 | done
54 | echo "${all_data[@]}"
55 | }
56 |
57 | fetch_followers() {
58 | local user="$1"
59 | local page=1
60 | local per_page=100
61 | local all_followers=()
62 | local followers_url="https://api.github.com/users/$user/followers?per_page=$per_page&page="
63 | while :; do
64 | local response=$(curl -s -H "Authorization: token $personal_github_token" "${followers_url}${page}")
65 | if [[ "$response" == *"API rate limit exceeded"* ]]; then
66 | handle_rate_limit
67 | response=$(curl -s -H "Authorization: token $personal_github_token" "${followers_url}${page}")
68 | fi
69 | local followers=$(echo "$response" | jq -r '.[].login')
70 | if [[ -z "$followers" ]]; then
71 | break
72 | fi
73 | all_followers+=($followers)
74 | if [[ $(echo "$response" | jq '. | length') -lt $per_page ]]; then
75 | break
76 | fi
77 | page=$((page + 1))
78 | done
79 | rate_reset
80 | echo "${all_followers[@]}"
81 | }
82 |
83 | fetch_following() {
84 | local user="$1"
85 | local page=1
86 | local per_page=100
87 | local all_following=()
88 | local following_url="https://api.github.com/users/$user/following?per_page=$per_page&page="
89 | while :; do
90 | local response=$(curl -s -H "Authorization: token $personal_github_token" "${following_url}${page}")
91 | if [[ "$response" == *"API rate limit exceeded"* ]]; then
92 | handle_rate_limit
93 | response=$(curl -s -H "Authorization: token $personal_github_token" "${following_url}${page}")
94 | fi
95 | local following=$(echo "$response" | jq -r '.[].login')
96 | if [[ -z "$following" ]]; then
97 | break
98 | fi
99 | all_following+=($following)
100 | if [[ $(echo "$response" | jq '. | length') -lt $per_page ]]; then
101 | break
102 | fi
103 | page=$((page + 1))
104 | done
105 | rate_reset
106 | echo "${all_following[@]}"
107 | }
108 |
109 | follow_user() {
110 | local user="$1"
111 | local user_info=$(curl -s -H "Authorization: token $personal_github_token" "https://api.github.com/users/$user")
112 | local followers=$(echo "$user_info" | jq -r '.followers')
113 | local following=$(echo "$user_info" | jq -r '.following')
114 | if [[ "$followers" == "null" || "$following" == "null" ]]; then
115 | #echo "User $user not found or profile is private."
116 | return
117 | fi
118 | if [[ "$followers" -eq 0 && "$following" -eq 0 ]]; then
119 | #echo "User $user has 0 followers and 0 following. Possibly a private profile or new account."
120 | return
121 | fi
122 | local response=$(curl -s -o /dev/null -w "%{http_code}" -X PUT -H "Authorization: token $personal_github_token" "https://api.github.com/user/following/$user")
123 | case "$response" in
124 | "204")
125 | echo "User: $user has been followed!"
126 | ;;
127 | "403"|"429"|"500")
128 | echo "Error following $user: Forbidden. Handling rate limit..."
129 | handle_rate_limit
130 | follow_user "$user"
131 | ;;
132 | "404")
133 | echo "Error following $user: User not found or profile is private."
134 | ;;
135 | "422")
136 | echo "Error following $user: User already followed or invalid request."
137 | ;;
138 | *)
139 | echo "Error following $user: $response"
140 | exit 1
141 | ;;
142 | esac
143 | rate_reset
144 | }
145 |
146 | unfollow_user() {
147 | local user="$1"
148 | local user_info=$(curl -s -H "Authorization: token $personal_github_token" "https://api.github.com/users/$user")
149 | local followers=$(echo "$user_info" | jq -r '.followers')
150 | local following=$(echo "$user_info" | jq -r '.following')
151 | if [[ "$followers" == "null" || "$following" == "null" ]]; then
152 | #echo "User $user not found or profile is private."
153 | return
154 | fi
155 | if [[ "$followers" -eq 0 && "$following" -eq 0 ]]; then
156 | #echo "User $user has 0 followers and 0 following. Possibly a private profile or new account."
157 | return
158 | fi
159 | local response=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE -H "Authorization: token $personal_github_token" "https://api.github.com/user/following/$user")
160 | case "$response" in
161 | "204")
162 | echo "User: $user has been unfollowed!"
163 | ;;
164 | "403"|"429"|"500")
165 | echo "Error unfollowing $user: Forbidden. Handling rate limit..."
166 | handle_rate_limit
167 | unfollow_user "$user"
168 | ;;
169 | *)
170 | echo "Error unfollowing $user: $response"
171 | exit 1
172 | ;;
173 | esac
174 | rate_reset
175 | }
176 |
177 | process_users() {
178 | local users=("$@")
179 | local max_jobs=10
180 | local job_count=0
181 | for user in "${users[@]}"; do
182 | if [[ "$command" == "unfollow" ]]; then
183 | unfollow_user "$user"
184 | elif [[ "$command" == "follow" ]]; then
185 | follow_user "$user"
186 | fi
187 | ((job_count++))
188 | if [[ $job_count -ge $max_jobs ]]; then
189 | wait -n
190 | job_count=$((job_count - 1))
191 | fi
192 | done
193 | wait
194 | }
195 |
196 | parse_blacklist() {
197 | local blacklist_input="$1"
198 | IFS=$'\n' read -r -d '' -a blacklist <<< "$(echo "$blacklist_input" | tr ',' '\n')"
199 | }
200 |
201 | cleanup() {
202 | rate_reset
203 | exit 0
204 | }
205 |
206 | trap cleanup SIGINT EXIT
207 |
208 | command="$1"
209 | shift
210 |
211 | blacklist=()
212 |
213 | case "$command" in
214 | "unfollow")
215 | if [[ ! -z "$1" ]]; then
216 | blacklist_input="$1"
217 | parse_blacklist "$blacklist_input"
218 | fi
219 | following=($(fetch_following "$github_user"))
220 | followers=($(fetch_followers "$github_user"))
221 | users_to_unfollow=()
222 | for user in "${following[@]}"; do
223 | if [[ ! " ${followers[@]} " =~ " $user " && "$user" != "$github_user" && ! " ${blacklist[@]} " =~ " $user " ]]; then
224 | users_to_unfollow+=("$user")
225 | fi
226 | done
227 | process_users "${users_to_unfollow[@]}"
228 | ;;
229 | "followers")
230 | followers=($(fetch_followers "$github_user"))
231 | echo "You have ${#followers[@]} followers."
232 | ;;
233 | "following")
234 | following=($(fetch_following "$github_user"))
235 | echo "You follow ${#following[@]} users."
236 | ;;
237 | "follow")
238 | if [[ ! -z "$1" ]]; then
239 | target_user="$1"
240 | shift
241 | elif [[ -z "$target_user" ]]; then
242 | read -p "User to fetch? " target_user
243 | fi
244 | if [[ ! -z "$1" ]]; then
245 | blacklist_input="$1"
246 | parse_blacklist "$blacklist_input"
247 | fi
248 | followers=($(fetch_followers "$target_user"))
249 | following=($(fetch_following "$github_user"))
250 | users_to_follow=()
251 | for user in "${followers[@]}"; do
252 | if [[ ! " ${following[@]} " =~ " $user " && "$user" != "$github_user" && ! " ${blacklist[@]} " =~ " $user " ]]; then
253 | users_to_follow+=("$user")
254 | fi
255 | done
256 | process_users "${users_to_follow[@]}"
257 | ;;
258 | *)
259 | cat << EOF
260 | Invalid option: $command
261 |
262 | Usage:
263 | ghbot followers # Display the count of your followers
264 | ghbot following # Display the count of users you follow
265 | ghbot follow [username] [blacklist] # Follow users who follow the specified username
266 | ghbot unfollow [blacklist] # Unfollow users who do not follow you back
267 |
268 | Info:
269 | blacklist -> \"usr0,usr1,..\" (Optional)
270 |
271 | EOF
272 | exit 1
273 | ;;
274 | esac
275 | echo -e "Operation completed!"
276 | cleanup
277 |
--------------------------------------------------------------------------------
/install:
--------------------------------------------------------------------------------
1 | #!/bin/env bash
2 | need() {
3 | if ! command -v "$1" &>/dev/null; then
4 | if command -v pacman &>/dev/null; then
5 | sudo pacman -Sy --needed --noconfirm "$1"
6 | else
7 | echo "install $1 with your distro pkgmanager"
8 | fi
9 | fi
10 | }
11 | need curl
12 | need git
13 | need jq
14 | git clone https://github.com/X3ric/octobot
15 | cd octobot
16 | chmod +x "./ghbot"
17 | if [[ ":$PATH:" == *":$HOME/.local/bin:"* ]]; then
18 | cp "./ghbot" "$HOME/.local/bin"
19 | else
20 | sudo cp "./ghbot" "/usr/bin"
21 | fi
22 | cd ..
23 | rm -rf octobot
24 |
--------------------------------------------------------------------------------