package main import ( "context" "encoding/json" "fmt" "net/http" "os" "path" "time" "golang.org/x/oauth2" "golang.org/x/oauth2/google" "google.golang.org/api/calendar/v3" "google.golang.org/api/option" ) type GCal struct { httpc *http.Client } func NewGCal() *GCal { return &GCal{} } type Event struct { Name string Time time.Time } func (gcal *GCal) EventsToday(ctx context.Context) ([]Event, error) { srv, err := calendar.NewService(ctx, option.WithHTTPClient(gcal.httpc)) if err != nil { return nil, err } today := time.Now().Add(-1 * time.Hour * time.Duration(time.Now().Hour())) tomorrow := today.Add(24 * time.Hour) events, err := srv.Events. List("primary"). ShowDeleted(false). SingleEvents(true). TimeMin(today.Format(time.RFC3339)). MaxResults(100). OrderBy("startTime"). Do() if err != nil { return nil, err } result := make([]Event, 0, len(events.Items)) for i := range events.Items { if events.Items[i].Start.DateTime == "" { continue } t, err := time.Parse(time.RFC3339, events.Items[i].Start.DateTime) if err != nil { return nil, err } else if t.After(tomorrow) { continue } result = append(result, Event{ Name: events.Items[i].Summary, Time: t, }) } return result, nil } func (gcal *GCal) Login(ctx context.Context) error { token, err := gcal.getToken(ctx) if err != nil { return err } conf, err := gcal.oauth2Config() if err != nil { return err } gcal.httpc = conf.Client(ctx, &token) return nil } func (gcal *GCal) getToken(ctx context.Context) (oauth2.Token, error) { var token oauth2.Token if b, _ := os.ReadFile(gcal.tokenFile()); len(b) == 0 { } else if err := json.Unmarshal(b, &token); err == nil { return token, nil } token, err := gcal.newToken(ctx) if err != nil { return token, err } b, _ := json.Marshal(token) return token, os.WriteFile(gcal.tokenFile(), b, os.ModePerm) } func (gcal *GCal) newToken(ctx context.Context) (oauth2.Token, error) { conf, err := gcal.oauth2Config() if err != nil { return oauth2.Token{}, err } u := conf.AuthCodeURL("state-token", oauth2.AccessTypeOffline) fmt.Printf("visit %s\n", u) var code string if _, err := fmt.Scan(&code); err != nil { return oauth2.Token{}, fmt.Errorf("failed to read oauth confirmation: %w", err) } token, err := conf.Exchange(ctx, code) if err != nil { return oauth2.Token{}, fmt.Errorf("failed to use code: %w", err) } return *token, nil } func (gcal *GCal) tokenFile() string { return path.Join(os.Getenv("HOME"), "timer.gcal") } func (gcal *GCal) oauth2Config() (*oauth2.Config, error) { p := path.Join(os.Getenv("HOME"), ".config", "gcloud", "calendar.json") b, err := os.ReadFile(p) if err != nil { return nil, fmt.Errorf("failed to read %s: %w", p, err) } conf, err := google.ConfigFromJSON(b, calendar.CalendarReadonlyScope) if err != nil { return nil, fmt.Errorf("failed to parse %s: %w", p, err) } return conf, nil }