Compare commits

...

2 Commits

Author SHA1 Message Date
rinpatch
ce20a74b20 Fix deletion by making it possible to insert activities as a deactivated user 2021-02-25 12:41:22 +03:00
rinpatch
4286a383df Improve user deletion consistency
An attempt to ensure something like
https://git.pleroma.social/pleroma/pleroma/-/issues/1415 does not happen
or is at least debuggable.

- Deactivate the user before deletion to ensure no new posts/follows
can be made during it
- Run the deletion in a transaction. This should reduce performance
impact of a deletion since it will only use a single connection. Also
makes sure an account cannot get stuck in a weird state between deleted
and active. Made it possible to disable though, in case someone hits
the issue mentioned above.
- Log more errors
2021-02-24 18:52:39 +03:00
28 changed files with 295 additions and 138 deletions

View File

@ -652,7 +652,9 @@ config :pleroma, :oauth2,
issue_new_refresh_token: true, issue_new_refresh_token: true,
clean_expired_tokens: false clean_expired_tokens: false
config :pleroma, :database, rum_enabled: false config :pleroma, :database,
rum_enabled: false,
rollback_on_activity_deletion_errors: true
config :pleroma, :env, Mix.env() config :pleroma, :env, Mix.env()

View File

@ -72,6 +72,20 @@ frontend_options = [
config :pleroma, :config_description, [ config :pleroma, :config_description, [
%{ %{
group: :pleroma, group: :pleroma,
key: :database,
type: :group,
description: "Database settings",
children: [
%{
key: :rollback_on_activity_deletion_errors,
type: :boolean,
description:
"Rollback the transaction if Pleroma fails to delete an activity during user deletion. If you need to disable this, please report the issue you were having on the bugtracker."
}
]
},
%{
group: :pleroma,
key: Pleroma.Upload, key: Pleroma.Upload,
type: :group, type: :group,
description: "Upload general settings", description: "Upload general settings",

View File

@ -133,6 +133,10 @@ config :pleroma, :side_effects,
ap_streamer: Pleroma.Web.ActivityPub.ActivityPubMock, ap_streamer: Pleroma.Web.ActivityPub.ActivityPubMock,
logger: Pleroma.LoggerMock logger: Pleroma.LoggerMock
# Disable transaction check by default unless the test wants otherwise
# because all tests run in a transaction.
config :pleroma, Pleroma.Workers.BackgroundWorker, ignore_transaction_check: true
if File.exists?("./config/test.secret.exs") do if File.exists?("./config/test.secret.exs") do
import_config "test.secret.exs" import_config "test.secret.exs"
else else

View File

@ -37,7 +37,7 @@ defmodule Pleroma.Object.Fetcher do
Logger.debug("Reinjecting object #{new_data["id"]}") Logger.debug("Reinjecting object #{new_data["id"]}")
with data <- maybe_reinject_internal_fields(object, new_data), with data <- maybe_reinject_internal_fields(object, new_data),
{:ok, data, _} <- ObjectValidator.validate(data, %{}), {:ok, data, _} <- ObjectValidator.validate(data, []),
changeset <- Object.change(object, %{data: data}), changeset <- Object.change(object, %{data: data}),
changeset <- touch_changeset(changeset), changeset <- touch_changeset(changeset),
{:ok, object} <- Repo.insert_or_update(changeset), {:ok, object} <- Repo.insert_or_update(changeset),

View File

@ -1072,7 +1072,19 @@ defmodule Pleroma.User do
def update_and_set_cache(changeset) do def update_and_set_cache(changeset) do
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
set_cache(user) BackgroundWorker.execute_or_enqueue_if_in_transaction(fn
false ->
set_cache(user)
# If the function has been enqueued, there is a chance something changed
# before the worker got to executing it, so refetch the user from the database
true ->
user.id
|> get_by_id()
|> set_cache()
end)
{:ok, user}
end end
end end
@ -1339,7 +1351,7 @@ defmodule Pleroma.User do
user user
|> follow_information_changeset(%{follower_count: follower_count}) |> follow_information_changeset(%{follower_count: follower_count})
|> update_and_set_cache |> update_and_set_cache()
else else
{:ok, maybe_fetch_follow_information(user)} {:ok, maybe_fetch_follow_information(user)}
end end
@ -1726,48 +1738,83 @@ defmodule Pleroma.User do
defp delete_or_deactivate(%User{local: false} = user), do: delete_and_invalidate_cache(user) defp delete_or_deactivate(%User{local: false} = user), do: delete_and_invalidate_cache(user)
defp delete_or_deactivate(%User{is_confirmed: false} = user),
do: delete_and_invalidate_cache(user)
defp delete_or_deactivate(%User{is_approved: false} = user),
do: delete_and_invalidate_cache(user)
defp delete_or_deactivate(%User{local: true} = user) do defp delete_or_deactivate(%User{local: true} = user) do
status = account_status(user) user
|> purge_user_changeset()
case status do |> update_and_set_cache()
:confirmation_pending ->
delete_and_invalidate_cache(user)
:approval_pending ->
delete_and_invalidate_cache(user)
_ ->
user
|> purge_user_changeset()
|> update_and_set_cache()
end
end end
def perform(:force_password_reset, user), do: force_password_reset(user) def perform(:force_password_reset, user), do: force_password_reset(user)
@spec perform(atom(), User.t()) :: {:ok, User.t()} @spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:delete, %User{} = user) do def perform(:delete, %User{} = user) do
# Remove all relationships # Deactivate the user before starting the deletion
user # to make sure they are not able to make new posts/follows during it
|> get_followers() {:ok, user} = set_activation_status(user, false)
|> Enum.each(fn follower ->
ActivityPub.unfollow(follower, user)
unfollow(follower, user)
end)
user Repo.transaction(
|> get_friends() fn ->
|> Enum.each(fn followed -> # Remove all relationships
ActivityPub.unfollow(user, followed) # No need to handle errors from ActivityPub.unfollow because
unfollow(user, followed) # they will automatically rollback the transaction.
end) user
|> get_followers()
|> Enum.each(fn follower ->
ActivityPub.unfollow(follower, user)
unfollow(follower, user)
end)
delete_user_activities(user) user
delete_notifications_from_user_activities(user) |> get_friends()
|> Enum.each(fn followed ->
ActivityPub.unfollow(user, followed, nil, true, true)
unfollow(user, followed)
end)
delete_outgoing_pending_follow_requests(user) rollback_on_activity_deletion_errors =
Config.get([:database, :rollback_on_activity_deletion_errors], true)
delete_or_deactivate(user) case {delete_user_activities(user), rollback_on_activity_deletion_errors} do
{res, rollback} when res == :ok or rollback == false ->
case res do
{:error, _} ->
Logger.warn(fn ->
"Deleting #{user.ap_id}: Failed deleting some of the activities, proceeding anyway."
end)
_ ->
:noop
end
delete_notifications_from_user_activities(user)
delete_outgoing_pending_follow_requests(user)
case delete_or_deactivate(user) do
{:ok, user} -> user
{:error, e} -> Repo.rollback(e)
end
{{:error, e}, true} ->
Logger.error(fn ->
"""
Deleting #{user.ap_id}: Failed deleting some of the activities, rolling back.
Set `config :pleroma, :database, rollback_on_activity_deletion_errors: true`
and restart the deletion if you want to continue anyway. Please report this on Pleroma bugtracker.
"""
end)
Repo.rollback({:deleting_activities, e})
end
end,
timeout: :infinity
)
end end
def perform(:set_activation_async, user, status), do: set_activation(user, status) def perform(:set_activation_async, user, status), do: set_activation(user, status)
@ -1807,20 +1854,52 @@ defmodule Pleroma.User do
|> Repo.delete_all() |> Repo.delete_all()
end end
@type activity_id :: String.t()
@spec delete_user_activities(User.t()) ::
:ok | {:error, [{:error, activity_id(), any()}]}
def delete_user_activities(%User{ap_id: ap_id} = user) do def delete_user_activities(%User{ap_id: ap_id} = user) do
ap_id errors =
|> Activity.Queries.by_actor() ap_id
|> Repo.chunk_stream(50, :batches) |> Activity.Queries.by_actor()
|> Stream.each(fn activities -> |> Repo.chunk_stream(50)
Enum.each(activities, fn activity -> delete_activity(activity, user) end) |> Stream.flat_map(fn activity ->
end) case delete_activity(activity, user) do
|> Stream.run() {:ok, _activity, _meta} ->
[]
{:error, error} ->
Logger.error(fn ->
"Deleting #{ap_id}: could not delete or undo #{activity.data["id"]}.\n Reason: #{
inspect(error)
}"
end)
[{:error, activity.id, error}]
:noop ->
Logger.debug(fn ->
"Deleting #{ap_id}: nothing to do for #{activity.data["id"]} of type #{
activity.data["type"]
}"
end)
[]
end
end)
|> Enum.to_list()
case errors do
[] -> :ok
errors -> {:error, errors}
end
end end
@spec delete_activity(Pleroma.Activity.t(), User.t()) ::
{:ok, Activity.t(), keyword()} | {:error, any()} | :noop
defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)}, with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
{:ok, delete_data, _} <- Builder.delete(user, object) do {:ok, delete_data, _} <- Builder.delete(user, object) do
Pipeline.common_pipeline(delete_data, local: user.local) Pipeline.common_pipeline(delete_data, local: user.local, allow_deactivated_actor: true)
else else
{:find_object, nil} -> {:find_object, nil} ->
# We have the create activity, but not the object, it was probably pruned. # We have the create activity, but not the object, it was probably pruned.
@ -1831,18 +1910,20 @@ defmodule Pleroma.User do
end end
e -> e ->
Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}") e
Logger.error("Error: #{inspect(e)}")
end end
end end
defp delete_activity(%{data: %{"type" => type}} = activity, user) defp delete_activity(%{data: %{"type" => type}} = activity, user)
when type in ["Like", "Announce"] do when type in ["Like", "Announce"] do
{:ok, undo, _} = Builder.undo(user, activity) with {:ok, undo, _} <- Builder.undo(user, activity) do
Pipeline.common_pipeline(undo, local: user.local) Pipeline.common_pipeline(undo, local: user.local, allow_deactivated_actor: true)
else
e -> e
end
end end
defp delete_activity(_activity, _user), do: "Doing nothing" defp delete_activity(_activity, _user), do: :noop
defp delete_outgoing_pending_follow_requests(user) do defp delete_outgoing_pending_follow_requests(user) do
user user

