Merge develop
[akkoma] / lib / pleroma / web / mastodon_api / views / status_view.ex
index f0bdb49c31f35ccf58f3a9348ed2c84612d11517..ca42917fc4bd06e0749886a7b8058359cad18903 100644 (file)
@@ -8,11 +8,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
   require Pleroma.Constants
 
   alias Pleroma.Activity
-  alias Pleroma.ActivityExpiration
   alias Pleroma.HTML
   alias Pleroma.Object
   alias Pleroma.Repo
   alias Pleroma.User
+  alias Pleroma.UserRelationship
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.CommonAPI.Utils
   alias Pleroma.Web.MastodonAPI.AccountView
@@ -20,7 +20,18 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
   alias Pleroma.Web.MastodonAPI.StatusView
   alias Pleroma.Web.MediaProxy
 
-  import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1]
+  import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]
+
+  # This is a naive way to do this, just spawning a process per activity
+  # to fetch the preview. However it should be fine considering
+  # pagination is restricted to 40 activities at a time
+  defp fetch_rich_media_for_activities(activities) do
+    Enum.each(activities, fn activity ->
+      spawn(fn ->
+        Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
+      end)
+    end)
+  end
 
   # TODO: Add cached version.
   defp get_replied_to_activities([]), do: %{}
@@ -44,7 +55,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
     end)
   end
 
-  defp get_user(ap_id) do
+  def get_user(ap_id, fake_record_fallback \\ true) do
     cond do
       user = User.get_cached_by_ap_id(ap_id) ->
         user
@@ -52,8 +63,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
       user = User.get_by_guessed_nickname(ap_id) ->
         user
 
-      true ->
+      fake_record_fallback ->
+        # TODO: refactor (fake records is never a good idea)
         User.error_user(ap_id)
+
+      true ->
+        nil
     end
   end
 
@@ -71,10 +86,52 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
   end
 
   def render("index.json", opts) do
-    replied_to_activities = get_replied_to_activities(opts.activities)
-    opts = Map.put(opts, :replied_to_activities, replied_to_activities)
+    reading_user = opts[:for]
+
+    # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
+    activities = Enum.filter(opts.activities, & &1)
+
+    # Start fetching rich media before doing anything else, so that later calls to get the cards
+    # only block for timeout in the worst case, as opposed to
+    # length(activities_with_links) * timeout
+    fetch_rich_media_for_activities(activities)
+    replied_to_activities = get_replied_to_activities(activities)
+
+    parent_activities =
+      activities
+      |> Enum.filter(&(&1.data["type"] == "Announce" && &1.data["object"]))
+      |> Enum.map(&Object.normalize(&1).data["id"])
+      |> Activity.create_by_object_ap_id()
+      |> Activity.with_preloaded_object(:left)
+      |> Activity.with_preloaded_bookmark(reading_user)
+      |> Activity.with_set_thread_muted_field(reading_user)
+      |> Repo.all()
+
+    relationships_opt =
+      cond do
+        Map.has_key?(opts, :relationships) ->
+          opts[:relationships]
+
+        is_nil(reading_user) ->
+          UserRelationship.view_relationships_option(nil, [])
+
+        true ->
+          # Note: unresolved users are filtered out
+          actors =
+            (activities ++ parent_activities)
+            |> Enum.map(&get_user(&1.data["actor"], false))
+            |> Enum.filter(& &1)
+
+          UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes)
+      end
+
+    opts =
+      opts
+      |> Map.put(:replied_to_activities, replied_to_activities)
+      |> Map.put(:parent_activities, parent_activities)
+      |> Map.put(:relationships, relationships_opt)
 
-    safe_render_many(opts.activities, StatusView, "show.json", opts)
+    safe_render_many(activities, StatusView, "show.json", opts)
   end
 
   def render(
@@ -85,17 +142,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
     created_at = Utils.to_masto_date(activity.data["published"])
     activity_object = Object.normalize(activity)
 
-    reblogged_activity =
-      Activity.create_by_object_ap_id(activity_object.data["id"])
-      |> Activity.with_preloaded_bookmark(opts[:for])
-      |> Activity.with_set_thread_muted_field(opts[:for])
-      |> Repo.one()
+    reblogged_parent_activity =
+      if opts[:parent_activities] do
+        Activity.Queries.find_by_object_ap_id(
+          opts[:parent_activities],
+          activity_object.data["id"]
+        )
+      else
+        Activity.create_by_object_ap_id(activity_object.data["id"])
+        |> Activity.with_preloaded_bookmark(opts[:for])
+        |> Activity.with_set_thread_muted_field(opts[:for])
+        |> Repo.one()
+      end
 
-    reblogged = render("show.json", Map.put(opts, :activity, reblogged_activity))
+    reblog_rendering_opts = Map.put(opts, :activity, reblogged_parent_activity)
+    reblogged = render("show.json", reblog_rendering_opts)
 
     favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
 
-    bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil
+    bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil
 
     mentions =
       activity.recipients
@@ -107,7 +172,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
       id: to_string(activity.id),
       uri: activity_object.data["id"],
       url: activity_object.data["id"],
-      account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
+      account:
+        AccountView.render("show.json", %{
+          user: user,
+          for: opts[:for]
+        }),
       in_reply_to_id: nil,
       in_reply_to_account_id: nil,
       reblog: reblogged,
@@ -116,7 +185,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
       reblogs_count: 0,
       replies_count: 0,
       favourites_count: 0,
-      reblogged: reblogged?(reblogged_activity, opts[:for]),
+      reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
       favourited: present?(favorited),
       bookmarked: present?(bookmarked),
       muted: false,
@@ -175,17 +244,18 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
 
     expires_at =
       with true <- client_posted_this_activity,
-           %ActivityExpiration{scheduled_at: scheduled_at} <-
-             ActivityExpiration.get_by_activity_id(activity.id) do
+           %Oban.Job{scheduled_at: scheduled_at} <-
+             Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) do
         scheduled_at
       else
         _ -> nil
       end
 
     thread_muted? =
