github.com/wtfutil/wtf@v0.43.0/support/github.go (about) 1 package support 2 3 import ( 4 "context" 5 "errors" 6 "net/http" 7 8 ghb "github.com/google/go-github/v32/github" 9 "github.com/shurcooL/githubv4" 10 "golang.org/x/oauth2" 11 "golang.org/x/sync/errgroup" 12 ) 13 14 var sponsorQuery struct { 15 User struct { 16 SponsorshipsAsSponsor struct { 17 Nodes []struct { 18 Sponsorable struct { 19 SponsorsListing struct { 20 Slug string 21 } 22 } 23 } 24 } `graphql:"sponsorshipsAsSponsor(first: 10)"` 25 } `graphql:"user(login: $loginName)"` 26 } 27 28 // GitHubUser represents a GitHub user account as defined by a GitHub API access key 29 // This is used to determine whether or not the WTF user is a sponsor (via GitHub sponsors) 30 // and/or a contributor to WTF 31 type GitHubUser struct { 32 apiKey string 33 34 loginName string 35 36 clientV3 *ghb.Client 37 clientV4 *githubv4.Client 38 39 IsContributor bool 40 IsSponsor bool 41 } 42 43 // NewGitHubUser creates and returns an instance of GitHub user with the boolean fields 44 // populated 45 func NewGitHubUser(githubAPIKey string) *GitHubUser { 46 ghUser := GitHubUser{ 47 apiKey: githubAPIKey, 48 49 clientV3: nil, 50 clientV4: nil, 51 52 loginName: "", 53 54 IsContributor: false, 55 IsSponsor: false, 56 } 57 58 if ghUser.hasAPIKey() { 59 // Use the v3 API to get the contributors because this doesn't seem to be supported by the v4 API yet 60 clientV3, _ := ghUser.authenticateV3() 61 ghUser.clientV3 = clientV3 62 63 // Use the v4 API to get sponsors because this doesn't seem to be supported in v3 64 clientV4, _ := ghUser.authenticateV4() 65 ghUser.clientV4 = clientV4 66 } 67 68 return &ghUser 69 } 70 71 /* -------------------- Exported Functions -------------------- */ 72 73 // Load loads the user's data from GitHub 74 func (ghUser *GitHubUser) Load() error { 75 err := ghUser.verifyGitHubClients() 76 if err != nil { 77 return err 78 } 79 80 err = ghUser.loadGitHubData() 81 if err != nil { 82 return err 83 } 84 85 return nil 86 } 87 88 /* -------------------- Unexported Functions -------------------- */ 89 90 func (ghUser *GitHubUser) authenticateV3() (*ghb.Client, error) { 91 src := oauth2.StaticTokenSource( 92 &oauth2.Token{AccessToken: ghUser.apiKey}, 93 ) 94 95 oauthClient := oauth2.NewClient(context.Background(), src) 96 client := ghb.NewClient(oauthClient) 97 98 return client, nil 99 } 100 101 func (ghUser *GitHubUser) authenticateV4() (*githubv4.Client, error) { 102 src := oauth2.StaticTokenSource( 103 &oauth2.Token{AccessToken: ghUser.apiKey}, 104 ) 105 106 oauthClient := oauth2.NewClient(context.Background(), src) 107 client := githubv4.NewClient(oauthClient) 108 109 return client, nil 110 } 111 112 // hasAPIKey returns TRUE if the user has put a GitHub API key into their 113 // configuration and we've managed to find and read it 114 func (ghUser *GitHubUser) hasAPIKey() bool { 115 return ghUser.apiKey != "" 116 } 117 118 func (ghUser *GitHubUser) loadGitHubData() error { 119 var err error 120 121 login, err := ghUser.loadLoginName() 122 if err != nil { 123 return err 124 } 125 ghUser.loginName = login 126 127 var isContrib, isSponsor bool 128 129 ctx := context.Background() 130 g, ctx := errgroup.WithContext(ctx) 131 132 g.Go(func() error { 133 isContrib, err = ghUser.loadContributorStatus(ctx) 134 return err 135 }) 136 137 g.Go(func() error { 138 isSponsor, err = ghUser.loadSponsorStatus(ctx) 139 return err 140 }) 141 142 err = g.Wait() 143 if err != nil { 144 return err 145 } 146 147 ghUser.IsContributor = isContrib 148 ghUser.IsSponsor = isSponsor 149 150 return nil 151 } 152 153 // loadLoginName figures out the GitHub user's login name from their API key 154 func (ghUser *GitHubUser) loadLoginName() (string, error) { 155 user, _, err := ghUser.clientV3.Users.Get(context.Background(), "") 156 if err != nil { 157 return "", err 158 } 159 160 login := user.GetLogin() 161 162 return login, nil 163 } 164 165 // loadContributorStatus figures out if this GitHub account has contributed to WTF 166 func (ghUser *GitHubUser) loadContributorStatus(ctx context.Context) (bool, error) { 167 page := 1 168 isContributor := false 169 170 for { 171 opts := &ghb.ListContributorsOptions{ 172 ListOptions: ghb.ListOptions{ 173 Page: page, 174 PerPage: 100, 175 }, 176 } 177 178 contributors, resp, err := ghUser.clientV3.Repositories.ListContributors(ctx, "wtfutil", "wtf", opts) 179 if err != nil { 180 return false, err 181 } 182 183 if resp.StatusCode != http.StatusOK || len(contributors) < 1 { 184 break 185 } 186 187 for _, contrib := range contributors { 188 if contrib.GetLogin() == ghUser.loginName { 189 isContributor = true 190 break 191 } 192 } 193 194 page++ 195 } 196 197 return isContributor, nil 198 } 199 200 // loadSponsorStatus figures out if this GitHub account has sponsored WTF 201 func (ghUser *GitHubUser) loadSponsorStatus(ctx context.Context) (bool, error) { 202 vars := map[string]interface{}{ 203 "loginName": githubv4.String(ghUser.loginName), 204 } 205 206 err := ghUser.clientV4.Query(ctx, &sponsorQuery, vars) 207 if err != nil { 208 return false, err 209 } 210 211 isSponsor := false 212 213 for _, spon := range sponsorQuery.User.SponsorshipsAsSponsor.Nodes { 214 if spon.Sponsorable.SponsorsListing.Slug == "sponsors-senorprogrammer" { 215 isSponsor = true 216 break 217 } 218 } 219 220 return isSponsor, nil 221 } 222 223 func (ghUser *GitHubUser) verifyGitHubClients() error { 224 if ghUser.clientV3 == nil { 225 return errors.New("github client v3 failed to load") 226 } 227 228 if ghUser.clientV4 == nil { 229 return errors.New("github client v4 failed to load") 230 } 231 232 return nil 233 }