Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into remake-remodel-dms

This commit is contained in:
lain 2020-05-06 17:36:08 +02:00
commit 1748e26948
27 changed files with 635 additions and 388 deletions

View File

@ -8,6 +8,10 @@ For from source installations Pleroma configuration works by first importing the
To add configuration to your config file, you can copy it from the base config. The latest version of it can be viewed [here](https://git.pleroma.social/pleroma/pleroma/blob/develop/config/config.exs). You can also use this file if you don't know how an option is supposed to be formatted. To add configuration to your config file, you can copy it from the base config. The latest version of it can be viewed [here](https://git.pleroma.social/pleroma/pleroma/blob/develop/config/config.exs). You can also use this file if you don't know how an option is supposed to be formatted.
## :chat
* `enabled` - Enables the backend chat. Defaults to `true`.
## :instance ## :instance
* `name`: The instances name. * `name`: The instances name.
* `email`: Email used to reach an Administrator/Moderator of the instance. * `email`: Email used to reach an Administrator/Moderator of the instance.

View File

@ -32,9 +32,8 @@ CustomLog ${APACHE_LOG_DIR}/access.log combined
<VirtualHost *:443> <VirtualHost *:443>
SSLEngine on SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/${servername}/cert.pem SSLCertificateFile /etc/letsencrypt/live/${servername}/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/${servername}/privkey.pem SSLCertificateKeyFile /etc/letsencrypt/live/${servername}/privkey.pem
SSLCertificateChainFile /etc/letsencrypt/live/${servername}/fullchain.pem
# Mozilla modern configuration, tweak to your needs # Mozilla modern configuration, tweak to your needs
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1

View File

@ -8,6 +8,8 @@ defmodule Mix.Tasks.Pleroma.User do
alias Ecto.Changeset alias Ecto.Changeset
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
@shortdoc "Manages Pleroma users" @shortdoc "Manages Pleroma users"
@moduledoc File.read!("docs/administration/CLI_tasks/user.md") @moduledoc File.read!("docs/administration/CLI_tasks/user.md")
@ -96,8 +98,9 @@ def run(["new", nickname, email | rest]) do
def run(["rm", nickname]) do def run(["rm", nickname]) do
start_pleroma() start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
User.perform(:delete, user) {:ok, delete_data, _} <- Builder.delete(user, user.ap_id),
{:ok, _delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
shell_info("User #{nickname} deleted.") shell_info("User #{nickname} deleted.")
else else
_ -> shell_error("No local user #{nickname}") _ -> shell_error("No local user #{nickname}")

View File

@ -29,7 +29,9 @@ defmodule Pleroma.User do
alias Pleroma.UserRelationship alias Pleroma.UserRelationship
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.ObjectValidators.Types alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
@ -1425,8 +1427,6 @@ 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
{:ok, _user} = ActivityPub.delete(user)
# Remove all relationships # Remove all relationships
user user
|> get_followers() |> get_followers()
@ -1536,21 +1536,23 @@ def follow_import(%User{} = follower, followed_identifiers)
}) })
end end
def delete_user_activities(%User{ap_id: ap_id}) do def delete_user_activities(%User{ap_id: ap_id} = user) do
ap_id ap_id
|> Activity.Queries.by_actor() |> Activity.Queries.by_actor()
|> RepoStreamer.chunk_stream(50) |> RepoStreamer.chunk_stream(50)
|> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end) |> Stream.each(fn activities ->
Enum.each(activities, fn activity -> delete_activity(activity, user) end)
end)
|> Stream.run() |> Stream.run()
end end
defp delete_activity(%{data: %{"type" => "Create"}} = activity) do defp delete_activity(%{data: %{"type" => "Create", "object" => object}}, user) do
activity {:ok, delete_data, _} = Builder.delete(user, object)
|> Object.normalize()
|> ActivityPub.delete() Pipeline.common_pipeline(delete_data, local: true)
end end
defp delete_activity(%{data: %{"type" => "Like"}} = activity) do defp delete_activity(%{data: %{"type" => "Like"}} = activity, _user) do
object = Object.normalize(activity) object = Object.normalize(activity)
activity.actor activity.actor
@ -1558,7 +1560,7 @@ defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
|> ActivityPub.unlike(object) |> ActivityPub.unlike(object)
end end
defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do defp delete_activity(%{data: %{"type" => "Announce"}} = activity, _user) do
object = Object.normalize(activity) object = Object.normalize(activity)
activity.actor activity.actor
@ -1566,7 +1568,7 @@ defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
|> ActivityPub.unannounce(object) |> ActivityPub.unannounce(object)
end end
defp delete_activity(_activity), do: "Doing nothing" defp delete_activity(_activity, _user), do: "Doing nothing"
def html_filter_policy(%User{no_rich_text: true}) do def html_filter_policy(%User{no_rich_text: true}) do
Pleroma.HTML.Scrubber.TwitterText Pleroma.HTML.Scrubber.TwitterText

