soumya92/barista

View on GitHub
modules/gsuite/calendar/calendar.go

Summary

Maintainability
A
2 hrs
Test Coverage
A
100%
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package calendar provides a Google Calendar barista module.
package calendar

import (
    "net/http"
    "time"

    "github.com/soumya92/barista/bar"
    "github.com/soumya92/barista/base/value"
    "github.com/soumya92/barista/oauth"
    "github.com/soumya92/barista/outputs"
    "github.com/soumya92/barista/timing"

    "golang.org/x/oauth2/google"
    calendar "google.golang.org/api/calendar/v3"
)

// Status represents the response/status for an event.
type Status string

// Possible values for status per the calendar API.
const (
    StatusUnknown     = Status("")
    StatusConfirmed   = Status("confirmed")
    StatusTentative   = Status("tentative")
    StatusCancelled   = Status("cancelled")
    StatusDeclined    = Status("declined")
    StatusUnresponded = Status("needsAction")
)

// Event represents a calendar event.
type Event struct {
    Start       time.Time
    End         time.Time
    Alert       time.Time
    EventStatus Status
    Response    Status
    Location    string
    Summary     string
}

// UntilStart returns the time remaining until the event starts.
func (e Event) UntilStart() time.Duration {
    return e.Start.Sub(timing.Now())
}

// UntilEnd returns the time remaining until the event ends.
func (e Event) UntilEnd() time.Duration {
    return e.End.Sub(timing.Now())
}

// UntilAlert returns the time remaining until a notification should be
// displayed for this event.
func (e Event) UntilAlert() time.Duration {
    return e.Alert.Sub(timing.Now())
}

// EventList represents the list of events split by the temporal state of each
// event: in progress, alerting (upcoming but within notification duration), or
// upcoming beyond the notification duration.
type EventList struct {
    // All events currently in progress
    InProgress []Event
    // Events where the time until start is less than the notification duration
    Alerting []Event
    // All other future events
    Upcoming []Event
}

type config struct {
    calendarID   string
    lookahead    time.Duration
    showDeclined bool
}

// Module represents a Google Calendar barista module.
type Module struct {
    oauthConfig *oauth.Config
    config      value.Value // of config
    scheduler   *timing.Scheduler
    outputFunc  value.Value // of func(EventList) bar.Output
}

// New creates a calendar module from the given oauth config.
func New(clientConfig []byte) *Module {
    conf, err := google.ConfigFromJSON(clientConfig, calendar.CalendarReadonlyScope)
    if err != nil {
        panic("Bad client config: " + err.Error())
    }
    m := &Module{
        oauthConfig: oauth.Register(conf),
        scheduler:   timing.NewScheduler(),
    }
    m.config.Set(config{calendarID: "primary"})
    m.RefreshInterval(10 * time.Minute)
    m.TimeWindow(18 * time.Hour)
    m.Output(func(evts EventList) bar.Output {
        hasEvent := false
        out := outputs.Group()
        for _, e := range evts.InProgress {
            out.Append(outputs.Textf("ends %s: %s",
                e.End.Format("15:04"), e.Summary))
            hasEvent = true
        }
        for _, e := range evts.Alerting {
            out.Append(outputs.Textf("%s: %s",
                e.Start.Format("15:04"), e.Summary))
            hasEvent = true
        }
        if !hasEvent {
            for _, e := range evts.Upcoming {
                // If no other events have been displayed, show next upcoming.
                out.Append(outputs.Textf("%s: %s",
                    e.Start.Format("15:04"), e.Summary))
                break
            }
        }
        return out
    })
    return m
}

// for tests, to wrap the client in a transport that redirects requests.
var wrapForTest func(*http.Client)

// Stream starts the module.
func (m *Module) Stream(sink bar.Sink) {
    client, _ := m.oauthConfig.Client()
    if wrapForTest != nil {
        wrapForTest(client)
    }
    srv, _ := calendar.New(client)
    outf := m.outputFunc.Get().(func(EventList) bar.Output)
    nextOutputFunc, done := m.outputFunc.Subscribe()
    defer done()
    conf := m.getConfig()
    nextConfig, done := m.config.Subscribe()
    defer done()
    renderer := timing.NewScheduler()
    evts, err := fetch(srv, conf)
    for {
        if sink.Error(err) {
            return
        }
        list, refresh := makeEventList(evts)
        if !refresh.IsZero() {
            renderer.At(refresh.Add(time.Duration(1)))
        }
        sink.Output(outf(list))
        select {
        case <-nextOutputFunc:
            outf = m.outputFunc.Get().(func(EventList) bar.Output)
        case <-nextConfig:
            conf = m.getConfig()
            evts, err = fetch(srv, conf)
        case <-m.scheduler.C:
            evts, err = fetch(srv, conf)
        case <-renderer.C:
        }
    }
}

