diff --git a/wherewhen/cmd/wherewhen/main.go b/wherewhen/cmd/wherewhen/main.go new file mode 100644 index 0000000..63831cb --- /dev/null +++ b/wherewhen/cmd/wherewhen/main.go @@ -0,0 +1,209 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "os/user" + "path/filepath" + "strings" + "time" + + "golang.org/x/net/context" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "google.golang.org/api/calendar/v3" +) + +var ( + nowTime = time.Now() + oneYear, _ = time.ParseDuration("365d") + clientSecretFlag = flag.String("s", "client_secret.json", "client secret json file") + calendarIDFlag = flag.String("i", "primary", "calendar ID to search") + endTimeFlag = flag.String("e", nowTime.Format(time.RFC3339), "end time for search") + timeWindowFlag = flag.Duration("t", oneYear, "time window duration from before end time") + defaultLocationFlag = flag.String("l", "notset", "default location for where") + + locationAliases = map[string]string{ + "pittsburgh": "dan", + "kennywood": "dan", + "akron": "sarah", + } +) + +func main() { + flag.Parse() + + ctx := context.Background() + + b, err := ioutil.ReadFile(*clientSecretFlag) + if err != nil { + log.Fatalf("Unable to read client secret file: %v", err) + } + + config, err := google.ConfigFromJSON(b, calendar.CalendarReadonlyScope) + if err != nil { + log.Fatalf("Unable to parse client secret file to config: %v", err) + } + client := getClient(ctx, config) + + srv, err := calendar.New(client) + if err != nil { + log.Fatalf("Unable to retrieve calendar Client %v", err) + } + + endT, err := time.Parse(time.RFC3339, *endTimeFlag) + if err != nil { + log.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 { + log.Fatalf("Unable to retrieve next %d events. %v", maxResults, err) + } + + // days := generateDays(startT, endT) + found := []*miniEvent{} + defaultLocation := *defaultLocationFlag + + if len(events.Items) > 0 { + for _, i := range events.Items { + var ( + startTime string + endTime string + ) + if i.Start.DateTime != "" { + startTime = i.Start.DateTime + endTime = i.End.DateTime + } else { + startTime = i.Start.Date + endTime = i.End.Date + } + + evt := &miniEvent{ + Start: startTime, + End: endTime, + 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 + + found = append(found, evt) + } + } else { + fmt.Fprintf(os.Stderr, "No events found.\n") + } + + // TODO: sort found events + + for _, evt := range found { + asJson, err := json.Marshal(evt) + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) + continue + } + fmt.Printf("%s\n", string(asJson)) + } +} + +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) +} + +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 +} + +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 { + log.Fatalf("Unable to cache oauth token: %v", err) + } + defer f.Close() + json.NewEncoder(f).Encode(token) +} + +type miniEvent struct { + Start string `json:"start"` + StartDate string `json:"start_date"` + End string `json:"end"` + EndDate string `json:"end_date"` + Location string `json:"loc"` + Summary string `json:"summ"` +} + +func generateDays(startT, endT time.Time) []string { + return []string{} +} diff --git a/wherewhen/quickstart.go b/wherewhen/quickstart.go deleted file mode 100644 index c665a01..0000000 --- a/wherewhen/quickstart.go +++ /dev/null @@ -1,137 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" - "net/url" - "os" - "os/user" - "path/filepath" - "time" - - "golang.org/x/net/context" - "golang.org/x/oauth2" - "golang.org/x/oauth2/google" - "google.golang.org/api/calendar/v3" -) - -// 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 main() { - ctx := context.Background() - - b, err := ioutil.ReadFile("client_secret.json") - if err != nil { - log.Fatalf("Unable to read client secret file: %v", err) - } - - // If modifying these scopes, delete your previously saved credentials - // at ~/.credentials/calendar-go-quickstart.json - config, err := google.ConfigFromJSON(b, calendar.CalendarReadonlyScope) - if err != nil { - log.Fatalf("Unable to parse client secret file to config: %v", err) - } - client := getClient(ctx, config) - - srv, err := calendar.New(client) - if err != nil { - log.Fatalf("Unable to retrieve calendar Client %v", err) - } - - t := time.Now().Format(time.RFC3339) - events, err := srv.Events.List("primary").ShowDeleted(false). - SingleEvents(true).TimeMin(t).MaxResults(10).OrderBy("startTime").Do() - if err != nil { - log.Fatalf("Unable to retrieve next ten of the user's events. %v", err) - } - - fmt.Println("Upcoming events:") - if len(events.Items) > 0 { - for _, i := range events.Items { - var when string - // If the DateTime is an empty string the Event is an all-day Event. - // So only Date is available. - if i.Start.DateTime != "" { - when = i.Start.DateTime - } else { - when = i.Start.Date - } - fmt.Printf("%s (%s)\n", i.Summary, when) - } - } else { - fmt.Printf("No upcoming events found.\n") - } -}