From 96cf231814d27f6dc9451e4eef0b7faaa63fc3c5 Mon Sep 17 00:00:00 2001 From: Peter Kurfer Date: Fri, 17 Jan 2025 14:45:14 +0100 Subject: [PATCH] feat(comics): add xkcd comics --- comics/main.go | 81 ++++++++++++++++++++++++++++++++++++++---------- comics/spin.toml | 2 +- 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/comics/main.go b/comics/main.go index c55d17d..23b8ace 100644 --- a/comics/main.go +++ b/comics/main.go @@ -6,6 +6,7 @@ import ( "log/slog" "math/rand/v2" "net/http" + "net/url" "github.com/PuerkitoBio/goquery" spinhttp "github.com/fermyon/spin/sdk/go/v2/http" @@ -22,8 +23,9 @@ type MonkeyUserEntry struct { } type Comic struct { - Title string `json:"title"` - URL string `json:"url"` + Source string `json:"source"` + Title string `json:"title"` + URL string `json:"url"` } func init() { @@ -31,12 +33,69 @@ func init() { router.GET("/comics/random", randomComic) router.GET("/comics/monkeyuser", monkeyUserComic) + router.GET("/comics/xkcd", xkcdComic) spinhttp.Handle(router.ServeHTTP) } func randomComic(w http.ResponseWriter, re *http.Request, params httprouter.Params) { - monkeyUserComic(w, re, params) + possibleHandlers := []httprouter.Handle{ + xkcdComic, + monkeyUserComic, + } + + possibleHandlers[rand.IntN(len(possibleHandlers))](w, re, params) +} + +func xkcdComic(w http.ResponseWriter, re *http.Request, _ httprouter.Params) { + pageResponse, err := spinhttp.Get("https://c.xkcd.com/random/comic/") + if err != nil { + slog.Error("failed to fetch xkcd comic page", slog.String("err", err.Error())) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + slog.Info("Fetched xkcd comic page", slog.Int("status", pageResponse.StatusCode)) + + defer pageResponse.Body.Close() + if pageResponse.StatusCode != http.StatusOK { + http.Error(w, pageResponse.Status, pageResponse.StatusCode) + return + } + + comicDoc, err := goquery.NewDocumentFromReader(pageResponse.Body) + if err != nil { + slog.Error("failed to parse comic page", slog.String("err", err.Error())) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + slog.Info("Extracting comic info") + + ref := Comic{ + Source: "xkcd", + Title: comicDoc.Find("#ctitle").Text(), + URL: comicDoc.Find("#comic > img").AttrOr("src", ""), + } + + parsed, err := url.Parse(ref.URL) + if err != nil { + slog.Error("failed to parse comic URL", slog.String("err", err.Error())) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if parsed.Scheme == "" { + parsed.Scheme = "https" + ref.URL = parsed.String() + } + + w.Header().Add(contentTypeHeader, "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(ref); err != nil { + slog.Error("failed to encode image ref", slog.String("err", err.Error())) + http.Error(w, err.Error(), http.StatusInternalServerError) + } } func monkeyUserComic(w http.ResponseWriter, re *http.Request, _ httprouter.Params) { @@ -86,21 +145,11 @@ func monkeyUserComic(w http.ResponseWriter, re *http.Request, _ httprouter.Param } ref := Comic{ - Title: entry.Title, + Source: "monkeyuser", + Title: entry.Title, + URL: fmt.Sprintf("https://www.monkeyuser.com%s", comicDoc.Find("div.content > p > img").AttrOr("src", "")), } - comicDoc.Find("div.content > p > img").Each(func(_ int, s *goquery.Selection) { - for _, node := range s.Nodes { - for _, attr := range node.Attr { - if attr.Key == "src" { - ref.URL = fmt.Sprintf("https://www.monkeyuser.com%s", attr.Val) - break - } - } - slog.Info("found URL", slog.String("url", ref.URL)) - } - }) - w.Header().Add(contentTypeHeader, "application/json") w.WriteHeader(http.StatusOK) if err := json.NewEncoder(w).Encode(ref); err != nil { diff --git a/comics/spin.toml b/comics/spin.toml index f867fc1..0f08b2d 100644 --- a/comics/spin.toml +++ b/comics/spin.toml @@ -12,7 +12,7 @@ component = "comics" [component.comics] source = "main.wasm" -allowed_outbound_hosts = [ "https://www.monkeyuser.com" ] +allowed_outbound_hosts = [ "https://www.monkeyuser.com", "https://c.xkcd.com" ] [component.comics.build] command = "tinygo build -target=wasip1 -gc=leaking -no-debug -scheduler=none -buildmode=c-shared -o main.wasm main.go" watch = ["**/*.go", "go.mod"]