diff --git a/gcal/client.go b/gcal/client.go index 4523ebb7f..b34a249d7 100644 --- a/gcal/client.go +++ b/gcal/client.go @@ -8,42 +8,27 @@ package gcal import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" - "net/url" - "os" - "os/user" - "path/filepath" "sort" "time" "github.com/senorprogrammer/wtf/wtf" - "golang.org/x/oauth2" - "golang.org/x/oauth2/google" "google.golang.org/api/calendar/v3" ) /* -------------------- Exported Functions -------------------- */ -func Fetch() ([]*CalEvent, error) { - ctx := context.Background() - - secretPath, _ := wtf.ExpandHomeDir(wtf.Config.UString("wtf.mods.gcal.secretFile")) - - b, err := ioutil.ReadFile(secretPath) - if err != nil { - return nil, err +func Fetch(oauthHandler OAuthUIHandler) ([]*CalEvent, error) { + secretFile, _ := wtf.ExpandHomeDir(wtf.Config.UString("wtf.mods.gcal.secretFile")) + tokenFile, _ := wtf.ExpandHomeDir(wtf.Config.UString("wtf.mods.gcal.tokenFile", "~/.gcal.token")) + config := OAuthClientConfig{ + SecretFile: secretFile, + TokenFile: tokenFile, + Scope: calendar.CalendarReadonlyScope, } - - config, err := google.ConfigFromJSON(b, calendar.CalendarReadonlyScope) + client, err := BuildOAuthClient(config, oauthHandler) if err != nil { return nil, err } - client := getClient(ctx, config) srv, err := calendar.New(client) if err != nil { @@ -100,79 +85,6 @@ func fromMidnight() time.Time { return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) } -// 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("calendar-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) -} - func getCalendarIdList(srv *calendar.Service) ([]string, error) { // Return single calendar if settings specify we should if !wtf.Config.UBool("wtf.mods.gcal.multiCalendar", false) { diff --git a/gcal/google.go b/gcal/google.go new file mode 100644 index 000000000..1c1dfa08e --- /dev/null +++ b/gcal/google.go @@ -0,0 +1,107 @@ +package gcal + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + + "github.com/senorprogrammer/wtf/logger" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" +) + +// OAuthClientConfig describes Google oauth2 client configuration +type OAuthClientConfig struct { + SecretFile string + TokenFile string + Scope string +} + +// OAuthUIHandler returns oauth2 authorization code, implements user interaction +type OAuthUIHandler = func(string, chan string) + +func BuildOAuthClient(config OAuthClientConfig, handler OAuthUIHandler) (*http.Client, error) { + ctx := context.Background() + + b, err := ioutil.ReadFile(config.SecretFile) + if err != nil { + logger.Log(fmt.Sprintf("Invalid secret file provided (%s): %v", config.SecretFile, err)) + return nil, err + } + + oauthConfig, err := google.ConfigFromJSON(b, config.Scope) + if err != nil { + logger.Log(fmt.Sprintf("Secret file is not readable as JSON (%s): %v", config.SecretFile, err)) + return nil, err + } + + tok, err := tokenFromFile(config.TokenFile) + if err != nil { + result := make(chan string) + go handler(oauthConfig.AuthCodeURL("state-token", oauth2.AccessTypeOffline), result) + code, ok := <-result + if !ok { + // Cancelled + return nil, err + } + tok, err = oauthConfig.Exchange(oauth2.NoContext, code) + if err != nil { + logger.Log(fmt.Sprintf("Invalid exchange code provided (%s): %v", code, err)) + return nil, err + } + err = saveToken(config.TokenFile, tok) + if err != nil { + logger.Log(fmt.Sprintf("Unable to store oauth token (%s): %v", config.TokenFile, err)) + } + + } + return oauthConfig.Client(ctx, tok), nil +} + +// CreateCodeInputDialog shows oauth2 code input dialog +func CreateCodeInputDialog(title string, widget *Widget) OAuthUIHandler { + return func(url string, result chan string) { + readInput := func() { + fmt.Printf("\r\nOAuth authorization [%s]\r\n", title) + fmt.Printf("Please open the following URL in your browser and paste authorization code below:\r\n") + fmt.Printf("%s\r\nAuthorization code: ", url) + var code string + if _, err := fmt.Scan(&code); err != nil { + logger.Log(fmt.Sprintf("Unable to read authorization code: %v", err)) + close(result) + } else { + result <- code + } + } + widget.app.Suspend(readInput) + } +} + +// 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) error { + // 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 { + return err + } + defer f.Close() + json.NewEncoder(f).Encode(token) + return nil +} diff --git a/gcal/widget.go b/gcal/widget.go index 7ad4bc25d..7d75da6f0 100644 --- a/gcal/widget.go +++ b/gcal/widget.go @@ -4,6 +4,7 @@ import ( "sync" "time" + "github.com/rivo/tview" "github.com/senorprogrammer/wtf/wtf" ) @@ -13,11 +14,13 @@ type Widget struct { calEvents []*CalEvent ch chan struct{} mutex sync.Mutex + app *tview.Application } -func NewWidget() *Widget { +func NewWidget(app *tview.Application) *Widget { widget := Widget{ - TextWidget: wtf.NewTextWidget(" Calendar ", "gcal", false), + TextWidget: wtf.NewTextWidget(" Calendar ", "gcal", true), + app: app, ch: make(chan struct{}), } @@ -34,7 +37,7 @@ func (widget *Widget) Disable() { } func (widget *Widget) Refresh() { - calEvents, err := Fetch() + calEvents, err := Fetch(CreateCodeInputDialog(" Calendar ", widget)) if err != nil { widget.calEvents = []*CalEvent{} } else { diff --git a/wtf.go b/wtf.go index 79b2a6d8b..39a8b99e0 100644 --- a/wtf.go +++ b/wtf.go @@ -179,7 +179,7 @@ func addWidget(app *tview.Application, pages *tview.Pages, widgetName string) { case "cryptolive": Widgets = append(Widgets, cryptolive.NewWidget()) case "gcal": - Widgets = append(Widgets, gcal.NewWidget()) + Widgets = append(Widgets, gcal.NewWidget(app)) case "gerrit": Widgets = append(Widgets, gerrit.NewWidget(app, pages)) case "git":