View File

@ -318,20 +318,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end end
end end
@spec unfollow(User.t(), User.t(), String.t() | nil, boolean()) :: @spec unfollow(User.t(), User.t(), String.t() | nil, boolean(), boolean()) ::
{:ok, Activity.t()} | nil | {:error, any()} {:ok, Activity.t()} | nil | {:error, any()}
def unfollow(follower, followed, activity_id \\ nil, local \\ true) do def unfollow(follower, followed, activity_id \\ nil, local \\ true, bypass_actor_check \\ false) do
with {:ok, result} <- with {:ok, result} <-
Repo.transaction(fn -> do_unfollow(follower, followed, activity_id, local) end) do Repo.transaction(fn ->
do_unfollow(follower, followed, activity_id, local, bypass_actor_check)
end) do
result result
end end
end end
defp do_unfollow(follower, followed, activity_id, local) do defp do_unfollow(follower, followed, activity_id, local, bypass_actor_check) do
with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed), with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"), {:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id), unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
{:ok, activity} <- insert(unfollow_data, local), {:ok, activity} <- insert(unfollow_data, local, false, bypass_actor_check),
_ <- notify_and_stream(activity), _ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}

View File

@ -41,7 +41,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
when type in ~w[Accept Reject] do when type in ~w[Accept Reject] do
with {:ok, object} <- with {:ok, object} <-
object object
|> AcceptRejectValidator.cast_and_validate() |> AcceptRejectValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do |> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object) object = stringify_keys(object)
{:ok, object, meta} {:ok, object, meta}
@ -51,7 +51,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def validate(%{"type" => "Event"} = object, meta) do def validate(%{"type" => "Event"} = object, meta) do
with {:ok, object} <- with {:ok, object} <-
object object
|> EventValidator.cast_and_validate() |> EventValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do |> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object) object = stringify_keys(object)
{:ok, object, meta} {:ok, object, meta}
@ -61,7 +61,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def validate(%{"type" => "Follow"} = object, meta) do def validate(%{"type" => "Follow"} = object, meta) do
with {:ok, object} <- with {:ok, object} <-
object object
|> FollowValidator.cast_and_validate() |> FollowValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do |> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object) object = stringify_keys(object)
{:ok, object, meta} {:ok, object, meta}
@ -71,7 +71,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def validate(%{"type" => "Block"} = block_activity, meta) do def validate(%{"type" => "Block"} = block_activity, meta) do
with {:ok, block_activity} <- with {:ok, block_activity} <-
block_activity block_activity
|> BlockValidator.cast_and_validate() |> BlockValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do |> Ecto.Changeset.apply_action(:insert) do
block_activity = stringify_keys(block_activity) block_activity = stringify_keys(block_activity)
outgoing_blocks = Pleroma.Config.get([:activitypub, :outgoing_blocks]) outgoing_blocks = Pleroma.Config.get([:activitypub, :outgoing_blocks])
@ -90,7 +90,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def validate(%{"type" => "Update"} = update_activity, meta) do def validate(%{"type" => "Update"} = update_activity, meta) do
with {:ok, update_activity} <- with {:ok, update_activity} <-
update_activity update_activity
|> UpdateValidator.cast_and_validate() |> UpdateValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do |> Ecto.Changeset.apply_action(:insert) do
update_activity = stringify_keys(update_activity) update_activity = stringify_keys(update_activity)
{:ok, update_activity, meta} {:ok, update_activity, meta}
@ -100,7 +100,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def validate(%{"type" => "Undo"} = object, meta) do def validate(%{"type" => "Undo"} = object, meta) do
with {:ok, object} <- with {:ok, object} <-
object object
|> UndoValidator.cast_and_validate() |> UndoValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do |> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object) object = stringify_keys(object)
undone_object = Activity.get_by_ap_id(object["object"]) undone_object = Activity.get_by_ap_id(object["object"])
@ -114,7 +114,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end end
def validate(%{"type" => "Delete"} = object, meta) do def validate(%{"type" => "Delete"} = object, meta) do
with cng <- DeleteValidator.cast_and_validate(object), with cng <- DeleteValidator.cast_and_validate(object, meta),
do_not_federate <- DeleteValidator.do_not_federate?(cng), do_not_federate <- DeleteValidator.do_not_federate?(cng),
{:ok, object} <- Ecto.Changeset.apply_action(cng, :insert) do {:ok, object} <- Ecto.Changeset.apply_action(cng, :insert) do
object = stringify_keys(object) object = stringify_keys(object)
@ -126,7 +126,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def validate(%{"type" => "Like"} = object, meta) do def validate(%{"type" => "Like"} = object, meta) do
with {:ok, object} <- with {:ok, object} <-
object object
|> LikeValidator.cast_and_validate() |> LikeValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do |> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object) object = stringify_keys(object)
{:ok, object, meta} {:ok, object, meta}
@ -146,7 +146,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def validate(%{"type" => "Question"} = object, meta) do def validate(%{"type" => "Question"} = object, meta) do
with {:ok, object} <- with {:ok, object} <-
object object
|> QuestionValidator.cast_and_validate() |> QuestionValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do |> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object) object = stringify_keys(object)
{:ok, object, meta} {:ok, object, meta}
@ -156,7 +156,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def validate(%{"type" => type} = object, meta) when type in ~w[Audio Video] do def validate(%{"type" => type} = object, meta) when type in ~w[Audio Video] do
with {:ok, object} <- with {:ok, object} <-
object object
|> AudioVideoValidator.cast_and_validate() |> AudioVideoValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do |> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object) object = stringify_keys(object)
{:ok, object, meta} {:ok, object, meta}
@ -166,7 +166,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def validate(%{"type" => "Article"} = object, meta) do def validate(%{"type" => "Article"} = object, meta) do
with {:ok, object} <- with {:ok, object} <-
object object
|> ArticleNoteValidator.cast_and_validate() |> ArticleNoteValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do |> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object) object = stringify_keys(object)
{:ok, object, meta} {:ok, object, meta}
@ -176,7 +176,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def validate(%{"type" => "Answer"} = object, meta) do def validate(%{"type" => "Answer"} = object, meta) do
with {:ok, object} <- with {:ok, object} <-
object object
|> AnswerValidator.cast_and_validate() |> AnswerValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do |> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object) object = stringify_keys(object)
{:ok, object, meta} {:ok, object, meta}
@ -186,7 +186,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def validate(%{"type" => "EmojiReact"} = object, meta) do def validate(%{"type" => "EmojiReact"} = object, meta) do
with {:ok, object} <- with {:ok, object} <-
object object
|> EmojiReactValidator.cast_and_validate() |> EmojiReactValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do |> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object) object = stringify_keys(object)
{:ok, object, meta} {:ok, object, meta}
@ -227,7 +227,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def validate(%{"type" => "Announce"} = object, meta) do def validate(%{"type" => "Announce"} = object, meta) do
with {:ok, object} <- with {:ok, object} <-
object object
|> AnnounceValidator.cast_and_validate() |> AnnounceValidator.cast_and_validate(meta)
|> Ecto.Changeset.apply_action(:insert) do |> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object) object = stringify_keys(object)
{:ok, object, meta} {:ok, object, meta}

