forked from potsda.mn/mobilizon
2a57340a82
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
306 lines
10 KiB
Elixir
306 lines
10 KiB
Elixir
defmodule Mobilizon.Service.ActorSuspension do
|
|
@moduledoc """
|
|
Handle actor suspensions
|
|
"""
|
|
|
|
alias Ecto.Multi
|
|
alias Mobilizon.{Actors, Events, Medias, Users}
|
|
alias Mobilizon.Actors.{Actor, Member}
|
|
alias Mobilizon.Discussions.{Comment, Discussion}
|
|
alias Mobilizon.Events.{Event, Participant}
|
|
alias Mobilizon.Medias.File
|
|
alias Mobilizon.Posts.Post
|
|
alias Mobilizon.Resources.Resource
|
|
alias Mobilizon.Service.ErrorReporting.Sentry
|
|
alias Mobilizon.Service.Export.Cachable
|
|
alias Mobilizon.Storage.Repo
|
|
alias Mobilizon.Users.User
|
|
alias Mobilizon.Web.Email.Actor, as: ActorEmail
|
|
alias Mobilizon.Web.Email.Group
|
|
require Logger
|
|
import Ecto.Query
|
|
|
|
@actor_preloads [:user, :organized_events, :participations, :comments]
|
|
@delete_actor_default_options [reserve_username: true, suspension: false]
|
|
@valid_actor_types [:Person, :Group]
|
|
|
|
@doc """
|
|
Deletes an actor.
|
|
"""
|
|
@spec suspend_actor(Actor.t(), Keyword.t()) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
|
|
def suspend_actor(%Actor{} = actor, options \\ @delete_actor_default_options) do
|
|
Logger.info("Going to delete actor #{actor.url}")
|
|
actor = Repo.preload(actor, @actor_preloads)
|
|
|
|
delete_actor_options = Keyword.merge(@delete_actor_default_options, options)
|
|
Logger.debug(inspect(delete_actor_options))
|
|
|
|
send_suspension_notification(actor)
|
|
|
|
Logger.debug(
|
|
"Sending suspension notifications to participants from events created by this actor"
|
|
)
|
|
|
|
notify_event_participants_from_suspension(actor)
|
|
delete_participations(actor)
|
|
|
|
multi =
|
|
Multi.new()
|
|
|> maybe_reset_actor_id(actor)
|
|
|> delete_actor_empty_comments(actor)
|
|
|> Multi.run(:remove_banner, fn _, _ -> remove_banner(actor) end)
|
|
|> Multi.run(:remove_avatar, fn _, _ -> remove_avatar(actor) end)
|
|
|
|
multi =
|
|
if Keyword.get(delete_actor_options, :reserve_username, true) do
|
|
multi
|
|
|> delete_actor_events(actor)
|
|
|> delete_posts(actor)
|
|
|> delete_ressources(actor)
|
|
|> delete_discussions(actor)
|
|
|> delete_members(actor)
|
|
|> Multi.update(:actor, Actor.delete_changeset(actor))
|
|
else
|
|
Multi.delete(multi, :actor, actor)
|
|
end
|
|
|
|
Logger.debug("Going to run the transaction")
|
|
|
|
case Repo.transaction(multi, timeout: 60_000) do
|
|
{:ok, %{actor: %Actor{} = actor}} ->
|
|
{:ok, true} = Cachex.del(:activity_pub, "actor_#{actor.preferred_username}")
|
|
Cachable.clear_all_caches(actor)
|
|
Logger.info("Deleted actor #{actor.url}")
|
|
{:ok, actor}
|
|
|
|
{:error, remove, error, _} when remove in [:remove_banner, :remove_avatar] ->
|
|
Logger.error("Error while deleting actor's banner or avatar")
|
|
|
|
Sentry.capture_message("Error while deleting actor's banner or avatar",
|
|
extra: %{err: error}
|
|
)
|
|
|
|
Logger.debug(inspect(error, pretty: true))
|
|
{:error, error}
|
|
|
|
err ->
|
|
Logger.error("Unknown error while deleting actor")
|
|
|
|
Sentry.capture_message("Error while deleting actor's banner or avatar",
|
|
extra: %{err: err}
|
|
)
|
|
|
|
Logger.debug(inspect(err, pretty: true))
|
|
{:error, err}
|
|
end
|
|
end
|
|
|
|
# When deleting a profile, reset default_actor_id
|
|
@spec maybe_reset_actor_id(Multi.t(), Actor.t()) :: Multi.t()
|
|
defp maybe_reset_actor_id(%Multi{} = multi, %Actor{type: :Person} = actor) do
|
|
Multi.run(multi, :reset_default_actor_id, fn _, _ ->
|
|
reset_default_actor_id(actor)
|
|
end)
|
|
end
|
|
|
|
defp maybe_reset_actor_id(%Multi{} = multi, %Actor{type: :Group} = _actor) do
|
|
multi
|
|
end
|
|
|
|
defp delete_actor_empty_comments(%Multi{} = multi, %Actor{id: actor_id}) do
|
|
now = DateTime.utc_now() |> DateTime.truncate(:second)
|
|
|
|
Multi.update_all(multi, :empty_comments, where(Comment, [c], c.actor_id == ^actor_id),
|
|
set: [
|
|
text: nil,
|
|
actor_id: nil,
|
|
deleted_at: now
|
|
]
|
|
)
|
|
end
|
|
|
|
@spec notify_event_participants_from_suspension(Actor.t()) :: :ok
|
|
defp notify_event_participants_from_suspension(%Actor{id: actor_id, type: actor_type} = actor)
|
|
when actor_type in @valid_actor_types do
|
|
actor
|
|
|> get_actor_organizer_events_participations()
|
|
|> preload([:actor, :event])
|
|
|> Repo.all()
|
|
|> Enum.filter(fn %Participant{actor: %Actor{id: participant_actor_id}} ->
|
|
participant_actor_id != actor_id
|
|
end)
|
|
|> Enum.map(fn %Participant{} = participant ->
|
|
ActorEmail.send_notification_event_participants_from_suspension(participant, actor)
|
|
participant
|
|
end)
|
|
|> Enum.each(&Events.delete_participant/1)
|
|
end
|
|
|
|
defp notify_event_participants_from_suspension(_), do: :ok
|
|
|
|
@spec get_actor_organizer_events_participations(Actor.t()) :: Ecto.Query.t()
|
|
defp get_actor_organizer_events_participations(%Actor{type: :Person, id: actor_id}) do
|
|
do_get_actor_organizer_events_participations()
|
|
|> where([_p, e], e.organizer_actor_id == ^actor_id)
|
|
end
|
|
|
|
defp get_actor_organizer_events_participations(%Actor{type: :Group, id: actor_id}) do
|
|
do_get_actor_organizer_events_participations()
|
|
|> where([_p, e], e.attributed_to_id == ^actor_id)
|
|
end
|
|
|
|
@spec do_get_actor_organizer_events_participations :: Ecto.Query.t()
|
|
defp do_get_actor_organizer_events_participations do
|
|
Participant
|
|
|> join(:inner, [p], e in Event, on: p.event_id == e.id)
|
|
|> where([_p, e], e.begins_on > ^DateTime.utc_now())
|
|
|> where([p, _e], p.role in [:participant, :moderator, :administrator])
|
|
end
|
|
|
|
@spec delete_actor_events(Ecto.Multi.t(), Actor.t()) :: Ecto.Multi.t()
|
|
defp delete_actor_events(%Multi{} = multi, %Actor{type: :Person, id: actor_id}) do
|
|
Logger.debug("Delete profile's events")
|
|
Multi.delete_all(multi, :delete_events, where(Event, [e], e.organizer_actor_id == ^actor_id))
|
|
end
|
|
|
|
defp delete_actor_events(%Multi{} = multi, %Actor{type: :Group, id: actor_id}) do
|
|
Logger.debug("Delete group's events")
|
|
Multi.delete_all(multi, :delete_events, where(Event, [e], e.attributed_to_id == ^actor_id))
|
|
end
|
|
|
|
defp delete_posts(%Multi{} = multi, %Actor{type: :Person, id: actor_id}) do
|
|
Logger.debug("Delete profile's posts")
|
|
Multi.delete_all(multi, :delete_posts, where(Post, [e], e.author_id == ^actor_id))
|
|
end
|
|
|
|
defp delete_posts(%Multi{} = multi, %Actor{type: :Group, id: actor_id}) do
|
|
Logger.debug("Delete group's posts")
|
|
Multi.delete_all(multi, :delete_posts, where(Post, [e], e.attributed_to_id == ^actor_id))
|
|
end
|
|
|
|
defp delete_ressources(%Multi{} = multi, %Actor{type: :Person, id: actor_id}) do
|
|
Logger.debug("Delete profile's resources")
|
|
Multi.delete_all(multi, :delete_resources, where(Resource, [e], e.creator_id == ^actor_id))
|
|
end
|
|
|
|
defp delete_ressources(%Multi{} = multi, %Actor{type: :Group, id: actor_id}) do
|
|
Logger.debug("Delete group's resources")
|
|
Multi.delete_all(multi, :delete_resources, where(Resource, [e], e.actor_id == ^actor_id))
|
|
end
|
|
|
|
# Keep discussions just in case, comments are already emptied
|
|
defp delete_discussions(%Multi{} = multi, %Actor{type: :Person}) do
|
|
multi
|
|
end
|
|
|
|
defp delete_discussions(%Multi{} = multi, %Actor{type: :Group, id: actor_id}) do
|
|
Logger.debug("Delete group's discussions")
|
|
|
|
multi =
|
|
Multi.run(multi, :group_discussion_comments, fn _, _ ->
|
|
group_comments_ids =
|
|
Comment
|
|
|> join(:inner, [c], d in Discussion, on: c.discussion_id == d.id)
|
|
|> where([_c, d], d.actor_id == ^actor_id)
|
|
|> select([c], [c.id])
|
|
|> Repo.all()
|
|
|> Enum.concat()
|
|
|
|
{:ok, group_comments_ids}
|
|
end)
|
|
|
|
multi =
|
|
Multi.delete_all(
|
|
multi,
|
|
:delete_discussions_comments,
|
|
fn %{group_discussion_comments: group_discussion_comments} ->
|
|
where(Comment, [c], c.id in ^group_discussion_comments)
|
|
end
|
|
)
|
|
|
|
Multi.delete_all(multi, :delete_discussions, where(Discussion, [e], e.actor_id == ^actor_id))
|
|
end
|
|
|
|
@spec delete_participations(Actor.t()) :: :ok
|
|
defp delete_participations(%Actor{type: :Person} = actor) do
|
|
Logger.debug("Delete participations from events created by this actor")
|
|
%Actor{participations: participations} = Repo.preload(actor, [:participations])
|
|
Enum.each(participations, &Events.delete_participant/1)
|
|
end
|
|
|
|
# Ignore for all other types of actors
|
|
defp delete_participations(%Actor{}), do: :ok
|
|
|
|
@spec delete_members(Multi.t(), Actor.t()) :: Multi.t()
|
|
defp delete_members(%Multi{} = multi, %Actor{type: :Person, id: actor_id}) do
|
|
Multi.delete_all(multi, :delete_members, where(Member, [e], e.actor_id == ^actor_id))
|
|
end
|
|
|
|
defp delete_members(%Multi{} = multi, %Actor{type: :Group, id: actor_id}) do
|
|
Multi.delete_all(multi, :delete_members, where(Member, [e], e.parent_id == ^actor_id))
|
|
end
|
|
|
|
@spec reset_default_actor_id(Actor.t()) :: {:ok, User.t() | nil} | {:error, :user_not_found}
|
|
defp reset_default_actor_id(%Actor{type: :Person, user: %User{} = user, id: actor_id}) do
|
|
Logger.debug("reset_default_actor_id")
|
|
|
|
new_actor =
|
|
user
|
|
|> Users.get_actors_for_user()
|
|
|> Enum.find(&(&1.id !== actor_id))
|
|
|
|
{:ok, Users.update_user_default_actor(user, new_actor)}
|
|
rescue
|
|
_e in Ecto.NoResultsError ->
|
|
{:error, :user_not_found}
|
|
end
|
|
|
|
defp reset_default_actor_id(%Actor{type: :Person, user: nil}), do: {:ok, nil}
|
|
|
|
@spec remove_banner(Actor.t()) :: {:ok, Actor.t()}
|
|
defp remove_banner(%Actor{banner: nil} = actor), do: {:ok, actor}
|
|
|
|
defp remove_banner(%Actor{banner: %File{url: url}} = actor) do
|
|
safe_remove_file(url, actor)
|
|
{:ok, actor}
|
|
end
|
|
|
|
@spec remove_avatar(Actor.t()) :: {:ok, Actor.t()}
|
|
defp remove_avatar(%Actor{avatar: avatar} = actor) do
|
|
case avatar do
|
|
%File{url: url} ->
|
|
safe_remove_file(url, actor)
|
|
{:ok, actor}
|
|
|
|
nil ->
|
|
{:ok, actor}
|
|
end
|
|
end
|
|
|
|
@spec safe_remove_file(String.t(), Actor.t()) :: {:ok, Actor.t()}
|
|
defp safe_remove_file(url, %Actor{} = actor) do
|
|
case Medias.delete_user_profile_media_by_url(url) do
|
|
{:ok, _value} ->
|
|
{:ok, actor}
|
|
|
|
{:error, error} ->
|
|
Logger.error(
|
|
"Error while removing an upload file: #{inspect(error)}, actor: #{Actor.preferred_username_and_domain(actor)}, file_url: #{url}"
|
|
)
|
|
|
|
{:ok, actor}
|
|
end
|
|
end
|
|
|
|
@spec send_suspension_notification(Actor.t()) :: :ok
|
|
defp send_suspension_notification(%Actor{type: :Group} = group) do
|
|
Logger.debug("Sending suspension notifications to group members")
|
|
|
|
group
|
|
|> Actors.list_all_local_members_for_group()
|
|
|> Enum.each(&Group.send_group_suspension_notification/1)
|
|
end
|
|
|
|
defp send_suspension_notification(%Actor{} = _actor), do: :ok
|
|
end
|