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