Skip to content

Instantly share code, notes, and snippets.

@lauslim12
Created December 17, 2021 03:09
Show Gist options
  • Save lauslim12/4a4f604be59d8b67cd58a5de7ca7d3a6 to your computer and use it in GitHub Desktop.
Save lauslim12/4a4f604be59d8b67cd58a5de7ca7d3a6 to your computer and use it in GitHub Desktop.
Find people who do not follow you back on GitHub.
// Package main is a CLI runner to run this script ('github-unfollowers.go').
// This script is used to get a user's 'unfollowers' - which means people who do not follow that user's back.
// You do not need GitHub Tokens, as all the endpoints are public. Use this with responsibility though, do not spam.
//
// How to run: copy this script to a workspace, place your username, then run 'go run github-unfollowers.go'
// You can place your username by using 'export GITHUB_USERNAME=YOUR_GITHUB_USERNAME' or by editing the source code and filling it manually.
//
// Author & License: Nicholas (MIT)
package main
import (
"encoding/json"
"errors"
"fmt"
"log"
"math"
"net/http"
"os"
"strings"
)
// Social is a struct representative of 'Followers' and 'Following' entity from the API.
type Social struct {
Login string `json:"login"`
ID uint64 `json:"id"`
NodeID string `json:"node_id"`
AvatarURL string `json:"avatar_url"`
GravatarID string `json:"gravatar_id"`
URL string `json:"url"`
HTMLURL string `json:"html_url"`
FollowersURL string `json:"followers_url"`
FollowingURL string `json:"following_url"`
GistsURL string `json:"gists_url"`
StarredURL string `json:"starred_url"`
SubscriptionsURL string `json:"subscriptions_url"`
OrganizationsURL string `json:"organizations_url"`
ReposURL string `json:"repos_url"`
EventsURL string `json:"events_url"`
ReceivedEventsURL string `json:"received_events_url"`
Type string `json:"type"`
SiteAdmin bool `json:"site_admin"`
}
// User is a struct representative of 'User' data from GitHub's API.
type User struct {
Login string `json:"login"`
ID uint64 `json:"id"`
NodeID string `json:"node_id"`
AvatarURL string `json:"avatar_url"`
GravatarID string `json:"gravatar_id"`
URL string `json:"url"`
HTMLURL string `json:"html_url"`
FollowersURL string `json:"followers_url"`
FollowingURL string `json:"following_url"`
GistsURL string `json:"gists_url"`
StarredURL string `json:"starred_url"`
SubscriptionsURL string `json:"subscriptions_url"`
OrganizationsURL string `json:"organizations_url"`
ReposURL string `json:"repos_url"`
EventsURL string `json:"events_url"`
ReceivedEventsURL string `json:"received_events_url"`
Type string `json:"type"`
SiteAdmin bool `json:"site_admin"`
Name string `json:"name"`
Company string `json:"company"`
Blog string `json:"blog"`
Location string `json:"location"`
Bio string `json:"bio"`
TwitterUsername string `json:"twitter_username"`
PublicRepos uint64 `json:"public_repos"`
PublicGists uint64 `json:"public_gists"`
Followers uint64 `json:"followers"`
Following uint64 `json:"following"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// createRequest creates a single request to fetch from GitHub API using GitHub's unique
// 'Accept' header (application/vnd.github.v3+json).
func createRequest(url string) (*http.Response, error) {
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
req.Header.Set("Accept", "application/vnd.github.v3+json")
if err != nil {
return nil, err
}
res, err := client.Do(req)
if err != nil {
return nil, err
}
return res, nil
}
// findUnfollowers finds all users who does not follow you back.
// This is done by performing a search on the existing sets.
func findUnfollowers(followersSet map[string]bool, followingSet map[string]bool) map[string]bool {
unfollowers := map[string]bool{}
for key := range followingSet {
if ok := followersSet[key]; !ok {
unfollowers[key] = true
}
}
return unfollowers
}
// getSocial fetches either 'followers' or 'following' data from GitHub API.
// Variable 'numberOfRequests' is divided by 100 as 100 is the maximum number of data
// in a single page.
func getSocial(username string, total uint64, subject string) ([]Social, error) {
social := []Social{}
if subject != "followers" && subject != "following" {
return nil, errors.New("social: subject has to be either 'followers' or 'following'")
}
numberOfRequests := math.Ceil(float64(total) / 100)
for i := 1; i <= int(numberOfRequests); i++ {
currentSocial := []Social{}
url := fmt.Sprintf("https://api.github.com/users/%s/%s?per_page=100&page=%d", username, subject, i)
res, err := createRequest(url)
if err != nil {
return nil, err
}
defer res.Body.Close()
err = json.NewDecoder(res.Body).Decode(&currentSocial)
if err != nil {
return nil, err
}
social = append(social, currentSocial...)
}
return social, nil
}
// getUser fetches data of a single user from GitHub API.
func getUser(username string) (*User, error) {
user := &User{}
url := fmt.Sprintf("https://api.github.com/users/%s", username)
res, err := createRequest(url)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode == 404 {
return nil, errors.New("getUser: failed to find a GitHub user with that username")
}
err = json.NewDecoder(res.Body).Decode(user)
if err != nil {
return nil, err
}
return user, nil
}
// printSet prints all of a set's contents with a unique message.
func printSet(message string, set map[string]bool) {
counter := 1
fmt.Println(message)
for key := range set {
fmt.Printf("%d. %s\n", counter, key)
counter++
}
fmt.Printf("\n\n")
}
// socialToSet transforms a social array into a set data structure.
func socialToSet(social []Social) map[string]bool {
set := map[string]bool{}
for _, user := range social {
set[user.Login] = true
}
return set
}
// Driver code that runs the whole program. If you want to fill your username,
// you can use 'export GITHUB_USERNAME=YOUR_GITHUB_USERNAME' before running this script,
// or by editing the 'username' variable manually.
func main() {
username := strings.TrimSpace(os.Getenv("GITHUB_USERNAME"))
if username == "" {
log.Fatal(errors.New("main: variable 'username' is blank, it has to defined beforehand"))
}
user, err := getUser(username)
if err != nil {
log.Fatal(err.Error())
}
followers, err := getSocial(username, user.Followers, "followers")
if err != nil {
log.Fatal(err.Error())
}
following, err := getSocial(username, user.Following, "following")
if err != nil {
log.Fatal(err.Error())
}
followersSet := socialToSet(followers)
printSet("Followers:", followersSet)
followingSet := socialToSet(following)
printSet("Following:", followingSet)
unfollowers := findUnfollowers(followersSet, followingSet)
printSet("Unfollowers:", unfollowers)
fmt.Println("GitHub Unfollowers has finished running.")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment