Merge remote-tracking branch 'upstream/develop' into attachment-meta
authorAlex Gleason <alex@alexgleason.me>
Thu, 13 May 2021 01:10:52 +0000 (20:10 -0500)
committerAlex Gleason <alex@alexgleason.me>
Thu, 13 May 2021 01:10:52 +0000 (20:10 -0500)
1  2 
lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
lib/pleroma/web/activity_pub/transmogrifier.ex
lib/pleroma/web/mastodon_api/views/status_view.ex
test/pleroma/web/mastodon_api/views/status_view_test.exs

index a99b40adce85398f0fc06a06d85d8a125eb77ffb,4a0d1473de8e755f9def08c247641f7a21036e6e..bba2f5eb072e0f07170ed9f63647b064ea677698
@@@ -6,7 -6,6 +6,6 @@@ defmodule Pleroma.Web.ActivityPub.Objec
    use Ecto.Schema
  
    alias Pleroma.EctoType.ActivityPub.ObjectValidators
-   alias Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator
  
    import Ecto.Changeset
  
@@@ -21,8 -20,6 +20,8 @@@
        field(:type, :string)
        field(:href, ObjectValidators.Uri)
        field(:mediaType, :string, default: "application/octet-stream")
 +      field(:width, :integer)
 +      field(:height, :integer)
      end
    end
  
@@@ -54,7 -51,7 +53,7 @@@
      data = fix_media_type(data)
  
      struct
 -    |> cast(data, [:type, :href, :mediaType])
 +    |> cast(data, [:type, :href, :mediaType, :width, :height])
      |> validate_inclusion(:type, ["Link"])
      |> validate_required([:type, :href, :mediaType])
    end
@@@ -92,7 -89,7 +91,7 @@@
      end
    end
  
-   def validate_data(cng) do
+   defp validate_data(cng) do
      cng
      |> validate_inclusion(:type, ~w[Document Audio Image Video])
      |> validate_required([:mediaType, :url, :type])
