X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Fpleroma%2Fweb%2Fmastodon_api%2Fviews%2Fstatus_view.ex;h=84ab20a1c8cdd00caab9a476a35cc0dab70f4906;hb=7f692343c80ddf353712490edfbcdb14866f5685;hp=200bb453dd04dc07e3c090751969e7520d6b6384;hpb=926bf114b7385761c3cac50e262d061f47fda4b8;p=akkoma diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 200bb453d..cea76e735 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -1,35 +1,47 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors +# Copyright © 2017-2020 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.MastodonAPI.StatusView do use Pleroma.Web, :view + 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 + alias Pleroma.Web.MastodonAPI.PollView alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MediaProxy + import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1] + # TODO: Add cached version. + defp get_replied_to_activities([]), do: %{} + defp get_replied_to_activities(activities) do activities |> Enum.map(fn - %{data: %{"type" => "Create", "object" => %{"inReplyTo" => in_reply_to}}} -> - in_reply_to != "" && in_reply_to + %{data: %{"type" => "Create"}} = activity -> + object = Object.normalize(activity) + object && object.data["inReplyTo"] != "" && object.data["inReplyTo"] _ -> nil end) |> Enum.filter(& &1) - |> Activity.create_by_object_ap_id() + |> Activity.create_by_object_ap_id_with_object() |> Repo.all() |> Enum.reduce(%{}, fn activity, acc -> - Map.put(acc, activity.data["object"]["id"], activity) + object = Object.normalize(activity) + if object, do: Map.put(acc, object.data["id"], activity), else: acc end) end @@ -54,26 +66,78 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do defp get_context_id(_), do: nil + defp reblogged?(activity, user) do + object = Object.normalize(activity) || %{} + present?(user && user.ap_id in (object.data["announcements"] || [])) + end + def render("index.json", opts) do - replied_to_activities = get_replied_to_activities(opts.activities) - - opts.activities - |> safe_render_many( - StatusView, - "status.json", - 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) + 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 -> + actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"])) + + UserRelationship.view_relationships_option(reading_user, actors) + 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(activities, StatusView, "show.json", opts) end def render( - "status.json", - %{activity: %{data: %{"type" => "Announce", "object" => object}} = activity} = opts + "show.json", + %{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts ) do user = get_user(activity.data["actor"]) created_at = Utils.to_masto_date(activity.data["published"]) + activity_object = Object.normalize(activity) + + 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 + + reblog_rendering_opts = Map.put(opts, :activity, reblogged_parent_activity) + reblogged = render("show.json", reblog_rendering_opts) - reblogged = Activity.get_create_by_object_ap_id(object) - reblogged = render("status.json", Map.put(opts, :activity, reblogged)) + favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || []) + + bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil mentions = activity.recipients @@ -83,9 +147,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do %{ id: to_string(activity.id), - uri: object, - url: object, - account: AccountView.render("account.json", %{user: user}), + uri: activity_object.data["id"], + url: activity_object.data["id"], + account: + AccountView.render("show.json", %{ + user: user, + for: opts[:for], + relationships: opts[:relationships] + }), in_reply_to_id: nil, in_reply_to_account_id: nil, reblog: reblogged, @@ -94,14 +163,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do reblogs_count: 0, replies_count: 0, favourites_count: 0, - reblogged: false, - favourited: false, - bookmarked: false, + reblogged: reblogged?(reblogged_parent_activity, opts[:for]), + favourited: present?(favorited), + bookmarked: present?(bookmarked), muted: false, pinned: pinned?(activity, user), sensitive: false, spoiler_text: "", - visibility: "public", + visibility: get_visibility(activity), media_attachments: reblogged[:media_attachments] || [], mentions: mentions, tags: reblogged[:tags] || [], @@ -117,74 +186,160 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do } end - def render("status.json", %{activity: %{data: %{"object" => object}} = activity} = opts) do + def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do + object = Object.normalize(activity) + user = get_user(activity.data["actor"]) + user_follower_address = user.follower_address + + like_count = object.data["like_count"] || 0 + announcement_count = object.data["announcement_count"] || 0 - like_count = object["like_count"] || 0 - announcement_count = object["announcement_count"] || 0 + tags = object.data["tag"] || [] + sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw") - tags = object["tag"] || [] - sensitive = object["sensitive"] || Enum.member?(tags, "nsfw") + tag_mentions = + tags + |> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end) + |> Enum.map(fn tag -> tag["href"] end) mentions = - activity.recipients - |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end) + (object.data["to"] ++ tag_mentions) + |> Enum.uniq() + |> Enum.map(fn + Pleroma.Constants.as_public() -> nil + ^user_follower_address -> nil + ap_id -> User.get_cached_by_ap_id(ap_id) + end) |> Enum.filter(& &1) |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end) - repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || []) - favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || []) - bookmarked = opts[:for] && object["id"] in opts[:for].bookmarks + favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || []) - attachment_data = object["attachment"] || [] + bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil + + client_posted_this_activity = opts[:for] && user.id == opts[:for].id + + expires_at = + with true <- client_posted_this_activity, + %ActivityExpiration{scheduled_at: scheduled_at} <- + ActivityExpiration.get_by_activity_id(activity.id) do + scheduled_at + else + _ -> nil + end + + thread_muted? = + 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"] || [] attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment) - created_at = Utils.to_masto_date(object["published"]) + created_at = Utils.to_masto_date(object.data["published"]) reply_to = get_reply_to(activity, opts) + reply_to_user = reply_to && get_user(reply_to.data["actor"]) content = object |> render_content() - |> HTML.get_cached_scrubbed_html_for_object( + + content_html = + content + |> HTML.get_cached_scrubbed_html_for_activity( User.html_filter_policy(opts[:for]), activity, - __MODULE__ + "mastoapi:content" + ) + + content_plaintext = + content + |> HTML.get_cached_stripped_html_for_activity( + activity, + "mastoapi:content" ) + summary = object.data["summary"] || "" + card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)) url = if user.local do Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity) else - object["external_url"] || object["id"] + object.data["url"] || object.data["external_url"] || object.data["id"] + end + + direct_conversation_id = + with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]}, + {_, true} <- {:include_id, opts[:with_direct_conversation_id]}, + {_, %User{} = for_user} <- {:for_user, opts[:for]} do + Activity.direct_conversation_id(activity, for_user) + else + {:direct_conversation_id, participation_id} when is_integer(participation_id) -> + participation_id + + _e -> + nil + end + + 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) + } + end) + else + _ -> [] end + 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["id"], + uri: object.data["id"], url: url, - account: AccountView.render("account.json", %{user: user}), + account: + AccountView.render("show.json", %{ + user: user, + for: opts[:for], + relationships: opts[:relationships] + }), 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, + content: content_html, created_at: created_at, reblogs_count: announcement_count, - replies_count: object["repliesCount"] || 0, + replies_count: object.data["repliesCount"] || 0, favourites_count: like_count, - reblogged: present?(repeated), + reblogged: reblogged?(activity, opts[:for]), favourited: present?(favorited), bookmarked: present?(bookmarked), - muted: CommonAPI.thread_muted?(user, activity) || User.mutes?(opts[:for], user), + muted: muted, pinned: pinned?(activity, user), sensitive: sensitive, - spoiler_text: object["summary"] || "", + spoiler_text: summary, visibility: get_visibility(object), media_attachments: attachments, + poll: render(PollView, "show.json", object: object, for: opts[:for]), mentions: mentions, tags: build_tags(tags), application: %{ @@ -192,15 +347,22 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do website: nil }, language: nil, - emojis: build_emojis(activity.data["object"]["emoji"]), + emojis: build_emojis(object.data["emoji"]), pleroma: %{ local: activity.local, - conversation_id: get_context_id(activity) + conversation_id: get_context_id(activity), + in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, + content: %{"text/plain" => content_plaintext}, + spoiler_text: %{"text/plain" => summary}, + expires_at: expires_at, + direct_conversation_id: direct_conversation_id, + thread_muted: thread_muted?, + emoji_reactions: emoji_reactions } } end - def render("status.json", _) do + def render("show.json", _) do nil end @@ -224,25 +386,21 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do nil end - site_name = rich_media[:site_name] || page_url_data.host - %{ type: "link", - provider_name: site_name, + provider_name: page_url_data.host, provider_url: page_url_data.scheme <> "://" <> page_url_data.host, url: page_url, image: image_url |> MediaProxy.url(), - title: rich_media[:title], - description: rich_media[:description], + title: rich_media[:title] || "", + description: rich_media[:description] || "", pleroma: %{ opengraph: rich_media } } end - def render("card.json", _) do - nil - end + def render("card.json", _), do: nil def render("attachment.json", %{attachment: attachment}) do [attachment_url | _] = attachment["url"] @@ -271,62 +429,80 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do } end - def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do - _id = activity.data["object"]["inReplyTo"] - replied_to_activities[activity.data["object"]["inReplyTo"]] + 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 get_reply_to(%{data: %{"object" => object}}, _) do - if object["inReplyTo"] && object["inReplyTo"] != "" do - Activity.get_create_by_object_ap_id(object["inReplyTo"]) - else - nil - end + def render("listens.json", opts) do + safe_render_many(opts.activities, StatusView, "listen.json", opts) end - def get_visibility(object) do - public = "https://www.w3.org/ns/activitystreams#Public" - to = object["to"] || [] - cc = object["cc"] || [] + def render("context.json", %{activity: activity, activities: activities, user: user}) do + %{ancestors: ancestors, descendants: descendants} = + activities + |> Enum.reverse() + |> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end) + |> Map.put_new(:ancestors, []) + |> Map.put_new(:descendants, []) - cond do - public in to -> - "public" + %{ + ancestors: render("index.json", for: user, activities: ancestors, as: :activity), + descendants: render("index.json", for: user, activities: descendants, as: :activity) + } + end - public in cc -> - "unlisted" + def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do + object = Object.normalize(activity) - # this should use the sql for the object's activity - Enum.any?(to, &String.contains?(&1, "/followers")) -> - "private" + with nil <- replied_to_activities[object.data["inReplyTo"]] do + # If user didn't participate in the thread + Activity.get_in_reply_to_activity(activity) + end + end - length(cc) > 0 -> - "private" + def get_reply_to(%{data: %{"object" => _object}} = activity, _) do + object = Object.normalize(activity) - true -> - "direct" + if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do + Activity.get_create_by_object_ap_id(object.data["inReplyTo"]) + else + nil end end - def render_content(%{"type" => "Video"} = object) do - with name when not is_nil(name) and name != "" <- object["name"] do - "

#{name}

#{object["content"]}" + 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 + "

#{name}

#{object.data["content"]}" else - _ -> object["content"] || "" + _ -> object.data["content"] || "" end end - def render_content(%{"type" => object_type} = object) + 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["name"], - url when is_bitstring(url) <- object["url"] do - "

#{summary}

#{object["content"]}" + with summary when not is_nil(summary) and summary != "" <- object.data["name"], + url when is_bitstring(url) <- object.data["url"] do + "

#{summary}

#{object.data["content"]}" else - _ -> object["content"] || "" + _ -> object.data["content"] || "" end end - def render_content(object), do: object["content"] || "" + def render_content(object), do: object.data["content"] || "" @doc """ Builds a dictionary tags. @@ -343,7 +519,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView 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/#{tag}"}] + tags ++ [%{name: tag, url: "/tag/#{URI.encode(tag)}"}] end) end @@ -383,6 +559,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do defp present?(false), do: false defp present?(_), do: true - defp pinned?(%Activity{id: id}, %User{info: %{pinned_activities: pinned_activities}}), + defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}), do: id in pinned_activities end