Merge branch 'feature/1734-user-deletion' into 'develop'
authorlain <lain@soykaf.club>
Wed, 20 May 2020 11:43:49 +0000 (11:43 +0000)
committerlain <lain@soykaf.club>
Wed, 20 May 2020 11:43:49 +0000 (11:43 +0000)
User deletion

Closes #1734

See merge request pleroma/pleroma!2493

1  2 
lib/pleroma/user.ex
test/user_test.exs
test/web/activity_pub/side_effects_test.exs

diff --combined lib/pleroma/user.ex
index 6ca1e9a79b8e9ab46280fb71609ce4a724279906,278129ad2839a7d4f85991e888e19198d15c0460..e8013bf4035344a6b843d0bb83af43a1e994c259
@@@ -9,6 -9,7 +9,6 @@@ defmodule Pleroma.User d
    import Ecto.Query
    import Ecto, only: [assoc: 2]
  
 -  alias Comeonin.Pbkdf2
    alias Ecto.Multi
    alias Pleroma.Activity
    alias Pleroma.Config
    def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
      to = [actor | to]
  
 -    User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
 +    query = User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
 +
 +    query
      |> Repo.all()
    end
  
      BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
    end
  
+   defp delete_and_invalidate_cache(%User{} = user) do
+     invalidate_cache(user)
+     Repo.delete(user)
+   end
+   defp delete_or_deactivate(%User{local: false} = user), do: delete_and_invalidate_cache(user)
+   defp delete_or_deactivate(%User{local: true} = user) do
+     status = account_status(user)
+     if status == :confirmation_pending do
+       delete_and_invalidate_cache(user)
+     else
+       user
+       |> change(%{deactivated: true, email: nil})
+       |> update_and_set_cache()
+     end
+   end
    def perform(:force_password_reset, user), do: force_password_reset(user)
  
    @spec perform(atom(), User.t()) :: {:ok, User.t()}
  
      delete_user_activities(user)
  
-     if user.local do
-       user
-       |> change(%{deactivated: true, email: nil})
-       |> update_and_set_cache()
-     else
-       invalidate_cache(user)
-       Repo.delete(user)
-     end
+     delete_or_deactivate(user)
    end
  
    def perform(:deactivate_async, user, status), do: deactivate(user, status)
      |> Stream.run()
    end
  
 -  defp delete_activity(%{data: %{"type" => "Create", "object" => object}}, user) do
 -    {:ok, delete_data, _} = Builder.delete(user, object)
 +  defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
 +    with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
 +         {:ok, delete_data, _} <- Builder.delete(user, object) do
 +      Pipeline.common_pipeline(delete_data, local: user.local)
 +    else
 +      {:find_object, nil} ->
 +        # We have the create activity, but not the object, it was probably pruned.
 +        # Insert a tombstone and try again
 +        with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object),
 +             {:ok, _tombstone} <- Object.create(tombstone_data) do
 +          delete_activity(activity, user)
 +        end
  
 -    Pipeline.common_pipeline(delete_data, local: user.local)
 +      e ->
 +        Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
 +        Logger.error("Error: #{inspect(e)}")
 +    end
    end
  
    defp delete_activity(%{data: %{"type" => type}} = activity, user)
    defp put_password_hash(
           %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
         ) do
 -    change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
 +    change(changeset, password_hash: Pbkdf2.hash_pwd_salt(password))
    end
  
    defp put_password_hash(changeset), do: changeset