View File

@ -27,19 +27,19 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator do
|> cast(data, __schema__(:fields)) |> cast(data, __schema__(:fields))
end end
def validate_data(cng) do def validate_data(cng, meta \\ []) do
cng cng
|> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Accept", "Reject"]) |> validate_inclusion(:type, ["Accept", "Reject"])
|> validate_actor_presence() |> validate_actor_presence(meta)
|> validate_object_presence(allowed_types: ["Follow"]) |> validate_object_presence(allowed_types: ["Follow"])
|> validate_accept_reject_rights() |> validate_accept_reject_rights()
end end
def cast_and_validate(data) do def cast_and_validate(data, meta) do
data data
|> cast_data |> cast_data
|> validate_data |> validate_data(meta)
end end
def validate_accept_reject_rights(cng) do def validate_accept_reject_rights(cng) do

View File

@ -29,10 +29,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do
field(:published, ObjectValidators.DateTime) field(:published, ObjectValidators.DateTime)
end end
def cast_and_validate(data) do def cast_and_validate(data, meta) do
data data
|> cast_data() |> cast_data()
|> validate_data() |> validate_data(meta)
end end
def cast_data(data) do def cast_data(data) do
@ -50,11 +50,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do
cng cng
end end
def validate_data(data_cng) do def validate_data(data_cng, meta \\ []) do
data_cng data_cng
|> validate_inclusion(:type, ["Announce"]) |> validate_inclusion(:type, ["Announce"])
|> validate_required([:id, :type, :object, :actor, :to, :cc]) |> validate_required([:id, :type, :object, :actor, :to, :cc])
|> validate_actor_presence() |> validate_actor_presence(meta)
|> validate_object_presence() |> validate_object_presence()
|> validate_existing_announce() |> validate_existing_announce()
|> validate_announcable() |> validate_announcable()