index acb4f4b3ead7232ab3a44c5e9c67acd423a8210a,d27d0bed4e11f096b52e1afaa404ddffa4b24305..d1a0867e261494e83d8e1cd92b46611a0376930f
@@@ -32,18 -32,17 +32,17 @@@ defmodule Pleroma.Web.ActivityPub.Trans
    """
    def fix_object(object, options \\ []) do
      object
-     |> strip_internal_fields
-     |> fix_actor
-     |> fix_url
-     |> fix_attachments
-     |> fix_context
+     |> strip_internal_fields()
+     |> fix_actor()
+     |> fix_url()
+     |> fix_attachments()
+     |> fix_context()
      |> fix_in_reply_to(options)
-     |> fix_emoji
-     |> fix_tag
-     |> set_sensitive
-     |> fix_content_map
-     |> fix_addressing
-     |> fix_summary
+     |> fix_emoji()
+     |> fix_tag()
+     |> fix_content_map()
+     |> fix_addressing()
+     |> fix_summary()
      |> fix_type(options)
    end
  
                "type" => Map.get(url || %{}, "type", "Link")
              }
              |> Maps.put_if_present("mediaType", media_type)
 +            |> Maps.put_if_present("width", (url || %{})["width"] || data["width"])
 +            |> Maps.put_if_present("height", (url || %{})["height"] || data["height"])
  
            %{
              "url" => [attachment_url],
      tags =
        tag
        |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
-       |> Enum.map(fn %{"name" => name} ->
-         name
-         |> String.slice(1..-1)
-         |> String.downcase()
+       |> Enum.map(fn
+         %{"name" => "#" <> hashtag} -> String.downcase(hashtag)
+         %{"name" => hashtag} -> String.downcase(hashtag)
        end)
  
      Map.put(object, "tag", tag ++ tags)
    end
  
    def handle_incoming(%{"type" => type} = data, _options)
-       when type in ~w{Like EmojiReact Announce} do
+       when type in ~w{Like EmojiReact Announce Add Remove} do
      with :ok <- ObjectValidator.fetch_actor_and_object(data),
           {:ok, activity, _meta} <-
             Pipeline.common_pipeline(data, local: false) do
             Pipeline.common_pipeline(data, local: false) do
        {:ok, activity}
      else
-       {:error, {:validate_object, _}} = e ->
+       {:error, {:validate, _}} = e ->
          # Check if we have a create activity for this
          with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),
               %Activity{data: %{"actor" => actor}} <-
    # Prepares the object of an outgoing create activity.
    def prepare_object(object) do
      object
-     |> set_sensitive
      |> add_hashtags
      |> add_mention_tags
      |> add_emoji_tags
      Map.put(object, "conversation", object["context"])
    end
  
-   def set_sensitive(%{"sensitive" => _} = object) do
-     object
-   end
-   def set_sensitive(object) do
-     tags = object["tag"] || []
-     Map.put(object, "sensitive", "nsfw" in tags)
-   end
    def set_type(%{"type" => "Answer"} = object) do
      Map.put(object, "type", "Note")
    end
        object
        |> Map.get("attachment", [])
        |> Enum.map(fn data ->
 -        [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
 +        [%{"mediaType" => media_type, "href" => href} = url | _] = data["url"]
  
          %{
            "url" => href,
            "name" => data["name"],
            "type" => "Document"
          }
 +        |> Maps.put_if_present("width", url["width"])
 +        |> Maps.put_if_present("height", url["height"])
        end)
  
      Map.put(object, "attachment", attachments)
      with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
           {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
           {:ok, user} <- update_user(user, data) do
+       {:ok, _pid} = Task.start(fn -> ActivityPub.pinned_fetch_task(user) end)
        TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
        {:ok, user}
      else
index 7f318e81b03e3010c61fbd67ccf42cc3358b336c,814b3d142ba907ad01ce52947254d0a9afab9b66..8fdf30883777f2271b8680f2fec1867e53aafa81
@@@ -9,7 -9,6 +9,7 @@@ defmodule Pleroma.Web.MastodonAPI.Statu
  
    alias Pleroma.Activity
    alias Pleroma.HTML
 +  alias Pleroma.Maps
    alias Pleroma.Object
    alias Pleroma.Repo
    alias Pleroma.User
        ) do
      user = CommonAPI.get_user(activity.data["actor"])
      created_at = Utils.to_masto_date(activity.data["published"])
-     activity_object = Object.normalize(activity, fetch: false)
+     object = Object.normalize(activity, fetch: false)
  
      reblogged_parent_activity =
        if opts[:parent_activities] do
          Activity.Queries.find_by_object_ap_id(
            opts[:parent_activities],
-           activity_object.data["id"]
+           object.data["id"]
          )
        else
-         Activity.create_by_object_ap_id(activity_object.data["id"])
+         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()
      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 (activity_object.data["likes"] || [])
+     favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
  
      bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil
  
        |> 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: activity_object.data["id"],
-       url: activity_object.data["id"],
+       uri: object.data["id"],
+       url: object.data["id"],
        account:
          AccountView.render("show.json", %{
            user: user,
        favourited: present?(favorited),
        bookmarked: present?(bookmarked),
        muted: false,
-       pinned: pinned?(activity, user),
+       pinned: pinned?,
        sensitive: false,
        spoiler_text: "",
        visibility: get_visibility(activity),
        media_attachments: reblogged[:media_attachments] || [],
        mentions: mentions,
        tags: reblogged[:tags] || [],
-       application: build_application(activity_object.data["generator"]),
+       application: build_application(object.data["generator"]),
        language: nil,
        emojis: [],
        pleroma: %{
-         local: activity.local
+         local: activity.local,
+         pinned_at: pinned_at
        }
      }
    end
      like_count = object.data["like_count"] || 0
      announcement_count = object.data["announcement_count"] || 0
  
-     tags = object.data["tag"] || []
-     sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw")
+     hashtags = Object.hashtags(object)
+     sensitive = object.data["sensitive"] || Enum.member?(hashtags, "nsfw")
+     tags = Object.tags(object)
  
      tag_mentions =
        tags
            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"],
        favourited: present?(favorited),
        bookmarked: present?(bookmarked),
        muted: muted,
-       pinned: pinned?(activity, user),
+       pinned: pinned?,
        sensitive: sensitive,
        spoiler_text: summary,
        visibility: get_visibility(object),
          direct_conversation_id: direct_conversation_id,
          thread_muted: thread_muted?,
          emoji_reactions: emoji_reactions,
-         parent_visible: visible_for_user?(reply_to, opts[:for])
+         parent_visible: visible_for_user?(reply_to, opts[:for]),
+         pinned_at: pinned_at
        }
      }
    end
  
      page_url = page_url_data |> to_string
  
-     image_url =
+     image_url_data =
        if is_binary(rich_media["image"]) do
-         URI.merge(page_url_data, URI.parse(rich_media["image"]))
-         |> to_string
+         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,
      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
        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
    defp present?(false), do: false
    defp present?(_), do: true
  
-   defp pinned?(%Activity{id: id}, %User{pinned_activities: pinned_activities}),
-     do: id in pinned_activities
+   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
      %{
    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(%{"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
  end
index e6c37e7825d4b2b3e85dfffbc94d230cdaace667,fbea25079b8bd780594a27311c0110c99cbfc68e..9dfdf8bf064da99035d867dc7f2a022cb84fc0f9
@@@ -262,8 -262,8 +262,8 @@@ defmodule Pleroma.Web.MastodonAPI.Statu
        mentions: [],
        tags: [
          %{
-           name: "#{object_data["tag"]}",
-           url: "http://localhost:4001/tag/#{object_data["tag"]}"
+           name: "#{hd(object_data["tag"])}",
+           url: "http://localhost:4001/tag/#{hd(object_data["tag"])}"
          }
        ],
        application: nil,
          direct_conversation_id: nil,
          thread_muted: false,
          emoji_reactions: [],
-         parent_visible: false
+         parent_visible: false,
+         pinned_at: nil
        }
      }
  
        "url" => [
          %{
            "mediaType" => "image/png",
 -          "href" => "someurl"
 +          "href" => "someurl",
 +          "width" => 200,
 +          "height" => 100
          }
        ],
        "blurhash" => "UJJ8X[xYW,%Jtq%NNFbXB5j]IVM|9GV=WHRn",
        text_url: "someurl",
        description: nil,
        pleroma: %{mime_type: "image/png"},
 +      meta: %{original: %{width: 200, height: 100, aspect: 2}},
        blurhash: "UJJ8X[xYW,%Jtq%NNFbXB5j]IVM|9GV=WHRn"
      }