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 ac185b2a..dc681155 100644 --- a/wtf.go +++ b/wtf.go @@ -17,6 +17,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/gitlab" @@ -228,6 +229,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 gitlab.Config = Config