Post editing (#202)
[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 use 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 object = Object.normalize(activity, fetch: false)
213
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
362 %{
363 id: to_string(activity.id),
364 uri: object.data["id"],
365 url: url,
366 account:
367 AccountView.render("show.json", %{
368 user: user,
369 for: opts[:for]
370 }),
371 in_reply_to_id: reply_to && to_string(reply_to.id),
372 in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
373 reblog: nil,
374 card: card,
375 content: content_html,
376 text: opts[:with_source] && get_source_text(object.data["source"]),
377 created_at: created_at,
378 edited_at: edited_at,
379 reblogs_count: announcement_count,
380 replies_count: object.data["repliesCount"] || 0,
381 favourites_count: like_count,
382 reblogged: reblogged?(activity, opts[:for]),
383 favourited: present?(favorited),
384 bookmarked: present?(bookmarked),
385 muted: muted,
386 pinned: pinned?,
387 sensitive: sensitive,
388 spoiler_text: summary,
389 visibility: get_visibility(object),
390 media_attachments: attachments,
391 poll: render(PollView, "show.json", object: object, for: opts[:for]),
392 mentions: mentions,
393 tags: build_tags(tags),
394 application: build_application(object.data["generator"]),
395 language: nil,
396 emojis: build_emojis(object.data["emoji"]),
397 quote_id: if(quote, do: quote.id, else: nil),
398 quote: maybe_render_quote(quote, opts),
399 emoji_reactions: emoji_reactions,
400 pleroma: %{
401 local: activity.local,
402 conversation_id: get_context_id(activity),
403 context: object.data["context"],
404 in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
405 content: %{"text/plain" => content_plaintext},
406 spoiler_text: %{"text/plain" => summary},
407 expires_at: expires_at,
408 direct_conversation_id: direct_conversation_id,
409 thread_muted: thread_muted?,
410 emoji_reactions: emoji_reactions,
411 parent_visible: visible_for_user?(reply_to, opts[:for]),
412 pinned_at: pinned_at
413 },
414 akkoma: %{
415 source: object.data["source"]
416 }
417 }
418 end
419
420 def render("show.json", _) do
421 nil
422 end
423
424 def render("history.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
425 object = Object.normalize(activity, fetch: false)
426
427 hashtags = Object.hashtags(object)
428
429 user = CommonAPI.get_user(activity.data["actor"])
430
431 past_history =
432 Object.Updater.history_for(object.data)
433 |> Map.get("orderedItems")
434 |> Enum.map(&Map.put(&1, "id", object.data["id"]))
435 |> Enum.map(&%Object{data: &1, id: object.id})
436
437 history =
438 [object | past_history]
439 # Mastodon expects the original to be at the first
440 |> Enum.reverse()
441 |> Enum.with_index()
442 |> Enum.map(fn {object, chrono_order} ->
443 %{
444 # The history is prepended every time there is a new edit.
445 # In chrono_order, the oldest item is always at 0, and so on.
446 # The chrono_order is an invariant kept between edits.
447 chrono_order: chrono_order,
448 object: object
449 }
450 end)
451
452 individual_opts =
453 opts
454 |> Map.put(:as, :item)
455 |> Map.put(:user, user)
456 |> Map.put(:hashtags, hashtags)
457
458 render_many(history, StatusView, "history_item.json", individual_opts)
459 end
460
461 def render(
462 "history_item.json",
463 %{
464 activity: activity,
465 user: user,
466 item: %{object: object, chrono_order: chrono_order},
467 hashtags: hashtags
468 } = opts
469 ) do
470 sensitive = object.data["sensitive"] || Enum.member?(hashtags, "nsfw")
471
472 attachment_data = object.data["attachment"] || []
473 attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
474
475 created_at = Utils.to_masto_date(object.data["updated"] || object.data["published"])
476
477 content =
478 object
479 |> render_content()
480
481 content_html =
482 content
483 |> Activity.HTML.get_cached_scrubbed_html_for_activity(
484 User.html_filter_policy(opts[:for]),
485 activity,
486 "mastoapi:content:#{chrono_order}"
487 )
488
489 summary = object.data["summary"] || ""
490
491 %{
492 account:
493 AccountView.render("show.json", %{
494 user: user,
495 for: opts[:for]
496 }),
497 content: content_html,
498 sensitive: sensitive,
499 spoiler_text: summary,
500 created_at: created_at,
501 media_attachments: attachments,
502 emojis: build_emojis(object.data["emoji"]),
503 poll: render(PollView, "show.json", object: object, for: opts[:for])
504 }
505 end
506
507 def render("source.json", %{activity: %{data: %{"object" => _object}} = activity} = _opts) do
508 object = Object.normalize(activity, fetch: false)
509
510 %{
511 id: activity.id,
512 text: get_source_text(Map.get(object.data, "source", "")),
513 spoiler_text: Map.get(object.data, "summary", ""),
514 content_type: get_source_content_type(object.data["source"])
515 }
516 end
517
518 def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
519 page_url_data = URI.parse(page_url)
520
521 page_url_data =
522 if is_binary(rich_media["url"]) do
523 URI.merge(page_url_data, URI.parse(rich_media["url"]))
524 else
525 page_url_data
526 end
527
528 page_url = page_url_data |> to_string
529
530 image_url_data =
531 if is_binary(rich_media["image"]) do
532 URI.parse(rich_media["image"])
533 else
534 nil
535 end
536
537 image_url = build_image_url(image_url_data, page_url_data)
538
539 %{
540 type: "link",
541 provider_name: page_url_data.host,
542 provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
543 url: page_url,
544 image: image_url |> MediaProxy.url(),
545 title: rich_media["title"] || "",
546 description: rich_media["description"] || "",
547 pleroma: %{
548 opengraph: rich_media
549 }
550 }
551 end
552
553 def render("card.json", _), do: nil
554
555 def render("attachment.json", %{attachment: attachment}) do
556 [attachment_url | _] = attachment["url"]
557 media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
558 href = attachment_url["href"] |> MediaProxy.url()
559 href_preview = attachment_url["href"] |> MediaProxy.preview_url()
560 meta = render("attachment_meta.json", %{attachment: attachment})
561
562 type =
563 cond do
564 String.contains?(media_type, "image") -> "image"
565 String.contains?(media_type, "video") -> "video"
566 String.contains?(media_type, "audio") -> "audio"
567 true -> "unknown"
568 end
569
570 attachment_id =
571 with {_, ap_id} when is_binary(ap_id) <- {:ap_id, attachment["id"]},
572 {_, %Object{data: _object_data, id: object_id}} <-
573 {:object, Object.get_by_ap_id(ap_id)} do
574 to_string(object_id)
575 else
576 _ ->
577 <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
578 to_string(attachment["id"] || hash_id)
579 end
580
581 %{
582 id: attachment_id,
583 url: href,
584 remote_url: href,
585 preview_url: href_preview,
586 text_url: href,
587 type: type,
588 description: attachment["name"],
589 pleroma: %{mime_type: media_type},
590 blurhash: attachment["blurhash"]
591 }
592 |> Maps.put_if_present(:meta, meta)
593 end
594
595 def render("attachment_meta.json", %{
596 attachment: %{"url" => [%{"width" => width, "height" => height} | _]}
597 })
598 when is_integer(width) and is_integer(height) do
599 %{
600 original: %{
601 width: width,
602 height: height,
603 aspect: width / height
604 }
605 }
606 end
607
608 def render("attachment_meta.json", _), do: nil
609
610 def render("context.json", %{activity: activity, activities: activities, user: user}) do
611 %{ancestors: ancestors, descendants: descendants} =
612 activities
613 |> Enum.reverse()
614 |> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
615 |> Map.put_new(:ancestors, [])
616 |> Map.put_new(:descendants, [])
617
618 %{
619 ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
620 descendants: render("index.json", for: user, activities: descendants, as: :activity)
621 }
622 end
623
624 def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
625 object = Object.normalize(activity, fetch: false)
626
627 with nil <- replied_to_activities[object.data["inReplyTo"]] do
628 # If user didn't participate in the thread
629 Activity.get_in_reply_to_activity(activity)
630 end
631 end
632
633 def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
634 object = Object.normalize(activity, fetch: false)
635
636 if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
637 Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
638 else
639 nil
640 end
641 end
642
643 def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do
644 url = object.data["url"] || object.data["id"]
645
646 "<p><a href=\"#{url}\">#{name}</a></p>#{object.data["content"]}"
647 end
648
649 def render_content(object), do: object.data["content"] || ""
650
651 @doc """
652 Builds a dictionary tags.
653
654 ## Examples
655
656 iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
657 [{"name": "fediverse", "url": "/tag/fediverse"},
658 {"name": "nextcloud", "url": "/tag/nextcloud"}]
659
660 """
661 @spec build_tags(list(any())) :: list(map())
662 def build_tags(object_tags) when is_list(object_tags) do
663 object_tags
664 |> Enum.filter(&is_binary/1)
665 |> Enum.map(&%{name: &1, url: "#{Pleroma.Web.Endpoint.url()}/tag/#{URI.encode(&1)}"})
666 end
667
668 def build_tags(_), do: []
669
670 @doc """
671 Builds list emojis.
672
673 Arguments: `nil` or list tuple of name and url.
674
675 Returns list emojis.
676
677 ## Examples
678
679 iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
680 [%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
681
682 """
683 @spec build_emojis(nil | list(tuple())) :: list(map())
684 def build_emojis(nil), do: []
685
686 def build_emojis(emojis) do
687 emojis
688 |> Enum.map(fn {name, url} ->
689 name = HTML.strip_tags(name)
690
691 url =
692 url
693 |> HTML.strip_tags()
694 |> MediaProxy.url()
695
696 %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
697 end)
698 end
699
700 defp present?(nil), do: false
701 defp present?(false), do: false
702 defp present?(_), do: true
703
704 defp pin_data(%Object{data: %{"id" => object_id}}, %User{pinned_objects: pinned_objects}) do
705 if pinned_at = pinned_objects[object_id] do
706 {true, Utils.to_masto_date(pinned_at)}
707 else
708 {false, nil}
709 end
710 end
711
712 defp build_emoji_map(emoji, users, url, current_user) do
713 %{
714 name: Pleroma.Web.PleromaAPI.EmojiReactionView.emoji_name(emoji, url),
715 count: length(users),
716 url: MediaProxy.url(url),
717 me: !!(current_user && current_user.ap_id in users),
718 account_ids: Enum.map(users, fn user -> User.get_cached_by_ap_id(user).id end)
719 }
720 end
721
722 @spec build_application(map() | nil) :: map() | nil
723 defp build_application(%{"type" => _type, "name" => name, "url" => url}),
724 do: %{name: name, website: url}
725
726 defp build_application(_), do: nil
727
728 # Workaround for Elixir issue #10771
729 # Avoid applying URI.merge unless necessary
730 # TODO: revert to always attempting URI.merge(image_url_data, page_url_data)
731 # when Elixir 1.12 is the minimum supported version
732 @spec build_image_url(struct() | nil, struct()) :: String.t() | nil
733 defp build_image_url(
734 %URI{scheme: image_scheme, host: image_host} = image_url_data,
735 %URI{} = _page_url_data
736 )
737 when not is_nil(image_scheme) and not is_nil(image_host) do
738 image_url_data |> to_string
739 end
740
741 defp build_image_url(%URI{} = image_url_data, %URI{} = page_url_data) do
742 URI.merge(page_url_data, image_url_data) |> to_string
743 end
744
745 defp build_image_url(_, _), do: nil
746
747 defp maybe_render_quote(nil, _), do: nil
748
749 defp maybe_render_quote(quote, opts) do
750 with %User{} = quoted_user <- User.get_cached_by_ap_id(quote.actor),
751 false <- Map.get(opts, :do_not_recurse, false),
752 true <- visible_for_user?(quote, opts[:for]),
753 false <- User.blocks?(opts[:for], quoted_user),
754 false <- User.mutes?(opts[:for], quoted_user) do
755 opts =
756 opts
757 |> Map.put(:activity, quote)
758 |> Map.put(:do_not_recurse, true)
759
760 render("show.json", opts)
761 else
762 _ -> nil
763 end
764 end
765
766 defp get_source_text(%{"content" => content} = _source) do
767 content
768 end
769
770 defp get_source_text(source) when is_binary(source) do
771 source
772 end
773
774 defp get_source_text(_) do
775 ""
776 end
777
778 defp get_source_content_type(%{"mediaType" => type} = _source) do
779 type
780 end
781
782 defp get_source_content_type(_source) do
783 Utils.get_content_type(nil)
784 end
785 end