diff --combined test/user_test.exs
index 239d167994c0e9a92a0725b7a6c962311e67157e,96116fca67f3ad9006efa06912384bf6ab9c18ce..863e0106cfe0b84e27b43b9ef6274fb963556f17
@@@ -555,7 -555,6 +555,7 @@@ defmodule Pleroma.UserTest d
        assert user == fetched_user
      end
  
 +    @tag capture_log: true
      test "returns nil if no user could be fetched" do
        {:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la")
        assert fetched_user == "not found nonexistant@social.heldscal.la"
        actor = insert(:user)
        user = insert(:user, local: true)
  
 -      {:ok, activity} = CommonAPI.post(actor, %{"status" => "hello"})
 +      {:ok, activity} = CommonAPI.post(actor, %{status: "hello"})
        {:ok, announce, _} = CommonAPI.repeat(activity.id, user)
  
        recipients = User.get_recipients_from_activity(announce)
  
        {:ok, activity} =
          CommonAPI.post(actor, %{
 -          "status" => "hey @#{addressed.nickname} @#{addressed_remote.nickname}"
 +          status: "hey @#{addressed.nickname} @#{addressed_remote.nickname}"
          })
  
        assert Enum.map([actor, addressed], & &1.ap_id) --
  
        {:ok, activity} =
          CommonAPI.post(actor, %{
 -          "status" => "hey @#{addressed.nickname}"
 +          status: "hey @#{addressed.nickname}"
          })
  
        assert Enum.map([actor, addressed], & &1.ap_id) --
  
        {:ok, user2} = User.follow(user2, user)
  
 -      {:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{user2.nickname}"})
 +      {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{user2.nickname}"})
  
        activity = Repo.preload(activity, :bookmark)
  
      setup do: clear_config([:instance, :federating])
  
      test ".delete_user_activities deletes all create activities", %{user: user} do
 -      {:ok, activity} = CommonAPI.post(user, %{"status" => "2hu"})
 +      {:ok, activity} = CommonAPI.post(user, %{status: "2hu"})
  
        User.delete_user_activities(user)
  
      end
    end
  
+   describe "delete/1 when confirmation is pending" do
+     setup do
+       user = insert(:user, confirmation_pending: true)
+       {:ok, user: user}
+     end
+     test "deletes user from database when activation required", %{user: user} do
+       clear_config([:instance, :account_activation_required], true)
+       {:ok, job} = User.delete(user)
+       {:ok, _} = ObanHelpers.perform(job)
+       refute User.get_cached_by_id(user.id)
+       refute User.get_by_id(user.id)
+     end
+     test "deactivates user when activation is not required", %{user: user} do
+       clear_config([:instance, :account_activation_required], false)
+       {:ok, job} = User.delete(user)
+       {:ok, _} = ObanHelpers.perform(job)
+       assert %{deactivated: true} = User.get_cached_by_id(user.id)
+       assert %{deactivated: true} = User.get_by_id(user.id)
+     end
+   end
    test "get_public_key_for_ap_id fetches a user that's not in the db" do
      assert {:ok, _key} = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin")
    end
  
          {:ok, _} =
            CommonAPI.post(user, %{
 -            "status" => "hey @#{to.nickname}"
 +            status: "hey @#{to.nickname}"
            })
        end)
  
        Enum.each(recipients, fn to ->
          {:ok, _} =
            CommonAPI.post(sender, %{
 -            "status" => "hey @#{to.nickname}"
 +            status: "hey @#{to.nickname}"
            })
  
          {:ok, _} =
            CommonAPI.post(sender, %{
 -            "status" => "hey again @#{to.nickname}"
 +            status: "hey again @#{to.nickname}"
            })
        end)
  
index 797f00d0880a9d4a6cc5f8959e90c07d53d4f298,5c06dc864fccfa48a9dd79f1b1ff8004defb83db..a46254a05b358328ab5852a8d62dfa4962233940
@@@ -25,58 -25,17 +25,58 @@@ defmodule Pleroma.Web.ActivityPub.SideE
        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})
 +      {:ok, op} = CommonAPI.post(other_user, %{status: "big oof"})
 +      {:ok, post} = CommonAPI.post(user, %{status: "hey", in_reply_to_id: op})
 +      {:ok, favorite} = CommonAPI.favorite(user, post.id)
        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}
 +
 +      %{
 +        user: user,
 +        delete: delete,
 +        post: post,
 +        object: object,
 +        delete_user: delete_user,
 +        op: op,
 +        favorite: favorite
 +      }
      end
  
      test "it handles object deletions", %{
 +      delete: delete,
 +      post: post,
 +      object: object,
 +      user: user,
 +      op: op,
 +      favorite: favorite
 +    } 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)
 +      refute Activity.get_by_id(favorite.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 object deletions when the object itself has been pruned", %{
        delete: delete,
        post: post,
        object: object,
        poster = insert(:user)
        user = insert(:user)
  
 -      {:ok, post} = CommonAPI.post(poster, %{"status" => "hey"})
 +      {: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)
      end
    end
  
+   describe "delete users with confirmation pending" do
+     setup do
+       user = insert(:user, confirmation_pending: true)
+       {:ok, delete_user_data, _meta} = Builder.delete(user, user.ap_id)
+       {:ok, delete_user, _meta} = ActivityPub.persist(delete_user_data, local: true)
+       {:ok, delete: delete_user, user: user}
+     end
+     test "when activation is not required", %{delete: delete, user: user} do
+       clear_config([:instance, :account_activation_required], false)
+       {:ok, _, _} = SideEffects.handle(delete)
+       ObanHelpers.perform_all()
+       assert User.get_cached_by_id(user.id).deactivated
+     end
+     test "when activation is required", %{delete: delete, user: user} do
+       clear_config([:instance, :account_activation_required], true)
+       {:ok, _, _} = SideEffects.handle(delete)
+       ObanHelpers.perform_all()
+       refute User.get_cached_by_id(user.id)
+     end
+   end
    describe "Undo objects" do
      setup do
        poster = insert(:user)
        user = insert(:user)
 -      {:ok, post} = CommonAPI.post(poster, %{"status" => "hey"})
 +      {: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)
      setup do
        poster = insert(:user)
        user = insert(:user)
 -      {:ok, post} = CommonAPI.post(poster, %{"status" => "hey"})
 +      {:ok, post} = CommonAPI.post(poster, %{status: "hey"})
  
        {:ok, like_data, _meta} = Builder.like(user, post.object)
        {:ok, like, _meta} = ActivityPub.persist(like_data, local: true)