diff --git a/app/exit_message.go b/app/exit_message.go new file mode 100644 index 00000000..4a26c002 --- /dev/null +++ b/app/exit_message.go @@ -0,0 +1,110 @@ +package app + +import ( + "fmt" + "os" + "strings" + + "github.com/logrusorgru/aurora" + "github.com/olebedev/config" + "github.com/wtfutil/wtf/support" +) + +const exitMessageHeader = ` + ____ __ ____ .___________. _______ + \ \ / \ / / | || ____| + \ \/ \/ / ----| |-----| |__ + \ / | | | __| + \ /\ / | | | | + \__/ \__/ |__| |__| + + the personal information dashboard for your terminal +` + +// DisplayExitMessage displays the onscreen exit message when the app quits +func (wtfApp *WtfApp) DisplayExitMessage() { + githubAPIKey := readGitHubAPIKey(wtfApp.config) + ghUser := support.NewGitHubUser(githubAPIKey) + + exitMessageIsDisplayable := readDisplayableConfig(wtfApp.config) + + wtfApp.displayExitMsg(ghUser, exitMessageIsDisplayable) +} + +/* -------------------- Unexported Functions -------------------- */ + +func (wtfApp *WtfApp) displayExitMsg(ghUser *support.GitHubUser, exitMessageIsDisplayable bool) string { + _ = ghUser.Load() + + // If a sponsor or contributor and opt out of seeing the exit message, do not display it + if (ghUser.IsContributor || ghUser.IsSponsor) && !exitMessageIsDisplayable { + return "" + } + + msgs := []string{} + + msgs = append(msgs, aurora.Magenta(exitMessageHeader).String()) + + if ghUser.IsContributor { + msgs = append(msgs, wtfApp.contributorThankYouMessage()) + } + + if ghUser.IsSponsor { + msgs = append(msgs, wtfApp.sponsorThankYouMessage()) + } + + if !ghUser.IsContributor && !ghUser.IsSponsor { + msgs = append(msgs, wtfApp.supportRequestMessage()) + } + + displayMsg := strings.Join(msgs, "\n") + + fmt.Println(displayMsg) + + return displayMsg +} + +// readDisplayableConfig figures out whether or not the exit message should be displayed +// per the user's wishes. It allows contributors and sponsors to opt out of the exit message +func readDisplayableConfig(cfg *config.Config) bool { + displayExitMsg := cfg.UBool("wtf.exitMessage.display", true) + return displayExitMsg +} + +// readGitHubAPIKey attempts to find a GitHub API key somewhere in the configuration file +func readGitHubAPIKey(cfg *config.Config) string { + apiKey := cfg.UString("wtf.exitMessage.githubAPIKey", os.Getenv("WTF_GITHUB_TOKEN")) + if apiKey != "" { + return apiKey + } + + moduleConfig, err := cfg.Get("wtf.mods.github") + if err != nil { + return "" + } + + return moduleConfig.UString("apiKey", "") +} + +/* -------------------- Messaging -------------------- */ + +func (wtfApp *WtfApp) contributorThankYouMessage() string { + str := " On behalf of all the users of WTF, thank you for contributing to the source code." + str += fmt.Sprintf(" %s", aurora.Green("You rock.")) + + return str +} + +func (wtfApp *WtfApp) sponsorThankYouMessage() string { + str := " Your sponsorship of WTF makes a difference. Thank you for sponsoring and supporting WTF." + str += fmt.Sprintf(" %s", aurora.Green("You're awesome.")) + + return str +} + +func (wtfApp *WtfApp) supportRequestMessage() string { + str := " The development and maintenance of WTF is supported by sponsorships.\n" + str += fmt.Sprintf(" Please consider sponsoring WTF at %s\n", aurora.Green("https://github.com/sponsors/senorprogrammer")) + + return str +} diff --git a/app/exit_message_test.go b/app/exit_message_test.go new file mode 100644 index 00000000..06dcdfad --- /dev/null +++ b/app/exit_message_test.go @@ -0,0 +1,71 @@ +package app + +import ( + "strings" + "testing" + + "github.com/wtfutil/wtf/support" + "gotest.tools/assert" +) + +func Test_displayExitMessage(t *testing.T) { + tests := []struct { + name string + isDisplayable bool + isContributor bool + isSponsor bool + compareWith string + expected string + }{ + { + name: "when not displayable", + isDisplayable: false, + isContributor: true, + isSponsor: true, + compareWith: "equals", + expected: "", + }, + { + name: "when contributor", + isDisplayable: true, + isContributor: true, + compareWith: "contains", + expected: "thank you for contributing", + }, + { + name: "when sponsor", + isDisplayable: true, + isSponsor: true, + compareWith: "contains", + expected: "Thank you for sponsoring", + }, + { + name: "when user", + isDisplayable: true, + isContributor: false, + isSponsor: false, + compareWith: "contains", + expected: "supported by sponsorships", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + wtfApp := WtfApp{} + ghUser := &support.GitHubUser{ + IsContributor: tt.isContributor, + IsSponsor: tt.isSponsor, + } + + actual := wtfApp.displayExitMsg(ghUser, tt.isDisplayable) + + if tt.compareWith == "equals" { + assert.Equal(t, actual, tt.expected) + } + + if tt.compareWith == "contains" { + assert.Equal(t, true, strings.Contains(actual, tt.expected)) + } + }) + } +} diff --git a/app/wtf_app.go b/app/wtf_app.go index 5bb40a74..9ff8e910 100644 --- a/app/wtf_app.go +++ b/app/wtf_app.go @@ -87,6 +87,10 @@ func (wtfApp *WtfApp) stopAllWidgets() { func (wtfApp *WtfApp) keyboardIntercept(event *tcell.EventKey) *tcell.EventKey { // These keys are global keys used by the app. Widgets should not implement these keys switch event.Key() { + case tcell.KeyCtrlC: + wtfApp.Stop() + wtfApp.app.Stop() + wtfApp.DisplayExitMessage() case tcell.KeyCtrlR: wtfApp.refreshAllWidgets() return nil @@ -142,8 +146,8 @@ func (wtfApp *WtfApp) watchForConfigChanges() { config := cfg.LoadWtfConfigFile(wtfApp.configFilePath) newApp := NewWtfApp(wtfApp.app, config, wtfApp.configFilePath) - openUrlUtil := utils.ToStrs(config.UList("wtf.openUrlUtil", []interface{}{})) - utils.Init(config.UString("wtf.openFileUtil", "open"), openUrlUtil) + openURLUtil := utils.ToStrs(config.UList("wtf.openUrlUtil", []interface{}{})) + utils.Init(config.UString("wtf.openFileUtil", "open"), openURLUtil) newApp.Start() case err := <-watch.Error: diff --git a/go.mod b/go.mod index 8cfdb8f2..23e1f8a4 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/gdamore/tcell v1.4.0 github.com/go-ole/go-ole v1.2.4 // indirect github.com/godbus/dbus v4.1.0+incompatible // indirect - github.com/google/go-github/v26 v26.1.3 + github.com/google/go-github/v32 v32.1.0 github.com/gophercloud/gophercloud v0.5.0 // indirect github.com/hekmon/cunits v2.0.1+incompatible // indirect github.com/hekmon/transmissionrpc v0.0.0-20190525133028-1d589625bacd @@ -54,6 +54,8 @@ require ( github.com/radovskyb/watcher v1.0.7 github.com/rivo/tview v0.0.0-20200108161608-1316ea7a4b35 github.com/shirou/gopsutil v2.20.9+incompatible + github.com/shurcooL/githubv4 v0.0.0-20200802174311-f27d2ca7f6d5 + github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect github.com/spf13/cobra v0.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.6.1 // indirect @@ -65,6 +67,7 @@ require ( github.com/zmb3/spotify v0.0.0-20191010212056-e12fb981aacb github.com/zorkian/go-datadog-api v2.29.0+incompatible golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d + golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 golang.org/x/text v0.3.3 google.golang.org/api v0.30.0 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect diff --git a/go.sum b/go.sum index c1ec6c28..1eecbb9f 100644 --- a/go.sum +++ b/go.sum @@ -316,8 +316,8 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-github/v26 v26.1.3 h1:n03e8IGgLdD78L+ETWxvqpBIBWEZLlTBCQVU2yImw1o= -github.com/google/go-github/v26 v26.1.3/go.mod h1:v6/FmX9au22j4CtYxnMhJJkP+JfOQDXALk7hI+MPDNM= +github.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II= +github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= @@ -432,7 +432,6 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -476,7 +475,6 @@ github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b h1:PMbSa9CgaiQR9NLlUTwKi+7aeLl3GG5JX5ERJxfQ3IE= github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4= github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= @@ -676,6 +674,10 @@ github.com/sguiheux/go-coverage v0.0.0-20190710153556-287b082a7197/go.mod h1:0hh github.com/shirou/gopsutil v0.0.0-20170406131756-e49a95f3d5f8/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/gopsutil v2.20.9+incompatible h1:msXs2frUV+O/JLva9EDLpuJ84PrFsdCTCQex8PUdtkQ= github.com/shirou/gopsutil v2.20.9+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shurcooL/githubv4 v0.0.0-20200802174311-f27d2ca7f6d5 h1:CA6Mjshr+g5YHENwllpQNR0UaYO7VGKo6TzJLM64WJQ= +github.com/shurcooL/githubv4 v0.0.0-20200802174311-f27d2ca7f6d5/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= +github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk= +github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -773,7 +775,6 @@ go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -791,7 +792,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -824,7 +824,6 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -855,7 +854,6 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= @@ -877,7 +875,6 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -917,7 +914,6 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY= golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -999,7 +995,6 @@ google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/ google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0 h1:BaiDisFir8O4IJxvAabCGGkQ6yCJegNQqSVoYUNAnbk= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= @@ -1008,7 +1003,6 @@ google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= @@ -1033,7 +1027,6 @@ google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940 h1:MRHtG0U6SnaUb+s+LhNE1qt1FQ1wlhqr5E4usBKC0uA= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= @@ -1053,7 +1046,6 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0 h1:bO/TA4OxCOummhSf10siHuG7vJOiwh7SpRpFZDkOgl4= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= @@ -1129,7 +1121,6 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= jaytaylor.com/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:mub0MmFLOn8XLikZOAhgLD1kXJq8jgftSrrv7m00xFo= diff --git a/main.go b/main.go index 40c65452..8d0090e9 100644 --- a/main.go +++ b/main.go @@ -65,8 +65,8 @@ func main() { defer profile.Start(profile.MemProfile).Stop() } - openUrlUtil := utils.ToStrs(config.UList("wtf.openUrlUtil", []interface{}{})) - utils.Init(config.UString("wtf.openFileUtil", "open"), openUrlUtil) + openURLUtil := utils.ToStrs(config.UList("wtf.openUrlUtil", []interface{}{})) + utils.Init(config.UString("wtf.openFileUtil", "open"), openURLUtil) setTerm(config) diff --git a/modules/github/display.go b/modules/github/display.go index e9145335..ffbf2346 100644 --- a/modules/github/display.go +++ b/modules/github/display.go @@ -3,7 +3,7 @@ package github import ( "fmt" - "github.com/google/go-github/v26/github" + ghb "github.com/google/go-github/v32/github" "golang.org/x/text/language" "golang.org/x/text/message" ) @@ -152,7 +152,7 @@ var mergeIcons = map[string]string{ "blocked": "[red]\u2717[white] ", } -func (widget *Widget) mergeString(pr *github.PullRequest) string { +func (widget *Widget) mergeString(pr *ghb.PullRequest) string { if !widget.settings.enableStatus { return "" } diff --git a/modules/github/github_repo.go b/modules/github/github_repo.go index 1a22d5db..f2eb32a7 100644 --- a/modules/github/github_repo.go +++ b/modules/github/github_repo.go @@ -5,7 +5,7 @@ import ( "fmt" "net/http" - ghb "github.com/google/go-github/v26/github" + ghb "github.com/google/go-github/v32/github" "github.com/wtfutil/wtf/utils" "golang.org/x/oauth2" ) diff --git a/support/github.go b/support/github.go new file mode 100644 index 00000000..842af27e --- /dev/null +++ b/support/github.go @@ -0,0 +1,233 @@ +package support + +import ( + "context" + "errors" + "net/http" + + ghb "github.com/google/go-github/v32/github" + "github.com/shurcooL/githubv4" + "golang.org/x/oauth2" + "golang.org/x/sync/errgroup" +) + +var sponsorQuery struct { + User struct { + SponsorshipsAsSponsor struct { + Nodes []struct { + Sponsorable struct { + SponsorsListing struct { + Slug string + } + } + } + } `graphql:"sponsorshipsAsSponsor(first: 10)"` + } `graphql:"user(login: $loginName)"` +} + +// GitHubUser represents a GitHub user account as defined by a GitHub API access key +// This is used to determine whether or not the WTF user is a sponsor (via GitHub sponsors) +// and/or a contributor to WTF +type GitHubUser struct { + apiKey string + + loginName string + + clientV3 *ghb.Client + clientV4 *githubv4.Client + + IsContributor bool + IsSponsor bool +} + +// NewGitHubUser creates and returns an instance of GitHub user with the boolean fields +// populated +func NewGitHubUser(githubAPIKey string) *GitHubUser { + ghUser := GitHubUser{ + apiKey: githubAPIKey, + + clientV3: nil, + clientV4: nil, + + loginName: "", + + IsContributor: false, + IsSponsor: false, + } + + if ghUser.hasAPIKey() { + // Use the v3 API to get the contributors because this doesn't seem to be supported by the v4 API yet + clientV3, _ := ghUser.authenticateV3() + ghUser.clientV3 = clientV3 + + // Use the v4 API to get sponsors because this doesn't seem to be supported in v3 + clientV4, _ := ghUser.authenticateV4() + ghUser.clientV4 = clientV4 + } + + return &ghUser +} + +/* -------------------- Exported Functions -------------------- */ + +// Load loads the user's data from GitHub +func (ghUser *GitHubUser) Load() error { + err := ghUser.verifyGitHubClients() + if err != nil { + return err + } + + err = ghUser.loadGitHubData() + if err != nil { + return err + } + + return nil +} + +/* -------------------- Unexported Functions -------------------- */ + +func (ghUser *GitHubUser) authenticateV3() (*ghb.Client, error) { + src := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: ghUser.apiKey}, + ) + + oauthClient := oauth2.NewClient(context.Background(), src) + client := ghb.NewClient(oauthClient) + + return client, nil +} + +func (ghUser *GitHubUser) authenticateV4() (*githubv4.Client, error) { + src := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: ghUser.apiKey}, + ) + + oauthClient := oauth2.NewClient(context.Background(), src) + client := githubv4.NewClient(oauthClient) + + return client, nil +} + +// hasAPIKey returns TRUE if the user has put a GitHub API key into their +// configuration and we've managed to find and read it +func (ghUser *GitHubUser) hasAPIKey() bool { + return ghUser.apiKey != "" +} + +func (ghUser *GitHubUser) loadGitHubData() error { + var err error + + login, err := ghUser.loadLoginName() + if err != nil { + return err + } + ghUser.loginName = login + + var isContrib, isSponsor bool + + ctx := context.Background() + g, ctx := errgroup.WithContext(ctx) + + g.Go(func() error { + isContrib, err = ghUser.loadContributorStatus(ctx) + return err + }) + + g.Go(func() error { + isSponsor, err = ghUser.loadSponsorStatus(ctx) + return err + }) + + err = g.Wait() + if err != nil { + return err + } + + ghUser.IsContributor = isContrib + ghUser.IsSponsor = isSponsor + + return nil +} + +// loadLoginName figures out the GitHub user's login name from their API key +func (ghUser *GitHubUser) loadLoginName() (string, error) { + user, _, err := ghUser.clientV3.Users.Get(context.Background(), "") + if err != nil { + return "", err + } + + login := user.GetLogin() + + return login, nil +} + +// loadContributorStatus figures out if this GitHub account has contributed to WTF +func (ghUser *GitHubUser) loadContributorStatus(ctx context.Context) (bool, error) { + page := 1 + isContributor := false + + for { + opts := &ghb.ListContributorsOptions{ + ListOptions: ghb.ListOptions{ + Page: page, + PerPage: 100, + }, + } + + contributors, resp, err := ghUser.clientV3.Repositories.ListContributors(ctx, "wtfutil", "wtf", opts) + if err != nil { + return false, err + } + + if resp.StatusCode != http.StatusOK || len(contributors) < 1 { + break + } + + for _, contrib := range contributors { + if contrib.GetLogin() == ghUser.loginName { + isContributor = true + break + } + } + + page++ + } + + return isContributor, nil +} + +// loadSponsorStatus figures out if this GitHub account has sponsored WTF +func (ghUser *GitHubUser) loadSponsorStatus(ctx context.Context) (bool, error) { + vars := map[string]interface{}{ + "loginName": githubv4.String(ghUser.loginName), + } + + err := ghUser.clientV4.Query(ctx, &sponsorQuery, vars) + if err != nil { + return false, err + } + + isSponsor := false + + for _, spon := range sponsorQuery.User.SponsorshipsAsSponsor.Nodes { + if spon.Sponsorable.SponsorsListing.Slug == "sponsors-senorprogrammer" { + isSponsor = true + break + } + } + + return isSponsor, nil +} + +func (ghUser *GitHubUser) verifyGitHubClients() error { + if ghUser.clientV3 == nil { + return errors.New("github client v3 failed to load") + } + + if ghUser.clientV4 == nil { + return errors.New("github client v4 failed to load") + } + + return nil +} diff --git a/wtf/wtfable.go b/wtf/wtfable.go index cb6fd39a..0930a358 100644 --- a/wtf/wtfable.go +++ b/wtf/wtfable.go @@ -17,8 +17,8 @@ type Wtfable interface { FocusChar() string Focusable() bool HelpText() string - QuitChan() chan bool Name() string + QuitChan() chan bool SetFocusChar(string) TextView() *tview.TextView