├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.yml └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── LICENSE ├── Readme.md ├── cmd └── sponsors-api │ └── main.go ├── go.mod ├── go.sum └── server.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: tj -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.yml: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | * [ ] I searched to see if the issue already exists. 4 | 5 | ## Description 6 | 7 | Describe the bug or feature. 8 | 9 | ## Steps to Reproduce 10 | 11 | Describe the steps required to reproduce the issue if applicable. 12 | 13 | ## Slack 14 | 15 | Join us on Slack https://chat.apex.sh/ 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Open an issue and discuss changes before spending time on them, unless the change is trivial or an issue already exists. 2 | 3 | Use the commit message format: `VERB some thing here. Closes #n`, where VERB is one of: 4 | 5 | - add 6 | - remove 7 | - change 8 | - refactor 9 | 10 | If the change is documentation related prefix with "docs: ", as these are filtered from the changelog, for example: 11 | 12 | docs: add ~/.aws/config 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | requests.md 3 | up.json 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019 TJ Holowaychuk tj@tjholowaychuk.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Sponsors API 2 | 3 | Sponsors API is a [GitHub Sponsors](https://github.com/sponsors) server for displaying sponsor avatars in your project Readme. It looks like this: 4 | 5 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/0) 6 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/1) 7 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/2) 8 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/3) 9 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/4) 10 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/5) 11 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/6) 12 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/7) 13 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/8) 14 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/9) 15 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/10) 16 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/11) 17 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/12) 18 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/13) 19 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/14) 20 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/15) 21 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/16) 22 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/17) 23 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/18) 24 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/19) 25 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/20) 26 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/21) 27 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/22) 28 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/23) 29 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/24) 30 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/25) 31 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/26) 32 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/27) 33 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/28) 34 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/29) 35 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/30) 36 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/31) 37 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/32) 38 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/33) 39 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/34) 40 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/35) 41 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/36) 42 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/37) 43 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/38) 44 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/39) 45 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/40) 46 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/41) 47 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/42) 48 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/43) 49 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/44) 50 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/45) 51 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/46) 52 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/47) 53 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/48) 54 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/49) 55 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/50) 56 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/51) 57 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/52) 58 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/53) 59 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/54) 60 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/55) 61 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/56) 62 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/57) 63 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/58) 64 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/59) 65 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/60) 66 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/61) 67 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/62) 68 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/63) 69 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/64) 70 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/65) 71 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/66) 72 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/67) 73 | [](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/68) 74 | 75 | ## Deploying 76 | 77 | Deploy to host which supports HTTP, for example using [Apex Up](https://github.com/apex/up) or Heroku. The following environment variables are supported: 78 | 79 | - `GITHUB_TOKEN` the GitHub API token (no scopes are required) 80 | - `PORT` the server port (defaults to 3000) 81 | - `URL` the url to your endpoint such as "https://sponsors.myhost.com" (optional) 82 | 83 | ## Usage 84 | 85 | Visit the `/sponsor/markdown` path for the markdown to copy/paste. Sponsors are cached for an hour by default, tweak with the `-cache-ttl` flag. 86 | -------------------------------------------------------------------------------- /cmd/sponsors-api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "log" 7 | "net/http" 8 | "os" 9 | "time" 10 | 11 | "github.com/shurcooL/githubv4" 12 | "github.com/tj/go/env" 13 | "golang.org/x/oauth2" 14 | 15 | sponsors "github.com/tj/sponsors-api" 16 | ) 17 | 18 | func main() { 19 | cacheTTL := flag.String("cache-ttl", "1h", "Sponsor cache duration") 20 | flag.Parse() 21 | 22 | src := oauth2.StaticTokenSource( 23 | &oauth2.Token{AccessToken: os.Getenv("GITHUB_TOKEN")}, 24 | ) 25 | httpClient := oauth2.NewClient(context.Background(), src) 26 | client := githubv4.NewClient(httpClient) 27 | 28 | ttl, err := time.ParseDuration(*cacheTTL) 29 | if err != nil { 30 | log.Fatalf("error parsing cache ttl: %s", err) 31 | } 32 | 33 | s := &sponsors.Server{ 34 | URL: env.GetDefault("URL", "http://localhost:3000"), 35 | CacheTTL: ttl, 36 | Client: client, 37 | } 38 | 39 | addr := "0.0.0.0:" + env.GetDefault("PORT", "3000") 40 | log.Printf("Listening on %s", addr) 41 | log.Fatal(http.ListenAndServe(addr, s)) 42 | } 43 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tj/sponsors-api 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/shurcooL/githubv4 v0.0.0-20200414012201-bbc966b061dd 7 | github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect 8 | github.com/tj/go v1.8.6 9 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f // indirect 10 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 3 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 4 | github.com/shurcooL/githubv4 v0.0.0-20200414012201-bbc966b061dd h1:EwtC+kDj8s9OKiaStPZtTv3neldOyr98AXIxvmn3Gss= 5 | github.com/shurcooL/githubv4 v0.0.0-20200414012201-bbc966b061dd/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= 6 | github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk= 7 | github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= 8 | github.com/tj/go v1.8.6 h1:HZ+XV+wB4vqN5y5VLoZqYUuUJTBF+2kblBru7aUa44E= 9 | github.com/tj/go v1.8.6/go.mod h1:iDIwBG1ZkyeGIOBZLZQfpIztHr5m0gG+YGXrKaUC4yE= 10 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 11 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 12 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= 13 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 14 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f h1:QBjCr1Fz5kw158VqdE9JfI9cJnl/ymnJWAdMuinqL7Y= 15 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 16 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= 17 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 18 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= 19 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 20 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 21 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 22 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 23 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 24 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 25 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | // Package sponsors provides GitHub sponsors management. 2 | package sponsors 3 | 4 | import ( 5 | "bytes" 6 | "context" 7 | "fmt" 8 | "image" 9 | "image/color" 10 | "image/draw" 11 | "image/png" 12 | "io" 13 | "log" 14 | "net/http" 15 | "strconv" 16 | "strings" 17 | "sync" 18 | "time" 19 | 20 | "github.com/shurcooL/githubv4" 21 | ) 22 | 23 | // pixel is a png used for missing avatars. 24 | var pixel []byte 25 | 26 | // initialize gray pixel for missing avatar responses. 27 | func init() { 28 | var buf bytes.Buffer 29 | r := image.Rect(0, 0, 1, 1) 30 | img := image.NewRGBA(r) 31 | c := color.RGBA{0xF8, 0xF9, 0xFA, 0xFF} 32 | draw.Draw(img, r, &image.Uniform{c}, image.ZP, draw.Src) 33 | png.Encode(&buf, img) 34 | pixel = buf.Bytes() 35 | } 36 | 37 | // Sponsor model. 38 | type Sponsor struct { 39 | // Name of the sponsor. 40 | Name string 41 | 42 | // Login name of the sponsor. 43 | Login string 44 | 45 | // AvatarURL of the sponsor. 46 | AvatarURL string 47 | } 48 | 49 | // Server manager. 50 | type Server struct { 51 | // URL is the url of the server. 52 | URL string 53 | 54 | // Client is the github client. 55 | Client *githubv4.Client 56 | 57 | // CacheTTL is the duration until the cache expires. 58 | CacheTTL time.Duration 59 | 60 | // cache 61 | mu sync.Mutex 62 | cacheTimestamp time.Time 63 | cache []Sponsor 64 | } 65 | 66 | // ServeHTTP implementation. 67 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 68 | ctx := r.Context() 69 | path := r.URL.Path 70 | 71 | // logging 72 | start := time.Now() 73 | log.Printf("%s %s", r.Method, path) 74 | defer func() { 75 | log.Printf("%s %s -> %s", r.Method, path, time.Since(start)) 76 | }() 77 | 78 | // prime cache 79 | err := s.primeCache(ctx) 80 | if err != nil { 81 | log.Printf("error priming cache: %s", err) 82 | http.Error(w, "Error fetching sponsors", http.StatusInternalServerError) 83 | return 84 | } 85 | 86 | // routing 87 | switch { 88 | case strings.HasPrefix(path, "/sponsor/markdown"): 89 | s.serveMarkdown(w, r) 90 | case strings.HasPrefix(path, "/sponsor/avatar"): 91 | s.serveAvatar(w, r) 92 | case strings.HasPrefix(path, "/sponsor/profile"): 93 | s.serveProfile(w, r) 94 | default: 95 | http.Error(w, "Not Found", http.StatusNotImplemented) 96 | } 97 | } 98 | 99 | // serveMarkdown serves a list of markdown links which you can copy/paste into your Readme. 100 | func (s *Server) serveMarkdown(w http.ResponseWriter, r *http.Request) { 101 | w.Header().Set("Content-Type", "text/markdown") 102 | for i := 0; i < 100; i++ { 103 | fmt.Fprintf(w, `[](%s/sponsor/profile/%d)`, s.URL, i, s.URL, i) 104 | fmt.Fprintf(w, "\n") 105 | } 106 | } 107 | 108 | // serveAvatar redirects to a sponsor's avatar image. 109 | func (s *Server) serveAvatar(w http.ResponseWriter, r *http.Request) { 110 | // /sponsor/avatar/{index} 111 | index := strings.Replace(r.URL.Path, "/sponsor/avatar/", "", 1) 112 | n, err := strconv.Atoi(index) 113 | if err != nil { 114 | log.Printf("error parsing index: %s", err) 115 | http.Error(w, "Sponsor index must be a number", http.StatusBadRequest) 116 | return 117 | } 118 | 119 | // check index bounds 120 | if n > len(s.cache)-1 { 121 | w.Header().Set("Content-Type", "image/png") 122 | io.Copy(w, bytes.NewReader(pixel)) 123 | return 124 | } 125 | 126 | // redirect to avatar 127 | sponsor := s.cache[n] 128 | w.Header().Set("Location", sponsor.AvatarURL) 129 | w.WriteHeader(http.StatusTemporaryRedirect) 130 | fmt.Fprintf(w, "Redirecting to %s", sponsor.AvatarURL) 131 | } 132 | 133 | // serveProfile redirects to a sponsor's profile. 134 | func (s *Server) serveProfile(w http.ResponseWriter, r *http.Request) { 135 | // /sponsor/profile/{index} 136 | index := strings.Replace(r.URL.Path, "/sponsor/profile/", "", 1) 137 | n, err := strconv.Atoi(index) 138 | if err != nil { 139 | log.Printf("error parsing index: %s", err) 140 | http.Error(w, "Sponsor index must be a number", http.StatusBadRequest) 141 | return 142 | } 143 | 144 | // check index bounds 145 | if n > len(s.cache)-1 { 146 | http.Error(w, "Not found", http.StatusNotFound) 147 | return 148 | } 149 | 150 | // redirect to profile 151 | sponsor := s.cache[n] 152 | url := fmt.Sprintf("https://github.com/%s", sponsor.Login) 153 | w.Header().Set("Location", url) 154 | w.WriteHeader(http.StatusTemporaryRedirect) 155 | fmt.Fprintf(w, "Redirecting to %s", url) 156 | } 157 | 158 | // primeCache implementation. 159 | func (s *Server) primeCache(ctx context.Context) error { 160 | s.mu.Lock() 161 | defer s.mu.Unlock() 162 | 163 | // check ttl 164 | if time.Since(s.cacheTimestamp) <= s.CacheTTL { 165 | return nil 166 | } 167 | 168 | // fetch 169 | log.Printf("cache miss, fetching sponsors") 170 | sponsors, err := s.getSponsors(ctx) 171 | if err != nil { 172 | return err 173 | } 174 | 175 | s.cache = sponsors 176 | s.cacheTimestamp = time.Now() 177 | return nil 178 | } 179 | 180 | // getSponsors implementation. 181 | func (s *Server) getSponsors(ctx context.Context) ([]Sponsor, error) { 182 | var sponsors []Sponsor 183 | var q sponsorships 184 | var cursor string 185 | 186 | for { 187 | err := s.Client.Query(ctx, &q, map[string]interface{}{ 188 | "cursor": githubv4.String(cursor), 189 | }) 190 | 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | for _, edge := range q.Viewer.SponsorshipsAsMaintainer.Edges { 196 | sponsor := edge.Node.Sponsor 197 | sponsors = append(sponsors, sponsor) 198 | } 199 | 200 | if !q.Viewer.SponsorshipsAsMaintainer.PageInfo.HasNextPage { 201 | break 202 | } 203 | 204 | cursor = q.Viewer.SponsorshipsAsMaintainer.PageInfo.EndCursor 205 | } 206 | 207 | return sponsors, nil 208 | } 209 | 210 | // sponsorships query. 211 | type sponsorships struct { 212 | Viewer struct { 213 | Login string 214 | SponsorshipsAsMaintainer struct { 215 | PageInfo struct { 216 | EndCursor string 217 | HasNextPage bool 218 | } 219 | 220 | Edges []struct { 221 | Node struct { 222 | Sponsor Sponsor 223 | } 224 | Cursor string 225 | } 226 | } `graphql:"sponsorshipsAsMaintainer(first: 100, after: $cursor)"` 227 | } 228 | } 229 | --------------------------------------------------------------------------------