From 22fc271e23a5dd1570ea7429b563f6edc42613c4 Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov <parallel588@gmail.com>
Date: Fri, 6 Dec 2019 09:32:29 +0300
Subject: [PATCH 1/6] init tag feed

---
 lib/pleroma/emails/admin_email.ex             |  2 +-
 lib/pleroma/web/feed/tag_controller.ex        | 36 +++++++++++++++++++
 ...{feed_controller.ex => user_controller.ex} |  8 +++--
 lib/pleroma/web/metadata/feed.ex              |  2 +-
 lib/pleroma/web/router.ex                     |  6 ++--
 .../web/templates/feed/feed/tag.xml.eex       | 10 ++++++
 .../feed/feed/{feed.xml.eex => user.xml.eex}  |  6 ++--
 test/emails/admin_email_test.exs              |  4 +--
 test/user_test.exs                            |  4 +--
 test/web/feed/tag_controller_test.exs         | 31 ++++++++++++++++
 ...ller_test.exs => user_controller_test.exs} |  2 +-
 11 files changed, 96 insertions(+), 15 deletions(-)
 create mode 100644 lib/pleroma/web/feed/tag_controller.ex
 rename lib/pleroma/web/feed/{feed_controller.ex => user_controller.ex} (89%)
 create mode 100644 lib/pleroma/web/templates/feed/feed/tag.xml.eex
 rename lib/pleroma/web/templates/feed/feed/{feed.xml.eex => user.xml.eex} (67%)
 create mode 100644 test/web/feed/tag_controller_test.exs
 rename test/web/feed/{feed_controller_test.exs => user_controller_test.exs} (99%)

diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex
index b15e4041b..d7dd4b2e0 100644
--- a/lib/pleroma/emails/admin_email.ex
+++ b/lib/pleroma/emails/admin_email.ex
@@ -17,7 +17,7 @@ defp instance_notify_email do
   end
 
   defp user_url(user) do
-    Helpers.feed_url(Pleroma.Web.Endpoint, :feed_redirect, user.id)
+    Helpers.user_feed_url(Pleroma.Web.Endpoint, :feed_redirect, user.id)
   end
 
   def report(to, reporter, account, statuses, comment) do
diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex
new file mode 100644
index 000000000..66abc186b
--- /dev/null
+++ b/lib/pleroma/web/feed/tag_controller.ex
@@ -0,0 +1,36 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Feed.TagController do
+  use Pleroma.Web, :controller
+
+  alias Pleroma.Config
+  alias Pleroma.Web.ActivityPub.ActivityPub
+  alias Pleroma.Web.Feed.FeedView
+
+  def feed(conn, %{"tag" => tag} = params) do
+    activities =
+      %{
+        "type" => ["Create"],
+        "whole_db" => true,
+        "tag" => parse_tag(tag)
+      }
+      |> Map.merge(Map.take(params, ["max_id"]))
+      |> ActivityPub.fetch_public_activities()
+
+    conn
+    |> put_resp_content_type("application/atom+xml")
+    |> put_view(FeedView)
+    |> render("tag.xml", activities: activities, feed_config: Config.get([:feed]))
+  end
+
+  defp parse_tag(raw_tag) when is_binary(raw_tag) do
+    case Enum.reverse(String.split(raw_tag, ".")) do
+      [format | tag] when format in ["atom", "rss"] -> Enum.join(tag, ".")
+      _ -> raw_tag
+    end
+  end
+
+  defp parse_tag(raw_tag), do: raw_tag
+end
diff --git a/lib/pleroma/web/feed/feed_controller.ex b/lib/pleroma/web/feed/user_controller.ex
similarity index 89%
rename from lib/pleroma/web/feed/feed_controller.ex
rename to lib/pleroma/web/feed/user_controller.ex
index d0e23007d..e5d8427ce 100644
--- a/lib/pleroma/web/feed/feed_controller.ex
+++ b/lib/pleroma/web/feed/user_controller.ex
@@ -2,13 +2,14 @@
 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
-defmodule Pleroma.Web.Feed.FeedController do
+defmodule Pleroma.Web.Feed.UserController do
   use Pleroma.Web, :controller
 
   alias Fallback.RedirectController
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.ActivityPubController
+  alias Pleroma.Web.Feed.FeedView
 
   plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect])
 
@@ -27,7 +28,7 @@ def feed_redirect(%{assigns: %{format: format}} = conn, _params)
 
   def feed_redirect(conn, %{"nickname" => nickname}) do
     with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
-      redirect(conn, external: "#{feed_url(conn, :feed, user.nickname)}.atom")
+      redirect(conn, external: "#{user_feed_url(conn, :feed, user.nickname)}.atom")
     end
   end
 
@@ -44,7 +45,8 @@ def feed(conn, %{"nickname" => nickname} = params) do
 
       conn
       |> put_resp_content_type("application/atom+xml")
