Merge branch 'feature/user_deletion' into develop

This commit is contained in:
Roger Braun 2017-12-09 10:10:45 +01:00
commit d5a13c10ac
9 changed files with 175 additions and 26 deletions

View File

@ -7,7 +7,7 @@ defmodule Pleroma.Activity do
field :data, :map field :data, :map
field :local, :boolean, default: true field :local, :boolean, default: true
field :actor, :string field :actor, :string
has_many :notifications, Notification has_many :notifications, Notification, on_delete: :delete_all
timestamps() timestamps()
end end

View File

@ -12,6 +12,7 @@ def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(conn, opts) do def call(conn, opts) do
with {:ok, username, password} <- decode_header(conn), with {:ok, username, password} <- decode_header(conn),
{:ok, user} <- opts[:fetcher].(username), {:ok, user} <- opts[:fetcher].(username),
false <- !!user.info["deactivated"],
saved_user_id <- get_session(conn, :user_id), saved_user_id <- get_session(conn, :user_id),
{:ok, verified_user} <- verify(user, password, saved_user_id) {:ok, verified_user} <- verify(user, password, saved_user_id)
do do

View File

@ -16,7 +16,8 @@ def call(conn, _) do
end end
with token when not is_nil(token) <- token, with token when not is_nil(token) <- token,
%Token{user_id: user_id} <- Repo.get_by(Token, token: token), %Token{user_id: user_id} <- Repo.get_by(Token, token: token),
%User{} = user <- Repo.get(User, user_id) do %User{} = user <- Repo.get(User, user_id),
false <- !!user.info["deactivated"] do
conn conn
|> assign(:user, user) |> assign(:user, user)
else else

View File

