1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.StatusView do
8 require Pleroma.Constants
10 alias Pleroma.Activity
11 alias Pleroma.ActivityExpiration
16 alias Pleroma.UserRelationship
17 alias Pleroma.Web.CommonAPI
18 alias Pleroma.Web.CommonAPI.Utils
19 alias Pleroma.Web.MastodonAPI.AccountView
20 alias Pleroma.Web.MastodonAPI.PollView
21 alias Pleroma.Web.MastodonAPI.StatusView
22 alias Pleroma.Web.MediaProxy
24 import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1]
26 # TODO: Add cached version.
27 defp get_replied_to_activities([]), do: %{}
29 defp get_replied_to_activities(activities) do
32 %{data: %{"type" => "Create"}} = activity ->
33 object = Object.normalize(activity)
34 object && object.data["inReplyTo"] != "" && object.data["inReplyTo"]
40 |> Activity.create_by_object_ap_id_with_object()
42 |> Enum.reduce(%{}, fn activity, acc ->
43 object = Object.normalize(activity)
44 if object, do: Map.put(acc, object.data["id"], activity), else: acc
48 def get_user(ap_id, fake_record_fallback \\ true) do
50 user = User.get_cached_by_ap_id(ap_id) ->
53 user = User.get_by_guessed_nickname(ap_id) ->
56 fake_record_fallback ->
57 # TODO: refactor (fake records is never a good idea)
58 User.error_user(ap_id)
65 defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id),
68 defp get_context_id(%{data: %{"context" => context}}) when is_binary(context),
69 do: Utils.context_to_conversation_id(context)
71 defp get_context_id(_), do: nil
73 defp reblogged?(activity, user) do
74 object = Object.normalize(activity) || %{}
75 present?(user && user.ap_id in (object.data["announcements"] || []))
78 def render("index.json", opts) do
79 reading_user = opts[:for]
81 # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
82 activities = Enum.filter(opts.activities, & &1)
83 replied_to_activities = get_replied_to_activities(activities)
87 |> Enum.filter(&(&1.data["type"] == "Announce" && &1.data["object"]))
88 |> Enum.map(&Object.normalize(&1).data["id"])
89 |> Activity.create_by_object_ap_id()
90 |> Activity.with_preloaded_object(:left)
91 |> Activity.with_preloaded_bookmark(reading_user)
92 |> Activity.with_set_thread_muted_field(reading_user)
97 Map.has_key?(opts, :relationships) ->
100 is_nil(reading_user) ->
101 UserRelationship.view_relationships_option(nil, [])
104 # Note: unresolved users are filtered out
106 (activities ++ parent_activities)
107 |> Enum.map(&get_user(&1.data["actor"], false))
110 UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes)
115 |> Map.put(:replied_to_activities, replied_to_activities)
116 |> Map.put(:parent_activities, parent_activities)
117 |> Map.put(:relationships, relationships_opt)
119 safe_render_many(activities, StatusView, "show.json", opts)
124 %{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
126 user = get_user(activity.data["actor"])
127 created_at = Utils.to_masto_date(activity.data["published"])
128 activity_object = Object.normalize(activity)
130 reblogged_parent_activity =
131 if opts[:parent_activities] do
132 Activity.Queries.find_by_object_ap_id(
133 opts[:parent_activities],
134 activity_object.data["id"]
137 Activity.create_by_object_ap_id(activity_object.data["id"])
138 |> Activity.with_preloaded_bookmark(opts[:for])
139 |> Activity.with_set_thread_muted_field(opts[:for])
143 reblog_rendering_opts = Map.put(opts, :activity, reblogged_parent_activity)
144 reblogged = render("show.json", reblog_rendering_opts)
146 favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
148 bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil
152 |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
154 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
157 id: to_string(activity.id),
158 uri: activity_object.data["id"],
159 url: activity_object.data["id"],
161 AccountView.render("show.json", %{
166 in_reply_to_account_id: nil,
168 content: reblogged[:content] || "",
169 created_at: created_at,
173 reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
174 favourited: present?(favorited),
175 bookmarked: present?(bookmarked),
177 pinned: pinned?(activity, user),
180 visibility: get_visibility(activity),
181 media_attachments: reblogged[:media_attachments] || [],
183 tags: reblogged[:tags] || [],
191 local: activity.local
196 def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
197 object = Object.normalize(activity)
199 user = get_user(activity.data["actor"])
200 user_follower_address = user.follower_address
202 like_count = object.data["like_count"] || 0
203 announcement_count = object.data["announcement_count"] || 0
205 tags = object.data["tag"] || []
206 sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw")
210 |> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end)
211 |> Enum.map(fn tag -> tag["href"] end)
214 (object.data["to"] ++ tag_mentions)
217 Pleroma.Constants.as_public() -> nil
218 ^user_follower_address -> nil
219 ap_id -> User.get_cached_by_ap_id(ap_id)
222 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
224 favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
226 bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
228 client_posted_this_activity = opts[:for] && user.id == opts[:for].id
231 with true <- client_posted_this_activity,
232 %ActivityExpiration{scheduled_at: scheduled_at} <-
233 ActivityExpiration.get_by_activity_id(activity.id) do
241 is_nil(opts[:for]) -> false
242 is_boolean(activity.thread_muted?) -> activity.thread_muted?
243 true -> CommonAPI.thread_muted?(opts[:for], activity)
246 attachment_data = object.data["attachment"] || []
247 attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
249 created_at = Utils.to_masto_date(object.data["published"])
251 reply_to = get_reply_to(activity, opts)
253 reply_to_user = reply_to && get_user(reply_to.data["actor"])
261 |> HTML.get_cached_scrubbed_html_for_activity(
262 User.html_filter_policy(opts[:for]),
269 |> HTML.get_cached_stripped_html_for_activity(
274 summary = object.data["summary"] || ""
276 card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
280 Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
282 object.data["url"] || object.data["external_url"] || object.data["id"]
285 direct_conversation_id =
286 with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]},
287 {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
288 {_, %User{} = for_user} <- {:for_user, opts[:for]} do
289 Activity.direct_conversation_id(activity, for_user)
291 {:direct_conversation_id, participation_id} when is_integer(participation_id) ->
299 with %{data: %{"reactions" => emoji_reactions}} <- object do
300 Enum.map(emoji_reactions, fn [emoji, users] ->
303 count: length(users),
304 me: !!(opts[:for] && opts[:for].ap_id in users)
311 # Status muted state (would do 1 request per status unless user mutes are preloaded)
314 UserRelationship.exists?(
315 get_in(opts, [:relationships, :user_relationships]),
319 fn for_user, user -> User.mutes?(for_user, user) end
323 id: to_string(activity.id),
324 uri: object.data["id"],
327 AccountView.render("show.json", %{
331 in_reply_to_id: reply_to && to_string(reply_to.id),
332 in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
335 content: content_html,
336 created_at: created_at,
337 reblogs_count: announcement_count,
338 replies_count: object.data["repliesCount"] || 0,
339 favourites_count: like_count,
340 reblogged: reblogged?(activity, opts[:for]),
341 favourited: present?(favorited),
342 bookmarked: present?(bookmarked),
344 pinned: pinned?(activity, user),
345 sensitive: sensitive,
346 spoiler_text: summary,
347 visibility: get_visibility(object),
348 media_attachments: attachments,
349 poll: render(PollView, "show.json", object: object, for: opts[:for]),
351 tags: build_tags(tags),
357 emojis: build_emojis(object.data["emoji"]),
359 local: activity.local,
360 conversation_id: get_context_id(activity),
361 in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
362 content: %{"text/plain" => content_plaintext},
363 spoiler_text: %{"text/plain" => summary},
364 expires_at: expires_at,
365 direct_conversation_id: direct_conversation_id,
366 thread_muted: thread_muted?,
367 emoji_reactions: emoji_reactions
372 def render("show.json", _) do
376 def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
377 page_url_data = URI.parse(page_url)
380 if is_binary(rich_media["url"]) do
381 URI.merge(page_url_data, URI.parse(rich_media["url"]))
386 page_url = page_url_data |> to_string
389 if is_binary(rich_media["image"]) do
390 URI.merge(page_url_data, URI.parse(rich_media["image"]))
396 provider_name: page_url_data.host,
397 provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
399 image: image_url |> MediaProxy.url(),
400 title: rich_media["title"] || "",
401 description: rich_media["description"] || "",
403 opengraph: rich_media
408 def render("card.json", _), do: nil
410 def render("attachment.json", %{attachment: attachment}) do
411 [attachment_url | _] = attachment["url"]
412 media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
413 href = attachment_url["href"] |> MediaProxy.url()
417 String.contains?(media_type, "image") -> "image"
418 String.contains?(media_type, "video") -> "video"
419 String.contains?(media_type, "audio") -> "audio"
423 <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
426 id: to_string(attachment["id"] || hash_id),
432 description: attachment["name"],
433 pleroma: %{mime_type: media_type}
437 def render("context.json", %{activity: activity, activities: activities, user: user}) do
438 %{ancestors: ancestors, descendants: descendants} =
441 |> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
442 |> Map.put_new(:ancestors, [])
443 |> Map.put_new(:descendants, [])
446 ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
447 descendants: render("index.json", for: user, activities: descendants, as: :activity)
451 def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
452 object = Object.normalize(activity)
454 with nil <- replied_to_activities[object.data["inReplyTo"]] do
455 # If user didn't participate in the thread
456 Activity.get_in_reply_to_activity(activity)
460 def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
461 object = Object.normalize(activity)
463 if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
464 Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
470 def render_content(%{data: %{"type" => object_type}} = object)
471 when object_type in ["Video", "Event", "Audio"] do
472 with name when not is_nil(name) and name != "" <- object.data["name"] do
473 "<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
475 _ -> object.data["content"] || ""
479 def render_content(%{data: %{"type" => object_type}} = object)
480 when object_type in ["Article", "Page"] do
481 with summary when not is_nil(summary) and summary != "" <- object.data["name"],
482 url when is_bitstring(url) <- object.data["url"] do
483 "<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
485 _ -> object.data["content"] || ""
489 def render_content(object), do: object.data["content"] || ""
492 Builds a dictionary tags.
496 iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
497 [{"name": "fediverse", "url": "/tag/fediverse"},
498 {"name": "nextcloud", "url": "/tag/nextcloud"}]
501 @spec build_tags(list(any())) :: list(map())
502 def build_tags(object_tags) when is_list(object_tags) do
504 |> Enum.filter(&is_binary/1)
505 |> Enum.map(&%{name: &1, url: "/tag/#{URI.encode(&1)}"})
508 def build_tags(_), do: []
513 Arguments: `nil` or list tuple of name and url.
519 iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
520 [%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
523 @spec build_emojis(nil | list(tuple())) :: list(map())
524 def build_emojis(nil), do: []
526 def build_emojis(emojis) do
528 |> Enum.map(fn {name, url} ->
529 name = HTML.strip_tags(name)
536 %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
540 defp present?(nil), do: false
541 defp present?(false), do: false
542 defp present?(_), do: true
544 defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}),
545 do: id in pinned_activities