Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into feature/emojireactv...
authorlain <lain@soykaf.club>
Thu, 7 May 2020 13:03:12 +0000 (15:03 +0200)
committerlain <lain@soykaf.club>
Thu, 7 May 2020 13:03:12 +0000 (15:03 +0200)
13 files changed:
1  2 
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/builder.ex
lib/pleroma/web/activity_pub/object_validator.ex
lib/pleroma/web/activity_pub/side_effects.ex
lib/pleroma/web/activity_pub/transmogrifier.ex
lib/pleroma/web/common_api/common_api.ex
test/notification_test.exs
test/web/activity_pub/activity_pub_test.exs
test/web/activity_pub/object_validator_test.exs
test/web/activity_pub/side_effects_test.exs
test/web/activity_pub/transmogrifier_test.exs
test/web/common_api/common_api_test.exs
test/web/mastodon_api/views/status_view_test.exs

index 2a763645c4016c8d6edfbf6ba37e75466d7da49c,1345a3a3e2cf2430e42990f0d8b758f1d7a20266..d130176cf745c8622191522c9e15a67e6a09d8f9
@@@ -10,18 -10,33 +10,45 @@@ defmodule Pleroma.Web.ActivityPub.Build
    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 delete(User.t(), String.t()) :: {:ok, map(), keyword()}
+   def delete(actor, object_id) do
+     object = Object.normalize(object_id, false)
+     user = !object && User.get_cached_by_ap_id(object_id)
+     to =
+       case {object, user} do
+         {%Object{}, _} ->
+           # We are deleting an object, address everyone who was originally mentioned
+           (object.data["to"] || []) ++ (object.data["cc"] || [])
+         {_, %User{follower_address: follower_address}} ->
+           # We are deleting a user, address the followers of that user
+           [follower_address]
+       end
+     {:ok,
+      %{
+        "id" => Utils.generate_activity_id(),
+        "actor" => actor.ap_id,
+        "object" => object_id,
+        "to" => to,
+        "type" => "Delete"
+      }, []}
+   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 d730cb062a91dc23484077f7321f6121cdbf9993,479f922f51296a5a55e8bf0fb4c8c2937f9320d3..e51a8e0a824bbb5ff17b37751ca7e65a0a7cc614
@@@ -11,8 -11,9 +11,10 @@@ defmodule Pleroma.Web.ActivityPub.Objec
  
    alias Pleroma.Object
    alias Pleroma.User
+   alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
 +  alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
    alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
+   alias Pleroma.Web.ActivityPub.ObjectValidators.Types
  
    @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
    def validate(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())
 +      {:ok, object, meta}
 +    end
 +  end
 +
+   def stringify_keys(%{__struct__: _} = object) do
+     object
+     |> Map.from_struct()
+     |> stringify_keys
+   end
    def stringify_keys(object) do
      object
      |> Map.new(fn {key, val} -> {to_string(key), val} end)
