diff --git a/src/cmd/cron/main.go b/src/cmd/cron/main.go index a0de98e..b2802bf 100644 --- a/src/cmd/cron/main.go +++ b/src/cmd/cron/main.go @@ -1,9 +1,16 @@ package cron import ( + "bytes" "context" "fmt" + "io" + "net/http" + "net/url" "show-rss/src/feeds" + "show-rss/src/webhooks" + "strings" + "text/template" "time" ) @@ -44,7 +51,88 @@ func one(ctx context.Context, feed feeds.Feed) error { return err } - return fmt.Errorf("what do with %+v", items) + for _, item := range items { + if err := oneItem(ctx, feed, item); err != nil { + return err + } + } return feed.Executed(ctx) } + +func oneItem(ctx context.Context, feed feeds.Feed, item feeds.Item) error { + type Arg struct { + Feed feeds.Feed + Item feeds.Item + } + arg := Arg{ + Feed: feed, + Item: item, + } + + method, err := render(feed.Version.WebhookMethod, arg) + if err != nil { + return err + } + + wurl, err := render(feed.Version.WebhookURL, arg) + if err != nil { + return err + } + + body, err := render(feed.Version.WebhookBody, arg) + if err != nil { + return err + } + + var user *url.Userinfo + if u, _ := url.Parse(wurl); u.User != nil { + user = u.User + u.User = &url.Userinfo{} + wurl = u.String() + } + + if did, err := webhooks.Did(ctx, method, wurl, body); err != nil || did { + return err + } + + req, err := http.NewRequest(method, wurl, strings.NewReader(body)) + if err != nil { + return err + } + req = req.WithContext(ctx) + if user != nil { + u := user.Username() + p, _ := user.Password() + req.SetBasicAuth(u, p) + } + + c := http.Client{ + Timeout: time.Minute, + Transport: &http.Transport{DisableKeepAlives: true}, + } + resp, err := c.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + defer io.Copy(io.Discard, resp.Body) + + if resp.StatusCode > 204 { + b, _ := io.ReadAll(resp.Body) + return fmt.Errorf("failed %s %s w/ %s: (%d) %s", method, wurl, body, resp.StatusCode, b) + } + + return webhooks.Record(ctx, method, wurl, body) +} + +func render(text string, arg any) (string, error) { + tmpl, err := template.New("render").Parse(text) + if err != nil { + return "", err + } + + b := bytes.NewBuffer(nil) + err = tmpl.Execute(b, arg) + return string(b.Bytes()), err +} diff --git a/src/cmd/cron/main_test.go b/src/cmd/cron/main_test.go index 96d8935..052d844 100644 --- a/src/cmd/cron/main_test.go +++ b/src/cmd/cron/main_test.go @@ -10,7 +10,6 @@ import ( "show-rss/src/cmd/cron" "show-rss/src/db" "show-rss/src/feeds" - "slices" "strconv" "testing" "time" @@ -31,11 +30,14 @@ func TestOne(t *testing.T) { Addr: ":10000", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gets = append(gets, r.URL.String()) - t.Logf("serving fetch %s", gets[len(gets)-1]) + rb, _ := io.ReadAll(r.Body) + t.Logf("serving fetch %s (%s)", gets[len(gets)-1], rb) switch r.URL.Query().Get("idx") { case "0": case "1": + case "10": + case "11": default: http.NotFound(w, r) } @@ -47,8 +49,7 @@ func TestOne(t *testing.T) { go s.ListenAndServe() t.Cleanup(func() { s.Close() }) t.Cleanup(func() { - slices.Sort(gets) - if len(gets) != 1+2+2+2 { // healthcheck, id=0+id=1 for each of 2 unrecycled ctx, id=0+id=1 for one across shared ctx + if len(gets) != 1+2+2*2 { // healthcheck, (id=0,1) for feeds, 3 matching webhooks deduped down to 2 t.Errorf("didn't call urls: %+v", gets) } }) @@ -65,7 +66,7 @@ func TestOne(t *testing.T) { ctx := db.Test(t, ctx) for i := 0; i < 2; i++ { - if _, err := feeds.Insert(ctx, fmt.Sprintf("%s?idx=%d", sURL, i), "* * * * *", "matches", http.MethodHead, fmt.Sprintf("%s?idx=1%d", sURL, i), "{{.Title}}"); err != nil { + if _, err := feeds.Insert(ctx, fmt.Sprintf("%s?idx=%d", sURL, i), "* * * * *", "matches", http.MethodHead, fmt.Sprintf("%s?idx=1%d", sURL, i), "{{.Item.Title}}"); err != nil { t.Fatal(err) } }