├── 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 | Arch Linux 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 | --------------------------------------------------------------------------------