From 2d2b50cccaa99b551b88be36a4b33b271300d3c8 Mon Sep 17 00:00:00 2001
From: Sergey Suprunenko <suprunenko.s@gmail.com>
Date: Wed, 10 Jul 2019 05:16:08 +0000
Subject: [PATCH] Send and handle "Delete" activity for deleted users

---
 lib/pleroma/user.ex                           |  6 +-
 lib/pleroma/web/activity_pub/activity_pub.ex  | 13 +++
 .../web/activity_pub/transmogrifier.ex        | 27 +++++-
 test/fixtures/mastodon-delete-user.json       | 24 +++++
 test/user_test.exs                            | 92 +++++++++++++------
 test/web/activity_pub/transmogrifier_test.exs | 24 +++++
 6 files changed, 152 insertions(+), 34 deletions(-)
 create mode 100644 test/fixtures/mastodon-delete-user.json

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index d03810d1a..034c414bf 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -937,6 +937,8 @@ def delete(%User{} = user),
 
   @spec perform(atom(), User.t()) :: {:ok, User.t()}
   def perform(:delete, %User{} = user) do
+    {:ok, _user} = ActivityPub.delete(user)
+
     # Remove all relationships
     {:ok, followers} = User.get_followers(user)
 
@@ -953,8 +955,8 @@ def perform(:delete, %User{} = user) do
     end)
 
     delete_user_activities(user)
-
-    {:ok, _user} = Repo.delete(user)
+    invalidate_cache(user)
+    Repo.delete(user)
   end
 
   @spec perform(atom(), User.t()) :: {:ok, User.t()}
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 55315d66e..41b55bbab 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -405,6 +405,19 @@ def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
     end
   end
 
+  def delete(%User{ap_id: ap_id, follower_address: follower_address} = user) do
+    with data <- %{
+           "to" => [follower_address],
+           "type" => "Delete",
+           "actor" => ap_id,
+           "object" => %{"type" => "Person", "id" => ap_id}
+         },
+         {:ok, activity} <- insert(data, true, true),
+         :ok <- maybe_federate(activity) do
+      {:ok, user}
+    end
+  end
+
   def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do
     user = User.get_cached_by_ap_id(actor)
     to = (object.data["to"] || []) ++ (object.data["cc"] || [])
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 543d4bb7d..e34fe6611 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -641,7 +641,7 @@ def handle_incoming(
   # an error or a tombstone.  This would allow us to verify that a deletion actually took
   # place.
   def handle_incoming(
-        %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data,
+        %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data,
         _options
       ) do
     object_id = Utils.get_ap_id(object_id)
@@ -653,7 +653,30 @@ def handle_incoming(
          {:ok, activity} <- ActivityPub.delete(object, false) do
       {:ok, activity}
     else
-      _e -> :error
+      nil ->
+        case User.get_cached_by_ap_id(object_id) do
+          %User{ap_id: ^actor} = user ->
+            {:ok, followers} = User.get_followers(user)
+
+            Enum.each(followers, fn follower ->
+              User.unfollow(follower, user)
+            end)
+
+            {:ok, friends} = User.get_friends(user)
+
+            Enum.each(friends, fn followed ->
+              User.unfollow(user, followed)
+            end)
+
+            User.invalidate_cache(user)
+            Repo.delete(user)
+
+          nil ->
+            :error
+        end
+
+      _e ->
+        :error
     end
   end
 
diff --git a/test/fixtures/mastodon-delete-user.json b/test/fixtures/mastodon-delete-user.json
new file mode 100644
index 000000000..f19088fec
--- /dev/null
+++ b/test/fixtures/mastodon-delete-user.json
@@ -0,0 +1,24 @@
+{
+  "type": "Delete",
+  "object": {
+    "type": "Person",
+    "id": "http://mastodon.example.org/users/admin",
+    "atomUri": "http://mastodon.example.org/users/admin"
+  },
+  "id": "http://mastodon.example.org/users/admin#delete",
+  "actor": "http://mastodon.example.org/users/admin",
+  "@context": [
+    {
+      "toot": "http://joinmastodon.org/ns#",
+      "sensitive": "as:sensitive",
+      "ostatus": "http://ostatus.org#",
+      "movedTo": "as:movedTo",
+      "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+      "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+      "conversation": "ostatus:conversation",
+      "atomUri": "ostatus:atomUri",
+      "Hashtag": "as:Hashtag",
+      "Emoji": "toot:Emoji"
+    }
+  ]
+}
diff --git a/test/user_test.exs b/test/user_test.exs
index 0f27d73f7..62be79b4f 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -14,6 +14,7 @@ defmodule Pleroma.UserTest do
   use Pleroma.DataCase
 
   import Pleroma.Factory
+  import Mock
 
   setup_all do
     Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
@@ -915,49 +916,80 @@ test "hide a user's statuses from timelines and notifications" do
     end
   end
 
-  test ".delete_user_activities deletes all create activities" do
-    user = insert(:user)
+  describe "delete" do
+    setup do
+      {:ok, user} = insert(:user) |> User.set_cache()
 
-    {:ok, activity} = CommonAPI.post(user, %{"status" => "2hu"})
+      [user: user]
+    end
 
-    {:ok, _} = User.delete_user_activities(user)
+    test ".delete_user_activities deletes all create activities", %{user: user} do
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "2hu"})
 