@ -5,7 +5,7 @@ defmodule Pleroma.User do
alias Pleroma.{Repo, User, Object, Web, Activity, Notification} alias Pleroma.{Repo, User, Object, Web, Activity, Notification}
alias Comeonin.Pbkdf2 alias Comeonin.Pbkdf2
alias Pleroma.Web.{OStatus, Websub} alias Pleroma.Web.{OStatus, Websub}
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.{Utils, ActivityPub}
schema "users" do schema "users" do
field :bio, :string field :bio, :string
@ -113,7 +113,7 @@ def password_update_changeset(struct, params) do
end end
def reset_password(user, data) do def reset_password(user, data) do
Repo.update(password_update_changeset(user, data)) update_and_set_cache(password_update_changeset(user, data))
end end
def register_changeset(struct, params \\ %{}) do def register_changeset(struct, params \\ %{}) do
@ -142,9 +142,9 @@ def register_changeset(struct, params \\ %{}) do
end end
end end
def follow(%User{} = follower, %User{} = followed) do def follow(%User{} = follower, %User{info: info} = followed) do
ap_followers = followed.follower_address ap_followers = followed.follower_address
if following?(follower, followed) do if following?(follower, followed) or info["deactivated"] do
{:error, {:error,
"Could not follow user: #{followed.nickname} is already on your list."} "Could not follow user: #{followed.nickname} is already on your list."}
else else
@ -157,7 +157,7 @@ def follow(%User{} = follower, %User{} = followed) do
follower = follower follower = follower
|> follow_changeset(%{following: following}) |> follow_changeset(%{following: following})
|> Repo.update |> update_and_set_cache
{:ok, _} = update_follower_count(followed) {:ok, _} = update_follower_count(followed)
@ -173,7 +173,7 @@ def unfollow(%User{} = follower, %User{} = followed) do
{ :ok, follower } = follower { :ok, follower } = follower
|> follow_changeset(%{following: following}) |> follow_changeset(%{following: following})
|> Repo.update |> update_and_set_cache
{:ok, followed} = update_follower_count(followed) {:ok, followed} = update_follower_count(followed)
@ -191,6 +191,17 @@ def get_by_ap_id(ap_id) do
Repo.get_by(User, ap_id: ap_id) Repo.get_by(User, ap_id: ap_id)
end end
def update_and_set_cache(changeset) do
with {:ok, user} <- Repo.update(changeset) do
Cachex.set(:user_cache, "ap_id:#{user.ap_id}", user)
Cachex.set(:user_cache, "nickname:#{user.nickname}", user)
Cachex.set(:user_cache, "user_info:#{user.id}", user_info(user))
{:ok, user}
else
e -> e
end
end
def get_cached_by_ap_id(ap_id) do def get_cached_by_ap_id(ap_id) do
key = "ap_id:#{ap_id}" key = "ap_id:#{ap_id}"
Cachex.get!(:user_cache, key, fallback: fn(_) -> get_by_ap_id(ap_id) end) Cachex.get!(:user_cache, key, fallback: fn(_) -> get_by_ap_id(ap_id) end)
@ -245,7 +256,7 @@ def increase_note_count(%User{} = user) do
cs = info_changeset(user, %{info: new_info}) cs = info_changeset(user, %{info: new_info})
Repo.update(cs) update_and_set_cache(cs)
end end
def update_note_count(%User{} = user) do def update_note_count(%User{} = user) do
@ -259,7 +270,7 @@ def update_note_count(%User{} = user) do
cs = info_changeset(user, %{info: new_info}) cs = info_changeset(user, %{info: new_info})
Repo.update(cs) update_and_set_cache(cs)
end end
def update_follower_count(%User{} = user) do def update_follower_count(%User{} = user) do
@ -274,7 +285,7 @@ def update_follower_count(%User{} = user) do
cs = info_changeset(user, %{info: new_info}) cs = info_changeset(user, %{info: new_info})
Repo.update(cs) update_and_set_cache(cs)
end end
def get_notified_from_activity(%Activity{data: %{"to" => to}}) do def get_notified_from_activity(%Activity{data: %{"to" => to}}) do
@ -312,7 +323,7 @@ def block(user, %{ap_id: ap_id}) do
new_info = Map.put(user.info, "blocks", new_blocks) new_info = Map.put(user.info, "blocks", new_blocks)
cs = User.info_changeset(user, %{info: new_info}) cs = User.info_changeset(user, %{info: new_info})
Repo.update(cs) update_and_set_cache(cs)
end end
def unblock(user, %{ap_id: ap_id}) do def unblock(user, %{ap_id: ap_id}) do
@ -321,7 +332,7 @@ def unblock(user, %{ap_id: ap_id}) do
new_info = Map.put(user.info, "blocks", new_blocks) new_info = Map.put(user.info, "blocks", new_blocks)
cs = User.info_changeset(user, %{info: new_info}) cs = User.info_changeset(user, %{info: new_info})
Repo.update(cs) update_and_set_cache(cs)
end end
def blocks?(user, %{ap_id: ap_id}) do def blocks?(user, %{ap_id: ap_id}) do
@ -334,4 +345,35 @@ def local_user_query() do
where: u.local == true where: u.local == true
end end
def deactivate (%User{} = user) do
new_info = Map.put(user.info, "deactivated", true)
cs = User.info_changeset(user, %{info: new_info})
update_and_set_cache(cs)
end
def delete (%User{} = user) do
{:ok, user} = User.deactivate(user)
# Remove all relationships
{:ok, followers } = User.get_followers(user)
followers
|> Enum.each(fn (follower) -> User.unfollow(follower, user) end)
{:ok, friends} = User.get_friends(user)
friends
|> Enum.each(fn (followed) -> User.unfollow(user, followed) end)
query = from a in Activity,
where: a.actor == ^user.ap_id
Repo.all(query)
|> Enum.each(fn (activity) ->
case activity.data["type"] do
"Create" -> ActivityPub.delete(Object.get_by_ap_id(activity.data["object"]["id"]))
_ -> "Doing nothing" # TODO: Do something with likes, follows, repeats.
end
end)
:ok
end
end end

View File