View File

@ -526,67 +526,6 @@ defp do_unfollow(follower, followed, activity_id, local) do
end end
end end
@spec delete(User.t() | Object.t(), keyword()) :: {:ok, User.t() | Object.t()} | {:error, any()}
def delete(entity, options \\ []) do
with {:ok, result} <- Repo.transaction(fn -> do_delete(entity, options) end) do
result
end
end
defp do_delete(%User{ap_id: ap_id, follower_address: follower_address} = user, _) do
with data <- %{
"to" => [follower_address],
"type" => "Delete",
"actor" => ap_id,
"object" => %{"type" => "Person", "id" => ap_id}
},
{:ok, activity} <- insert(data, true, true, true),
:ok <- maybe_federate(activity) do
{:ok, user}
end
end
defp do_delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options) do
local = Keyword.get(options, :local, true)
activity_id = Keyword.get(options, :activity_id, nil)
actor = Keyword.get(options, :actor, actor)
user = User.get_cached_by_ap_id(actor)
to = (object.data["to"] || []) ++ (object.data["cc"] || [])
with create_activity <- Activity.get_create_by_object_ap_id(id),
data <-
%{
"type" => "Delete",
"actor" => actor,
"object" => id,
"to" => to,
"deleted_activity_id" => create_activity && create_activity.id
}
|> maybe_put("id", activity_id),
{:ok, activity} <- insert(data, local, false),
{:ok, object, _create_activity} <- Object.delete(object),
stream_out_participations(object, user),
_ <- decrease_replies_count_if_reply(object),
{:ok, _actor} <- decrease_note_count_if_public(user, object),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
{:error, error} ->
Repo.rollback(error)
end
end
defp do_delete(%Object{data: %{"type" => "Tombstone", "id" => ap_id}}, _) do
activity =
ap_id
|> Activity.Queries.by_object_id()
|> Activity.Queries.by_type("Delete")
|> Repo.one()
{:ok, activity}
end
@spec block(User.t(), User.t(), String.t() | nil, boolean()) :: @spec block(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | {:error, any()} {:ok, Activity.t()} | {:error, any()}
def block(blocker, blocked, activity_id \\ nil, local \\ true) do def block(blocker, blocked, activity_id \\ nil, local \\ true) do

View File

@ -415,7 +415,8 @@ defp handle_user_activity(%User{} = user, %{"type" => "Create"} = params) do
defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
with %Object{} = object <- Object.normalize(params["object"]), with %Object{} = object <- Object.normalize(params["object"]),
true <- user.is_moderator || user.ap_id == object.data["actor"], true <- user.is_moderator || user.ap_id == object.data["actor"],
{:ok, delete} <- ActivityPub.delete(object) do {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
{:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
{:ok, delete} {:ok, delete}
else else
_ -> {:error, dgettext("errors", "Can't delete object")} _ -> {:error, dgettext("errors", "Can't delete object")}

View File

@ -11,6 +11,33 @@ defmodule Pleroma.Web.ActivityPub.Builder do
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
@spec delete(User.t(), String.t()) :: {:ok, map(), keyword()}
def delete(actor, object_id) do
object = Object.normalize(object_id, false)
user = !object && User.get_cached_by_ap_id(object_id)
to =
case {object, user} do
{%Object{}, _} ->
# We are deleting an object, address everyone who was originally mentioned
(object.data["to"] || []) ++ (object.data["cc"] || [])
{_, %User{follower_address: follower_address}} ->
# We are deleting a user, address the followers of that user
[follower_address]
end
{:ok,
%{
"id" => Utils.generate_activity_id(),
"actor" => actor.ap_id,
"object" => object_id,
"to" => to,
"type" => "Delete"
}, []}
end
def create(actor, object, recipients) do def create(actor, object, recipients) do
{:ok, {:ok,
%{ %{

View File

@ -13,12 +13,23 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.Types alias Pleroma.Web.ActivityPub.ObjectValidators.Types
@spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()} @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
def validate(object, meta) def validate(object, meta)
def validate(%{"type" => "Delete"} = object, meta) do
with cng <- DeleteValidator.cast_and_validate(object),
do_not_federate <- DeleteValidator.do_not_federate?(cng),
{:ok, object} <- Ecto.Changeset.apply_action(cng, :insert) do
object = stringify_keys(object)
meta = Keyword.put(meta, :do_not_federate, do_not_federate)
{:ok, object, meta}
end
end
def validate(%{"type" => "Like"} = object, meta) do def validate(%{"type" => "Like"} = object, meta) do
with {:ok, object} <- with {:ok, object} <-
object object

View File

@ -8,11 +8,29 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
def validate_actor_presence(cng) do def validate_recipients_presence(cng, fields \\ [:to, :cc]) do
validate_user_presence(cng, :actor) non_empty =
fields
|> Enum.map(fn field -> get_field(cng, field) end)
|> Enum.any?(fn
[] -> false
_ -> true
end)
if non_empty do
cng
else
fields
|> Enum.reduce(cng, fn field, cng ->
cng
|> add_error(field, "no recipients in any field")
end)
end
end end
def validate_user_presence(cng, field_name) do def validate_actor_presence(cng, options \\ []) do
field_name = Keyword.get(options, :field_name, :actor)
cng cng
|> validate_change(field_name, fn field_name, actor -> |> validate_change(field_name, fn field_name, actor ->
if User.get_cached_by_ap_id(actor) do if User.get_cached_by_ap_id(actor) do
@ -23,14 +41,39 @@ def validate_user_presence(cng, field_name) do
end) end)
end end
def validate_object_presence(cng, field_name \\ :object) do def validate_object_presence(cng, options \\ []) do
field_name = Keyword.get(options, :field_name, :object)
allowed_types = Keyword.get(options, :allowed_types, false)
cng cng
|> validate_change(field_name, fn field_name, object -> |> validate_change(field_name, fn field_name, object_id ->
if Object.get_cached_by_ap_id(object) do object = Object.get_cached_by_ap_id(object_id)
[]
else cond do
[{field_name, "can't find object"}] !object ->
[{field_name, "can't find object"}]
object && allowed_types && object.data["type"] not in allowed_types ->
[{field_name, "object not in allowed types"}]
true ->
[]
end end
end) end)
end end
def validate_object_or_user_presence(cng, options \\ []) do
field_name = Keyword.get(options, :field_name, :object)
options = Keyword.put(options, :field_name, field_name)
actor_cng =
cng
|> validate_actor_presence(options)
object_cng =
cng
|> validate_object_presence(options)
if actor_cng.valid?, do: actor_cng, else: object_cng
end
end end

View File

@ -0,0 +1,99 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
use Ecto.Schema
alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@primary_key false
embedded_schema do
field(:id, Types.ObjectID, primary_key: true)
field(:type, :string)
field(:actor, Types.ObjectID)
field(:to, Types.Recipients, default: [])
field(:cc, Types.Recipients, default: [])
field(:deleted_activity_id, Types.ObjectID)
field(:object, Types.ObjectID)
end
def cast_data(data) do
%__MODULE__{}
|> cast(data, __schema__(:fields))
end
def add_deleted_activity_id(cng) do
object =
cng
|> get_field(:object)
with %Activity{id: id} <- Activity.get_create_by_object_ap_id(object) do
cng
|> put_change(:deleted_activity_id, id)
else
_ -> cng
end
end
@deletable_types ~w{
Answer
Article
Audio
Event
Note
Page
Question
Video
}
def validate_data(cng) do
cng
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Delete"])
|> validate_actor_presence()
|> validate_deletion_rights()
|> validate_object_or_user_presence(allowed_types: @deletable_types)
|> add_deleted_activity_id()
end
def do_not_federate?(cng) do
!same_domain?(cng)
end
defp same_domain?(cng) do
actor_uri =
cng
|> get_field(:actor)
|> URI.parse()
object_uri =
cng
|> get_field(:object)
|> URI.parse()
object_uri.host == actor_uri.host
end
def validate_deletion_rights(cng) do
actor = User.get_cached_by_ap_id(get_field(cng, :actor))
if User.superuser?(actor) || same_domain?(cng) do
cng
else
cng
|> add_error(:actor, "is not allowed to delete object")
end
end
def cast_and_validate(data) do
data
|> cast_data
|> validate_data
end
end

View File

@ -20,8 +20,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
field(:object, Types.ObjectID) field(:object, Types.ObjectID)
field(:actor, Types.ObjectID) field(:actor, Types.ObjectID)
field(:context, :string) field(:context, :string)
field(:to, {:array, :string}, default: []) field(:to, Types.Recipients, default: [])
field(:cc, {:array, :string}, default: []) field(:cc, Types.Recipients, default: [])
end end
def cast_and_validate(data) do def cast_and_validate(data) do

View File

@ -44,7 +44,9 @@ defp maybe_federate(%Object{}, _), do: {:ok, :not_federated}
defp maybe_federate(%Activity{} = activity, meta) do defp maybe_federate(%Activity{} = activity, meta) do
with {:ok, local} <- Keyword.fetch(meta, :local) do with {:ok, local} <- Keyword.fetch(meta, :local) do
if local do do_not_federate = meta[:do_not_federate]
if !do_not_federate && local do
Federator.publish(activity) Federator.publish(activity)
{:ok, :federated} {:ok, :federated}
else else

View File

@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
@ -40,6 +41,49 @@ def handle(%{data: %{"type" => "Create"}} = activity, meta) do
end end
end end
# Tasks this handles:
# - Delete and unpins the create activity
# - Replace object with Tombstone
# - Set up notification
# - Reduce the user note count
# - Reduce the reply count
# - Stream out the activity
def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do
deleted_object =
Object.normalize(deleted_object, false) || User.get_cached_by_ap_id(deleted_object)
result =
case deleted_object do
%Object{} ->
with {:ok, deleted_object, activity} <- Object.delete(deleted_object),
%User{} = user <- User.get_cached_by_ap_id(deleted_object.data["actor"]) do
User.remove_pinnned_activity(user, activity)
{:ok, user} = ActivityPub.decrease_note_count_if_public(user, deleted_object)
if in_reply_to = deleted_object.data["inReplyTo"] do
Object.decrease_replies_count(in_reply_to)
end
ActivityPub.stream_out(object)
ActivityPub.stream_out_participations(deleted_object, user)
:ok
end
%User{} ->
with {:ok, _} <- User.delete(deleted_object) do
:ok
end
end
if result == :ok do
Notification.create_notifications(object)
{:ok, object, meta}
else
{:error, result}
end
end
# Nothing to do # Nothing to do
def handle(object, meta) do def handle(object, meta) do
{:ok, object, meta} {:ok, object, meta}

View File

@ -745,36 +745,12 @@ def handle_incoming(
end end
end end
# TODO: We presently assume that any actor on the same origin domain as the object being
# deleted has the rights to delete that object. A better way to validate whether or not
# the object should be deleted is to refetch the object URI, which should return either
# an error or a tombstone. This would allow us to verify that a deletion actually took
# place.
def handle_incoming( def handle_incoming(
%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data, %{"type" => "Delete"} = data,
_options _options
) do ) do
object_id = Utils.get_ap_id(object_id) with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
:ok <- Containment.contain_origin(actor.ap_id, object.data),
{:ok, activity} <-
ActivityPub.delete(object, local: false, activity_id: id, actor: actor.ap_id) do
{:ok, activity} {:ok, activity}
else
nil ->
case User.get_cached_by_ap_id(object_id) do
%User{ap_id: ^actor} = user ->
User.delete(user)
nil ->
:error
end
_e ->
:error
end end
end end

View File

@ -512,7 +512,7 @@ def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
#### Announce-related helpers #### Announce-related helpers
@doc """ @doc """
Retruns an existing announce activity if the notice has already been announced Returns an existing announce activity if the notice has already been announced
""" """
@spec get_existing_announce(String.t(), map()) :: Activity.t() | nil @spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do

View File

@ -17,6 +17,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.AccountView
@ -133,23 +135,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
action_fallback(:errors) action_fallback(:errors)
def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do def user_delete(conn, %{"nickname" => nickname}) do
user = User.get_cached_by_nickname(nickname) user_delete(conn, %{"nicknames" => [nickname]})
User.delete(user)
ModerationLog.insert_log(%{
actor: admin,
subject: [user],
action: "delete"
})
conn
|> json(nickname)
end end
def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1) users =
User.delete(users) nicknames
|> Enum.map(&User.get_cached_by_nickname/1)
users
|> Enum.each(fn user ->
{:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
Pipeline.common_pipeline(delete_data, local: true)
end)
ModerationLog.insert_log(%{ ModerationLog.insert_log(%{
actor: admin, actor: admin,

View File

@ -110,8 +110,8 @@ def delete(activity_id, user) do
{:find_activity, Activity.get_by_id_with_object(activity_id)}, {:find_activity, Activity.get_by_id_with_object(activity_id)},
%Object{} = object <- Object.normalize(activity), %Object{} = object <- Object.normalize(activity),
true <- User.superuser?(user) || user.ap_id == object.data["actor"], true <- User.superuser?(user) || user.ap_id == object.data["actor"],
{:ok, _} <- unpin(activity_id, user), {:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
{:ok, delete} <- ActivityPub.delete(object) do {:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
{:ok, delete} {:ok, delete}
else else
{:find_activity, _} -> {:error, :not_found} {:find_activity, _} -> {:error, :not_found}

View File

@ -4,14 +4,17 @@
defmodule Mix.Tasks.Pleroma.UserTest do defmodule Mix.Tasks.Pleroma.UserTest do
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
use Pleroma.DataCase use Pleroma.DataCase
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
import ExUnit.CaptureIO import ExUnit.CaptureIO
import Mock
import Pleroma.Factory
setup_all do setup_all do
Mix.shell(Mix.Shell.Process) Mix.shell(Mix.Shell.Process)
@ -87,12 +90,17 @@ test "user is not created" do
test "user is deleted" do test "user is deleted" do
user = insert(:user) user = insert(:user)
Mix.Tasks.Pleroma.User.run(["rm", user.nickname]) with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
Mix.Tasks.Pleroma.User.run(["rm", user.nickname])
ObanHelpers.perform_all()
assert_received {:mix_shell, :info, [message]} assert_received {:mix_shell, :info, [message]}
assert message =~ " deleted" assert message =~ " deleted"
assert %{deactivated: true} = User.get_by_nickname(user.nickname)
assert %{deactivated: true} = User.get_by_nickname(user.nickname) assert called(Pleroma.Web.Federator.publish(:_))
end
end end
test "no user to delete" do test "no user to delete" do

View File

@ -15,7 +15,6 @@ defmodule Pleroma.UserTest do
use Pleroma.DataCase use Pleroma.DataCase
use Oban.Testing, repo: Pleroma.Repo use Oban.Testing, repo: Pleroma.Repo
import Mock
import Pleroma.Factory import Pleroma.Factory
import ExUnit.CaptureLog import ExUnit.CaptureLog
@ -1131,7 +1130,7 @@ test ".delete_user_activities deletes all create activities", %{user: user} do
User.delete_user_activities(user) User.delete_user_activities(user)
# TODO: Remove favorites, repeats, delete activities. # TODO: Test removal favorites, repeats, delete activities.
refute Activity.get_by_id(activity.id) refute Activity.get_by_id(activity.id)
end end
@ -1170,31 +1169,6 @@ test "it deactivates a user, all follow relationships and all activities", %{use
refute Activity.get_by_id(like_two.id) refute Activity.get_by_id(like_two.id)
refute Activity.get_by_id(repeat.id) refute Activity.get_by_id(repeat.id)
end end
test_with_mock "it sends out User Delete activity",
%{user: user},
Pleroma.Web.ActivityPub.Publisher,
[:passthrough],
[] do
Pleroma.Config.put([:instance, :federating], true)
{:ok, follower} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin")
{:ok, _} = User.follow(follower, user)
{:ok, job} = User.delete(user)
{:ok, _user} = ObanHelpers.perform(job)
assert ObanHelpers.member?(
%{
"op" => "publish_one",
"params" => %{
"inbox" => "http://mastodon.example.org/inbox",
"id" => "pleroma:fakeid"
}
},
all_enqueued(worker: Pleroma.Workers.PublisherWorker)
)
end
end end
test "get_public_key_for_ap_id fetches a user that's not in the db" do test "get_public_key_for_ap_id fetches a user that's not in the db" do

View File

@ -1332,143 +1332,6 @@ test "creates an undo activity for the last block" do
end end
end end
describe "deletion" do
setup do: clear_config([:instance, :rewrite_policy])
test "it reverts deletion on error" do
note = insert(:note_activity)
object = Object.normalize(note)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.delete(object)
end
assert Repo.aggregate(Activity, :count, :id) == 1
assert Repo.get(Object, object.id) == object
assert Activity.get_by_id(note.id) == note
end
test "it creates a delete activity and deletes the original object" do
note = insert(:note_activity)
object = Object.normalize(note)
{:ok, delete} = ActivityPub.delete(object)
assert delete.data["type"] == "Delete"
assert delete.data["actor"] == note.data["actor"]
assert delete.data["object"] == object.data["id"]
assert Activity.get_by_id(delete.id) != nil
assert Repo.get(Object, object.id).data["type"] == "Tombstone"
end
test "it doesn't fail when an activity was already deleted" do
{:ok, delete} = insert(:note_activity) |> Object.normalize() |> ActivityPub.delete()
assert {:ok, ^delete} = delete |> Object.normalize() |> ActivityPub.delete()
end
test "decrements user note count only for public activities" do
user = insert(:user, note_count: 10)
{:ok, a1} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "public"
})
{:ok, a2} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "unlisted"
})
{:ok, a3} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "private"
})
{:ok, a4} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "direct"
})
{:ok, _} = Object.normalize(a1) |> ActivityPub.delete()
{:ok, _} = Object.normalize(a2) |> ActivityPub.delete()
{:ok, _} = Object.normalize(a3) |> ActivityPub.delete()
{:ok, _} = Object.normalize(a4) |> ActivityPub.delete()
user = User.get_cached_by_id(user.id)
assert user.note_count == 10
end
test "it creates a delete activity and checks that it is also sent to users mentioned by the deleted object" do
user = insert(:user)
note = insert(:note_activity)
object = Object.normalize(note)
{:ok, object} =
object
|> Object.change(%{
data: %{
"actor" => object.data["actor"],
"id" => object.data["id"],
"to" => [user.ap_id],
"type" => "Note"
}
})
|> Object.update_and_set_cache()
{:ok, delete} = ActivityPub.delete(object)
assert user.ap_id in delete.data["to"]
end
test "decreases reply count" do
user = insert(:user)
user2 = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"})
reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id}
ap_id = activity.data["id"]
{:ok, public_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public"))
{:ok, unlisted_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted"))
{:ok, private_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private"))
{:ok, direct_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct"))
_ = CommonAPI.delete(direct_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
_ = CommonAPI.delete(private_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
_ = CommonAPI.delete(public_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 1
_ = CommonAPI.delete(unlisted_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 0
end
test "it passes delete activity through MRF before deleting the object" do
Pleroma.Config.put([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.DropPolicy)
note = insert(:note_activity)
object = Object.normalize(note)
{:error, {:reject, _}} = ActivityPub.delete(object)
assert Activity.get_by_id(note.id)
assert Repo.get(Object, object.id).data["type"] == object.data["type"]
end
end
describe "timeline post-processing" do describe "timeline post-processing" do
test "it filters broken threads" do test "it filters broken threads" do
user1 = insert(:user) user1 = insert(:user)

View File

@ -156,6 +156,98 @@ test "does not validate if it doesn't concern local users" do
end end
end end
describe "deletes" do
setup do
user = insert(:user)
{:ok, post_activity} = CommonAPI.post(user, %{"status" => "cancel me daddy"})
{:ok, valid_post_delete, _} = Builder.delete(user, post_activity.data["object"])
{:ok, valid_user_delete, _} = Builder.delete(user, user.ap_id)
%{user: user, valid_post_delete: valid_post_delete, valid_user_delete: valid_user_delete}
end
test "it is valid for a post deletion", %{valid_post_delete: valid_post_delete} do
{:ok, valid_post_delete, _} = ObjectValidator.validate(valid_post_delete, [])
assert valid_post_delete["deleted_activity_id"]
end
test "it is invalid if the object isn't in a list of certain types", %{
valid_post_delete: valid_post_delete
} do
object = Object.get_by_ap_id(valid_post_delete["object"])
data =
object.data
|> Map.put("type", "Like")
{:ok, _object} =
object
|> Ecto.Changeset.change(%{data: data})
|> Object.update_and_set_cache()
{:error, cng} = ObjectValidator.validate(valid_post_delete, [])
assert {:object, {"object not in allowed types", []}} in cng.errors
end
test "it is valid for a user deletion", %{valid_user_delete: valid_user_delete} do
assert match?({:ok, _, _}, ObjectValidator.validate(valid_user_delete, []))
end
test "it's invalid if the id is missing", %{valid_post_delete: valid_post_delete} do
no_id =
valid_post_delete
|> Map.delete("id")
{:error, cng} = ObjectValidator.validate(no_id, [])
assert {:id, {"can't be blank", [validation: :required]}} in cng.errors
end
test "it's invalid if the object doesn't exist", %{valid_post_delete: valid_post_delete} do
missing_object =
valid_post_delete
|> Map.put("object", "http://does.not/exist")
{:error, cng} = ObjectValidator.validate(missing_object, [])
assert {:object, {"can't find object", []}} in cng.errors
end
test "it's invalid if the actor of the object and the actor of delete are from different domains",
%{valid_post_delete: valid_post_delete} do
valid_user = insert(:user)
valid_other_actor =
valid_post_delete
|> Map.put("actor", valid_user.ap_id)
assert match?({:ok, _, _}, ObjectValidator.validate(valid_other_actor, []))
invalid_other_actor =
valid_post_delete
|> Map.put("actor", "https://gensokyo.2hu/users/raymoo")
{:error, cng} = ObjectValidator.validate(invalid_other_actor, [])
assert {:actor, {"is not allowed to delete object", []}} in cng.errors
end
test "it's valid if the actor of the object is a local superuser",
%{valid_post_delete: valid_post_delete} do
user =
insert(:user, local: true, is_moderator: true, ap_id: "https://gensokyo.2hu/users/raymoo")
valid_other_actor =
valid_post_delete
|> Map.put("actor", user.ap_id)
{:ok, _, meta} = ObjectValidator.validate(valid_other_actor, [])
assert meta[:do_not_federate]
end
end
describe "likes" do describe "likes" do
setup do setup do
user = insert(:user) user = insert(:user)

View File

@ -3,18 +3,75 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.SideEffectsTest do defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Chat alias Pleroma.Chat
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.SideEffects alias Pleroma.Web.ActivityPub.SideEffects
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
import Pleroma.Factory import Pleroma.Factory
import Mock
describe "delete objects" do
setup do
user = insert(:user)
other_user = insert(:user)
{:ok, op} = CommonAPI.post(other_user, %{"status" => "big oof"})
{:ok, post} = CommonAPI.post(user, %{"status" => "hey", "in_reply_to_id" => op})
object = Object.normalize(post)
{:ok, delete_data, _meta} = Builder.delete(user, object.data["id"])
{:ok, delete_user_data, _meta} = Builder.delete(user, user.ap_id)
{:ok, delete, _meta} = ActivityPub.persist(delete_data, local: true)
{:ok, delete_user, _meta} = ActivityPub.persist(delete_user_data, local: true)
%{user: user, delete: delete, post: post, object: object, delete_user: delete_user, op: op}
end
test "it handles object deletions", %{
delete: delete,
post: post,
object: object,
user: user,
op: op
} do
with_mock Pleroma.Web.ActivityPub.ActivityPub, [:passthrough],
stream_out: fn _ -> nil end,
stream_out_participations: fn _, _ -> nil end do
{:ok, delete, _} = SideEffects.handle(delete)
user = User.get_cached_by_ap_id(object.data["actor"])
assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out(delete))
assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out_participations(object, user))
end
object = Object.get_by_id(object.id)
assert object.data["type"] == "Tombstone"
refute Activity.get_by_id(post.id)
user = User.get_by_id(user.id)
assert user.note_count == 0
object = Object.normalize(op.data["object"], false)
assert object.data["repliesCount"] == 0
end
test "it handles user deletions", %{delete_user: delete, user: user} do
{:ok, _delete, _} = SideEffects.handle(delete)
ObanHelpers.perform_all()
assert User.get_cached_by_ap_id(user.ap_id).deactivated
end
end
describe "like objects" do describe "like objects" do
setup do setup do

View File

@ -0,0 +1,86 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.DeleteHandlingTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
test "it works for incoming deletes" do
activity = insert(:note_activity)
deleting_user = insert(:user)
data =
File.read!("test/fixtures/mastodon-delete.json")
|> Poison.decode!()
|> Map.put("actor", deleting_user.ap_id)
|> put_in(["object", "id"], activity.data["object"])
{:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} =
Transmogrifier.handle_incoming(data)
assert id == data["id"]
# We delete the Create activity because we base our timelines on it.
# This should be changed after we unify objects and activities
refute Activity.get_by_id(activity.id)
assert actor == deleting_user.ap_id
# Objects are replaced by a tombstone object.
object = Object.normalize(activity.data["object"])
assert object.data["type"] == "Tombstone"
end
test "it fails for incoming deletes with spoofed origin" do
activity = insert(:note_activity)
%{ap_id: ap_id} = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo")
data =
File.read!("test/fixtures/mastodon-delete.json")
|> Poison.decode!()
|> Map.put("actor", ap_id)
|> put_in(["object", "id"], activity.data["object"])
assert match?({:error, _}, Transmogrifier.handle_incoming(data))
end
@tag capture_log: true
test "it works for incoming user deletes" do
%{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin")
data =
File.read!("test/fixtures/mastodon-delete-user.json")
|> Poison.decode!()
{:ok, _} = Transmogrifier.handle_incoming(data)
ObanHelpers.perform_all()
assert User.get_cached_by_ap_id(ap_id).deactivated
end
test "it fails for incoming user deletes with spoofed origin" do
%{ap_id: ap_id} = insert(:user)
data =
File.read!("test/fixtures/mastodon-delete-user.json")
|> Poison.decode!()
|> Map.put("actor", ap_id)
assert match?({:error, _}, Transmogrifier.handle_incoming(data))
assert User.get_cached_by_ap_id(ap_id)
end
end

View File

@ -766,84 +766,6 @@ test "it works for incoming update activities which lock the account" do
assert user.locked == true assert user.locked == true
end end
test "it works for incoming deletes" do
activity = insert(:note_activity)
deleting_user = insert(:user)
data =
File.read!("test/fixtures/mastodon-delete.json")
|> Poison.decode!()
object =
data["object"]
|> Map.put("id", activity.data["object"])
data =
data
|> Map.put("object", object)
|> Map.put("actor", deleting_user.ap_id)
{:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} =
Transmogrifier.handle_incoming(data)
assert id == data["id"]
refute Activity.get_by_id(activity.id)
assert actor == deleting_user.ap_id
end
test "it fails for incoming deletes with spoofed origin" do
activity = insert(:note_activity)
data =
File.read!("test/fixtures/mastodon-delete.json")
|> Poison.decode!()
object =
data["object"]
|> Map.put("id", activity.data["object"])
data =
data
|> Map.put("object", object)
assert capture_log(fn ->
:error = Transmogrifier.handle_incoming(data)
end) =~
"[error] Could not decode user at fetch http://mastodon.example.org/users/gargron, {:error, :nxdomain}"
assert Activity.get_by_id(activity.id)
end
@tag capture_log: true
test "it works for incoming user deletes" do
%{ap_id: ap_id} =
insert(:user, ap_id: "http://mastodon.example.org/users/admin", local: false)
data =
File.read!("test/fixtures/mastodon-delete-user.json")
|> Poison.decode!()
{:ok, _} = Transmogrifier.handle_incoming(data)
ObanHelpers.perform_all()
refute User.get_cached_by_ap_id(ap_id)
end
test "it fails for incoming user deletes with spoofed origin" do
%{ap_id: ap_id} = insert(:user)
data =
File.read!("test/fixtures/mastodon-delete-user.json")
|> Poison.decode!()
|> Map.put("actor", ap_id)
assert capture_log(fn ->
assert :error == Transmogrifier.handle_incoming(data)
end) =~ "Object containment failed"
assert User.get_cached_by_ap_id(ap_id)
end
test "it works for incoming unannounces with an existing notice" do test "it works for incoming unannounces with an existing notice" do
user = insert(:user) user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"}) {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})

View File

@ -6,8 +6,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
import ExUnit.CaptureLog import ExUnit.CaptureLog
import Mock
import Pleroma.Factory
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Config alias Pleroma.Config
@ -147,17 +148,26 @@ test "GET /api/pleroma/admin/users/:nickname requires " <>
test "single user", %{admin: admin, conn: conn} do test "single user", %{admin: admin, conn: conn} do
user = insert(:user) user = insert(:user)
conn = with_mock Pleroma.Web.Federator,
conn publish: fn _ -> nil end do
|> put_req_header("accept", "application/json") conn =
|> delete("/api/pleroma/admin/users?nickname=#{user.nickname}") conn
|> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/users?nickname=#{user.nickname}")
log_entry = Repo.one(ModerationLog) ObanHelpers.perform_all()
assert ModerationLog.get_log_entry_message(log_entry) == assert User.get_by_nickname(user.nickname).deactivated
"@#{admin.nickname} deleted users: @#{user.nickname}"
assert json_response(conn, 200) == user.nickname log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deleted users: @#{user.nickname}"
assert json_response(conn, 200) == [user.nickname]
assert called(Pleroma.Web.Federator.publish(:_))
end
end end
test "multiple users", %{admin: admin, conn: conn} do test "multiple users", %{admin: admin, conn: conn} do

View File

@ -10,11 +10,13 @@ defmodule Pleroma.Web.CommonAPITest do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
import Pleroma.Factory import Pleroma.Factory
import Mock
require Pleroma.Constants require Pleroma.Constants
@ -71,6 +73,84 @@ test "it reject messages over the local limit" do
end end
end end
describe "deletion" do
test "it allows users to delete their posts" do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{"status" => "namu amida butsu"})
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
assert {:ok, delete} = CommonAPI.delete(post.id, user)
assert delete.local
assert called(Pleroma.Web.Federator.publish(delete))
end
refute Activity.get_by_id(post.id)
end
test "it does not allow a user to delete their posts" do
user = insert(:user)
other_user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{"status" => "namu amida butsu"})
assert {:error, "Could not delete"} = CommonAPI.delete(post.id, other_user)
assert Activity.get_by_id(post.id)
end
test "it allows moderators to delete other user's posts" do
user = insert(:user)
moderator = insert(:user, is_moderator: true)
{:ok, post} = CommonAPI.post(user, %{"status" => "namu amida butsu"})
assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
assert delete.local
refute Activity.get_by_id(post.id)
end
test "it allows admins to delete other user's posts" do
user = insert(:user)
moderator = insert(:user, is_admin: true)
{:ok, post} = CommonAPI.post(user, %{"status" => "namu amida butsu"})
assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
assert delete.local
refute Activity.get_by_id(post.id)
end
test "superusers deleting non-local posts won't federate the delete" do
# This is the user of the ingested activity
_user =
insert(:user,
local: false,
ap_id: "http://mastodon.example.org/users/admin",
last_refreshed_at: NaiveDateTime.utc_now()
)
moderator = insert(:user, is_admin: true)
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Jason.decode!()
{:ok, post} = Transmogrifier.handle_incoming(data)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
assert delete.local
refute called(Pleroma.Web.Federator.publish(:_))
end
refute Activity.get_by_id(post.id)
end
end
test "favoriting race condition" do test "favoriting race condition" do
user = insert(:user) user = insert(:user)
users_serial = insert_list(10, :user) users_serial = insert_list(10, :user)

View File

@ -210,6 +210,12 @@ test "it sends to public" do
Worker.push_to_socket(topics, "public", activity) Worker.push_to_socket(topics, "public", activity)
Task.await(task) Task.await(task)
end
test "works for deletions" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "Test"})
task = task =
Task.async(fn -> Task.async(fn ->