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 defp get_user(ap_id) do
50 user = User.get_cached_by_ap_id(ap_id) ->
53 user = User.get_by_guessed_nickname(ap_id) ->
57 User.error_user(ap_id)
61 defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id),
64 defp get_context_id(%{data: %{"context" => context}}) when is_binary(context),
65 do: Utils.context_to_conversation_id(context)
67 defp get_context_id(_), do: nil
69 defp reblogged?(activity, user) do
70 object = Object.normalize(activity) || %{}
71 present?(user && user.ap_id in (object.data["announcements"] || []))
74 def render("index.json", opts) do
75 # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
76 activities = Enum.filter(opts.activities, & &1)
77 replied_to_activities = get_replied_to_activities(activities)
81 |> Enum.filter(&(&1.data["type"] == "Announce" && &1.data["object"]))
82 |> Enum.map(&Object.normalize(&1).data["id"])
83 |> Activity.create_by_object_ap_id()
84 |> Activity.with_preloaded_object(:left)
85 |> Activity.with_preloaded_bookmark(opts[:for])
86 |> Activity.with_set_thread_muted_field(opts[:for])
91 Map.has_key?(opts, :relationships) ->
95 UserRelationship.view_relationships_option(nil, [])
98 actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"]))
100 UserRelationship.view_relationships_option(opts[:for], actors)
105 |> Map.put(:replied_to_activities, replied_to_activities)
106 |> Map.put(:parent_activities, parent_activities)
107 |> Map.put(:relationships, relationships_opt)
109 safe_render_many(activities, StatusView, "show.json", opts)
114 %{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
116 user = get_user(activity.data["actor"])
117 created_at = Utils.to_masto_date(activity.data["published"])
118 activity_object = Object.normalize(activity)
120 reblogged_parent_activity =
121 if opts[:parent_activities] do
122 Activity.Queries.find_by_object_ap_id(
123 opts[:parent_activities],
124 activity_object.data["id"]
127 Activity.create_by_object_ap_id(activity_object.data["id"])
128 |> Activity.with_preloaded_bookmark(opts[:for])
129 |> Activity.with_set_thread_muted_field(opts[:for])
133 reblog_rendering_opts = Map.put(opts, :activity, reblogged_parent_activity)
134 reblogged = render("show.json", reblog_rendering_opts)
136 favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
138 bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil
142 |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
144 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
147 id: to_string(activity.id),
148 uri: activity_object.data["id"],
149 url: activity_object.data["id"],
151 AccountView.render("show.json", %{
154 relationships: opts[:relationships]
157 in_reply_to_account_id: nil,
159 content: reblogged[:content] || "",
160 created_at: created_at,
164 reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
165 favourited: present?(favorited),
166 bookmarked: present?(bookmarked),
168 pinned: pinned?(activity, user),
171 visibility: get_visibility(activity),
172 media_attachments: reblogged[:media_attachments] || [],
174 tags: reblogged[:tags] || [],
182 local: activity.local
187 def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
188 object = Object.normalize(activity)
190 user = get_user(activity.data["actor"])
191 user_follower_address = user.follower_address
193 like_count = object.data["like_count"] || 0
194 announcement_count = object.data["announcement_count"] || 0
196 tags = object.data["tag"] || []
197 sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw")
201 |> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end)
202 |> Enum.map(fn tag -> tag["href"] end)
205 (object.data["to"] ++ tag_mentions)
208 Pleroma.Constants.as_public() -> nil
209 ^user_follower_address -> nil
210 ap_id -> User.get_cached_by_ap_id(ap_id)
213 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
215 favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
217 bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
219 client_posted_this_activity = opts[:for] && user.id == opts[:for].id
222 with true <- client_posted_this_activity,
223 %ActivityExpiration{scheduled_at: scheduled_at} <-
224 ActivityExpiration.get_by_activity_id(activity.id) do
232 is_nil(opts[:for]) -> false
233 is_boolean(activity.thread_muted?) -> activity.thread_muted?
234 true -> CommonAPI.thread_muted?(opts[:for], activity)
237 attachment_data = object.data["attachment"] || []
238 attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
240 created_at = Utils.to_masto_date(object.data["published"])
242 reply_to = get_reply_to(activity, opts)
244 reply_to_user = reply_to && get_user(reply_to.data["actor"])
252 |> HTML.get_cached_scrubbed_html_for_activity(
253 User.html_filter_policy(opts[:for]),
260 |> HTML.get_cached_stripped_html_for_activity(
265 summary = object.data["summary"] || ""
267 card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
271 Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
273 object.data["url"] || object.data["external_url"] || object.data["id"]
276 direct_conversation_id =
277 with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]},
278 {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
279 {_, %User{} = for_user} <- {:for_user, opts[:for]} do
280 Activity.direct_conversation_id(activity, for_user)
282 {:direct_conversation_id, participation_id} when is_integer(participation_id) ->
290 with %{data: %{"reactions" => emoji_reactions}} <- object do
291 Enum.map(emoji_reactions, fn [emoji, users] ->
294 count: length(users),
295 me: !!(opts[:for] && opts[:for].ap_id in users)
304 UserRelationship.exists?(
305 get_in(opts, [:relationships, :user_relationships]),
309 fn for_user, user -> User.mutes?(for_user, user) end
313 id: to_string(activity.id),
314 uri: object.data["id"],
317 AccountView.render("show.json", %{
320 relationships: opts[:relationships]
322 in_reply_to_id: reply_to && to_string(reply_to.id),
323 in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
326 content: content_html,
327 created_at: created_at,
328 reblogs_count: announcement_count,
329 replies_count: object.data["repliesCount"] || 0,
330 favourites_count: like_count,
331 reblogged: reblogged?(activity, opts[:for]),
332 favourited: present?(favorited),
333 bookmarked: present?(bookmarked),
335 pinned: pinned?(activity, user),
336 sensitive: sensitive,
337 spoiler_text: summary,
338 visibility: get_visibility(object),
339 media_attachments: attachments,
340 poll: render(PollView, "show.json", object: object, for: opts[:for]),
342 tags: build_tags(tags),
348 emojis: build_emojis(object.data["emoji"]),
350 local: activity.local,
351 conversation_id: get_context_id(activity),
352 in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
353 content: %{"text/plain" => content_plaintext},
354 spoiler_text: %{"text/plain" => summary},
355 expires_at: expires_at,
356 direct_conversation_id: direct_conversation_id,
357 thread_muted: thread_muted?,
358 emoji_reactions: emoji_reactions
363 def render("show.json", _) do
367 def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
368 page_url_data = URI.parse(page_url)
371 if rich_media[:url] != nil do
372 URI.merge(page_url_data, URI.parse(rich_media[:url]))
377 page_url = page_url_data |> to_string
380 if rich_media[:image] != nil do
381 URI.merge(page_url_data, URI.parse(rich_media[:image]))
389 provider_name: page_url_data.host,
390 provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
392 image: image_url |> MediaProxy.url(),
393 title: rich_media[:title] || "",
394 description: rich_media[:description] || "",
396 opengraph: rich_media
401 def render("card.json", _), do: nil
403 def render("attachment.json", %{attachment: attachment}) do
404 [attachment_url | _] = attachment["url"]
405 media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
406 href = attachment_url["href"] |> MediaProxy.url()
410 String.contains?(media_type, "image") -> "image"
411 String.contains?(media_type, "video") -> "video"
412 String.contains?(media_type, "audio") -> "audio"
416 <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
419 id: to_string(attachment["id"] || hash_id),
425 description: attachment["name"],
426 pleroma: %{mime_type: media_type}
430 def render("listen.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
431 object = Object.normalize(activity)
433 user = get_user(activity.data["actor"])
434 created_at = Utils.to_masto_date(activity.data["published"])
438 account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
439 created_at: created_at,
440 title: object.data["title"] |> HTML.strip_tags(),
441 artist: object.data["artist"] |> HTML.strip_tags(),
442 album: object.data["album"] |> HTML.strip_tags(),
443 length: object.data["length"]
447 def render("listens.json", opts) do
448 safe_render_many(opts.activities, StatusView, "listen.json", opts)
451 def render("context.json", %{activity: activity, activities: activities, user: user}) do
452 %{ancestors: ancestors, descendants: descendants} =
455 |> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
456 |> Map.put_new(:ancestors, [])
457 |> Map.put_new(:descendants, [])
460 ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
461 descendants: render("index.json", for: user, activities: descendants, as: :activity)
465 def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
466 object = Object.normalize(activity)
468 with nil <- replied_to_activities[object.data["inReplyTo"]] do
469 # If user didn't participate in the thread
470 Activity.get_in_reply_to_activity(activity)
474 def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
475 object = Object.normalize(activity)
477 if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
478 Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
484 def render_content(%{data: %{"type" => object_type}} = object)
485 when object_type in ["Video", "Event"] do
486 with name when not is_nil(name) and name != "" <- object.data["name"] do
487 "<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
489 _ -> object.data["content"] || ""
493 def render_content(%{data: %{"type" => object_type}} = object)
494 when object_type in ["Article", "Page"] do
495 with summary when not is_nil(summary) and summary != "" <- object.data["name"],
496 url when is_bitstring(url) <- object.data["url"] do
497 "<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
499 _ -> object.data["content"] || ""
503 def render_content(object), do: object.data["content"] || ""
506 Builds a dictionary tags.
510 iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
511 [{"name": "fediverse", "url": "/tag/fediverse"},
512 {"name": "nextcloud", "url": "/tag/nextcloud"}]
515 @spec build_tags(list(any())) :: list(map())
516 def build_tags(object_tags) when is_list(object_tags) do
517 object_tags = for tag when is_binary(tag) <- object_tags, do: tag
519 Enum.reduce(object_tags, [], fn tag, tags ->
520 tags ++ [%{name: tag, url: "/tag/#{URI.encode(tag)}"}]
524 def build_tags(_), do: []
529 Arguments: `nil` or list tuple of name and url.
535 iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
536 [%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
539 @spec build_emojis(nil | list(tuple())) :: list(map())
540 def build_emojis(nil), do: []
542 def build_emojis(emojis) do
544 |> Enum.map(fn {name, url} ->
545 name = HTML.strip_tags(name)
552 %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
556 defp present?(nil), do: false
557 defp present?(false), do: false
558 defp present?(_), do: true
560 defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}),
561 do: id in pinned_activities