View File

@ -34,10 +34,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator do
|> apply_action(:insert) |> apply_action(:insert)
end end
def cast_and_validate(data) do def cast_and_validate(data, meta) do
data data
|> cast_data() |> cast_data()
|> validate_data() |> validate_data(meta)
end end
def cast_data(data) do def cast_data(data) do
@ -50,13 +50,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator do
|> cast(data, __schema__(:fields)) |> cast(data, __schema__(:fields))
end end
def validate_data(data_cng) do def validate_data(data_cng, meta \\ []) do
data_cng data_cng
|> validate_inclusion(:type, ["Answer"]) |> validate_inclusion(:type, ["Answer"])
|> validate_required([:id, :inReplyTo, :name, :attributedTo, :actor]) |> validate_required([:id, :inReplyTo, :name, :attributedTo, :actor])
|> CommonValidations.validate_any_presence([:cc, :to]) |> CommonValidations.validate_any_presence([:cc, :to])
|> CommonValidations.validate_fields_match([:actor, :attributedTo]) |> CommonValidations.validate_fields_match([:actor, :attributedTo])
|> CommonValidations.validate_actor_presence() |> CommonValidations.validate_actor_presence(meta)
|> CommonValidations.validate_host_match() |> CommonValidations.validate_host_match()
end end
end end

