package main import ( "encoding/json" "flag" "fmt" "io/ioutil" "net/http" "net/url" "os" "os/user" "path/filepath" "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" ) 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") locationAliases = map[string]string{ "pittsburgh": "dan", "kennywood": "dan", "akron": "sarah", } ) 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 } func main() { flag.Parse() 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)).Infof("found events") logrus.Debugf("generating days between %v and %v", startT, endT) days := generateDays(startT, endT) foundEvents := []*lilEvent{} defaultLocation := *defaultLocationFlag logrus.Debugf("generated %d days between %v and %v", len(days), startT, endT) 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 } } } for _, day := range days { asJson, err := json.Marshal(day) if err != nil { logrus.Error(err) continue } fmt.Println(string(asJson)) } } 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) } 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) } }