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 reading_user = opts[:for]
77 # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
78 activities = Enum.filter(opts.activities, & &1)
79 replied_to_activities = get_replied_to_activities(activities)
83 |> Enum.filter(&(&1.data["type"] == "Announce" && &1.data["object"]))
84 |> Enum.map(&Object.normalize(&1).data["id"])
85 |> Activity.create_by_object_ap_id()
86 |> Activity.with_preloaded_object(:left)
87 |> Activity.with_preloaded_bookmark(reading_user)
88 |> Activity.with_set_thread_muted_field(reading_user)
93 Map.has_key?(opts, :relationships) ->
96 is_nil(reading_user) ->
97 UserRelationship.view_relationships_option(nil, [])
100 actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"]))
102 UserRelationship.view_relationships_option(reading_user, actors)
107 |> Map.put(:replied_to_activities, replied_to_activities)
108 |> Map.put(:parent_activities, parent_activities)
109 |> Map.put(:relationships, relationships_opt)
111 safe_render_many(activities, StatusView, "show.json", opts)
116 %{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
118 user = get_user(activity.data["actor"])
119 created_at = Utils.to_masto_date(activity.data["published"])
120 activity_object = Object.normalize(activity)
122 reblogged_parent_activity =
123 if opts[:parent_activities] do
124 Activity.Queries.find_by_object_ap_id(
125 opts[:parent_activities],
126 activity_object.data["id"]
129 Activity.create_by_object_ap_id(activity_object.data["id"])
130 |> Activity.with_preloaded_bookmark(opts[:for])
131 |> Activity.with_set_thread_muted_field(opts[:for])
135 reblog_rendering_opts = Map.put(opts, :activity, reblogged_parent_activity)
136 reblogged = render("show.json", reblog_rendering_opts)
138 favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
140 bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil
144 |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
146 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
149 id: to_string(activity.id),
150 uri: activity_object.data["id"],
151 url: activity_object.data["id"],
153 AccountView.render("show.json", %{
156 relationships: opts[:relationships]
159 in_reply_to_account_id: nil,
161 content: reblogged[:content] || "",
162 created_at: created_at,
166 reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
167 favourited: present?(favorited),
168 bookmarked: present?(bookmarked),
170 pinned: pinned?(activity, user),
173 visibility: get_visibility(activity),
174 media_attachments: reblogged[:media_attachments] || [],
176 tags: reblogged[:tags] || [],
184 local: activity.local
189 def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
190 object = Object.normalize(activity)
192 user = get_user(activity.data["actor"])
193 user_follower_address = user.follower_address
195 like_count = object.data["like_count"] || 0
196 announcement_count = object.data["announcement_count"] || 0
198 tags = object.data["tag"] || []
199 sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw")
203 |> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end)
204 |> Enum.map(fn tag -> tag["href"] end)
207 (object.data["to"] ++ tag_mentions)
210 Pleroma.Constants.as_public() -> nil
211 ^user_follower_address -> nil
212 ap_id -> User.get_cached_by_ap_id(ap_id)
215 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
217 favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
219 bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
221 client_posted_this_activity = opts[:for] && user.id == opts[:for].id
224 with true <- client_posted_this_activity,
225 %ActivityExpiration{scheduled_at: scheduled_at} <-
226 ActivityExpiration.get_by_activity_id(activity.id) do
234 is_nil(opts[:for]) -> false
235 is_boolean(activity.thread_muted?) -> activity.thread_muted?
236 true -> CommonAPI.thread_muted?(opts[:for], activity)
239 attachment_data = object.data["attachment"] || []
240 attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
242 created_at = Utils.to_masto_date(object.data["published"])
244 reply_to = get_reply_to(activity, opts)
246 reply_to_user = reply_to && get_user(reply_to.data["actor"])
254 |> HTML.get_cached_scrubbed_html_for_activity(
255 User.html_filter_policy(opts[:for]),
262 |> HTML.get_cached_stripped_html_for_activity(
267 summary = object.data["summary"] || ""
269 card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
273 Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
275 object.data["url"] || object.data["external_url"] || object.data["id"]
278 direct_conversation_id =
279 with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]},
280 {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
281 {_, %User{} = for_user} <- {:for_user, opts[:for]} do
282 Activity.direct_conversation_id(activity, for_user)
284 {:direct_conversation_id, participation_id} when is_integer(participation_id) ->
292 with %{data: %{"reactions" => emoji_reactions}} <- object do
293 Enum.map(emoji_reactions, fn [emoji, users] ->
296 count: length(users),
297 me: !!(opts[:for] && opts[:for].ap_id in users)
306 UserRelationship.exists?(
307 get_in(opts, [:relationships, :user_relationships]),
311 fn for_user, user -> User.mutes?(for_user, user) end
315 id: to_string(activity.id),
316 uri: object.data["id"],
319 AccountView.render("show.json", %{
322 relationships: opts[:relationships]
324 in_reply_to_id: reply_to && to_string(reply_to.id),
325 in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
328 content: content_html,
329 created_at: created_at,
330 reblogs_count: announcement_count,
331 replies_count: object.data["repliesCount"] || 0,
332 favourites_count: like_count,
333 reblogged: reblogged?(activity, opts[:for]),
334 favourited: present?(favorited),
335 bookmarked: present?(bookmarked),
337 pinned: pinned?(activity, user),
338 sensitive: sensitive,
339 spoiler_text: summary,
340 visibility: get_visibility(object),
341 media_attachments: attachments,
342 poll: render(PollView, "show.json", object: object, for: opts[:for]),
344 tags: build_tags(tags),
350 emojis: build_emojis(object.data["emoji"]),
352 local: activity.local,
353 conversation_id: get_context_id(activity),
354 in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
355 content: %{"text/plain" => content_plaintext},
356 spoiler_text: %{"text/plain" => summary},
357 expires_at: expires_at,
358 direct_conversation_id: direct_conversation_id,
359 thread_muted: thread_muted?,
360 emoji_reactions: emoji_reactions
365 def render("show.json", _) do
369 def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
370 page_url_data = URI.parse(page_url)
373 if rich_media[:url] != nil do
374 URI.merge(page_url_data, URI.parse(rich_media[:url]))
379 page_url = page_url_data |> to_string
382 if rich_media[:image] != nil do
383 URI.merge(page_url_data, URI.parse(rich_media[:image]))
391 provider_name: page_url_data.host,
392 provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
394 image: image_url |> MediaProxy.url(),
395 title: rich_media[:title] || "",
396 description: rich_media[:description] || "",
398 opengraph: rich_media
403 def render("card.json", _), do: nil
405 def render("attachment.json", %{attachment: attachment}) do
406 [attachment_url | _] = attachment["url"]
407 media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
408 href = attachment_url["href"] |> MediaProxy.url()
412 String.contains?(media_type, "image") -> "image"
413 String.contains?(media_type, "video") -> "video"
414 String.contains?(media_type, "audio") -> "audio"
418 <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
421 id: to_string(attachment["id"] || hash_id),
427 description: attachment["name"],
428 pleroma: %{mime_type: media_type}
432 def render("listen.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
433 object = Object.normalize(activity)
435 user = get_user(activity.data["actor"])
436 created_at = Utils.to_masto_date(activity.data["published"])
440 account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
441 created_at: created_at,
442 title: object.data["title"] |> HTML.strip_tags(),
443 artist: object.data["artist"] |> HTML.strip_tags(),
444 album: object.data["album"] |> HTML.strip_tags(),
445 length: object.data["length"]
449 def render("listens.json", opts) do
450 safe_render_many(opts.activities, StatusView, "listen.json", opts)
453 def render("context.json", %{activity: activity, activities: activities, user: user}) do
454 %{ancestors: ancestors, descendants: descendants} =
457 |> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
458 |> Map.put_new(:ancestors, [])
459 |> Map.put_new(:descendants, [])
462 ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
463 descendants: render("index.json", for: user, activities: descendants, as: :activity)
467 def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
468 object = Object.normalize(activity)
470 with nil <- replied_to_activities[object.data["inReplyTo"]] do
471 # If user didn't participate in the thread
472 Activity.get_in_reply_to_activity(activity)
476 def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
477 object = Object.normalize(activity)
479 if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
480 Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
486 def render_content(%{data: %{"type" => object_type}} = object)
487 when object_type in ["Video", "Event", "Audio"] do
488 with name when not is_nil(name) and name != "" <- object.data["name"] do
489 "<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
491 _ -> object.data["content"] || ""
495 def render_content(%{data: %{"type" => object_type}} = object)
496 when object_type in ["Article", "Page"] do
497 with summary when not is_nil(summary) and summary != "" <- object.data["name"],
498 url when is_bitstring(url) <- object.data["url"] do
499 "<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
501 _ -> object.data["content"] || ""
505 def render_content(object), do: object.data["content"] || ""
508 Builds a dictionary tags.
512 iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
513 [{"name": "fediverse", "url": "/tag/fediverse"},
514 {"name": "nextcloud", "url": "/tag/nextcloud"}]
517 @spec build_tags(list(any())) :: list(map())
518 def build_tags(object_tags) when is_list(object_tags) do
519 object_tags = for tag when is_binary(tag) <- object_tags, do: tag
521 Enum.reduce(object_tags, [], fn tag, tags ->
522 tags ++ [%{name: tag, url: "/tag/#{URI.encode(tag)}"}]
526 def build_tags(_), do: []
531 Arguments: `nil` or list tuple of name and url.
537 iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
538 [%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
541 @spec build_emojis(nil | list(tuple())) :: list(map())
542 def build_emojis(nil), do: []
544 def build_emojis(emojis) do
546 |> Enum.map(fn {name, url} ->
547 name = HTML.strip_tags(name)
554 %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
558 defp present?(nil), do: false
559 defp present?(false), do: false
560 defp present?(_), do: true
562 defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}),
563 do: id in pinned_activities