From 79e6c7f48ef91bc30123fd2ab8b49c83a3a55254 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 18 Dec 2017 13:20:21 -0500 Subject: [PATCH] Break out stock-ish google calendar bits + more fiddling --- wherewhen/Makefile | 2 +- wherewhen/google_calendar.go | 77 +++++++++++ wherewhen/main.go | 261 +++++++++++++++++------------------ 3 files changed, 205 insertions(+), 135 deletions(-) create mode 100644 wherewhen/google_calendar.go diff --git a/wherewhen/Makefile b/wherewhen/Makefile index 1f875d9..f5fdd25 100644 --- a/wherewhen/Makefile +++ b/wherewhen/Makefile @@ -1,2 +1,2 @@ wherewhen: $(shell git ls-files '*.go') - go build + go build -x diff --git a/wherewhen/google_calendar.go b/wherewhen/google_calendar.go new file mode 100644 index 0000000..c006708 --- /dev/null +++ b/wherewhen/google_calendar.go @@ -0,0 +1,77 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "os" + "os/user" + "path/filepath" + + "github.com/sirupsen/logrus" + "golang.org/x/oauth2" +) + +func getClient(ctx context.Context, config *oauth2.Config) *http.Client { + cacheFile, err := tokenCacheFile() + if err != nil { + logrus.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) +} + +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 { + logrus.Fatalf("Unable to read authorization code %v", err) + } + + tok, err := config.Exchange(oauth2.NoContext, code) + if err != nil { + logrus.Fatalf("Unable to retrieve token from web %v", err) + } + return tok +} + +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("wherewhen.json")), err +} + +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 +} + +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 { + logrus.Fatalf("Unable to cache oauth token: %v", err) + } + defer f.Close() + json.NewEncoder(f).Encode(token) +} diff --git a/wherewhen/main.go b/wherewhen/main.go index c03dfca..7c0bb03 100644 --- a/wherewhen/main.go +++ b/wherewhen/main.go @@ -5,18 +5,13 @@ import ( "flag" "fmt" "io/ioutil" - "net/http" - "net/url" - "os" - "os/user" - "path/filepath" + "strconv" "strings" "syscall" "time" "github.com/sirupsen/logrus" "golang.org/x/net/context" - "golang.org/x/oauth2" "golang.org/x/oauth2/google" "google.golang.org/api/calendar/v3" ) @@ -39,12 +34,13 @@ var ( }(), "time window duration from before end time") defaultLocationFlag = flag.String( "l", getEnvDefault("DEFAULT_LOCATION", "notset"), "default location for where") - - locationAliases = map[string]string{ - "pittsburgh": "dan", - "kennywood": "dan", - "akron": "sarah", - } + locationAliasesFlag = flag.String( + "a", getEnvDefault("LOCATION_ALIASES", ""), "location alias key:value mappings") + verboseFlag = flag.Bool("v", func() bool { + stringVal := getEnvDefault("VERBOSE", "no") + val, _ := strconv.ParseBool(stringVal) + return val + }(), "enable verbose-er output") ) const ( @@ -60,8 +56,81 @@ func getEnvDefault(key, defVal string) string { return defVal } +type lilEvent struct { + Start string `json:"start"` + End string `json:"end"` + Location string `json:"loc"` + Summary string `json:"summ"` +} + +func (e *lilEvent) HasDate(d string) bool { + if e.Start == d { + logrus.Debugf("match for %s at %s", d, e.Start) + return true + } + + dT, err := time.Parse(dateFmt, d) + if err != nil { + logrus.Error(err) + return false + } + + dr := e.dateRange() + return dr[0].Before(dT) && dr[1].After(dT) +} + +func (e *lilEvent) dateRange() []time.Time { + ret := []time.Time{} + for _, t := range []string{e.Start, e.End} { + parsedDate, err := time.Parse(dateFmt, t) + if err != nil { + logrus.Error(err) + ret = append(ret, time.Time{}) + continue + } + ret = append(ret, parsedDate) + } + + return ret +} + +type lilDay struct { + Date string `json:"date"` + Location string `json:"loc"` +} + +func generateDays(startT, endT time.Time) []*lilDay { + defaultLocation := *defaultLocationFlag + days := []*lilDay{} + + curT := startT + for { + if curT.After(endT) { + return days + } + + days = append(days, &lilDay{ + Date: curT.Format(dateFmt), + Location: defaultLocation, + }) + + curT = curT.Add(24 * time.Hour) + } +} + +type lilStats struct { + Locs map[string]int `json:"locs"` + LocsPercentage map[string]float64 `json:"locs_pct"` + TotalDays int `json:"total_days"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` +} + func main() { flag.Parse() + if *verboseFlag { + logrus.SetLevel(logrus.DebugLevel) + } ctx := context.Background() @@ -96,15 +165,35 @@ func main() { logrus.Fatalf("Unable to retrieve next %d events. %v", maxResults, err) } - logrus.WithField("count", len(events.Items)).Infof("found events") + logrus.WithField("count", len(events.Items)).Debugf("found events") logrus.Debugf("generating days between %v and %v", startT, endT) days := generateDays(startT, endT) + if len(days) <= 0 { + logrus.WithFields(logrus.Fields{ + "start_time": startT, + "end_time": endT, + }).Fatal("no days generated") + } + foundEvents := []*lilEvent{} defaultLocation := *defaultLocationFlag logrus.Debugf("generated %d days between %v and %v", len(days), startT, endT) + locationAliases := map[string]string{} + for _, str := range strings.Split(*locationAliasesFlag, ",") { + strParts := strings.SplitN(strings.ToLower(strings.TrimSpace(str)), ":", 2) + if len(strParts) < 2 { + logrus.WithField("parts", strParts).Warnf("unexpected location alias mapping length") + continue + } + + locationAliases[strParts[0]] = strParts[1] + } + + logrus.WithField("location_aliases", locationAliases).Debug("parsed location aliases") + if len(events.Items) > 0 { for _, i := range events.Items { var ( @@ -179,7 +268,25 @@ func main() { } } + stats := &lilStats{ + Locs: map[string]int{}, + LocsPercentage: map[string]float64{}, + TotalDays: len(days), + StartDate: days[0].Date, + EndDate: days[len(days)-1].Date, + } + for _, day := range days { + if _, ok := stats.Locs[day.Location]; !ok { + stats.Locs[day.Location] = 0 + } + + stats.Locs[day.Location]++ + + if !*verboseFlag { + continue + } + asJson, err := json.Marshal(day) if err != nil { logrus.Error(err) @@ -188,128 +295,14 @@ func main() { fmt.Println(string(asJson)) } -} -func getClient(ctx context.Context, config *oauth2.Config) *http.Client { - cacheFile, err := tokenCacheFile() + for loc, count := range stats.Locs { + stats.LocsPercentage[loc] = float64(count) / float64(len(days)) + } + + statsJson, err := json.Marshal(map[string]*lilStats{"stats": stats}) if err != nil { - logrus.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) -} - -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 { - logrus.Fatalf("Unable to read authorization code %v", err) - } - - tok, err := config.Exchange(oauth2.NoContext, code) - if err != nil { - logrus.Fatalf("Unable to retrieve token from web %v", err) - } - return tok -} - -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("wherewhen.json")), err -} - -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 -} - -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 { - logrus.Fatalf("Unable to cache oauth token: %v", err) - } - defer f.Close() - json.NewEncoder(f).Encode(token) -} - -type lilEvent struct { - Start string `json:"start"` - End string `json:"end"` - Location string `json:"loc"` - Summary string `json:"summ"` -} - -func (e *lilEvent) HasDate(d string) bool { - if e.Start == d { - logrus.Debugf("match for %s at %s", d, e.Start) - return true - } - - dT, err := time.Parse(dateFmt, d) - if err != nil { - logrus.Error(err) - return false - } - - dr := e.dateRange() - return dr[0].Before(dT) && dr[1].After(dT) -} - -func (e *lilEvent) dateRange() []time.Time { - ret := []time.Time{} - for _, t := range []string{e.Start, e.End} { - parsedDate, err := time.Parse(dateFmt, t) - if err != nil { - logrus.Error(err) - ret = append(ret, time.Time{}) - continue - } - ret = append(ret, parsedDate) - } - - return ret -} - -type lilDay struct { - Date string `json:"date"` - Location string `json:"loc"` -} - -func generateDays(startT, endT time.Time) []*lilDay { - defaultLocation := *defaultLocationFlag - days := []*lilDay{} - - curT := startT - for { - if curT.After(endT) { - return days - } - - days = append(days, &lilDay{ - Date: curT.Format(dateFmt), - Location: defaultLocation, - }) - - curT = curT.Add(24 * time.Hour) + logrus.Fatal(err) } + fmt.Println(string(statsJson)) }