diff --git a/gcal.go b/gcal.go index 38de7c8..ea9dc12 100644 --- a/gcal.go +++ b/gcal.go @@ -36,6 +36,7 @@ func (gcal *GCal) EventsToday(ctx context.Context) ([]Event, error) { 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). diff --git a/main.go b/main.go index ca62ada..cf51f7f 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "net/url" "os" "os/signal" + "slices" "strings" "syscall" "time" @@ -27,53 +28,93 @@ func main() { panic(err) } - if *gcal { - if err := alertGCal(ctx, *ntfy); err != nil { - panic(err) - } - } else { - if err := alertAfter(ctx, *ntfy, fs.Args()[0]); err != nil { + alerts, err := alerts(ctx, *gcal, fs.Args()) + if err != nil { + panic(err) + } + + for alert := range alerts { + if err := alertAt(ctx, *ntfy, time.Now(), alert, time.Now().Format("15:04")); err != nil { panic(err) } } } -func alertGCal(ctx context.Context, ntfy string) error { +func alerts(ctx context.Context, gcal bool, args []string) (chan string, error) { + if gcal { + return alertsGCal(ctx) + } + duration, err := time.ParseDuration(args[0]) + if err != nil { + return nil, err + } + return alertsAfter(ctx, duration) +} + +func alertsAfter(ctx context.Context, dur time.Duration) (chan string, error) { + ch := make(chan string) + go func() { + defer close(ch) + select { + case <-ctx.Done(): + case <-time.After(dur): + } + ch <- "alerting after " + dur.String() + }() + return ch, nil +} + +func alertsGCal(ctx context.Context) (chan string, error) { gcal := NewGCal() if err := gcal.Login(ctx); err != nil { - return err + return nil, 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 + var events []Event + refresh := func() error { + es, err := gcal.EventsToday(ctx) + es = slices.DeleteFunc(es, func(s Event) bool { + return time.Now().After(s.Time) + }) + if !slices.Equal(es, events) { + events = es + if len(events) > 0 { + log.Println("alerting about", events[0].Name, "at", events[0].Time.Format("15:04"), "(", time.Until(events[0].Time), ")") + } } - - 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()) + if err := refresh(); err != nil { + return nil, err + } + + ch := make(chan string) + go func() { + defer close(ch) + + c := time.NewTicker(time.Minute * 13) + defer c.Stop() + for ctx.Err() == nil && len(events) > 0 { + select { + case <-c.C: + if err := refresh(); err != nil { + panic(err) + } + case <-time.After(time.Until(events[0].Time.Add(-2 * time.Minute))): + select { + case <-ctx.Done(): + case ch <- events[0].Name: + } + events = events[1:] + case <-ctx.Done(): + } + } + }() + + return ch, nil } func alertAt(ctx context.Context, ntfy string, deadline time.Time, title, msg string) error { - // n := int64(time.Until(deadline) / time.Second) c := time.NewTicker(time.Second) defer c.Stop() func() {