1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 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
12 alias Pleroma.Conversation
13 alias Pleroma.Conversation.Participation
18 alias Pleroma.Web.CommonAPI
19 alias Pleroma.Web.CommonAPI.Utils
20 alias Pleroma.Web.MastodonAPI.AccountView
21 alias Pleroma.Web.MastodonAPI.PollView
22 alias Pleroma.Web.MastodonAPI.StatusView
23 alias Pleroma.Web.MediaProxy
25 import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1]
27 # TODO: Add cached version.
28 defp get_replied_to_activities([]), do: %{}
30 defp get_replied_to_activities(activities) do
33 %{data: %{"type" => "Create"}} = activity ->
34 object = Object.normalize(activity)
35 object && object.data["inReplyTo"] != "" && object.data["inReplyTo"]
41 |> Activity.create_by_object_ap_id_with_object()
43 |> Enum.reduce(%{}, fn activity, acc ->
44 object = Object.normalize(activity)
45 if object, do: Map.put(acc, object.data["id"], activity), else: acc
49 defp get_user(ap_id) do
51 user = User.get_cached_by_ap_id(ap_id) ->
54 user = User.get_by_guessed_nickname(ap_id) ->
58 User.error_user(ap_id)
62 defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id),
65 defp get_context_id(%{data: %{"context" => context}}) when is_binary(context),
66 do: Utils.context_to_conversation_id(context)
68 defp get_context_id(_), do: nil
70 defp reblogged?(activity, user) do
71 object = Object.normalize(activity) || %{}
72 present?(user && user.ap_id in (object.data["announcements"] || []))
75 def render("index.json", opts) do
76 replied_to_activities = get_replied_to_activities(opts.activities)
77 opts = Map.put(opts, :replied_to_activities, replied_to_activities)
79 safe_render_many(opts.activities, StatusView, "show.json", opts)
84 %{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
86 user = get_user(activity.data["actor"])
87 created_at = Utils.to_masto_date(activity.data["published"])
88 activity_object = Object.normalize(activity)
91 Activity.create_by_object_ap_id(activity_object.data["id"])
92 |> Activity.with_preloaded_bookmark(opts[:for])
93 |> Activity.with_set_thread_muted_field(opts[:for])
96 reblogged = render("show.json", Map.put(opts, :activity, reblogged_activity))
98 favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
100 bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil
104 |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
106 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
109 id: to_string(activity.id),
110 uri: activity_object.data["id"],
111 url: activity_object.data["id"],
112 account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
114 in_reply_to_account_id: nil,
116 content: reblogged[:content] || "",
117 created_at: created_at,
121 reblogged: reblogged?(reblogged_activity, opts[:for]),
122 favourited: present?(favorited),
123 bookmarked: present?(bookmarked),
125 pinned: pinned?(activity, user),
128 visibility: get_visibility(activity),
129 media_attachments: reblogged[:media_attachments] || [],
131 tags: reblogged[:tags] || [],
139 local: activity.local
144 def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
145 object = Object.normalize(activity)
147 user = get_user(activity.data["actor"])
148 user_follower_address = user.follower_address
150 like_count = object.data["like_count"] || 0
151 announcement_count = object.data["announcement_count"] || 0
153 tags = object.data["tag"] || []
154 sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw")
158 |> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end)
159 |> Enum.map(fn tag -> tag["href"] end)
162 (object.data["to"] ++ tag_mentions)
165 Pleroma.Constants.as_public() -> nil
166 ^user_follower_address -> nil
167 ap_id -> User.get_cached_by_ap_id(ap_id)
170 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
172 favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
174 bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
176 client_posted_this_activity = opts[:for] && user.id == opts[:for].id
179 with true <- client_posted_this_activity,
180 expiration when not is_nil(expiration) <-
181 ActivityExpiration.get_by_activity_id(activity.id) do
182 expiration.scheduled_at
186 case activity.thread_muted? do
187 thread_muted? when is_boolean(thread_muted?) -> thread_muted?
188 nil -> (opts[:for] && CommonAPI.thread_muted?(opts[:for], activity)) || false
191 attachment_data = object.data["attachment"] || []
192 attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
194 created_at = Utils.to_masto_date(object.data["published"])
196 reply_to = get_reply_to(activity, opts)
198 reply_to_user = reply_to && get_user(reply_to.data["actor"])
206 |> HTML.get_cached_scrubbed_html_for_activity(
207 User.html_filter_policy(opts[:for]),
214 |> HTML.get_cached_stripped_html_for_activity(
219 summary = object.data["summary"] || ""
223 |> HTML.get_cached_scrubbed_html_for_activity(
224 User.html_filter_policy(opts[:for]),
231 |> HTML.get_cached_stripped_html_for_activity(
236 card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
240 Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
242 object.data["url"] || object.data["external_url"] || object.data["id"]
245 direct_conversation_id =
246 with {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
247 {_, %User{} = for_user} <- {:for_user, opts[:for]},
248 %{data: %{"context" => context}} when is_binary(context) <- activity,
249 %Conversation{} = conversation <- Conversation.get_for_ap_id(context),
250 %Participation{id: participation_id} <-
251 Participation.for_user_and_conversation(for_user, conversation) do
259 id: to_string(activity.id),
260 uri: object.data["id"],
262 account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
263 in_reply_to_id: reply_to && to_string(reply_to.id),
264 in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
267 content: content_html,
268 created_at: created_at,
269 reblogs_count: announcement_count,
270 replies_count: object.data["repliesCount"] || 0,
271 favourites_count: like_count,
272 reblogged: reblogged?(activity, opts[:for]),
273 favourited: present?(favorited),
274 bookmarked: present?(bookmarked),
275 muted: thread_muted? || User.mutes?(opts[:for], user),
276 pinned: pinned?(activity, user),
277 sensitive: sensitive,
278 spoiler_text: summary_html,
279 visibility: get_visibility(object),
280 media_attachments: attachments,
281 poll: render(PollView, "show.json", object: object, for: opts[:for]),
283 tags: build_tags(tags),
289 emojis: build_emojis(object.data["emoji"]),
291 local: activity.local,
292 conversation_id: get_context_id(activity),
293 in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
294 content: %{"text/plain" => content_plaintext},
295 spoiler_text: %{"text/plain" => summary_plaintext},
296 expires_at: expires_at,
297 direct_conversation_id: direct_conversation_id,
298 thread_muted: thread_muted?
303 def render("show.json", _) do
307 def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
308 page_url_data = URI.parse(page_url)
311 if rich_media[:url] != nil do
312 URI.merge(page_url_data, URI.parse(rich_media[:url]))
317 page_url = page_url_data |> to_string
320 if rich_media[:image] != nil do
321 URI.merge(page_url_data, URI.parse(rich_media[:image]))
327 site_name = rich_media[:site_name] || page_url_data.host
331 provider_name: site_name,
332 provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
334 image: image_url |> MediaProxy.url(),
335 title: rich_media[:title] || "",
336 description: rich_media[:description] || "",
338 opengraph: rich_media
343 def render("card.json", _), do: nil
345 def render("attachment.json", %{attachment: attachment}) do
346 [attachment_url | _] = attachment["url"]
347 media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
348 href = attachment_url["href"] |> MediaProxy.url()
352 String.contains?(media_type, "image") -> "image"
353 String.contains?(media_type, "video") -> "video"
354 String.contains?(media_type, "audio") -> "audio"
358 <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
361 id: to_string(attachment["id"] || hash_id),
367 description: attachment["name"],
368 pleroma: %{mime_type: media_type}
372 def render("listen.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
373 object = Object.normalize(activity)
375 user = get_user(activity.data["actor"])
376 created_at = Utils.to_masto_date(activity.data["published"])
380 account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
381 created_at: created_at,
382 title: object.data["title"] |> HTML.strip_tags(),
383 artist: object.data["artist"] |> HTML.strip_tags(),
384 album: object.data["album"] |> HTML.strip_tags(),
385 length: object.data["length"]
389 def render("listens.json", opts) do
390 safe_render_many(opts.activities, StatusView, "listen.json", opts)
393 def render("context.json", %{activity: activity, activities: activities, user: user}) do
394 %{ancestors: ancestors, descendants: descendants} =
397 |> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
398 |> Map.put_new(:ancestors, [])
399 |> Map.put_new(:descendants, [])
402 ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
403 descendants: render("index.json", for: user, activities: descendants, as: :activity)
407 def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
408 object = Object.normalize(activity)
410 with nil <- replied_to_activities[object.data["inReplyTo"]] do
411 # If user didn't participate in the thread
412 Activity.get_in_reply_to_activity(activity)
416 def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
417 object = Object.normalize(activity)
419 if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
420 Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
426 def render_content(%{data: %{"type" => "Video"}} = object) do
427 with name when not is_nil(name) and name != "" <- object.data["name"] do
428 "<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
430 _ -> object.data["content"] || ""
434 def render_content(%{data: %{"type" => object_type}} = object)
435 when object_type in ["Article", "Page"] do
436 with summary when not is_nil(summary) and summary != "" <- object.data["name"],
437 url when is_bitstring(url) <- object.data["url"] do
438 "<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
440 _ -> object.data["content"] || ""
444 def render_content(object), do: object.data["content"] || ""
447 Builds a dictionary tags.
451 iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
452 [{"name": "fediverse", "url": "/tag/fediverse"},
453 {"name": "nextcloud", "url": "/tag/nextcloud"}]
456 @spec build_tags(list(any())) :: list(map())
457 def build_tags(object_tags) when is_list(object_tags) do
458 object_tags = for tag when is_binary(tag) <- object_tags, do: tag
460 Enum.reduce(object_tags, [], fn tag, tags ->
461 tags ++ [%{name: tag, url: "/tag/#{URI.encode(tag)}"}]
465 def build_tags(_), do: []
470 Arguments: `nil` or list tuple of name and url.
476 iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
477 [%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
480 @spec build_emojis(nil | list(tuple())) :: list(map())
481 def build_emojis(nil), do: []
483 def build_emojis(emojis) do
485 |> Enum.map(fn {name, url} ->
486 name = HTML.strip_tags(name)
493 %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
497 defp present?(nil), do: false
498 defp present?(false), do: false
499 defp present?(_), do: true
501 defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}),
502 do: id in pinned_activities