View File

@ -58,10 +58,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator do
|> apply_action(:insert) |> apply_action(:insert)
end end
def cast_and_validate(data) do def cast_and_validate(data, meta) do
data data
|> cast_data() |> cast_data()
|> validate_data() |> validate_data(meta)
end end
def cast_data(data) do def cast_data(data) do
@ -94,13 +94,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidator do
|> cast_embed(:attachment) |> cast_embed(:attachment)
end end
def validate_data(data_cng) do def validate_data(data_cng, meta \\ []) do
data_cng data_cng
|> validate_inclusion(:type, ["Article", "Note"]) |> validate_inclusion(:type, ["Article", "Note"])
|> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id])
|> CommonValidations.validate_any_presence([:cc, :to]) |> CommonValidations.validate_any_presence([:cc, :to])
|> CommonValidations.validate_fields_match([:actor, :attributedTo]) |> CommonValidations.validate_fields_match([:actor, :attributedTo])
|> CommonValidations.validate_actor_presence() |> CommonValidations.validate_actor_presence(meta)
|> CommonValidations.validate_host_match() |> CommonValidations.validate_host_match()
end end
end end

View File

@ -59,10 +59,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do
|> apply_action(:insert) |> apply_action(:insert)
end end
def cast_and_validate(data) do def cast_and_validate(data, meta) do
data data
|> cast_data() |> cast_data()
|> validate_data() |> validate_data(meta)
end end
def cast_data(data) do def cast_data(data) do
@ -122,13 +122,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do
|> cast_embed(:attachment) |> cast_embed(:attachment)
end end
def validate_data(data_cng) do def validate_data(data_cng, meta \\ []) do
data_cng data_cng
|> validate_inclusion(:type, ["Audio", "Video"]) |> validate_inclusion(:type, ["Audio", "Video"])
|> validate_required([:id, :actor, :attributedTo, :type, :context, :attachment]) |> validate_required([:id, :actor, :attributedTo, :type, :context, :attachment])
|> CommonValidations.validate_any_presence([:cc, :to]) |> CommonValidations.validate_any_presence([:cc, :to])
|> CommonValidations.validate_fields_match([:actor, :attributedTo]) |> CommonValidations.validate_fields_match([:actor, :attributedTo])
|> CommonValidations.validate_actor_presence() |> CommonValidations.validate_actor_presence(meta)
|> CommonValidations.validate_host_match() |> CommonValidations.validate_host_match()
end end
end end

View File

@ -26,17 +26,17 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator do
|> cast(data, __schema__(:fields)) |> cast(data, __schema__(:fields))
end end
def validate_data(cng) do def validate_data(cng, meta \\ []) do
cng cng
|> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Block"]) |> validate_inclusion(:type, ["Block"])
|> validate_actor_presence() |> validate_actor_presence(meta)
|> validate_actor_presence(field_name: :object) |> validate_actor_presence(field_name: :object)
end end
def cast_and_validate(data) do def cast_and_validate(data, meta) do
data data
|> cast_data |> cast_data
|> validate_data |> validate_data(meta)
end end
end end

View File

@ -36,7 +36,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
|> validate_change(field_name, fn field_name, actor -> |> validate_change(field_name, fn field_name, actor ->
case User.get_cached_by_ap_id(actor) do case User.get_cached_by_ap_id(actor) do
%User{is_active: false} -> %User{is_active: false} ->
[{field_name, "user is deactivated"}] unless options[:allow_deactivated_actor] do
[{field_name, "user is deactivated"}]
else
[]
end
%User{} -> %User{} ->
[] []

View File

@ -83,7 +83,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator do
cng cng
|> validate_required([:actor, :type, :object]) |> validate_required([:actor, :type, :object])
|> validate_inclusion(:type, ["Create"]) |> validate_inclusion(:type, ["Create"])
|> CommonValidations.validate_actor_presence() |> CommonValidations.validate_actor_presence(meta)
|> CommonValidations.validate_any_presence([:to, :cc]) |> CommonValidations.validate_any_presence([:to, :cc])
|> validate_actors_match(meta) |> validate_actors_match(meta)
|> validate_context_match(meta) |> validate_context_match(meta)

View File

@ -53,11 +53,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
Tombstone Tombstone
Video Video
} }
def validate_data(cng) do def validate_data(cng, meta \\ []) do
cng cng
|> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Delete"]) |> validate_inclusion(:type, ["Delete"])
|> validate_actor_presence() |> validate_actor_presence(meta)
|> validate_modification_rights() |> validate_modification_rights()
|> validate_object_or_user_presence(allowed_types: @deletable_types) |> validate_object_or_user_presence(allowed_types: @deletable_types)
|> add_deleted_activity_id() |> add_deleted_activity_id()
@ -67,9 +67,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
!same_domain?(cng) !same_domain?(cng)
end end
def cast_and_validate(data) do def cast_and_validate(data, meta) do
data data
|> cast_data |> cast_data
|> validate_data |> validate_data(meta)
end end
end end