func fetch(srv *calendar.Service, conf config) ([]Event, error) {
    timeMin := timing.Now()
    timeMax := timeMin.Add(conf.lookahead)

    req := srv.Events.List(conf.calendarID)
    req.MaxAttendees(1)
    req.OrderBy("startTime")
    // Simplify recurring events by converting them to single events.
    req.SingleEvents(true)
    req.TimeMin(timeMin.Format(time.RFC3339))
    req.TimeMax(timeMax.Format(time.RFC3339))
    req.Fields("items(end,location,start,status,summary,attendees,reminders),defaultReminders")
    res, err := req.Do()
    if err != nil {
        return nil, err
    }
    defaultAlert := getEarliestPopupReminder(res.DefaultReminders)
    events := []Event{}
    for _, e := range res.Items {
        if e.Start.DateTime == "" || e.End.DateTime == "" {
            // All day events only have .Date, not .DateTime.
            continue
        }
        start, err := time.Parse(time.RFC3339, e.Start.DateTime)
        if err != nil {
            return nil, err
        }
        end, err := time.Parse(time.RFC3339, e.End.DateTime)
        if err != nil {
            return nil, err
        }
        if end.Before(timeMin) {
            continue
        }
        selfStatus := StatusUnknown
        for _, at := range e.Attendees {
            if at.Self {
                selfStatus = Status(at.ResponseStatus)
            }
        }
        if !conf.showDeclined && selfStatus == StatusDeclined {
            continue
        }
        alert := defaultAlert
        if e.Reminders != nil && !e.Reminders.UseDefault {
            alert = getEarliestPopupReminder(e.Reminders.Overrides)
        }
        eventStatus := Status(e.Status)
        if eventStatus == StatusCancelled {
            continue
        }
        events = append(events, Event{
            Start:       start,
            End:         end,
            Alert:       start.Add(-alert),
            EventStatus: eventStatus,
            Response:    selfStatus,
            Location:    e.Location,
            Summary:     e.Summary,
        })
    }
    return events, nil
}

func getEarliestPopupReminder(rs []*calendar.EventReminder) time.Duration {
    duration := time.Duration(0)
    for _, r := range rs {
        if r.Method != "popup" {
            continue
        }
        rDuration := time.Duration(r.Minutes) * time.Minute
        if rDuration > duration {
            duration = rDuration
        }
    }
    return duration
}

func makeEventList(events []Event) (EventList, time.Time) {
    now := timing.Now()
    var refresh time.Time
    list := EventList{}
    for _, e := range events {
        switch {
        case now.After(e.End):
            continue
        case now.After(e.Start) && e.End.After(now):
            setIfEarlier(&refresh, e.End)
            list.InProgress = append(list.InProgress, e)
        case now.After(e.Alert) && e.Start.After(now):
            setIfEarlier(&refresh, e.Start)
            list.Alerting = append(list.Alerting, e)
        default:
            setIfEarlier(&refresh, e.Alert)
            list.Upcoming = append(list.Upcoming, e)
        }
    }
    return list, refresh
}

func setIfEarlier(target *time.Time, source time.Time) {
    if target.IsZero() || source.Before(*target) {
        *target = source
    }
}

// Output sets the output format for the module.
func (m *Module) Output(outputFunc func(EventList) bar.Output) *Module {
    m.outputFunc.Set(outputFunc)
    return m
}

// RefreshInterval sets the interval for fetching new events. Note that this is
// distinct from the rendering interval, which is returned by the output func
// on each new output.
func (m *Module) RefreshInterval(interval time.Duration) *Module {
    m.scheduler.Every(interval)
    return m
}

func (m *Module) getConfig() config {
    return m.config.Get().(config)
}

// CalendarID sets the ID of the calendar to fetch events for.
func (m *Module) CalendarID(id string) *Module {
    c := m.getConfig()
    c.calendarID = id
    m.config.Set(c)
    return m
}

// TimeWindow controls the search window for future events.
func (m *Module) TimeWindow(window time.Duration) *Module {
    c := m.getConfig()
    c.lookahead = window
    m.config.Set(c)
    return m
}

// ShowDeclined controls whether declined events are shown or ignored.
func (m *Module) ShowDeclined(show bool) *Module {
    c := m.getConfig()
    c.showDeclined = show
    m.config.Set(c)
    return m
}