-      |> render("feed.xml",
+      |> put_view(FeedView)
+      |> render("user.xml",
         user: user,
         activities: activities,
         feed_config: Pleroma.Config.get([:feed])
diff --git a/lib/pleroma/web/metadata/feed.ex b/lib/pleroma/web/metadata/feed.ex
index 8043e6c54..ee48913a7 100644
--- a/lib/pleroma/web/metadata/feed.ex
+++ b/lib/pleroma/web/metadata/feed.ex
@@ -16,7 +16,7 @@ def build_tags(%{user: user}) do
        [
          rel: "alternate",
          type: "application/atom+xml",
-         href: Helpers.feed_path(Endpoint, :feed, user.nickname) <> ".atom"
+         href: Helpers.user_feed_path(Endpoint, :feed, user.nickname) <> ".atom"
        ], []}
     ]
   end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index e6c4f6f14..871f3bf85 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -526,8 +526,10 @@ defmodule Pleroma.Web.Router do
     get("/notice/:id", OStatus.OStatusController, :notice)
     get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
 
-    get("/users/:nickname/feed", Feed.FeedController, :feed)
-    get("/users/:nickname", Feed.FeedController, :feed_redirect)
+    get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed)
+    get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)
+
+    get("/tags/:tag", Feed.TagController, :feed, as: :tag_feed)
 
     get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
   end
diff --git a/lib/pleroma/web/templates/feed/feed/tag.xml.eex b/lib/pleroma/web/templates/feed/feed/tag.xml.eex
new file mode 100644
index 000000000..52b1d7b7d
--- /dev/null
+++ b/lib/pleroma/web/templates/feed/feed/tag.xml.eex
@@ -0,0 +1,10 @@
+<?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">
+
+  <title>TAGS</title>
+</feed>
diff --git a/lib/pleroma/web/templates/feed/feed/feed.xml.eex b/lib/pleroma/web/templates/feed/feed/user.xml.eex
similarity index 67%
rename from lib/pleroma/web/templates/feed/feed/feed.xml.eex
rename to lib/pleroma/web/templates/feed/feed/user.xml.eex
index 5ae36d345..d274c08ae 100644
--- a/lib/pleroma/web/templates/feed/feed/feed.xml.eex
+++ b/lib/pleroma/web/templates/feed/feed/user.xml.eex
@@ -6,16 +6,16 @@
   xmlns:poco="http://portablecontacts.net/spec/1.0"
   xmlns:ostatus="http://ostatus.org/schema/1.0">
 
-  <id><%= feed_url(@conn, :feed, @user.nickname) <> ".atom" %></id>
+  <id><%= user_feed_url(@conn, :feed, @user.nickname) <> ".atom" %></id>
   <title><%= @user.nickname <> "'s timeline" %></title>
   <updated><%= most_recent_update(@activities, @user) %></updated>
   <logo><%= logo(@user) %></logo>
-  <link rel="self" href="<%= '#{feed_url(@conn, :feed, @user.nickname)}.atom' %>" type="application/atom+xml"/>
+  <link rel="self" href="<%= '#{user_feed_url(@conn, :feed, @user.nickname)}.atom' %>" type="application/atom+xml"/>
 
   <%= render @view_module, "_author.xml", assigns %>
 
   <%= if last_activity(@activities) do %>
-    <link rel="next" href="<%= '#{feed_url(@conn, :feed, @user.nickname)}.atom?max_id=#{last_activity(@activities).id}' %>" type="application/atom+xml"/>
+    <link rel="next" href="<%= '#{user_feed_url(@conn, :feed, @user.nickname)}.atom?max_id=#{last_activity(@activities).id}' %>" type="application/atom+xml"/>
   <% end %>
 
   <%= for activity <- @activities do %>
diff --git a/test/emails/admin_email_test.exs b/test/emails/admin_email_test.exs
index ad89f9213..383cc3459 100644
--- a/test/emails/admin_email_test.exs
+++ b/test/emails/admin_email_test.exs
@@ -19,8 +19,8 @@ test "build report email" do
       AdminEmail.report(to_user, reporter, account, [%{name: "Test", id: "12"}], "Test comment")
 
     status_url = Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, "12")
-    reporter_url = Helpers.feed_url(Pleroma.Web.Endpoint, :feed_redirect, reporter.id)
-    account_url = Helpers.feed_url(Pleroma.Web.Endpoint, :feed_redirect, account.id)
+    reporter_url = Helpers.user_feed_url(Pleroma.Web.Endpoint, :feed_redirect, reporter.id)
+    account_url = Helpers.user_feed_url(Pleroma.Web.Endpoint, :feed_redirect, account.id)
 
     assert res.to == [{to_user.name, to_user.email}]
     assert res.from == {config[:name], config[:notify_email]}
diff --git a/test/user_test.exs b/test/user_test.exs
index bfa8faafa..7b0842e24 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -548,7 +548,7 @@ test "returns an ap_id for a user" do
     user = insert(:user)
 
     assert User.ap_id(user) ==
