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
12 alias Pleroma.FollowingRelationship
17 alias Pleroma.UserRelationship
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 defp relationships_opts(opts) do
76 reading_user = opts[:for]
78 {user_relationships, following_relationships} =
80 activities = opts[:activities]
81 actors = Enum.map(activities, fn a -> get_user(a.data["actor"]) end)
84 UserRelationship.dictionary(
87 [:block, :mute, :notification_mute, :reblog_mute],
88 [:block, :inverse_subscription]
91 following_relationships =
92 FollowingRelationship.all_between_user_sets([reading_user], actors)
94 {user_relationships, following_relationships}
99 %{user_relationships: user_relationships, following_relationships: following_relationships}
102 def render("index.json", opts) do
103 activities = opts.activities
104 replied_to_activities = get_replied_to_activities(activities)
108 |> Map.put(:replied_to_activities, replied_to_activities)
109 |> Map.merge(relationships_opts(opts))
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)
123 Activity.create_by_object_ap_id(activity_object.data["id"])
124 |> Activity.with_preloaded_bookmark(opts[:for])
125 |> Activity.with_set_thread_muted_field(opts[:for])
128 reblogged = render("show.json", Map.put(opts, :activity, reblogged_activity))
130 favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
132 bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil
136 |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
138 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
141 id: to_string(activity.id),
142 uri: activity_object.data["id"],
143 url: activity_object.data["id"],
145 AccountView.render("show.json", %{
148 user_relationships: opts[:user_relationships],
149 following_relationships: opts[:following_relationships]
152 in_reply_to_account_id: nil,
154 content: reblogged[:content] || "",
155 created_at: created_at,
159 reblogged: reblogged?(reblogged_activity, opts[:for]),
160 favourited: present?(favorited),
161 bookmarked: present?(bookmarked),
163 pinned: pinned?(activity, user),
166 visibility: get_visibility(activity),
167 media_attachments: reblogged[:media_attachments] || [],
169 tags: reblogged[:tags] || [],
177 local: activity.local
182 def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
183 object = Object.normalize(activity)
185 user = get_user(activity.data["actor"])
186 user_follower_address = user.follower_address
188 like_count = object.data["like_count"] || 0
189 announcement_count = object.data["announcement_count"] || 0
191 tags = object.data["tag"] || []
192 sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw")
196 |> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end)
197 |> Enum.map(fn tag -> tag["href"] end)
200 (object.data["to"] ++ tag_mentions)
203 Pleroma.Constants.as_public() -> nil
204 ^user_follower_address -> nil
205 ap_id -> User.get_cached_by_ap_id(ap_id)
208 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
210 favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
212 bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
214 client_posted_this_activity = opts[:for] && user.id == opts[:for].id
217 with true <- client_posted_this_activity,
218 %ActivityExpiration{scheduled_at: scheduled_at} <-
219 ActivityExpiration.get_by_activity_id(activity.id) do
226 case activity.thread_muted? do
227 thread_muted? when is_boolean(thread_muted?) -> thread_muted?
228 nil -> (opts[:for] && CommonAPI.thread_muted?(opts[:for], activity)) || false
231 attachment_data = object.data["attachment"] || []
232 attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
234 created_at = Utils.to_masto_date(object.data["published"])
236 reply_to = get_reply_to(activity, opts)
238 reply_to_user = reply_to && get_user(reply_to.data["actor"])
246 |> HTML.get_cached_scrubbed_html_for_activity(
247 User.html_filter_policy(opts[:for]),
254 |> HTML.get_cached_stripped_html_for_activity(
259 summary = object.data["summary"] || ""
261 card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
265 Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
267 object.data["url"] || object.data["external_url"] || object.data["id"]
270 direct_conversation_id =
271 with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]},
272 {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
273 {_, %User{} = for_user} <- {:for_user, opts[:for]} do
274 Activity.direct_conversation_id(activity, for_user)
276 {:direct_conversation_id, participation_id} when is_integer(participation_id) ->
284 with %{data: %{"reactions" => emoji_reactions}} <- object do
285 Enum.map(emoji_reactions, fn [emoji, users] ->
288 count: length(users),
289 me: !!(opts[:for] && opts[:for].ap_id in users)
296 user_relationships_opt = opts[:user_relationships]
300 UserRelationship.exists?(
301 user_relationships_opt,
305 fn for_user, user -> User.mutes?(for_user, user) end
309 id: to_string(activity.id),
310 uri: object.data["id"],
313 AccountView.render("show.json", %{
316 user_relationships: user_relationships_opt,
317 following_relationships: opts[:following_relationships]
319 in_reply_to_id: reply_to && to_string(reply_to.id),
320 in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
323 content: content_html,
324 created_at: created_at,
325 reblogs_count: announcement_count,
326 replies_count: object.data["repliesCount"] || 0,
327 favourites_count: like_count,
328 reblogged: reblogged?(activity, opts[:for]),
329 favourited: present?(favorited),
330 bookmarked: present?(bookmarked),
332 pinned: pinned?(activity, user),
333 sensitive: sensitive,
334 spoiler_text: summary,
335 visibility: get_visibility(object),
336 media_attachments: attachments,
337 poll: render(PollView, "show.json", object: object, for: opts[:for]),
339 tags: build_tags(tags),
345 emojis: build_emojis(object.data["emoji"]),
347 local: activity.local,
348 conversation_id: get_context_id(activity),
349 in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
350 content: %{"text/plain" => content_plaintext},
351 spoiler_text: %{"text/plain" => summary},
352 expires_at: expires_at,
353 direct_conversation_id: direct_conversation_id,
354 thread_muted: thread_muted?,
355 emoji_reactions: emoji_reactions
360 def render("show.json", _) do
364 def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
365 page_url_data = URI.parse(page_url)
368 if rich_media[:url] != nil do
369 URI.merge(page_url_data, URI.parse(rich_media[:url]))
374 page_url = page_url_data |> to_string
377 if rich_media[:image] != nil do
378 URI.merge(page_url_data, URI.parse(rich_media[:image]))
386 provider_name: page_url_data.host,
387 provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
389 image: image_url |> MediaProxy.url(),
390 title: rich_media[:title] || "",
391 description: rich_media[:description] || "",
393 opengraph: rich_media
398 def render("card.json", _), do: nil
400 def render("attachment.json", %{attachment: attachment}) do
401 [attachment_url | _] = attachment["url"]
402 media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
403 href = attachment_url["href"] |> MediaProxy.url()
407 String.contains?(media_type, "image") -> "image"
408 String.contains?(media_type, "video") -> "video"
409 String.contains?(media_type, "audio") -> "audio"
413 <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
416 id: to_string(attachment["id"] || hash_id),
422 description: attachment["name"],
423 pleroma: %{mime_type: media_type}
427 def render("listen.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
428 object = Object.normalize(activity)
430 user = get_user(activity.data["actor"])
431 created_at = Utils.to_masto_date(activity.data["published"])
435 account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
436 created_at: created_at,
437 title: object.data["title"] |> HTML.strip_tags(),
438 artist: object.data["artist"] |> HTML.strip_tags(),
439 album: object.data["album"] |> HTML.strip_tags(),
440 length: object.data["length"]
444 def render("listens.json", opts) do
445 safe_render_many(opts.activities, StatusView, "listen.json", opts)
448 def render("context.json", %{activity: activity, activities: activities, user: user}) do
449 %{ancestors: ancestors, descendants: descendants} =
452 |> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
453 |> Map.put_new(:ancestors, [])
454 |> Map.put_new(:descendants, [])
457 ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
458 descendants: render("index.json", for: user, activities: descendants, as: :activity)
462 def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
463 object = Object.normalize(activity)
465 with nil <- replied_to_activities[object.data["inReplyTo"]] do
466 # If user didn't participate in the thread
467 Activity.get_in_reply_to_activity(activity)
471 def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
472 object = Object.normalize(activity)
474 if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
475 Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
481 def render_content(%{data: %{"type" => object_type}} = object)
482 when object_type in ["Video", "Event"] do
483 with name when not is_nil(name) and name != "" <- object.data["name"] do
484 "<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
486 _ -> object.data["content"] || ""
490 def render_content(%{data: %{"type" => object_type}} = object)
491 when object_type in ["Article", "Page"] do
492 with summary when not is_nil(summary) and summary != "" <- object.data["name"],
493 url when is_bitstring(url) <- object.data["url"] do
494 "<p><a href=\"#{url}\">#{summary}</a></p>#{object.data["content"]}"
496 _ -> object.data["content"] || ""
500 def render_content(object), do: object.data["content"] || ""
503 Builds a dictionary tags.
507 iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
508 [{"name": "fediverse", "url": "/tag/fediverse"},
509 {"name": "nextcloud", "url": "/tag/nextcloud"}]
512 @spec build_tags(list(any())) :: list(map())
513 def build_tags(object_tags) when is_list(object_tags) do
514 object_tags = for tag when is_binary(tag) <- object_tags, do: tag
516 Enum.reduce(object_tags, [], fn tag, tags ->
517 tags ++ [%{name: tag, url: "/tag/#{URI.encode(tag)}"}]
521 def build_tags(_), do: []
526 Arguments: `nil` or list tuple of name and url.
532 iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
533 [%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
536 @spec build_emojis(nil | list(tuple())) :: list(map())
537 def build_emojis(nil), do: []
539 def build_emojis(emojis) do
541 |> Enum.map(fn {name, url} ->
542 name = HTML.strip_tags(name)
549 %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
553 defp present?(nil), do: false
554 defp present?(false), do: false
555 defp present?(_), do: true
557 defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}),
558 do: id in pinned_activities