Merge branch 'feature/disable-account' into 'develop'

[#694] allow users to disable their own account

See merge request pleroma/pleroma!895
This commit is contained in:
kaniini 2019-05-14 12:06:08 +00:00
commit 4e69d1239a
14 changed files with 202 additions and 37 deletions

View file

@ -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 - Deps: Updated Ecto to 3.0.7
- Don't ship finmoji by default, they can be installed as an emoji pack - 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` - Admin API: Move the user related API to `api/pleroma/admin/users`
- Hide deactivated users and their statuses
### Fixed ### Fixed
- Added an FTS index on objects. Running `vacuum analyze` and setting a larger `work_mem` is recommended. - Added an FTS index on objects. Running `vacuum analyze` and setting a larger `work_mem` is recommended.

View file

@ -424,7 +424,8 @@
mailer: 10, mailer: 10,
transmogrifier: 20, transmogrifier: 20,
scheduled_activities: 10, scheduled_activities: 10,
background: 5 background: 5,
user: 10
config :pleroma, :fetch_initial_posts, config :pleroma, :fetch_initial_posts,
enabled: false, enabled: false,

View 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 * Response: JSON. Returns `{"status": "success"}` if the deletion was successful, `{"error": "[error message]"}` otherwise
* Example response: `{"error": "Invalid password."}` * 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` ## `/api/account/register`
### Register a new user ### Register a new user
* Method `POST` * Method `POST`

View file

@ -132,7 +132,10 @@ def get_by_ap_id_with_object(ap_id) do
end end
def get_by_id(id) do def get_by_id(id) do
Repo.get(Activity, id) Activity
|> where([a], a.id == ^id)
|> restrict_deactivated_users()
|> Repo.one()
end end
def get_by_id_with_object(id) do def get_by_id_with_object(id) do
@ -200,6 +203,7 @@ def get_all_create_by_object_ap_id(ap_id) do
def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do def get_create_by_object_ap_id(ap_id) when is_binary(ap_id) do
create_by_object_ap_id(ap_id) create_by_object_ap_id(ap_id)
|> restrict_deactivated_users()
|> Repo.one() |> Repo.one()
end end
@ -314,4 +318,14 @@ def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do
def query_by_actor(actor) do def query_by_actor(actor) do
from(a in Activity, where: a.actor == ^actor) from(a in Activity, where: a.actor == ^actor)
end 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 end

View file

@ -33,6 +33,13 @@ def changeset(%Notification{} = notification, attrs) do
def for_user_query(user) do def for_user_query(user) do
Notification Notification
|> where(user_id: ^user.id) |> 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(:inner, [n], activity in assoc(n, :activity))
|> join(:left, [n, a], object in Object, |> join(:left, [n, a], object in Object,
on: on:

View file

@ -105,10 +105,8 @@ def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers" def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
def user_info(%User{} = user) do 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, note_count: user.info.note_count,
follower_count: user.info.follower_count, follower_count: user.info.follower_count,
locked: user.info.locked, locked: user.info.locked,
@ -117,6 +115,20 @@ def user_info(%User{} = user) do
} }
end 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 def remote_user_creation(params) do
params = params =
params params
@ -255,7 +267,7 @@ defp autofollow_users(user) do
candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames]) candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
autofollowed_users = autofollowed_users =
User.Query.build(%{nickname: candidates, local: true}) User.Query.build(%{nickname: candidates, local: true, deactivated: false})
|> Repo.all() |> Repo.all()
follow_all(user, autofollowed_users) follow_all(user, autofollowed_users)
@ -576,7 +588,7 @@ def fetch_initial_posts(user) do
@spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t() @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
def get_followers_query(%User{} = user, nil) do def get_followers_query(%User{} = user, nil) do
User.Query.build(%{followers: user}) User.Query.build(%{followers: user, deactivated: false})
end end
def get_followers_query(user, page) do def get_followers_query(user, page) do
@ -601,7 +613,7 @@ def get_followers_ids(user, page \\ nil) do
@spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t() @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
def get_friends_query(%User{} = user, nil) do def get_friends_query(%User{} = user, nil) do
User.Query.build(%{friends: user}) User.Query.build(%{friends: user, deactivated: false})
end end
def get_friends_query(user, page) do def get_friends_query(user, page) do
@ -691,16 +703,16 @@ def update_note_count(%User{} = user) do
info_cng = User.Info.set_note_count(user.info, note_count) info_cng = User.Info.set_note_count(user.info, note_count)
cng = user
change(user) |> change()
|> put_embed(:info, info_cng) |> put_embed(:info, info_cng)
|> update_and_set_cache()
update_and_set_cache(cng)
end end
def update_follower_count(%User{} = user) do def update_follower_count(%User{} = user) do
follower_count_query = 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 User
|> where(id: ^user.id) |> where(id: ^user.id)
@ -725,7 +737,7 @@ def update_follower_count(%User{} = user) do
@spec get_users_from_set([String.t()], boolean()) :: [User.t()] @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
def get_users_from_set(ap_ids, local_only \\ true) do 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 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
User.Query.build(criteria) User.Query.build(criteria)
@ -734,7 +746,7 @@ def get_users_from_set(ap_ids, local_only \\ true) do
@spec get_recipients_from_activity(Activity.t()) :: [User.t()] @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
def get_recipients_from_activity(%Activity{recipients: to}) do 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() |> Repo.all()
end end
@ -832,6 +844,7 @@ defp fts_search_subquery(term, query \\ User) do
^processed_query ^processed_query
) )
) )
|> restrict_deactivated()
end end
defp trigram_search_subquery(term) do defp trigram_search_subquery(term) do
@ -850,6 +863,7 @@ defp trigram_search_subquery(term) do
}, },
where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term) where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
) )
|> restrict_deactivated()
end end
def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
@ -999,19 +1013,19 @@ def subscribed_to?(user, %{ap_id: ap_id}) do
@spec muted_users(User.t()) :: [User.t()] @spec muted_users(User.t()) :: [User.t()]
def muted_users(user) do 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() |> Repo.all()
end end
@spec blocked_users(User.t()) :: [User.t()] @spec blocked_users(User.t()) :: [User.t()]
def blocked_users(user) do 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() |> Repo.all()
end end
@spec subscribers(User.t()) :: [User.t()] @spec subscribers(User.t()) :: [User.t()]
def subscribers(user) do def subscribers(user) do
User.Query.build(%{ap_id: user.info.subscribers}) User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
|> Repo.all() |> Repo.all()
end end
@ -1039,14 +1053,27 @@ def unblock_domain(user, domain) do
update_and_set_cache(cng) update_and_set_cache(cng)
end 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 def deactivate(%User{} = user, status \\ true) do
info_cng = User.Info.set_activation_status(user.info, status) info_cng = User.Info.set_activation_status(user.info, status)
cng = with {:ok, friends} <- User.get_friends(user),
change(user) {:ok, followers} <- User.get_followers(user),
{:ok, user} <-
user
|> change()
|> put_embed(:info, info_cng) |> 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 end
def update_notification_settings(%User{} = user, settings \\ %{}) do def update_notification_settings(%User{} = user, settings \\ %{}) do
@ -1320,7 +1347,7 @@ def error_user(ap_id) do
@spec all_superusers() :: [User.t()] @spec all_superusers() :: [User.t()]
def all_superusers do def all_superusers do
User.Query.build(%{super_users: true, local: true}) User.Query.build(%{super_users: true, local: true, deactivated: false})
|> Repo.all() |> Repo.all()
end end

View file

@ -118,7 +118,11 @@ defp compose_query({:active, _}, query) do
|> where([u], not is_nil(u.nickname)) |> where([u], not is_nil(u.nickname))
end 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(query, [u], fragment("?->'deactivated' @> 'true'", u.info))
|> where([u], not is_nil(u.nickname)) |> where([u], not is_nil(u.nickname))
end end

View file

@ -854,6 +854,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_reblogs(opts) |> restrict_reblogs(opts)
|> restrict_pinned(opts) |> restrict_pinned(opts)
|> restrict_muted_reblogs(opts) |> restrict_muted_reblogs(opts)
|> Activity.restrict_deactivated_users()
end end
def fetch_activities(recipients, opts \\ %{}) do def fetch_activities(recipients, opts \\ %{}) do

View file

@ -215,6 +215,7 @@ defmodule Pleroma.Web.Router do
post("/change_password", UtilController, :change_password) post("/change_password", UtilController, :change_password)
post("/delete_account", UtilController, :delete_account) post("/delete_account", UtilController, :delete_account)
put("/notification_settings", UtilController, :update_notificaton_settings) put("/notification_settings", UtilController, :update_notificaton_settings)
post("/disable_account", UtilController, :disable_account)
end end
scope [] do scope [] do

View file

@ -360,6 +360,17 @@ def delete_account(%{assigns: %{user: user}} = conn, params) do
end end
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 def captcha(conn, _params) do
json(conn, Pleroma.Captcha.new()) json(conn, Pleroma.Captcha.new())
end end

View file

@ -236,12 +236,15 @@ def password_reset(nickname_or_email) do
def get_user(user \\ nil, params) do def get_user(user \\ nil, params) do
case params do case params do
%{"user_id" => user_id} -> %{"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 -> nil ->
{:error, "No user with such user_id"} {:error, "No user with such user_id"}
_ -> %User{info: %{deactivated: true}} ->
{:ok, target} {:error, "User has been disabled"}
user ->
{:ok, user}
end end
%{"screen_name" => nickname} -> %{"screen_name" => nickname} ->

View file

@ -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

View file

@ -8,6 +8,7 @@ defmodule Pleroma.UserTest do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
use Pleroma.DataCase use Pleroma.DataCase
@ -213,8 +214,8 @@ test "test if a user is following another user" do
test "fetches correct profile for nickname beginning with number" do test "fetches correct profile for nickname beginning with number" do
# Use old-style integer ID to try to reproduce the problem # Use old-style integer ID to try to reproduce the problem
user = insert(:user, %{id: 1080}) user = insert(:user, %{id: 1080})
userwithnumbers = insert(:user, %{nickname: "#{user.id}garbage"}) user_with_numbers = insert(:user, %{nickname: "#{user.id}garbage"})
assert userwithnumbers == User.get_cached_by_nickname_or_id(userwithnumbers.nickname) assert user_with_numbers == User.get_cached_by_nickname_or_id(user_with_numbers.nickname)
end end
describe "user registration" do describe "user registration" do
@ -816,7 +817,8 @@ test "get recipients from activity" do
assert addressed in recipients assert addressed in recipients
end end
test ".deactivate can de-activate then re-activate a user" do describe ".deactivate" do
test "can de-activate then re-activate a user" do
user = insert(:user) user = insert(:user)
assert false == user.info.deactivated assert false == user.info.deactivated
{:ok, user} = User.deactivate(user) {:ok, user} = User.deactivate(user)
@ -825,6 +827,65 @@ test ".deactivate can de-activate then re-activate a user" do
assert false == user.info.deactivated assert false == user.info.deactivated
end 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 test ".delete_user_activities deletes all create activities" do
user = insert(:user) user = insert(:user)

View file

@ -251,4 +251,22 @@ test "GET /api/pleroma/healthcheck", %{conn: conn} do
assert conn.status in [200, 503] assert conn.status in [200, 503]
end 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 end