-    # TODO: Remove favorites, repeats, delete activities.
-    refute Activity.get_by_id(activity.id)
-  end
+      {:ok, _} = User.delete_user_activities(user)
 
-  test ".delete deactivates a user, all follow relationships and all activities" do
-    user = insert(:user)
-    follower = insert(:user)
+      # TODO: Remove favorites, repeats, delete activities.
+      refute Activity.get_by_id(activity.id)
+    end
 
-    {:ok, follower} = User.follow(follower, user)
+    test "it deletes a user, all follow relationships and all activities", %{user: user} do
+      follower = insert(:user)
+      {:ok, follower} = User.follow(follower, user)
 
-    {:ok, activity} = CommonAPI.post(user, %{"status" => "2hu"})
-    {:ok, activity_two} = CommonAPI.post(follower, %{"status" => "3hu"})
+      object = insert(:note, user: user)
+      activity = insert(:note_activity, user: user, note: object)
 
-    {:ok, like, _} = CommonAPI.favorite(activity_two.id, user)
-    {:ok, like_two, _} = CommonAPI.favorite(activity.id, follower)
-    {:ok, repeat, _} = CommonAPI.repeat(activity_two.id, user)
+      object_two = insert(:note, user: follower)
+      activity_two = insert(:note_activity, user: follower, note: object_two)
 
-    {:ok, _} = User.delete(user)
+      {:ok, like, _} = CommonAPI.favorite(activity_two.id, user)
+      {:ok, like_two, _} = CommonAPI.favorite(activity.id, follower)
+      {:ok, repeat, _} = CommonAPI.repeat(activity_two.id, user)
 
-    follower = User.get_cached_by_id(follower.id)
+      {:ok, _} = User.delete(user)
 
-    refute User.following?(follower, user)
-    refute User.get_by_id(user.id)
+      follower = User.get_cached_by_id(follower.id)
 
-    user_activities =
-      user.ap_id
-      |> Activity.query_by_actor()
-      |> Repo.all()
-      |> Enum.map(fn act -> act.data["type"] end)
+      refute User.following?(follower, user)
+      refute User.get_by_id(user.id)
+      assert {:ok, nil} == Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
 
-    assert Enum.all?(user_activities, fn act -> act in ~w(Delete Undo) end)
+      user_activities =
+        user.ap_id
+        |> Activity.query_by_actor()
+        |> Repo.all()
+        |> Enum.map(fn act -> act.data["type"] end)
 
-    refute Activity.get_by_id(activity.id)
-    refute Activity.get_by_id(like.id)
-    refute Activity.get_by_id(like_two.id)
-    refute Activity.get_by_id(repeat.id)
+      assert Enum.all?(user_activities, fn act -> act in ~w(Delete Undo) end)
+
+      refute Activity.get_by_id(activity.id)
+      refute Activity.get_by_id(like.id)
+      refute Activity.get_by_id(like_two.id)
+      refute Activity.get_by_id(repeat.id)
+    end
+
+    test_with_mock "it sends out User Delete activity",
+                   %{user: user},
+                   Pleroma.Web.ActivityPub.Publisher,
+                   [:passthrough],
+                   [] do
+      config_path = [:instance, :federating]
+      initial_setting = Pleroma.Config.get(config_path)
+      Pleroma.Config.put(config_path, true)
+
+      {:ok, follower} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin")
+      {:ok, _} = User.follow(follower, user)
+
+      {:ok, _user} = User.delete(user)
+
+      assert called(
+               Pleroma.Web.ActivityPub.Publisher.publish_one(%{
+                 inbox: "http://mastodon.example.org/inbox"
+               })
+             )
+
+      Pleroma.Config.put(config_path, initial_setting)
+    end
   end
 
   test "get_public_key_for_ap_id fetches a user that's not in the db" do
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index d152169b8..825e99879 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -553,6 +553,30 @@ test "it fails for incoming deletes with spoofed origin" do
       assert Activity.get_by_id(activity.id)
     end
 
+    test "it works for incoming user deletes" do
+      %{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin")
+
+      data =
+        File.read!("test/fixtures/mastodon-delete-user.json")
+        |> Poison.decode!()
+
+      {:ok, _} = Transmogrifier.handle_incoming(data)
+
+      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 :error == Transmogrifier.handle_incoming(data)
+      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"})