-      case activity.thread_muted? do
-        thread_muted? when is_boolean(thread_muted?) -> thread_muted?
-        nil -> (opts[:for] && CommonAPI.thread_muted?(opts[:for], activity)) || false
+      cond do
+        is_nil(opts[:for]) -> false
+        is_boolean(activity.thread_muted?) -> activity.thread_muted?
+        true -> CommonAPI.thread_muted?(opts[:for], activity)
       end
 
     attachment_data = object.data["attachment"] || []
@@ -242,27 +312,47 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
 
     emoji_reactions =
       with %{data: %{"reactions" => emoji_reactions}} <- object do
-        Enum.map(emoji_reactions, fn [emoji, users] ->
-          %{
-            name: emoji,
-            count: length(users),
-            me: !!(opts[:for] && opts[:for].ap_id in users)
-          }
+        Enum.map(emoji_reactions, fn
+          [emoji, users] when is_list(users) ->
+            build_emoji_map(emoji, users, opts[:for])
+
+          {emoji, users} when is_list(users) ->
+            build_emoji_map(emoji, users, opts[:for])
+
+          _ ->
+            nil
         end)
+        |> Enum.reject(&is_nil/1)
       else
         _ -> []
       end
 
+    # Status muted state (would do 1 request per status unless user mutes are preloaded)
+    muted =
+      thread_muted? ||
+        UserRelationship.exists?(
+          get_in(opts, [:relationships, :user_relationships]),
+          :mute,
+          opts[:for],
+          user,
+          fn for_user, user -> User.mutes?(for_user, user) end
+        )
+
     %{
       id: to_string(activity.id),
       uri: object.data["id"],
       url: url,
-      account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
+      account:
+        AccountView.render("show.json", %{
+          user: user,
+          for: opts[:for]
+        }),
       in_reply_to_id: reply_to && to_string(reply_to.id),
       in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
       reblog: nil,
       card: card,
       content: content_html,
+      text: opts[:with_source] && object.data["source"],
       created_at: created_at,
       reblogs_count: announcement_count,
       replies_count: object.data["repliesCount"] || 0,
@@ -270,7 +360,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
       reblogged: reblogged?(activity, opts[:for]),
       favourited: present?(favorited),
       bookmarked: present?(bookmarked),
-      muted: thread_muted? || User.mutes?(opts[:for], user),
+      muted: muted,
       pinned: pinned?(activity, user),
       sensitive: sensitive,
       spoiler_text: summary,
@@ -294,7 +384,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
         expires_at: expires_at,
         direct_conversation_id: direct_conversation_id,
         thread_muted: thread_muted?,
-        emoji_reactions: emoji_reactions
+        emoji_reactions: emoji_reactions,
+        parent_visible: visible_for_user?(reply_to, opts[:for])
       }
     }
   end
@@ -364,27 +455,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
     }
   end
 
-  def render("listen.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
-    object = Object.normalize(activity)
-
-    user = get_user(activity.data["actor"])
-    created_at = Utils.to_masto_date(activity.data["published"])
-
-    %{
-      id: activity.id,
-      account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
-      created_at: created_at,
-      title: object.data["title"] |> HTML.strip_tags(),
-      artist: object.data["artist"] |> HTML.strip_tags(),
-      album: object.data["album"] |> HTML.strip_tags(),
-      length: object.data["length"]
-    }
-  end
-
-  def render("listens.json", opts) do
-    safe_render_many(opts.activities, StatusView, "listen.json", opts)
-  end
-
   def render("context.json", %{activity: activity, activities: activities, user: user}) do
     %{ancestors: ancestors, descendants: descendants} =
       activities
@@ -418,23 +488,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
     end
   end
 
-  def render_content(%{data: %{"type" => object_type}} = object)
-      when object_type in ["Video", "Event", "Audio"] do
-    with name when not is_nil(name) and name != "" <- object.data["name"] do
-      "<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
-    else
-      _ -> object.data["content"] || ""
-    end
-  end
+  def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do
+    url = object.data["url"] || object.data["id"]
 
-  def render_content(%{data: %{"type" => object_type}} = object)
-      when object_type in ["Article", "Page"] do
-    with summary when not is_nil(summary) and summary != "" <- object.data["name"],
-         url when is_bitstring(url) <- object.data["url"] do
-      "<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
-    else
-      _ -> object.data["content"] || ""
-    end
+    "<p><a href=\"#{url}\">#{name}</a></p>#{object.data["content"]}"
   end
 
   def render_content(object), do: object.data["content"] || ""
@@ -451,11 +508,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
   """
   @spec build_tags(list(any())) :: list(map())
   def build_tags(object_tags) when is_list(object_tags) do
-    object_tags = for tag when is_binary(tag) <- object_tags, do: tag
-
-    Enum.reduce(object_tags, [], fn tag, tags ->
-      tags ++ [%{name: tag, url: "/tag/#{URI.encode(tag)}"}]
-    end)
+    object_tags
+    |> Enum.filter(&is_binary/1)
+    |> Enum.map(&%{name: &1, url: "/tag/#{URI.encode(&1)}"})
   end
 
   def build_tags(_), do: []
@@ -496,4 +551,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
 
   defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}),
     do: id in pinned_activities
+
+  defp build_emoji_map(emoji, users, current_user) do
+    %{
+      name: emoji,
+      count: length(users),
+      me: !!(current_user && current_user.ap_id in users)
+    }
+  end
 end