package github import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" ) var rateLimitError = fmt.Errorf("github rate limit exceeded. You can export GH_TOKEN=token for higher limits") // now the nice thing is that I don't NEED to define a model always, // go can be "loose" if you want it to be // THis is unlike the geodata package where I made a model and all... // Uses the user's public github events api, filters for push events and returns // an array of .patch commits // Eg https://api.github.com/users/xyz/events/public to // https://api.github.com/repos/USER/REPO/commits/HASH func getPushPatches(user string) ([]string, error) { url := fmt.Sprintf("https://api.github.com/users/%s/events/public", user) // another neat way is that you don't NEED a Client // it's good to have it for production for doing timeouts etc though // this uses the global shared default client // update: this comment above was meant for an older http.Get(url) based commit req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } addAuthHeaderIfTokenExists(req) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusTooManyRequests { if resp.Header.Get("X-RateLimit-Remaining") == "0" { return nil, rateLimitError } } if resp.StatusCode != 200 { return nil, fmt.Errorf("github api failed") } var apiResult []map[string]any // do YOU know diff b/w Unmarshal and NewDecoder? if err := json.NewDecoder(resp.Body).Decode(&apiResult); err != nil { return nil, fmt.Errorf("failed to decode github response") } // in place filter end := 0 for _, entry := range apiResult { if entry["type"].(string) == "PushEvent" { apiResult[end] = entry // THESE are NOT copies, do YOU know why? end++ } } // build patch urls // do YOU know the diff b/w make vs declaring it using var var patches []string for i := 0; i < end; i++ { repoPrefix := apiResult[i]["repo"].(map[string]any)["url"].(string) commitHash := apiResult[i]["payload"].(map[string]any)["head"].(string) patchUrl := fmt.Sprintf("%s/commits/%s", repoPrefix, commitHash) patches = append(patches, patchUrl) } return patches, nil } func getUniqueTzFromPatches(patchUrls []string) ([]string, error) { var tzs []string for _, patchUrl := range patchUrls { req, err := http.NewRequest("GET", patchUrl, nil) if err != nil { return nil, fmt.Errorf("failed to build req") } // Refer: https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#get-a-commit // this is how YOU do custom stuff req.Header.Set("Accept", "application/vnd.github.patch") addAuthHeaderIfTokenExists(req) // can potentially run for INIFINITY as no timeout resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to get patch") } defer resp.Body.Close() if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusTooManyRequests { if resp.Header.Get("X-RateLimit-Remaining") == "0" { return nil, rateLimitError } } if resp.StatusCode != 200 { // the DOCS say that some larges ones may time out // and they DID continue } // ^ THis is ugly body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read dpatch body") } // the date is the 3rd line lines := strings.Split(string(body), "\n") dateLine := lines[2] if !strings.HasPrefix(dateLine, "Date: ") { // my invariant failed and the world is ending panic("invariant of date on 3rd line failed") } // split by space and pick last spaceSplits := strings.Split(dateLine, " ") if len(spaceSplits) == 0 { panic("invariant of dateline having spaces failed") } tz := spaceSplits[len(spaceSplits)-1] tzs = append(tzs, tz) } // uniq it now // go seems to be getting a little TOO verbose for my taste now seen := make(map[string]int) end := 0 for _, tz := range tzs { // the first is the value which is 0 if missing, second is the exists if _, exists := seen[tz]; !exists { seen[tz] = 1 tzs[end] = tz end++ } } return tzs[:end], nil } func addAuthHeaderIfTokenExists(req *http.Request) { token := os.Getenv("GH_TOKEN") if token != "" { req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) } } func GetTzsForUser(username string) ([]string, error) { patches, err := getPushPatches(username) if err != nil { return nil, err } tzs, err := getUniqueTzFromPatches(patches) if err != nil { return nil, err } return tzs, nil }