Merge remote-tracking branch 'pleroma/develop' into feature/disable-account
authorEgor Kislitsyn <egor@kislitsyn.com>
Tue, 14 May 2019 11:15:56 +0000 (18:15 +0700)
committerEgor Kislitsyn <egor@kislitsyn.com>
Tue, 14 May 2019 11:15:56 +0000 (18:15 +0700)
13 files changed:
config/config.exs
docs/api/pleroma_api.md
lib/pleroma/activity.ex
lib/pleroma/notification.ex
lib/pleroma/user.ex
lib/pleroma/user/query.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/router.ex
lib/pleroma/web/twitter_api/controllers/util_controller.ex
lib/pleroma/web/twitter_api/twitter_api.ex
priv/repo/migrations/20190411094120_add_index_on_user_info_deactivated.exs [new file with mode: 0644]
test/user_test.exs
test/web/twitter_api/util_controller_test.exs

index 45034a77552d191a233701cbe1da128c659af793..8d44c96def995da04de471080d75d29f4b74033f 100644 (file)
@@ -424,7 +424,8 @@ config :pleroma_job_queue, :queues,
   mailer: 10,
   transmogrifier: 20,
   scheduled_activities: 10,
-  background: 5
+  background: 5,
+  user: 10
 
 config :pleroma, :fetch_initial_posts,
   enabled: false,
index 190846de937c963bf2920cb9f3700b3d911cb55c..dd0b6ca733dc79e47ccc75c4ef9aa7c4956d7ebc 100644 (file)
@@ -61,6 +61,15 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
 * Response: JSON. Returns `{"status": "success"}` if the deletion was successful, `{"error": "[error message]"}` otherwise
 * Example response: `{"error": "Invalid password."}`
 
+## `/api/pleroma/disable_account`
+### Disable an account
+* Method `POST`
+* Authentication: required
+* Params:
+    * `password`: user's password
+* Response: JSON. Returns `{"status": "success"}` if the account was successfully disabled, `{"error": "[error message]"}` otherwise
+* Example response: `{"error": "Invalid password."}`
+
 ## `/api/account/register`
 ### Register a new user
 * Method `POST`
index c121e800f6c206ca3bfd989282965d42ffd30029..4a09194786f3f58ca4c998fd67d99cb0a8dc4f40 100644 (file)
@@ -132,7 +132,10 @@ defmodule Pleroma.Activity do
   end
 
   def get_by_id(id) do
-    Repo.get(Activity, id)
+    Activity
+    |> where([a], a.id == ^id)
+    |> restrict_deactivated_users()
+    |> Repo.one()
   end
 
   def get_by_id_with_object(id) do
@@ -200,6 +203,7 @@ defmodule Pleroma.Activity do
 
   def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
     create_by_object_ap_id(ap_id)
+    |> restrict_deactivated_users()
     |> Repo.one()
   end
 
@@ -314,4 +318,14 @@ defmodule Pleroma.Activity do
   def query_by_actor(actor) do
     from(a in Activity, where: a.actor == ^actor)
   end
+
+  def restrict_deactivated_users(query) do
+    from(activity in query,
+      where:
+        fragment(
+          "? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
+          activity.actor
+        )
+    )
+  end
 end
index dd274cf6b22b8bdb2412b79d759f1bd7189e2239..8442643072cf5c9bc56bda9d0ce2d0116643b576 100644 (file)
@@ -33,6 +33,13 @@ defmodule Pleroma.Notification do
   def for_user_query(user) do
     Notification
     |> where(user_id: ^user.id)