View File

@ -24,10 +24,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
field(:cc, ObjectValidators.Recipients, default: []) field(:cc, ObjectValidators.Recipients, default: [])
end end
def cast_and_validate(data) do def cast_and_validate(data, meta) do
data data
|> cast_data() |> cast_data()
|> validate_data() |> validate_data(meta)
end end
def cast_data(data) do def cast_data(data) do
@ -70,11 +70,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
end end
end end
def validate_data(data_cng) do def validate_data(data_cng, meta \\ []) do
data_cng data_cng
|> validate_inclusion(:type, ["EmojiReact"]) |> validate_inclusion(:type, ["EmojiReact"])
|> validate_required([:id, :type, :object, :actor, :context, :to, :cc, :content]) |> validate_required([:id, :type, :object, :actor, :context, :to, :cc, :content])
|> validate_actor_presence() |> validate_actor_presence(meta)
|> validate_object_presence() |> validate_object_presence()
|> validate_emoji() |> validate_emoji()
end end

View File

@ -59,10 +59,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
|> apply_action(:insert) |> apply_action(:insert)
end end
def cast_and_validate(data) do def cast_and_validate(data, meta) do
data data
|> cast_data() |> cast_data()
|> validate_data() |> validate_data(meta)
end end
def cast_data(data) do def cast_data(data) do
@ -85,13 +85,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EventValidator do
|> cast_embed(:attachment) |> cast_embed(:attachment)
end end
def validate_data(data_cng) do def validate_data(data_cng, meta \\ []) do
data_cng data_cng
|> validate_inclusion(:type, ["Event"]) |> validate_inclusion(:type, ["Event"])
|> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id])
|> CommonValidations.validate_any_presence([:cc, :to]) |> CommonValidations.validate_any_presence([:cc, :to])
|> CommonValidations.validate_fields_match([:actor, :attributedTo]) |> CommonValidations.validate_fields_match([:actor, :attributedTo])
|> CommonValidations.validate_actor_presence() |> CommonValidations.validate_actor_presence(meta)
|> CommonValidations.validate_host_match() |> CommonValidations.validate_host_match()
end end
end end

View File

@ -27,18 +27,18 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator do
|> cast(data, __schema__(:fields)) |> cast(data, __schema__(:fields))
end end
def validate_data(cng) do def validate_data(cng, meta \\ []) do
cng cng
|> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Follow"]) |> validate_inclusion(:type, ["Follow"])
|> validate_inclusion(:state, ~w{pending reject accept}) |> validate_inclusion(:state, ~w{pending reject accept})
|> validate_actor_presence() |> validate_actor_presence(meta)
|> validate_actor_presence(field_name: :object) |> validate_actor_presence(field_name: :object)
end end
def cast_and_validate(data) do def cast_and_validate(data, meta) do
data data
|> cast_data |> cast_data
|> validate_data |> validate_data(meta)
end end
end end

View File

@ -24,10 +24,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
field(:cc, ObjectValidators.Recipients, default: []) field(:cc, ObjectValidators.Recipients, default: [])
end end
def cast_and_validate(data) do def cast_and_validate(data, meta) do
data data
|> cast_data() |> cast_data()
|> validate_data() |> validate_data(meta)
end end
def cast_data(data) do def cast_data(data) do
@ -76,11 +76,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
end end
end end
def validate_data(data_cng) do def validate_data(data_cng, meta) do
data_cng data_cng
|> validate_inclusion(:type, ["Like"]) |> validate_inclusion(:type, ["Like"])
|> validate_required([:id, :type, :object, :actor, :context, :to, :cc]) |> validate_required([:id, :type, :object, :actor, :context, :to, :cc])
|> validate_actor_presence() |> validate_actor_presence(meta)
|> validate_object_presence() |> validate_object_presence()
|> validate_existing_like() |> validate_existing_like()
end end

View File

@ -62,10 +62,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
|> apply_action(:insert) |> apply_action(:insert)
end end
def cast_and_validate(data) do def cast_and_validate(data, meta) do
data data
|> cast_data() |> cast_data()
|> validate_data() |> validate_data(meta)
end end
def cast_data(data) do def cast_data(data) do
@ -99,13 +99,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
|> cast_embed(:oneOf) |> cast_embed(:oneOf)
end end
def validate_data(data_cng) do def validate_data(data_cng, meta \\ []) do
data_cng data_cng
|> validate_inclusion(:type, ["Question"]) |> validate_inclusion(:type, ["Question"])
|> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id]) |> validate_required([:id, :actor, :attributedTo, :type, :context, :context_id])
|> CommonValidations.validate_any_presence([:cc, :to]) |> CommonValidations.validate_any_presence([:cc, :to])
|> CommonValidations.validate_fields_match([:actor, :attributedTo]) |> CommonValidations.validate_fields_match([:actor, :attributedTo])
|> CommonValidations.validate_actor_presence() |> CommonValidations.validate_actor_presence(meta)
|> CommonValidations.validate_any_presence([:oneOf, :anyOf]) |> CommonValidations.validate_any_presence([:oneOf, :anyOf])
|> CommonValidations.validate_host_match() |> CommonValidations.validate_host_match()
end end

