Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into feature/undo-valida...
authorlain <lain@soykaf.club>
Thu, 7 May 2020 12:45:20 +0000 (14:45 +0200)
committerlain <lain@soykaf.club>
Thu, 7 May 2020 12:45:20 +0000 (14:45 +0200)
16 files changed:
1  2 
lib/pleroma/user.ex
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/object_validators/common_validations.ex
lib/pleroma/web/activity_pub/side_effects.ex
lib/pleroma/web/activity_pub/transmogrifier.ex
lib/pleroma/web/activity_pub/utils.ex
lib/pleroma/web/common_api/common_api.ex
lib/pleroma/web/mastodon_api/controllers/account_controller.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

index aa675a521f68713449715248f4b7b3be145bbf86,a6f51f0bec630f90a5391b260dc6b43a1f981a51..921bdd93a7fb4e283be89817140ae991b156b34b
@@@ -1548,23 -1554,29 +1554,22 @@@ defmodule Pleroma.User d
      |> Stream.run()
    end
  
-   defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
-     activity
-     |> Object.normalize()
-     |> ActivityPub.delete()
+   defp delete_activity(%{data: %{"type" => "Create", "object" => object}}, user) do
+     {:ok, delete_data, _} = Builder.delete(user, object)
+     Pipeline.common_pipeline(delete_data, local: true)
    end
  
 -  defp delete_activity(%{data: %{"type" => "Like"}} = activity, _user) do
 -    object = Object.normalize(activity)
 +  defp delete_activity(%{data: %{"type" => type}} = activity) when type in ["Like", "Announce"] do
 +    actor =
 +      activity.actor
 +      |> get_cached_by_ap_id()
  
 -    activity.actor
 -    |> get_cached_by_ap_id()
 -    |> ActivityPub.unlike(object)
 -  end
 -
 -  defp delete_activity(%{data: %{"type" => "Announce"}} = activity, _user) do
 -    object = Object.normalize(activity)
 -
 -    activity.actor
 -    |> get_cached_by_ap_id()
 -    |> ActivityPub.unannounce(object)
 +    {:ok, undo, _} = Builder.undo(actor, activity)
 +    Pipeline.common_pipeline(undo, local: true)
    end
  
-   defp delete_activity(_activity), do: "Doing nothing"
+   defp delete_activity(_activity, _user), do: "Doing nothing"
  
    def html_filter_policy(%User{no_rich_text: true}) do
      Pleroma.HTML.Scrubber.TwitterText
index 380d8f56514a23cd2f48b7e1501e1b229fc8153a,1345a3a3e2cf2430e42990f0d8b758f1d7a20266..b0f447e28417a9deb34190084932af70bc8ae9d5
@@@ -10,19 -10,33 +10,46 @@@ defmodule Pleroma.Web.ActivityPub.Build
    alias Pleroma.Web.ActivityPub.Utils
    alias Pleroma.Web.ActivityPub.Visibility
  
 +  @spec undo(User.t(), Activity.t()) :: {:ok, map(), keyword()}
 +  def undo(actor, object) do
 +    {:ok,
 +     %{
 +       "id" => Utils.generate_activity_id(),
 +       "actor" => actor.ap_id,
 +       "type" => "Undo",
 +       "object" => object.data["id"],
 +       "to" => object.data["to"] || [],
 +       "cc" => object.data["cc"] || []
 +     }, []}
 +  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 b6937d2e1cc8cc0608ba7f673ad4b5fb7379dd50,479f922f51296a5a55e8bf0fb4c8c2937f9320d3..8e043287d15eba414befb13724f405fe10bf8943
@@@ -11,20 -11,23 +11,32 @@@ defmodule Pleroma.Web.ActivityPub.Objec
  
    alias Pleroma.Object
    alias Pleroma.User
+   alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
    alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
 +  alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
+   alias Pleroma.Web.ActivityPub.ObjectValidators.Types
  
    @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
    def validate(object, meta)
  
 +  def validate(%{"type" => "Undo"} = object, meta) do
 +    with {:ok, object} <-
 +           object |> UndoValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do
 +      object = stringify_keys(object |> Map.from_struct())
 +      {:ok, object, meta}
 +    end
 +  end
 +
