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 {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]},
247 {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
248 {_, %User{} = for_user} <- {:for_user, opts[:for]},
249 %{data: %{"context" => context}} when is_binary(context) <- activity,
250 %Conversation{} = conversation <- Conversation.get_for_ap_id(context),
251 %Participation{id: participation_id} <-
252 Participation.for_user_and_conversation(for_user, conversation) do
255 {:direct_conversation_id, participation_id} when is_integer(participation_id) ->
263 id: to_string(activity.id),
264 uri: object.data["id"],
266 account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
267 in_reply_to_id: reply_to && to_string(reply_to.id),
268 in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
271 content: content_html,
272 created_at: created_at,
273 reblogs_count: announcement_count,
274 replies_count: object.data["repliesCount"] || 0,
275 favourites_count: like_count,
276 reblogged: reblogged?(activity, opts[:for]),
277 favourited: present?(favorited),
278 bookmarked: present?(bookmarked),
279 muted: thread_muted? || User.mutes?(opts[:for], user),
280 pinned: pinned?(activity, user),
281 sensitive: sensitive,
282 spoiler_text: summary_html,
283 visibility: get_visibility(object),
284 media_attachments: attachments,
285 poll: render(PollView, "show.json", object: object, for: opts[:for]),
287 tags: build_tags(tags),
293 emojis: build_emojis(object.data["emoji"]),
295 local: activity.local,
296 conversation_id: get_context_id(activity),
297 in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
298 content: %{"text/plain" => content_plaintext},
299 spoiler_text: %{"text/plain" => summary_plaintext},
300 expires_at: expires_at,
301 direct_conversation_id: direct_conversation_id,
302 thread_muted: thread_muted?
307 def render("show.json", _) do
311 def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
312 page_url_data = URI.parse(page_url)
315 if rich_media[:url] != nil do
316 URI.merge(page_url_data, URI.parse(rich_media[:url]))
321 page_url = page_url_data |> to_string
324 if rich_media[:image] != nil do
325 URI.merge(page_url_data, URI.parse(rich_media[:image]))
331 site_name = rich_media[:site_name] || page_url_data.host
335 provider_name: site_name,
336 provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
338 image: image_url |> MediaProxy.url(),
339 title: rich_media[:title] || "",
340 description: rich_media[:description] || "",
342 opengraph: rich_media
347 def render("card.json", _), do: nil
349 def render("attachment.json", %{attachment: attachment}) do
350 [attachment_url | _] = attachment["url"]
351 media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
352 href = attachment_url["href"] |> MediaProxy.url()
356 String.contains?(media_type, "image") -> "image"
357 String.contains?(media_type, "video") -> "video"
358 String.contains?(media_type, "audio") -> "audio"
362 <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
365 id: to_string(attachment["id"] || hash_id),
371 description: attachment["name"],
372 pleroma: %{mime_type: media_type}
376 def render("listen.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
377 object = Object.normalize(activity)
379 user = get_user(activity.data["actor"])
380 created_at = Utils.to_masto_date(activity.data["published"])
384 account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
385 created_at: created_at,
386 title: object.data["title"] |> HTML.strip_tags(),
387 artist: object.data["artist"] |> HTML.strip_tags(),
388 album: object.data["album"] |> HTML.strip_tags(),
389 length: object.data["length"]
393 def render("listens.json", opts) do
394 safe_render_many(opts.activities, StatusView, "listen.json", opts)
397 def render("context.json", %{activity: activity, activities: activities, user: user}) do
398 %{ancestors: ancestors, descendants: descendants} =
401 |> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
402 |> Map.put_new(:ancestors, [])
403 |> Map.put_new(:descendants, [])
406 ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
407 descendants: render("index.json", for: user, activities: descendants, as: :activity)
411 def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
412 object = Object.normalize(activity)
414 with nil <- replied_to_activities[object.data["inReplyTo"]] do
415 # If user didn't participate in the thread
416 Activity.get_in_reply_to_activity(activity)
420 def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
421 object = Object.normalize(activity)
423 if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
424 Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
430 def render_content(%{data: %{"type" => "Video"}} = object) do
431 with name when not is_nil(name) and name != "" <- object.data["name"] do
432 "<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
434 _ -> object.data["content"] || ""
438 def render_content(%{data: %{"type" => object_type}} = object)
439 when object_type in ["Article", "Page"] do
440 with summary when not is_nil(summary) and summary != "" <- object.data["name"],
441 url when is_bitstring(url) <- object.data["url"] do
442 "<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
444 _ -> object.data["content"] || ""
448 def render_content(object), do: object.data["content"] || ""
451 Builds a dictionary tags.
455 iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
456 [{"name": "fediverse", "url": "/tag/fediverse"},
457 {"name": "nextcloud", "url": "/tag/nextcloud"}]
460 @spec build_tags(list(any())) :: list(map())
461 def build_tags(object_tags) when is_list(object_tags) do
462 object_tags = for tag when is_binary(tag) <- object_tags, do: tag
464 Enum.reduce(object_tags, [], fn tag, tags ->
465 tags ++ [%{name: tag, url: "/tag/#{URI.encode(tag)}"}]
469 def build_tags(_), do: []
474 Arguments: `nil` or list tuple of name and url.
480 iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
481 [%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
484 @spec build_emojis(nil | list(tuple())) :: list(map())
485 def build_emojis(nil), do: []
487 def build_emojis(emojis) do
489 |> Enum.map(fn {name, url} ->
490 name = HTML.strip_tags(name)
497 %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
501 defp present?(nil), do: false
502 defp present?(false), do: false
503 defp present?(_), do: true
505 defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}),
506 do: id in pinned_activities