Streak
Google APIs Client and OAuth2
Andrew Gerrand
Andrew Gerrand
Streak is a command-line productivity tool based around the "Seinfeld method".
[Seinfeld] revealed a unique calendar system he uses to pressure himself to write. Here's how it works. He told me to get a big wall calendar that has a whole year on one page and hang it on a prominent wall. The next step was to get a big red magic marker. He said for each day that I do my task of writing, I get to put a big red X over that day. "After a few days you'll have a chain. Just keep at it and the chain will grow longer every day. You'll like seeing that chain, especially when you get a few weeks under your belt. Your only job next is to not break the chain." "Don't break the chain," he said again for emphasis.
Streak maintains a Google Calendar named "Streaks", adding and extending multi-day events to represent a chain or "streak."
Add today to a streak (or create a streak if none exists):
$ streak
Remove today from a streak:
$ streak -remove
Add yesterday to a streak (or create if none exists):
$ streak -offset -1
Remove yesterday from a streak:
$ streak -offset -1 -remove
At startup:
When adding a day to a streak:
When removing a day from a streak:
Beyond the Go standard library, Streak has a two dependencies:
google-api-go-client, to access the Google Calendar API, andgoauth2, for OAuth2 authentication with Google.
Import the relevant package from the google-api-go-client repository:
import "code.google.com/p/google-api-go-client/calendar/v3"
With an OAuth-authenticated HTTP client (more on this later), create a calendar service:
service, err := calendar.New(transport.Client())
if err != nil {
log.Fatal(err)
}
To list the user's calendars, first build an *calendar.CalendarListCall value:
call := service.CalendarList.List()
Invoke the call's Do method, which returns a *calendar.CalendarList value:
list, err := call.Do()
if err != nil {
return "", err
}Then finally we can use the result:
for _, entry := range list.Items {
fmt.Println(entry.Summary)
}const calSummary = "Streaks"
func streakCalendarId(service *calendar.Service) (string, error) {
list, err := service.CalendarList.List().Do()
if err != nil {
return "", err
}
for _, entry := range list.Items {
if entry.Summary == calSummary {
return entry.Id, nil
}
}
return "", errors.New("couldn't find calendar named 'Streaks'")
}
All operations require a calendar service and the relevant calendar ID, so we'll put them in a Calendar type:
type Calendar struct {
Id string
*calendar.Service
}
At startup we create a service and find the Calendar ID, and store them in a Calendar:
service, err := calendar.New(transport.Client())
calId, err := streakCalendarId(service)
cal := &Calendar{
Id: calId,
Service: service,
}Similar to listing calendars.
First build an *calendar.EventsListCall value, and use its "method chaining" API to request only non-recurring events in chronological order:
call := service.Events.List(calId).SingleEvents(true).OrderBy("startTime")
Invoke the call's Do method, which returns a *calendar.Events value:
events, err := call.Do()
if err != nil {
return err
}Then we can use the result:
for _, e := range events.Items {
// Do something with the event, e.
}
Only 100 Events may be returned per API call, so we may need to make multiple calls to retrieve the full list. The *Events struct has a NextPageToken field for pagination.
var pageToken string
for {
call := service.Events.List(calId).SingleEvents(true).OrderBy("startTime")
if pageToken != "" {
call.PageToken(pageToken)
}
events, err := call.Do()
if err != nil {
return err
}
for _, e := range events.Items {
// Do something with the event, e.
}
pageToken = events.NextPageToken
if pageToken == "" {
break // This is the last page.
}
}We need to iterate through the events for both the add and remove operations, so it would be nice to abstract away this functionality somehow.
What if we could just write this instead?
cal.iterateEvents(func(e *calendar.Event) error {
// Do something with the event, e.
})func (c *Calendar) iterateEvents(fn func(e *calendar.Event) error) error {
var pageToken string
for {
call := c.Events.List(c.Id).SingleEvents(true).OrderBy("startTime")
if pageToken != "" {
call.PageToken(pageToken)
}
events, err := call.Do()
if err != nil {
return err
}
for _, e := range events.Items {
if err := fn(e); err != nil {
return err
}
}
pageToken = events.NextPageToken
if pageToken == "" {
return nil
}
}
panic("unreachable")
}
We don't always want to iterate over the entire list, so we add a Continue error value that the iterator function must return to continue the iteration (otherwise the iterator returns with the given error).
var Continue = errors.New("continue")
func (c *Calendar) iterateEvents(fn func(e *calendar.Event) error) error {
// ...
for _, e := range events.Items {
if err := fn(e); err != Continue {
return err
}
}
// ...
}cal.iterateEvents(func(e *calendar.Event) error {
if e.Summary == "Foo" {
return nil // stop iterating
}
return Continue
})
There's some other common behavior we can put in the iterator.
We only want all-day events named "Streak", and since we use the time package to perform computations on dates, we must convert the Calendar API's date strings to Go time.Time values.
type iteratorFunc func(e *calendar.Event, start, end time.Time) error
func (c *Calendar) iterateEvents(fn iteratorFunc) error {
// ...
for _, e := range events.Items {
if e.Start.Date == "" || e.End.Date == "" || e.Summary != evtSummary {
// Skip non-all-day event or non-streak events.
continue
}
start, end := parseDate(e.Start.Date), parseDate(e.End.Date)
if err := fn(e, start, end); err != Continue {
return err
}
}
// ...
}This code to uses the iterator to find the duration of the longest event:
var longest time.Duration
cal.iterateEvents(func(e *calendar.Event, start, end time.Time) error {
if d := end.Sub(start); d > longest {
longest = d
}
return Continue
})
fmt.Println("Longest streak:", longest)Output:
Longest streak: 360h0m0s
The addToStreak function adds the given date to a streak in the Calendar.
func (c *Calendar) addToStreak(today time.Time) (err error) {
create := true
err = c.iterateEvents(func(e *calendar.Event, start, end time.Time) error {
if start.After(today) {
if start.Add(-day).Equal(today) {
// This event starts tomorrow, update it to start today.
create = false
e.Start.Date = today.Format(dateFormat)
_, err = c.Events.Update(c.Id, e.Id, e).Do()
return err
}
// This event is too far in the future.
return Continue
}
if end.After(today) {
// Today fits inside this event, nothing to do.
create = false
return nil
}
// ...// ...
if end.Equal(today) {
// This event ends today, update it to end tomorrow.
create = false
e.End.Date = today.Add(day).Format(dateFormat)
_, err = c.Events.Update(c.Id, e.Id, e).Do()
if err != nil {
return err
}
}
return Continue
})
if err == nil && create {
// No existing events cover or are adjacent to today, so create one.
err = c.createEvent(today, today.Add(day))
}
return
}(This code is slightly abridged; the real program also combines adjacent events.)
The removeFromStreak function is just like addToStreak.
func (c *Calendar) removeFromStreak(today time.Time) (err error) {
err = c.iterateEvents(func(e *calendar.Event, start, end time.Time) error {
if start.After(today) || end.Before(today) || end.Equal(today) {
// This event is too far in the future or past.
return Continue
}
if start.Equal(today) {
if end.Equal(today.Add(day)) {
// Single day event; remove it.
return c.Events.Delete(c.Id, e.Id).Do()
}
// Starts today; shorten to begin tomorrow.
e.Start.Date = start.Add(day).Format(dateFormat)
_, err := c.Events.Update(c.Id, e.Id, e).Do()
return err
}
if end.Equal(today.Add(day)) {
// Ends tomorrow; shorten to end today.
e.End.Date = today.Format(dateFormat)
_, err := c.Events.Update(c.Id, e.Id, e).Do()
return err
}// ...
// Split into two events.
// Shorten first event to end today.
e.End.Date = today.Format(dateFormat)
_, err = c.Events.Update(c.Id, e.Id, e).Do()
if err != nil {
return err
}
// Create second event that starts tomorrow.
return c.createEvent(today.Add(day), end)
})
return
}Streak uses OAuth2 to authenticate with the Calendar API.
OAuth2 in a nutshell:
code query parametercode with Service for an Access Token
Streak uses the goauth2 package, which helps with the first, fourth, and final steps.
import "code.google.com/p/goauth2/oauth"
The client ID and secret are obtained from the Google APIs Console. The Scope specifies the service to access (Calendar), while the AuthURL and TokenURL point to Google's OAuth2 service.
config := &oauth.Config{
ClientId: "120233572441-d8vmojicfgje467joivr5a7j52dg2gnc.apps.googleusercontent.com",
ClientSecret: "vfZkluBV6PTfGBWxfIIyXbMS",
Scope: "https://www.googleapis.com/auth/calendar",
AuthURL: "https://accounts.google.com/o/oauth2/auth",
TokenURL: "https://accounts.google.com/o/oauth2/token",
TokenCache: oauth.CacheFile(*cachefile),
}
The TokenCache field and oauth.CacheFile helpers transparently store the access token on disk. A flag specifies the location of the cache file:
defaultCacheFile = filepath.Join(os.Getenv("HOME"), ".streak-request-token")
cachefile = flag.String("cachefile", defaultCacheFile, "Authentication token cache file")
At startup, try to read the cached token.
If that fails for any reason, call authenticate to go do the OAuth2 flow.
If it succeeds, store the token in the transport and build a Calendar service.
transport := &oauth.Transport{Config: config}
if token, err := config.TokenCache.Token(); err != nil {
err = authenticate(transport)
if err != nil {
log.Fatalln("authenticate:", err)
}
} else {
transport.Token = token
}
service, err := calendar.New(transport.Client())func authenticate(transport *oauth.Transport) error {
code := make(chan string)
listener, err := net.Listen("tcp", "localhost:0")
if err != nil {
return err
}
go http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, closeMessage)
code <- r.FormValue("code") // send code to OAuth flow
listener.Close() // shut down HTTP server
}))
transport.Config.RedirectURL = fmt.Sprintf("http://%s/", listener.Addr())
url := transport.Config.AuthCodeURL("")
if err := openURL(url); err != nil {
fmt.Fprintln(os.Stderr, visitMessage)
} else {
fmt.Fprintln(os.Stderr, openedMessage)
}
fmt.Fprintf(os.Stderr, "\n%s\n\n", url)
fmt.Fprintln(os.Stderr, resumeMessage)
_, err = transport.Exchange(<-code)
return err
}Andrew Gerrand