fix atom and rss feeds for users and tags

Changes:
  - make the XML closer to spec (RSS does not pass w3c's validator, but works)
  - fix dates (RFC3339 for Atom, doc says RFC822 for RSS but RFC1123 is closer)
  - fix attachment/enclosure links (but see below)
  - set feed item title to post's "summary" if present
  - pruned several elements that validators did not like
    - examples: ap_enabled, user banner urls.

Specs:
  - https://www.rssboard.org/rss-specification
  - https://validator.w3.org/feed/docs/atom.html
  - https://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared

Validators:
  - https://validator.w3.org/feed/
  - https://rssatom.com/feedvalidator.php

Attachment/enclosure links should have a "length" field (mandatory
according to the spec).  This is not present in the object's data
map.
This commit is contained in:
faried nawaz 2022-11-14 01:50:52 +05:00
parent 8db82932a7
commit 3f0783c0a5
No known key found for this signature in database
GPG key ID: C8EBC3669D3539B4
10 changed files with 162 additions and 119 deletions

View file

@ -21,7 +21,7 @@ def pub_date(date) when is_binary(date) do
|> pub_date
end
def pub_date(%DateTime{} = date), do: Timex.format!(date, "{RFC822}")
def pub_date(%DateTime{} = date), do: to_rfc1123(date)
def prepare_activity(activity, opts \\ []) do
object = Object.normalize(activity, fetch: false)
@ -41,13 +41,18 @@ def prepare_activity(activity, opts \\ []) do
def most_recent_update(activities) do
with %{updated_at: updated_at} <- List.first(activities) do
NaiveDateTime.to_iso8601(updated_at)
to_rfc3339(updated_at)
end
end
def most_recent_update(activities, user) do
def most_recent_update(activities, user, :atom) do
(List.first(activities) || user).updated_at
|> NaiveDateTime.to_iso8601()
|> to_rfc3339()
end
def most_recent_update(activities, user, :rss) do
(List.first(activities) || user).updated_at
|> to_rfc1123()
end
def feed_logo do
@ -61,6 +66,10 @@ def feed_logo do
|> MediaProxy.url()
end
def email(user) do
user.nickname <> "@" <> Pleroma.Web.Endpoint.host()
end
def logo(user) do
user
|> User.avatar_url()
@ -69,18 +78,35 @@ def logo(user) do
def last_activity(activities), do: List.last(activities)
def activity_title(%{"content" => content}, opts \\ %{}) do
content
def activity_title(%{"content" => content, "summary" => summary} = data, opts \\ %{}) do
title =
cond do
summary != "" -> summary
content != "" -> activity_content(data)
true -> "a post"
end
title
|> Pleroma.Web.Metadata.Utils.scrub_html()
|> Pleroma.Emoji.Formatter.demojify()
|> Formatter.truncate(opts[:max_length], opts[:omission])
|> escape()
end
def activity_description(data) do
content = activity_content(data)
summary = data["summary"]
cond do
content != "" -> escape(content)
summary != "" -> escape(summary)
true -> escape(data["type"])
end
end
def activity_content(%{"content" => content}) do
content
|> String.replace(~r/[\n\r]/, "")
|> escape()
end
def activity_content(_), do: ""
@ -112,4 +138,35 @@ def escape(html) do
|> html_escape()
|> safe_to_string()
end
@spec to_rfc3339(String.t() | NativeDateTime.t()) :: String.t()
def to_rfc3339(date) when is_binary(date) do
date
|> Timex.parse!("{ISO:Extended}")
|> to_rfc3339()
end
def to_rfc3339(nd) do
nd
|> Timex.to_datetime()
|> Timex.format!("{RFC3339}")
end
@spec to_rfc1123(String.t() | DateTime.t() | NativeDateTime.t()) :: String.t()
def to_rfc1123(datestr) when is_binary(datestr) do
datestr
|> Timex.parse!("{ISO:Extended}")
|> to_rfc1123()
end
def to_rfc1123(%DateTime{} = date) do
date
|> Timex.format!("{RFC1123}")
end
def to_rfc1123(nd) do
nd
|> Timex.to_datetime()
|> Timex.format!("{RFC1123}")
end
end

View file

@ -3,15 +3,15 @@
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<id><%= @data["id"] %></id>
<title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
<content type="html"><%= activity_content(@data) %></content>
<published><%= @activity.data["published"] %></published>
<updated><%= @activity.data["published"] %></updated>
<content type="html"><%= activity_description(@data) %></content>
<published><%= to_rfc3339(@activity.data["published"]) %></published>
<updated><%= to_rfc3339(@activity.data["published"]) %></updated>
<ostatus:conversation ref="<%= activity_context(@activity) %>">
<%= activity_context(@activity) %>
</ostatus:conversation>
<link href="<%= activity_context(@activity) %>" rel="ostatus:conversation"/>
<%= if @data["summary"] do %>
<%= if @data["summary"] != "" do %>
<summary><%= escape(@data["summary"]) %></summary>
<% end %>

View file

@ -3,17 +3,12 @@
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<guid><%= @data["id"] %></guid>
<title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
<description><%= activity_content(@data) %></description>
<pubDate><%= @activity.data["published"] %></pubDate>
<updated><%= @activity.data["published"] %></updated>
<description><%= activity_description(@data) %></description>
<pubDate><%= to_rfc1123(@activity.data["published"]) %></pubDate>
<ostatus:conversation ref="<%= activity_context(@activity) %>">
<%= activity_context(@activity) %>
</ostatus:conversation>
<%= if @data["summary"] do %>
<description><%= escape(@data["summary"]) %></description>
<% end %>
<%= if @activity.local do %>
<link><%= @data["id"] %></link>
<% else %>
@ -27,7 +22,7 @@
<% end %>
<%= for attachment <- @data["attachment"] || [] do %>
<link type="<%= attachment_type(attachment) %>"><%= attachment_href(attachment) %></link>
<enclosure url="<%= attachment_href(attachment) %>" type="<%= attachment_type(attachment) %>" />
<% end %>
<%= if @data["inReplyTo"] do %>

View file

@ -1,17 +1,14 @@
<author>
<id><%= @user.ap_id %></id>
<activity:object>http://activitystrea.ms/schema/1.0/person</activity:object>
<uri><%= @user.ap_id %></uri>
<name><%= @user.nickname %></name>
<activity:object>http://activitystrea.ms/schema/1.0/person</activity:object>
<activity:displayName><%= @user.name %></activity:displayName>
<activity:image><%= User.avatar_url(@user) %></activity:image>
<activity:id><%= @user.ap_id %></activity:id>
<activity:published><%= to_rfc3339(@user.inserted_at) %></activity:published>
<activity:updated><%= to_rfc3339(@user.updated_at) %></activity:updated>
<activity:url><%= @user.ap_id %></activity:url>
<poco:preferredUsername><%= @user.nickname %></poco:preferredUsername>
<poco:displayName><%= @user.name %></poco:displayName>
<poco:note><%= escape(@user.bio) %></poco:note>
<summary><%= escape(@user.bio) %></summary>
<name><%= @user.nickname %></name>
<link rel="avatar" href="<%= User.avatar_url(@user) %>"/>
<%= if User.banner_url(@user) do %>
<link rel="header" href="<%= User.banner_url(@user) %>"/>
<% end %>
<%= if @user.local do %>
<ap_enabled>true</ap_enabled>
<% end %>
</author>

View file

@ -1,17 +1,10 @@
<managingEditor>
<guid><%= @user.ap_id %></guid>
<activity:object>http://activitystrea.ms/schema/1.0/person</activity:object>
<uri><%= @user.ap_id %></uri>
<poco:preferredUsername><%= @user.nickname %></poco:preferredUsername>
<poco:displayName><%= @user.name %></poco:displayName>
<poco:note><%= escape(@user.bio) %></poco:note>
<description><%= escape(@user.bio) %></description>
<name><%= @user.nickname %></name>
<link rel="avatar"><%= User.avatar_url(@user) %></link>
<%= if User.banner_url(@user) do %>
<link rel="header"><%= User.banner_url(@user) %></link>
<% end %>
<%= if @user.local do %>
<ap_enabled>true</ap_enabled>
<% end %>
</managingEditor>
<managingEditor><%= "#{email(@user)} (#{escape(@user.name)})" %></managingEditor>
<activity:object>http://activitystrea.ms/schema/1.0/person</activity:object>
<activity:displayName><%= @user.name %></activity:displayName>
<activity:image><%= User.avatar_url(@user) %></activity:image>
<activity:id><%= @user.ap_id %></activity:id>
<activity:published><%= to_rfc3339(@user.inserted_at) %></activity:published>
<activity:updated><%= to_rfc3339(@user.updated_at) %></activity:updated>
<poco:preferredUsername><%= @user.nickname %></poco:preferredUsername>
<poco:displayName><%= @user.name %></poco:displayName>
<poco:note><%= escape(@user.bio) %></poco:note>

View file

@ -1,12 +1,22 @@
<entry>
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<%= render Phoenix.Controller.view_module(@conn), "_tag_author.atom", assigns %>
<id><%= @data["id"] %></id>
<title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
<content type="html"><%= activity_content(@data) %></content>
<id><%= @data["id"] %></id>
<title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
<content type="html"><%= activity_description(@data) %></content>
<published><%= to_rfc3339(@activity.data["published"]) %></published>
<updated><%= to_rfc3339(@activity.data["published"]) %></updated>
<ostatus:conversation ref="<%= activity_context(@activity) %>">
<%= activity_context(@activity) %>
</ostatus:conversation>
<link href="<%= activity_context(@activity) %>" rel="ostatus:conversation"/>
<%= if @data["summary"] != "" do %>
<summary><%= @data["summary"] %></summary>
<% end %>
<%= if @activity.local do %>
<link type="application/atom+xml" href='<%= @data["id"] %>' rel="self"/>
@ -15,37 +25,25 @@
<link type="text/html" href='<%= @data["external_url"] %>' rel="alternate"/>
<% end %>
<published><%= @activity.data["published"] %></published>
<updated><%= @activity.data["published"] %></updated>
<ostatus:conversation ref="<%= activity_context(@activity) %>">
<%= activity_context(@activity) %>
</ostatus:conversation>
<link href="<%= activity_context(@activity) %>" rel="ostatus:conversation"/>
<%= if @data["summary"] do %>
<summary><%= @data["summary"] %></summary>
<% end %>
<%= for id <- @activity.recipients do %>
<%= if id == Pleroma.Constants.as_public() do %>
<%= for id <- @activity.recipients do %>
<%= if id == Pleroma.Constants.as_public() do %>
<link rel="mentioned"
ostatus:object-type="http://activitystrea.ms/schema/1.0/collection"
href="http://activityschema.org/collection/public"/>
<% else %>
<%= unless Regex.match?(~r/^#{Pleroma.Web.Endpoint.url()}.+followers$/, id) do %>
<link rel="mentioned"
ostatus:object-type="http://activitystrea.ms/schema/1.0/collection"
href="http://activityschema.org/collection/public"/>
<% else %>
<%= unless Regex.match?(~r/^#{Pleroma.Web.Endpoint.url()}.+followers$/, id) do %>
<link rel="mentioned"
ostatus:object-type="http://activitystrea.ms/schema/1.0/person"
href="<%= id %>" />
<% end %>
ostatus:object-type="http://activitystrea.ms/schema/1.0/person"
href="<%= id %>" />
<% end %>
<% end %>
<% end %>
<%= for tag <- Pleroma.Object.hashtags(@object) do %>
<category term="<%= tag %>"></category>
<% end %>
<%= for tag <- Pleroma.Object.hashtags(@object) do %>
<category term="<%= tag %>"></category>
<% end %>
<%= for {emoji, file} <- @data["emoji"] || %{} do %>
<link name="<%= emoji %>" rel="emoji" href="<%= file %>"/>
<% end %>
<%= for {emoji, file} <- @data["emoji"] || %{} do %>
<link name="<%= emoji %>" rel="emoji" href="<%= file %>"/>
<% end %>
</entry>

View file

@ -1,18 +1,14 @@
<author>
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
<id><%= @actor.ap_id %></id>
<uri><%= @actor.ap_id %></uri>
<name><%= @actor.nickname %></name>
<summary><%= escape(@actor.bio) %></summary>
<link rel="avatar" href="<%= User.avatar_url(@actor) %>"/>
<%= if User.banner_url(@actor) do %>
<link rel="header" href="<%= User.banner_url(@actor) %>"/>
<% end %>
<%= if @actor.local do %>
<ap_enabled>true</ap_enabled>
<% end %>
<poco:preferredUsername><%= @actor.nickname %></poco:preferredUsername>
<poco:displayName><%= @actor.name %></poco:displayName>
<poco:note><%= escape(@actor.bio) %></poco:note>
<uri><%= @actor.ap_id %></uri>
<name><%= @actor.nickname %></name>
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
<activity:displayName><%= @actor.name %></activity:displayName>
<activity:image><%= User.avatar_url(@actor) %></activity:image>
<activity:id><%= @actor.ap_id %></activity:id>
<activity:published><%= to_rfc3339(@actor.inserted_at) %></activity:published>
<activity:updated><%= to_rfc3339(@actor.updated_at) %></activity:updated>
<activity:url><%= @actor.ap_id %></activity:url>
<poco:preferredUsername><%= @actor.nickname %></poco:preferredUsername>
<poco:displayName><%= @actor.name %></poco:displayName>
<poco:note><%= escape(@actor.bio) %></poco:note>
</author>

View file

@ -1,22 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<feed xml:lang="<%= Gettext.language_tag() %>" xmlns="http://www.w3.org/2005/Atom"
xmlns:thr="http://purl.org/syndication/thread/1.0"
xmlns:georss="http://www.georss.org/georss"
xmlns:activity="http://activitystrea.ms/spec/1.0/"
xmlns:media="http://purl.org/syndication/atommedia"
xmlns:poco="http://portablecontacts.net/spec/1.0"
xmlns:ostatus="http://ostatus.org/schema/1.0"
xmlns:statusnet="http://status.net/schema/api/1/">
<feed
xmlns="http://www.w3.org/2005/Atom"
xmlns:activity="http://activitystrea.ms/spec/1.0/"
xmlns:poco="http://portablecontacts.net/spec/1.0"
xmlns:ostatus="http://ostatus.org/schema/1.0"
xmlns:statusnet="http://status.net/schema/api/1/">
<id><%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.rss' %></id>
<title>#<%= @tag %></title>
<id><%= Routes.tag_feed_url(@conn, :feed, @tag) <> ".atom" %></id>
<title>#<%= @tag %></title>
<subtitle><%= Gettext.dpgettext("static_pages", "tag feed description", "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse.", tag: @tag) %></subtitle>
<logo><%= feed_logo() %></logo>
<updated><%= most_recent_update(@activities) %></updated>
<link rel="self" href="<%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.atom' %>" type="application/atom+xml"/>
<subtitle><%= Gettext.dpgettext("static_pages", "tag feed description", "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse.", tag: @tag) %></subtitle>
<logo><%= feed_logo() %></logo>
<updated><%= most_recent_update(@activities) %></updated>
<link rel="self" href="<%= '#{Routes.tag_feed_url(@conn, :feed, @tag)}.atom' %>" type="application/atom+xml"/>
<%= for activity <- @activities do %>
<%= render Phoenix.Controller.view_module(@conn), "_tag_activity.atom", Map.merge(assigns, prepare_activity(activity, actor: true)) %>
<% end %>
<%= for activity <- @activities do %>
<%= render Phoenix.Controller.view_module(@conn), "_tag_activity.atom", Map.merge(assigns, prepare_activity(activity, actor: true)) %>
<% end %>
</feed>

View file

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<feed
xmlns="http://www.w3.org/2005/Atom"
xmlns:thr="http://purl.org/syndication/thread/1.0"
xmlns:activity="http://activitystrea.ms/spec/1.0/"
xmlns:poco="http://portablecontacts.net/spec/1.0"
xmlns:ostatus="http://ostatus.org/schema/1.0">
<id><%= Routes.user_feed_url(@conn, :feed, @user.nickname) <> ".atom" %></id>
<title><%= @user.nickname <> "'s timeline" %></title>
<updated><%= most_recent_update(@activities, @user) %></updated>
<subtitle><%= escape(@user.bio) %></subtitle>
<updated><%= most_recent_update(@activities, @user, :atom) %></updated>
<logo><%= logo(@user) %></logo>
<link rel="self" href="<%= '#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.atom' %>" type="application/atom+xml"/>

View file

@ -1,11 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<rss version="2.0"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:activity="http://activitystrea.ms/spec/1.0/"
xmlns:ostatus="http://ostatus.org/schema/1.0"
xmlns:poco="http://portablecontacts.net/spec/1.0">
<channel>
<guid><%= Routes.user_feed_url(@conn, :feed, @user.nickname) <> ".rss" %></guid>
<title><%= @user.nickname <> "'s timeline" %></title>
<updated><%= most_recent_update(@activities, @user) %></updated>
<image><%= logo(@user) %></image>
<link><%= '#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.rss' %></link>
<atom:link href="<%= Routes.user_feed_url(@conn, :feed, @user.nickname) <> ".atom" %>"
rel="self" type="application/rss+xml" />
<description><%= escape(@user.bio) %></description>
<image>
<url><%= logo(@user) %></url>
<title><%= @user.nickname <> "'s timeline" %></title>
<link><%= '#{Routes.user_feed_url(@conn, :feed, @user.nickname)}.rss' %></link>
</image>
<%= render Phoenix.Controller.view_module(@conn), "_author.rss", assigns %>