Merge remote-tracking branch 'pleroma/develop' into features/poll-validation
authorHaelwenn (lanodan) Monnier <contact@hacktivis.me>
Fri, 31 Jul 2020 11:57:21 +0000 (13:57 +0200)
committerHaelwenn (lanodan) Monnier <contact@hacktivis.me>
Fri, 31 Jul 2020 11:57:21 +0000 (13:57 +0200)
1  2 
lib/pleroma/object/fetcher.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/object_validator.ex
lib/pleroma/web/activity_pub/transmogrifier.ex
test/web/activity_pub/transmogrifier_test.exs

index 3956bb7272d4513afbfa4cc652c235c44c386b87,e74c87269f2d5b5bc47e039818dae47a3c93b5c8..3ff25118d5160c82b3f4922b29593f8f8eca5f28
@@@ -9,7 -9,6 +9,7 @@@ defmodule Pleroma.Object.Fetcher d
    alias Pleroma.Repo
    alias Pleroma.Signature
    alias Pleroma.Web.ActivityPub.InternalFetchActor
 +  alias Pleroma.Web.ActivityPub.ObjectValidator
    alias Pleroma.Web.ActivityPub.Transmogrifier
    alias Pleroma.Web.Federator
  
      Ecto.Changeset.put_change(changeset, :updated_at, updated_at)
    end
  
 -  defp maybe_reinject_internal_fields(data, %{data: %{} = old_data}) do
 +  defp maybe_reinject_internal_fields(%{data: %{} = old_data}, new_data) do
      internal_fields = Map.take(old_data, Pleroma.Constants.object_internal_fields())
  
 -    Map.merge(data, internal_fields)
 +    Map.merge(new_data, internal_fields)
    end
  
 -  defp maybe_reinject_internal_fields(data, _), do: data
 +  defp maybe_reinject_internal_fields(_, new_data), do: new_data
  
    @spec reinject_object(struct(), map()) :: {:ok, Object.t()} | {:error, any()}
 -  defp reinject_object(struct, data) do
 -    Logger.debug("Reinjecting object #{data["id"]}")
 +  defp reinject_object(%Object{data: %{"type" => "Question"}} = object, new_data) do
 +    Logger.debug("Reinjecting object #{new_data["id"]}")
  
 -    with data <- Transmogrifier.fix_object(data),
 -         data <- maybe_reinject_internal_fields(data, struct),
 -         changeset <- Object.change(struct, %{data: data}),
 +    with new_data <- Transmogrifier.fix_object(new_data),
 +         data <- maybe_reinject_internal_fields(object, new_data),
 +         {:ok, data, _} <- ObjectValidator.validate(data, %{}),
 +         changeset <- Object.change(object, %{data: data}),
 +         changeset <- touch_changeset(changeset),
 +         {:ok, object} <- Repo.insert_or_update(changeset),
 +         {:ok, object} <- Object.set_cache(object) do
 +      {:ok, object}
 +    else
 +      e ->
 +        Logger.error("Error while processing object: #{inspect(e)}")
 +        {:error, e}
 +    end
 +  end
 +
 +  defp reinject_object(%Object{} = object, new_data) do
 +    Logger.debug("Reinjecting object #{new_data["id"]}")
 +
 +    with new_data <- Transmogrifier.fix_object(new_data),
 +         data <- maybe_reinject_internal_fields(object, new_data),
 +         changeset <- Object.change(object, %{data: data}),
           changeset <- touch_changeset(changeset),
           {:ok, object} <- Repo.insert_or_update(changeset),
           {:ok, object} <- Object.set_cache(object) do
