Introduce basic user and profile management
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -423,7 +423,8 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
"to" => [url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
|
||||
}
|
||||
|
||||
with {:ok, %Oban.Job{}} <- Actors.delete_actor(actor),
|
||||
# We completely delete the actor if activity is remote
|
||||
with {:ok, %Oban.Job{}} <- Actors.delete_actor(actor, reserve_username: local),
|
||||
{:ok, activity} <- create_activity(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, actor}
|
||||
|
||||
@@ -59,7 +59,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object}) do
|
||||
Logger.info("Handle incoming to create notes")
|
||||
|
||||
with object_data <-
|
||||
with object_data when is_map(object_data) <-
|
||||
object |> Converter.Comment.as_to_model_data(),
|
||||
{:existing_comment, {:error, :comment_not_found}} <-
|
||||
{:existing_comment, Conversations.get_comment_from_url_with_preload(object_data.url)},
|
||||
@@ -87,7 +87,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Event"} = object}) do
|
||||
Logger.info("Handle incoming to create event")
|
||||
|
||||
with object_data <-
|
||||
with object_data when is_map(object_data) <-
|
||||
object |> Converter.Event.as_to_model_data(),
|
||||
{:existing_event, nil} <- {:existing_event, Events.get_event_by_url(object_data.url)},
|
||||
{:ok, %Activity{} = activity, %Event{} = event} <-
|
||||
|
||||
@@ -36,7 +36,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
||||
Logger.debug(inspect(object))
|
||||
|
||||
with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"),
|
||||
{:ok, %Actor{id: actor_id, domain: domain}} <-
|
||||
{:ok, %Actor{id: actor_id, domain: domain, suspended: false}} <-
|
||||
ActivityPub.get_or_fetch_actor_by_url(author_url),
|
||||
{:tags, tags} <- {:tags, ConverterUtils.fetch_tags(Map.get(object, "tag", []))},
|
||||
{:mentions, mentions} <-
|
||||
@@ -90,6 +90,9 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
||||
|
||||
data
|
||||
end
|
||||
else
|
||||
{:ok, %Actor{suspended: true}} ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||
Logger.debug(inspect(object))
|
||||
|
||||
with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"),
|
||||
{:actor, {:ok, %Actor{id: actor_id, domain: actor_domain}}} <-
|
||||
{:actor, {:ok, %Actor{id: actor_id, domain: actor_domain, suspended: false}}} <-
|
||||
{:actor, ActivityPub.get_or_fetch_actor_by_url(author_url)},
|
||||
{:address, address_id} <-
|
||||
{:address, get_address(object["location"])},
|
||||
@@ -87,6 +87,9 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||
updated_at: object["updated"],
|
||||
publish_at: object["published"]
|
||||
}
|
||||
else
|
||||
{:ok, %Actor{suspended: true}} ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
||||
alias Mobilizon.Service.Statistics
|
||||
alias Mobilizon.Storage.Page
|
||||
alias Mobilizon.Users.User
|
||||
require Logger
|
||||
|
||||
def list_action_logs(
|
||||
_parent,
|
||||
@@ -96,6 +97,20 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
||||
}
|
||||
end
|
||||
|
||||
defp transform_action_log(Actor, :suspend, %ActionLog{changes: changes}) do
|
||||
%{
|
||||
action: :actor_suspension,
|
||||
object: convert_changes_to_struct(Actor, changes)
|
||||
}
|
||||
end
|
||||
|
||||
defp transform_action_log(Actor, :unsuspend, %ActionLog{changes: changes}) do
|
||||
%{
|
||||
action: :actor_unsuspension,
|
||||
object: convert_changes_to_struct(Actor, changes)
|
||||
}
|
||||
end
|
||||
|
||||
# Changes are stored as %{"key" => "value"} so we need to convert them back as struct
|
||||
defp convert_changes_to_struct(struct, %{"report_id" => _report_id} = changes) do
|
||||
with data <- for({key, val} <- changes, into: %{}, do: {String.to_atom(key), val}),
|
||||
|
||||
@@ -3,10 +3,14 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
||||
Handles the person-related GraphQL calls
|
||||
"""
|
||||
|
||||
import Mobilizon.Users.Guards
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Admin
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.Participant
|
||||
alias Mobilizon.Storage.Page
|
||||
alias Mobilizon.Users
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
@@ -17,8 +21,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
||||
@doc """
|
||||
Get a person
|
||||
"""
|
||||
def get_person(_parent, %{id: id}, _resolution) do
|
||||
with %Actor{} = actor <- Actors.get_actor_with_preload(id),
|
||||
def get_person(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}}) do
|
||||
with %Actor{suspended: suspended} = actor <- Actors.get_actor_with_preload(id, true),
|
||||
true <- suspended == false or is_moderator(role),
|
||||
actor <- proxify_pictures(actor) do
|
||||
{:ok, actor}
|
||||
else
|
||||
@@ -41,6 +46,30 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
||||
end
|
||||
end
|
||||
|
||||
def list_persons(
|
||||
_parent,
|
||||
%{
|
||||
preferred_username: preferred_username,
|
||||
name: name,
|
||||
domain: domain,
|
||||
local: local,
|
||||
suspended: suspended,
|
||||
page: page,
|
||||
limit: limit
|
||||
},
|
||||
%{
|
||||
context: %{current_user: %User{role: role}}
|
||||
}
|
||||
)
|
||||
when is_moderator(role) do
|
||||
{:ok,
|
||||
Actors.list_actors(:Person, preferred_username, name, domain, local, suspended, page, limit)}
|
||||
end
|
||||
|
||||
def list_persons(_parent, _args, _resolution) do
|
||||
{:error, "You need to be logged-in and a moderator to list persons"}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the current actor for the currently logged-in user
|
||||
"""
|
||||
@@ -201,25 +230,41 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
||||
with {:is_owned, %Actor{} = _actor} <- User.owns_actor(user, actor_id),
|
||||
{:no_participant, {:ok, %Participant{} = participant}} <-
|
||||
{:no_participant, Events.get_participant(event_id, actor_id)} do
|
||||
{:ok, [participant]}
|
||||
{:ok, %Page{elements: [participant], total: 1}}
|
||||
else
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
|
||||
{:no_participant, _} ->
|
||||
{:ok, []}
|
||||
{:ok, %Page{elements: [], total: 0}}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of events this person is going to
|
||||
"""
|
||||
def person_participations(%Actor{id: actor_id}, _args, %{context: %{current_user: user}}) do
|
||||
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
|
||||
participations <- Events.list_event_participations_for_actor(actor) do
|
||||
{:ok, participations}
|
||||
def person_participations(%Actor{id: actor_id} = actor, %{page: page, limit: limit}, %{
|
||||
context: %{current_user: %User{role: role} = user}
|
||||
}) do
|
||||
{:is_owned, actor_found} = User.owns_actor(user, actor_id)
|
||||
|
||||
res =
|
||||
cond do
|
||||
not is_nil(actor_found) ->
|
||||
true
|
||||
|
||||
is_moderator(role) ->
|
||||
true
|
||||
|
||||
true ->
|
||||
false
|
||||
end
|
||||
|
||||
with {:is_owned, true} <- {:is_owned, res},
|
||||
%Page{} = page <- Events.list_event_participations_for_actor(actor, page, limit) do
|
||||
{:ok, page}
|
||||
else
|
||||
{:is_owned, nil} ->
|
||||
{:is_owned, false} ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
end
|
||||
end
|
||||
@@ -243,6 +288,95 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
||||
|> proxify_banner
|
||||
end
|
||||
|
||||
def user_for_person(%Actor{type: :Person, user_id: user_id}, _args, %{
|
||||
context: %{current_user: %User{role: role}}
|
||||
})
|
||||
when is_moderator(role) do
|
||||
with false <- is_nil(user_id),
|
||||
%User{} = user <- Users.get_user(user_id) do
|
||||
{:ok, user}
|
||||
else
|
||||
true ->
|
||||
{:ok, nil}
|
||||
|
||||
_ ->
|
||||
{:error, "User not found"}
|
||||
end
|
||||
end
|
||||
|
||||
def user_for_person(_, _args, _resolution), do: {:error, nil}
|
||||
|
||||
def organized_events_for_person(
|
||||
%Actor{user_id: actor_user_id} = actor,
|
||||
%{page: page, limit: limit},
|
||||
%{
|
||||
context: %{current_user: %User{id: user_id, role: role}}
|
||||
}
|
||||
) do
|
||||
with true <- actor_user_id == user_id or is_moderator(role),
|
||||
%Page{} = page <- Events.list_organized_events_for_actor(actor, page, limit) do
|
||||
{:ok, page}
|
||||
end
|
||||
end
|
||||
|
||||
def suspend_profile(_parent, %{id: id}, %{
|
||||
context: %{current_user: %User{role: role} = user}
|
||||
})
|
||||
when is_moderator(role) do
|
||||
with {:moderator_actor, %Actor{} = moderator_actor} <-
|
||||
{:moderator_actor, Users.get_actor_for_user(user)},
|
||||
%Actor{suspended: false} = actor <- Actors.get_remote_actor_with_preload(id),
|
||||
{:ok, _} <- Actors.delete_actor(actor),
|
||||
{:ok, _} <- Admin.log_action(moderator_actor, "suspend", actor) do
|
||||
{:ok, actor}
|
||||
else
|
||||
{:moderator_actor, nil} ->
|
||||
{:error, "No actor found for the moderator user"}
|
||||
|
||||
%Actor{suspended: true} ->
|
||||
{:error, "Actor already suspended"}
|
||||
|
||||
nil ->
|
||||
{:error, "No remote profile found with this ID"}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, "Error while performing background task"}
|
||||
end
|
||||
end
|
||||
|
||||
def suspend_profile(_parent, _args, _resolution) do
|
||||
{:error, "Only moderators and administrators can suspend a profile"}
|
||||
end
|
||||
|
||||
def unsuspend_profile(_parent, %{id: id}, %{
|
||||
context: %{current_user: %User{role: role} = user}
|
||||
})
|
||||
when is_moderator(role) do
|
||||
with {:moderator_actor, %Actor{} = moderator_actor} <-
|
||||
{:moderator_actor, Users.get_actor_for_user(user)},
|
||||
%Actor{preferred_username: preferred_username, domain: domain} = actor <-
|
||||
Actors.get_remote_actor_with_preload(id, true),
|
||||
{:ok, _} <- Actors.update_actor(actor, %{suspended: false}),
|
||||
{:ok, %Actor{} = actor} <-
|
||||
ActivityPub.make_actor_from_nickname("#{preferred_username}@#{domain}"),
|
||||
{:ok, _} <- Admin.log_action(moderator_actor, "unsuspend", actor) do
|
||||
{:ok, actor}
|
||||
else
|
||||
{:moderator_actor, nil} ->
|
||||
{:error, "No actor found for the moderator user"}
|
||||
|
||||
nil ->
|
||||
{:error, "No remote profile found with this ID"}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, "Error while performing background task"}
|
||||
end
|
||||
end
|
||||
|
||||
def unsuspend_profile(_parent, _args, _resolution) do
|
||||
{:error, "Only moderators and administrators can unsuspend a profile"}
|
||||
end
|
||||
|
||||
# We check that the actor is not the last administrator/creator of a group
|
||||
@spec last_admin_of_a_group?(integer()) :: boolean()
|
||||
defp last_admin_of_a_group?(actor_id) do
|
||||
|
||||
@@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
alias Mobilizon.{Actors, Config, Events, Users}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Crypto
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
alias Mobilizon.Users.{Setting, User}
|
||||
|
||||
@@ -20,8 +21,11 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
@doc """
|
||||
Find an user by its ID
|
||||
"""
|
||||
def find_user(_parent, %{id: id}, _resolution) do
|
||||
Users.get_user_with_actors(id)
|
||||
def find_user(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}})
|
||||
when is_moderator(role) do
|
||||
with {:ok, %User{} = user} <- Users.get_user_with_actors(id) do
|
||||
{:ok, user}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -38,19 +42,16 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
@doc """
|
||||
List instance users
|
||||
"""
|
||||
def list_and_count_users(
|
||||
def list_users(
|
||||
_parent,
|
||||
%{page: page, limit: limit, sort: sort, direction: direction},
|
||||
%{email: email, page: page, limit: limit, sort: sort, direction: direction},
|
||||
%{context: %{current_user: %User{role: role}}}
|
||||
)
|
||||
when is_moderator(role) do
|
||||
total = Task.async(&Users.count_users/0)
|
||||
elements = Task.async(fn -> Users.list_users(page, limit, sort, direction) end)
|
||||
|
||||
{:ok, %{total: Task.await(total), elements: Task.await(elements)}}
|
||||
{:ok, Users.list_users(email, page, limit, sort, direction)}
|
||||
end
|
||||
|
||||
def list_and_count_users(_parent, _args, _resolution) do
|
||||
def list_users(_parent, _args, _resolution) do
|
||||
{:error, "You need to have admin access to list users"}
|
||||
end
|
||||
|
||||
@@ -242,9 +243,9 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
def user_participations(
|
||||
%User{id: user_id},
|
||||
args,
|
||||
%{context: %{current_user: %User{id: logged_user_id}}}
|
||||
%{context: %{current_user: %User{id: logged_user_id, role: role}}}
|
||||
) do
|
||||
with true <- user_id == logged_user_id,
|
||||
with true <- user_id == logged_user_id or is_moderator(role),
|
||||
%Page{} = page <-
|
||||
Events.list_participations_for_user(
|
||||
user_id,
|
||||
@@ -379,27 +380,58 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
def delete_account(_parent, %{password: password}, %{
|
||||
context: %{current_user: %User{password_hash: password_hash} = user}
|
||||
}) do
|
||||
with {:current_password, true} <-
|
||||
{:current_password, Argon2.verify_pass(password, password_hash)},
|
||||
actors <- Users.get_actors_for_user(user),
|
||||
# Detach actors from user
|
||||
:ok <- Enum.each(actors, fn actor -> Actors.update_actor(actor, %{user_id: nil}) end),
|
||||
# Launch a background job to delete actors
|
||||
:ok <- Enum.each(actors, &Actors.delete_actor/1),
|
||||
# Delete user
|
||||
{:ok, user} <- Users.delete_user(user) do
|
||||
{:ok, user}
|
||||
else
|
||||
case {:current_password, Argon2.verify_pass(password, password_hash)} do
|
||||
{:current_password, true} ->
|
||||
do_delete_account(user)
|
||||
|
||||
{:current_password, false} ->
|
||||
{:error, "The password provided is invalid"}
|
||||
end
|
||||
end
|
||||
|
||||
def delete_account(_parent, %{user_id: user_id}, %{
|
||||
context: %{current_user: %User{role: role}}
|
||||
})
|
||||
when is_moderator(role) do
|
||||
with %User{} = user <- Users.get_user(user_id) do
|
||||
do_delete_account(%User{} = user)
|
||||
end
|
||||
end
|
||||
|
||||
def delete_account(_parent, _args, _resolution) do
|
||||
{:error, "You need to be logged-in to delete your account"}
|
||||
end
|
||||
|
||||
defp do_delete_account(%User{} = user) do
|
||||
with actors <- Users.get_actors_for_user(user),
|
||||
activated <- not is_nil(user.confirmed_at),
|
||||
# Detach actors from user
|
||||
:ok <-
|
||||
if(activated,
|
||||
do: :ok,
|
||||
else: Enum.each(actors, fn actor -> Actors.update_actor(actor, %{user_id: nil}) end)
|
||||
),
|
||||
# Launch a background job to delete actors
|
||||
:ok <-
|
||||
Enum.each(actors, fn actor ->
|
||||
ActivityPub.delete(actor, true)
|
||||
end),
|
||||
# Delete user
|
||||
{:ok, user} <- Users.delete_user(user, reserve_email: activated) do
|
||||
{:ok, user}
|
||||
end
|
||||
end
|
||||
|
||||
@spec user_settings(User.t(), map(), map()) :: {:ok, list(Setting.t())} | {:error, String.t()}
|
||||
def user_settings(%User{} = user, _args, %{
|
||||
context: %{current_user: %User{role: role}}
|
||||
})
|
||||
when is_moderator(role) do
|
||||
with {:setting, settings} <- {:setting, Users.get_setting(user)} do
|
||||
{:ok, settings}
|
||||
end
|
||||
end
|
||||
|
||||
def user_settings(%User{id: user_id} = user, _args, %{
|
||||
context: %{current_user: %User{id: logged_user_id}}
|
||||
}) do
|
||||
|
||||
@@ -17,9 +17,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
||||
Represents a person identity
|
||||
"""
|
||||
object :person do
|
||||
interfaces([:actor])
|
||||
interfaces([:actor, :action_log_object])
|
||||
field(:id, :id, description: "Internal ID for this person")
|
||||
field(:user, :user, description: "The user this actor is associated to")
|
||||
|
||||
field(:user, :user,
|
||||
description: "The user this actor is associated to",
|
||||
resolve: &Person.user_for_person/3
|
||||
)
|
||||
|
||||
field(:member_of, list_of(:member), description: "The list of groups this person is member of")
|
||||
|
||||
@@ -52,16 +56,21 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
||||
)
|
||||
|
||||
# This one should have a privacy setting
|
||||
field(:organized_events, list_of(:event),
|
||||
resolve: dataloader(Events),
|
||||
field(:organized_events, :paginated_event_list,
|
||||
description: "A list of the events this actor has organized"
|
||||
)
|
||||
) do
|
||||
arg(:page, :integer, default_value: 1)
|
||||
arg(:limit, :integer, default_value: 10)
|
||||
resolve(&Person.organized_events_for_person/3)
|
||||
end
|
||||
|
||||
@desc "The list of events this person goes to"
|
||||
field(:participations, list_of(:participant),
|
||||
field(:participations, :paginated_participant_list,
|
||||
description: "The list of events this person goes to"
|
||||
) do
|
||||
arg(:event_id, :id)
|
||||
arg(:page, :integer, default_value: 1)
|
||||
arg(:limit, :integer, default_value: 10)
|
||||
resolve(&Person.person_participations/3)
|
||||
end
|
||||
|
||||
@@ -95,6 +104,17 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
||||
field :identities, list_of(:person) do
|
||||
resolve(&Person.identities/3)
|
||||
end
|
||||
|
||||
field :persons, :persons do
|
||||
arg(:preferred_username, :string, default_value: "")
|
||||
arg(:name, :string, default_value: "")
|
||||
arg(:domain, :string, default_value: "")
|
||||
arg(:local, :boolean, default_value: true)
|
||||
arg(:suspended, :boolean, default_value: false)
|
||||
arg(:page, :integer, default_value: 1)
|
||||
arg(:limit, :integer, default_value: 10)
|
||||
resolve(&Person.list_persons/3)
|
||||
end
|
||||
end
|
||||
|
||||
object :person_mutations do
|
||||
@@ -168,6 +188,16 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
||||
|
||||
resolve(handle_errors(&Person.register_person/3))
|
||||
end
|
||||
|
||||
field :suspend_profile, :deleted_object do
|
||||
arg(:id, :id, description: "The profile ID to suspend")
|
||||
resolve(&Person.suspend_profile/3)
|
||||
end
|
||||
|
||||
field :unsuspend_profile, :person do
|
||||
arg(:id, :id, description: "The profile ID to unsuspend")
|
||||
resolve(&Person.unsuspend_profile/3)
|
||||
end
|
||||
end
|
||||
|
||||
object :person_subscriptions do
|
||||
|
||||
@@ -5,6 +5,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
|
||||
|
||||
use Absinthe.Schema.Notation
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Conversations.Comment
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Reports.{Note, Report}
|
||||
@@ -29,6 +30,8 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
|
||||
value(:event_deletion)
|
||||
value(:comment_deletion)
|
||||
value(:event_update)
|
||||
value(:actor_suspension)
|
||||
value(:actor_unsuspension)
|
||||
end
|
||||
|
||||
@desc "The objects that can be in an action log"
|
||||
@@ -48,6 +51,9 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
|
||||
%Comment{}, _ ->
|
||||
:comment
|
||||
|
||||
%Actor{type: "Person"}, _ ->
|
||||
:person
|
||||
|
||||
_, _ ->
|
||||
nil
|
||||
end)
|
||||
|
||||
@@ -18,7 +18,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||
field(:id, non_null(:id), description: "The user's ID")
|
||||
field(:email, non_null(:string), description: "The user's email")
|
||||
|
||||
field(:profiles, non_null(list_of(:person)),
|
||||
field(:actors, non_null(list_of(:person)),
|
||||
description: "The user's list of profiles (identities)"
|
||||
)
|
||||
|
||||
@@ -51,6 +51,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||
|
||||
field(:locale, :string, description: "The user's locale")
|
||||
|
||||
field(:disabled, :boolean, description: "Whether the user is disabled")
|
||||
|
||||
field(:participations, :paginated_participant_list,
|
||||
description: "The list of participations this user has"
|
||||
) do
|
||||
@@ -144,13 +146,14 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||
|
||||
@desc "List instance users"
|
||||
field :users, :users do
|
||||
arg(:email, :string, default_value: "")
|
||||
arg(:page, :integer, default_value: 1)
|
||||
arg(:limit, :integer, default_value: 10)
|
||||
|
||||
arg(:sort, :sortable_user_field, default_value: :id)
|
||||
arg(:direction, :sort_direction, default_value: :desc)
|
||||
|
||||
resolve(&User.list_and_count_users/3)
|
||||
resolve(&User.list_users/3)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -233,7 +236,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||
|
||||
@desc "Delete an account"
|
||||
field :delete_account, :deleted_object do
|
||||
arg(:password, non_null(:string))
|
||||
arg(:password, :string)
|
||||
arg(:user_id, :id)
|
||||
resolve(&User.delete_account/3)
|
||||
end
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ defmodule Mix.Tasks.Mobilizon.Actors.Refresh do
|
||||
#{total} actors to process
|
||||
""")
|
||||
|
||||
query = from(a in Actor, where: not is_nil(a.domain))
|
||||
query = from(a in Actor, where: not is_nil(a.domain) and not a.suspended)
|
||||
|
||||
{:ok, _res} =
|
||||
Repo.transaction(
|
||||
|
||||
@@ -14,21 +14,24 @@ defmodule Mix.Tasks.Mobilizon.Users.Delete do
|
||||
OptionParser.parse(
|
||||
rest,
|
||||
strict: [
|
||||
assume_yes: :boolean
|
||||
assume_yes: :boolean,
|
||||
force: :boolean
|
||||
],
|
||||
aliases: [
|
||||
y: :assume_yes
|
||||
y: :assume_yes,
|
||||
f: :force
|
||||
]
|
||||
)
|
||||
|
||||
assume_yes? = Keyword.get(options, :assume_yes, false)
|
||||
force? = Keyword.get(options, :force, false)
|
||||
|
||||
Mix.Task.run("app.start")
|
||||
|
||||
with {:ok, %User{} = user} <- Users.get_user_by_email(email),
|
||||
true <- assume_yes? or Mix.shell().yes?("Continue with deleting user #{user.email}?"),
|
||||
{:ok, %User{} = user} <-
|
||||
Users.delete_user(user) do
|
||||
Users.delete_user(user, reserve_email: !force?) do
|
||||
Mix.shell().info("""
|
||||
The user #{user.email} has been deleted
|
||||
""")
|
||||
|
||||
@@ -73,9 +73,9 @@ defmodule Mobilizon.Actors do
|
||||
Gets an actor with preloaded relations.
|
||||
"""
|
||||
@spec get_actor_with_preload(integer | String.t()) :: Actor.t() | nil
|
||||
def get_actor_with_preload(id) do
|
||||
def get_actor_with_preload(id, include_suspended \\ false) do
|
||||
id
|
||||
|> actor_with_preload_query()
|
||||
|> actor_with_preload_query(include_suspended)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@@ -90,6 +90,14 @@ defmodule Mobilizon.Actors do
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@spec get_remote_actor_with_preload(integer | String.t(), boolean()) :: Actor.t() | nil
|
||||
def get_remote_actor_with_preload(id, include_suspended \\ false) do
|
||||
id
|
||||
|> actor_with_preload_query(include_suspended)
|
||||
|> filter_external()
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets an actor by its URL (ActivityPub ID). The `:preload` option allows to
|
||||
preload the followers relation.
|
||||
@@ -255,27 +263,41 @@ defmodule Mobilizon.Actors do
|
||||
end
|
||||
end
|
||||
|
||||
def delete_actor(%Actor{} = actor) do
|
||||
Workers.Background.enqueue("delete_actor", %{"actor_id" => actor.id})
|
||||
@delete_actor_default_options [reserve_username: true]
|
||||
|
||||
def delete_actor(%Actor{} = actor, options \\ @delete_actor_default_options) do
|
||||
delete_actor_options = Keyword.merge(@delete_actor_default_options, options)
|
||||
|
||||
Workers.Background.enqueue("delete_actor", %{
|
||||
"actor_id" => actor.id,
|
||||
"reserve_username" => Keyword.get(delete_actor_options, :reserve_username, true)
|
||||
})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes an actor.
|
||||
"""
|
||||
@spec perform(atom(), Actor.t()) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
|
||||
def perform(:delete_actor, %Actor{} = actor) do
|
||||
def perform(:delete_actor, %Actor{} = actor, options \\ @delete_actor_default_options) do
|
||||
actor = Repo.preload(actor, @actor_preloads)
|
||||
|
||||
transaction =
|
||||
delete_actor_options = Keyword.merge(@delete_actor_default_options, options)
|
||||
|
||||
multi =
|
||||
Multi.new()
|
||||
|> Multi.run(:delete_organized_events, fn _, _ -> delete_actor_organized_events(actor) end)
|
||||
|> Multi.run(:empty_comments, fn _, _ -> delete_actor_empty_comments(actor) end)
|
||||
|> Multi.run(:remove_banner, fn _, _ -> remove_banner(actor) end)
|
||||
|> Multi.run(:remove_avatar, fn _, _ -> remove_avatar(actor) end)
|
||||
|> Multi.update(:actor, Actor.delete_changeset(actor))
|
||||
|> Repo.transaction()
|
||||
|
||||
case transaction do
|
||||
multi =
|
||||
if Keyword.get(delete_actor_options, :reserve_username, true) do
|
||||
Multi.update(multi, :actor, Actor.delete_changeset(actor))
|
||||
else
|
||||
Multi.delete(multi, :actor, actor)
|
||||
end
|
||||
|
||||
case Repo.transaction(multi) do
|
||||
{:ok, %{actor: %Actor{} = actor}} ->
|
||||
{:ok, true} = Cachex.del(:activity_pub, "actor_#{actor.preferred_username}")
|
||||
{:ok, actor}
|
||||
@@ -295,8 +317,57 @@ defmodule Mobilizon.Actors do
|
||||
@doc """
|
||||
Returns the list of actors.
|
||||
"""
|
||||
@spec list_actors :: [Actor.t()]
|
||||
def list_actors, do: Repo.all(Actor)
|
||||
@spec list_actors(String.t(), String.t(), boolean, boolean, integer, integer) :: Page.t()
|
||||
def list_actors(
|
||||
type \\ :Person,
|
||||
preferred_username \\ "",
|
||||
name \\ "",
|
||||
domain \\ "",
|
||||
local \\ true,
|
||||
suspended \\ false,
|
||||
page \\ nil,
|
||||
limit \\ nil
|
||||
)
|
||||
|
||||
def list_actors(
|
||||
:Person,
|
||||
preferred_username,
|
||||
name,
|
||||
domain,
|
||||
local,
|
||||
suspended,
|
||||
page,
|
||||
limit
|
||||
) do
|
||||
person_query()
|
||||
|> filter_suspended(suspended)
|
||||
|> filter_preferred_username(preferred_username)
|
||||
|> filter_name(name)
|
||||
|> filter_domain(domain)
|
||||
|> filter_remote(local)
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
defp filter_preferred_username(query, ""), do: query
|
||||
|
||||
defp filter_preferred_username(query, preferred_username),
|
||||
do: where(query, [a], ilike(a.preferred_username, ^"%#{preferred_username}%"))
|
||||
|
||||
defp filter_name(query, ""), do: query
|
||||
|
||||
defp filter_name(query, name),
|
||||
do: where(query, [a], ilike(a.name, ^"%#{name}%"))
|
||||
|
||||
defp filter_domain(query, ""), do: query
|
||||
|
||||
defp filter_domain(query, domain),
|
||||
do: where(query, [a], ilike(a.domain, ^"%#{domain}%"))
|
||||
|
||||
defp filter_remote(query, true), do: filter_local(query)
|
||||
defp filter_remote(query, false), do: filter_external(query)
|
||||
|
||||
defp filter_suspended(query, true), do: where(query, [a], a.suspended)
|
||||
defp filter_suspended(query, false), do: where(query, [a], not a.suspended)
|
||||
|
||||
@doc """
|
||||
Returns the list of local actors by their username.
|
||||
@@ -945,13 +1016,19 @@ defmodule Mobilizon.Actors do
|
||||
changeset
|
||||
end
|
||||
|
||||
@spec actor_with_preload_query(integer | String.t()) :: Ecto.Query.t()
|
||||
defp actor_with_preload_query(actor_id) do
|
||||
from(
|
||||
a in Actor,
|
||||
where: a.id == ^actor_id and not a.suspended,
|
||||
preload: [:organized_events, :followers, :followings]
|
||||
)
|
||||
@spec actor_with_preload_query(integer | String.t(), boolean()) :: Ecto.Query.t()
|
||||
defp actor_with_preload_query(actor_id, include_suspended \\ false)
|
||||
|
||||
defp actor_with_preload_query(actor_id, false) do
|
||||
actor_id
|
||||
|> actor_with_preload_query(true)
|
||||
|> where([a], not a.suspended)
|
||||
end
|
||||
|
||||
defp actor_with_preload_query(actor_id, true) do
|
||||
Actor
|
||||
|> where([a], a.id == ^actor_id)
|
||||
|> preload([a], [:organized_events, :followers, :followings])
|
||||
end
|
||||
|
||||
@spec actor_by_username_query(String.t()) :: Ecto.Query.t()
|
||||
@@ -1000,6 +1077,11 @@ defmodule Mobilizon.Actors do
|
||||
)
|
||||
end
|
||||
|
||||
@spec person_query :: Ecto.Query.t()
|
||||
defp person_query do
|
||||
from(a in Actor, where: a.type == ^:Person)
|
||||
end
|
||||
|
||||
@spec group_query :: Ecto.Query.t()
|
||||
defp group_query do
|
||||
from(a in Actor, where: a.type == ^:Group)
|
||||
|
||||
@@ -16,7 +16,9 @@ defmodule Mobilizon.Admin do
|
||||
defenum(ActionLogAction, [
|
||||
"update",
|
||||
"create",
|
||||
"delete"
|
||||
"delete",
|
||||
"suspend",
|
||||
"unsuspend"
|
||||
])
|
||||
|
||||
alias Ecto.Multi
|
||||
|
||||
@@ -401,6 +401,14 @@ defmodule Mobilizon.Events do
|
||||
{:ok, events, events_count}
|
||||
end
|
||||
|
||||
@spec list_organized_events_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def list_organized_events_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
|
||||
actor_id
|
||||
|> event_for_actor_query()
|
||||
|> preload_for_event()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@spec list_organized_events_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def list_organized_events_for_group(%Actor{id: group_id}, page \\ nil, limit \\ nil) do
|
||||
group_id
|
||||
@@ -842,13 +850,11 @@ defmodule Mobilizon.Events do
|
||||
@doc """
|
||||
Returns the list of participations for an actor.
|
||||
"""
|
||||
@spec list_event_participations_for_actor(Actor.t(), integer | nil, integer | nil) ::
|
||||
[Participant.t()]
|
||||
@spec list_event_participations_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def list_event_participations_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
|
||||
actor_id
|
||||
|> event_participations_for_actor_query()
|
||||
|> Page.paginate(page, limit)
|
||||
|> Repo.all()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1414,7 +1420,8 @@ defmodule Mobilizon.Events do
|
||||
join: e in Event,
|
||||
on: p.event_id == e.id,
|
||||
where: p.actor_id == ^actor_id and p.role != ^:not_approved,
|
||||
preload: [:event]
|
||||
preload: [:event],
|
||||
order_by: [desc: e.begins_on]
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ defmodule Mobilizon.Media.File do
|
||||
@optional_attrs [:content_type, :size]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@derive Jason.Encoder
|
||||
embedded_schema do
|
||||
field(:name, :string)
|
||||
field(:url, :string)
|
||||
|
||||
@@ -25,6 +25,7 @@ defmodule Mobilizon.Users.User do
|
||||
reset_password_token: String.t(),
|
||||
locale: String.t(),
|
||||
default_actor: Actor.t(),
|
||||
disabled: boolean(),
|
||||
actors: [Actor.t()],
|
||||
feed_tokens: [FeedToken.t()]
|
||||
}
|
||||
@@ -40,7 +41,8 @@ defmodule Mobilizon.Users.User do
|
||||
:reset_password_sent_at,
|
||||
:reset_password_token,
|
||||
:locale,
|
||||
:unconfirmed_email
|
||||
:unconfirmed_email,
|
||||
:disabled
|
||||
]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@@ -64,6 +66,7 @@ defmodule Mobilizon.Users.User do
|
||||
field(:reset_password_token, :string)
|
||||
field(:unconfirmed_email, :string)
|
||||
field(:locale, :string, default: "en")
|
||||
field(:disabled, :boolean, default: false)
|
||||
|
||||
belongs_to(:default_actor, Actor)
|
||||
has_many(:actors, Actor)
|
||||
@@ -91,6 +94,13 @@ defmodule Mobilizon.Users.User do
|
||||
end
|
||||
end
|
||||
|
||||
def delete_changeset(%__MODULE__{} = user) do
|
||||
user
|
||||
|> change()
|
||||
|> put_change(:disabled, true)
|
||||
|> put_change(:default_actor_id, nil)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec registration_changeset(t, map) :: Ecto.Changeset.t()
|
||||
def registration_changeset(%__MODULE__{} = user, attrs) do
|
||||
|
||||
@@ -8,8 +8,10 @@ defmodule Mobilizon.Users do
|
||||
|
||||
import Mobilizon.Storage.Ecto
|
||||
|
||||
alias Ecto.Multi
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.FeedToken
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
alias Mobilizon.Users.{Setting, User}
|
||||
|
||||
@@ -46,7 +48,8 @@ defmodule Mobilizon.Users do
|
||||
@spec get_user!(integer | String.t()) :: User.t()
|
||||
def get_user!(id), do: Repo.get!(User, id)
|
||||
|
||||
@spec get_user(integer | String.t()) :: User.t() | nil
|
||||
@spec get_user(integer | String.t() | nil) :: User.t() | nil
|
||||
def get_user(nil), do: nil
|
||||
def get_user(id), do: Repo.get(User, id)
|
||||
|
||||
def get_user_with_settings!(id) do
|
||||
@@ -105,11 +108,35 @@ defmodule Mobilizon.Users do
|
||||
end
|
||||
end
|
||||
|
||||
@delete_user_default_options [reserve_email: true]
|
||||
|
||||
@doc """
|
||||
Deletes an user.
|
||||
"""
|
||||
@spec delete_user(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete_user(%User{} = user), do: Repo.delete(user)
|
||||
def delete_user(%User{id: user_id} = user, options \\ @delete_user_default_options) do
|
||||
delete_user_options = Keyword.merge(@delete_user_default_options, options)
|
||||
|
||||
multi =
|
||||
Multi.new()
|
||||
|> Multi.delete_all(:settings, from(s in Setting, where: s.user_id == ^user_id))
|
||||
|> Multi.delete_all(:feed_tokens, from(f in FeedToken, where: f.user_id == ^user_id))
|
||||
|
||||
multi =
|
||||
if Keyword.get(delete_user_options, :reserve_email, true) do
|
||||
Multi.update(multi, :user, User.delete_changeset(user))
|
||||
else
|
||||
Multi.delete(multi, :user, user)
|
||||
end
|
||||
|
||||
case Repo.transaction(multi) do
|
||||
{:ok, %{user: %User{} = user}} ->
|
||||
{:ok, user}
|
||||
|
||||
{:error, remove, error, _} when remove in [:settings, :feed_tokens] ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get an user with its actors
|
||||
@@ -196,12 +223,22 @@ defmodule Mobilizon.Users do
|
||||
@doc """
|
||||
Returns the list of users.
|
||||
"""
|
||||
@spec list_users(integer | nil, integer | nil, atom | nil, atom | nil) :: [User.t()]
|
||||
def list_users(page \\ nil, limit \\ nil, sort \\ nil, direction \\ nil) do
|
||||
@spec list_users(String.t(), integer | nil, integer | nil, atom | nil, atom | nil) :: Page.t()
|
||||
def list_users(email \\ "", page \\ nil, limit \\ nil, sort \\ nil, direction \\ nil)
|
||||
|
||||
def list_users("", page, limit, sort, direction) do
|
||||
User
|
||||
|> Page.paginate(page, limit)
|
||||
|> sort(sort, direction)
|
||||
|> Repo.all()
|
||||
|> preload([u], [:actors, :feed_tokens, :settings, :default_actor])
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
def list_users(email, page, limit, sort, direction) do
|
||||
User
|
||||
|> where([u], ilike(u.email, ^"%#{email}%"))
|
||||
|> sort(sort, direction)
|
||||
|> preload([u], [:actors, :feed_tokens, :settings, :default_actor])
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -10,6 +10,7 @@ defmodule Mobilizon.Service.Export.Feed do
|
||||
alias Mobilizon.{Actors, Events, Users}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.{Event, FeedToken}
|
||||
alias Mobilizon.Storage.Page
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
alias Mobilizon.Web.{Endpoint, MediaProxy}
|
||||
@@ -148,12 +149,12 @@ defmodule Mobilizon.Service.Export.Feed do
|
||||
end
|
||||
|
||||
defp fetch_identity_participations(%Actor{} = actor) do
|
||||
with events <- Events.list_event_participations_for_actor(actor) do
|
||||
events
|
||||
with %Page{} = page <- Events.list_event_participations_for_actor(actor) do
|
||||
page
|
||||
end
|
||||
end
|
||||
|
||||
defp participations_to_events(participations) do
|
||||
defp participations_to_events(%Page{elements: participations}) do
|
||||
participations
|
||||
|> Enum.map(& &1.event_id)
|
||||
|> Enum.map(&Events.get_event_with_preload!/1)
|
||||
|
||||
@@ -7,6 +7,7 @@ defmodule Mobilizon.Service.Export.ICalendar do
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.Events.{Event, FeedToken}
|
||||
alias Mobilizon.Storage.Page
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
@doc """
|
||||
@@ -123,7 +124,7 @@ defmodule Mobilizon.Service.Export.ICalendar do
|
||||
end
|
||||
end
|
||||
|
||||
defp participations_to_events(participations) do
|
||||
defp participations_to_events(%Page{elements: participations}) do
|
||||
participations
|
||||
|> Enum.map(& &1.event_id)
|
||||
|> Enum.map(&Events.get_event_with_preload!/1)
|
||||
|
||||
@@ -9,9 +9,11 @@ defmodule Mobilizon.Service.Workers.Background do
|
||||
use Mobilizon.Service.Workers.Helper, queue: "background"
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%{"op" => "delete_actor", "actor_id" => actor_id}, _job) do
|
||||
with %Actor{} = actor <- Actors.get_actor(actor_id) do
|
||||
Actors.perform(:delete_actor, actor)
|
||||
def perform(%{"op" => "delete_actor", "actor_id" => actor_id} = args, _job) do
|
||||
with reserve_username when is_boolean(reserve_username) <-
|
||||
Map.get(args, "reserve_username", true),
|
||||
%Actor{} = actor <- Actors.get_actor(actor_id) do
|
||||
Actors.perform(:delete_actor, actor, reserve_username: reserve_username)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user