+   def validate(%{"type" => "Delete"} = object, meta) do
+     with cng <- DeleteValidator.cast_and_validate(object),
+          do_not_federate <- DeleteValidator.do_not_federate?(cng),
+          {:ok, object} <- Ecto.Changeset.apply_action(cng, :insert) do
+       object = stringify_keys(object)
+       meta = Keyword.put(meta, :do_not_federate, do_not_federate)
+       {:ok, object, meta}
+     end
+   end
    def validate(%{"type" => "Like"} = object, meta) do
      with {:ok, object} <-
             object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do
index 067ee4f9aa686e9099d5a65b9df54468aff68d44,4e6ee2034168eb359e455bb1a4438d0d283dea4f..2ada9f09e5e9d8743ebbe01aae0102b4e9ffe7d1
@@@ -20,13 -41,23 +42,23 @@@ defmodule Pleroma.Web.ActivityPub.Objec
      end)
    end
  
-   def validate_object_presence(cng, field_name \\ :object) do
+   def validate_object_presence(cng, options \\ []) do
+     field_name = Keyword.get(options, :field_name, :object)
+     allowed_types = Keyword.get(options, :allowed_types, false)
      cng
-     |> validate_change(field_name, fn field_name, object ->
-       if Object.get_cached_by_ap_id(object) || Activity.get_by_ap_id(object) do
-         []
-       else
-         [{field_name, "can't find object"}]
+     |> validate_change(field_name, fn field_name, object_id ->
 -      object = Object.get_cached_by_ap_id(object_id)
++      object = Object.get_cached_by_ap_id(object_id) || Activity.get_by_ap_id(object)
+       cond do
+         !object ->
+           [{field_name, "can't find object"}]
+         object && allowed_types && object.data["type"] not in allowed_types ->
+           [{field_name, "object not in allowed types"}]
+         true ->
+           []
        end
      end)
    end
index 3fad6e4d8d7fe50fd720f6ff8c77da9d538c4956,7b53abeafc61de0f69bfef89b81c14f8265e8aaf..5049cb54ea2804ce21fd11854c366c3f2965f7d5
@@@ -5,11 -5,10 +5,12 @@@ defmodule Pleroma.Web.ActivityPub.SideE
    liked object, a `Follow` activity will add the user to the follower
    collection, and so on.
    """
 +  alias Pleroma.Activity
    alias Pleroma.Notification
    alias Pleroma.Object
 +  alias Pleroma.Repo
    alias Pleroma.User
+   alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.ActivityPub.Utils
  
    def handle(object, meta \\ [])
      {:ok, object, meta}
    end
  
 +  def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, meta) do
 +    with undone_object <- Activity.get_by_ap_id(undone_object),
 +         :ok <- handle_undoing(undone_object) do
 +      {:ok, object, meta}
 +    end
 +  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
Simple merge
index 7824095c78b53aa93c6c170069c3fc2b56061e36,4dc9c0f0a8b72ab81ac5153f34c1a276bc09dbdf..0a8a7119d16438d4acaa6b88de520d89ddc62228
@@@ -1136,145 -1298,40 +1136,8 @@@ defmodule Pleroma.Web.ActivityPub.Activ
        assert activity.data["actor"] == blocker.ap_id
        assert activity.data["object"] == blocked.ap_id
      end
 -
 -    test "reverts unblock activity on error" do
 -      [blocker, blocked] = insert_list(2, :user)
 -      {:ok, block_activity} = ActivityPub.block(blocker, blocked)
 -
 -      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
 -        assert {:error, :reverted} = ActivityPub.unblock(blocker, blocked)
 -      end
 -
 -      assert block_activity.data["type"] == "Block"
 -      assert block_activity.data["actor"] == blocker.ap_id
 -
 -      assert Repo.aggregate(Activity, :count, :id) == 1
 -      assert Repo.aggregate(Object, :count, :id) == 1
 -    end
 -
 -    test "creates an undo activity for the last block" do
 -      blocker = insert(:user)
 -      blocked = insert(:user)
 -
 -      {:ok, block_activity} = ActivityPub.block(blocker, blocked)
 -      {:ok, activity} = ActivityPub.unblock(blocker, blocked)
 -
 -      assert activity.data["type"] == "Undo"
 -      assert activity.data["actor"] == blocker.ap_id
 -
 -      embedded_object = activity.data["object"]
 -      assert is_map(embedded_object)
 -      assert embedded_object["type"] == "Block"
 -      assert embedded_object["object"] == blocked.ap_id
 -      assert embedded_object["id"] == block_activity.data["id"]
 -    end
    end
  
-   describe "deletion" do
-     setup do: clear_config([:instance, :rewrite_policy])
-     test "it reverts deletion on error" do
-       note = insert(:note_activity)
-       object = Object.normalize(note)
-       with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
-         assert {:error, :reverted} = ActivityPub.delete(object)
-       end
-       assert Repo.aggregate(Activity, :count, :id) == 1
-       assert Repo.get(Object, object.id) == object
-       assert Activity.get_by_id(note.id) == note
-     end
-     test "it creates a delete activity and deletes the original object" do
-       note = insert(:note_activity)
-       object = Object.normalize(note)
-       {:ok, delete} = ActivityPub.delete(object)
-       assert delete.data["type"] == "Delete"
-       assert delete.data["actor"] == note.data["actor"]
-       assert delete.data["object"] == object.data["id"]
-       assert Activity.get_by_id(delete.id) != nil
-       assert Repo.get(Object, object.id).data["type"] == "Tombstone"
-     end
-     test "it doesn't fail when an activity was already deleted" do
-       {:ok, delete} = insert(:note_activity) |> Object.normalize() |> ActivityPub.delete()
-       assert {:ok, ^delete} = delete |> Object.normalize() |> ActivityPub.delete()
-     end
-     test "decrements user note count only for public activities" do
-       user = insert(:user, note_count: 10)
-       {:ok, a1} =
-         CommonAPI.post(User.get_cached_by_id(user.id), %{
-           "status" => "yeah",
-           "visibility" => "public"
-         })
-       {:ok, a2} =
-         CommonAPI.post(User.get_cached_by_id(user.id), %{
-           "status" => "yeah",
-           "visibility" => "unlisted"
-         })
-       {:ok, a3} =
-         CommonAPI.post(User.get_cached_by_id(user.id), %{
-           "status" => "yeah",
-           "visibility" => "private"
-         })
-       {:ok, a4} =
-         CommonAPI.post(User.get_cached_by_id(user.id), %{
-           "status" => "yeah",
-           "visibility" => "direct"
-         })
-       {:ok, _} = Object.normalize(a1) |> ActivityPub.delete()
-       {:ok, _} = Object.normalize(a2) |> ActivityPub.delete()
-       {:ok, _} = Object.normalize(a3) |> ActivityPub.delete()
-       {:ok, _} = Object.normalize(a4) |> ActivityPub.delete()
-       user = User.get_cached_by_id(user.id)
-       assert user.note_count == 10
-     end
-     test "it creates a delete activity and checks that it is also sent to users mentioned by the deleted object" do
-       user = insert(:user)
-       note = insert(:note_activity)
-       object = Object.normalize(note)
-       {:ok, object} =
-         object
-         |> Object.change(%{
-           data: %{
-             "actor" => object.data["actor"],
-             "id" => object.data["id"],
-             "to" => [user.ap_id],
-             "type" => "Note"
-           }
-         })
-         |> Object.update_and_set_cache()
-       {:ok, delete} = ActivityPub.delete(object)
-       assert user.ap_id in delete.data["to"]
-     end
-     test "decreases reply count" do
-       user = insert(:user)
-       user2 = insert(:user)
-       {:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"})
-       reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id}
-       ap_id = activity.data["id"]
-       {:ok, public_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public"))
-       {:ok, unlisted_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted"))
-       {:ok, private_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private"))
-       {:ok, direct_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct"))
-       _ = CommonAPI.delete(direct_reply.id, user2)
-       assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
-       assert object.data["repliesCount"] == 2
-       _ = CommonAPI.delete(private_reply.id, user2)
-       assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
-       assert object.data["repliesCount"] == 2
-       _ = CommonAPI.delete(public_reply.id, user2)
-       assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
-       assert object.data["repliesCount"] == 1
-       _ = CommonAPI.delete(unlisted_reply.id, user2)
-       assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
-       assert object.data["repliesCount"] == 0
-     end
-     test "it passes delete activity through MRF before deleting the object" do
-       Pleroma.Config.put([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.DropPolicy)
-       note = insert(:note_activity)
-       object = Object.normalize(note)
-       {:error, {:reject, _}} = ActivityPub.delete(object)
-       assert Activity.get_by_id(note.id)
-       assert Repo.get(Object, object.id).data["type"] == object.data["type"]
-     end
-   end
    describe "timeline post-processing" do
      test "it filters broken threads" do
        user1 = insert(:user)
index 8626e127e1ee81650e7a06080f3b5881311ee00a,744c46781d33dab03549624f1b3db52873eba764..4d90a0cf347abf5232ca1685949a11dd3d896416
@@@ -9,46 -10,98 +10,139 @@@ defmodule Pleroma.Web.ActivityPub.Objec
  
    import Pleroma.Factory
  
 +  describe "Undos" do
 +    setup do
 +      user = insert(:user)
 +      {:ok, post_activity} = CommonAPI.post(user, %{"status" => "uguu"})
 +      {:ok, like} = CommonAPI.favorite(user, post_activity.id)
 +      {:ok, valid_like_undo, []} = Builder.undo(user, like)
 +
 +      %{user: user, like: like, valid_like_undo: valid_like_undo}
 +    end
 +
 +    test "it validates a basic like undo", %{valid_like_undo: valid_like_undo} do
 +      assert {:ok, _, _} = ObjectValidator.validate(valid_like_undo, [])
 +    end
 +
 +    test "it does not validate if the actor of the undo is not the actor of the object", %{
 +      valid_like_undo: valid_like_undo
 +    } do
 +      other_user = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo")
 +
 +      bad_actor =
 +        valid_like_undo
 +        |> Map.put("actor", other_user.ap_id)
 +
 +      {:error, cng} = ObjectValidator.validate(bad_actor, [])
 +
 +      assert {:actor, {"not the same as object actor", []}} in cng.errors
 +    end
 +
 +    test "it does not validate if the object is missing", %{valid_like_undo: valid_like_undo} do
 +      missing_object =
 +        valid_like_undo
 +        |> Map.put("object", "https://gensokyo.2hu/objects/1")
 +
 +      {:error, cng} = ObjectValidator.validate(missing_object, [])
 +
 +      assert {:object, {"can't find object", []}} in cng.errors
 +      assert length(cng.errors) == 1
 +    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
++      assert length(cng.errors) == 1
+     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 f41a7f3c1a935860bcdb2303646577a4b7725bcd,a9598d7b36ecbdc2c21507c177fd7aa2a4b52710..aafc450d30879744dc69e12dea305e02d98fb1ef
@@@ -16,107 -18,60 +18,160 @@@ 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 "Undo objects" do
 +    setup do
 +      poster = insert(:user)
 +      user = insert(:user)
 +      {:ok, post} = CommonAPI.post(poster, %{"status" => "hey"})
 +      {:ok, like} = CommonAPI.favorite(user, post.id)
 +      {:ok, reaction, _} = CommonAPI.react_with_emoji(post.id, user, "👍")
 +      {:ok, announce, _} = CommonAPI.repeat(post.id, user)
 +      {:ok, block} = ActivityPub.block(user, poster)
 +      User.block(user, poster)
 +
 +      {:ok, undo_data, _meta} = Builder.undo(user, like)
 +      {:ok, like_undo, _meta} = ActivityPub.persist(undo_data, local: true)
 +
 +      {:ok, undo_data, _meta} = Builder.undo(user, reaction)
 +      {:ok, reaction_undo, _meta} = ActivityPub.persist(undo_data, local: true)
 +
 +      {:ok, undo_data, _meta} = Builder.undo(user, announce)
 +      {:ok, announce_undo, _meta} = ActivityPub.persist(undo_data, local: true)
 +
 +      {:ok, undo_data, _meta} = Builder.undo(user, block)
 +      {:ok, block_undo, _meta} = ActivityPub.persist(undo_data, local: true)
 +
 +      %{
 +        like_undo: like_undo,
 +        post: post,
 +        like: like,
 +        reaction_undo: reaction_undo,
 +        reaction: reaction,
 +        announce_undo: announce_undo,
 +        announce: announce,
 +        block_undo: block_undo,
 +        block: block,
 +        poster: poster,
 +        user: user
 +      }
 +    end
 +
 +    test "deletes the original block", %{block_undo: block_undo, block: block} do
 +      {:ok, _block_undo, _} = SideEffects.handle(block_undo)
 +      refute Activity.get_by_id(block.id)
 +    end
 +
 +    test "unblocks the blocked user", %{block_undo: block_undo, block: block} do
 +      blocker = User.get_by_ap_id(block.data["actor"])
 +      blocked = User.get_by_ap_id(block.data["object"])
 +
 +      {:ok, _block_undo, _} = SideEffects.handle(block_undo)
 +      refute User.blocks?(blocker, blocked)
 +    end
 +
 +    test "an announce undo removes the announce from the object", %{
 +      announce_undo: announce_undo,
 +      post: post
 +    } do
 +      {:ok, _announce_undo, _} = SideEffects.handle(announce_undo)
 +
 +      object = Object.get_by_ap_id(post.data["object"])
 +
 +      assert object.data["announcement_count"] == 0
 +      assert object.data["announcements"] == []
 +    end
 +
 +    test "deletes the original announce", %{announce_undo: announce_undo, announce: announce} do
 +      {:ok, _announce_undo, _} = SideEffects.handle(announce_undo)
 +      refute Activity.get_by_id(announce.id)
 +    end
 +
 +    test "a reaction undo removes the reaction from the object", %{
 +      reaction_undo: reaction_undo,
 +      post: post
 +    } do
 +      {:ok, _reaction_undo, _} = SideEffects.handle(reaction_undo)
 +
 +      object = Object.get_by_ap_id(post.data["object"])
 +
 +      assert object.data["reaction_count"] == 0
 +      assert object.data["reactions"] == []
 +    end
 +
 +    test "deletes the original reaction", %{reaction_undo: reaction_undo, reaction: reaction} do
 +      {:ok, _reaction_undo, _} = SideEffects.handle(reaction_undo)
 +      refute Activity.get_by_id(reaction.id)
 +    end
 +
 +    test "a like undo removes the like from the object", %{like_undo: like_undo, post: post} do
 +      {:ok, _like_undo, _} = SideEffects.handle(like_undo)
 +
 +      object = Object.get_by_ap_id(post.data["object"])
 +
 +      assert object.data["like_count"] == 0
 +      assert object.data["likes"] == []
 +    end
 +
 +    test "deletes the original like", %{like_undo: like_undo, like: like} do
 +      {:ok, _like_undo, _} = SideEffects.handle(like_undo)
 +      refute Activity.get_by_id(like.id)
 +    end
 +  end
 +
    describe "like objects" do
      setup do
        poster = insert(:user)
index a315ff42d460b6b7afb5f1513036007a3fba13d7,6d43c3365e49daa4d596c4635c486fad66d3364e..ae5d3bf9254746c3c4927e2d774427f33b5206a3
@@@ -685,84 -766,60 +685,138 @@@ defmodule Pleroma.Web.ActivityPub.Trans
        assert user.locked == true
      end
  
 +    test "it works for incoming deletes" do
 +      activity = insert(:note_activity)
 +      deleting_user = insert(:user)
 +
 +      data =
 +        File.read!("test/fixtures/mastodon-delete.json")
 +        |> Poison.decode!()
 +
 +      object =
 +        data["object"]
 +        |> Map.put("id", activity.data["object"])
 +
 +      data =
 +        data
 +        |> Map.put("object", object)
 +        |> Map.put("actor", deleting_user.ap_id)
 +
 +      {:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} =
 +        Transmogrifier.handle_incoming(data)
 +
 +      assert id == data["id"]
 +      refute Activity.get_by_id(activity.id)
 +      assert actor == deleting_user.ap_id
 +    end
 +
 +    test "it fails for incoming deletes with spoofed origin" do
 +      activity = insert(:note_activity)
 +
 +      data =
 +        File.read!("test/fixtures/mastodon-delete.json")
 +        |> Poison.decode!()
 +
 +      object =
 +        data["object"]
 +        |> Map.put("id", activity.data["object"])
 +
 +      data =
 +        data
 +        |> Map.put("object", object)
 +
 +      assert capture_log(fn ->
 +               :error = Transmogrifier.handle_incoming(data)
 +             end) =~
 +               "[error] Could not decode user at fetch http://mastodon.example.org/users/gargron, {:error, :nxdomain}"
 +
 +      assert Activity.get_by_id(activity.id)
 +    end
 +
 +    @tag capture_log: true
 +    test "it works for incoming user deletes" do
 +      %{ap_id: ap_id} =
 +        insert(:user, ap_id: "http://mastodon.example.org/users/admin", local: false)
 +
 +      data =
 +        File.read!("test/fixtures/mastodon-delete-user.json")
 +        |> Poison.decode!()
 +
 +      {:ok, _} = Transmogrifier.handle_incoming(data)
 +      ObanHelpers.perform_all()
 +
 +      refute User.get_cached_by_ap_id(ap_id)
 +    end
 +
 +    test "it fails for incoming user deletes with spoofed origin" do
 +      %{ap_id: ap_id} = insert(:user)
 +
 +      data =
 +        File.read!("test/fixtures/mastodon-delete-user.json")
 +        |> Poison.decode!()
 +        |> Map.put("actor", ap_id)
 +
 +      assert capture_log(fn ->
 +               assert :error == Transmogrifier.handle_incoming(data)
 +             end) =~ "Object containment failed"
 +
 +      assert User.get_cached_by_ap_id(ap_id)
 +    end
 +
+     test "it works for incoming unannounces with an existing notice" do
+       user = insert(:user)
+       {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
+       announce_data =
+         File.read!("test/fixtures/mastodon-announce.json")
+         |> Poison.decode!()
+         |> Map.put("object", activity.data["object"])
+       {:ok, %Activity{data: announce_data, local: false}} =
+         Transmogrifier.handle_incoming(announce_data)
+       data =
+         File.read!("test/fixtures/mastodon-undo-announce.json")
+         |> Poison.decode!()
+         |> Map.put("object", announce_data)
+         |> Map.put("actor", announce_data["actor"])
+       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+       assert data["type"] == "Undo"
+       assert object_data = data["object"]
+       assert object_data["type"] == "Announce"
+       assert object_data["object"] == activity.data["object"]
+       assert object_data["id"] ==
+                "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
+     end
+     test "it works for incomming unfollows with an existing follow" do
+       user = insert(:user)
+       follow_data =
+         File.read!("test/fixtures/mastodon-follow-activity.json")
+         |> Poison.decode!()
+         |> Map.put("object", user.ap_id)
+       {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data)
+       data =
+         File.read!("test/fixtures/mastodon-unfollow-activity.json")
+         |> Poison.decode!()
+         |> Map.put("object", follow_data)
+       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+       assert data["type"] == "Undo"
+       assert data["object"]["type"] == "Follow"
+       assert data["object"]["object"] == user.ap_id
+       assert data["actor"] == "http://mastodon.example.org/users/admin"
+       refute User.following?(User.get_cached_by_ap_id(data["actor"]), user)
+     end
      test "it works for incoming follows to locked account" do
        pending_follower = insert(:user, ap_id: "http://mastodon.example.org/users/admin")
        user = insert(:user, locked: true)