From 66452f518faa1f079f02006943b0c2cdc830b47f Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Thu, 17 Oct 2019 18:36:52 +0200
Subject: [PATCH] ObjectValidator: Rewrite LikeValidator with Ecto.

---
 .../web/activity_pub/object_validator.ex      | 42 ++--------
 .../object_validators/like_validator.ex       | 69 ++++++++++++++++
 .../object_validators/types/object.ex         | 25 ++++++
 lib/pleroma/web/common_api/common_api.ex      | 10 ---
 .../activity_pub/activity_validator_test.exs  | 21 -----
 .../activity_pub/object_validator_test.exs    | 80 +++++++++++++++++++
 test/web/activity_pub/side_effects_test.exs   |  1 +
 7 files changed, 183 insertions(+), 65 deletions(-)
 create mode 100644 lib/pleroma/web/activity_pub/object_validators/like_validator.ex
 create mode 100644 lib/pleroma/web/activity_pub/object_validators/types/object.ex
 delete mode 100644 test/web/activity_pub/activity_validator_test.exs
 create mode 100644 test/web/activity_pub/object_validator_test.exs

diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex
index 0048cc4ec..adcb53c65 100644
--- a/lib/pleroma/web/activity_pub/object_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -9,50 +9,24 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
   the system.
   """
 
-  alias Pleroma.User
-  alias Pleroma.Object
-  alias Pleroma.Web.ActivityPub.Utils
-
-  def validate_id(object, meta) do
-    with {_, true} <- {:id_presence, Map.has_key?(object, "id")} do
-      {:ok, object, meta}
-    else
-      e -> {:error, e}
-    end
-  end
-
-  def validate_actor(object, meta) do
-    with {_, %User{}} <- {:actor_validation, User.get_cached_by_ap_id(object["actor"])} do
-      {:ok, object, meta}
-    else
-      e -> {:error, e}
-    end
-  end
-
-  def common_validations(object, meta) do
-    with {_, {:ok, object, meta}} <- {:validate_id, validate_id(object, meta)},
-         {_, {:ok, object, meta}} <- {:validate_actor, validate_actor(object, meta)} do
-      {:ok, object, meta}
-    else
-      e -> {:error, e}
-    end
-  end
+  alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
 
   @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
   def validate(object, meta)
 
   def validate(%{"type" => "Like"} = object, meta) do
-    with {:ok, object, meta} <- common_validations(object, meta),
-         {_, %Object{} = liked_object} <-
-           {:find_liked_object, Object.normalize(object["object"])},
-         {_, nil} <- {:existing_like, Utils.get_existing_like(object["actor"], liked_object)} do
+    with {_, %{valid?: true, changes: object}} <-
+           {:validate_object, LikeValidator.cast_and_validate(object)} do
+      object = stringify_keys(object)
       {:ok, object, meta}
     else
       e -> {:error, e}
     end
   end
 
-  def validate(object, meta) do
-    common_validations(object, meta)
+  defp stringify_keys(object) do
+    object
+    |> Enum.map(fn {key, val} -> {to_string(key), val} end)
+    |> Enum.into(%{})
   end
 end
diff --git a/lib/pleroma/web/activity_pub/object_validators/like_validator.ex b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex
new file mode 100644
index 000000000..d5a2f7202
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/like_validator.ex
@@ -0,0 +1,69 @@
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
+  use Ecto.Schema
+  import Ecto.Changeset
+
+  alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+  alias Pleroma.Web.ActivityPub.Utils
+  alias Pleroma.User
+  alias Pleroma.Object
+
+  @primary_key false
+
+  embedded_schema do
+    field(:id, :string, primary_key: true)
+    field(:type, :string)
+    field(:object, Types.ObjectID)
+    field(:actor, Types.ObjectID)
+    field(:context, :string)
+    field(:to, {:array, :string})
+    field(:cc, {:array, :string})
+  end
+
+  def cast_and_validate(data) do
+    data
+    |> cast_data()
+    |> validate_data()
+  end
+
+  def cast_data(data) do
+    %__MODULE__{}
+    |> cast(data, [:id, :type, :object, :actor, :context, :to, :cc])
+  end
+
+  def validate_data(data_cng) do
+    data_cng
+    |> validate_inclusion(:type, ["Like"])
+    |> validate_required([:id, :type, :object, :actor, :context])
+    |> validate_change(:actor, &actor_valid?/2)
+    |> validate_change(:object, &object_valid?/2)
+    |> validate_existing_like()
+  end
+
+  def validate_existing_like(%{changes: %{actor: actor, object: object}} = cng) do
+    if Utils.get_existing_like(actor, %{data: %{"id" => object}}) do
+      cng
+      |> add_error(:actor, "already liked this object")
+      |> add_error(:object, "already liked by this actor")
+    else
+      cng
+    end
+  end
+
+  def validate_existing_like(cng), do: cng
+
+  def actor_valid?(field_name, actor) do
+    if User.get_cached_by_ap_id(actor) do
+      []
+    else
+      [{field_name, "can't find user"}]
+    end
+  end
+
+  def object_valid?(field_name, object) do
+    if Object.get_cached_by_ap_id(object) do
+      []
+    else
+      [{field_name, "can't find object"}]
+    end
+  end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/types/object.ex b/lib/pleroma/web/activity_pub/object_validators/types/object.ex
new file mode 100644
index 000000000..92fc13ba8
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/types/object.ex
@@ -0,0 +1,25 @@
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID do
+  use Ecto.Type
+
+  def type, do: :string
+
+  def cast(object) when is_binary(object) do
+    {:ok, object}
+  end
+
+  def cast(%{"id" => object}) when is_binary(object) do
+    {:ok, object}
+  end
+
+  def cast(_) do
+    :error
+  end
+
+  def dump(data) do
+    {:ok, data}
+  end
+
+  def load(data) do
+    {:ok, data}
+  end
+end
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index 466beb724..e0b22a314 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -115,16 +115,6 @@ def favorite(%User{} = user, id) do
     end
   end
 
-  # def favorite(id_or_ap_id, user) do
-  #   with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
-  #        object <- Object.normalize(activity),
-  #        nil <- Utils.get_existing_like(user.ap_id, object) do
-  #     ActivityPub.like(user, object)
-  #   else
-  #     _ -> {:error, dgettext("errors", "Could not favorite")}
-  #   end
-  # end
-
   def unfavorite(id_or_ap_id, user) do
     with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
       object = Object.normalize(activity)
diff --git a/test/web/activity_pub/activity_validator_test.exs b/test/web/activity_pub/activity_validator_test.exs
deleted file mode 100644
index cb0895a81..000000000
--- a/test/web/activity_pub/activity_validator_test.exs
+++ /dev/null
@@ -1,21 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
-  use Pleroma.DataCase
-
-  import Pleroma.Factory
-
-  describe "likes" do
-    test "it is well formed" do
-      _required_fields = [
-        "id",
-        "actor",
-        "object"
-      ]
-
-      _user = insert(:user)
-    end
-  end
-end
diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs
new file mode 100644
index 000000000..374a7c0df
--- /dev/null
+++ b/test/web/activity_pub/object_validator_test.exs
@@ -0,0 +1,80 @@
+defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
+  use Pleroma.DataCase
+
+  alias Pleroma.Web.CommonAPI
+  alias Pleroma.Web.ActivityPub.ObjectValidator
+  alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
+  alias Pleroma.Web.ActivityPub.Utils
+  import Pleroma.Factory
+
+  describe "likes" do
+    setup do
+      user = insert(:user)
+      {:ok, post_activity} = CommonAPI.post(user, %{"status" => "uguu"})
+
+      valid_like = %{
+        "type" => "Like",
+        "id" => Utils.generate_activity_id(),
+        "object" => post_activity.data["object"],
+        "actor" => user.ap_id,
+        "context" => "a context"
+      }
+
+      %{valid_like: valid_like, user: user, post_activity: post_activity}
+    end
+
+    test "returns ok when called in the ObjectValidator", %{valid_like: valid_like} do
+      {:ok, object, _meta} = ObjectValidator.validate(valid_like, [])
+
+      assert "id" in Map.keys(object)
+    end
+
+    test "is valid for a valid object", %{valid_like: valid_like} do
+      assert LikeValidator.cast_and_validate(valid_like).valid?
+    end
+
+    test "it errors when the actor is missing or not known", %{valid_like: valid_like} do
+      without_actor = Map.delete(valid_like, "actor")
+
+      refute LikeValidator.cast_and_validate(without_actor).valid?
+
+      with_invalid_actor = Map.put(valid_like, "actor", "invalidactor")
+
+      refute LikeValidator.cast_and_validate(with_invalid_actor).valid?
+    end
+
+    test "it errors when the object is missing or not known", %{valid_like: valid_like} do
+      without_object = Map.delete(valid_like, "object")
+
+      refute LikeValidator.cast_and_validate(without_object).valid?
+
+      with_invalid_object = Map.put(valid_like, "object", "invalidobject")
+
+      refute LikeValidator.cast_and_validate(with_invalid_object).valid?
+    end
+
+    test "it errors when the actor has already like the object", %{
+      valid_like: valid_like,
+      user: user,
+      post_activity: post_activity
+    } do
+      _like = CommonAPI.favorite(user, post_activity.id)
+
+      refute LikeValidator.cast_and_validate(valid_like).valid?
+    end
+
+    test "it works when actor or object are wrapped in maps", %{valid_like: valid_like} do
+      wrapped_like =
+        valid_like
+        |> Map.put("actor", %{"id" => valid_like["actor"]})
+        |> Map.put("object", %{"id" => valid_like["object"]})
+
+      validated = LikeValidator.cast_and_validate(wrapped_like)
+
+      assert validated.valid?
+
+      assert {:actor, valid_like["actor"]} in validated.changes
+      assert {:object, valid_like["object"]} in validated.changes
+    end
+  end
+end
diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs
index e505ab4dd..9d99e05a0 100644
--- a/test/web/activity_pub/side_effects_test.exs
+++ b/test/web/activity_pub/side_effects_test.exs
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
   alias Pleroma.Web.ActivityPub.SideEffects
 
   import Pleroma.Factory
+
   describe "like objects" do
     setup do
       user = insert(:user)