@@@ -70,8 -51,8 +70,8 @@@
  
    def refetch_object(%Object{data: %{"id" => id}} = object) do
      with {:local, false} <- {:local, Object.local?(object)},
 -         {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
 -         {:ok, object} <- reinject_object(object, data) do
 +         {:ok, new_data} <- fetch_and_contain_remote_object_from_id(id),
 +         {:ok, object} <- reinject_object(object, new_data) do
        {:ok, object}
      else
        {:local, true} -> {:ok, object}
        {:error, "Object has been deleted"} ->
          nil
  
+       {:reject, reason} ->
+         Logger.info("Rejected #{id} while fetching: #{inspect(reason)}")
+         nil
        e ->
          Logger.error("Error while fetching #{id}: #{inspect(e)}")
          nil
index 9d13a06c439cbd1a8b196be65b7c90327bcb52d5,a4db1d87c42334432396e10ccc2cf6d41176a363..fe62673dce2515fa9f646dd1a7d8d5a91c2e61f4
@@@ -66,7 -66,7 +66,7 @@@ defmodule Pleroma.Web.ActivityPub.Activ
  
    defp check_remote_limit(_), do: true
  
 -  defp increase_note_count_if_public(actor, object) do
 +  def increase_note_count_if_public(actor, object) do
      if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
    end
  
  
    defp increase_replies_count_if_reply(_create_data), do: :noop
  
 -  defp increase_poll_votes_if_vote(%{
 -         "object" => %{"inReplyTo" => reply_ap_id, "name" => name},
 -         "type" => "Create",
 -         "actor" => actor
 -       }) do
 -    Object.increase_vote_count(reply_ap_id, name, actor)
 -  end
 -
 -  defp increase_poll_votes_if_vote(_create_data), do: :noop
 -
 -  @object_types ["ChatMessage"]
 +  @object_types ["ChatMessage", "Question", "Answer"]
    @spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
    def persist(%{"type" => type} = object, meta) when type in @object_types do
      with {:ok, object} <- Object.create(object) do
      with {:ok, activity} <- insert(create_data, local, fake),
           {:fake, false, activity} <- {:fake, fake, activity},
           _ <- increase_replies_count_if_reply(create_data),
 -         _ <- increase_poll_votes_if_vote(create_data),
           {:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
           {:ok, _actor} <- increase_note_count_if_public(actor, activity),
           _ <- notify_and_stream(activity),
          Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
          {:error, e}
  
+       {:error, {:reject, reason} = e} ->
+         Logger.info("Rejected user #{ap_id}: #{inspect(reason)}")
+         {:error, e}
        {:error, e} ->
          Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
          {:error, e}
index a24aaf00c60fd768a6afefcaf13826555c78ecc1,0dcc7be4dbd778a1eebace51c8848e4f86cb76cd..e1114a44d952f30d23d46d9273d4a57194e03012
@@@ -9,20 -9,18 +9,21 @@@ defmodule Pleroma.Web.ActivityPub.Objec
    the system.
    """
  
+   alias Pleroma.Activity
    alias Pleroma.EctoType.ActivityPub.ObjectValidators
    alias Pleroma.Object
    alias Pleroma.User
    alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator
 +  alias Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator
    alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator
    alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
    alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
 +  alias Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator
    alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
    alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
    alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator
    alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
 +  alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator
    alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
    alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator
  
             |> UndoValidator.cast_and_validate()
             |> Ecto.Changeset.apply_action(:insert) do
        object = stringify_keys(object)
+       undone_object = Activity.get_by_ap_id(object["object"])
+       meta =
+         meta
+         |> Keyword.put(:object_data, undone_object.data)
        {:ok, object, meta}
      end
    end
      end
    end
  
 +  def validate(%{"type" => "Question"} = object, meta) do
 +    with {:ok, object} <-
 +           object
 +           |> QuestionValidator.cast_and_validate()
 +           |> Ecto.Changeset.apply_action(:insert) do
 +      object = stringify_keys(object)
 +      {:ok, object, meta}
 +    end
 +  end
 +
 +  def validate(%{"type" => "Answer"} = object, meta) do
 +    with {:ok, object} <-
 +           object
 +           |> AnswerValidator.cast_and_validate()
 +           |> Ecto.Changeset.apply_action(:insert) do
 +      object = stringify_keys(object)
 +      {:ok, object, meta}
 +    end
 +  end
 +
    def validate(%{"type" => "EmojiReact"} = object, meta) do
      with {:ok, object} <-
             object
             |> EmojiReactValidator.cast_and_validate()
             |> Ecto.Changeset.apply_action(:insert) do
 -      object = stringify_keys(object |> Map.from_struct())
 +      object = stringify_keys(object)
        {:ok, object, meta}
      end
    end
  
 -  def validate(%{"type" => "Create", "object" => object} = create_activity, meta) do
 +  def validate(
 +        %{"type" => "Create", "object" => %{"type" => "ChatMessage"} = object} = create_activity,
 +        meta
 +      ) do
      with {:ok, object_data} <- cast_and_apply(object),
           meta = Keyword.put(meta, :object_data, object_data |> stringify_keys),
           {:ok, create_activity} <-
      end
    end
  
 +  def validate(
 +        %{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity,
 +        meta
 +      )
 +      when objtype in ["Question", "Answer"] do
 +    with {:ok, object_data} <- cast_and_apply(object),
 +         meta = Keyword.put(meta, :object_data, object_data |> stringify_keys),
 +         {:ok, create_activity} <-
 +           create_activity
 +           |> CreateGenericValidator.cast_and_validate(meta)
 +           |> Ecto.Changeset.apply_action(:insert) do
 +      create_activity = stringify_keys(create_activity)
 +      {:ok, create_activity, meta}
 +    end
 +  end
 +
    def validate(%{"type" => "Announce"} = object, meta) do
      with {:ok, object} <-
             object
             |> AnnounceValidator.cast_and_validate()
             |> Ecto.Changeset.apply_action(:insert) do
 -      object = stringify_keys(object |> Map.from_struct())
 +      object = stringify_keys(object)
        {:ok, object, meta}
      end
    end
      ChatMessageValidator.cast_and_apply(object)
    end
  
 +  def cast_and_apply(%{"type" => "Question"} = object) do
 +    QuestionValidator.cast_and_apply(object)
 +  end
 +
 +  def cast_and_apply(%{"type" => "Answer"} = object) do
 +    AnswerValidator.cast_and_apply(object)
 +  end
 +
    def cast_and_apply(o), do: {:error, {:validator_not_set, o}}
  
 +  # is_struct/1 isn't present in Elixir 1.8.x
    def stringify_keys(%{__struct__: _} = object) do
      object
      |> Map.from_struct()
index edabe11308ee49b35a2823289dd5be96d4081f3c,35aa05eb5a1025bfd725dd5d11f7acd739d19c5f..f85a266796f89ad2482602040109a2191fe83172
@@@ -157,12 -157,7 +157,12 @@@ defmodule Pleroma.Web.ActivityPub.Trans
    end
  
    def fix_actor(%{"attributedTo" => actor} = object) do
 -    Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
 +    actor = Containment.get_actor(%{"actor" => actor})
 +
 +    # TODO: Remove actor field for Objects
 +    object
 +    |> Map.put("actor", actor)
 +    |> Map.put("attributedTo", actor)
    end
  
    def fix_in_reply_to(object, options \\ [])
          |> Map.drop(["conversation"])
        else
          e ->
-           Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
+           Logger.warn("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
            object
        end
      else
  
          if href do
            attachment_url =
 -            %{"href" => href}
 +            %{
 +              "href" => href,
 +              "type" => Map.get(url || %{}, "type", "Link")
 +            }
              |> Maps.put_if_present("mediaType", media_type)
 -            |> Maps.put_if_present("type", Map.get(url || %{}, "type"))
  
 -          %{"url" => [attachment_url]}
 +          %{
 +            "url" => [attachment_url],
 +            "type" => data["type"] || "Document"
 +          }
            |> Maps.put_if_present("mediaType", media_type)
 -          |> Maps.put_if_present("type", data["type"])
            |> Maps.put_if_present("name", data["name"])
          else
            nil
      end)
    end
  
 +  # Compatibility wrapper for Mastodon votes
 +  defp handle_create(%{"object" => %{"type" => "Answer"}} = data, _user) do
 +    handle_incoming(data)
 +  end
 +
 +  defp handle_create(%{"object" => object} = data, user) do
 +    %{
 +      to: data["to"],
 +      object: object,
 +      actor: user,
 +      context: object["context"],
 +      local: false,
 +      published: data["published"],
 +      additional:
 +        Map.take(data, [
 +          "cc",
 +          "directMessage",
 +          "id"
 +        ])
 +    }
 +    |> ActivityPub.create()
 +  end
 +
    def handle_incoming(data, options \\ [])
  
    # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
          %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
          options
        )
 -      when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do
 +      when objtype in ["Article", "Event", "Note", "Video", "Page", "Audio"] do
      actor = Containment.get_actor(data)
  
      with nil <- Activity.get_create_by_object_ap_id(object["id"]),
 -         {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor),
 -         data <- Map.put(data, "actor", actor) |> fix_addressing() do
 -      object = fix_object(object, options)
 -
 -      params = %{
 -        to: data["to"],
 -        object: object,
 -        actor: user,
 -        context: object["context"],
 -        local: false,
 -        published: data["published"],
 -        additional:
 -          Map.take(data, [
 -            "cc",
 -            "directMessage",
 -            "id"
 -          ])
 -      }
 -
 -      with {:ok, created_activity} <- ActivityPub.create(params) do
 +         {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor) do
 +      data =
 +        data
 +        |> Map.put("object", fix_object(object, options))
 +        |> Map.put("actor", actor)
 +        |> fix_addressing()
 +
 +      with {:ok, created_activity} <- handle_create(data, user) do
          reply_depth = (options[:depth] || 0) + 1
  
          if Federator.allowed_thread_distance?(reply_depth) do
      |> handle_incoming(options)
    end
  
 +  def handle_incoming(
 +        %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
 +        _options
 +      )
 +      when objtype in ["Question", "Answer"] do
 +    data =
 +      data
 +      |> Map.put("object", fix_object(object))
 +      |> fix_addressing()
 +
 +    data = Map.put_new(data, "context", data["object"]["context"])
 +
 +    with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
 +         {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
 +      {:ok, activity}
 +    end
 +  end
 +
    def handle_incoming(
          %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
          _options
index 7269e81bb8b20c2eda609db7b20934afff4a8cd3,7d33feaf28176a4fff62159854ae960671f1fddc..92ab0f28f0247d2c9605bc6c75940f2da1577e45
@@@ -160,7 -160,7 +160,7 @@@ defmodule Pleroma.Web.ActivityPub.Trans
  
        assert capture_log(fn ->
                 {:ok, _returned_activity} = Transmogrifier.handle_incoming(data)
-              end) =~ "[error] Couldn't fetch \"https://404.site/whatever\", error: nil"
+              end) =~ "[warn] Couldn't fetch \"https://404.site/whatever\", error: nil"
      end
  
      test "it works for incoming notices" do
        assert Enum.at(object.data["tag"], 2) == "moo"
      end
  
 -    test "it works for incoming questions" do
 -      data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!()
 -
 -      {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
 -
 -      object = Object.normalize(activity)
 -
 -      assert Enum.all?(object.data["oneOf"], fn choice ->
 -               choice["name"] in [
 -                 "Dunno",
 -                 "Everyone knows that!",
 -                 "25 char limit is dumb",
 -                 "I can't even fit a funny"
 -               ]
 -             end)
 -    end
 -
      test "it works for incoming listens" do
        data = %{
          "@context" => "https://www.w3.org/ns/activitystreams",
        assert object.data["length"] == 180_000
      end
  
 -    test "it rewrites Note votes to Answers and increments vote counters on question activities" do
 -      user = insert(:user)
 -
 -      {:ok, activity} =
 -        CommonAPI.post(user, %{
 -          status: "suya...",
 -          poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10}
 -        })
 -
 -      object = Object.normalize(activity)
 -
 -      data =
 -        File.read!("test/fixtures/mastodon-vote.json")
 -        |> Poison.decode!()
 -        |> Kernel.put_in(["to"], user.ap_id)
 -        |> Kernel.put_in(["object", "inReplyTo"], object.data["id"])
 -        |> Kernel.put_in(["object", "to"], user.ap_id)
 -
 -      {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
 -      answer_object = Object.normalize(activity)
 -      assert answer_object.data["type"] == "Answer"
 -      object = Object.get_by_ap_id(object.data["id"])
 -
 -      assert Enum.any?(
 -               object.data["oneOf"],
 -               fn
 -                 %{"name" => "suya..", "replies" => %{"totalItems" => 1}} -> true
 -                 _ -> false
 -               end
 -             )
 -    end
 -
      test "it works for incoming notices with contentMap" do
        data =
          File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Poison.decode!()
                     %{
                       "href" =>
                         "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
 -                     "mediaType" => "video/mp4"
 +                     "mediaType" => "video/mp4",
 +                     "type" => "Link"
                     }
                   ]
                 }
                     %{
                       "href" =>
                         "https://framatube.org/static/webseed/6050732a-8a7a-43d4-a6cd-809525a1d206-1080.mp4",
 -                     "mediaType" => "video/mp4"
 +                     "mediaType" => "video/mp4",
 +                     "type" => "Link"
                     }
                   ]
                 }
          "id" => activity.data["id"],
          "content" => "test post",
          "published" => object.data["published"],
-         "actor" => AccountView.render("show.json", %{user: user})
+         "actor" => AccountView.render("show.json", %{user: user, skip_visibility_check: true})
        }
  
        message = %{
      end
    end
  
 -  test "Rewrites Answers to Notes" do
 -    user = insert(:user)
 -
 -    {:ok, poll_activity} =
 -      CommonAPI.post(user, %{
 -        status: "suya...",
 -        poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10}
 -      })
 -
 -    poll_object = Object.normalize(poll_activity)
 -    # TODO: Replace with CommonAPI vote creation when implemented
 -    data =
 -      File.read!("test/fixtures/mastodon-vote.json")
 -      |> Poison.decode!()
 -      |> Kernel.put_in(["to"], user.ap_id)
 -      |> Kernel.put_in(["object", "inReplyTo"], poll_object.data["id"])
 -      |> Kernel.put_in(["object", "to"], user.ap_id)
 -
 -    {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
 -    {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
 -
 -    assert data["object"]["type"] == "Note"
 -  end
 -
    describe "fix_explicit_addressing" do
      setup do
        user = insert(:user)
                 "attachment" => [
                   %{
                     "mediaType" => "video/mp4",
 +                   "type" => "Document",
                     "url" => [
 -                     %{"href" => "https://peertube.moe/stat-480.mp4", "mediaType" => "video/mp4"}
 +                     %{
 +                       "href" => "https://peertube.moe/stat-480.mp4",
 +                       "mediaType" => "video/mp4",
 +                       "type" => "Link"
 +                     }
                     ]
                   }
                 ]
                 "attachment" => [
                   %{
                     "mediaType" => "video/mp4",
 +                   "type" => "Document",
                     "url" => [
 -                     %{"href" => "https://pe.er/stat-480.mp4", "mediaType" => "video/mp4"}
 +                     %{
 +                       "href" => "https://pe.er/stat-480.mp4",
 +                       "mediaType" => "video/mp4",
 +                       "type" => "Link"
 +                     }
                     ]
                   },
                   %{
                     "mediaType" => "video/mp4",
 +                   "type" => "Document",
                     "url" => [
 -                     %{"href" => "https://pe.er/stat-480.mp4", "mediaType" => "video/mp4"}
 +                     %{
 +                       "href" => "https://pe.er/stat-480.mp4",
 +                       "mediaType" => "video/mp4",
 +                       "type" => "Link"
 +                     }
                     ]
                   }
                 ]