Transmogrifier: Move emoji reactions to common pipeline.
authorlain <lain@soykaf.club>
Tue, 5 May 2020 10:11:46 +0000 (12:11 +0200)
committerlain <lain@soykaf.club>
Tue, 5 May 2020 10:11:46 +0000 (12:11 +0200)
lib/pleroma/web/activity_pub/builder.ex
lib/pleroma/web/activity_pub/object_validator.ex
lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/side_effects.ex
lib/pleroma/web/activity_pub/transmogrifier.ex
test/web/activity_pub/object_validator_test.exs
test/web/activity_pub/side_effects_test.exs
test/web/activity_pub/transmogrifier/emoji_react_handling_test.exs

index 429a510b8118df4f136f614de281131fd5dd78b8..2a763645c4016c8d6edfbf6ba37e75466d7da49c 100644 (file)
@@ -10,6 +10,18 @@ defmodule Pleroma.Web.ActivityPub.Builder do
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.ActivityPub.Visibility
 
+  @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
+  def emoji_react(actor, object, emoji) do
+    with {:ok, data, meta} <- like(actor, object) do
+      data =
+        data
+        |> Map.put("content", emoji)
+        |> Map.put("type", "EmojiReact")
+
+      {:ok, data, meta}
+    end
+  end
+
   @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
   def like(actor, object) do
     object_actor = User.get_cached_by_ap_id(object.data["actor"])
index dc4bce0595a12c409475206b2aed0e7222b7ce18..8246558ede963a6442077e51896223ea5f2cd48d 100644 (file)
@@ -12,6 +12,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
   alias Pleroma.Object
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
+  alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
 
   @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
   def validate(object, meta)
@@ -24,6 +25,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
     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())
+      {:ok, object, meta}
+    end
+  end
+
   def stringify_keys(object) do
     object
     |> Map.new(fn {key, val} -> {to_string(key), val} end)
diff --git a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex
new file mode 100644 (file)
index 0000000..e87519c
--- /dev/null
@@ -0,0 +1,81 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
+  use Ecto.Schema
+
+  alias Pleroma.Object
+  alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+
+  import Ecto.Changeset
+  import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+  @primary_key false
+
+  embedded_schema do
+    field(:id, Types.ObjectID, primary_key: true)
+    field(:type, :string)
+    field(:object, Types.ObjectID)
+    field(:actor, Types.ObjectID)
+    field(:context, :string)
+    field(:content, :string)
+    field(:to, {:array, :string}, default: [])
+    field(:cc, {:array, :string}, default: [])
+  end
+
+  def cast_and_validate(data) do
+    data
+    |> cast_data()
+    |> validate_data()
+  end
+
+  def cast_data(data) do
+    %__MODULE__{}
+    |> changeset(data)
+  end
+
+  def changeset(struct, data) do
+    struct
+    |> cast(data, __schema__(:fields))
+    |> fix_after_cast()
+  end
+
+  def fix_after_cast(cng) do
+    cng
+    |> fix_context()
+  end
+
+  def fix_context(cng) do
+    object = get_field(cng, :object)
+
+    with nil <- get_field(cng, :context),
+         %Object{data: %{"context" => context}} <- Object.get_cached_by_ap_id(object) do
+      cng
+      |> put_change(:context, context)
+    else
+      _ ->
+        cng
+    end
+  end
+
+  def validate_emoji(cng) do
+    content = get_field(cng, :content)
+
+    if Pleroma.Emoji.is_unicode_emoji?(content) do
+      cng
+    else
+      cng
+      |> add_error(:content, "must be a single character emoji")
+    end
+  end
+
+  def validate_data(data_cng) do
+    data_cng
+    |> validate_inclusion(:type, ["EmojiReact"])
+    |> validate_required([:id, :type, :object, :actor, :context, :to, :cc, :content])
+    |> validate_actor_presence()
+    |> validate_object_presence()
+    |> validate_emoji()
+  end
+end
index 6a8f1af960ffe34aadb35b287d2c0e6d38c9365e..b15343c070821f81e590873da67fcb6d23fd65c7 100644 (file)
@@ -23,6 +23,18 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
     {:ok, object, meta}
   end
 
+  # Tasks this handles:
+  # - Add reaction to object
+  # - Set up notification
+  def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do
+    reacted_object = Object.get_by_ap_id(object.data["object"])
+    Utils.add_emoji_reaction_to_object(object, reacted_object)
+
+    Notification.create_notifications(object)
+
+    {:ok, object, meta}
+  end
+
   # Nothing to do
   def handle(object, meta) do
     {:ok, object, meta}
index 581e7040bfbbf3c2fd140e83283711e1010acb8f..81e763f883fc086ff1903ef49bc49c268b5161bf 100644 (file)
@@ -656,7 +656,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
     |> handle_incoming(options)
   end
 
-  def handle_incoming(%{"type" => "Like"} = data, _options) do
+  def handle_incoming(%{"type" => type} = data, _options) when type in ["Like", "EmojiReact"] do
     with :ok <- ObjectValidator.fetch_actor_and_object(data),
          {:ok, activity, _meta} <-
            Pipeline.common_pipeline(data, local: false) do
@@ -666,27 +666,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
     end
   end
 