@ -29,7 +29,12 @@ def generate_id(type) do
Enqueues an activity for federation if it's local Enqueues an activity for federation if it's local
""" """
def maybe_federate(%Activity{local: true} = activity) do def maybe_federate(%Activity{local: true} = activity) do
Pleroma.Web.Federator.enqueue(:publish, activity) priority = case activity.data["type"] do
"Delete" -> 10
"Create" -> 1
_ -> 5
end
Pleroma.Web.Federator.enqueue(:publish, activity, priority)
:ok :ok
end end
def maybe_federate(_), do: :ok def maybe_federate(_), do: :ok

View File

@ -15,8 +15,8 @@ def start_link do
enqueue(:refresh_subscriptions, nil) enqueue(:refresh_subscriptions, nil)
end) end)
GenServer.start_link(__MODULE__, %{ GenServer.start_link(__MODULE__, %{
in: {:sets.new(), :queue.new()}, in: {:sets.new(), []},
out: {:sets.new(), :queue.new()} out: {:sets.new(), []}
}, name: __MODULE__) }, name: __MODULE__)
end end
@ -79,17 +79,17 @@ def handle(type, _) do
{:error, "Don't know what do do with this"} {:error, "Don't know what do do with this"}
end end
def enqueue(type, payload) do def enqueue(type, payload, priority \\ 1) do
if Mix.env == :test do if Mix.env == :test do
handle(type, payload) handle(type, payload)
else else
GenServer.cast(__MODULE__, {:enqueue, type, payload}) GenServer.cast(__MODULE__, {:enqueue, type, payload, priority})
end end
end end
def maybe_start_job(running_jobs, queue) do def maybe_start_job(running_jobs, queue) do
if (:sets.size(running_jobs) < @max_jobs) && !:queue.is_empty(queue) do if (:sets.size(running_jobs) < @max_jobs) && queue != [] do
{{:value, {type, payload}}, queue} = :queue.out(queue) {{type, payload}, queue} = queue_pop(queue)
{:ok, pid} = Task.start(fn -> handle(type, payload) end) {:ok, pid} = Task.start(fn -> handle(type, payload) end)
mref = Process.monitor(pid) mref = Process.monitor(pid)
{:sets.add_element(mref, running_jobs), queue} {:sets.add_element(mref, running_jobs), queue}
@ -98,16 +98,16 @@ def maybe_start_job(running_jobs, queue) do
end end
end end
def handle_cast({:enqueue, type, payload}, state) when type in [:incoming_doc] do def handle_cast({:enqueue, type, payload, priority}, state) when type in [:incoming_doc] do
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
i_queue = :queue.in({type, payload}, i_queue) i_queue = enqueue_sorted(i_queue, {type, payload}, 1)
{i_running_jobs, i_queue} = maybe_start_job(i_running_jobs, i_queue) {i_running_jobs, i_queue} = maybe_start_job(i_running_jobs, i_queue)
{:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}} {:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
end end
def handle_cast({:enqueue, type, payload}, state) do def handle_cast({:enqueue, type, payload, priority}, state) do
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
o_queue = :queue.in({type, payload}, o_queue) o_queue = enqueue_sorted(o_queue, {type, payload}, 1)
{o_running_jobs, o_queue} = maybe_start_job(o_running_jobs, o_queue) {o_running_jobs, o_queue} = maybe_start_job(o_running_jobs, o_queue)
{:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}} {:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
end end
@ -126,4 +126,13 @@ def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do
{:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}} {:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
end end
def enqueue_sorted(queue, element, priority) do
[%{item: element, priority: priority} | queue]
|> Enum.sort_by(fn (%{priority: priority}) -> priority end)
end
def queue_pop([%{item: element} | queue]) do
{element, queue}
end
end end

View File

@ -14,6 +14,13 @@ defp fetch_nil(_name) do
password_hash: Comeonin.Pbkdf2.hashpwsalt("guy") password_hash: Comeonin.Pbkdf2.hashpwsalt("guy")
} }
@deactivated %User{
id: 1,
name: "dude",
password_hash: Comeonin.Pbkdf2.hashpwsalt("guy"),
info: %{"deactivated" => true}
}
@session_opts [ @session_opts [
store: :cookie, store: :cookie,
key: "_test", key: "_test",
@ -131,6 +138,26 @@ test "it assigns the user", %{conn: conn} do
end end
end end
describe "with a correct authorization header for an deactiviated user" do
test "it halts the appication", %{conn: conn} do
opts = %{
optional: false,
fetcher: fn _ -> @deactivated end
}
header = basic_auth_enc("dude", "guy")
conn = conn
|> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session
|> put_req_header("authorization", header)
|> AuthenticationPlug.call(opts)
assert conn.status == 403
assert conn.halted == true
end
end
describe "with a user_id in the session for an existing user" do describe "with a user_id in the session for an existing user" do
test "it assigns the user", %{conn: conn} do test "it assigns the user", %{conn: conn} do
opts = %{ opts = %{

View File

@ -1,6 +1,6 @@
defmodule Pleroma.UserTest do defmodule Pleroma.UserTest do
alias Pleroma.Builders.UserBuilder alias Pleroma.Builders.UserBuilder
alias Pleroma.{User, Repo} alias Pleroma.{User, Repo, Activity}
alias Pleroma.Web.OStatus alias Pleroma.Web.OStatus
alias Pleroma.Web.Websub.WebsubClientSubscription alias Pleroma.Web.Websub.WebsubClientSubscription
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
@ -39,6 +39,13 @@ test "follow takes a user and another user" do
assert User.ap_followers(followed) in user.following assert User.ap_followers(followed) in user.following
end end
test "can't follow a deactivated users" do
user = insert(:user)
followed = insert(:user, info: %{"deactivated" => true})
{:error, _} = User.follow(user, followed)
end
test "following a remote user will ensure a websub subscription is present" do test "following a remote user will ensure a websub subscription is present" do
user = insert(:user) user = insert(:user)
{:ok, followed} = OStatus.make_user("shp@social.heldscal.la") {:ok, followed} = OStatus.make_user("shp@social.heldscal.la")
@ -325,5 +332,42 @@ test "get recipients from activity" do
assert user in recipients assert user in recipients
assert addressed in recipients assert addressed in recipients
end end
end
test ".deactivate deactivates a user" do
user = insert(:user)
assert false == !!user.info["deactivated"]
{:ok, user} = User.deactivate(user)
assert true == user.info["deactivated"]
end
test ".delete deactivates a user, all follow relationships and all create activities" do
user = insert(:user)
followed = insert(:user)
follower = insert(:user)
{:ok, user} = User.follow(user, followed)
{:ok, follower} = User.follow(follower, user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "2hu"})
{:ok, activity_two} = CommonAPI.post(follower, %{"status" => "3hu"})
{:ok, _, _} = CommonAPI.favorite(activity_two.id, user)
{:ok, _, _} = CommonAPI.favorite(activity.id, follower)
{:ok, _, _} = CommonAPI.repeat(activity.id, follower)
:ok = User.delete(user)
followed = Repo.get(User, followed.id)
follower = Repo.get(User, follower.id)
user = Repo.get(User, user.id)
assert user.info["deactivated"]
refute User.following?(user, followed)
refute User.following?(followed, follower)
# TODO: Remove favorites, repeats, delete activities.
refute Repo.get(Activity, activity.id)
end
end

View File

@ -0,0 +1,20 @@
defmodule Pleroma.Web.FederatorTest do
alias Pleroma.Web.Federator
use Pleroma.DataCase
test "enqueues an element according to priority" do
queue = [%{item: 1, priority: 2}]
new_queue = Federator.enqueue_sorted(queue, 2, 1)
assert new_queue == [%{item: 2, priority: 1}, %{item: 1, priority: 2}]
new_queue = Federator.enqueue_sorted(queue, 2, 3)
assert new_queue == [%{item: 1, priority: 2}, %{item: 2, priority: 3}]
end
test "pop first item" do
queue = [%{item: 2, priority: 1}, %{item: 1, priority: 2}]
assert {2, [%{item: 1, priority: 2}]} = Federator.queue_pop(queue)
end
end