Add debug logs to timeline rendering to assist debugging
[akkoma] / lib / pleroma / web / mastodon_api / views / status_view.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 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.HTML
12 alias Pleroma.Maps
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 alias Pleroma.Web.PleromaAPI.EmojiReactionController
24 require Logger
25
26 import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]
27
28 # This is a naive way to do this, just spawning a process per activity
29 # to fetch the preview. However it should be fine considering
30 # pagination is restricted to 40 activities at a time
31 defp fetch_rich_media_for_activities(activities) do
32 Enum.each(activities, fn activity ->
33 spawn(fn ->
34 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
35 end)
36 end)
37 end
38
39 # TODO: Add cached version.
40 defp get_replied_to_activities([]), do: %{}
41
42 defp get_replied_to_activities(activities) do
43 activities
44 |> Enum.map(fn
45 %{data: %{"type" => "Create"}} = activity ->
46 object = Object.normalize(activity, fetch: false)
47 object && object.data["inReplyTo"] != "" && object.data["inReplyTo"]
48
49 _ ->
50 nil
51 end)
52 |> Enum.filter(& &1)
53 |> Activity.create_by_object_ap_id_with_object()
54 |> Repo.all()
55 |> Enum.reduce(%{}, fn activity, acc ->
56 object = Object.normalize(activity, fetch: false)
57 if object, do: Map.put(acc, object.data["id"], activity), else: acc
58 end)
59 end
60
61 # DEPRECATED This field seems to be a left-over from the StatusNet era.
62 # If your application uses `pleroma.conversation_id`: this field is deprecated.
63 # It is currently stubbed instead by doing a CRC32 of the context, and
64 # clearing the MSB to avoid overflow exceptions with signed integers on the
65 # different clients using this field (Java/Kotlin code, mostly; see Husky.)
66 # This should be removed in a future version of Pleroma. Pleroma-FE currently
67 # depends on this field, as well.
68 defp get_context_id(%{data: %{"context" => context}}) when is_binary(context) do
69 import Bitwise
70
71 :erlang.crc32(context)
72 |> band(bnot(0x8000_0000))
73 end
74
75 defp get_context_id(_), do: nil
76
77 # Check if the user reblogged this status
78 defp reblogged?(activity, %User{ap_id: ap_id}) do
79 with %Object{data: %{"announcements" => announcements}} when is_list(announcements) <-
80 Object.normalize(activity, fetch: false) do
81 ap_id in announcements
82 else
83 _ -> false
84 end
85 end
86
87 # False if the user is logged out
88 defp reblogged?(_activity, _user), do: false
89
90 def render("index.json", opts) do
91 Logger.debug("Rendering index")
92 reading_user = opts[:for]
93 # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
94 activities = Enum.filter(opts.activities, & &1)
95
96 # Start fetching rich media before doing anything else, so that later calls to get the cards
97 # only block for timeout in the worst case, as opposed to
98 # length(activities_with_links) * timeout
99 fetch_rich_media_for_activities(activities)
100 replied_to_activities = get_replied_to_activities(activities)
101
102 parent_activities =
103 activities
104 |> Enum.filter(&(&1.data["type"] == "Announce" && &1.data["object"]))
105 |> Enum.map(&Object.normalize(&1, fetch: false).data["id"])
106 |> Activity.create_by_object_ap_id()
107 |> Activity.with_preloaded_object(:left)
108 |> Activity.with_preloaded_bookmark(reading_user)
109 |> Activity.with_set_thread_muted_field(reading_user)
110 |> Repo.all()
111
112 relationships_opt =
113 cond do
114 Map.has_key?(opts, :relationships) ->
115 opts[:relationships]
116
117 is_nil(reading_user) ->
118 UserRelationship.view_relationships_option(nil, [])
119
120 true ->
121 # Note: unresolved users are filtered out
122 actors =
123 (activities ++ parent_activities)
124 |> Enum.map(&CommonAPI.get_user(&1.data["actor"], false))
125 |> Enum.filter(& &1)
126
127 UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes)
128 end
129
130 opts =
131 opts
132 |> Map.put(:replied_to_activities, replied_to_activities)
133 |> Map.put(:parent_activities, parent_activities)
134 |> Map.put(:relationships, relationships_opt)
135
136 render_many(activities, StatusView, "show.json", opts)
137 end
138
139 def render(
140 "show.json",
141 %{activity: %{id: id, data: %{"type" => "Announce", "object" => _object}} = activity} =
142 opts
143 ) do
144 Logger.debug("Rendering reblog #{id}")
145 user = CommonAPI.get_user(activity.data["actor"])
146 created_at = Utils.to_masto_date(activity.data["published"])
147 object = Object.normalize(activity, fetch: false)
148
149 reblogged_parent_activity =
150 if opts[:parent_activities] do
151 Activity.Queries.find_by_object_ap_id(
152 opts[:parent_activities],
153 object.data["id"]
154 )
155 else
156 Activity.create_by_object_ap_id(object.data["id"])
157 |> Activity.with_preloaded_bookmark(opts[:for])
158 |> Activity.with_set_thread_muted_field(opts[:for])
159 |> Repo.one()
160 end
161
162 reblog_rendering_opts = Map.put(opts, :activity, reblogged_parent_activity)
163 reblogged = render("show.json", reblog_rendering_opts)
164
165 favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
166
167 bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil
168
169 mentions =
170 activity.recipients
171 |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
172 |> Enum.filter(& &1)
173 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
174
175 {pinned?, pinned_at} = pin_data(object, user)
176 lang = language(object)
177
178 %{
179 id: to_string(activity.id),
180 uri: object.data["id"],
181 url: object.data["id"],
182 account:
183 AccountView.render("show.json", %{
184 user: user,
185 for: opts[:for]
186 }),
187 in_reply_to_id: nil,
188 in_reply_to_account_id: nil,
189 reblog: reblogged,
190 content: "",
191 created_at: created_at,
192 reblogs_count: 0,
193 replies_count: 0,
194 favourites_count: 0,
195 reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
196 favourited: present?(favorited),
197 bookmarked: present?(bookmarked),
198 muted: false,
199 pinned: pinned?,
200 sensitive: false,
201 spoiler_text: "",
202 visibility: get_visibility(activity),
203 media_attachments: reblogged[:media_attachments] || [],
204 mentions: mentions,
205 tags: reblogged[:tags] || [],
206 application: build_application(object.data["generator"]),
207 language: lang,
208 emojis: [],
209 pleroma: %{
210 local: activity.local,
211 pinned_at: pinned_at
212 }
213 }
214 end
215
216 def render("show.json", %{activity: %{id: id, data: %{"object" => _object}} = activity} = opts) do
217 Logger.debug("Rendering status #{id}")
218
219 with %Object{} = object <- Object.normalize(activity, fetch: false) do
220 user = CommonAPI.get_user(activity.data["actor"])
221 user_follower_address = user.follower_address
222
223 like_count = object.data["like_count"] || 0
224 announcement_count = object.data["announcement_count"] || 0
225
226 hashtags = Object.hashtags(object)
227 sensitive = object.data["sensitive"] || Enum.member?(hashtags, "nsfw")
228
229 tags = Object.tags(object)
230
231 tag_mentions =
232 tags
233 |> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end)
234 |> Enum.map(fn tag -> tag["href"] end)
235
236 to_data = if is_nil(object.data["to"]), do: [], else: object.data["to"]
237
238 mentions =
239 (to_data ++ tag_mentions)
240 |> Enum.uniq()
241 |> Enum.map(fn
242 Pleroma.Constants.as_public() -> nil
243 ^user_follower_address -> nil
244 ap_id -> User.get_cached_by_ap_id(ap_id)
245 end)
246 |> Enum.filter(& &1)
247 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
248
249 favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
250
251 bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
252
253 client_posted_this_activity = opts[:for] && user.id == opts[:for].id
254
255 expires_at =
256 with true <- client_posted_this_activity,
257 %Oban.Job{scheduled_at: scheduled_at} <-
258 Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) do
259 scheduled_at
260 else
261 _ -> nil
262 end
263
264 thread_muted? =
265 cond do
266 is_nil(opts[:for]) -> false
267 is_boolean(activity.thread_muted?) -> activity.thread_muted?
268 true -> CommonAPI.thread_muted?(opts[:for], activity)
269 end
270
271 attachment_data = object.data["attachment"] || []
272 attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
273
274 created_at = Utils.to_masto_date(object.data["published"])
275
276 edited_at =
277 with %{"updated" => updated} <- object.data,
278 date <- Utils.to_masto_date(updated),
279 true <- date != "" do
280 date
281 else
282 _ ->
283 nil
284 end
285
286 reply_to = get_reply_to(activity, opts)
287
288 reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
289
290 history_len =
291 1 +
292 (Object.Updater.history_for(object.data)
293 |> Map.get("orderedItems")
294 |> length())
295
296 # See render("history.json", ...) for more details
297 # Here the implicit index of the current content is 0
298 chrono_order = history_len - 1
299
300 content =
301 object
302 |> render_content()
303
304 content_html =
305 content
306 |> Activity.HTML.get_cached_scrubbed_html_for_activity(
307 User.html_filter_policy(opts[:for]),
308 activity,
309 "mastoapi:content:#{chrono_order}"
310 )
311
312 content_plaintext =
313 content
314 |> Activity.HTML.get_cached_stripped_html_for_activity(
315 activity,
316 "mastoapi:content:#{chrono_order}"
317 )
318
319 summary = object.data["summary"] || ""
320
321 card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
322
323 url =
324 if user.local do
325 Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
326 else
327 object.data["url"] || object.data["external_url"] || object.data["id"]
328 end
329
330 direct_conversation_id =
331 with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]},
332 {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
333 {_, %User{} = for_user} <- {:for_user, opts[:for]} do
334 Activity.direct_conversation_id(activity, for_user)
335 else
336 {:direct_conversation_id, participation_id} when is_integer(participation_id) ->
337 participation_id
338
339 _e ->
340 nil
341 end
342
343 emoji_reactions =
344 object.data
345 |> Map.get("reactions", [])
346 |> EmojiReactionController.filter_allowed_users(
347 opts[:for],
348 Map.get(opts, :with_muted, false)
349 )
350 |> Stream.map(fn {emoji, users, url} ->
351 build_emoji_map(emoji, users, url, opts[:for])
352 end)
353 |> Enum.to_list()
354
355 # Status muted state (would do 1 request per status unless user mutes are preloaded)
356 muted =
357 thread_muted? ||
358 UserRelationship.exists?(
359 get_in(opts, [:relationships, :user_relationships]),
360 :mute,
361 opts[:for],
362 user,
363 fn for_user, user -> User.mutes?(for_user, user) end
364 )
365
366 {pinned?, pinned_at} = pin_data(object, user)
367
368 quote = Activity.get_quoted_activity_from_object(object)
369 lang = language(object)
370
371 %{
372 id: to_string(activity.id),
373 uri: object.data["id"],
374 url: url,
375 account:
376 AccountView.render("show.json", %{
377 user: user,
378 for: opts[:for]
379 }),
380 in_reply_to_id: reply_to && to_string(reply_to.id),
381 in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
382 reblog: nil,
383 card: card,
384 content: content_html,
385 text: opts[:with_source] && get_source_text(object.data["source"]),
386 created_at: created_at,
387 edited_at: edited_at,
388 reblogs_count: announcement_count,
389 replies_count: object.data["repliesCount"] || 0,
390 favourites_count: like_count,
391 reblogged: reblogged?(activity, opts[:for]),
392 favourited: present?(favorited),
393 bookmarked: present?(bookmarked),
394 muted: muted,
395 pinned: pinned?,
396 sensitive: sensitive,
397 spoiler_text: summary,
398 visibility: get_visibility(object),
399 media_attachments: attachments,
400 poll: render(PollView, "show.json", object: object, for: opts[:for]),
401 mentions: mentions,
402 tags: build_tags(tags),
403 application: build_application(object.data["generator"]),
404 language: lang,
405 emojis: build_emojis(object.data["emoji"]),
406 quote_id: if(quote, do: quote.id, else: nil),
407 quote: maybe_render_quote(quote, opts),
408 emoji_reactions: emoji_reactions,
409 pleroma: %{
410 local: activity.local,
411 conversation_id: get_context_id(activity),
412 context: object.data["context"],
413 in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
414 content: %{"text/plain" => content_plaintext},
415 spoiler_text: %{"text/plain" => summary},
416 expires_at: expires_at,
417 direct_conversation_id: direct_conversation_id,
418 thread_muted: thread_muted?,
419 emoji_reactions: emoji_reactions,
420 parent_visible: visible_for_user?(reply_to, opts[:for]),
421 pinned_at: pinned_at
422 },
423 akkoma: %{
424 source: object.data["source"]
425 }
426 }
427 else
428 nil -> nil
429 end
430 end
431
432 def render("show.json", _) do
433 nil
434 end
435
436 def render("history.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
437 Logger.debug("Rendering history for #{activity.id}")
438 object = Object.normalize(activity, fetch: false)
439
440 hashtags = Object.hashtags(object)
441
442 user = CommonAPI.get_user(activity.data["actor"])
443
444 past_history =
445 Object.Updater.history_for(object.data)
446 |> Map.get("orderedItems")
447 |> Enum.map(&Map.put(&1, "id", object.data["id"]))
448 |> Enum.map(&%Object{data: &1, id: object.id})
449
450 history =
451 [object | past_history]
452 # Mastodon expects the original to be at the first
453 |> Enum.reverse()
454 |> Enum.with_index()
455 |> Enum.map(fn {object, chrono_order} ->
456 %{
457 # The history is prepended every time there is a new edit.
458 # In chrono_order, the oldest item is always at 0, and so on.
459 # The chrono_order is an invariant kept between edits.
460 chrono_order: chrono_order,
461 object: object
462 }
463 end)
464
465 individual_opts =
466 opts
467 |> Map.put(:as, :item)
468 |> Map.put(:user, user)
469 |> Map.put(:hashtags, hashtags)
470
471 render_many(history, StatusView, "history_item.json", individual_opts)
472 end
473
474 def render(
475 "history_item.json",
476 %{
477 activity: activity,
478 user: user,
479 item: %{object: object, chrono_order: chrono_order},
480 hashtags: hashtags
481 } = opts
482 ) do
483 sensitive = object.data["sensitive"] || Enum.member?(hashtags, "nsfw")
484
485 attachment_data = object.data["attachment"] || []
486 attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
487
488 created_at = Utils.to_masto_date(object.data["updated"] || object.data["published"])
489
490 content =
491 object
492 |> render_content()
493
494 content_html =
495 content
496 |> Activity.HTML.get_cached_scrubbed_html_for_activity(
497 User.html_filter_policy(opts[:for]),
498 activity,
499 "mastoapi:content:#{chrono_order}"
500 )
501
502 summary = object.data["summary"] || ""
503
504 %{
505 account:
506 AccountView.render("show.json", %{
507 user: user,
508 for: opts[:for]
509 }),
510 content: content_html,
511 sensitive: sensitive,
512 spoiler_text: summary,
513 created_at: created_at,
514 media_attachments: attachments,
515 emojis: build_emojis(object.data["emoji"]),
516 poll: render(PollView, "show.json", object: object, for: opts[:for])
517 }
518 end
519
520 def render("source.json", %{activity: %{data: %{"object" => _object}} = activity} = _opts) do
521 object = Object.normalize(activity, fetch: false)
522
523 %{
524 id: activity.id,
525 text: get_source_text(Map.get(object.data, "source", "")),
526 spoiler_text: Map.get(object.data, "summary", ""),
527 content_type: get_source_content_type(object.data["source"])
528 }
529 end
530
531 def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
532 page_url_data = URI.parse(page_url)
533
534 page_url_data =
535 if is_binary(rich_media["url"]) do
536 URI.merge(page_url_data, URI.parse(rich_media["url"]))
537 else
538 page_url_data
539 end
540
541 page_url = page_url_data |> to_string
542
543 image_url_data =
544 if is_binary(rich_media["image"]) do
545 URI.parse(rich_media["image"])
546 else
547 nil
548 end
549
550 image_url = build_image_url(image_url_data, page_url_data)
551
552 %{
553 type: "link",
554 provider_name: page_url_data.host,
555 provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
556 url: page_url,
557 image: image_url |> MediaProxy.url(),
558 title: rich_media["title"] || "",
559 description: rich_media["description"] || "",
560 pleroma: %{
561 opengraph: rich_media
562 }
563 }
564 end
565
566 def render("card.json", _), do: nil
567
568 def render("attachment.json", %{attachment: attachment}) do
569 [attachment_url | _] = attachment["url"]
570 media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
571 href = attachment_url["href"] |> MediaProxy.url()
572 href_preview = attachment_url["href"] |> MediaProxy.preview_url()
573 meta = render("attachment_meta.json", %{attachment: attachment})
574
575 type =
576 cond do
577 String.contains?(media_type, "image") -> "image"
578 String.contains?(media_type, "video") -> "video"
579 String.contains?(media_type, "audio") -> "audio"
580 true -> "unknown"
581 end
582
583 attachment_id =
584 with {_, ap_id} when is_binary(ap_id) <- {:ap_id, attachment["id"]},
585 {_, %Object{data: _object_data, id: object_id}} <-
586 {:object, Object.get_by_ap_id(ap_id)} do
587 to_string(object_id)
588 else
589 _ ->
590 <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
591 to_string(attachment["id"] || hash_id)
592 end
593
594 %{
595 id: attachment_id,
596 url: href,
597 remote_url: href,
598 preview_url: href_preview,
599 text_url: href,
600 type: type,
601 description: attachment["name"],
602 pleroma: %{mime_type: media_type},
603 blurhash: attachment["blurhash"]
604 }
605 |> Maps.put_if_present(:meta, meta)
606 end
607
608 def render("attachment_meta.json", %{
609 attachment: %{"url" => [%{"width" => width, "height" => height} | _]}
610 })
611 when is_integer(width) and is_integer(height) do
612 %{
613 original: %{
614 width: width,
615 height: height,
616 aspect: width / height
617 }
618 }
619 end
620
621 def render("attachment_meta.json", _), do: nil
622
623 def render("context.json", %{activity: activity, activities: activities, user: user}) do
624 Logger.debug("Rendering context for #{activity.id}")
625
626 %{ancestors: ancestors, descendants: descendants} =
627 activities
628 |> Enum.reverse()
629 |> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
630 |> Map.put_new(:ancestors, [])
631 |> Map.put_new(:descendants, [])
632
633 %{
634 ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
635 descendants: render("index.json", for: user, activities: descendants, as: :activity)
636 }
637 end
638
639 def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
640 object = Object.normalize(activity, fetch: false)
641
642 with nil <- replied_to_activities[object.data["inReplyTo"]] do
643 # If user didn't participate in the thread
644 Activity.get_in_reply_to_activity(activity)
645 end
646 end
647
648 def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
649 object = Object.normalize(activity, fetch: false)
650
651 if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
652 Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
653 else
654 nil
655 end
656 end
657
658 def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do
659 url = object.data["url"] || object.data["id"]
660
661 "<p><a href=\"#{url}\">#{name}</a></p>#{object.data["content"]}"
662 end
663
664 def render_content(object), do: object.data["content"] || ""
665
666 @doc """
667 Builds a dictionary tags.
668
669 ## Examples
670
671 iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
672 [{"name": "fediverse", "url": "/tag/fediverse"},
673 {"name": "nextcloud", "url": "/tag/nextcloud"}]
674
675 """
676 @spec build_tags(list(any())) :: list(map())
677 def build_tags(object_tags) when is_list(object_tags) do
678 object_tags
679 |> Enum.filter(&is_binary/1)
680 |> Enum.map(&%{name: &1, url: "#{Pleroma.Web.Endpoint.url()}/tag/#{URI.encode(&1)}"})
681 end
682
683 def build_tags(_), do: []
684
685 @doc """
686 Builds list emojis.
687
688 Arguments: `nil` or list tuple of name and url.
689
690 Returns list emojis.
691
692 ## Examples
693
694 iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
695 [%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
696
697 """
698 @spec build_emojis(nil | list(tuple())) :: list(map())
699 def build_emojis(nil), do: []
700
701 def build_emojis(emojis) do
702 emojis
703 |> Enum.map(fn {name, url} ->
704 name = HTML.strip_tags(name)
705
706 url =
707 url
708 |> HTML.strip_tags()
709 |> MediaProxy.url()
710
711 %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
712 end)
713 end
714
715 defp present?(nil), do: false
716 defp present?(false), do: false
717 defp present?(_), do: true
718
719 defp pin_data(%Object{data: %{"id" => object_id}}, %User{pinned_objects: pinned_objects}) do
720 if pinned_at = pinned_objects[object_id] do
721 {true, Utils.to_masto_date(pinned_at)}
722 else
723 {false, nil}
724 end
725 end
726
727 defp build_emoji_map(emoji, users, url, current_user) do
728 %{
729 name: Pleroma.Web.PleromaAPI.EmojiReactionView.emoji_name(emoji, url),
730 count: length(users),
731 url: MediaProxy.url(url),
732 me: !!(current_user && current_user.ap_id in users),
733 account_ids: Enum.map(users, fn user -> User.get_cached_by_ap_id(user).id end)
734 }
735 end
736
737 @spec build_application(map() | nil) :: map() | nil
738 defp build_application(%{"type" => _type, "name" => name, "url" => url}),
739 do: %{name: name, website: url}
740
741 defp build_application(_), do: nil
742
743 # Workaround for Elixir issue #10771
744 # Avoid applying URI.merge unless necessary
745 # TODO: revert to always attempting URI.merge(image_url_data, page_url_data)
746 # when Elixir 1.12 is the minimum supported version
747 @spec build_image_url(struct() | nil, struct()) :: String.t() | nil
748 defp build_image_url(
749 %URI{scheme: image_scheme, host: image_host} = image_url_data,
750 %URI{} = _page_url_data
751 )
752 when not is_nil(image_scheme) and not is_nil(image_host) do
753 image_url_data |> to_string
754 end
755
756 defp build_image_url(%URI{} = image_url_data, %URI{} = page_url_data) do
757 URI.merge(page_url_data, image_url_data) |> to_string
758 end
759
760 defp build_image_url(_, _), do: nil
761
762 defp maybe_render_quote(nil, _), do: nil
763
764 defp maybe_render_quote(quote, opts) do
765 with %User{} = quoted_user <- User.get_cached_by_ap_id(quote.actor),
766 false <- Map.get(opts, :do_not_recurse, false),
767 true <- visible_for_user?(quote, opts[:for]),
768 false <- User.blocks?(opts[:for], quoted_user),
769 false <- User.mutes?(opts[:for], quoted_user) do
770 opts =
771 opts
772 |> Map.put(:activity, quote)
773 |> Map.put(:do_not_recurse, true)
774
775 render("show.json", opts)
776 else
777 _ -> nil
778 end
779 end
780
781 defp get_source_text(%{"content" => content} = _source) do
782 content
783 end
784
785 defp get_source_text(source) when is_binary(source) do
786 source
787 end
788
789 defp get_source_text(_) do
790 ""
791 end
792
793 defp get_source_content_type(%{"mediaType" => type} = _source) do
794 type
795 end
796
797 defp get_source_content_type(_source) do
798 Utils.get_content_type(nil)
799 end
800
801 defp language(%Object{data: %{"contentMap" => contentMap}}) when is_map(contentMap) do
802 contentMap
803 |> Map.keys()
804 |> Enum.at(0)
805 end
806
807 defp language(_), do: nil
808 end