index b15343c070821f81e590873da67fcb6d23fd65c7,7b53abeafc61de0f69bfef89b81c14f8265e8aaf..8e5586e88702df23795a0c507cbc224e5f3758ca
@@@ -23,18 -25,49 +25,61 @@@ defmodule Pleroma.Web.ActivityPub.SideE
      {: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
 +
+   # Tasks this handles:
+   # - Delete and unpins the create activity
+   # - Replace object with Tombstone
+   # - Set up notification
+   # - Reduce the user note count
+   # - Reduce the reply count
+   # - Stream out the activity
+   def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do
+     deleted_object =
+       Object.normalize(deleted_object, false) || User.get_cached_by_ap_id(deleted_object)
+     result =
+       case deleted_object do
+         %Object{} ->
+           with {:ok, deleted_object, activity} <- Object.delete(deleted_object),
+                %User{} = user <- User.get_cached_by_ap_id(deleted_object.data["actor"]) do
+             User.remove_pinnned_activity(user, activity)
+             {:ok, user} = ActivityPub.decrease_note_count_if_public(user, deleted_object)
+             if in_reply_to = deleted_object.data["inReplyTo"] do
+               Object.decrease_replies_count(in_reply_to)
+             end
+             ActivityPub.stream_out(object)
+             ActivityPub.stream_out_participations(deleted_object, user)
+             :ok
+           end
+         %User{} ->
+           with {:ok, _} <- User.delete(deleted_object) do
+             :ok
+           end
+       end
+     if result == :ok do
+       Notification.create_notifications(object)
+       {:ok, object, meta}
+     else
+       {:error, result}
+     end
+   end
    # Nothing to do
    def handle(object, meta) do
      {:ok, object, meta}
Simple merge
index a7ad8e6462085b40458da870c57e6eccfa399818,744c46781d33dab03549624f1b3db52873eba764..4cae5207703ae1a813e4e3934bf6212042d5a486
@@@ -9,46 -10,98 +10,138 @@@ defmodule Pleroma.Web.ActivityPub.Objec
  
    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 "deletes" do
+     setup do
+       user = insert(:user)
+       {:ok, post_activity} = CommonAPI.post(user, %{"status" => "cancel me daddy"})
+       {:ok, valid_post_delete, _} = Builder.delete(user, post_activity.data["object"])
+       {:ok, valid_user_delete, _} = Builder.delete(user, user.ap_id)
+       %{user: user, valid_post_delete: valid_post_delete, valid_user_delete: valid_user_delete}
+     end
+     test "it is valid for a post deletion", %{valid_post_delete: valid_post_delete} do
+       {:ok, valid_post_delete, _} = ObjectValidator.validate(valid_post_delete, [])
+       assert valid_post_delete["deleted_activity_id"]
+     end
+     test "it is invalid if the object isn't in a list of certain types", %{
+       valid_post_delete: valid_post_delete
+     } do
+       object = Object.get_by_ap_id(valid_post_delete["object"])
+       data =
+         object.data
+         |> Map.put("type", "Like")
+       {:ok, _object} =
+         object
+         |> Ecto.Changeset.change(%{data: data})
+         |> Object.update_and_set_cache()
+       {:error, cng} = ObjectValidator.validate(valid_post_delete, [])
+       assert {:object, {"object not in allowed types", []}} in cng.errors
+     end
+     test "it is valid for a user deletion", %{valid_user_delete: valid_user_delete} do
+       assert match?({:ok, _, _}, ObjectValidator.validate(valid_user_delete, []))
+     end
+     test "it's invalid if the id is missing", %{valid_post_delete: valid_post_delete} do
+       no_id =
+         valid_post_delete
+         |> Map.delete("id")
+       {:error, cng} = ObjectValidator.validate(no_id, [])
+       assert {:id, {"can't be blank", [validation: :required]}} in cng.errors
+     end
+     test "it's invalid if the object doesn't exist", %{valid_post_delete: valid_post_delete} do
+       missing_object =
+         valid_post_delete
+         |> Map.put("object", "http://does.not/exist")
+       {:error, cng} = ObjectValidator.validate(missing_object, [])
+       assert {:object, {"can't find object", []}} in cng.errors
+     end
+     test "it's invalid if the actor of the object and the actor of delete are from different domains",
+          %{valid_post_delete: valid_post_delete} do
+       valid_user = insert(:user)
+       valid_other_actor =
+         valid_post_delete
+         |> Map.put("actor", valid_user.ap_id)
+       assert match?({:ok, _, _}, ObjectValidator.validate(valid_other_actor, []))
+       invalid_other_actor =
+         valid_post_delete
+         |> Map.put("actor", "https://gensokyo.2hu/users/raymoo")
+       {:error, cng} = ObjectValidator.validate(invalid_other_actor, [])
+       assert {:actor, {"is not allowed to delete object", []}} in cng.errors
+     end
+     test "it's valid if the actor of the object is a local superuser",
+          %{valid_post_delete: valid_post_delete} do
+       user =
+         insert(:user, local: true, is_moderator: true, ap_id: "https://gensokyo.2hu/users/raymoo")
+       valid_other_actor =
+         valid_post_delete
+         |> Map.put("actor", user.ap_id)
+       {:ok, _, meta} = ObjectValidator.validate(valid_other_actor, [])
+       assert meta[:do_not_federate]
+     end
+   end
    describe "likes" do
      setup do
        user = insert(:user)
index 9271d5ba14a3f845d56f0996491a59bc4dc6d139,a9598d7b36ecbdc2c21507c177fd7aa2a4b52710..404b129ea752e850efa3b79c047d512a31585f0b
@@@ -14,34 -18,60 +18,87 @@@ defmodule Pleroma.Web.ActivityPub.SideE
    alias Pleroma.Web.CommonAPI
  
    import Pleroma.Factory
+   import Mock
+   describe "delete objects" do
+     setup do
+       user = insert(:user)
+       other_user = insert(:user)
+       {:ok, op} = CommonAPI.post(other_user, %{"status" => "big oof"})
+       {:ok, post} = CommonAPI.post(user, %{"status" => "hey", "in_reply_to_id" => op})
+       object = Object.normalize(post)
+       {:ok, delete_data, _meta} = Builder.delete(user, object.data["id"])
+       {:ok, delete_user_data, _meta} = Builder.delete(user, user.ap_id)
+       {:ok, delete, _meta} = ActivityPub.persist(delete_data, local: true)
+       {:ok, delete_user, _meta} = ActivityPub.persist(delete_user_data, local: true)
+       %{user: user, delete: delete, post: post, object: object, delete_user: delete_user, op: op}
+     end
+     test "it handles object deletions", %{
+       delete: delete,
+       post: post,
+       object: object,
+       user: user,
+       op: op
+     } do
+       with_mock Pleroma.Web.ActivityPub.ActivityPub, [:passthrough],
+         stream_out: fn _ -> nil end,
+         stream_out_participations: fn _, _ -> nil end do
+         {:ok, delete, _} = SideEffects.handle(delete)
+         user = User.get_cached_by_ap_id(object.data["actor"])
+         assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out(delete))
+         assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out_participations(object, user))
+       end
+       object = Object.get_by_id(object.id)
+       assert object.data["type"] == "Tombstone"
+       refute Activity.get_by_id(post.id)
+       user = User.get_by_id(user.id)
+       assert user.note_count == 0
+       object = Object.normalize(op.data["object"], false)
+       assert object.data["repliesCount"] == 0
+     end
+     test "it handles user deletions", %{delete_user: delete, user: user} do
+       {:ok, _delete, _} = SideEffects.handle(delete)
+       ObanHelpers.perform_all()
+       assert User.get_cached_by_ap_id(user.ap_id).deactivated
+     end
+   end
  
 +  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)