+    |> where(
+      [n, a],
+      fragment(
+        "? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
+        a.actor
+      )
+    )
     |> join(:inner, [n], activity in assoc(n, :activity))
     |> join(:left, [n, a], object in Object,
       on:
index 474de9ba5c7ec716c99fd0ed4ec55c878dd36fba..cf378d46772d73f41bee44e5dcbb3547e3f488a1 100644 (file)
@@ -105,10 +105,8 @@ defmodule Pleroma.User do
   def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
 
   def user_info(%User{} = user) do
-    oneself = if user.local, do: 1, else: 0
-
     %{
-      following_count: length(user.following) - oneself,
+      following_count: following_count(user),
       note_count: user.info.note_count,
       follower_count: user.info.follower_count,
       locked: user.info.locked,
@@ -117,6 +115,20 @@ defmodule Pleroma.User do
     }
   end
 
+  defp restrict_deactivated(query) do
+    from(u in query,
+      where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
+    )
+  end
+
+  def following_count(%User{following: []}), do: 0
+
+  def following_count(%User{} = user) do
+    user
+    |> get_friends_query()
+    |> Repo.aggregate(:count, :id)
+  end
+
   def remote_user_creation(params) do
     params =
       params
@@ -255,7 +267,7 @@ defmodule Pleroma.User do
     candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
 
     autofollowed_users =
-      User.Query.build(%{nickname: candidates, local: true})
+      User.Query.build(%{nickname: candidates, local: true, deactivated: false})
       |> Repo.all()
 
     follow_all(user, autofollowed_users)
@@ -576,7 +588,7 @@ defmodule Pleroma.User do
 
   @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
   def get_followers_query(%User{} = user, nil) do
-    User.Query.build(%{followers: user})
+    User.Query.build(%{followers: user, deactivated: false})
   end
 
   def get_followers_query(user, page) do
@@ -601,7 +613,7 @@ defmodule Pleroma.User do
 
   @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
   def get_friends_query(%User{} = user, nil) do
-    User.Query.build(%{friends: user})
+    User.Query.build(%{friends: user, deactivated: false})
   end
 
   def get_friends_query(user, page) do
@@ -691,16 +703,16 @@ defmodule Pleroma.User do
 
     info_cng = User.Info.set_note_count(user.info, note_count)
 
-    cng =
-      change(user)
-      |> put_embed(:info, info_cng)
-
-    update_and_set_cache(cng)
+    user
+    |> change()
+    |> put_embed(:info, info_cng)
+    |> update_and_set_cache()
   end
 
   def update_follower_count(%User{} = user) do
     follower_count_query =
-      User.Query.build(%{followers: user}) |> select([u], %{count: count(u.id)})
+      User.Query.build(%{followers: user, deactivated: false})
+      |> select([u], %{count: count(u.id)})
 
     User
     |> where(id: ^user.id)
@@ -725,7 +737,7 @@ defmodule Pleroma.User do
 
   @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
   def get_users_from_set(ap_ids, local_only \\ true) do
-    criteria = %{ap_id: ap_ids}
+    criteria = %{ap_id: ap_ids, deactivated: false}
     criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
 
     User.Query.build(criteria)
@@ -734,7 +746,7 @@ defmodule Pleroma.User do
 
   @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
   def get_recipients_from_activity(%Activity{recipients: to}) do
-    User.Query.build(%{recipients_from_activity: to, local: true})
+    User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
     |> Repo.all()
   end
 
@@ -832,6 +844,7 @@ defmodule Pleroma.User do
           ^processed_query
         )
     )
+    |> restrict_deactivated()
   end
 
   defp trigram_search_subquery(term) do
@@ -850,6 +863,7 @@ defmodule Pleroma.User do
       },
       where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
     )
+    |> restrict_deactivated()
   end
 
   def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
@@ -999,19 +1013,19 @@ defmodule Pleroma.User do
 
   @spec muted_users(User.t()) :: [User.t()]
   def muted_users(user) do
-    User.Query.build(%{ap_id: user.info.mutes})
+    User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
     |> Repo.all()
   end
 
   @spec blocked_users(User.t()) :: [User.t()]
   def blocked_users(user) do
-    User.Query.build(%{ap_id: user.info.blocks})
+    User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
     |> Repo.all()
   end
 
   @spec subscribers(User.t()) :: [User.t()]
   def subscribers(user) do
-    User.Query.build(%{ap_id: user.info.subscribers})
+    User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
     |> Repo.all()
   end
 
@@ -1039,14 +1053,27 @@ defmodule Pleroma.User do
     update_and_set_cache(cng)
   end
 
+  def deactivate_async(user, status \\ true) do
+    PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
+  end
+
+  def perform(:deactivate_async, user, status), do: deactivate(user, status)
+
   def deactivate(%User{} = user, status \\ true) do
     info_cng = User.Info.set_activation_status(user.info, status)
 
-    cng =
-      change(user)
-      |> put_embed(:info, info_cng)
+    with {:ok, friends} <- User.get_friends(user),
+         {:ok, followers} <- User.get_followers(user),
+         {:ok, user} <-
+           user
+           |> change()
+           |> put_embed(:info, info_cng)
+           |> update_and_set_cache() do
+      Enum.each(followers, &invalidate_cache(&1))
+      Enum.each(friends, &update_follower_count(&1))
 