View File

@ -22,10 +22,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator do
field(:cc, ObjectValidators.Recipients, default: []) field(:cc, ObjectValidators.Recipients, default: [])
end end
def cast_and_validate(data) do def cast_and_validate(data, meta) do
data data
|> cast_data() |> cast_data()
|> validate_data() |> validate_data(meta)
end end
def cast_data(data) do def cast_data(data) do
@ -38,11 +38,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator do
|> cast(data, __schema__(:fields)) |> cast(data, __schema__(:fields))
end end
def validate_data(data_cng) do def validate_data(data_cng, meta \\ []) do
data_cng data_cng
|> validate_inclusion(:type, ["Undo"]) |> validate_inclusion(:type, ["Undo"])
|> validate_required([:id, :type, :object, :actor, :to, :cc]) |> validate_required([:id, :type, :object, :actor, :to, :cc])
|> validate_actor_presence() |> validate_actor_presence(meta)
|> validate_object_presence() |> validate_object_presence()
|> validate_undo_rights() |> validate_undo_rights()
end end

View File

@ -28,18 +28,18 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do
|> cast(data, __schema__(:fields)) |> cast(data, __schema__(:fields))
end end
def validate_data(cng) do def validate_data(cng, meta \\ []) do
cng cng
|> validate_required([:id, :type, :actor, :to, :cc, :object]) |> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Update"]) |> validate_inclusion(:type, ["Update"])
|> validate_actor_presence() |> validate_actor_presence(meta)
|> validate_updating_rights() |> validate_updating_rights()
end end
def cast_and_validate(data) do def cast_and_validate(data, meta) do
data data
|> cast_data |> cast_data
|> validate_data |> validate_data(meta)
end end
# For now we only support updating users, and here the rule is easy: # For now we only support updating users, and here the rule is easy:

View File

@ -13,6 +13,7 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
alias Pleroma.Web.ActivityPub.SideEffects alias Pleroma.Web.ActivityPub.SideEffects
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator alias Pleroma.Web.Federator
alias Pleroma.Workers.BackgroundWorker
@side_effects Config.get([:pipeline, :side_effects], SideEffects) @side_effects Config.get([:pipeline, :side_effects], SideEffects)
@federator Config.get([:pipeline, :federator], Federator) @federator Config.get([:pipeline, :federator], Federator)
@ -21,12 +22,17 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
@activity_pub Config.get([:pipeline, :activity_pub], ActivityPub) @activity_pub Config.get([:pipeline, :activity_pub], ActivityPub)
@config Config.get([:pipeline, :config], Config) @config Config.get([:pipeline, :config], Config)
@spec common_pipeline(map(), keyword()) :: @type common_pipeline_meta_option ::
{:local, boolean()} | {:allow_deactivated_actor, boolean()} | {atom(), term()}
@spec common_pipeline(map(), [common_pipeline_meta_option()]) ::
{:ok, Activity.t() | Object.t(), keyword()} | {:error, any()} {:ok, Activity.t() | Object.t(), keyword()} | {:error, any()}
def common_pipeline(object, meta) do def common_pipeline(object, meta) do
case Repo.transaction(fn -> do_common_pipeline(object, meta) end) do case Repo.transaction(fn -> do_common_pipeline(object, meta) end) do
{:ok, {:ok, activity, meta}} -> {:ok, {:ok, activity, meta}} ->
@side_effects.handle_after_transaction(meta) BackgroundWorker.execute_or_enqueue_if_in_transaction(fn ->
@side_effects.handle_after_transaction(meta)
end)
{:ok, activity, meta} {:ok, activity, meta}
{:ok, value} -> {:ok, value} ->

View File

@ -18,6 +18,7 @@ defmodule Pleroma.Web.Streamer do
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.StreamerView alias Pleroma.Web.StreamerView
alias Pleroma.Workers.BackgroundWorker
@mix_env Mix.env() @mix_env Mix.env()
@registry Pleroma.Web.StreamerRegistry @registry Pleroma.Web.StreamerRegistry
@ -135,9 +136,11 @@ defmodule Pleroma.Web.Streamer do
def stream(topics, items) do def stream(topics, items) do
if should_env_send?() do if should_env_send?() do
for topic <- List.wrap(topics), item <- List.wrap(items) do BackgroundWorker.execute_or_enqueue_if_in_transaction(fn ->
spawn(fn -> do_stream(topic, item) end) for topic <- List.wrap(topics), item <- List.wrap(items) do
end spawn(fn -> do_stream(topic, item) end)
end
end)
end end
end end

