timer/gcal.go

148 lines
3.1 KiB
Go

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
Duration time.Duration
}
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
}
var d time.Duration
if t2, err := time.Parse(time.RFC3339, events.Items[i].End.DateTime); err == nil {
d = t2.Sub(t)
}
result = append(result, Event{
Name: events.Items[i].Summary,
Time: t,
Duration: d,
})
}
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
}