From 7fb6654f1b098f6c2f20770facd128f46fdbbdde Mon Sep 17 00:00:00 2001
From: Damien Le Berrigaud
Date: Thu, 15 Feb 2024 12:45:35 -0700
Subject: [PATCH] Parse and display media from Mastodon RSS
---
Damo.Io.Server/src/Article.fs | 11 +++++++++-
Damo.Io.Server/src/ArticleListTemplate.fs | 3 +--
Damo.Io.Server/src/ArticleTemplate.fs | 20 ++++++++++++++-----
Damo.Io.Server/src/ArticlesRepository.fs | 7 ++++---
Damo.Io.Server/src/FeedsProcessor.fs | 5 ++---
Damo.Io.Server/www/styles/app.css | 20 ++++++++++++++++---
FeedsProcessing.Tests/src/XmlTests.fs | 18 ++++++++++++++++-
.../resources/samples/rss.sample.xml | 11 +++++++++-
FeedsProcessing/src/Article.fs | 7 ++++++-
FeedsProcessing/src/Xml.fs | 16 +++++++++++++--
10 files changed, 96 insertions(+), 22 deletions(-)
diff --git a/Damo.Io.Server/src/Article.fs b/Damo.Io.Server/src/Article.fs
index 2b59985..af24886 100644
--- a/Damo.Io.Server/src/Article.fs
+++ b/Damo.Io.Server/src/Article.fs
@@ -1,11 +1,20 @@
module DamoIoServer.Article
+open FeedsProcessing.Article
open Time
open DamoIoServer.Source
-type Article =
+type MediaRecord = { Url: string; Description: string }
+
+module MediaRecord =
+ let ofMedia (media: Media) =
+ { Url = media.Url
+ Description = media.Description }
+
+type ArticleRecord =
{ Title: string option
Link: string option
Content: string
+ Media: MediaRecord option
Date: Posix option
Source: Source }
diff --git a/Damo.Io.Server/src/ArticleListTemplate.fs b/Damo.Io.Server/src/ArticleListTemplate.fs
index 943f877..7cb37e0 100644
--- a/Damo.Io.Server/src/ArticleListTemplate.fs
+++ b/Damo.Io.Server/src/ArticleListTemplate.fs
@@ -41,10 +41,9 @@ let private sourceLink selectedSources source =
li [] [ a attrs [ str (Source.toString source) ] ]
-let render (articles: Article list) (sources: Source list) : XmlNode =
+let render (articles: ArticleRecord list) (sources: Source list) : XmlNode =
let sourceLinks = Source.all |> List.map (sourceLink sources)
let articleList = articles |> List.map ArticleTemplate.render
-
let logo = h1 [] [ str "damo.io" ]
let menu = ul [ _class "main-menu" ] sourceLinks
diff --git a/Damo.Io.Server/src/ArticleTemplate.fs b/Damo.Io.Server/src/ArticleTemplate.fs
index 35283e2..f4fdfc0 100644
--- a/Damo.Io.Server/src/ArticleTemplate.fs
+++ b/Damo.Io.Server/src/ArticleTemplate.fs
@@ -31,7 +31,7 @@ let private dateToString posix =
open Giraffe.ViewEngine
-let private articleTitle (article: Article) title =
+let private articleTitle (article: ArticleRecord) title =
match article.Link with
| Some link -> h1 [] [ a [ _href link ] [ str title ] ]
| None -> h1 [] [ str title ]
@@ -39,7 +39,7 @@ let private articleTitle (article: Article) title =
let private articleDate (date: Posix) =
h2 [ _class "date" ] [ str (dateToString date) ]
-let private articleHeader (article: Article) =
+let private articleHeader (article: ArticleRecord) =
header
[]
(List.choose
@@ -47,12 +47,18 @@ let private articleHeader (article: Article) =
[ Option.map (articleTitle article) article.Title
Option.map articleDate article.Date ])
-let private trySourceLink (article: Article) : XmlNode option =
+let private trySourceLink (article: ArticleRecord) : XmlNode option =
match (article.Title, article.Link) with
| None, Some url -> Some(nav [] [ a [ _href url; _target "_blank" ] [ str "Source" ] ])
| _, _ -> None
-let render (article: Article) : XmlNode =
+let private renderMedia (media: MediaRecord) : XmlNode =
+ figure
+ []
+ [ img [ _src media.Url; _alt media.Description ]
+ figcaption [] [ str media.Description ] ]
+
+let render (article: ArticleRecord) : XmlNode =
let articleHeader = articleHeader article
let articleContent = section [] [ rawText article.Content ]
let maybeSourceLink = trySourceLink article
@@ -62,6 +68,10 @@ let render (article: Article) : XmlNode =
[ yield articleHeader
yield articleContent
+ match article.Media with
+ | Some media -> yield renderMedia media
+ | None -> ()
+
match maybeSourceLink with
| Some sourceLink -> yield sourceLink
- | _ -> () ]
+ | None -> () ]
diff --git a/Damo.Io.Server/src/ArticlesRepository.fs b/Damo.Io.Server/src/ArticlesRepository.fs
index 432014d..c95e5c0 100644
--- a/Damo.Io.Server/src/ArticlesRepository.fs
+++ b/Damo.Io.Server/src/ArticlesRepository.fs
@@ -40,18 +40,19 @@ let private aboutContent =
"""
-let private about: Article =
+let private about: ArticleRecord =
{ Title = Some "About"
Link = None
Content = aboutContent
+ Media = None
Date = None
Source = About }
-let mutable private allRecords: Article list = []
+let mutable private allRecords: ArticleRecord list = []
let findAll () = about :: allRecords
-type FindAllBySources = Source list -> Article list
+type FindAllBySources = Source list -> ArticleRecord list
let findAllBySources: FindAllBySources =
fun sources -> findAll () |> List.filter (fun r -> List.contains r.Source sources)
diff --git a/Damo.Io.Server/src/FeedsProcessor.fs b/Damo.Io.Server/src/FeedsProcessor.fs
index 6552a6a..ffbe7c6 100644
--- a/Damo.Io.Server/src/FeedsProcessor.fs
+++ b/Damo.Io.Server/src/FeedsProcessor.fs
@@ -1,7 +1,7 @@
module DamoIoServer.FeedsProcessor
+open DamoIoServer.Article
open FSharp.Control
-open FeedsProcessing
open FeedsProcessing.Article
open FeedsProcessing.DataGateway
open FeedsProcessing.Feeds
@@ -9,12 +9,11 @@ open FeedsProcessing.ProcessingResult
open FeedsProcessing.Xml
open Microsoft.Extensions.Logging
-type ArticleRecord = DamoIoServer.Article.Article
-
let private articleToRecord sourceType (article: Article) : ArticleRecord =
{ Title = Article.title article
Link = Article.link article
Content = Article.content article
+ Media = Article.media article |> Option.map MediaRecord.ofMedia
Date = Article.date article
Source = sourceType }
diff --git a/Damo.Io.Server/www/styles/app.css b/Damo.Io.Server/www/styles/app.css
index 9403994..fe0e4cc 100644
--- a/Damo.Io.Server/www/styles/app.css
+++ b/Damo.Io.Server/www/styles/app.css
@@ -156,7 +156,7 @@ pre {
background-color: var(--code-bg-color);
padding: 2.4rem;
margin-bottom: 2.4rem;
- border-radius: 1rem;
+ border-radius: .5rem;
font-family: var(--monospace);
font-size: 1.4rem;
@@ -188,12 +188,26 @@ p {
em, strong {
font-weight: 700;
}
+
+figure {
+ margin: 0;
+
+ img {
+ border-radius: .5rem;
+ }
+
+ figcaption {
+ font-size: 1.4rem;
+ text-align: center;
+ font-style: italic;
+ }
+}
article {
background-color: var(--card-color);
padding: 2.4rem 4.8rem;
margin: 0 0 2.4rem 0;
- border-radius: 3rem;
+ border-radius: 1rem;
box-shadow: var(--card-shadow);
}
@@ -230,7 +244,7 @@ article section {
article.Social {
background-image: url('data:image/svg+xml;utf8,%3Csvg%20height%3D%222500%22%20width%3D%222331%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%22-0.41%200.22%20747.62%20801.4499999999999%22%3E%3Cpath%20d%3D%22M729.94%20479.5c-10.96%2056.4-98.17%20118.12-198.34%20130.08-52.23%206.23-103.66%2011.96-158.49%209.44-89.68-4.1-160.45-21.4-160.45-21.4%200%208.73.54%2017.04%201.62%2024.81%2011.66%2088.52%2087.76%2093.82%20159.84%2096.29%2072.76%202.49%20137.55-17.94%20137.55-17.94l2.99%2065.79s-50.89%2027.32-141.55%2032.35c-50%202.75-112.07-1.26-184.37-20.39C31.94%20737.02%204.97%20569.86.85%20400.26-.41%20349.9.37%20302.42.37%20262.7.37%2089.27%20113.99%2038.44%20113.99%2038.44%20171.28%2012.12%20269.59%201.06%20371.79.22h2.52c102.19.84%20200.57%2011.9%20257.86%2038.22%200%200%20113.62%2050.83%20113.62%20224.26%200%200%201.42%20127.96-15.85%20216.8%22%20fill%3D%22%23888%22%2F%3E%3Cpath%20d%3D%22M611.77%20276.16v209.99h-83.2V282.33c0-42.97-18.07-64.77-54.23-64.77-39.98%200-60.01%2025.86-60.01%2077.02v111.57h-82.71V294.58c0-51.16-20.04-77.02-60.01-77.02-36.16%200-54.24%2021.8-54.24%2064.77v203.82h-83.19V276.16c0-42.92%2010.93-77.03%2032.88-102.26%2022.63-25.23%2052.27-38.17%2089.07-38.17%2042.57%200%2074.81%2016.37%2096.12%2049.1l20.72%2034.74%2020.73-34.74c21.31-32.73%2053.55-49.1%2096.12-49.1%2036.79%200%2066.44%2012.94%2089.07%2038.17%2021.95%2025.23%2032.88%2059.34%2032.88%20102.26z%22%20fill%3D%22%23fff%22%2F%3E%3C%2Fsvg%3E');
background-repeat: no-repeat;
- background-position: 1.6rem 1.6rem;
+ background-position: 4.8rem 2.4rem;
background-size: 4.8rem 4.8rem;
}
diff --git a/FeedsProcessing.Tests/src/XmlTests.fs b/FeedsProcessing.Tests/src/XmlTests.fs
index 59ebb6c..96dd998 100644
--- a/FeedsProcessing.Tests/src/XmlTests.fs
+++ b/FeedsProcessing.Tests/src/XmlTests.fs
@@ -116,8 +116,24 @@ let ``with mastodon RSS`` () =
match result with
| Error _ -> Assert.Fail "Expected success"
- | Ok records -> List.length records |> should equal 19
+ | Ok records ->
+ List.length records |> should equal 19
+
+ let mediaAt index =
+ records |> List.item index |> Article.media
+
+ let expectedMediaNoDescription =
+ { Url = "https://mastodon.kleph.eu/system/media_attachments/files/000/055/839/original/4874168bf454bddb.jpg"
+ Description = "" }
+
+ mediaAt 17 |> should equal (Some expectedMediaNoDescription)
+
+ let expectedMediaAndDescription =
+ { Url =
+ "https://mastodon.kleph.eu/system/media_attachments/files/108/207/041/262/751/249/original/1e8a0ccac5f59165.jpg"
+ Description = "Mountain landscape at sunset with bicycle on the foreground." }
+ mediaAt 13 |> should equal (Some expectedMediaAndDescription)
[]
let ``processFeed with slashdot RDF XML`` () =
diff --git a/FeedsProcessing/resources/samples/rss.sample.xml b/FeedsProcessing/resources/samples/rss.sample.xml
index 0931b3b..bd5b86b 100644
--- a/FeedsProcessing/resources/samples/rss.sample.xml
+++ b/FeedsProcessing/resources/samples/rss.sample.xml
@@ -1,5 +1,7 @@
-
@@ -23,6 +25,10 @@
Tue, 20 Sep 2016 12:54:44 GMT
2016-09-20T12:54:44.561Z
This is the content in encoded tag]]>
+
+ nonadult
+ Mountain landscape at sunset with bicycle on the foreground.
+
-
@@ -32,6 +38,9 @@
Tue, 20 Sep 2016 12:54:44 GMT
2016-09-20T12:54:44.561Z
This is the content in description tag]]>
+
+ nonadult
+
-
diff --git a/FeedsProcessing/src/Article.fs b/FeedsProcessing/src/Article.fs
index 0dd17bc..829091c 100644
--- a/FeedsProcessing/src/Article.fs
+++ b/FeedsProcessing/src/Article.fs
@@ -13,18 +13,23 @@ and private Fields =
{ Title: string option
Link: string option
Content: string
+ Media: Media option
Date: Posix option }
+and Media = { Url: string; Description: string }
+
[]
module Article =
let title (Article fields) = fields.Title
let link (Article fields) = fields.Link
let content (Article fields) = fields.Content
+ let media (Article fields) = fields.Media
let date (Article fields) = fields.Date
- let create title link content date =
+ let create title link content media date =
Article
{ Title = title |> Option.bind stringToOption
Link = stringToOption link
Content = content |> Option.bind stringToOption |> Option.defaultValue "" |> Html.sanitize
+ Media = media
Date = Option.map Posix.fromDateTimeOffset date }
diff --git a/FeedsProcessing/src/Xml.fs b/FeedsProcessing/src/Xml.fs
index e24e558..5f553b3 100644
--- a/FeedsProcessing/src/Xml.fs
+++ b/FeedsProcessing/src/Xml.fs
@@ -39,8 +39,19 @@ module private Rss =
type private RssProvider = XmlProvider<"../FeedsProcessing/resources/samples/rss.sample.xml">
+ let private descriptionToString (description: RssProvider.Description) = description.Value
+
+ let private contentToMedia (content: RssProvider.Content) =
+ { Url = content.Url
+ Description = content.Description |> Option.map descriptionToString |> Option.defaultValue "" }
+
let private itemToArticle (item: RssProvider.Item) =
- Article.create item.Title item.Link (item.Encoded |> Option.orElse item.Description) (Some item.PubDate)
+ Article.create
+ item.Title
+ item.Link
+ (item.Encoded |> Option.orElse item.Description)
+ (item.Content |> Option.map contentToMedia)
+ (Some item.PubDate)
let private toArticles (rss: RssProvider.Rss) =
Try.value "Rss to articles" (fun _ -> rss.Channel.Items |> Seq.map itemToArticle |> Seq.toList)
@@ -66,6 +77,7 @@ module private Atom =
(Some entry.Title.Value)
(entry.Links |> Array.head |> (fun l -> l.Href))
(Some entry.Content.Value)
+ None
(Some entry.Published)
let private toArticles (atom: AtomProvider.Feed) =
@@ -94,7 +106,7 @@ module private Rdf =
type private RdfProvider = XmlProvider<"../FeedsProcessing/resources/samples/rdf.sample.xml">
let private itemToArticle (item: RdfProvider.Item) =
- Article.create (Some item.Title) item.Link (Some item.Description) (Some item.Date)
+ Article.create (Some item.Title) item.Link (Some item.Description) None (Some item.Date)
let private toArticles (rdf: RdfProvider.Rdf) : Result =
Try.result