package main import ( "encoding/json" "fmt" "log/slog" "math/rand/v2" "net/http" "net/url" "github.com/PuerkitoBio/goquery" spinhttp "github.com/fermyon/spin/sdk/go/v2/http" "github.com/julienschmidt/httprouter" ) const ( contentTypeHeader = "Content-Type" ) type MonkeyUserEntry struct { Title string `json:"title"` URL string `json:"url"` } type Comic struct { Source string `json:"source"` Title string `json:"title"` URL string `json:"url"` } func init() { router := spinhttp.NewRouter() 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) { 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) { indexResponse, err := spinhttp.Get("https://www.monkeyuser.com/index.json") if err != nil { slog.Error("failed to fetch MonkeyUser index", slog.String("err", err.Error())) http.Error(w, err.Error(), http.StatusInternalServerError) return } slog.Info("Fetched MonkeyUser index") defer indexResponse.Body.Close() if indexResponse.StatusCode != http.StatusOK { w.WriteHeader(indexResponse.StatusCode) return } var entries []MonkeyUserEntry if err := json.NewDecoder(indexResponse.Body).Decode(&entries); err != nil { slog.Error("failed to decode MonkeyUser index", slog.String("err", err.Error())) http.Error(w, err.Error(), http.StatusInternalServerError) return } slog.Info("Decoded MonkeyUser index") entry := entries[rand.IntN(len(entries))] pageResponse, err := spinhttp.Get(fmt.Sprintf("https://www.monkeyuser.com%s", entry.URL)) if err != nil { slog.Error("failed to fetch monkeyuser comic page", slog.String("err", err.Error())) http.Error(w, err.Error(), http.StatusInternalServerError) return } defer pageResponse.Body.Close() if pageResponse.StatusCode != http.StatusOK { w.WriteHeader(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 } ref := Comic{ Source: "monkeyuser", Title: entry.Title, URL: fmt.Sprintf("https://www.monkeyuser.com%s", comicDoc.Find("div.content > p > img").AttrOr("src", "")), } 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) } }