View File

@ -38,4 +38,45 @@ defmodule Pleroma.Workers.BackgroundWorker do
Pleroma.FollowingRelationship.move_following(origin, target) Pleroma.FollowingRelationship.move_following(origin, target)
end end
def perform(%Job{args: %{"op" => "transaction_side_effects", "function" => encoded_function}}) do
function =
encoded_function
|> Base.decode64!()
|> :erlang.binary_to_term()
maybe_execute_function_with_worker_info(function, true)
:ok
end
@doc "Executes a function right away if not running in transaction. Otherwise enqueues it to be executed by BackgroundWorker after transaction commit. Intended for side effects that can not be rolled back. If the function has an arity of 1, the first argument will be a boolean indicating whether it is run by BackgroundWorker or not."
@spec execute_or_enqueue_if_in_transaction((() -> any()) | (boolean() -> any())) ::
{:ok, {:enqueued, Oban.Job.t()}}
| {:error, {:enqueue, Oban.job_changeset()}}
| {:error, {:enqueue, term()}}
| {:ok, {:executed, term()}}
def execute_or_enqueue_if_in_transaction(function) do
if Pleroma.Repo.in_transaction?() and
!Pleroma.Config.get([__MODULE__, :ignore_transaction_check], false) do
encoded_function =
function
|> :erlang.term_to_binary()
|> Base.encode64()
case enqueue("transaction_side_effects", %{"function" => encoded_function}) do
{:ok, job} -> {:ok, {:enqueued, job}}
{:error, e} -> {:error, {:enqueue, e}}
end
else
{:ok, {:executed, maybe_execute_function_with_worker_info(function, false)}}
end
end
defp maybe_execute_function_with_worker_info(function, executed_by_worker) do
if :erlang.fun_info(function)[:arity] == 1 do
function.(executed_by_worker)
else
function.()
end
end
end end

View File

@ -29,7 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNoteValidatorTest do
end end
test "a basic note validates", %{note: note} do test "a basic note validates", %{note: note} do
%{valid?: true} = ArticleNoteValidator.cast_and_validate(note) %{valid?: true} = ArticleNoteValidator.cast_and_validate(note, [])
end end
end end
end end

View File

@ -37,7 +37,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidationTest do
end end
test "is valid for a valid object", %{valid_like: valid_like} do test "is valid for a valid object", %{valid_like: valid_like} do
assert LikeValidator.cast_and_validate(valid_like).valid? assert LikeValidator.cast_and_validate(valid_like, []).valid?
end end
test "sets the 'to' field to the object actor if no recipients are given", %{ test "sets the 'to' field to the object actor if no recipients are given", %{
@ -69,21 +69,21 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidationTest do
test "it errors when the actor is missing or not known", %{valid_like: valid_like} do test "it errors when the actor is missing or not known", %{valid_like: valid_like} do
without_actor = Map.delete(valid_like, "actor") without_actor = Map.delete(valid_like, "actor")
refute LikeValidator.cast_and_validate(without_actor).valid? refute LikeValidator.cast_and_validate(without_actor, []).valid?
with_invalid_actor = Map.put(valid_like, "actor", "invalidactor") with_invalid_actor = Map.put(valid_like, "actor", "invalidactor")
refute LikeValidator.cast_and_validate(with_invalid_actor).valid? refute LikeValidator.cast_and_validate(with_invalid_actor, []).valid?
end end
test "it errors when the object is missing or not known", %{valid_like: valid_like} do test "it errors when the object is missing or not known", %{valid_like: valid_like} do
without_object = Map.delete(valid_like, "object") without_object = Map.delete(valid_like, "object")
refute LikeValidator.cast_and_validate(without_object).valid? refute LikeValidator.cast_and_validate(without_object, []).valid?
with_invalid_object = Map.put(valid_like, "object", "invalidobject") with_invalid_object = Map.put(valid_like, "object", "invalidobject")
refute LikeValidator.cast_and_validate(with_invalid_object).valid? refute LikeValidator.cast_and_validate(with_invalid_object, []).valid?
end end
test "it errors when the actor has already like the object", %{ test "it errors when the actor has already like the object", %{
@ -93,7 +93,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidationTest do
} do } do
_like = CommonAPI.favorite(user, post_activity.id) _like = CommonAPI.favorite(user, post_activity.id)
refute LikeValidator.cast_and_validate(valid_like).valid? refute LikeValidator.cast_and_validate(valid_like, []).valid?
end end
test "it works when actor or object are wrapped in maps", %{valid_like: valid_like} do test "it works when actor or object are wrapped in maps", %{valid_like: valid_like} do
@ -102,7 +102,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidationTest do
|> Map.put("actor", %{"id" => valid_like["actor"]}) |> Map.put("actor", %{"id" => valid_like["actor"]})
|> Map.put("object", %{"id" => valid_like["object"]}) |> Map.put("object", %{"id" => valid_like["object"]})
validated = LikeValidator.cast_and_validate(wrapped_like) validated = LikeValidator.cast_and_validate(wrapped_like, [])
assert validated.valid? assert validated.valid?