main
Bel LaPointe 2024-10-31 12:19:22 -06:00
parent 79cf212eb6
commit 8e7b4a4f5f
3 changed files with 129 additions and 17 deletions

View File

@ -7,22 +7,66 @@ import (
"net/http" "net/http"
"os" "os"
"path" "path"
"time"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/oauth2/google" "golang.org/x/oauth2/google"
"google.golang.org/api/calendar/v3" "google.golang.org/api/calendar/v3"
"google.golang.org/api/option"
) )
type GCal struct { type GCal struct {
httpc interface { httpc *http.Client
Do(*http.Request) (*http.Response, error)
}
} }
func NewGCal() *GCal { func NewGCal() *GCal {
return &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 { func (gcal *GCal) Login(ctx context.Context) error {
token, err := gcal.getToken(ctx) token, err := gcal.getToken(ctx)
if err != nil { 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) { func (gcal *GCal) getToken(ctx context.Context) (oauth2.Token, error) {
var token oauth2.Token var token oauth2.Token
if b, _ := os.ReadFile(gcal.tokenFile()); len(b) == 0 { 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 return token, nil
} }
token, err := gcal.newToken(ctx) token, err := gcal.newToken(ctx)
if err != nil { if err != nil {
return token, err return token, err
} }
b, _ := json.Marshal(token) b, _ := json.Marshal(token)
return token, os.WriteFile(gcal.tokenFile(), b, os.ModePerm) 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) fmt.Printf("visit %s\n", u)
var code string var code string
if _, err := fmt.Scan(&code); err != nil { 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) token, err := conf.Exchange(ctx, code)
if err != nil { if err != nil {
return oauth2.Token{}, err return oauth2.Token{}, fmt.Errorf("failed to use code: %w", err)
} }
return *token, nil return *token, nil
@ -78,14 +124,15 @@ func (gcal *GCal) tokenFile() string {
} }
func (gcal *GCal) oauth2Config() (*oauth2.Config, error) { 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 { if err != nil {
return nil, err return nil, fmt.Errorf("failed to read %s: %w", p, err)
} }
conf, err := google.ConfigFromJSON(b, calendar.CalendarReadonlyScope) conf, err := google.ConfigFromJSON(b, calendar.CalendarReadonlyScope)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to parse %s: %w", p, err)
} }
return conf, nil return conf, nil
} }

81
main.go
View File

@ -2,7 +2,15 @@ package main
import ( import (
"context" "context"
"flag"
"fmt"
"log"
"net/http"
"net/url"
"os" "os"
"os/signal"
"strings"
"syscall"
"time" "time"
"github.com/gen2brain/beeep" "github.com/gen2brain/beeep"
@ -10,20 +18,69 @@ import (
) )
func main() { func main() {
d, err := time.ParseDuration(os.Args[1]) ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
if err != nil { 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) panic(err)
} }
deadline := time.Now().Add(d) if *gcal {
ctx, can := context.WithDeadline(context.Background(), deadline) if err := alertGCal(ctx, *ntfy); err != nil {
defer can() 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) n := int64(time.Until(deadline) / time.Second)
bar := progressbar.Default(n) bar := progressbar.Default(n)
c := time.NewTicker(time.Second) c := time.NewTicker(time.Second)
defer c.Stop() defer c.Stop()
func() { func() {
ctx, can := context.WithDeadline(ctx, deadline)
defer can()
for { for {
select { select {
case <-c.C: case <-c.C:
@ -36,7 +93,15 @@ func main() {
bar.Finish() bar.Finish()
bar.Exit() bar.Exit()
msg := "alerting after " + d.String() if err := ctx.Err(); err == nil {
beeep.Alert(os.Args[0], msg, "/dev/null") beeep.Alert(title, msg, "/dev/null")
time.Sleep(time.Second) 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()
} }