From eb84de01439c4ee25f59390e5be4ffa7f36e01b8 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 4 Mar 2019 19:55:11 +0700 Subject: [PATCH] allow users to disable their own account --- config/config.exs | 3 +- lib/mix/tasks/pleroma/user.ex | 20 +++++- lib/pleroma/activity.ex | 16 ++++- lib/pleroma/gopher/server.ex | 3 +- lib/pleroma/notification.ex | 32 +++++----- lib/pleroma/user.ex | 64 +++++++++++++++---- lib/pleroma/user/info.ex | 9 +++ lib/pleroma/web/activity_pub/activity_pub.ex | 1 + .../web/admin_api/admin_api_controller.ex | 10 +++ lib/pleroma/web/common_api/utils.ex | 4 +- .../mastodon_api/mastodon_api_controller.ex | 12 ++-- lib/pleroma/web/router.ex | 2 + .../controllers/util_controller.ex | 11 ++++ lib/pleroma/web/twitter_api/twitter_api.ex | 20 ++++-- .../web/twitter_api/twitter_api_controller.ex | 4 +- ...0190228121252_users_add_disabled_index.exs | 7 ++ 16 files changed, 167 insertions(+), 51 deletions(-) create mode 100644 priv/repo/migrations/20190228121252_users_add_disabled_index.exs diff --git a/config/config.exs b/config/config.exs index a620e7451..d9ed43dda 100644 --- a/config/config.exs +++ b/config/config.exs @@ -343,7 +343,8 @@ config :pleroma, Pleroma.Jobs, federator_incoming: [max_jobs: 50], federator_outgoing: [max_jobs: 50], - mailer: [max_jobs: 10] + mailer: [max_jobs: 10], + user: [max_jobs: 10] config :auto_linker, opts: [ diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 037e44716..297332bc4 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -23,7 +23,7 @@ defmodule Mix.Tasks.Pleroma.User do - `--password PASSWORD` - the user's password - `--moderator`/`--no-moderator` - whether the user is a moderator - `--admin`/`--no-admin` - whether the user is an admin - - `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions + - `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions ## Generate an invite link. @@ -37,6 +37,10 @@ defmodule Mix.Tasks.Pleroma.User do mix pleroma.user toggle_activated NICKNAME + ## Disable or enable the user's account. + + mix pleroma.user toggle_disabled NICKNAME + ## Unsubscribe local users from user's account and deactivate it mix pleroma.user unsubscribe NICKNAME @@ -170,6 +174,20 @@ def run(["toggle_activated", nickname]) do end end + def run(["toggle_disabled", nickname]) do + Common.start_pleroma() + + case User.get_by_nickname(nickname) do + %User{} = user -> + {:ok, user} = User.disable(user, !user.info.disabled) + status = if(user.info.disabled, do: "ON", else: "OFF") + Mix.shell().info("Disabled status of #{nickname}: #{status}") + + _ -> + Mix.shell().error("No user #{nickname}") + end + end + def run(["reset_password", nickname]) do Common.start_pleroma() diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 66854dc2d..c466bff7f 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -42,7 +42,10 @@ def get_by_ap_id(ap_id) do end def get_by_id(id) do - Repo.get(Activity, id) + Activity + |> where([a], a.id == ^id) + |> restrict_disabled_users() + |> Repo.one() end def by_object_ap_id(ap_id) do @@ -92,6 +95,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 create_by_object_ap_id(ap_id) + |> restrict_disabled_users() |> Repo.one() end @@ -123,4 +127,14 @@ def all_by_actor_and_id(actor, status_ids) do |> where([s], s.actor == ^actor) |> Repo.all() end + + def restrict_disabled_users(query) do + from(activity in query, + where: + fragment( + "? not in (SELECT ap_id FROM users WHERE info->'disabled' @> 'true')", + activity.actor + ) + ) + end end diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex index ba9614029..24190574e 100644 --- a/lib/pleroma/gopher/server.ex +++ b/lib/pleroma/gopher/server.ex @@ -41,7 +41,6 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do alias Pleroma.Activity alias Pleroma.HTML alias Pleroma.User - alias Pleroma.Repo def start_link(ref, socket, transport, opts) do pid = spawn_link(__MODULE__, :init, [ref, socket, transport, opts]) @@ -110,7 +109,7 @@ def response("/main/all") do end def response("/notices/" <> id) do - with %Activity{} = activity <- Repo.get(Activity, id), + with %Activity{} = activity <- Activity.get_by_id(id), true <- Visibility.is_public?(activity) do activities = ActivityPub.fetch_activities_for_context(activity.data["context"]) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index c88512567..0f9f74b1e 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -36,22 +36,22 @@ defp restrict_since(query, %{"since_id" => since_id}) do defp restrict_since(query, _), do: query def for_user(user, opts \\ %{}) do - query = - from( - n in Notification, - where: n.user_id == ^user.id, - order_by: [desc: n.id], - join: activity in assoc(n, :activity), - preload: [activity: activity], - limit: 20 - ) - - query = - query - |> restrict_since(opts) - |> restrict_max(opts) - - Repo.all(query) + from( + n in Notification, + where: n.user_id == ^user.id, + order_by: [desc: n.id], + join: activity in assoc(n, :activity), + preload: [activity: activity], + limit: 20, + where: + fragment( + "? not in (SELECT ap_id FROM users WHERE info->'disabled' @> 'true')", + activity.actor + ) + ) + |> restrict_since(opts) + |> restrict_max(opts) + |> Repo.all() end def set_read_up_to(%{id: user_id} = _user, id) do diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 50e7e7ccd..f02051174 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -108,10 +108,8 @@ def ap_followers(%User{} = user) do end 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, @@ -120,6 +118,23 @@ def user_info(%User{} = user) do } end + defp restrict_disabled(query) do + from(u in query, + where: not fragment("? \\? 'disabled' AND ?->'disabled' @> 'true'", u.info, u.info) + ) + end + + def following_count(%User{following: []}), do: 0 + + def following_count(%User{following: following, id: id}) do + from(u in User, + where: u.follower_address in ^following, + where: u.id != ^id + ) + |> restrict_disabled() + |> Repo.aggregate(:count, :id) + end + def remote_user_creation(params) do params = params @@ -545,6 +560,7 @@ def get_followers_query(%User{id: id, follower_address: follower_address}, nil) where: fragment("? <@ ?", ^[follower_address], u.following), where: u.id != ^id ) + |> restrict_disabled() end def get_followers_query(user, page) do @@ -572,6 +588,7 @@ def get_friends_query(%User{id: id, following: following}, nil) do where: u.follower_address in ^following, where: u.id != ^id ) + |> restrict_disabled() end def get_friends_query(user, page) do @@ -681,11 +698,10 @@ def update_note_count(%User{} = 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 @@ -694,6 +710,7 @@ def update_follower_count(%User{} = user) do |> where([u], ^user.follower_address in u.following) |> where([u], u.id != ^user.id) |> select([u], %{count: count(u.id)}) + |> restrict_disabled() User |> where(id: ^user.id) @@ -860,6 +877,7 @@ defp fts_search_subquery(term, query \\ User) do ^processed_query ) ) + |> restrict_disabled() end defp trigram_search_subquery(term) do @@ -876,6 +894,7 @@ defp trigram_search_subquery(term) do }, where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term) ) + |> restrict_disabled() end defp boost_search_results(results, nil), do: results @@ -1062,11 +1081,10 @@ def moderator_user_query do 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) - - update_and_set_cache(cng) + user + |> change() + |> put_embed(:info, info_cng) + |> update_and_set_cache() end def delete(%User{} = user) do @@ -1100,6 +1118,26 @@ def delete(%User{} = user) do {:ok, user} end + def disable_async(user, status \\ true) do + Pleroma.Jobs.enqueue(:user, __MODULE__, [:disable_async, user, status]) + end + + def disable(%User{} = user, status \\ true) do + with {:ok, user} <- User.deactivate(user, status), + info_cng <- User.Info.set_disabled_status(user.info, status), + {:ok, user} <- + user + |> change() + |> put_embed(:info, info_cng) + |> update_and_set_cache(), + {:ok, friends} <- User.get_friends(user) do + Enum.each(friends, &update_follower_count(&1)) + {:ok, user} + end + end + + def perform(:disable_async, user, status), do: disable(user, status) + def html_filter_policy(%User{info: %{no_rich_text: true}}) do Pleroma.HTML.Scrubber.TwitterText end diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 818b64645..1ec356ba9 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -36,6 +36,7 @@ defmodule Pleroma.User.Info do field(:hide_follows, :boolean, default: false) field(:pinned_activities, {:array, :string}, default: []) field(:flavour, :string, default: nil) + field(:disabled, :boolean, default: false) # Found in the wild # ap_id -> Where is this used? @@ -54,6 +55,14 @@ def set_activation_status(info, deactivated) do |> validate_required([:deactivated]) end + def set_disabled_status(info, disabled) do + params = %{disabled: disabled} + + info + |> cast(params, [:disabled]) + |> validate_required([:disabled]) + end + def add_to_note_count(info, number) do set_note_count(info, info.note_count + number) end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 783491b67..aa20990f3 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -703,6 +703,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_replies(opts) |> restrict_reblogs(opts) |> restrict_pinned(opts) + |> Activity.restrict_disabled_users() end def fetch_activities(recipients, opts \\ %{}) do diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index aae02cab8..1b94f0609 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -44,6 +44,16 @@ def user_create( |> json(user.nickname) end + def user_toggle_disabled(conn, %{"nickname" => nickname}) do + user = User.get_by_nickname(nickname) + + {:ok, updated_user} = User.disable(user, !user.info.disabled) + + conn + |> put_view(AccountView) + |> render("show.json", %{user: updated_user}) + end + def user_toggle_activation(conn, %{"nickname" => nickname}) do user = User.get_by_nickname(nickname) diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index e4b9102c5..60d1185d3 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do # This is a hack for twidere. def get_by_id_or_ap_id(id) do - activity = Repo.get(Activity, id) || Activity.get_create_by_object_ap_id(id) + activity = Activity.get_by_id(id) || Activity.get_create_by_object_ap_id(id) activity && if activity.data["type"] == "Create" do @@ -30,7 +30,7 @@ def get_by_id_or_ap_id(id) do def get_replied_to_activity(""), do: nil def get_replied_to_activity(id) when not is_nil(id) do - Repo.get(Activity, id) + Activity.get_by_id(id) end def get_replied_to_activity(_), do: nil diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 056be49b0..00a0f1351 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -307,7 +307,7 @@ def dm_timeline(%{assigns: %{user: user}} = conn, params) do end def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Activity{} = activity <- Repo.get(Activity, id), + with %Activity{} = activity <- Activity.get_by_id(id), true <- Visibility.visible_for_user?(activity, user) do conn |> put_view(StatusView) @@ -316,7 +316,7 @@ def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do end def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Activity{} = activity <- Repo.get(Activity, id), + with %Activity{} = activity <- Activity.get_by_id(id), activities <- ActivityPub.fetch_activities_for_context(activity.data["context"], %{ "blocking_user" => user, @@ -448,7 +448,7 @@ def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do end def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Activity{} = activity <- Repo.get(Activity, id), + with %Activity{} = activity <- Activity.get_by_id(id), %User{} = user <- User.get_by_nickname(user.nickname), true <- Visibility.visible_for_user?(activity, user), {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do @@ -459,7 +459,7 @@ def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do end def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Activity{} = activity <- Repo.get(Activity, id), + with %Activity{} = activity <- Activity.get_by_id(id), %User{} = user <- User.get_by_nickname(user.nickname), true <- Visibility.visible_for_user?(activity, user), {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do @@ -583,7 +583,7 @@ def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do end def favourited_by(conn, %{"id" => id}) do - with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do + with %Activity{data: %{"object" => %{"likes" => likes}}} <- Activity.get_by_id(id) do q = from(u in User, where: u.ap_id in ^likes) users = Repo.all(q) @@ -596,7 +596,7 @@ def favourited_by(conn, %{"id" => id}) do end def reblogged_by(conn, %{"id" => id}) do - with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do + with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Activity.get_by_id(id) do q = from(u in User, where: u.ap_id in ^announces) users = Repo.all(q) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 6fcb46878..5033b5446 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -143,6 +143,7 @@ defmodule Pleroma.Web.Router do get("/users/search", AdminAPIController, :search_users) delete("/user", AdminAPIController, :user_delete) patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation) + patch("/users/:nickname/toggle_disabled", AdminAPIController, :user_toggle_disabled) post("/user", AdminAPIController, :user_create) put("/users/tag", AdminAPIController, :tag_users) delete("/users/tag", AdminAPIController, :untag_users) @@ -183,6 +184,7 @@ defmodule Pleroma.Web.Router do post("/change_password", UtilController, :change_password) post("/delete_account", UtilController, :delete_account) + 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 e2fdedb25..0006d53e8 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -311,6 +311,17 @@ def delete_account(%{assigns: %{user: user}} = conn, params) 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.disable_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 ab6470d78..615a34be9 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -21,7 +21,7 @@ def create_status(%User{} = user, %{"status" => _} = data) do end def delete(%User{} = user, id) do - with %Activity{data: %{"type" => _type}} <- Repo.get(Activity, id), + with %Activity{data: %{"type" => _type}} <- Activity.get_by_id(id), {:ok, activity} <- CommonAPI.delete(id, user) do {:ok, activity} end @@ -232,21 +232,27 @@ def password_reset(nickname_or_email) 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: %{disabled: true}} -> + {:error, "User has been disabled"} + + user -> + {:ok, user} end %{"screen_name" => nickname} -> - case target = Repo.get_by(User, nickname: nickname) do + case User.get_by_nickname(nickname) do nil -> {:error, "No user with such screen_name"} - _ -> - {:ok, target} + %User{info: %{disabled: true}} -> + {:error, "User has been disabled"} + + user -> + {:ok, user} end _ -> diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index de7b9f24c..0769f8698 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -269,7 +269,7 @@ def unfollow(%{assigns: %{user: user}} = conn, params) do end def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Activity{} = activity <- Repo.get(Activity, id), + with %Activity{} = activity <- Activity.get_by_id(id), true <- Visibility.visible_for_user?(activity, user) do conn |> put_view(ActivityView) @@ -341,7 +341,7 @@ def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do end def get_by_id_or_ap_id(id) do - activity = Repo.get(Activity, id) || Activity.get_create_by_object_ap_id(id) + activity = Activity.get_by_id(id) || Activity.get_create_by_object_ap_id(id) if activity.data["type"] == "Create" do activity diff --git a/priv/repo/migrations/20190228121252_users_add_disabled_index.exs b/priv/repo/migrations/20190228121252_users_add_disabled_index.exs new file mode 100644 index 000000000..7b921d3e7 --- /dev/null +++ b/priv/repo/migrations/20190228121252_users_add_disabled_index.exs @@ -0,0 +1,7 @@ +defmodule Pleroma.Repo.Migrations.UsersAddDisabledIndex do + use Ecto.Migration + + def change do + create(index(:users, ["(info->'disabled')"], name: :users_disabled_index, using: :gin)) + end +end