148 lines
3.1 KiB
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
|
|
}
|