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