-             Pleroma.Web.Router.Helpers.feed_url(
+             Pleroma.Web.Router.Helpers.user_feed_url(
                Pleroma.Web.Endpoint,
                :feed_redirect,
                user.nickname
@@ -559,7 +559,7 @@ test "returns an ap_followers link for a user" do
     user = insert(:user)
 
     assert User.ap_followers(user) ==
-             Pleroma.Web.Router.Helpers.feed_url(
+             Pleroma.Web.Router.Helpers.user_feed_url(
                Pleroma.Web.Endpoint,
                :feed_redirect,
                user.nickname
diff --git a/test/web/feed/tag_controller_test.exs b/test/web/feed/tag_controller_test.exs
new file mode 100644
index 000000000..82115f811
--- /dev/null
+++ b/test/web/feed/tag_controller_test.exs
@@ -0,0 +1,31 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Feed.TagControllerTest do
+  use Pleroma.Web.ConnCase
+
+  import Pleroma.Factory
+
+  clear_config([:feed])
+
+  test "gets a feed", %{conn: conn} do
+    Pleroma.Config.put(
+      [:feed, :post_title],
+      %{max_length: 10, omission: "..."}
+    )
+
+    user = insert(:user)
+    {:ok, _activity1} = Pleroma.Web.CommonAPI.post(user, %{"status" => "yeah #PleromaArt"})
+
+    {:ok, _activity2} =
+      Pleroma.Web.CommonAPI.post(user, %{"status" => "42 This is :moominmamma #PleromaArt"})
+
+    {:ok, _activity3} = Pleroma.Web.CommonAPI.post(user, %{"status" => "This is :moominmamma"})
+
+    assert conn
+           |> put_req_header("content-type", "application/atom+xml")
+           |> get("/tags/pleromaart.rss")
+           |> response(200)
+  end
+end
diff --git a/test/web/feed/feed_controller_test.exs b/test/web/feed/user_controller_test.exs
similarity index 99%
rename from test/web/feed/feed_controller_test.exs
rename to test/web/feed/user_controller_test.exs
index 6f61acf43..e4386ff2c 100644
--- a/test/web/feed/feed_controller_test.exs
+++ b/test/web/feed/user_controller_test.exs
@@ -2,7 +2,7 @@
 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
-defmodule Pleroma.Web.Feed.FeedControllerTest do
+defmodule Pleroma.Web.Feed.UserControllerTest do
   use Pleroma.Web.ConnCase
 
   import Pleroma.Factory

From b53573a837e606ef3536ec338510cadacab55d7c Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov <parallel588@gmail.com>
Date: Tue, 17 Dec 2019 22:13:45 +0300
Subject: [PATCH 2/6] add tag feeds

---
 CHANGELOG.md                                  |  2 +
 lib/pleroma/web/controller_helper.ex          |  7 +-
 lib/pleroma/web/feed/feed_view.ex             | 24 +++++++
 lib/pleroma/web/feed/tag_controller.ex        | 20 +++---
 lib/pleroma/web/feed/user_controller.ex       | 10 ++-
 lib/pleroma/web/metadata/utils.ex             | 11 ++-
 .../templates/feed/feed/_tag_activity.xml.eex | 15 ++++
 .../web/templates/feed/feed/tag.xml.eex       | 21 +++---
 test/web/feed/tag_controller_test.exs         | 72 +++++++++++++++++--
 9 files changed, 149 insertions(+), 33 deletions(-)
 create mode 100644 lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c133cd9ec..feb3f9b3c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -86,6 +86,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Mastodon API: `/api/v1/update_credentials` accepts `actor_type` field.
 - Captcha: Support native provider
 - Captcha: Enable by default
+- Configuration: `feed.logo` option for tag feed.
+- Tag feed: `/tags/:tag.rss` - list public statuses by hashtag.
 </details>
 
 ### Fixed
diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex
index 9a4e322c9..e3d7a465b 100644
--- a/lib/pleroma/web/controller_helper.ex
+++ b/lib/pleroma/web/controller_helper.ex
@@ -76,8 +76,7 @@ def assign_account_by_id(%{params: %{"id" => id}} = conn, _) do
     end
   end
 
-  def try_render(conn, target, params)
-      when is_binary(target) do
+  def try_render(conn, target, params) when is_binary(target) do
     case render(conn, target, params) do
       nil -> render_error(conn, :not_implemented, "Can't display this activity")
       res -> res
@@ -87,4 +86,8 @@ def try_render(conn, target, params)
   def try_render(conn, _, _) do
     render_error(conn, :not_implemented, "Can't display this activity")
   end
+
+  @spec put_in_if_exist(map(), atom() | String.t(), any) :: map()
+  def put_in_if_exist(map, _key, nil), do: map
+  def put_in_if_exist(map, key, value), do: put_in(map, key, value)
 end
diff --git a/lib/pleroma/web/feed/feed_view.ex b/lib/pleroma/web/feed/feed_view.ex
index bb1332fd3..2e7db1ebb 100644
--- a/lib/pleroma/web/feed/feed_view.ex
+++ b/lib/pleroma/web/feed/feed_view.ex
@@ -13,6 +13,15 @@ defmodule Pleroma.Web.Feed.FeedView do
 
   require Pleroma.Constants
 
+  @spec pub_date(String.t() | DateTime.t()) :: String.t()
+  def pub_date(date) when is_binary(date) do
+    date
+    |> Timex.parse!("{ISO:Extended}")
+    |> pub_date
+  end
+
+  def pub_date(%DateTime{} = date), do: Timex.format!(date, "{RFC822}")
+
   def prepare_activity(activity) do
     object = activity_object(activity)
 
@@ -28,6 +37,17 @@ def most_recent_update(activities, user) do
     |> NaiveDateTime.to_iso8601()
   end
 
+  def feed_logo do
+    case Pleroma.Config.get([:feed, :logo]) do
+      nil ->
+        "#{Pleroma.Web.base_url()}/static/logo.png"
+
+      logo ->
+        "#{Pleroma.Web.base_url()}#{logo}"
+    end
+    |> MediaProxy.url()
+  end
+
   def logo(user) do
     user
     |> User.avatar_url()
@@ -40,6 +60,8 @@ def activity_object(activity), do: Object.normalize(activity)
 
   def activity_title(%{data: %{"content" => content}}, opts \\ %{}) do
     content
+    |> Pleroma.Web.Metadata.Utils.scrub_html()
+    |> Pleroma.Emoji.Formatter.demojify()
     |> Formatter.truncate(opts[:max_length], opts[:omission])
     |> escape()
   end
@@ -50,6 +72,8 @@ def activity_content(%{data: %{"content" => content}}) do
     |> escape()
   end
 
+  def activity_content(_), do: ""
+
   def activity_context(activity), do: activity.data["context"]
 
   def attachment_href(attachment) do
diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex
index 66abc186b..97ce147de 100644
--- a/lib/pleroma/web/feed/tag_controller.ex
+++ b/lib/pleroma/web/feed/tag_controller.ex
@@ -9,20 +9,24 @@ defmodule Pleroma.Web.Feed.TagController do
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.Feed.FeedView
 
-  def feed(conn, %{"tag" => tag} = params) do
+  import Pleroma.Web.ControllerHelper, only: [put_in_if_exist: 3]
+
+  def feed(conn, %{"tag" => raw_tag} = params) do
+    tag = parse_tag(raw_tag)
+
     activities =
-      %{
-        "type" => ["Create"],
-        "whole_db" => true,
-        "tag" => parse_tag(tag)
-      }
-      |> Map.merge(Map.take(params, ["max_id"]))
+      %{"type" => ["Create"], "whole_db" => true, "tag" => tag}
+      |> put_in_if_exist("max_id", params["max_id"])
       |> ActivityPub.fetch_public_activities()
 
     conn
     |> put_resp_content_type("application/atom+xml")
     |> put_view(FeedView)
-    |> render("tag.xml", activities: activities, feed_config: Config.get([:feed]))
+    |> render("tag.xml",
+      activities: activities,
+      tag: tag,
+      feed_config: Config.get([:feed])
+    )
   end
 
   defp parse_tag(raw_tag) when is_binary(raw_tag) do
diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex
index e5d8427ce..cf04fd497 100644
--- a/lib/pleroma/web/feed/user_controller.ex
+++ b/lib/pleroma/web/feed/user_controller.ex
@@ -11,6 +11,8 @@ defmodule Pleroma.Web.Feed.UserController do
   alias Pleroma.Web.ActivityPub.ActivityPubController
   alias Pleroma.Web.Feed.FeedView
 
+  import Pleroma.Web.ControllerHelper, only: [put_in_if_exist: 3]
+
   plug(Pleroma.Plugs.SetFormatPlug when action in [:feed_redirect])
 
   action_fallback(:errors)
@@ -35,12 +37,8 @@ def feed_redirect(conn, %{"nickname" => nickname}) do
   def feed(conn, %{"nickname" => nickname} = params) do
     with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
       activities =
-        %{
-          "type" => ["Create"],
-          "whole_db" => true,
-          "actor_id" => user.ap_id
-        }
-        |> Map.merge(Map.take(params, ["max_id"]))
+        %{"type" => ["Create"], "whole_db" => true, "actor_id" => user.ap_id}
+        |> put_in_if_exist("max_id", params["max_id"])
         |> ActivityPub.fetch_public_activities()
 
       conn
diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex
index 382ecf426..c0dae1b7e 100644
--- a/lib/pleroma/web/metadata/utils.ex
+++ b/lib/pleroma/web/metadata/utils.ex
@@ -19,15 +19,22 @@ def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do
   end
 
   def scrub_html_and_truncate(content, max_length \\ 200) when is_binary(content) do
+    content
+    |> scrub_html
+    |> Emoji.Formatter.demojify()
+    |> Formatter.truncate(max_length)
+  end
+
+  def scrub_html(content) when is_binary(content) do
     content
     # html content comes from DB already encoded, decode first and scrub after
     |> HtmlEntities.decode()
     |> String.replace(~r/<br\s?\/?>/, " ")
     |> HTML.strip_tags()
-    |> Emoji.Formatter.demojify()
-    |> Formatter.truncate(max_length)
   end
 
+  def scrub_html(content), do: content
+
   def attachment_url(url) do
     MediaProxy.url(url)
   end
diff --git a/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex b/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex
new file mode 100644
index 000000000..295574df1
--- /dev/null
+++ b/lib/pleroma/web/templates/feed/feed/_tag_activity.xml.eex
@@ -0,0 +1,15 @@
+<item>
+  <title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title>
+  
+  
+  <guid isPermalink="true"><%= activity_context(@activity) %></guid>
+  <link><%= activity_context(@activity) %></link>
+  <pubDate><%= pub_date(@data["published"]) %></pubDate>
+  
+  <description><%= activity_content(@object) %></description>
+  <%= for attachment <- @data["attachment"] || [] do %>
+    <enclosure url="<%= attachment_href(attachment) %>" type="<%= attachment_type(attachment) %>"/>
+  <% end %>
+  
+</item>
+
diff --git a/lib/pleroma/web/templates/feed/feed/tag.xml.eex b/lib/pleroma/web/templates/feed/feed/tag.xml.eex
index 52b1d7b7d..eeda01a04 100644
--- a/lib/pleroma/web/templates/feed/feed/tag.xml.eex
+++ b/lib/pleroma/web/templates/feed/feed/tag.xml.eex
@@ -1,10 +1,15 @@
 <?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">
+<rss version="2.0" xmlns:webfeeds="http://webfeeds.org/rss/1.0">
+  <channel>
 
-  <title>TAGS</title>
-</feed>
+
+    <title>#<%= @tag %></title>
+    <description>These are public toots tagged with #<%= @tag %>. You can interact with them if you have an account anywhere in the fediverse.</description>
+    <link><%= '#{tag_feed_url(@conn, :feed, @tag)}.rss' %></link>
+    <webfeeds:logo><%= feed_logo() %></webfeeds:logo>
+    <webfeeds:accentColor>2b90d9</webfeeds:accentColor>
+    <%= for activity <- @activities do %>
+    <%= render @view_module, "_tag_activity.xml", Map.merge(assigns, prepare_activity(activity)) %>
+    <% end %>
+  </channel>
+</rss>
diff --git a/test/web/feed/tag_controller_test.exs b/test/web/feed/tag_controller_test.exs
index 82115f811..e9b58c8cd 100644
--- a/test/web/feed/tag_controller_test.exs
+++ b/test/web/feed/tag_controller_test.exs
@@ -6,26 +6,84 @@ defmodule Pleroma.Web.Feed.TagControllerTest do
   use Pleroma.Web.ConnCase
 
   import Pleroma.Factory
+  import SweetXml
+
+  alias Pleroma.Web.Feed.FeedView
 
   clear_config([:feed])
 
   test "gets a feed", %{conn: conn} do
     Pleroma.Config.put(
       [:feed, :post_title],
-      %{max_length: 10, omission: "..."}
+      %{max_length: 25, omission: "..."}
     )
 
     user = insert(:user)
-    {:ok, _activity1} = Pleroma.Web.CommonAPI.post(user, %{"status" => "yeah #PleromaArt"})
+    {:ok, activity1} = Pleroma.Web.CommonAPI.post(user, %{"status" => "yeah #PleromaArt"})
 
-    {:ok, _activity2} =
+    object = Pleroma.Object.normalize(activity1)
+
+    object_data =
+      Map.put(object.data, "attachment", [
+        %{
+          "url" => [
+            %{
+              "href" =>
+                "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
+              "mediaType" => "video/mp4",
+              "type" => "Link"
+            }
+          ]
+        }
+      ])
+
+    object
+    |> Ecto.Changeset.change(data: object_data)
+    |> Pleroma.Repo.update()
+
+    {:ok, activity2} =
       Pleroma.Web.CommonAPI.post(user, %{"status" => "42 This is :moominmamma #PleromaArt"})
 
     {:ok, _activity3} = Pleroma.Web.CommonAPI.post(user, %{"status" => "This is :moominmamma"})
 
-    assert conn
-           |> put_req_header("content-type", "application/atom+xml")
-           |> get("/tags/pleromaart.rss")
-           |> response(200)
+    response =
+      conn
+      |> put_req_header("content-type", "application/atom+xml")
+      |> get("/tags/pleromaart.rss")
+      |> response(200)
+
+    xml = parse(response)
+    assert xpath(xml, ~x"//channel/title/text()") == '#pleromaart'
+
+    assert xpath(xml, ~x"//channel/description/text()"s) ==
+             "These are public toots tagged with #pleromaart. You can interact with them if you have an account anywhere in the fediverse."
+
+    assert xpath(xml, ~x"//channel/link/text()") ==
+             '#{Pleroma.Web.base_url()}/tags/pleromaart.rss'
+
+    assert xpath(xml, ~x"//channel/webfeeds:logo/text()") ==
+             '#{Pleroma.Web.base_url()}/static/logo.png'
+
+    assert xpath(xml, ~x"//channel/item/title/text()"l) == [
+             '42 This is :moominmamm...',
+             'yeah #PleromaArt'
+           ]
+
+    assert xpath(xml, ~x"//channel/item/pubDate/text()"sl) == [
+             FeedView.pub_date(activity1.data["published"]),
+             FeedView.pub_date(activity2.data["published"])
+           ]
+
+    assert xpath(xml, ~x"//channel/item/enclosure/@url"sl) == [
+             "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4"
+           ]
+
+    obj1 = Pleroma.Object.normalize(activity1)
+    obj2 = Pleroma.Object.normalize(activity2)
+
+    assert xpath(xml, ~x"//channel/item/description/text()"sl) == [
+             HtmlEntities.decode(FeedView.activity_content(obj2)),
+             HtmlEntities.decode(FeedView.activity_content(obj1))
+           ]
   end
 end

From 969769730e0b7578ddc6a5cd02f9b24eff5902a0 Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov <parallel588@gmail.com>
Date: Mon, 23 Dec 2019 17:12:55 +0300
Subject: [PATCH 3/6] update tests

---
 test/web/feed/tag_controller_test.exs  | 2 +-
 test/web/feed/user_controller_test.exs | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/test/web/feed/tag_controller_test.exs b/test/web/feed/tag_controller_test.exs
index e9b58c8cd..efc588070 100644
--- a/test/web/feed/tag_controller_test.exs
+++ b/test/web/feed/tag_controller_test.exs
@@ -49,7 +49,7 @@ test "gets a feed", %{conn: conn} do
     response =
       conn
       |> put_req_header("content-type", "application/atom+xml")
-      |> get("/tags/pleromaart.rss")
+      |> get(tag_feed_path(conn, :feed, "pleromaart.rss"))
       |> response(200)
 
     xml = parse(response)
diff --git a/test/web/feed/user_controller_test.exs b/test/web/feed/user_controller_test.exs
index e4386ff2c..41cc9e07e 100644
--- a/test/web/feed/user_controller_test.exs
+++ b/test/web/feed/user_controller_test.exs
@@ -49,7 +49,7 @@ test "gets a feed", %{conn: conn} do
     resp =
       conn
       |> put_req_header("content-type", "application/atom+xml")
-      |> get("/users/#{user.nickname}/feed.atom")
+      |> get(user_feed_path(conn, :feed, user.nickname))
       |> response(200)
 
     activity_titles =
@@ -65,7 +65,7 @@ test "returns 404 for a missing feed", %{conn: conn} do
     conn =
       conn
       |> put_req_header("content-type", "application/atom+xml")
-      |> get("/users/nonexisting/feed.atom")
+      |> get(user_feed_path(conn, :feed, "nonexisting"))
 
     assert response(conn, 404)
   end
@@ -91,7 +91,7 @@ test "undefined format. it returns error when user not found", %{conn: conn} do
       response =
         conn
         |> put_req_header("accept", "application/xml")
-        |> get("/users/jimm")
+        |> get(user_feed_path(conn, :feed, "jimm"))
         |> response(404)
 
       assert response == ~S({"error":"Not found"})

From 99c0a11c584ac13c368a3d8372f9e914cef14a06 Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov <parallel588@gmail.com>
Date: Fri, 24 Jan 2020 22:08:10 +0300
Subject: [PATCH 4/6] added atom feed

---
 lib/pleroma/web/feed/feed_view.ex             | 16 +++++-
 lib/pleroma/web/feed/tag_controller.ex        | 11 ++--
 .../web/templates/feed/feed/_activity.xml.eex |  2 +-
 .../feed/feed/_tag_activity.atom.eex          | 51 +++++++++++++++++
 .../templates/feed/feed/_tag_author.atom.eex  | 18 ++++++
 .../web/templates/feed/feed/tag.atom.eex      | 22 ++++++++
 .../feed/feed/{tag.xml.eex => tag.rss.eex}    |  0
 test/web/feed/tag_controller_test.exs         | 55 ++++++++++++++++++-
 8 files changed, 166 insertions(+), 9 deletions(-)
 create mode 100644 lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex
 create mode 100644 lib/pleroma/web/templates/feed/feed/_tag_author.atom.eex
 create mode 100644 lib/pleroma/web/templates/feed/feed/tag.atom.eex
 rename lib/pleroma/web/templates/feed/feed/{tag.xml.eex => tag.rss.eex} (100%)

diff --git a/lib/pleroma/web/feed/feed_view.ex b/lib/pleroma/web/feed/feed_view.ex
index 2e7db1ebb..334802e0a 100644
--- a/lib/pleroma/web/feed/feed_view.ex
+++ b/lib/pleroma/web/feed/feed_view.ex
@@ -22,16 +22,28 @@ def pub_date(date) when is_binary(date) do
 
   def pub_date(%DateTime{} = date), do: Timex.format!(date, "{RFC822}")
 
-  def prepare_activity(activity) do
+  def prepare_activity(activity, opts \\ []) do
     object = activity_object(activity)
 
+    actor =
+      if opts[:actor] do
+        Pleroma.User.get_cached_by_ap_id(activity.actor)
+      end
+
     %{
       activity: activity,
       data: Map.get(object, :data),
-      object: object
+      object: object,
+      actor: actor
     }
   end
 
+  def most_recent_update(activities) do
+    with %{updated_at: updated_at} <- List.first(activities) do
+      NaiveDateTime.to_iso8601(updated_at)
+    end
+  end
+
   def most_recent_update(activities, user) do
     (List.first(activities) || user).updated_at
     |> NaiveDateTime.to_iso8601()
diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex
index 97ce147de..9b722336c 100644
--- a/lib/pleroma/web/feed/tag_controller.ex
+++ b/lib/pleroma/web/feed/tag_controller.ex
@@ -12,7 +12,7 @@ defmodule Pleroma.Web.Feed.TagController do
   import Pleroma.Web.ControllerHelper, only: [put_in_if_exist: 3]
 
   def feed(conn, %{"tag" => raw_tag} = params) do
-    tag = parse_tag(raw_tag)
+    {format, tag} = parse_tag(raw_tag)
 
     activities =
       %{"type" => ["Create"], "whole_db" => true, "tag" => tag}
@@ -22,19 +22,20 @@ def feed(conn, %{"tag" => raw_tag} = params) do
     conn
     |> put_resp_content_type("application/atom+xml")
     |> put_view(FeedView)
-    |> render("tag.xml",
+    |> render("tag.#{format}",
       activities: activities,
       tag: tag,
       feed_config: Config.get([:feed])
     )
   end
 
+  @spec parse_tag(binary() | any()) :: {format :: String.t(), tag :: String.t()}
   defp parse_tag(raw_tag) when is_binary(raw_tag) do
     case Enum.reverse(String.split(raw_tag, ".")) do
-      [format | tag] when format in ["atom", "rss"] -> Enum.join(tag, ".")
-      _ -> raw_tag
+      [format | tag] when format in ["atom", "rss"] -> {format, Enum.join(tag, ".")}
+      _ -> {"rss", raw_tag}
     end
   end
 
-  defp parse_tag(raw_tag), do: raw_tag
+  defp parse_tag(raw_tag), do: {"rss", raw_tag}
 end
diff --git a/lib/pleroma/web/templates/feed/feed/_activity.xml.eex b/lib/pleroma/web/templates/feed/feed/_activity.xml.eex
index 514eacaed..ac8a75009 100644
--- a/lib/pleroma/web/templates/feed/feed/_activity.xml.eex
+++ b/lib/pleroma/web/templates/feed/feed/_activity.xml.eex
@@ -9,7 +9,7 @@
   <ostatus:conversation ref="<%= activity_context(@activity) %>">
     <%= activity_context(@activity) %>
   </ostatus:conversation>
-  <link ref="<%= activity_context(@activity) %>" rel="ostatus:conversation"/>
+  <link href="<%= activity_context(@activity) %>" rel="ostatus:conversation"/>
 
   <%= if @data["summary"] do %>
     <summary><%= @data["summary"] %></summary>
diff --git a/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex b/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex
new file mode 100644
index 000000000..da4fa6d6c
--- /dev/null
+++ b/lib/pleroma/web/templates/feed/feed/_tag_activity.atom.eex
@@ -0,0 +1,51 @@
+<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>
+    
+    <%= render @view_module, "_tag_author.atom", assigns %>
+    
+    <id><%= @data["id"] %></id>
+    <title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title>
+    <content type="html"><%= activity_content(@object) %></content>
+
+  <%= if @activity.local do %>
+    <link type="application/atom+xml" href='<%= @data["id"] %>' rel="self"/>
+    <link type="text/html" href='<%= @data["id"] %>' rel="alternate"/>
+  <% else %>
+    <link type="text/html" href='<%= @data["external_url"] %>' rel="alternate"/>
+  <% end %>
+
+    <published><%= @data["published"] %></published>
+    <updated><%= @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 %>
+        <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.base_url()}.+followers$/, id) do %>
+          <link rel="mentioned"
+            ostatus:object-type="http://activitystrea.ms/schema/1.0/person"
+            href="<%= id %>" />
+        <% end %>
+      <% end %>
+    <% end %>
+  
+    <%= for tag <- @data["tag"] || [] do %>
+      <category term="<%= tag %>"></category>
+    <% end %>
+
+    <%= for {emoji, file} <- @data["emoji"] || %{} do %>
+      <link name="<%= emoji %>" rel="emoji" href="<%= file %>"/>
+    <% end %>
+</entry>
diff --git a/lib/pleroma/web/templates/feed/feed/_tag_author.atom.eex b/lib/pleroma/web/templates/feed/feed/_tag_author.atom.eex
new file mode 100644
index 000000000..997c4936e
--- /dev/null
+++ b/lib/pleroma/web/templates/feed/feed/_tag_author.atom.eex
@@ -0,0 +1,18 @@
+<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>    
+</author>
diff --git a/lib/pleroma/web/templates/feed/feed/tag.atom.eex b/lib/pleroma/web/templates/feed/feed/tag.atom.eex
new file mode 100644
index 000000000..a288539ed
--- /dev/null
+++ b/lib/pleroma/web/templates/feed/feed/tag.atom.eex
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<feed xml:lang="en-US" 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/">
+
+    <id><%= '#{tag_feed_url(@conn, :feed, @tag)}.rss' %></id>
+    <title>#<%= @tag %></title>
+
+    <subtitle>These are public toots tagged with #<%= @tag %>. You can interact with them if you have an account anywhere in the fediverse.</subtitle>
+    <logo><%= feed_logo() %></logo>
+    <updated><%= most_recent_update(@activities) %></updated>
+    <link rel="self" href="<%= '#{tag_feed_url(@conn, :feed, @tag)}.atom'  %>" type="application/atom+xml"/>
+    <%= for activity <- @activities do %>
+    <%= render @view_module, "_tag_activity.atom", Map.merge(assigns, prepare_activity(activity, actor: true)) %>
+    <% end %>
+</feed>
diff --git a/lib/pleroma/web/templates/feed/feed/tag.xml.eex b/lib/pleroma/web/templates/feed/feed/tag.rss.eex
similarity index 100%
rename from lib/pleroma/web/templates/feed/feed/tag.xml.eex
rename to lib/pleroma/web/templates/feed/feed/tag.rss.eex
diff --git a/test/web/feed/tag_controller_test.exs b/test/web/feed/tag_controller_test.exs
index a56a18738..214698192 100644
--- a/test/web/feed/tag_controller_test.exs
+++ b/test/web/feed/tag_controller_test.exs
@@ -12,7 +12,60 @@ defmodule Pleroma.Web.Feed.TagControllerTest do
 
   clear_config([:feed])
 
-  test "gets a feed", %{conn: conn} do
+  test "gets a feed (ATOM)", %{conn: conn} do
+    Pleroma.Config.put(
+      [:feed, :post_title],
+      %{max_length: 25, omission: "..."}
+    )
+
+    user = insert(:user)
+    {:ok, activity1} = Pleroma.Web.CommonAPI.post(user, %{"status" => "yeah #PleromaArt"})
+
+    object = Pleroma.Object.normalize(activity1)
+
+    object_data =
+      Map.put(object.data, "attachment", [
+        %{
+          "url" => [
+            %{
+              "href" =>
+                "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
+              "mediaType" => "video/mp4",
+              "type" => "Link"
+            }
+          ]
+        }
+      ])
+
+    object
+    |> Ecto.Changeset.change(data: object_data)
+    |> Pleroma.Repo.update()
+
+    {:ok, _activity2} =
+      Pleroma.Web.CommonAPI.post(user, %{"status" => "42 This is :moominmamma #PleromaArt"})
+
+    {:ok, _activity3} = Pleroma.Web.CommonAPI.post(user, %{"status" => "This is :moominmamma"})
+
+    response =
+      conn
+      |> put_req_header("content-type", "application/atom+xml")
+      |> get(tag_feed_path(conn, :feed, "pleromaart.atom"))
+      |> response(200)
+
+    xml = parse(response)
+
+    assert xpath(xml, ~x"//feed/title/text()") == '#pleromaart'
+
+    assert xpath(xml, ~x"//feed/entry/title/text()"l) == [
+             '42 This is :moominmamm...',
+             'yeah #PleromaArt'
+           ]
+
+    assert xpath(xml, ~x"//feed/entry/author/name/text()"ls) == [user.nickname, user.nickname]
+    assert xpath(xml, ~x"//feed/entry/author/id/text()"ls) == [user.ap_id, user.ap_id]
+  end
+
+  test "gets a feed (RSS)", %{conn: conn} do
     Pleroma.Config.put(
       [:feed, :post_title],
       %{max_length: 25, omission: "..."}

From be926863a8c4aa322dde0969e33c6addba85593a Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov <parallel588@gmail.com>
Date: Mon, 27 Jan 2020 21:20:33 +0300
Subject: [PATCH 5/6] fix test

---
 test/web/feed/tag_controller_test.exs                           | 2 +-
 .../web/mastodon_api/controllers/suggestion_controller_test.exs | 1 -
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/test/web/feed/tag_controller_test.exs b/test/web/feed/tag_controller_test.exs
index 214698192..2aa1b9587 100644
--- a/test/web/feed/tag_controller_test.exs
+++ b/test/web/feed/tag_controller_test.exs
@@ -101,7 +101,7 @@ test "gets a feed (RSS)", %{conn: conn} do
 
     response =
       conn
-      |> put_req_header("content-type", "application/atom+xml")
+      |> put_req_header("content-type", "application/rss+xml")
       |> get(tag_feed_path(conn, :feed, "pleromaart.rss"))
       |> response(200)
 
diff --git a/test/web/mastodon_api/controllers/suggestion_controller_test.exs b/test/web/mastodon_api/controllers/suggestion_controller_test.exs
index c288c2fff..0319d3475 100644
--- a/test/web/mastodon_api/controllers/suggestion_controller_test.exs
+++ b/test/web/mastodon_api/controllers/suggestion_controller_test.exs
@@ -7,7 +7,6 @@ defmodule Pleroma.Web.MastodonAPI.SuggestionControllerTest do
 
   alias Pleroma.Config
 
-  import ExUnit.CaptureLog
   import Pleroma.Factory
   import Tesla.Mock
 

From e3fae3380e2af0978436191127b148923f62ae53 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Tue, 28 Jan 2020 13:38:49 +0000
Subject: [PATCH 6/6] Apply suggestion to
 lib/pleroma/web/feed/tag_controller.ex

---
 lib/pleroma/web/feed/tag_controller.ex | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/pleroma/web/feed/tag_controller.ex b/lib/pleroma/web/feed/tag_controller.ex
index 9b722336c..9accd0872 100644
--- a/lib/pleroma/web/feed/tag_controller.ex
+++ b/lib/pleroma/web/feed/tag_controller.ex
@@ -15,7 +15,7 @@ def feed(conn, %{"tag" => raw_tag} = params) do
     {format, tag} = parse_tag(raw_tag)
 
     activities =
-      %{"type" => ["Create"], "whole_db" => true, "tag" => tag}
+      %{"type" => ["Create"], "tag" => tag}
       |> put_in_if_exist("max_id", params["max_id"])
       |> ActivityPub.fetch_public_activities()