├── .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 |
--------------------------------------------------------------------------------