178 lines
3.7 KiB
Go
178 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/signal"
|
|
"slices"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/gen2brain/beeep"
|
|
)
|
|
|
|
func main() {
|
|
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", "", "https://squeaky2x3:dYbtypGkHXFtq1E00k2H42SPGUowi@ntfy.home.blapointe.com/alerts-render")
|
|
if err := fs.Parse(os.Args[1:]); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
alerts, err := alerts(ctx, *gcal, fs.Args())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
for alert := range alerts {
|
|
if ctx.Err() != nil {
|
|
break
|
|
}
|
|
if err := alertAt(ctx, *ntfy, time.Now(), alert, time.Now().Format("15:04")); err != nil && ctx.Err() == nil {
|
|
panic(err)
|
|
}
|
|
if ctx.Err() != nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
if err := ctx.Err(); err != nil {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
msg := "alerting after " + duration.String()
|
|
if len(args) > 1 {
|
|
msg = fmt.Sprintf("%s (%s)", args[1], duration.String())
|
|
}
|
|
return alertsAfter(ctx, duration, msg)
|
|
}
|
|
|
|
func alertsAfter(ctx context.Context, dur time.Duration, msg string) (chan string, error) {
|
|
ch := make(chan string)
|
|
deadline := time.Now().Add(dur)
|
|
go func() {
|
|
defer close(ch)
|
|
for ctx.Err() == nil && time.Now().Before(deadline) {
|
|
seconds := int(time.Until(deadline).Seconds())
|
|
fmt.Printf("\r%v ", time.Duration(seconds)*time.Second)
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-time.After(time.Second):
|
|
}
|
|
}
|
|
if ctx.Err() == nil {
|
|
fmt.Println(msg)
|
|
}
|
|
ch <- msg
|
|
}()
|
|
return ch, nil
|
|
}
|
|
|
|
func alertsGCal(ctx context.Context) (chan string, error) {
|
|
gcal := NewGCal()
|
|
if err := gcal.Login(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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), ")")
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
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)):
|
|
select {
|
|
case <-ctx.Done():
|
|
case ch <- events[0].Name:
|
|
if events[0].Duration > 0 {
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-time.After(events[0].Duration):
|
|
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 {
|
|
c := time.NewTicker(time.Second)
|
|
defer c.Stop()
|
|
func() {
|
|
ctx, can := context.WithDeadline(ctx, deadline)
|
|
defer can()
|
|
for ctx.Err() == nil {
|
|
select {
|
|
case <-c.C:
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
fmt.Printf("\r%s ", deadline.Sub(time.Now()))
|
|
}
|
|
}()
|
|
fmt.Println()
|
|
|
|
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()
|
|
}
|