diff --git a/flake.lock b/flake.lock index aad7ab2..d0b6d2f 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1776169885, - "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", + "lastModified": 1781577229, + "narHash": "sha256-lrp67w8AulE9Ks53n27I45ADSzbOCn4H+CNW1Ck8B+8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", + "rev": "567a49d1913ce81ac6e9582e3553dd90a955875f", "type": "github" }, "original": { diff --git a/justfile b/justfile index aba581e..da54f8e 100644 --- a/justfile +++ b/justfile @@ -44,7 +44,6 @@ update-sql: prepare-db docker-build: podman build . --network host -t "eater" - # start docker compose with --build compose-up: docker-build podman compose up diff --git a/src/eater/backend/smtp.gleam b/src/eater/backend/smtp.gleam index 0a018e2..4c3bc38 100644 --- a/src/eater/backend/smtp.gleam +++ b/src/eater/backend/smtp.gleam @@ -78,13 +78,36 @@ pub fn feed_update_to_email( feed.RssUpdate(new:, channel:) -> #( channel.title, new.title, - new.description, + case new.description { + option.Some(description) -> [html.text(description)] + option.None -> [ + html.text( + " +This RSS item doesn't have a description associated with it. + +If you think this is a problem with eater, please open an issue on ", + ), + html.a( + [ + attribute.href( + "https://tangled.org/ollie.earth/eater/issues/new ", + ), + ], + [html.text("tangled")], + ), + html.text("or send me an email at "), + html.a([attribute.href("mailto:eater@ollie.earth")], [ + html.text("eater@ollie.earth"), + ]), + html.text(". Thank you!"), + ] + }, new.link, ) feed.AtomUpdate(new:, feed:) -> #( feed.title, new.title, - new.summary, + [html.text(new.summary)], new.link, ) } @@ -98,7 +121,7 @@ pub fn feed_update_to_email( html.html([], [ html.body([], [ html.h1([], [html.text(update_title)]), - html.p([], [html.text(description)]), + html.p([], description), html.p([], [ html.a([attribute.href(link)], [ html.text("Open article"), diff --git a/src/eater/feed.gleam b/src/eater/feed.gleam index 48b437f..a8a43ac 100644 --- a/src/eater/feed.gleam +++ b/src/eater/feed.gleam @@ -64,7 +64,17 @@ pub type Channel { } pub type FeedDetails { - RssChannelDetails(title: String, link: String, description: String) + /// `title`, `link` and `description` are required + /// by the rss 2.0 standard + /// + /// But since it is quite common for feeds to be missing the `description` field, + /// we've made it optional. So we may still support these feeds in some capacity. + /// + RssChannelDetails( + title: String, + link: String, + description: option.Option(String), + ) AtomDetails(title: String, link: String, updated: timestamp.Timestamp) } @@ -72,7 +82,11 @@ fn rss_channel_decoder() -> decode.Decoder(Channel) { use title <- decode.subfield(["title", "$text"], decode.string) use link <- link_decoder() - use description <- decode.subfield(["description", "$text"], decode.string) + use description <- decode.then(decode.optionally_at( + ["description", "$text"], + option.None, + decode.optional(decode.string), + )) use items <- decode.field("item", decode.list(rss_item_decoder())) decode.success(Channel(RssChannelDetails(title:, link:, description:), items:)) } @@ -128,7 +142,10 @@ pub type Item { /// `title`, `link` and `description` are required /// by the rss 2.0 standard /// - RssItem(title: String, link: String, description: String) + /// But since it is quite common for feeds to be missing the `description` field, + /// we've made it optional. So we may still support these feeds in some capacity. + /// + RssItem(title: String, link: String, description: option.Option(String)) } pub type Entry { @@ -145,7 +162,13 @@ pub type Entry { fn rss_item_decoder() -> decode.Decoder(Item) { use title <- decode.subfield(["title", "$text"], decode.string) use link <- decode.subfield(["link", "$text"], decode.string) - use description <- decode.subfield(["description", "$text"], decode.string) + + use description <- decode.then(decode.optionally_at( + ["description", "$text"], + option.None, + decode.optional(decode.string), + )) + decode.success(RssItem(title:, link:, description:)) } diff --git a/test/eater_test.gleam b/test/eater_test.gleam index bd5f34d..00809e5 100644 --- a/test/eater_test.gleam +++ b/test/eater_test.gleam @@ -111,3 +111,64 @@ pub fn use_first_link_test() { let assert Ok("some-link") = xml.parse(text, feed.link_decoder(decode.success)) } + +pub fn rss_channel_missing_description_test() { + // snippet from https://coding.social/blog/rss.xml as of 26.06.2026 + let input = + " + + Social coding commons | Blog + https://coding.social/ + en + + Fediverse of dreams + https://coding.social/blog/fediverse-of-dreams/ + https://coding.social/blog/fediverse-of-dreams/ + Thu, 21 May 2026 00:00:00 GMT +

Invest in your dreams

+
+
+ + Grassroots fediverse evolution + https://coding.social/blog/grassroots-evolution/ + https://coding.social/blog/grassroots-evolution/ + Fri, 10 Apr 2026 00:00:00 GMT +

The ActivityPub fediverse is doing great, right?

+
+
+
+
" + + let assert Ok(_) = xml.parse(input, feed.rss_decoder()) +} + +pub fn rss_item_missing_description_test() { + // snippet from https://coding.social/blog/rss.xml as of 26.06.2026 + let input = + " + + Social coding commons | Blog + + https://coding.social/ + en + + Fediverse of dreams + https://coding.social/blog/fediverse-of-dreams/ + https://coding.social/blog/fediverse-of-dreams/ + Thu, 21 May 2026 00:00:00 GMT +

Invest in your dreams

+
+
+ + Grassroots fediverse evolution + https://coding.social/blog/grassroots-evolution/ + https://coding.social/blog/grassroots-evolution/ + Fri, 10 Apr 2026 00:00:00 GMT +

The ActivityPub fediverse is doing great, right?

+
+
+
+
" + + let assert Ok(_) = xml.parse(input, feed.rss_decoder()) +}