-    update_and_set_cache(cng)
+      {:ok, user}
+    end
   end
 
   def update_notification_settings(%User{} = user, settings \\ %{}) do
@@ -1320,7 +1347,7 @@ defmodule Pleroma.User do
 
   @spec all_superusers() :: [User.t()]
   def all_superusers do
-    User.Query.build(%{super_users: true, local: true})
+    User.Query.build(%{super_users: true, local: true, deactivated: false})
     |> Repo.all()
   end
 
index 2dfe5ce92c4b154099890d15c0220884d3a7d4a5..3873ef80c15c007a208c54c18fcf8c0475c5337f 100644 (file)
@@ -118,7 +118,13 @@ defmodule Pleroma.User.Query do
     |> where([u], not is_nil(u.nickname))
   end
 
-  defp compose_query({:deactivated, _}, query) do
+  defp compose_query({:deactivated, false}, query) do
+    from(u in query,
+      where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
+    )
+  end
+
+  defp compose_query({:deactivated, true}, query) do
     where(query, [u], fragment("?->'deactivated' @> 'true'", u.info))
     |> where([u], not is_nil(u.nickname))
   end
index 11777c220cf522318a5150b70386c8f310bfcbef..9a137d8de8bb8abb2ee888565db7d228b1a6e669 100644 (file)
@@ -854,6 +854,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     |> restrict_reblogs(opts)
     |> restrict_pinned(opts)
     |> restrict_muted_reblogs(opts)
+    |> Activity.restrict_deactivated_users()
   end
 
   def fetch_activities(recipients, opts \\ %{}) do
index 51146d010606fd0734a221241ca91cbb0e918c55..80af0afe17f8011f651bf9fae252314b60450921 100644 (file)
@@ -215,6 +215,7 @@ defmodule Pleroma.Web.Router do
       post("/change_password", UtilController, :change_password)
       post("/delete_account", UtilController, :delete_account)
       put("/notification_settings", UtilController, :update_notificaton_settings)
+      post("/disable_account", UtilController, :disable_account)
     end
 
     scope [] do
index c03f8ab3a9e4ab7b4c3a920c8c3a45d94119a638..7b7fd912b367e3b386c0ed0fa838b3a8e900401b 100644 (file)
@@ -360,6 +360,17 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
     end
   end
 
+  def disable_account(%{assigns: %{user: user}} = conn, params) do
+    case CommonAPI.Utils.confirm_current_password(user, params["password"]) do
+      {:ok, user} ->
+        User.deactivate_async(user)
+        json(conn, %{status: "success"})
+
+      {:error, msg} ->
+        json(conn, %{error: msg})
+    end
+  end
+
   def captcha(conn, _params) do
     json(conn, Pleroma.Captcha.new())
   end
index 1362ef57cf81894c2251721e98511844a7733eae..41e1c287744b48813b0f1a582b420e4855adf341 100644 (file)
@@ -236,12 +236,15 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
   def get_user(user \\ nil, params) do
     case params do
       %{"user_id" => user_id} ->
-        case target = User.get_cached_by_nickname_or_id(user_id) do
+        case User.get_cached_by_nickname_or_id(user_id) do
           nil ->
             {:error, "No user with such user_id"}
 
-          _ ->
-            {:ok, target}
+          %User{info: %{deactivated: true}} ->
+            {:error, "User has been disabled"}
+
+          user ->
+            {:ok, user}
         end
 
       %{"screen_name" => nickname} ->
diff --git a/priv/repo/migrations/20190411094120_add_index_on_user_info_deactivated.exs b/priv/repo/migrations/20190411094120_add_index_on_user_info_deactivated.exs
new file mode 100644 (file)
index 0000000..d701dce
--- /dev/null
@@ -0,0 +1,7 @@
+defmodule Pleroma.Repo.Migrations.AddIndexOnUserInfoDeactivated do
+  use Ecto.Migration
+
+  def change do
+    create(index(:users, ["(info->'deactivated')"], name: :users_deactivated_index, using: :gin))
+  end
+end
index 60de0206e551552594cd59d05f827a9ebc072112..0b65e89e9bdceaffe3aa86f2477935c5ac6a209a 100644 (file)
@@ -8,6 +8,7 @@ defmodule Pleroma.UserTest do
   alias Pleroma.Object
   alias Pleroma.Repo
   alias Pleroma.User
