aboutsummaryrefslogtreecommitdiff
path: root/lib/pleroma/web/mastodon_api/views/status_view.ex
diff options
context:
space:
mode:
authordcc <dcc@logografos.com>2023-09-02 00:52:52 -0700
committerdcc <dcc@logografos.com>2023-09-02 00:52:52 -0700
commit3a4773c3c2bd0bbef244eb519b07208da9108e49 (patch)
tree973567a6f3abb37bfb0f785b1cad14ed55840ef5 /lib/pleroma/web/mastodon_api/views/status_view.ex
downloadanni-3a4773c3c2bd0bbef244eb519b07208da9108e49.tar.gz
anni-3a4773c3c2bd0bbef244eb519b07208da9108e49.tar.bz2
anni-3a4773c3c2bd0bbef244eb519b07208da9108e49.zip
First
Diffstat (limited to 'lib/pleroma/web/mastodon_api/views/status_view.ex')
-rw-r--r--lib/pleroma/web/mastodon_api/views/status_view.ex757
1 files changed, 757 insertions, 0 deletions
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
new file mode 100644
index 0000000..0a8c98b
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -0,0 +1,757 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.StatusView do
+ use Pleroma.Web, :view
+
+ require Pleroma.Constants
+
+ alias Pleroma.Activity
+ alias Pleroma.HTML
+ alias Pleroma.Maps
+ alias Pleroma.Object
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.UserRelationship
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.MastodonAPI.AccountView
+ alias Pleroma.Web.MastodonAPI.PollView
+ alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.MediaProxy
+ alias Pleroma.Web.PleromaAPI.EmojiReactionController
+
+ import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]
+
+ # This is a naive way to do this, just spawning a process per activity
+ # to fetch the preview. However it should be fine considering
+ # pagination is restricted to 40 activities at a time
+ defp fetch_rich_media_for_activities(activities) do
+ Enum.each(activities, fn activity ->
+ spawn(fn ->
+ Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
+ end)
+ end)
+ end
+
+ # TODO: Add cached version.
+ defp get_replied_to_activities([]), do: %{}
+
+ defp get_replied_to_activities(activities) do
+ activities
+ |> Enum.map(fn
+ %{data: %{"type" => "Create"}} = activity ->
+ object = Object.normalize(activity, fetch: false)
+ object && object.data["inReplyTo"] != "" && object.data["inReplyTo"]
+
+ _ ->
+ nil
+ end)
+ |> Enum.filter(& &1)
+ |> Activity.create_by_object_ap_id_with_object()
+ |> Repo.all()
+ |> Enum.reduce(%{}, fn activity, acc ->
+ object = Object.normalize(activity, fetch: false)
+ if object, do: Map.put(acc, object.data["id"], activity), else: acc
+ end)
+ end
+
+ # DEPRECATED This field seems to be a left-over from the StatusNet era.
+ # If your application uses `pleroma.conversation_id`: this field is deprecated.
+ # It is currently stubbed instead by doing a CRC32 of the context, and
+ # clearing the MSB to avoid overflow exceptions with signed integers on the
+ # different clients using this field (Java/Kotlin code, mostly; see Husky.)
+ # This should be removed in a future version of Pleroma. Pleroma-FE currently
+ # depends on this field, as well.
+ defp get_context_id(%{data: %{"context" => context}}) when is_binary(context) do
+ import Bitwise
+
+ :erlang.crc32(context)
+ |> band(bnot(0x8000_0000))
+ end
+
+ defp get_context_id(_), do: nil
+
+ # Check if the user reblogged this status
+ defp reblogged?(activity, %User{ap_id: ap_id}) do
+ with %Object{data: %{"announcements" => announcements}} when is_list(announcements) <-
+ Object.normalize(activity, fetch: false) do
+ ap_id in announcements
+ else
+ _ -> false
+ end
+ end
+
+ # False if the user is logged out
+ defp reblogged?(_activity, _user), do: false
+
+ def render("index.json", opts) do
+ reading_user = opts[:for]
+
+ # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
+ activities = Enum.filter(opts.activities, & &1)
+
+ # Start fetching rich media before doing anything else, so that later calls to get the cards
+ # only block for timeout in the worst case, as opposed to
+ # length(activities_with_links) * timeout
+ fetch_rich_media_for_activities(activities)
+ replied_to_activities = get_replied_to_activities(activities)
+
+ parent_activities =
+ activities
+ |> Enum.filter(&(&1.data["type"] == "Announce" && &1.data["object"]))
+ |> Enum.map(&Object.normalize(&1, fetch: false).data["id"])
+ |> Activity.create_by_object_ap_id()
+ |> Activity.with_preloaded_object(:left)
+ |> Activity.with_preloaded_bookmark(reading_user)
+ |> Activity.with_set_thread_muted_field(reading_user)
+ |> Repo.all()
+
+ relationships_opt =
+ cond do
+ Map.has_key?(opts, :relationships) ->
+ opts[:relationships]
+
+ is_nil(reading_user) ->
+ UserRelationship.view_relationships_option(nil, [])
+
+ true ->
+ # Note: unresolved users are filtered out
+ actors =
+ (activities ++ parent_activities)
+ |> Enum.map(&CommonAPI.get_user(&1.data["actor"], false))
+ |> Enum.filter(& &1)
+
+ UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes)
+ end
+
+ opts =
+ opts
+ |> Map.put(:replied_to_activities, replied_to_activities)
+ |> Map.put(:parent_activities, parent_activities)
+ |> Map.put(:relationships, relationships_opt)
+
+ safe_render_many(activities, StatusView, "show.json", opts)
+ end
+
+ def render(
+ "show.json",
+ %{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
+ ) do
+ user = CommonAPI.get_user(activity.data["actor"])
+ created_at = Utils.to_masto_date(activity.data["published"])
+ object = Object.normalize(activity, fetch: false)
+
+ reblogged_parent_activity =
+ if opts[:parent_activities] do
+ Activity.Queries.find_by_object_ap_id(
+ opts[:parent_activities],
+ object.data["id"]
+ )
+ else
+ Activity.create_by_object_ap_id(object.data["id"])
+ |> Activity.with_preloaded_bookmark(opts[:for])
+ |> Activity.with_set_thread_muted_field(opts[:for])
+ |> Repo.one()
+ end
+
+ reblog_rendering_opts = Map.put(opts, :activity, reblogged_parent_activity)
+ reblogged = render("show.json", reblog_rendering_opts)
+
+ favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
+
+ bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil
+
+ mentions =
+ activity.recipients
+ |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
+ |> Enum.filter(& &1)
+ |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
+
+ {pinned?, pinned_at} = pin_data(object, user)
+
+ %{
+ id: to_string(activity.id),
+ uri: object.data["id"],
+ url: object.data["id"],
+ account:
+ AccountView.render("show.json", %{
+ user: user,
+ for: opts[:for]
+ }),
+ in_reply_to_id: nil,
+ in_reply_to_account_id: nil,
+ reblog: reblogged,
+ content: reblogged[:content] || "",
+ created_at: created_at,
+ reblogs_count: 0,
+ replies_count: 0,
+ favourites_count: 0,
+ reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
+ favourited: present?(favorited),
+ bookmarked: present?(bookmarked),
+ muted: false,
+ pinned: pinned?,
+ sensitive: false,
+ spoiler_text: "",
+ visibility: get_visibility(activity),
+ media_attachments: reblogged[:media_attachments] || [],
+ mentions: mentions,
+ tags: reblogged[:tags] || [],
+ application: build_application(object.data["generator"]),
+ language: nil,
+ emojis: [],
+ pleroma: %{
+ local: activity.local,
+ pinned_at: pinned_at
+ }
+ }
+ end
+
+ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
+ object = Object.normalize(activity, fetch: false)
+
+ user = CommonAPI.get_user(activity.data["actor"])
+ user_follower_address = user.follower_address
+
+ like_count = object.data["like_count"] || 0
+ announcement_count = object.data["announcement_count"] || 0
+
+ hashtags = Object.hashtags(object)
+ sensitive = object.data["sensitive"] || Enum.member?(hashtags, "nsfw")
+
+ tags = Object.tags(object)
+
+ tag_mentions =
+ tags
+ |> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end)
+ |> Enum.map(fn tag -> tag["href"] end)
+
+ mentions =
+ (object.data["to"] ++ tag_mentions)
+ |> Enum.uniq()
+ |> Enum.map(fn
+ Pleroma.Constants.as_public() -> nil
+ ^user_follower_address -> nil
+ ap_id -> User.get_cached_by_ap_id(ap_id)
+ end)
+ |> Enum.filter(& &1)
+ |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
+
+ favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
+
+ bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
+
+ client_posted_this_activity = opts[:for] && user.id == opts[:for].id
+
+ expires_at =
+ with true <- client_posted_this_activity,
+ %Oban.Job{scheduled_at: scheduled_at} <-
+ Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) do
+ scheduled_at
+ else
+ _ -> nil
+ end
+
+ thread_muted? =
+ cond do
+ is_nil(opts[:for]) -> false
+ is_boolean(activity.thread_muted?) -> activity.thread_muted?
+ true -> CommonAPI.thread_muted?(opts[:for], activity)
+ end
+
+ attachment_data = object.data["attachment"] || []
+ attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
+
+ created_at = Utils.to_masto_date(object.data["published"])
+
+ edited_at =
+ with %{"updated" => updated} <- object.data,
+ date <- Utils.to_masto_date(updated),
+ true <- date != "" do
+ date
+ else
+ _ ->
+ nil
+ end
+
+ reply_to = get_reply_to(activity, opts)
+
+ reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
+
+ history_len =
+ 1 +
+ (Object.Updater.history_for(object.data)
+ |> Map.get("orderedItems")
+ |> length())
+
+ # See render("history.json", ...) for more details
+ # Here the implicit index of the current content is 0
+ chrono_order = history_len - 1
+
+ content =
+ object
+ |> render_content()
+
+ content_html =
+ content
+ |> Activity.HTML.get_cached_scrubbed_html_for_activity(
+ User.html_filter_policy(opts[:for]),
+ activity,
+ "mastoapi:content:#{chrono_order}"
+ )
+
+ content_plaintext =
+ content
+ |> Activity.HTML.get_cached_stripped_html_for_activity(
+ activity,
+ "mastoapi:content:#{chrono_order}"
+ )
+
+ summary = object.data["summary"] || ""
+
+ card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
+
+ url =
+ if user.local do
+ Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
+ else
+ object.data["url"] || object.data["external_url"] || object.data["id"]
+ end
+
+ direct_conversation_id =
+ with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]},
+ {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
+ {_, %User{} = for_user} <- {:for_user, opts[:for]} do
+ Activity.direct_conversation_id(activity, for_user)
+ else
+ {:direct_conversation_id, participation_id} when is_integer(participation_id) ->
+ participation_id
+
+ _e ->
+ nil
+ end
+
+ emoji_reactions =
+ object.data
+ |> Map.get("reactions", [])
+ |> EmojiReactionController.filter_allowed_users(
+ opts[:for],
+ Map.get(opts, :with_muted, false)
+ )
+ |> Stream.map(fn {emoji, users} ->
+ build_emoji_map(emoji, users, opts[:for])
+ end)
+ |> Enum.to_list()
+
+ # Status muted state (would do 1 request per status unless user mutes are preloaded)
+ muted =
+ thread_muted? ||
+ UserRelationship.exists?(
+ get_in(opts, [:relationships, :user_relationships]),
+ :mute,
+ opts[:for],
+ user,
+ fn for_user, user -> User.mutes?(for_user, user) end
+ )
+
+ {pinned?, pinned_at} = pin_data(object, user)
+
+ %{
+ id: to_string(activity.id),
+ uri: object.data["id"],
+ url: url,
+ account:
+ AccountView.render("show.json", %{
+ user: user,
+ for: opts[:for]
+ }),
+ in_reply_to_id: reply_to && to_string(reply_to.id),
+ in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
+ reblog: nil,
+ card: card,
+ content: content_html,
+ text: opts[:with_source] && get_source_text(object.data["source"]),
+ created_at: created_at,
+ edited_at: edited_at,
+ reblogs_count: announcement_count,
+ replies_count: object.data["repliesCount"] || 0,
+ favourites_count: like_count,
+ reblogged: reblogged?(activity, opts[:for]),
+ favourited: present?(favorited),
+ bookmarked: present?(bookmarked),
+ muted: muted,
+ pinned: pinned?,
+ sensitive: sensitive,
+ spoiler_text: summary,
+ visibility: get_visibility(object),
+ media_attachments: attachments,
+ poll: render(PollView, "show.json", object: object, for: opts[:for]),
+ mentions: mentions,
+ tags: build_tags(tags),
+ application: build_application(object.data["generator"]),
+ language: nil,
+ emojis: build_emojis(object.data["emoji"]),
+ pleroma: %{
+ local: activity.local,
+ conversation_id: get_context_id(activity),
+ context: object.data["context"],
+ in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
+ content: %{"text/plain" => content_plaintext},
+ spoiler_text: %{"text/plain" => summary},
+ expires_at: expires_at,
+ direct_conversation_id: direct_conversation_id,
+ thread_muted: thread_muted?,
+ emoji_reactions: emoji_reactions,
+ parent_visible: visible_for_user?(reply_to, opts[:for]),
+ pinned_at: pinned_at
+ }
+ }
+ end
+
+ def render("show.json", _) do
+ nil
+ end
+
+ def render("history.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
+ object = Object.normalize(activity, fetch: false)
+
+ hashtags = Object.hashtags(object)
+
+ user = CommonAPI.get_user(activity.data["actor"])
+
+ past_history =
+ Object.Updater.history_for(object.data)
+ |> Map.get("orderedItems")
+ |> Enum.map(&Map.put(&1, "id", object.data["id"]))
+ |> Enum.map(&%Object{data: &1, id: object.id})
+
+ history =
+ [object | past_history]
+ # Mastodon expects the original to be at the first
+ |> Enum.reverse()
+ |> Enum.with_index()
+ |> Enum.map(fn {object, chrono_order} ->
+ %{
+ # The history is prepended every time there is a new edit.
+ # In chrono_order, the oldest item is always at 0, and so on.
+ # The chrono_order is an invariant kept between edits.
+ chrono_order: chrono_order,
+ object: object
+ }
+ end)
+
+ individual_opts =
+ opts
+ |> Map.put(:as, :item)
+ |> Map.put(:user, user)
+ |> Map.put(:hashtags, hashtags)
+
+ render_many(history, StatusView, "history_item.json", individual_opts)
+ end
+
+ def render(
+ "history_item.json",
+ %{
+ activity: activity,
+ user: user,
+ item: %{object: object, chrono_order: chrono_order},
+ hashtags: hashtags
+ } = opts
+ ) do
+ sensitive = object.data["sensitive"] || Enum.member?(hashtags, "nsfw")
+
+ attachment_data = object.data["attachment"] || []
+ attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
+
+ created_at = Utils.to_masto_date(object.data["updated"] || object.data["published"])
+
+ content =
+ object
+ |> render_content()
+
+ content_html =
+ content
+ |> Activity.HTML.get_cached_scrubbed_html_for_activity(
+ User.html_filter_policy(opts[:for]),
+ activity,
+ "mastoapi:content:#{chrono_order}"
+ )
+
+ summary = object.data["summary"] || ""
+
+ %{
+ account:
+ AccountView.render("show.json", %{
+ user: user,
+ for: opts[:for]
+ }),
+ content: content_html,
+ sensitive: sensitive,
+ spoiler_text: summary,
+ created_at: created_at,
+ media_attachments: attachments,
+ emojis: build_emojis(object.data["emoji"]),
+ poll: render(PollView, "show.json", object: object, for: opts[:for])
+ }
+ end
+
+ def render("source.json", %{activity: %{data: %{"object" => _object}} = activity} = _opts) do
+ object = Object.normalize(activity, fetch: false)
+
+ %{
+ id: activity.id,
+ text: get_source_text(Map.get(object.data, "source", "")),
+ spoiler_text: Map.get(object.data, "summary", ""),
+ content_type: get_source_content_type(object.data["source"])
+ }
+ end
+
+ def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
+ page_url_data = URI.parse(page_url)
+
+ page_url_data =
+ if is_binary(rich_media["url"]) do
+ URI.merge(page_url_data, URI.parse(rich_media["url"]))
+ else
+ page_url_data
+ end
+
+ page_url = page_url_data |> to_string
+
+ image_url_data =
+ if is_binary(rich_media["image"]) do
+ URI.parse(rich_media["image"])
+ else
+ nil
+ end
+
+ image_url = build_image_url(image_url_data, page_url_data)
+
+ %{
+ type: "link",
+ provider_name: page_url_data.host,
+ provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
+ url: page_url,
+ image: image_url |> MediaProxy.url(),
+ title: rich_media["title"] || "",
+ description: rich_media["description"] || "",
+ pleroma: %{
+ opengraph: rich_media
+ }
+ }
+ end
+
+ def render("card.json", _), do: nil
+
+ def render("attachment.json", %{attachment: attachment}) do
+ [attachment_url | _] = attachment["url"]
+ media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
+ href = attachment_url["href"] |> MediaProxy.url()
+ href_preview = attachment_url["href"] |> MediaProxy.preview_url()
+ meta = render("attachment_meta.json", %{attachment: attachment})
+
+ type =
+ cond do
+ String.contains?(media_type, "image") -> "image"
+ String.contains?(media_type, "video") -> "video"
+ String.contains?(media_type, "audio") -> "audio"
+ true -> "unknown"
+ end
+
+ attachment_id =
+ with {_, ap_id} when is_binary(ap_id) <- {:ap_id, attachment["id"]},
+ {_, %Object{data: _object_data, id: object_id}} <-
+ {:object, Object.get_by_ap_id(ap_id)} do
+ to_string(object_id)
+ else
+ _ ->
+ <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
+ to_string(attachment["id"] || hash_id)
+ end
+
+ %{
+ id: attachment_id,
+ url: href,
+ remote_url: href,
+ preview_url: href_preview,
+ text_url: href,
+ type: type,
+ description: attachment["name"],
+ pleroma: %{mime_type: media_type},
+ blurhash: attachment["blurhash"]
+ }
+ |> Maps.put_if_present(:meta, meta)
+ end
+
+ def render("attachment_meta.json", %{
+ attachment: %{"url" => [%{"width" => width, "height" => height} | _]}
+ })
+ when is_integer(width) and is_integer(height) do
+ %{
+ original: %{
+ width: width,
+ height: height,
+ aspect: width / height
+ }
+ }
+ end
+
+ def render("attachment_meta.json", _), do: nil
+
+ def render("context.json", %{activity: activity, activities: activities, user: user}) do
+ %{ancestors: ancestors, descendants: descendants} =
+ activities
+ |> Enum.reverse()
+ |> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
+ |> Map.put_new(:ancestors, [])
+ |> Map.put_new(:descendants, [])
+
+ %{
+ ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
+ descendants: render("index.json", for: user, activities: descendants, as: :activity)
+ }
+ end
+
+ def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
+ object = Object.normalize(activity, fetch: false)
+
+ with nil <- replied_to_activities[object.data["inReplyTo"]] do
+ # If user didn't participate in the thread
+ Activity.get_in_reply_to_activity(activity)
+ end
+ end
+
+ def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
+ object = Object.normalize(activity, fetch: false)
+
+ if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
+ Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
+ else
+ nil
+ end
+ end
+
+ def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do
+ url = object.data["url"] || object.data["id"]
+
+ "<p><a href=\"#{url}\">#{name}</a></p>#{object.data["content"]}"
+ end
+
+ def render_content(object), do: object.data["content"] || ""
+
+ @doc """
+ Builds a dictionary tags.
+
+ ## Examples
+
+ iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
+ [{"name": "fediverse", "url": "/tag/fediverse"},
+ {"name": "nextcloud", "url": "/tag/nextcloud"}]
+
+ """
+ @spec build_tags(list(any())) :: list(map())
+ def build_tags(object_tags) when is_list(object_tags) do
+ object_tags
+ |> Enum.filter(&is_binary/1)
+ |> Enum.map(&%{name: &1, url: "#{Pleroma.Web.Endpoint.url()}/tag/#{URI.encode(&1)}"})
+ end
+
+ def build_tags(_), do: []
+
+ @doc """
+ Builds list emojis.
+
+ Arguments: `nil` or list tuple of name and url.
+
+ Returns list emojis.
+
+ ## Examples
+
+ iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
+ [%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
+
+ """
+ @spec build_emojis(nil | list(tuple())) :: list(map())
+ def build_emojis(nil), do: []
+
+ def build_emojis(emojis) do
+ emojis
+ |> Enum.map(fn {name, url} ->
+ name = HTML.strip_tags(name)
+
+ url =
+ url
+ |> HTML.strip_tags()
+ |> MediaProxy.url()
+
+ %{shortcode: name, url: url, static_url: url, visible_in_picker: false}
+ end)
+ end
+
+ defp present?(nil), do: false
+ defp present?(false), do: false
+ defp present?(_), do: true
+
+ defp pin_data(%Object{data: %{"id" => object_id}}, %User{pinned_objects: pinned_objects}) do
+ if pinned_at = pinned_objects[object_id] do
+ {true, Utils.to_masto_date(pinned_at)}
+ else
+ {false, nil}
+ end
+ end
+
+ defp build_emoji_map(emoji, users, current_user) do
+ %{
+ name: emoji,
+ count: length(users),
+ me: !!(current_user && current_user.ap_id in users)
+ }
+ end
+
+ @spec build_application(map() | nil) :: map() | nil
+ defp build_application(%{"type" => _type, "name" => name, "url" => url}),
+ do: %{name: name, website: url}
+
+ defp build_application(_), do: nil
+
+ # Workaround for Elixir issue #10771
+ # Avoid applying URI.merge unless necessary
+ # TODO: revert to always attempting URI.merge(image_url_data, page_url_data)
+ # when Elixir 1.12 is the minimum supported version
+ @spec build_image_url(struct() | nil, struct()) :: String.t() | nil
+ defp build_image_url(
+ %URI{scheme: image_scheme, host: image_host} = image_url_data,
+ %URI{} = _page_url_data
+ )
+ when not is_nil(image_scheme) and not is_nil(image_host) do
+ image_url_data |> to_string
+ end
+
+ defp build_image_url(%URI{} = image_url_data, %URI{} = page_url_data) do
+ URI.merge(page_url_data, image_url_data) |> to_string
+ end
+
+ defp build_image_url(_, _), do: nil
+
+ defp get_source_text(%{"content" => content} = _source) do
+ content
+ end
+
+ defp get_source_text(source) when is_binary(source) do
+ source
+ end
+
+ defp get_source_text(_) do
+ ""
+ end
+
+ defp get_source_content_type(%{"mediaType" => type} = _source) do
+ type
+ end
+
+ defp get_source_content_type(_source) do
+ Utils.get_content_type(nil)
+ end
+end