Add ability to set a default post expiry (#321)
[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 safe_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
173 %{
174 id: to_string(activity.id),
175 uri: object.data["id"],
176 url: object.data["id"],
177 account:
178 AccountView.render("show.json", %{
179 user: user,
180 for: opts[:for]
181 }),
182 in_reply_to_id: nil,
183 in_reply_to_account_id: nil,
184 reblog: reblogged,
185 content: reblogged[:content] || "",
186 created_at: created_at,
187 reblogs_count: 0,
188 replies_count: 0,
189 favourites_count: 0,
190 reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
191 favourited: present?(favorited),
192 bookmarked: present?(bookmarked),
193 muted: false,
194 pinned: pinned?,
195 sensitive: false,
196 spoiler_text: "",
197 visibility: get_visibility(activity),
198 media_attachments: reblogged[:media_attachments] || [],
199 mentions: mentions,
200 tags: reblogged[:tags] || [],
201 application: build_application(object.data["generator"]),
202 language: nil,
203 emojis: [],
204 pleroma: %{
205 local: activity.local,
206 pinned_at: pinned_at
207 }
208 }
209 end
210
211 def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
212 with %Object{} = object <- Object.normalize(activity, fetch: false) do
213 user = CommonAPI.get_user(activity.data["actor"])
214 user_follower_address = user.follower_address
215
216 like_count = object.data["like_count"] || 0
217 announcement_count = object.data["announcement_count"] || 0
218
219 hashtags = Object.hashtags(object)
220 sensitive = object.data["sensitive"] || Enum.member?(hashtags, "nsfw")
221
222 tags = Object.tags(object)
223
224 tag_mentions =
225 tags
226 |> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end)
227 |> Enum.map(fn tag -> tag["href"] end)
228
229 mentions =
230 (object.data["to"] ++ tag_mentions)
231 |> Enum.uniq()
232 |> Enum.map(fn
233 Pleroma.Constants.as_public() -> nil
234 ^user_follower_address -> nil
235 ap_id -> User.get_cached_by_ap_id(ap_id)
236 end)
237 |> Enum.filter(& &1)
238 |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
239
240 favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
241
242 bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
243
244 client_posted_this_activity = opts[:for] && user.id == opts[:for].id
245
246 expires_at =
247 with true <- client_posted_this_activity,
248 %Oban.Job{scheduled_at: scheduled_at} <-
249 Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) do
250 scheduled_at
251 else
252 _ -> nil
253 end
254
255 thread_muted? =
256 cond do
257 is_nil(opts[:for]) -> false
258 is_boolean(activity.thread_muted?) -> activity.thread_muted?
259 true -> CommonAPI.thread_muted?(opts[:for], activity)
260 end
261
262 attachment_data = object.data["attachment"] || []
263 attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
264
265 created_at = Utils.to_masto_date(object.data["published"])
266
267 edited_at =
268 with %{"updated" => updated} <- object.data,
269 date <- Utils.to_masto_date(updated),
270 true <- date != "" do
271 date
272 else
273 _ ->
274 nil
275 end
276
277 reply_to = get_reply_to(activity, opts)
278
279 reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
280
281 history_len =
282 1 +
283 (Object.Updater.history_for(object.data)
284 |> Map.get("orderedItems")
285 |> length())
286
287 # See render("history.json", ...) for more details
288 # Here the implicit index of the current content is 0
289 chrono_order = history_len - 1
290
291 content =
292 object
293 |> render_content()
294
295 content_html =
296 content
297 |> Activity.HTML.get_cached_scrubbed_html_for_activity(
298 User.html_filter_policy(opts[:for]),
299 activity,
300 "mastoapi:content:#{chrono_order}"
301 )
302
303 content_plaintext =
304 content
305 |> Activity.HTML.get_cached_stripped_html_for_activity(
306 activity,
307 "mastoapi:content:#{chrono_order}"
308 )
309
310 summary = object.data["summary"] || ""
311
312 card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
313
314 url =
315 if user.local do
316 Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
317 else
318 object.data["url"] || object.data["external_url"] || object.data["id"]
319 end
320
321 direct_conversation_id =
322 with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]},
323 {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
324 {_, %User{} = for_user} <- {:for_user, opts[:for]} do
325 Activity.direct_conversation_id(activity, for_user)
326 else
327 {:direct_conversation_id, participation_id} when is_integer(participation_id) ->
328 participation_id
329
330 _e ->
331 nil
332 end
333
334 emoji_reactions =
335 object.data
336 |> Map.get("reactions", [])
337 |> EmojiReactionController.filter_allowed_users(
338 opts[:for],
339 Map.get(opts, :with_muted, false)
340 )
341 |> Stream.map(fn {emoji, users, url} ->
342 build_emoji_map(emoji, users, url, opts[:for])
343 end)
344 |> Enum.to_list()
345
346 # Status muted state (would do 1 request per status unless user mutes are preloaded)
347 muted =
348 thread_muted? ||
349 UserRelationship.exists?(
350 get_in(opts, [:relationships, :user_relationships]),
351 :mute,
352 opts[:for],
353 user,
354 fn for_user, user -> User.mutes?(for_user, user) end
355 )
356
357 {pinned?, pinned_at} = pin_data(object, user)
358
359 quote = Activity.get_quoted_activity_from_object(object)
360
361 %{
362 id: to_string(activity.id),
363 uri: object.data["id"],
364 url: url,
365 account:
366 AccountView.render("show.json", %{
367 user: user,
368 for: opts[:for]
369 }),
370 in_reply_to_id: reply_to && to_string(reply_to.id),
371 in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
372 reblog: nil,
373 card: card,
374 content: content_html,
375 text: opts[:with_source] && get_source_text(object.data["source"]),
376 created_at: created_at,
377 edited_at: edited_at,
378 reblogs_count: announcement_count,
379 replies_count: object.data["repliesCount"] || 0,
380 favourites_count: like_count,
381 reblogged: reblogged?(activity, opts[:for]),
382 favourited: present?(favorited),
383 bookmarked: present?(bookmarked),
384 muted: muted,
385 pinned: pinned?,
386 sensitive: sensitive,
387 spoiler_text: summary,
388 visibility: get_visibility(object),
389 media_attachments: attachments,
390 poll: render(PollView, "show.json", object: object, for: opts[:for]),
391 mentions: mentions,
392 tags: build_tags(tags),
393 application: build_application(object.data["generator"]),
394 language: nil,
395 emojis: build_emojis(object.data["emoji"]),
396 quote_id: if(quote, do: quote.id, else: nil),
397 quote: maybe_render_quote(quote, opts),
398 emoji_reactions: emoji_reactions,
399 pleroma: %{
400 local: activity.local,
401 conversation_id: get_context_id(activity),
402 context: object.data["context"],
403 in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
404 content: %{"text/plain" => content_plaintext},
405 spoiler_text: %{"text/plain" => summary},
406 expires_at: expires_at,
407 direct_conversation_id: direct_conversation_id,
408 thread_muted: thread_muted?,
409 emoji_reactions: emoji_reactions,
410 parent_visible: visible_for_user?(reply_to, opts[:for]),
411 pinned_at: pinned_at
412 },
413 akkoma: %{
414 source: object.data["source"]
415 }
416 }
417 else
418 nil -> nil
419 end
420 end
421
422 def render("show.json", _) do
423 nil
424 end
425
426 def render("history.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
427 object = Object.normalize(activity, fetch: false)
428
429 hashtags = Object.hashtags(object)
430
431 user = CommonAPI.get_user(activity.data["actor"])
432
433 past_history =
434 Object.Updater.history_for(object.data)
435 |> Map.get("orderedItems")
436 |> Enum.map(&Map.put(&1, "id", object.data["id"]))
437 |> Enum.map(&%Object{data: &1, id: object.id})
438
439 history =
440 [object | past_history]
441 # Mastodon expects the original to be at the first
442 |> Enum.reverse()
443 |> Enum.with_index()
444 |> Enum.map(fn {object, chrono_order} ->
445 %{
446 # The history is prepended every time there is a new edit.
447 # In chrono_order, the oldest item is always at 0, and so on.
448 # The chrono_order is an invariant kept between edits.
449 chrono_order: chrono_order,
450 object: object
451 }
452 end)
453
454 individual_opts =
455 opts
456 |> Map.put(:as, :item)
457 |> Map.put(:user, user)
458 |> Map.put(:hashtags, hashtags)
459
460 render_many(history, StatusView, "history_item.json", individual_opts)
461 end
462
463 def render(
464 "history_item.json",
465 %{
466 activity: activity,
467 user: user,
468 item: %{object: object, chrono_order: chrono_order},
469 hashtags: hashtags
470 } = opts
471 ) do
472 sensitive = object.data["sensitive"] || Enum.member?(hashtags, "nsfw")
473
474 attachment_data = object.data["attachment"] || []
475 attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
476
477 created_at = Utils.to_masto_date(object.data["updated"] || object.data["published"])
478
479 content =
480 object
481 |> render_content()
482
483 content_html =
484 content
485 |> Activity.HTML.get_cached_scrubbed_html_for_activity(
486 User.html_filter_policy(opts[:for]),
487 activity,
488 "mastoapi:content:#{chrono_order}"
489 )
490
491 summary = object.data["summary"] || ""
492
493 %{
494 account:
495 AccountView.render("show.json", %{
496 user: user,
497 for: opts[:for]
498 }),
499 content: content_html,
500 sensitive: sensitive,
501 spoiler_text: summary,
502 created_at: created_at,
503 media_attachments: attachments,
504 emojis: build_emojis(object.data["emoji"]),
505 poll: render(PollView, "show.json", object: object, for: opts[:for])
506 }
507 end
508
509 def render("source.json", %{activity: %{data: %{"object" => _object}} = activity} = _opts) do
510 object = Object.normalize(activity, fetch: false)
511
512 %{
513 id: activity.id,
514 text: get_source_text(Map.get(object.data, "source", "")),
515 spoiler_text: Map.get(object.data, "summary", ""),
516 content_type: get_source_content_type(object.data["source"])
517 }
518 end
519
520 def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
521 page_url_data = URI.parse(page_url)
522
523 page_url_data =
524 if is_binary(rich_media["url"]) do
525 URI.merge(page_url_data, URI.parse(rich_media["url"]))
526 else
527 page_url_data
528 end
529
530 page_url = page_url_data |> to_string
531
532 image_url_data =
533 if is_binary(rich_media["image"]) do
534 URI.parse(rich_media["image"])
535 else
536 nil
537 end
538
539 image_url = build_image_url(image_url_data, page_url_data)
540
541 %{
542 type: "link",
543 provider_name: page_url_data.host,
544 provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
545 url: page_url,
546 image: image_url |> MediaProxy.url(),
547 title: rich_media["title"] || "",
548 description: rich_media["description"] || "",
549 pleroma: %{
550 opengraph: rich_media
551 }
552 }
553 end
554
555 def render("card.json", _), do: nil
556
557 def render("attachment.json", %{attachment: attachment}) do
558 [attachment_url | _] = attachment["url"]
559 media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
560 href = attachment_url["href"] |> MediaProxy.url()
561 href_preview = attachment_url["href"] |> MediaProxy.preview_url()
562 meta = render("attachment_meta.json", %{attachment: attachment})
563
564 type =
565 cond do
566 String.contains?(media_type, "image") -> "image"
567 String.contains?(media_type, "video") -> "video"
568 String.contains?(media_type, "audio") -> "audio"
569 true -> "unknown"
570 end
571
572 attachment_id =
573 with {_, ap_id} when is_binary(ap_id) <- {:ap_id, attachment["id"]},
574 {_, %Object{data: _object_data, id: object_id}} <-
575 {:object, Object.get_by_ap_id(ap_id)} do
576 to_string(object_id)
577 else
578 _ ->
579 <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
580 to_string(attachment["id"] || hash_id)
581 end
582
583 %{
584 id: attachment_id,
585 url: href,
586 remote_url: href,
587 preview_url: href_preview,
588 text_url: href,
589 type: type,
590 description: attachment["name"],
591 pleroma: %{mime_type: media_type},
592 blurhash: attachment["blurhash"]
593 }
594 |> Maps.put_if_present(:meta, meta)
595 end
596
597 def render("attachment_meta.json", %{
598 attachment: %{"url" => [%{"width" => width, "height" => height} | _]}
599 })
600 when is_integer(width) and is_integer(height) do
601 %{
602 original: %{
603 width: width,
604 height: height,
605 aspect: width / height
606 }
607 }
608 end
609
610 def render("attachment_meta.json", _), do: nil
611
612 def render("context.json", %{activity: activity, activities: activities, user: user}) do
613 %{ancestors: ancestors, descendants: descendants} =
614 activities
615 |> Enum.reverse()
616 |> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
617 |> Map.put_new(:ancestors, [])
618 |> Map.put_new(:descendants, [])
619
620 %{
621 ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
622 descendants: render("index.json", for: user, activities: descendants, as: :activity)
623 }
624 end
625
626 def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
627 object = Object.normalize(activity, fetch: false)
628
629 with nil <- replied_to_activities[object.data["inReplyTo"]] do
630 # If user didn't participate in the thread
631 Activity.get_in_reply_to_activity(activity)
632 end
633 end
634
635 def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
636 object = Object.normalize(activity, fetch: false)
637
638 if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
639 Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
640 else
641 nil
642 end
643 end
644
645 def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do
646 url = object.data["url"] || object.data["id"]
647
648 "<p><a href=\"#{url}\">#{name}</a></p>#{object.data["content"]}"
649 end
650
651 def render_content(object), do: object.data["content"] || ""
652
653 @doc """
654 Builds a dictionary tags.
655
656 ## Examples
657
658 iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
659 [{"name": "fediverse", "url": "/tag/fediverse"},
660 {"name": "nextcloud", "url": "/tag/nextcloud"}]
661
662 """
663 @spec build_tags(list(any())) :: list(map())
664 def build_tags(object_tags) when is_list(object_tags) do
665 object_tags
666 |> Enum.filter(&is_binary/1)
667 |> Enum.map(&%{name: &1, url: "#{Pleroma.Web.Endpoint.url()}/tag/#{URI.encode(&1)}"})
668 end
669
670 def build_tags(_), do: []
671
672 @doc """
673 Builds list emojis.
674
675 Arguments: `nil` or list tuple of name and url.
676
677 Returns list emojis.
678
679 ## Examples
680
681 iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
682 [%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
683
684 """
685 @spec build_emojis(nil | list(tuple())) :: list(map())
686 def build_emojis(nil), do: []
687
688 def build_emojis(emojis) do
689 emojis
690 |> Enum.map(fn {name, url} ->
691 name = HTML.strip_tags(name)
692
693 url =
694 url
695 |> HTML.strip_tags()
696 |> MediaProxy.url()
697
698 %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
699 end)
700 end
701
702 defp present?(nil), do: false
703 defp present?(false), do: false
704 defp present?(_), do: true
705
706 defp pin_data(%Object{data: %{"id" => object_id}}, %User{pinned_objects: pinned_objects}) do
707 if pinned_at = pinned_objects[object_id] do
708 {true, Utils.to_masto_date(pinned_at)}
709 else
710 {false, nil}
711 end
712 end
713
714 defp build_emoji_map(emoji, users, url, current_user) do
715 %{
716 name: Pleroma.Web.PleromaAPI.EmojiReactionView.emoji_name(emoji, url),
717 count: length(users),
718 url: MediaProxy.url(url),
719 me: !!(current_user && current_user.ap_id in users),
720 account_ids: Enum.map(users, fn user -> User.get_cached_by_ap_id(user).id end)
721 }
722 end
723
724 @spec build_application(map() | nil) :: map() | nil
725 defp build_application(%{"type" => _type, "name" => name, "url" => url}),
726 do: %{name: name, website: url}
727
728 defp build_application(_), do: nil
729
730 # Workaround for Elixir issue #10771
731 # Avoid applying URI.merge unless necessary
732 # TODO: revert to always attempting URI.merge(image_url_data, page_url_data)
733 # when Elixir 1.12 is the minimum supported version
734 @spec build_image_url(struct() | nil, struct()) :: String.t() | nil
735 defp build_image_url(
736 %URI{scheme: image_scheme, host: image_host} = image_url_data,
737 %URI{} = _page_url_data
738 )
739 when not is_nil(image_scheme) and not is_nil(image_host) do
740 image_url_data |> to_string
741 end
742
743 defp build_image_url(%URI{} = image_url_data, %URI{} = page_url_data) do
744 URI.merge(page_url_data, image_url_data) |> to_string
745 end
746
747 defp build_image_url(_, _), do: nil
748
749 defp maybe_render_quote(nil, _), do: nil
750
751 defp maybe_render_quote(quote, opts) do
752 with %User{} = quoted_user <- User.get_cached_by_ap_id(quote.actor),
753 false <- Map.get(opts, :do_not_recurse, false),
754 true <- visible_for_user?(quote, opts[:for]),
755 false <- User.blocks?(opts[:for], quoted_user),
756 false <- User.mutes?(opts[:for], quoted_user) do
757 opts =
758 opts
759 |> Map.put(:activity, quote)
760 |> Map.put(:do_not_recurse, true)
761
762 render("show.json", opts)
763 else
764 _ -> nil
765 end
766 end
767
768 defp get_source_text(%{"content" => content} = _source) do
769 content
770 end
771
772 defp get_source_text(source) when is_binary(source) do
773 source
774 end
775
776 defp get_source_text(_) do
777 ""
778 end
779
780 defp get_source_content_type(%{"mediaType" => type} = _source) do
781 type
782 end
783
784 defp get_source_content_type(_source) do
785 Utils.get_content_type(nil)
786 end
787 end