diff options
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | config/config.exs | 3 | ||||
| -rw-r--r-- | docs/api/pleroma_api.md | 9 | ||||
| -rw-r--r-- | lib/pleroma/activity.ex | 16 | ||||
| -rw-r--r-- | lib/pleroma/notification.ex | 7 | ||||
| -rw-r--r-- | lib/pleroma/user.ex | 71 | ||||
| -rw-r--r-- | lib/pleroma/user/query.ex | 6 | ||||
| -rw-r--r-- | lib/pleroma/web/activity_pub/activity_pub.ex | 1 | ||||
| -rw-r--r-- | lib/pleroma/web/router.ex | 1 | ||||
| -rw-r--r-- | lib/pleroma/web/twitter_api/controllers/util_controller.ex | 11 | ||||
| -rw-r--r-- | lib/pleroma/web/twitter_api/twitter_api.ex | 9 | ||||
| -rw-r--r-- | priv/repo/migrations/20190411094120_add_index_on_user_info_deactivated.exs | 7 | ||||
| -rw-r--r-- | test/user_test.exs | 79 | ||||
| -rw-r--r-- | test/web/twitter_api/util_controller_test.exs | 18 | 
14 files changed, 202 insertions, 37 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c0baa317..17e913648 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).  - Deps: Updated Ecto to 3.0.7  - Don't ship finmoji by default, they can be installed as an emoji pack  - Admin API: Move the user related API to `api/pleroma/admin/users` +- Hide deactivated users and their statuses  ### Fixed  - Added an FTS index on objects. Running `vacuum analyze` and setting a larger `work_mem` is recommended. diff --git a/config/config.exs b/config/config.exs index 45034a775..8d44c96de 100644 --- a/config/config.exs +++ b/config/config.exs @@ -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, diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md index 190846de9..dd0b6ca73 100644 --- a/docs/api/pleroma_api.md +++ b/docs/api/pleroma_api.md @@ -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` diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index c121e800f..4a0919478 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -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 diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index dd274cf6b..844264307 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -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: diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 474de9ba5..3eb684c3a 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -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 +  def 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 diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index 2dfe5ce92..ace9c05f2 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -118,7 +118,11 @@ 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 +    User.restrict_deactivated(query) +  end + +  defp compose_query({:deactivated, true}, query) do      where(query, [u], fragment("?->'deactivated' @> 'true'", u.info))      |> where([u], not is_nil(u.nickname))    end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 11777c220..9a137d8de 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -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 diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 51146d010..80af0afe1 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -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 diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index c03f8ab3a..7b7fd912b 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -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 diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 1362ef57c..41e1c2877 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -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 index 000000000..d701dcecc --- /dev/null +++ b/priv/repo/migrations/20190411094120_add_index_on_user_info_deactivated.exs @@ -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 diff --git a/test/user_test.exs b/test/user_test.exs index 60de0206e..0b65e89e9 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -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 diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index 56474447b..14a8225f0 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -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  | 