-  def handle_incoming(
-        %{
-          "type" => "EmojiReact",
-          "object" => object_id,
-          "actor" => _actor,
-          "id" => id,
-          "content" => emoji
-        } = data,
-        _options
-      ) do
-    with actor <- Containment.get_actor(data),
-         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
-         {:ok, object} <- get_obj_helper(object_id),
-         {:ok, activity, _object} <-
-           ActivityPub.react_with_emoji(actor, object, emoji, activity_id: id, local: false) do
-      {:ok, activity}
-    else
-      _e -> :error
-    end
-  end
-
   def handle_incoming(
         %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
         _options
index 93989e28ae6d7efce9a136c5c5e21224cd7d9528..a7ad8e6462085b40458da870c57e6eccfa399818 100644 (file)
@@ -1,6 +1,7 @@
 defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
   use Pleroma.DataCase
 
+  alias Pleroma.Web.ActivityPub.Builder
   alias Pleroma.Web.ActivityPub.ObjectValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
   alias Pleroma.Web.ActivityPub.Utils
@@ -8,6 +9,46 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
 
   import Pleroma.Factory
 
+  describe "EmojiReacts" do
+    setup do
+      user = insert(:user)
+      {:ok, post_activity} = CommonAPI.post(user, %{"status" => "uguu"})
+
+      object = Pleroma.Object.get_by_ap_id(post_activity.data["object"])
+
+      {:ok, valid_emoji_react, []} = Builder.emoji_react(user, object, "👌")
+
+      %{user: user, post_activity: post_activity, valid_emoji_react: valid_emoji_react}
+    end
+
+    test "it validates a valid EmojiReact", %{valid_emoji_react: valid_emoji_react} do
+      assert {:ok, _, _} = ObjectValidator.validate(valid_emoji_react, [])
+    end
+
+    test "it is not valid without a 'content' field", %{valid_emoji_react: valid_emoji_react} do
+      without_content =
+        valid_emoji_react
+        |> Map.delete("content")
+
+      {:error, cng} = ObjectValidator.validate(without_content, [])
+
+      refute cng.valid?
+      assert {:content, {"can't be blank", [validation: :required]}} in cng.errors
+    end
+
+    test "it is not valid with a non-emoji content field", %{valid_emoji_react: valid_emoji_react} do
+      without_emoji_content =
+        valid_emoji_react
+        |> Map.put("content", "x")
+
+      {:error, cng} = ObjectValidator.validate(without_emoji_content, [])
+
+      refute cng.valid?
+
+      assert {:content, {"must be a single character emoji", []}} in cng.errors
+    end
+  end
+
   describe "likes" do
     setup do
       user = insert(:user)
index 0b6b551564723da5d3869fe1be0ca053f68d80c8..9271d5ba14a3f845d56f0996491a59bc4dc6d139 100644 (file)
@@ -15,6 +15,33 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
 
   import Pleroma.Factory
 
+  describe "EmojiReact objects" do
+    setup do
+      poster = insert(:user)
+      user = insert(:user)
+
+      {:ok, post} = CommonAPI.post(poster, %{"status" => "hey"})
+
+      {:ok, emoji_react_data, []} = Builder.emoji_react(user, post.object, "👌")
+      {:ok, emoji_react, _meta} = ActivityPub.persist(emoji_react_data, local: true)
+
+      %{emoji_react: emoji_react, user: user, poster: poster}
+    end
+
+    test "adds the reaction to the object", %{emoji_react: emoji_react, user: user} do
+      {:ok, emoji_react, _} = SideEffects.handle(emoji_react)
+      object = Object.get_by_ap_id(emoji_react.data["object"])
+
+      assert object.data["reaction_count"] == 1
+      assert ["👌", [user.ap_id]] in object.data["reactions"]
+    end
+
+    test "creates a notification", %{emoji_react: emoji_react, poster: poster} do
+      {:ok, emoji_react, _} = SideEffects.handle(emoji_react)
+      assert Repo.get_by(Notification, user_id: poster.id, activity_id: emoji_react.id)
+    end
+  end
+
   describe "like objects" do
     setup do
       poster = insert(:user)
index 9f4f6b29665a241b0f02654da8dde989b7484d39..6988e3e0a0c86906b1115f38541fba66742babff 100644 (file)
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do
   use Pleroma.DataCase
 
   alias Pleroma.Activity
+  alias Pleroma.Object
   alias Pleroma.Web.ActivityPub.Transmogrifier
   alias Pleroma.Web.CommonAPI
 
@@ -29,6 +30,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do
     assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2"
     assert data["object"] == activity.data["object"]
     assert data["content"] == "👌"
+
+    object = Object.get_by_ap_id(data["object"])
+
+    assert object.data["reaction_count"] == 1
+    assert match?([["👌", _]], object.data["reactions"])
   end
 
   test "it reject invalid emoji reactions" do
@@ -42,7 +48,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do
       |> Map.put("object", activity.data["object"])
       |> Map.put("actor", other_user.ap_id)
 
-    assert :error = Transmogrifier.handle_incoming(data)
+    assert {:error, _} = Transmogrifier.handle_incoming(data)
 
     data =
       File.read!("test/fixtures/emoji-reaction-no-emoji.json")
@@ -50,6 +56,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do
       |> Map.put("object", activity.data["object"])
       |> Map.put("actor", other_user.ap_id)
 
-    assert :error = Transmogrifier.handle_incoming(data)
+    assert {:error, _} = Transmogrifier.handle_incoming(data)
   end
 end