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.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 replied_to_activities = get_replied_to_activities(opts.activities)
76 parallel = unless is_nil(opts[:parallel]), do: opts[:parallel], else: true
82 Map.put(opts, :replied_to_activities, replied_to_activities),
89 %{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
91 user = get_user(activity.data["actor"])
92 created_at = Utils.to_masto_date(activity.data["published"])
93 activity_object = Object.normalize(activity)
96 Activity.create_by_object_ap_id(activity_object.data["id"])
97 |> Activity.with_preloaded_bookmark(opts[:for])
98 |> Activity.with_set_thread_muted_field(opts[:for])
101 reblogged = render("status.json", Map.put(opts, :activity, reblogged_activity))
103 favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
105 bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil
109 |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
111 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
114 id: to_string(activity.id),
115 uri: activity_object.data["id"],
116 url: activity_object.data["id"],
117 account: AccountView.render("account.json", %{user: user, for: opts[:for]}),
119 in_reply_to_account_id: nil,
121 content: reblogged[:content] || "",
122 created_at: created_at,
126 reblogged: reblogged?(reblogged_activity, opts[:for]),
127 favourited: present?(favorited),
128 bookmarked: present?(bookmarked),
130 pinned: pinned?(activity, user),
133 visibility: "public",
134 media_attachments: reblogged[:media_attachments] || [],
136 tags: reblogged[:tags] || [],
144 local: activity.local
149 def render("status.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
150 object = Object.normalize(activity)
152 user = get_user(activity.data["actor"])
153 user_follower_address = user.follower_address
155 like_count = object.data["like_count"] || 0
156 announcement_count = object.data["announcement_count"] || 0
158 tags = object.data["tag"] || []
159 sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw")
163 |> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end)
164 |> Enum.map(fn tag -> tag["href"] end)
167 (object.data["to"] ++ tag_mentions)
170 Pleroma.Constants.as_public() -> nil
171 ^user_follower_address -> nil
172 ap_id -> User.get_cached_by_ap_id(ap_id)
175 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
177 favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
179 bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
181 client_posted_this_activity = opts[:for] && user.id == opts[:for].id
184 with true <- client_posted_this_activity,
185 expiration when not is_nil(expiration) <-
186 ActivityExpiration.get_by_activity_id(activity.id) do
187 expiration.scheduled_at
191 case activity.thread_muted? do
192 thread_muted? when is_boolean(thread_muted?) -> thread_muted?
193 nil -> (opts[:for] && CommonAPI.thread_muted?(opts[:for], activity)) || false
196 attachment_data = object.data["attachment"] || []
197 attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
199 created_at = Utils.to_masto_date(object.data["published"])
201 reply_to = get_reply_to(activity, opts)
203 reply_to_user = reply_to && get_user(reply_to.data["actor"])
211 |> HTML.get_cached_scrubbed_html_for_activity(
212 User.html_filter_policy(opts[:for]),
219 |> HTML.get_cached_stripped_html_for_activity(
224 summary = object.data["summary"] || ""
228 |> HTML.get_cached_scrubbed_html_for_activity(
229 User.html_filter_policy(opts[:for]),
236 |> HTML.get_cached_stripped_html_for_activity(
241 card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
245 Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
247 object.data["url"] || object.data["external_url"] || object.data["id"]
250 direct_conversation_id =
251 with {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
252 {_, %User{} = for_user} <- {:for_user, opts[:for]},
253 %{data: %{"context" => context}} when is_binary(context) <- activity,
254 %Conversation{} = conversation <- Conversation.get_for_ap_id(context),
255 %Participation{id: participation_id} <-
256 Participation.for_user_and_conversation(for_user, conversation) do
264 id: to_string(activity.id),
265 uri: object.data["id"],
267 account: AccountView.render("account.json", %{user: user, for: opts[:for]}),
268 in_reply_to_id: reply_to && to_string(reply_to.id),
269 in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
272 content: content_html,
273 created_at: created_at,
274 reblogs_count: announcement_count,
275 replies_count: object.data["repliesCount"] || 0,
276 favourites_count: like_count,
277 reblogged: reblogged?(activity, opts[:for]),
278 favourited: present?(favorited),
279 bookmarked: present?(bookmarked),
280 muted: thread_muted? || User.mutes?(opts[:for], user),
281 pinned: pinned?(activity, user),
282 sensitive: sensitive,
283 spoiler_text: summary_html,
284 visibility: get_visibility(object),
285 media_attachments: attachments,
286 poll: render("poll.json", %{object: object, for: opts[:for]}),
288 tags: build_tags(tags),
294 emojis: build_emojis(object.data["emoji"]),
296 local: activity.local,
297 conversation_id: get_context_id(activity),
298 in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
299 content: %{"text/plain" => content_plaintext},
300 spoiler_text: %{"text/plain" => summary_plaintext},
301 expires_at: expires_at,
302 direct_conversation_id: direct_conversation_id,
303 thread_muted: thread_muted?
308 def render("status.json", _) do
312 def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
313 page_url_data = URI.parse(page_url)
316 if rich_media[:url] != nil do
317 URI.merge(page_url_data, URI.parse(rich_media[:url]))
322 page_url = page_url_data |> to_string
325 if rich_media[:image] != nil do
326 URI.merge(page_url_data, URI.parse(rich_media[:image]))
332 site_name = rich_media[:site_name] || page_url_data.host
336 provider_name: site_name,
337 provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
339 image: image_url |> MediaProxy.url(),
340 title: rich_media[:title] || "",
341 description: rich_media[:description] || "",
343 opengraph: rich_media
348 def render("card.json", _) do
352 def render("attachment.json", %{attachment: attachment}) do
353 [attachment_url | _] = attachment["url"]
354 media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
355 href = attachment_url["href"] |> MediaProxy.url()
359 String.contains?(media_type, "image") -> "image"
360 String.contains?(media_type, "video") -> "video"
361 String.contains?(media_type, "audio") -> "audio"
365 <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
368 id: to_string(attachment["id"] || hash_id),
374 description: attachment["name"],
375 pleroma: %{mime_type: media_type}
379 def render("poll.json", %{object: object} = opts) do
380 {multiple, options} =
382 %{"anyOf" => options} when is_list(options) -> {true, options}
383 %{"oneOf" => options} when is_list(options) -> {false, options}
388 {end_time, expired} =
389 case object.data["closed"] || object.data["endTime"] do
390 end_time when is_binary(end_time) ->
392 (object.data["closed"] || object.data["endTime"])
393 |> NaiveDateTime.from_iso8601!()
397 |> NaiveDateTime.compare(NaiveDateTime.utc_now())
403 end_time = Utils.to_masto_date(end_time)
414 Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object)
416 existing_votes != [] or opts[:for].ap_id == object.data["actor"]
421 {options, votes_count} =
422 Enum.map_reduce(options, 0, fn %{"name" => name} = option, count ->
423 current_count = option["replies"]["totalItems"] || 0
426 title: HTML.strip_tags(name),
427 votes_count: current_count
428 }, current_count + count}
432 # Mastodon uses separate ids for polls, but an object can't have
433 # more than one poll embedded so object id is fine
434 id: to_string(object.id),
435 expires_at: end_time,
438 votes_count: votes_count,
441 emojis: build_emojis(object.data["emoji"])
448 def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
449 object = Object.normalize(activity)
451 with nil <- replied_to_activities[object.data["inReplyTo"]] do
452 # If user didn't participate in the thread
453 Activity.get_in_reply_to_activity(activity)
457 def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
458 object = Object.normalize(activity)
460 if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
461 Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
467 def render_content(%{data: %{"type" => "Video"}} = object) do
468 with name when not is_nil(name) and name != "" <- object.data["name"] do
469 "<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
471 _ -> object.data["content"] || ""
475 def render_content(%{data: %{"type" => object_type}} = object)
476 when object_type in ["Article", "Page"] do
477 with summary when not is_nil(summary) and summary != "" <- object.data["name"],
478 url when is_bitstring(url) <- object.data["url"] do
479 "<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
481 _ -> object.data["content"] || ""
485 def render_content(object), do: object.data["content"] || ""
488 Builds a dictionary tags.
492 iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
493 [{"name": "fediverse", "url": "/tag/fediverse"},
494 {"name": "nextcloud", "url": "/tag/nextcloud"}]
497 @spec build_tags(list(any())) :: list(map())
498 def build_tags(object_tags) when is_list(object_tags) do
499 object_tags = for tag when is_binary(tag) <- object_tags, do: tag
501 Enum.reduce(object_tags, [], fn tag, tags ->
502 tags ++ [%{name: tag, url: "/tag/#{tag}"}]
506 def build_tags(_), do: []
511 Arguments: `nil` or list tuple of name and url.
517 iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
518 [%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
521 @spec build_emojis(nil | list(tuple())) :: list(map())
522 def build_emojis(nil), do: []
524 def build_emojis(emojis) do
526 |> Enum.map(fn {name, url} ->
527 name = HTML.strip_tags(name)
534 %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
538 defp present?(nil), do: false
539 defp present?(false), do: false
540 defp present?(_), do: true
542 defp pinned?(%Activity{id: id}, %User{info: %{pinned_activities: pinned_activities}}),
543 do: id in pinned_activities