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, visible_for_user?: 2]
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 text: opts[:with_source] && object.data["source"],
337 created_at: created_at,
338 reblogs_count: announcement_count,
339 replies_count: object.data["repliesCount"] || 0,
340 favourites_count: like_count,
341 reblogged: reblogged?(activity, opts[:for]),
342 favourited: present?(favorited),
343 bookmarked: present?(bookmarked),
345 pinned: pinned?(activity, user),
346 sensitive: sensitive,
347 spoiler_text: summary,
348 visibility: get_visibility(object),
349 media_attachments: attachments,
350 poll: render(PollView, "show.json", object: object, for: opts[:for]),
352 tags: build_tags(tags),
358 emojis: build_emojis(object.data["emoji"]),
360 local: activity.local,
361 conversation_id: get_context_id(activity),
362 in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
363 content: %{"text/plain" => content_plaintext},
364 spoiler_text: %{"text/plain" => summary},
365 expires_at: expires_at,
366 direct_conversation_id: direct_conversation_id,
367 thread_muted: thread_muted?,
368 emoji_reactions: emoji_reactions,
369 parent_visible: visible_for_user?(reply_to, opts[:for])
374 def render("show.json", _) do
378 def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
379 page_url_data = URI.parse(page_url)
382 if is_binary(rich_media["url"]) do
383 URI.merge(page_url_data, URI.parse(rich_media["url"]))
388 page_url = page_url_data |> to_string
391 if is_binary(rich_media["image"]) do
392 URI.merge(page_url_data, URI.parse(rich_media["image"]))
398 provider_name: page_url_data.host,
399 provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
401 image: image_url |> MediaProxy.url(),
402 title: rich_media["title"] || "",
403 description: rich_media["description"] || "",
405 opengraph: rich_media
410 def render("card.json", _), do: nil
412 def render("attachment.json", %{attachment: attachment}) do
413 [attachment_url | _] = attachment["url"]
414 media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
415 href = attachment_url["href"] |> MediaProxy.url()
419 String.contains?(media_type, "image") -> "image"
420 String.contains?(media_type, "video") -> "video"
421 String.contains?(media_type, "audio") -> "audio"
425 <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
428 id: to_string(attachment["id"] || hash_id),
434 description: attachment["name"],
435 pleroma: %{mime_type: media_type}
439 def render("context.json", %{activity: activity, activities: activities, user: user}) do
440 %{ancestors: ancestors, descendants: descendants} =
443 |> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
444 |> Map.put_new(:ancestors, [])
445 |> Map.put_new(:descendants, [])
448 ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
449 descendants: render("index.json", for: user, activities: descendants, as: :activity)
453 def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
454 object = Object.normalize(activity)
456 with nil <- replied_to_activities[object.data["inReplyTo"]] do
457 # If user didn't participate in the thread
458 Activity.get_in_reply_to_activity(activity)
462 def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
463 object = Object.normalize(activity)
465 if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
466 Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
472 def render_content(%{data: %{"type" => object_type}} = object)
473 when object_type in ["Video", "Event", "Audio"] do
474 with name when not is_nil(name) and name != "" <- object.data["name"] do
475 "<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
477 _ -> object.data["content"] || ""
481 def render_content(%{data: %{"type" => object_type}} = object)
482 when object_type in ["Article", "Page"] do
483 with summary when not is_nil(summary) and summary != "" <- object.data["name"],
484 url when is_bitstring(url) <- object.data["url"] do
485 "<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
487 _ -> object.data["content"] || ""
491 def render_content(object), do: object.data["content"] || ""
494 Builds a dictionary tags.
498 iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
499 [{"name": "fediverse", "url": "/tag/fediverse"},
500 {"name": "nextcloud", "url": "/tag/nextcloud"}]
503 @spec build_tags(list(any())) :: list(map())
504 def build_tags(object_tags) when is_list(object_tags) do
506 |> Enum.filter(&is_binary/1)
507 |> Enum.map(&%{name: &1, url: "/tag/#{URI.encode(&1)}"})
510 def build_tags(_), do: []
515 Arguments: `nil` or list tuple of name and url.
521 iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
522 [%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
525 @spec build_emojis(nil | list(tuple())) :: list(map())
526 def build_emojis(nil), do: []
528 def build_emojis(emojis) do
530 |> Enum.map(fn {name, url} ->
531 name = HTML.strip_tags(name)
538 %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
542 defp present?(nil), do: false
543 defp present?(false), do: false
544 defp present?(_), do: true
546 defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}),
547 do: id in pinned_activities