package main import ( "encoding/json" "flag" "fmt" "io/ioutil" "strconv" "strings" "syscall" "time" "github.com/sirupsen/logrus" "golang.org/x/net/context" "golang.org/x/oauth2/google" "google.golang.org/api/calendar/v3" ) var ( nowTime = time.Now() oneYearString = fmt.Sprintf("%dh", 365*24) oneYear, _ = time.ParseDuration(oneYearString) clientSecretFlag = flag.String( "s", getEnvDefault("CLIENT_SECRET", "client_secret.json"), "client secret json file") calendarIDFlag = flag.String( "i", getEnvDefault("CALENDAR", "primary"), "calendar ID to search") endTimeFlag = flag.String( "e", getEnvDefault("END_TIME", nowTime.Format(dateFmt)), "end time for search") timeWindowFlag = flag.Duration( "t", func() time.Duration { stringVal := getEnvDefault("DURATION", oneYearString) val, _ := time.ParseDuration(stringVal) return val }(), "time window duration from before end time") defaultLocationFlag = flag.String( "l", getEnvDefault("DEFAULT_LOCATION", "notset"), "default location for where") 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 ( dateFmt = "2006-01-02" ) func getEnvDefault(key, defVal string) string { for _, k := range []string{"WHEREWHEN_" + key, key} { if v, ok := syscall.Getenv(k); ok { return v } } 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() b, err := ioutil.ReadFile(*clientSecretFlag) if err != nil { logrus.Fatalf("Unable to read client secret file: %v", err) } config, err := google.ConfigFromJSON(b, calendar.CalendarReadonlyScope) if err != nil { logrus.Fatalf("Unable to parse client secret file to config: %v", err) } client := getClient(ctx, config) srv, err := calendar.New(client) if err != nil { logrus.Fatalf("Unable to retrieve calendar Client %v", err) } endT, err := time.Parse(dateFmt, *endTimeFlag) if err != nil { logrus.Fatalf("Unable to parse end time %q: %v", *endTimeFlag, err) } maxResults := int64(2500) startT := endT.Add(-*timeWindowFlag) t := startT.Format(time.RFC3339) events, err := srv.Events.List(*calendarIDFlag).ShowDeleted(false). SingleEvents(true).TimeMin(t).TimeMax(endT.Format(time.RFC3339)). MaxResults(maxResults).OrderBy("startTime").Do() if err != nil { logrus.Fatalf("Unable to retrieve next %d events. %v", maxResults, err) } 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 ( startDate string endDate string ) if i.Start.DateTime != "" { startT, err := time.Parse(time.RFC3339, i.Start.DateTime) if err != nil { logrus.Errorf("error parsing start date: %v", err) continue } endT, err := time.Parse(time.RFC3339, i.End.DateTime) if err != nil { logrus.Errorf("error parsing end date: %v", err) continue } startDate = startT.Format(dateFmt) endDate = endT.Format(dateFmt) } else { startDate = i.Start.Date endDate = i.End.Date } evt := &lilEvent{ Start: startDate, End: endDate, Location: defaultLocation, Summary: strings.ToLower(strings.TrimSpace(i.Summary)), } summ := strings.ToLower(i.Summary) if strings.Contains(summ, "possible") { continue } parts := strings.Split(summ, " ") locIdx := 0 for i, part := range parts { if part == "in" || part == "with" { locIdx = i + 1 } } loc := strings.TrimSpace(parts[locIdx]) if locAlias, ok := locationAliases[loc]; ok { loc = locAlias } evt.Location = loc if evt.Start == evt.End { logrus.Debugf("skipping same-day event on %s (%s)", evt.Start, evt.Summary) continue } foundEvents = append(foundEvents, evt) } } else { logrus.Info("No events found") } for _, day := range days { logrus.Debugf("working on date %v", day) for _, evt := range foundEvents { logrus.Debugf("checking if %s is in range %s %s", day.Date, evt.Start, evt.End) if evt.HasDate(day.Date) { logrus.Debugf("match found for %s in range %s %s, setting location to %s", day.Date, evt.Start, evt.End, evt.Location) day.Location = evt.Location } } } 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) continue } fmt.Println(string(asJson)) } 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.Fatal(err) } fmt.Println(string(statsJson)) }