From f2b943e3d6178ab0d9613b226c6ef5363119b39a Mon Sep 17 00:00:00 2001 From: Andrew_Zol Date: Fri, 8 Jun 2018 20:27:39 +0300 Subject: [PATCH 1/3] Added gspreadsheets module --- .gitignore | 1 + gspreadsheets/client.go | 145 ++++++++++++++++++++++++++++++++++++++++ gspreadsheets/widget.go | 56 ++++++++++++++++ wtf.go | 2 + 4 files changed, 204 insertions(+) create mode 100644 gspreadsheets/client.go create mode 100644 gspreadsheets/widget.go diff --git a/.gitignore b/.gitignore index 7048d6ec..2d038a3c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ # Misc .DS_Store gcal/client_secret.json +gspreadsheets/client_secret.json #intellij idea .idea/ diff --git a/gspreadsheets/client.go b/gspreadsheets/client.go new file mode 100644 index 00000000..1cf16321 --- /dev/null +++ b/gspreadsheets/client.go @@ -0,0 +1,145 @@ +/* +* This butt-ugly code is direct from Google itself +* https://developers.google.com/sheets/api/quickstart/go + */ + +package gspreadsheets + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "os/user" + "path/filepath" + "strings" + + "github.com/senorprogrammer/wtf/wtf" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + sheets "google.golang.org/api/sheets/v4" +) + +/* -------------------- Exported Functions -------------------- */ + +func Fetch() ([]*sheets.ValueRange, error) { + ctx := context.Background() + + secretPath, _ := wtf.ExpandHomeDir(Config.UString("wtf.mods.gspreadsheets.secretFile")) + + b, err := ioutil.ReadFile(secretPath) + if err != nil { + log.Fatalf("Unable to read secretPath. %v", err) + return nil, err + } + + config, err := google.ConfigFromJSON(b, "https://www.googleapis.com/auth/spreadsheets.readonly") + + if err != nil { + log.Fatalf("Unable to get config from JSON. %v", err) + return nil, err + } + client := getClient(ctx, config) + + srv, err := sheets.New(client) + if err != nil { + log.Fatalf("Unable to get create server. %v", err) + return nil, err + } + + cells := wtf.ToStrs(Config.UList("wtf.mods.gspreadsheets.cells.addresses")) + documentId := Config.UString("wtf.mods.gspreadsheets.sheetId") + addresses := strings.Join(cells[:], ";") + + responses := make([]*sheets.ValueRange, len(cells)) + + for i := 0; i < len(cells); i++ { + resp, err := srv.Spreadsheets.Values.Get(documentId, cells[i]).Do() + if err != nil { + log.Fatalf("Error fetching cells %s", addresses) + return nil, err + } + responses[i] = resp + } + + return responses, err +} + +/* -------------------- Unexported Functions -------------------- */ + +// getClient uses a Context and Config to retrieve a Token +// then generate a Client. It returns the generated Client. +func getClient(ctx context.Context, config *oauth2.Config) *http.Client { + cacheFile, err := tokenCacheFile() + if err != nil { + log.Fatalf("Unable to get path to cached credential file. %v", err) + } + tok, err := tokenFromFile(cacheFile) + if err != nil { + tok = getTokenFromWeb(config) + saveToken(cacheFile, tok) + } + return config.Client(ctx, tok) +} + +// getTokenFromWeb uses Config to request a Token. +// It returns the retrieved Token. +func getTokenFromWeb(config *oauth2.Config) *oauth2.Token { + authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) + fmt.Printf("Go to the following link in your browser then type the "+ + "authorization code: \n%v\n", authURL) + + var code string + if _, err := fmt.Scan(&code); err != nil { + log.Fatalf("Unable to read authorization code %v", err) + } + + tok, err := config.Exchange(oauth2.NoContext, code) + if err != nil { + log.Fatalf("Unable to retrieve token from web %v", err) + } + return tok +} + +// tokenCacheFile generates credential file path/filename. +// It returns the generated credential path/filename. +func tokenCacheFile() (string, error) { + usr, err := user.Current() + if err != nil { + return "", err + } + tokenCacheDir := filepath.Join(usr.HomeDir, ".credentials") + os.MkdirAll(tokenCacheDir, 0700) + return filepath.Join(tokenCacheDir, + url.QueryEscape("spreadsheets-go-quickstart.json")), err +} + +// tokenFromFile retrieves a Token from a given file path. +// It returns the retrieved Token and any read error encountered. +func tokenFromFile(file string) (*oauth2.Token, error) { + f, err := os.Open(file) + if err != nil { + return nil, err + } + t := &oauth2.Token{} + err = json.NewDecoder(f).Decode(t) + defer f.Close() + return t, err +} + +// saveToken uses a file path to create a file and store the +// token in it. +func saveToken(file string, token *oauth2.Token) { + fmt.Printf("Saving credential file to: %s\n", file) + f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + log.Fatalf("Unable to cache oauth token: %v", err) + } + defer f.Close() + + json.NewEncoder(f).Encode(token) +} diff --git a/gspreadsheets/widget.go b/gspreadsheets/widget.go new file mode 100644 index 00000000..e754ce74 --- /dev/null +++ b/gspreadsheets/widget.go @@ -0,0 +1,56 @@ +package gspreadsheets + +import ( + "fmt" + + "github.com/senorprogrammer/wtf/wtf" + "github.com/olebedev/config" + sheets "google.golang.org/api/sheets/v4" +) + +// Config is a pointer to the global config object +var Config *config.Config + +type Widget struct { + wtf.TextWidget +} + +func NewWidget() *Widget { + widget := Widget{ + TextWidget: wtf.NewTextWidget(" Google Spreadsheets ", "gspreadsheets", false), + } + + return &widget +} + +/* -------------------- Exported Functions -------------------- */ + +func (widget *Widget) Refresh() { + if widget.Disabled() { + return + } + + cells, _ := Fetch() + + widget.UpdateRefreshedAt() + + widget.View.SetText(fmt.Sprintf("%s", widget.contentFrom(cells))) +} + +/* -------------------- Unexported Functions -------------------- */ + +func (widget *Widget) contentFrom(valueRanges []*sheets.ValueRange) string { + if valueRanges == nil { + return "error 1" + } + + valuesColor := Config.UString("wtf.mods.gspreadsheets.colors.values", "green") + res := "" + + cells := wtf.ToStrs(Config.UList("wtf.mods.gspreadsheets.cells.names")) + for i := 0; i < len(valueRanges); i++ { + res = res + fmt.Sprintf("%s\t[%s]%s\n", cells[i], valuesColor, valueRanges[i].Values[0][0]) + } + + return res +} diff --git a/wtf.go b/wtf.go index b9627490..baa24779 100644 --- a/wtf.go +++ b/wtf.go @@ -16,6 +16,7 @@ import ( "github.com/senorprogrammer/wtf/cryptoexchanges/bittrex" "github.com/senorprogrammer/wtf/cryptoexchanges/cryptolive" "github.com/senorprogrammer/wtf/gcal" + "github.com/senorprogrammer/wtf/gspreadsheets" "github.com/senorprogrammer/wtf/git" "github.com/senorprogrammer/wtf/github" "github.com/senorprogrammer/wtf/help" @@ -220,6 +221,7 @@ func makeWidgets(app *tview.Application, pages *tview.Pages) { cmdrunner.Config = Config cryptolive.Config = Config gcal.Config = Config + gspreadsheets.Config = Config git.Config = Config github.Config = Config ipinfo.Config = Config From 0ae1061ced6682b84b7552c42a29d91eb3e7ee54 Mon Sep 17 00:00:00 2001 From: Andrew_Zol Date: Fri, 8 Jun 2018 20:31:44 +0300 Subject: [PATCH 2/3] Added updated config. --- _sample_configs/complex_config.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/_sample_configs/complex_config.yml b/_sample_configs/complex_config.yml index c21bdb72..e07aa209 100644 --- a/_sample_configs/complex_config.yml +++ b/_sample_configs/complex_config.yml @@ -79,6 +79,25 @@ wtf: refreshInterval: 300 secretFile: "~/.wtf/gcal/client_secret.json" withLocation: true + gspreadsheets: + enabled: true + secretFile: "~/.wtf/gspreadsheets/client_secret.json" + refreshInterval: "300" + sheetId: "id_of_google_spreadsheet" + colors: + values: "green" + cells: + names: + - "Cell 1 name" + - "Cell 2 name" + addresses: + - "A1" + - "A2" + position: + top: 0 + left: 0 + width: 1 + height: 1 git: commitCount: 5 enabled: true From e319ab69e78ded09aeb7635247f18b3fd214b105 Mon Sep 17 00:00:00 2001 From: Chris Cummer Date: Fri, 8 Jun 2018 15:57:22 -0700 Subject: [PATCH 3/3] Make Google Spreadsheet widget work with new module lazy-loading --- gspreadsheets/widget.go | 6 +----- wtf.go | 6 ++++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/gspreadsheets/widget.go b/gspreadsheets/widget.go index e754ce74..680869cf 100644 --- a/gspreadsheets/widget.go +++ b/gspreadsheets/widget.go @@ -3,8 +3,8 @@ package gspreadsheets import ( "fmt" - "github.com/senorprogrammer/wtf/wtf" "github.com/olebedev/config" + "github.com/senorprogrammer/wtf/wtf" sheets "google.golang.org/api/sheets/v4" ) @@ -26,10 +26,6 @@ func NewWidget() *Widget { /* -------------------- Exported Functions -------------------- */ func (widget *Widget) Refresh() { - if widget.Disabled() { - return - } - cells, _ := Fetch() widget.UpdateRefreshedAt() diff --git a/wtf.go b/wtf.go index 22659cd7..954d3871 100644 --- a/wtf.go +++ b/wtf.go @@ -17,9 +17,9 @@ import ( "github.com/senorprogrammer/wtf/cryptoexchanges/bittrex" "github.com/senorprogrammer/wtf/cryptoexchanges/cryptolive" "github.com/senorprogrammer/wtf/gcal" - "github.com/senorprogrammer/wtf/gspreadsheets" "github.com/senorprogrammer/wtf/git" "github.com/senorprogrammer/wtf/github" + "github.com/senorprogrammer/wtf/gspreadsheets" "github.com/senorprogrammer/wtf/help" "github.com/senorprogrammer/wtf/ipinfo" "github.com/senorprogrammer/wtf/jira" @@ -186,6 +186,8 @@ func addWidget(app *tview.Application, pages *tview.Pages, widgetName string) { Widgets = append(Widgets, git.NewWidget(app, pages)) case "github": Widgets = append(Widgets, github.NewWidget(app, pages)) + case "gspreadsheets": + Widgets = append(Widgets, gspreadsheets.NewWidget()) case "ipinfo": Widgets = append(Widgets, ipinfo.NewWidget()) case "jira": @@ -225,9 +227,9 @@ func makeWidgets(app *tview.Application, pages *tview.Pages) { cmdrunner.Config = Config cryptolive.Config = Config gcal.Config = Config - gspreadsheets.Config = Config git.Config = Config github.Config = Config + gspreadsheets.Config = Config ipinfo.Config = Config jira.Config = Config newrelic.Config = Config