diff --git a/app/pictures/anonymous.jpg b/app/pictures/anonymous.jpg new file mode 100644 index 0000000..128ca66 Binary files /dev/null and b/app/pictures/anonymous.jpg differ diff --git a/app/pictures/handler.go b/app/pictures/handler.go index 1873d09..79be4f1 100644 --- a/app/pictures/handler.go +++ b/app/pictures/handler.go @@ -2,22 +2,47 @@ package pictures import ( "brainbaking.com/go-jamming/db" + _ "embed" "github.com/gorilla/mux" + "github.com/rs/zerolog/log" "net/http" ) +//go:embed anonymous.jpg +var anonymous []byte + +const ( + Anonymous = "anonymous" +) + +func init() { + if anonymous == nil { + log.Fatal().Msg("embedded anonymous image missing?") + } +} + // Handle handles picture GET calls. // It does not validate the picture query as it's part of a composite key anyway. func Handle(repo db.MentionRepo) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { picDomain := mux.Vars(r)["picture"] + if picDomain == Anonymous { + servePicture(w, anonymous) + return + } + picData := repo.GetPicture(picDomain) if picData == nil { http.NotFound(w, r) return } - w.WriteHeader(http.StatusOK) - // TODO response headers? is this a jpeg, png, gif, webm? should we? - w.Write(picData) + servePicture(w, picData) } } + +// servePicture writes an OK and raw bytes. +// For some reason, headers - although they should be there - aren't needed? +func servePicture(w http.ResponseWriter, bytes []byte) { + w.WriteHeader(http.StatusOK) + w.Write(bytes) +} diff --git a/app/webmention/recv/receive.go b/app/webmention/recv/receive.go index df6675c..31d1079 100644 --- a/app/webmention/recv/receive.go +++ b/app/webmention/recv/receive.go @@ -2,6 +2,7 @@ package recv import ( "brainbaking.com/go-jamming/app/mf" + "brainbaking.com/go-jamming/app/pictures" "brainbaking.com/go-jamming/common" "brainbaking.com/go-jamming/db" "brainbaking.com/go-jamming/rest" @@ -97,16 +98,22 @@ func (recv *Receiver) parseBodyAsNonIndiewebSite(body string, wm mf.Mention) *mf } } +// saveAuthorPictureLocally tries to download the author picture. +// If it succeeds, it alters the picture path to a local /pictures/x one. +// If it fails, it falls back to a local default image. +// This *should* also validate image byte headers, like https://stackoverflow.com/questions/670546/determine-if-file-is-an-image func (recv *Receiver) saveAuthorPictureLocally(indieweb *mf.IndiewebData) { _, picData, err := recv.RestClient.GetBody(indieweb.Author.Picture) if err != nil { - log.Warn().Err(err).Str("url", indieweb.Author.Picture).Msg("Unable to download author picture. Ignoring.") + log.Warn().Err(err).Str("url", indieweb.Author.Picture).Msg("Unable to download author picture. Reverting to anonymous.") + indieweb.Author.Picture = fmt.Sprintf("/pictures/%s", pictures.Anonymous) return } srcDomain := rest.Domain(indieweb.Source) _, dberr := recv.Repo.SavePicture(picData, srcDomain) if dberr != nil { - log.Warn().Err(err).Str("url", indieweb.Author.Picture).Msg("Unable to save downloaded author picture. Ignoring.") + log.Warn().Err(err).Str("url", indieweb.Author.Picture).Msg("Unable to save downloaded author picture. Reverting to anonymous.") + indieweb.Author.Picture = fmt.Sprintf("/pictures/%s", pictures.Anonymous) return } diff --git a/app/webmention/recv/receive_test.go b/app/webmention/recv/receive_test.go index 1814838..3da7d11 100644 --- a/app/webmention/recv/receive_test.go +++ b/app/webmention/recv/receive_test.go @@ -22,6 +22,48 @@ var conf = &common.Config{ ConString: ":memory:", } +func TestSaveAuthorPictureLocally(t *testing.T) { + cases := []struct { + label string + pictureUrl string + expectedPictureUrl string + }{ + { + "Absolute URL gets 'downloaded' and replaced by relative", + "https://brainbaking.com/picture.jpg", + "/pictures/brainbaking.com", + }, + { + "Absolute URL gets replaced by anonymous if download fails", + "https://brainbaking.com/thedogatemypic-nowitsmissing-shiii.png", + "/pictures/anonymous", + }, + } + + for _, tc := range cases { + t.Run(tc.label, func(t *testing.T) { + repo := db.NewMentionRepo(conf) + recv := &Receiver{ + Conf: conf, + Repo: repo, + RestClient: &mocks.RestClientMock{ + GetBodyFunc: mocks.RelPathGetBodyFunc(t, "../../../mocks/"), + }, + } + + indieweb := &mf.IndiewebData{ + Source: "https://brainbaking.com", + Author: mf.IndiewebAuthor{ + Picture: tc.pictureUrl, + }, + } + recv.saveAuthorPictureLocally(indieweb) + + assert.Equal(t, tc.expectedPictureUrl, indieweb.Author.Picture) + }) + } +} + func TestReceive(t *testing.T) { cases := []struct { label string diff --git a/mocks/restclient.go b/mocks/restclient.go index 517c67d..3bc2432 100644 --- a/mocks/restclient.go +++ b/mocks/restclient.go @@ -50,7 +50,7 @@ func RelPathGetBodyFunc(t *testing.T, relPath string) func(string) (http.Header, mockfile := relPath + strings.ReplaceAll(url, "https://brainbaking.com/", "") html, err := ioutil.ReadFile(mockfile) if err != nil { - t.Error(err) + return nil, "", err } headerData, headerFileErr := ioutil.ReadFile(strings.ReplaceAll(mockfile, ".html", "-headers.json"))