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 defp user_relationships_opt(opts) do
75 reading_user = opts[:for]
78 activities = opts[:activities]
79 actors = Enum.map(activities, fn a -> get_user(a.data["actor"]) end)
81 UserRelationship.dictionary(
84 [:block, :mute, :notification_mute, :reblog_mute],
85 [:block, :inverse_subscription]
92 def render("index.json", opts) do
93 activities = opts.activities
94 replied_to_activities = get_replied_to_activities(activities)
98 |> Map.put(:replied_to_activities, replied_to_activities)
99 |> Map.put(:user_relationships, user_relationships_opt(opts))
101 safe_render_many(activities, StatusView, "show.json", opts)
106 %{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
108 user = get_user(activity.data["actor"])
109 created_at = Utils.to_masto_date(activity.data["published"])
110 activity_object = Object.normalize(activity)
113 Activity.create_by_object_ap_id(activity_object.data["id"])
114 |> Activity.with_preloaded_bookmark(opts[:for])
115 |> Activity.with_set_thread_muted_field(opts[:for])
118 reblogged = render("show.json", Map.put(opts, :activity, reblogged_activity))
120 favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
122 bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil
126 |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
128 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
131 id: to_string(activity.id),
132 uri: activity_object.data["id"],
133 url: activity_object.data["id"],
135 AccountView.render("show.json", %{
138 user_relationships: opts[:user_relationships]
141 in_reply_to_account_id: nil,
143 content: reblogged[:content] || "",
144 created_at: created_at,
148 reblogged: reblogged?(reblogged_activity, opts[:for]),
149 favourited: present?(favorited),
150 bookmarked: present?(bookmarked),
152 pinned: pinned?(activity, user),
155 visibility: get_visibility(activity),
156 media_attachments: reblogged[:media_attachments] || [],
158 tags: reblogged[:tags] || [],
166 local: activity.local
171 def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
172 object = Object.normalize(activity)
174 user = get_user(activity.data["actor"])
175 user_follower_address = user.follower_address
177 like_count = object.data["like_count"] || 0
178 announcement_count = object.data["announcement_count"] || 0
180 tags = object.data["tag"] || []
181 sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw")
185 |> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end)
186 |> Enum.map(fn tag -> tag["href"] end)
189 (object.data["to"] ++ tag_mentions)
192 Pleroma.Constants.as_public() -> nil
193 ^user_follower_address -> nil
194 ap_id -> User.get_cached_by_ap_id(ap_id)
197 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
199 favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
201 bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
203 client_posted_this_activity = opts[:for] && user.id == opts[:for].id
206 with true <- client_posted_this_activity,
207 %ActivityExpiration{scheduled_at: scheduled_at} <-
208 ActivityExpiration.get_by_activity_id(activity.id) do
215 case activity.thread_muted? do
216 thread_muted? when is_boolean(thread_muted?) -> thread_muted?
217 nil -> (opts[:for] && CommonAPI.thread_muted?(opts[:for], activity)) || false
220 attachment_data = object.data["attachment"] || []
221 attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
223 created_at = Utils.to_masto_date(object.data["published"])
225 reply_to = get_reply_to(activity, opts)
227 reply_to_user = reply_to && get_user(reply_to.data["actor"])
235 |> HTML.get_cached_scrubbed_html_for_activity(
236 User.html_filter_policy(opts[:for]),
243 |> HTML.get_cached_stripped_html_for_activity(
248 summary = object.data["summary"] || ""
250 card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
254 Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
256 object.data["url"] || object.data["external_url"] || object.data["id"]
259 direct_conversation_id =
260 with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]},
261 {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
262 {_, %User{} = for_user} <- {:for_user, opts[:for]} do
263 Activity.direct_conversation_id(activity, for_user)
265 {:direct_conversation_id, participation_id} when is_integer(participation_id) ->
273 with %{data: %{"reactions" => emoji_reactions}} <- object do
274 Enum.map(emoji_reactions, fn [emoji, users] ->
277 count: length(users),
278 me: !!(opts[:for] && opts[:for].ap_id in users)
285 user_relationships_opt = opts[:user_relationships]
289 Pleroma.Web.MastodonAPI.AccountView.test_rel(
290 user_relationships_opt,
294 fn for_user, user -> User.mutes?(for_user, user) end
298 id: to_string(activity.id),
299 uri: object.data["id"],
302 AccountView.render("show.json", %{
305 user_relationships: user_relationships_opt
307 in_reply_to_id: reply_to && to_string(reply_to.id),
308 in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
311 content: content_html,
312 created_at: created_at,
313 reblogs_count: announcement_count,
314 replies_count: object.data["repliesCount"] || 0,
315 favourites_count: like_count,
316 reblogged: reblogged?(activity, opts[:for]),
317 favourited: present?(favorited),
318 bookmarked: present?(bookmarked),
320 pinned: pinned?(activity, user),
321 sensitive: sensitive,
322 spoiler_text: summary,
323 visibility: get_visibility(object),
324 media_attachments: attachments,
325 poll: render(PollView, "show.json", object: object, for: opts[:for]),
327 tags: build_tags(tags),
333 emojis: build_emojis(object.data["emoji"]),
335 local: activity.local,
336 conversation_id: get_context_id(activity),
337 in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
338 content: %{"text/plain" => content_plaintext},
339 spoiler_text: %{"text/plain" => summary},
340 expires_at: expires_at,
341 direct_conversation_id: direct_conversation_id,
342 thread_muted: thread_muted?,
343 emoji_reactions: emoji_reactions
348 def render("show.json", _) do
352 def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
353 page_url_data = URI.parse(page_url)
356 if rich_media[:url] != nil do
357 URI.merge(page_url_data, URI.parse(rich_media[:url]))
362 page_url = page_url_data |> to_string
365 if rich_media[:image] != nil do
366 URI.merge(page_url_data, URI.parse(rich_media[:image]))
374 provider_name: page_url_data.host,
375 provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
377 image: image_url |> MediaProxy.url(),
378 title: rich_media[:title] || "",
379 description: rich_media[:description] || "",
381 opengraph: rich_media
386 def render("card.json", _), do: nil
388 def render("attachment.json", %{attachment: attachment}) do
389 [attachment_url | _] = attachment["url"]
390 media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
391 href = attachment_url["href"] |> MediaProxy.url()
395 String.contains?(media_type, "image") -> "image"
396 String.contains?(media_type, "video") -> "video"
397 String.contains?(media_type, "audio") -> "audio"
401 <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
404 id: to_string(attachment["id"] || hash_id),
410 description: attachment["name"],
411 pleroma: %{mime_type: media_type}
415 def render("listen.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
416 object = Object.normalize(activity)
418 user = get_user(activity.data["actor"])
419 created_at = Utils.to_masto_date(activity.data["published"])
423 account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
424 created_at: created_at,
425 title: object.data["title"] |> HTML.strip_tags(),
426 artist: object.data["artist"] |> HTML.strip_tags(),
427 album: object.data["album"] |> HTML.strip_tags(),
428 length: object.data["length"]
432 def render("listens.json", opts) do
433 safe_render_many(opts.activities, StatusView, "listen.json", opts)
436 def render("context.json", %{activity: activity, activities: activities, user: user}) do
437 %{ancestors: ancestors, descendants: descendants} =
440 |> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
441 |> Map.put_new(:ancestors, [])
442 |> Map.put_new(:descendants, [])
445 ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
446 descendants: render("index.json", for: user, activities: descendants, as: :activity)
450 def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
451 object = Object.normalize(activity)
453 with nil <- replied_to_activities[object.data["inReplyTo"]] do
454 # If user didn't participate in the thread
455 Activity.get_in_reply_to_activity(activity)
459 def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
460 object = Object.normalize(activity)
462 if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
463 Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
469 def render_content(%{data: %{"type" => object_type}} = object)
470 when object_type in ["Video", "Event"] do
471 with name when not is_nil(name) and name != "" <- object.data["name"] do
472 "<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
474 _ -> object.data["content"] || ""
478 def render_content(%{data: %{"type" => object_type}} = object)
479 when object_type in ["Article", "Page"] do
480 with summary when not is_nil(summary) and summary != "" <- object.data["name"],
481 url when is_bitstring(url) <- object.data["url"] do
482 "<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
484 _ -> object.data["content"] || ""
488 def render_content(object), do: object.data["content"] || ""
491 Builds a dictionary tags.
495 iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
496 [{"name": "fediverse", "url": "/tag/fediverse"},
497 {"name": "nextcloud", "url": "/tag/nextcloud"}]
500 @spec build_tags(list(any())) :: list(map())
501 def build_tags(object_tags) when is_list(object_tags) do
502 object_tags = for tag when is_binary(tag) <- object_tags, do: tag
504 Enum.reduce(object_tags, [], fn tag, tags ->
505 tags ++ [%{name: tag, url: "/tag/#{URI.encode(tag)}"}]
509 def build_tags(_), do: []
514 Arguments: `nil` or list tuple of name and url.
520 iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
521 [%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
524 @spec build_emojis(nil | list(tuple())) :: list(map())
525 def build_emojis(nil), do: []
527 def build_emojis(emojis) do
529 |> Enum.map(fn {name, url} ->
530 name = HTML.strip_tags(name)
537 %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
541 defp present?(nil), do: false
542 defp present?(false), do: false
543 defp present?(_), do: true
545 defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}),
546 do: id in pinned_activities