MastodonAPI.StatusView.get_user/1 --> CommonAPI.get_user/1
[akkoma] / lib / pleroma / web / mastodon_api / views / status_view.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.MastodonAPI.StatusView do
6 use Pleroma.Web, :view
7
8 require Pleroma.Constants
9
10 alias Pleroma.Activity
11 alias Pleroma.ActivityExpiration
12 alias Pleroma.HTML
13 alias Pleroma.Object
14 alias Pleroma.Repo
15 alias Pleroma.User
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
23
24 import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]
25
26 # This is a naive way to do this, just spawning a process per activity
27 # to fetch the preview. However it should be fine considering
28 # pagination is restricted to 40 activities at a time
29 defp fetch_rich_media_for_activities(activities) do
30 Enum.each(activities, fn activity ->
31 spawn(fn ->
32 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
33 end)
34 end)
35 end
36
37 # TODO: Add cached version.
38 defp get_replied_to_activities([]), do: %{}
39
40 defp get_replied_to_activities(activities) do
41 activities
42 |> Enum.map(fn
43 %{data: %{"type" => "Create"}} = activity ->
44 object = Object.normalize(activity)
45 object && object.data["inReplyTo"] != "" && object.data["inReplyTo"]
46
47 _ ->
48 nil
49 end)
50 |> Enum.filter(& &1)
51 |> Activity.create_by_object_ap_id_with_object()
52 |> Repo.all()
53 |> Enum.reduce(%{}, fn activity, acc ->
54 object = Object.normalize(activity)
55 if object, do: Map.put(acc, object.data["id"], activity), else: acc
56 end)
57 end
58
59 defp get_context_id(%{data: %{"context_id" => context_id}}) when not is_nil(context_id),
60 do: context_id
61
62 defp get_context_id(%{data: %{"context" => context}}) when is_binary(context),
63 do: Utils.context_to_conversation_id(context)
64
65 defp get_context_id(_), do: nil
66
67 defp reblogged?(activity, user) do
68 object = Object.normalize(activity) || %{}
69 present?(user && user.ap_id in (object.data["announcements"] || []))
70 end
71
72 def render("index.json", opts) do
73 reading_user = opts[:for]
74
75 # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
76 activities = Enum.filter(opts.activities, & &1)
77
78 # Start fetching rich media before doing anything else, so that later calls to get the cards
79 # only block for timeout in the worst case, as opposed to
80 # length(activities_with_links) * timeout
81 fetch_rich_media_for_activities(activities)
82 replied_to_activities = get_replied_to_activities(activities)
83
84 parent_activities =
85 activities
86 |> Enum.filter(&(&1.data["type"] == "Announce" && &1.data["object"]))
87 |> Enum.map(&Object.normalize(&1).data["id"])
88 |> Activity.create_by_object_ap_id()
89 |> Activity.with_preloaded_object(:left)
90 |> Activity.with_preloaded_bookmark(reading_user)
91 |> Activity.with_set_thread_muted_field(reading_user)
92 |> Repo.all()
93
94 relationships_opt =
95 cond do
96 Map.has_key?(opts, :relationships) ->
97 opts[:relationships]
98
99 is_nil(reading_user) ->
100 UserRelationship.view_relationships_option(nil, [])
101
102 true ->
103 # Note: unresolved users are filtered out
104 actors =
105 (activities ++ parent_activities)
106 |> Enum.map(&CommonAPI.get_user(&1.data["actor"], false))
107 |> Enum.filter(& &1)
108
109 UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes)
110 end
111
112 opts =
113 opts
114 |> Map.put(:replied_to_activities, replied_to_activities)
115 |> Map.put(:parent_activities, parent_activities)
116 |> Map.put(:relationships, relationships_opt)
117
118 safe_render_many(activities, StatusView, "show.json", opts)
119 end
120
121 def render(
122 "show.json",
123 %{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
124 ) do
125 user = CommonAPI.get_user(activity.data["actor"])
126 created_at = Utils.to_masto_date(activity.data["published"])
127 activity_object = Object.normalize(activity)
128
129 reblogged_parent_activity =
130 if opts[:parent_activities] do
131 Activity.Queries.find_by_object_ap_id(
132 opts[:parent_activities],
133 activity_object.data["id"]
134 )
135 else
136 Activity.create_by_object_ap_id(activity_object.data["id"])
137 |> Activity.with_preloaded_bookmark(opts[:for])
138 |> Activity.with_set_thread_muted_field(opts[:for])
139 |> Repo.one()
140 end
141
142 reblog_rendering_opts = Map.put(opts, :activity, reblogged_parent_activity)
143 reblogged = render("show.json", reblog_rendering_opts)
144
145 favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
146
147 bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil
148
149 mentions =
150 activity.recipients
151 |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
152 |> Enum.filter(& &1)
153 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
154
155 %{
156 id: to_string(activity.id),
157 uri: activity_object.data["id"],
158 url: activity_object.data["id"],
159 account:
160 AccountView.render("show.json", %{
161 user: user,
162 for: opts[:for]
163 }),
164 in_reply_to_id: nil,
165 in_reply_to_account_id: nil,
166 reblog: reblogged,
167 content: reblogged[:content] || "",
168 created_at: created_at,
169 reblogs_count: 0,
170 replies_count: 0,
171 favourites_count: 0,
172 reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
173 favourited: present?(favorited),
174 bookmarked: present?(bookmarked),
175 muted: false,
176 pinned: pinned?(activity, user),
177 sensitive: false,
178 spoiler_text: "",
179 visibility: get_visibility(activity),
180 media_attachments: reblogged[:media_attachments] || [],
181 mentions: mentions,
182 tags: reblogged[:tags] || [],
183 application: %{
184 name: "Web",
185 website: nil
186 },
187 language: nil,
188 emojis: [],
189 pleroma: %{
190 local: activity.local
191 }
192 }
193 end
194
195 def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
196 object = Object.normalize(activity)
197
198 user = CommonAPI.get_user(activity.data["actor"])
199 user_follower_address = user.follower_address
200
201 like_count = object.data["like_count"] || 0
202 announcement_count = object.data["announcement_count"] || 0
203
204 tags = object.data["tag"] || []
205 sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw")
206
207 tag_mentions =
208 tags
209 |> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end)
210 |> Enum.map(fn tag -> tag["href"] end)
211
212 mentions =
213 (object.data["to"] ++ tag_mentions)
214 |> Enum.uniq()
215 |> Enum.map(fn
216 Pleroma.Constants.as_public() -> nil
217 ^user_follower_address -> nil
218 ap_id -> User.get_cached_by_ap_id(ap_id)
219 end)
220 |> Enum.filter(& &1)
221 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
222
223 favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
224
225 bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
226
227 client_posted_this_activity = opts[:for] && user.id == opts[:for].id
228
229 expires_at =
230 with true <- client_posted_this_activity,
231 %ActivityExpiration{scheduled_at: scheduled_at} <-
232 ActivityExpiration.get_by_activity_id(activity.id) do
233 scheduled_at
234 else
235 _ -> nil
236 end
237
238 thread_muted? =
239 cond do
240 is_nil(opts[:for]) -> false
241 is_boolean(activity.thread_muted?) -> activity.thread_muted?
242 true -> CommonAPI.thread_muted?(opts[:for], activity)
243 end
244
245 attachment_data = object.data["attachment"] || []
246 attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
247
248 created_at = Utils.to_masto_date(object.data["published"])
249
250 reply_to = get_reply_to(activity, opts)
251
252 reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
253
254 content =
255 object
256 |> render_content()
257
258 content_html =
259 content
260 |> HTML.get_cached_scrubbed_html_for_activity(
261 User.html_filter_policy(opts[:for]),
262 activity,
263 "mastoapi:content"
264 )
265
266 content_plaintext =
267 content
268 |> HTML.get_cached_stripped_html_for_activity(
269 activity,
270 "mastoapi:content"
271 )
272
273 summary = object.data["summary"] || ""
274
275 card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
276
277 url =
278 if user.local do
279 Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
280 else
281 object.data["url"] || object.data["external_url"] || object.data["id"]
282 end
283
284 direct_conversation_id =
285 with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]},
286 {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
287 {_, %User{} = for_user} <- {:for_user, opts[:for]} do
288 Activity.direct_conversation_id(activity, for_user)
289 else
290 {:direct_conversation_id, participation_id} when is_integer(participation_id) ->
291 participation_id
292
293 _e ->
294 nil
295 end
296
297 emoji_reactions =
298 with %{data: %{"reactions" => emoji_reactions}} <- object do
299 Enum.map(emoji_reactions, fn
300 [emoji, users] when is_list(users) ->
301 build_emoji_map(emoji, users, opts[:for])
302
303 {emoji, users} when is_list(users) ->
304 build_emoji_map(emoji, users, opts[:for])
305
306 _ ->
307 nil
308 end)
309 |> Enum.reject(&is_nil/1)
310 else
311 _ -> []
312 end
313
314 # Status muted state (would do 1 request per status unless user mutes are preloaded)
315 muted =
316 thread_muted? ||
317 UserRelationship.exists?(
318 get_in(opts, [:relationships, :user_relationships]),
319 :mute,
320 opts[:for],
321 user,
322 fn for_user, user -> User.mutes?(for_user, user) end
323 )
324
325 %{
326 id: to_string(activity.id),
327 uri: object.data["id"],
328 url: url,
329 account:
330 AccountView.render("show.json", %{
331 user: user,
332 for: opts[:for]
333 }),
334 in_reply_to_id: reply_to && to_string(reply_to.id),
335 in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
336 reblog: nil,
337 card: card,
338 content: content_html,
339 text: opts[:with_source] && object.data["source"],
340 created_at: created_at,
341 reblogs_count: announcement_count,
342 replies_count: object.data["repliesCount"] || 0,
343 favourites_count: like_count,
344 reblogged: reblogged?(activity, opts[:for]),
345 favourited: present?(favorited),
346 bookmarked: present?(bookmarked),
347 muted: muted,
348 pinned: pinned?(activity, user),
349 sensitive: sensitive,
350 spoiler_text: summary,
351 visibility: get_visibility(object),
352 media_attachments: attachments,
353 poll: render(PollView, "show.json", object: object, for: opts[:for]),
354 mentions: mentions,
355 tags: build_tags(tags),
356 application: %{
357 name: "Web",
358 website: nil
359 },
360 language: nil,
361 emojis: build_emojis(object.data["emoji"]),
362 pleroma: %{
363 local: activity.local,
364 conversation_id: get_context_id(activity),
365 in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
366 content: %{"text/plain" => content_plaintext},
367 spoiler_text: %{"text/plain" => summary},
368 expires_at: expires_at,
369 direct_conversation_id: direct_conversation_id,
370 thread_muted: thread_muted?,
371 emoji_reactions: emoji_reactions,
372 parent_visible: visible_for_user?(reply_to, opts[:for])
373 }
374 }
375 end
376
377 def render("show.json", _) do
378 nil
379 end
380
381 def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
382 page_url_data = URI.parse(page_url)
383
384 page_url_data =
385 if is_binary(rich_media["url"]) do
386 URI.merge(page_url_data, URI.parse(rich_media["url"]))
387 else
388 page_url_data
389 end
390
391 page_url = page_url_data |> to_string
392
393 image_url =
394 if is_binary(rich_media["image"]) do
395 URI.merge(page_url_data, URI.parse(rich_media["image"]))
396 |> to_string
397 end
398
399 %{
400 type: "link",
401 provider_name: page_url_data.host,
402 provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
403 url: page_url,
404 image: image_url |> MediaProxy.url(),
405 title: rich_media["title"] || "",
406 description: rich_media["description"] || "",
407 pleroma: %{
408 opengraph: rich_media
409 }
410 }
411 end
412
413 def render("card.json", _), do: nil
414
415 def render("attachment.json", %{attachment: attachment}) do
416 [attachment_url | _] = attachment["url"]
417 media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
418 href = attachment_url["href"] |> MediaProxy.url()
419
420 type =
421 cond do
422 String.contains?(media_type, "image") -> "image"
423 String.contains?(media_type, "video") -> "video"
424 String.contains?(media_type, "audio") -> "audio"
425 true -> "unknown"
426 end
427
428 <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
429
430 %{
431 id: to_string(attachment["id"] || hash_id),
432 url: href,
433 remote_url: href,
434 preview_url: href,
435 text_url: href,
436 type: type,
437 description: attachment["name"],
438 pleroma: %{mime_type: media_type}
439 }
440 end
441
442 def render("context.json", %{activity: activity, activities: activities, user: user}) do
443 %{ancestors: ancestors, descendants: descendants} =
444 activities
445 |> Enum.reverse()
446 |> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
447 |> Map.put_new(:ancestors, [])
448 |> Map.put_new(:descendants, [])
449
450 %{
451 ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
452 descendants: render("index.json", for: user, activities: descendants, as: :activity)
453 }
454 end
455
456 def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
457 object = Object.normalize(activity)
458
459 with nil <- replied_to_activities[object.data["inReplyTo"]] do
460 # If user didn't participate in the thread
461 Activity.get_in_reply_to_activity(activity)
462 end
463 end
464
465 def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
466 object = Object.normalize(activity)
467
468 if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
469 Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
470 else
471 nil
472 end
473 end
474
475 def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do
476 url = object.data["url"] || object.data["id"]
477
478 "<p><a href=\"#{url}\">#{name}</a></p>#{object.data["content"]}"
479 end
480
481 def render_content(object), do: object.data["content"] || ""
482
483 @doc """
484 Builds a dictionary tags.
485
486 ## Examples
487
488 iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
489 [{"name": "fediverse", "url": "/tag/fediverse"},
490 {"name": "nextcloud", "url": "/tag/nextcloud"}]
491
492 """
493 @spec build_tags(list(any())) :: list(map())
494 def build_tags(object_tags) when is_list(object_tags) do
495 object_tags
496 |> Enum.filter(&is_binary/1)
497 |> Enum.map(&%{name: &1, url: "/tag/#{URI.encode(&1)}"})
498 end
499
500 def build_tags(_), do: []
501
502 @doc """
503 Builds list emojis.
504
505 Arguments: `nil` or list tuple of name and url.
506
507 Returns list emojis.
508
509 ## Examples
510
511 iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
512 [%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
513
514 """
515 @spec build_emojis(nil | list(tuple())) :: list(map())
516 def build_emojis(nil), do: []
517
518 def build_emojis(emojis) do
519 emojis
520 |> Enum.map(fn {name, url} ->
521 name = HTML.strip_tags(name)
522
523 url =
524 url
525 |> HTML.strip_tags()
526 |> MediaProxy.url()
527
528 %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
529 end)
530 end
531
532 defp present?(nil), do: false
533 defp present?(false), do: false
534 defp present?(_), do: true
535
536 defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}),
537 do: id in pinned_activities
538
539 defp build_emoji_map(emoji, users, current_user) do
540 %{
541 name: emoji,
542 count: length(users),
543 me: !!(current_user && current_user.ap_id in users)
544 }
545 end
546 end