+  alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.CommonAPI
 
   use Pleroma.DataCase
@@ -213,8 +214,8 @@ defmodule Pleroma.UserTest do
   test "fetches correct profile for nickname beginning with number" do
     # Use old-style integer ID to try to reproduce the problem
     user = insert(:user, %{id: 1080})
-    userwithnumbers = insert(:user, %{nickname: "#{user.id}garbage"})
-    assert userwithnumbers == User.get_cached_by_nickname_or_id(userwithnumbers.nickname)
+    user_with_numbers = insert(:user, %{nickname: "#{user.id}garbage"})
+    assert user_with_numbers == User.get_cached_by_nickname_or_id(user_with_numbers.nickname)
   end
 
   describe "user registration" do
@@ -816,13 +817,73 @@ defmodule Pleroma.UserTest do
     assert addressed in recipients
   end
 
-  test ".deactivate can de-activate then re-activate a user" do
-    user = insert(:user)
-    assert false == user.info.deactivated
-    {:ok, user} = User.deactivate(user)
-    assert true == user.info.deactivated
-    {:ok, user} = User.deactivate(user, false)
-    assert false == user.info.deactivated
+  describe ".deactivate" do
+    test "can de-activate then re-activate a user" do
+      user = insert(:user)
+      assert false == user.info.deactivated
+      {:ok, user} = User.deactivate(user)
+      assert true == user.info.deactivated
+      {:ok, user} = User.deactivate(user, false)
+      assert false == user.info.deactivated
+    end
+
+    test "hide a user from followers " do
+      user = insert(:user)
+      user2 = insert(:user)
+
+      {:ok, user} = User.follow(user, user2)
+      {:ok, _user} = User.deactivate(user)
+
+      info = User.get_cached_user_info(user2)
+
+      assert info.follower_count == 0
+      assert {:ok, []} = User.get_followers(user2)
+    end
+
+    test "hide a user from friends" do
+      user = insert(:user)
+      user2 = insert(:user)
+
+      {:ok, user2} = User.follow(user2, user)
+      assert User.following_count(user2) == 1
+
+      {:ok, _user} = User.deactivate(user)
+
+      info = User.get_cached_user_info(user2)
+
+      assert info.following_count == 0
+      assert User.following_count(user2) == 0
+      assert {:ok, []} = User.get_friends(user2)
+    end
+
+    test "hide a user's statuses from timelines and notifications" do
+      user = insert(:user)
+      user2 = insert(:user)
+
+      {:ok, user2} = User.follow(user2, user)
+
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{user2.nickname}"})
+
+      activity = Repo.preload(activity, :bookmark)
+
+      [notification] = Pleroma.Notification.for_user(user2)
+      assert notification.activity.id == activity.id
+
+      assert [activity] == ActivityPub.fetch_public_activities(%{}) |> Repo.preload(:bookmark)
+
+      assert [activity] ==
+               ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2})
+               |> ActivityPub.contain_timeline(user2)
+
+      {:ok, _user} = User.deactivate(user)
+
+      assert [] == ActivityPub.fetch_public_activities(%{})
+      assert [] == Pleroma.Notification.for_user(user2)
+
+      assert [] ==
+               ActivityPub.fetch_activities([user2.ap_id | user2.following], %{"user" => user2})
+               |> ActivityPub.contain_timeline(user2)
+    end
   end
 
   test ".delete_user_activities deletes all create activities" do
index 56474447b59cfa1cee948fffb39cb32829c48459..14a8225f097caac84a6c9de636e77357930fa36f 100644 (file)
@@ -251,4 +251,22 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
 
     assert conn.status in [200, 503]
   end
+
+  describe "POST /api/pleroma/disable_account" do
+    test "it returns HTTP 200", %{conn: conn} do
+      user = insert(:user)
+
+      response =
+        conn
+        |> assign(:user, user)
+        |> post("/api/pleroma/disable_account", %{"password" => "test"})
+        |> json_response(:ok)
+
+      assert response == %{"status" => "success"}
+
+      user = User.get_cached_by_id(user.id)
+
+      assert user.info.deactivated == true
+    end
+  end
 end