From 8e7b4a4f5fe8780e717061f922a2ff4aba44a0d8 Mon Sep 17 00:00:00 2001 From: Bel LaPointe <153096461+breel-render@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:19:22 -0600 Subject: [PATCH] ntfy too --- .gcal.go => gcal.go | 65 ++++++++++++--- ...ration_test.go => gcal_integration_test.go | 0 main.go | 81 +++++++++++++++++-- 3 files changed, 129 insertions(+), 17 deletions(-) rename .gcal.go => gcal.go (52%) rename .gcal_integration_test.go => gcal_integration_test.go (100%) diff --git a/.gcal.go b/gcal.go similarity index 52% rename from .gcal.go rename to gcal.go index fd84ee3..38de7c8 100644 --- a/.gcal.go +++ b/gcal.go @@ -7,22 +7,66 @@ import ( "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 interface { - Do(*http.Request) (*http.Response, error) - } + 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 { @@ -41,13 +85,15 @@ func (gcal *GCal) Login(ctx context.Context) error { 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 { + } 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) } @@ -62,12 +108,12 @@ func (gcal *GCal) newToken(ctx context.Context) (oauth2.Token, error) { fmt.Printf("visit %s\n", u) var code string if _, err := fmt.Scan(&code); err != nil { - return oauth2.Token{}, err + return oauth2.Token{}, fmt.Errorf("failed to read oauth confirmation: %w", err) } token, err := conf.Exchange(ctx, code) if err != nil { - return oauth2.Token{}, err + return oauth2.Token{}, fmt.Errorf("failed to use code: %w", err) } return *token, nil @@ -78,14 +124,15 @@ func (gcal *GCal) tokenFile() string { } func (gcal *GCal) oauth2Config() (*oauth2.Config, error) { - b, err := os.ReadFile(path.Join(os.Getenv("HOME"), ".config", "gcloud", "application_default_credentials.json")) + p := path.Join(os.Getenv("HOME"), ".config", "gcloud", "calendar.json") + b, err := os.ReadFile(p) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to read %s: %w", p, err) } conf, err := google.ConfigFromJSON(b, calendar.CalendarReadonlyScope) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to parse %s: %w", p, err) } return conf, nil } diff --git a/.gcal_integration_test.go b/gcal_integration_test.go similarity index 100% rename from .gcal_integration_test.go rename to gcal_integration_test.go diff --git a/main.go b/main.go index 37c9dfb..bab9c30 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,15 @@ package main import ( "context" + "flag" + "fmt" + "log" + "net/http" + "net/url" "os" + "os/signal" + "strings" + "syscall" "time" "github.com/gen2brain/beeep" @@ -10,20 +18,69 @@ import ( ) func main() { - d, err := time.ParseDuration(os.Args[1]) - if err != nil { + ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT) + defer can() + + fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) + gcal := fs.Bool("gcal", false, "wait for google cal events") + ntfy := fs.String("http", "", "curl -X POST -d YO") + if err := fs.Parse(os.Args[1:]); err != nil { panic(err) } - deadline := time.Now().Add(d) - ctx, can := context.WithDeadline(context.Background(), deadline) - defer can() + if *gcal { + if err := alertGCal(ctx, *ntfy); err != nil { + panic(err) + } + } else { + if err := alertAfter(ctx, *ntfy, fs.Args()[0]); err != nil { + panic(err) + } + } +} +func alertGCal(ctx context.Context, ntfy string) error { + gcal := NewGCal() + if err := gcal.Login(ctx); err != nil { + return err + } + + events, err := gcal.EventsToday(ctx) + if err != nil { + return err + } + + for _, event := range events { + until := event.Time.Add(-1 * time.Minute) + if until.Sub(time.Now()) < 1 { + continue + } + + log.Println("alerting about", event.Name, "at", until.Format("15:04"), "(", time.Until(until), ")") + if err := alertAt(ctx, ntfy, until, event.Name, until.Format("15:04")); err != nil { + return err + } + } + + return nil +} + +func alertAfter(ctx context.Context, ntfy string, duration string) error { + d, err := time.ParseDuration(duration) + if err != nil { + return err + } + return alertAt(ctx, ntfy, time.Now().Add(d), os.Args[0], "alerting after "+d.String()) +} + +func alertAt(ctx context.Context, ntfy string, deadline time.Time, title, msg string) error { n := int64(time.Until(deadline) / time.Second) bar := progressbar.Default(n) c := time.NewTicker(time.Second) defer c.Stop() func() { + ctx, can := context.WithDeadline(ctx, deadline) + defer can() for { select { case <-c.C: @@ -36,7 +93,15 @@ func main() { bar.Finish() bar.Exit() - msg := "alerting after " + d.String() - beeep.Alert(os.Args[0], msg, "/dev/null") - time.Sleep(time.Second) + if err := ctx.Err(); err == nil { + beeep.Alert(title, msg, "/dev/null") + if ntfy != "" { + u, _ := url.Parse(ntfy) + req, _ := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(fmt.Sprintf("%s: %s", title, msg))) + req = req.WithContext(ctx) + http.DefaultClient.Do(req) + } + } + + return ctx.Err() }