package github
import (
"encoding/json"
"fmt"
"log"
"net/http"
"regexp"
"strings"
"time"
"most-active-github-users-counter/net"
)
const root string = "https://api.github.com/"
type HTTPGithubClient struct {
wrappers []net.Wrapper
}
func (client HTTPGithubClient) Request(url string, body string) ([]byte, error) {
httpClient := &http.Client{}
var req *http.Request
var err error
if body != "" {
req, err = http.NewRequest("POST", url, strings.NewReader(body))
} else {
req, err = http.NewRequest("GET", url, nil)
}
if err != nil {
return []byte{}, err
}
return net.Compose(client.wrappers...)(net.MakeRequester(httpClient))(req)
}
func (client HTTPGithubClient) CurrentUser() (User, error) {
body, err := client.Request(fmt.Sprintf("%suser", root), "")
if err != nil {
return User{}, err
}
user := User{}
if err := json.Unmarshal(body, &user); err != nil {
return User{}, err
}
return user, nil
}
func (client HTTPGithubClient) User(login string) (User, error) {
body, err := client.Request(fmt.Sprintf("%susers/%s", root, login), "")
if err != nil {
return User{}, err
}
user := User{}
if err := json.Unmarshal(body, &user); err != nil {
return User{}, err
}
return user, nil
}
func (client HTTPGithubClient) SearchUsers(query UserSearchQuery) (GithubSearchResults, error) {
users := []User{}
userLogins := map[string]bool{}
totalCount := 0
minFollowerCount := -1
maxPerQuery := 1000
perPage := 5
totalUsersCount := 0
retryCount := 0
maxRetryCount := 10
Pages:
for totalCount < query.MaxUsers {
previousCursor := ""
followerCountQueryStr := ""
if minFollowerCount >= 0 {
followerCountQueryStr = fmt.Sprintf(" followers:<%d", minFollowerCount)
}
for currentPage := 1; currentPage <= (maxPerQuery / perPage); currentPage++ {
cursorQueryStr := ""
if previousCursor != "" {
cursorQueryStr = fmt.Sprintf(", after: \\\"%s\\\"", previousCursor)
}
graphQlString := fmt.Sprintf(`{ "query": "query {
search(type: USER, query:\"%s%s sort:%s-%s\", first: %d%s) {
userCount
edges {
node {
__typename
... on User {
login,
avatarUrl,
name,
company,
organizations(first: 100) {
nodes {
login
}
}
followers {
totalCount
}
contributionsCollection {
contributionCalendar {
totalContributions
},
totalCommitContributions,
totalPullRequestContributions,
restrictedContributionsCount
}
}
},
cursor
}
}
}" }`, query.Q, followerCountQueryStr, query.Sort, query.Order, perPage, cursorQueryStr)
re := regexp.MustCompile(`\r?\n`)
graphQlString = re.ReplaceAllString(graphQlString, " ")
body, err := client.Request("https://api.github.com/graphql", graphQlString)
if err != nil {
retryCount++
if retryCount < maxRetryCount {
log.Println("error making graphql request... retrying")
time.Sleep(10 * time.Second)
continue Pages
} else {
log.Fatalln("Too many errors received. Quitting.")
}
}
var response interface{}
if err := json.Unmarshal(body, &response); err != nil {
retryCount++
if retryCount < maxRetryCount {
log.Println("error unmarshalling JSON response... retrying")
time.Sleep(10 * time.Second)
continue Pages
} else {
log.Fatalln("Too many errors received. Quitting.")
}
}
rootNode := response.(map[string]interface{})
if val, ok := rootNode["errors"]; ok {
retryCount++
if retryCount < maxRetryCount {
log.Printf("Received error response (retrying): %+v", val)
time.Sleep(10 * time.Second)
continue Pages
} else {
log.Fatalln("Too many errors received. Quitting.")
}
}
dataNode, ok := rootNode["data"].(map[string]interface{})
if !ok {
retryCount++
if retryCount < maxRetryCount {
log.Println("Error accessing data element")
time.Sleep(10 * time.Second)
continue Pages
} else {
log.Fatalln("Too many errors received. Quitting.")
}
}
searchNode := dataNode["search"].(map[string]interface{})
totalUsersCount = int(searchNode["userCount"].(float64))
edgeNodes := searchNode["edges"].([]interface{})
if len(edgeNodes) == 0 {
break Pages
}
totalCount += len(edgeNodes)
Edges:
for _, edge := range edgeNodes {
edgeNode := edge.(map[string]interface{})
userNode := edgeNode["node"].(map[string]interface{})
typename := userNode["__typename"].(string)
if typename != "User" {
continue Edges
}
login := userNode["login"].(string)
avatarURL := userNode["avatarUrl"].(string)
name := strPropOrEmpty(userNode, "name")
company := strPropOrEmpty(userNode, "company")
organizations := []string{}
orgNodes := userNode["organizations"].(map[string]interface{})["nodes"].([]interface{})
for _, orgNode := range orgNodes {
organizations = append(organizations, orgNode.(map[string]interface{})["login"].(string))
}
followerCount := int(userNode["followers"].(map[string]interface{})["totalCount"].(float64))
contributionsCollection := userNode["contributionsCollection"].(map[string]interface{})
contributionCount := int(contributionsCollection["contributionCalendar"].(map[string]interface{})["totalContributions"].(float64))
privateContributionCount := int(contributionsCollection["restrictedContributionsCount"].(float64))
commitsCount := int(contributionsCollection["totalCommitContributions"].(float64))
pullRequestsCount := int(contributionsCollection["totalPullRequestContributions"].(float64))
user := User{
Login: login,
AvatarURL: avatarURL,
Name: name,
Company: company,
Organizations: organizations,
FollowerCount: followerCount,
ContributionCount: contributionCount,
PublicContributionCount: (contributionCount - privateContributionCount),
PrivateContributionCount: privateContributionCount,
CommitsCount: commitsCount,
PullRequestsCount: pullRequestsCount}
if !userLogins[login] {
userLogins[login] = true
users = append(users, user)
}
previousCursor = edgeNode["cursor"].(string)
minFollowerCount = int(followerCount)
}
}
}
return GithubSearchResults{
Users: users,
MinimumFollowerCount: minFollowerCount,
TotalUserCount: totalUsersCount}, nil
}
func strPropOrEmpty(obj map[string]interface{}, prop string) string {
switch t := obj[prop].(type) {
case string:
return t
default:
return ""
}
}
func (client HTTPGithubClient) Organizations(login string) ([]string, error) {
url := fmt.Sprintf("https://api.github.com/users/%s/orgs", login)
body, err := client.Request(url, "")
if err != nil {
log.Fatalf("error requesting organizations for user %+v", login)
return []string{}, err
}
orgResp := []OrgResponse{}
err = json.Unmarshal(body, &orgResp)
if err != nil {
log.Fatalf("error parsing organizations JSON for user %+v", login)
return []string{}, err
}
orgs := []string{}
for _, org := range orgResp {
orgs = append(orgs, org.Organization)
}
return orgs, err
}
type OrgResponse struct {
Organization string `json:"login"`
}
func NewGithubClient(wrappers ...net.Wrapper) HTTPGithubClient {
return HTTPGithubClient{wrappers: wrappers}
}
type User struct {
Login string
AvatarURL string
Name string
Company string
Organizations []string
FollowerCount int
ContributionCount int
PublicContributionCount int
PrivateContributionCount int
CommitsCount int
PullRequestsCount int
}
type UserSearchQuery struct {
Q string
Sort string
Order string
MaxUsers int
}
type GithubSearchResults struct {
Users []User
MinimumFollowerCount int
TotalUserCount int
}