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  }