Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into feature/emojireactv...
authorlain <lain@soykaf.club>
Thu, 7 May 2020 13:03:12 +0000 (15:03 +0200)
committerlain <lain@soykaf.club>
Thu, 7 May 2020 13:03:12 +0000 (15:03 +0200)
13 files changed:
1  2 
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/builder.ex
lib/pleroma/web/activity_pub/object_validator.ex
lib/pleroma/web/activity_pub/side_effects.ex
lib/pleroma/web/activity_pub/transmogrifier.ex
lib/pleroma/web/common_api/common_api.ex
test/notification_test.exs
test/web/activity_pub/activity_pub_test.exs
test/web/activity_pub/object_validator_test.exs
test/web/activity_pub/side_effects_test.exs
test/web/activity_pub/transmogrifier_test.exs
test/web/common_api/common_api_test.exs
test/web/mastodon_api/views/status_view_test.exs

index 4c6ac9241d0d74048997d0cc558c7913a11cdd3d,8baaf97ac822348975ae8ec7fb2a36b880cf93ee..fcc3ce7289b71497d681a992b2dd5d6fe88f55a7
@@@ -170,12 -170,6 +170,6 @@@ defmodule Pleroma.Web.ActivityPub.Activ
  
        BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id})
  
-       Notification.create_notifications(activity)
-       conversation = create_or_bump_conversation(activity, map["actor"])
-       participations = get_participations(conversation)
-       stream_out(activity)
-       stream_out_participations(participations)
        {:ok, activity}
      else
        %Activity{} = activity ->
      end
    end
  
+   def notify_and_stream(activity) do
+     Notification.create_notifications(activity)
+     conversation = create_or_bump_conversation(activity, activity.actor)
+     participations = get_participations(conversation)
+     stream_out(activity)
+     stream_out_participations(participations)
+   end
    defp create_or_bump_conversation(activity, actor) do
      with {:ok, conversation} <- Conversation.create_or_bump_for(activity),
           %User{} = user <- User.get_cached_by_ap_id(actor),
           _ <- increase_poll_votes_if_vote(create_data),
           {:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
           {:ok, _actor} <- increase_note_count_if_public(actor, activity),
+          _ <- notify_and_stream(activity),
           :ok <- maybe_federate(activity) do
        {:ok, activity}
      else
               additional
             ),
           {:ok, activity} <- insert(listen_data, local),
+          _ <- notify_and_stream(activity),
           :ok <- maybe_federate(activity) do
        {:ok, activity}
      end
             %{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object}
             |> Utils.maybe_put("id", activity_id),
           {:ok, activity} <- insert(data, local),
+          _ <- notify_and_stream(activity),
           :ok <- maybe_federate(activity) do
        {:ok, activity}
      end
           },
           data <- Utils.maybe_put(data, "id", activity_id),
           {:ok, activity} <- insert(data, local),
+          _ <- notify_and_stream(activity),
           :ok <- maybe_federate(activity) do
        {:ok, activity}
      end
    end
  
 -  @spec react_with_emoji(User.t(), Object.t(), String.t(), keyword()) ::
 -          {:ok, Activity.t(), Object.t()} | {:error, any()}
 -  def react_with_emoji(user, object, emoji, options \\ []) do
 -    with {:ok, result} <-
 -           Repo.transaction(fn -> do_react_with_emoji(user, object, emoji, options) end) do
 -      result
 -    end
 -  end
 -
 -  defp do_react_with_emoji(user, object, emoji, options) do
 -    with local <- Keyword.get(options, :local, true),
 -         activity_id <- Keyword.get(options, :activity_id, nil),
 -         true <- Pleroma.Emoji.is_unicode_emoji?(emoji),
 -         reaction_data <- make_emoji_reaction_data(user, object, emoji, activity_id),
 -         {:ok, activity} <- insert(reaction_data, local),
 -         {:ok, object} <- add_emoji_reaction_to_object(activity, object),
 -         _ <- notify_and_stream(activity),
 -         :ok <- maybe_federate(activity) do
 -      {:ok, activity, object}
 -    else
 -      false -> {:error, false}
 -      {:error, error} -> Repo.rollback(error)
 -    end
 -  end
 -
    @spec unreact_with_emoji(User.t(), String.t(), keyword()) ::
            {:ok, Activity.t(), Object.t()} | {:error, any()}
    def unreact_with_emoji(user, reaction_id, options \\ []) do
           unreact_data <- make_undo_data(user, reaction_activity, activity_id),
           {:ok, activity} <- insert(unreact_data, local),
           {:ok, object} <- remove_emoji_reaction_from_object(reaction_activity, object),
+          _ <- notify_and_stream(activity),
           :ok <- maybe_federate(activity) do
        {:ok, activity, object}
      else
           {:ok, unlike_activity} <- insert(unlike_data, local),
           {:ok, _activity} <- Repo.delete(like_activity),
           {:ok, object} <- remove_like_from_object(like_activity, object),
+          _ <- notify_and_stream(unlike_activity),
           :ok <- maybe_federate(unlike_activity) do
        {:ok, unlike_activity, like_activity, object}
      else
           announce_data <- make_announce_data(user, object, activity_id, public),
           {:ok, activity} <- insert(announce_data, local),
           {:ok, object} <- add_announce_to_object(activity, object),
+          _ <- notify_and_stream(activity),
           :ok <- maybe_federate(activity) do
        {:ok, activity, object}
      else
      with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object),
           unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
           {:ok, unannounce_activity} <- insert(unannounce_data, local),
+          _ <- notify_and_stream(unannounce_activity),
           :ok <- maybe_federate(unannounce_activity),
           {:ok, _activity} <- Repo.delete(announce_activity),
           {:ok, object} <- remove_announce_from_object(announce_activity, object) do
    defp do_follow(follower, followed, activity_id, local) do
      with data <- make_follow_data(follower, followed, activity_id),
           {:ok, activity} <- insert(data, local),
+          _ <- notify_and_stream(activity),
           :ok <- maybe_federate(activity) do
        {:ok, activity}
      else
           {:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
           unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
           {:ok, activity} <- insert(unfollow_data, local),
+          _ <- notify_and_stream(activity),
           :ok <- maybe_federate(activity) do
        {:ok, activity}
      else
      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()) ::
            {:ok, Activity.t()} | {:error, any()}
    def block(blocker, blocked, activity_id \\ nil, local \\ true) do
      with true <- outgoing_blocks,
           block_data <- make_block_data(blocker, blocked, activity_id),
           {:ok, activity} <- insert(block_data, local),
+          _ <- notify_and_stream(activity),
           :ok <- maybe_federate(activity) do
        {:ok, activity}
      else
      with %Activity{} = block_activity <- fetch_latest_block(blocker, blocked),
           unblock_data <- make_unblock_data(blocker, blocked, block_activity, activity_id),
           {:ok, activity} <- insert(unblock_data, local),
+          _ <- notify_and_stream(activity),
           :ok <- maybe_federate(activity) do
        {:ok, activity}
      else
      with flag_data <- make_flag_data(params, additional),
           {:ok, activity} <- insert(flag_data, local),
           {:ok, stripped_activity} <- strip_report_status_data(activity),
+          _ <- notify_and_stream(activity),
           :ok <- maybe_federate(stripped_activity) do
        User.all_superusers()
        |> Enum.filter(fn user -> not is_nil(user.email) end)
      }
  
      with true <- origin.ap_id in target.also_known_as,
-          {:ok, activity} <- insert(params, local) do
+          {:ok, activity} <- insert(params, local),
+          _ <- notify_and_stream(activity) do
        maybe_federate(activity)
  
        BackgroundWorker.enqueue("move_following", %{
index 2a763645c4016c8d6edfbf6ba37e75466d7da49c,1345a3a3e2cf2430e42990f0d8b758f1d7a20266..d130176cf745c8622191522c9e15a67e6a09d8f9
@@@ -10,18 -10,33 +10,45 @@@ defmodule Pleroma.Web.ActivityPub.Build
    alias Pleroma.Web.ActivityPub.Utils
    alias Pleroma.Web.ActivityPub.Visibility
  
 +  @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
 +  def emoji_react(actor, object, emoji) do
 +    with {:ok, data, meta} <- like(actor, object) do
 +      data =
 +        data
 +        |> Map.put("content", emoji)
 +        |> Map.put("type", "EmojiReact")
 +
 +      {:ok, data, meta}
 +    end
 +  end
 +
+   @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
    @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
    def like(actor, object) do
      object_actor = User.get_cached_by_ap_id(object.data["actor"])
index d730cb062a91dc23484077f7321f6121cdbf9993,479f922f51296a5a55e8bf0fb4c8c2937f9320d3..e51a8e0a824bbb5ff17b37751ca7e65a0a7cc614
@@@ -11,12 -11,23 +11,24 @@@ defmodule Pleroma.Web.ActivityPub.Objec
  
    alias Pleroma.Object
    alias Pleroma.User
+   alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
 +  alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
    alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
+   alias Pleroma.Web.ActivityPub.ObjectValidators.Types
  
    @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
    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
      with {:ok, object} <-
             object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do
      end
    end
  
 +  def validate(%{"type" => "EmojiReact"} = object, meta) do
 +    with {:ok, object} <-
 +           object
 +           |> EmojiReactValidator.cast_and_validate()
 +           |> Ecto.Changeset.apply_action(:insert) do
 +      object = stringify_keys(object |> Map.from_struct())
 +      {:ok, object, meta}
 +    end
 +  end
 +
+   def stringify_keys(%{__struct__: _} = object) do
+     object
+     |> Map.from_struct()
+     |> stringify_keys
+   end
    def stringify_keys(object) do
      object
      |> Map.new(fn {key, val} -> {to_string(key), val} end)
    end
  
+   def fetch_actor(object) do
+     with {:ok, actor} <- Types.ObjectID.cast(object["actor"]) do
+       User.get_or_fetch_by_ap_id(actor)
+     end
+   end
    def fetch_actor_and_object(object) do
-     User.get_or_fetch_by_ap_id(object["actor"])
+     fetch_actor(object)
      Object.normalize(object["object"])
      :ok
    end
index b15343c070821f81e590873da67fcb6d23fd65c7,7b53abeafc61de0f69bfef89b81c14f8265e8aaf..8e5586e88702df23795a0c507cbc224e5f3758ca
@@@ -7,6 -7,8 +7,8 @@@ defmodule Pleroma.Web.ActivityPub.SideE
    """
    alias Pleroma.Notification
    alias Pleroma.Object
+   alias Pleroma.User
+   alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.ActivityPub.Utils
  
    def handle(object, meta \\ [])
      {:ok, object, meta}
    end
  
 +  # Tasks this handles:
 +  # - Add reaction to object
 +  # - Set up notification
 +  def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do
 +    reacted_object = Object.get_by_ap_id(object.data["object"])
 +    Utils.add_emoji_reaction_to_object(object, reacted_object)
 +
 +    Notification.create_notifications(object)
 +
 +    {:ok, object, meta}
 +  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
    def handle(object, meta) do
      {:ok, object, meta}
index 81e763f883fc086ff1903ef49bc49c268b5161bf,0e4e7261b1177caebb5b2dc630fbe02ea8a1b8f3..ee6fc31cef0242b46b80dfa62009e1e18ec7bfcf
@@@ -656,7 -656,7 +656,7 @@@ defmodule Pleroma.Web.ActivityPub.Trans
      |> handle_incoming(options)
    end
  
 -  def handle_incoming(%{"type" => "Like"} = data, _options) do
 +  def handle_incoming(%{"type" => type} = data, _options) when type in ["Like", "EmojiReact"] do
      with :ok <- ObjectValidator.fetch_actor_and_object(data),
           {:ok, activity, _meta} <-
             Pipeline.common_pipeline(data, local: false) do
      end
    end
  
 -  def handle_incoming(
 -        %{
 -          "type" => "EmojiReact",
 -          "object" => object_id,
 -          "actor" => _actor,
 -          "id" => id,
 -          "content" => emoji
 -        } = data,
 -        _options
 -      ) 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, activity, _object} <-
 -           ActivityPub.react_with_emoji(actor, object, emoji, activity_id: id, local: false) do
 -      {:ok, activity}
 -    else
 -      _e -> :error
 -    end
 -  end
 -
    def handle_incoming(
          %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
          _options
      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(
-         %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
+         %{"type" => "Delete"} = data,
          _options
        ) do
-     object_id = Utils.get_ap_id(object_id)
-     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
+     with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
        {: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
  
      Map.put(object, "conversation", object["context"])
    end
  
+   def set_sensitive(%{"sensitive" => true} = object) do
+     object
+   end
    def set_sensitive(object) do
      tags = object["tag"] || []
      Map.put(object, "sensitive", "nsfw" in tags)
index 192c84edaa2b895a1ed31bc0d7777bd21716d2f9,986e8d3f8bb8a6cc8ac6d3ca2f8a8a1553ea200c..b23de2bfff2cde60ea522bb5f4654c96ba82e58d
@@@ -79,8 -79,8 +79,8 @@@ defmodule Pleroma.Web.CommonAPI d
             {:find_activity, Activity.get_by_id_with_object(activity_id)},
           %Object{} = object <- Object.normalize(activity),
           true <- User.superuser?(user) || user.ap_id == object.data["actor"],
-          {:ok, _} <- unpin(activity_id, user),
-          {: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}
      else
        {:find_activity, _} -> {:error, :not_found}
  
    def react_with_emoji(id, user, emoji) do
      with %Activity{} = activity <- Activity.get_by_id(id),
 -         object <- Object.normalize(activity) do
 -      ActivityPub.react_with_emoji(user, object, emoji)
 +         object <- Object.normalize(activity),
 +         {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
 +         {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
 +      {:ok, activity}
      else
        _ ->
          {:error, dgettext("errors", "Could not add reaction emoji")}
index bd562c85cf86300f316e3cc6463ef2173da871e6,5c85f3368ba05376ecdecb20ead42b2e6128864a..5b514e9dbde0a48bceca444f5853b84e617f008c
@@@ -24,7 -24,7 +24,7 @@@ defmodule Pleroma.NotificationTest d
        other_user = insert(:user)
  
        {:ok, activity} = CommonAPI.post(user, %{"status" => "yeah"})
 -      {:ok, activity, _object} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
 +      {:ok, activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
  
        {:ok, [notification]} = Notification.create_notifications(activity)
  
      @tag needs_streamer: true
      test "it creates a notification for user and send to the 'user' and the 'user:notification' stream" do
        user = insert(:user)
-       task = Task.async(fn -> assert_receive {:text, _}, 4_000 end)
-       task_user_notification = Task.async(fn -> assert_receive {:text, _}, 4_000 end)
-       Streamer.add_socket("user", %{transport_pid: task.pid, assigns: %{user: user}})
-       Streamer.add_socket(
-         "user:notification",
-         %{transport_pid: task_user_notification.pid, assigns: %{user: user}}
-       )
+       task =
+         Task.async(fn ->
+           Streamer.add_socket("user", user)
+           assert_receive {:render_with_user, _, _, _}, 4_000
+         end)
+       task_user_notification =
+         Task.async(fn ->
+           Streamer.add_socket("user:notification", user)
+           assert_receive {:render_with_user, _, _, _}, 4_000
+         end)
  
        activity = insert(:note_activity)
  
index 1ac4f9896de65be1c6339dedc00e301a5db753c8,4dc9c0f0a8b72ab81ac5153f34c1a276bc09dbdf..4b70af5a610b8b01cdb7e7cd6921550ed51b77b0
@@@ -874,6 -874,71 +874,6 @@@ defmodule Pleroma.Web.ActivityPub.Activ
      end
    end
  
 -  describe "react to an object" do
 -    test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
 -      Config.put([:instance, :federating], true)
 -      user = insert(:user)
 -      reactor = insert(:user)
 -      {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
 -      assert object = Object.normalize(activity)
 -
 -      {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
 -
 -      assert called(Federator.publish(reaction_activity))
 -    end
 -
 -    test "adds an emoji reaction activity to the db" do
 -      user = insert(:user)
 -      reactor = insert(:user)
 -      third_user = insert(:user)
 -      fourth_user = insert(:user)
 -      {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
 -      assert object = Object.normalize(activity)
 -
 -      {:ok, reaction_activity, object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
 -
 -      assert reaction_activity
 -
 -      assert reaction_activity.data["actor"] == reactor.ap_id
 -      assert reaction_activity.data["type"] == "EmojiReact"
 -      assert reaction_activity.data["content"] == "🔥"
 -      assert reaction_activity.data["object"] == object.data["id"]
 -      assert reaction_activity.data["to"] == [User.ap_followers(reactor), activity.data["actor"]]
 -      assert reaction_activity.data["context"] == object.data["context"]
 -      assert object.data["reaction_count"] == 1
 -      assert object.data["reactions"] == [["🔥", [reactor.ap_id]]]
 -
 -      {:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(third_user, object, "☕")
 -
 -      assert object.data["reaction_count"] == 2
 -      assert object.data["reactions"] == [["🔥", [reactor.ap_id]], ["☕", [third_user.ap_id]]]
 -
 -      {:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(fourth_user, object, "🔥")
 -
 -      assert object.data["reaction_count"] == 3
 -
 -      assert object.data["reactions"] == [
 -               ["🔥", [fourth_user.ap_id, reactor.ap_id]],
 -               ["☕", [third_user.ap_id]]
 -             ]
 -    end
 -
 -    test "reverts emoji reaction on error" do
 -      [user, reactor] = insert_list(2, :user)
 -
 -      {:ok, activity} = CommonAPI.post(user, %{"status" => "Status"})
 -      object = Object.normalize(activity)
 -
 -      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
 -        assert {:error, :reverted} = ActivityPub.react_with_emoji(reactor, object, "😀")
 -      end
 -
 -      object = Object.get_by_ap_id(object.data["id"])
 -      refute object.data["reaction_count"]
 -      refute object.data["reactions"]
 -    end
 -  end
 -
    describe "unreacting to an object" do
      test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
        Config.put([:instance, :federating], true)
        {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
        assert object = Object.normalize(activity)
  
 -      {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
 +      {:ok, reaction_activity} = CommonAPI.react_with_emoji(activity.id, reactor, "🔥")
  
        assert called(Federator.publish(reaction_activity))
  
        {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
        assert object = Object.normalize(activity)
  
 -      {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
 +      {:ok, reaction_activity} = CommonAPI.react_with_emoji(activity.id, reactor, "🔥")
  
        {:ok, unreaction_activity, _object} =
          ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
        {:ok, activity} = CommonAPI.post(user, %{"status" => "Status"})
        object = Object.normalize(activity)
  
 -      {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "😀")
 +      {:ok, reaction_activity} = CommonAPI.react_with_emoji(activity.id, reactor, "😀")
  
        with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
          assert {:error, :reverted} =
      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
      test "it filters broken threads" do
        user1 = insert(:user)
index a7ad8e6462085b40458da870c57e6eccfa399818,744c46781d33dab03549624f1b3db52873eba764..4cae5207703ae1a813e4e3934bf6212042d5a486
@@@ -1,6 -1,7 +1,7 @@@
  defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
    use Pleroma.DataCase
  
+   alias Pleroma.Object
    alias Pleroma.Web.ActivityPub.Builder
    alias Pleroma.Web.ActivityPub.ObjectValidator
    alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
  
    import Pleroma.Factory
  
 +  describe "EmojiReacts" do
 +    setup do
 +      user = insert(:user)
 +      {:ok, post_activity} = CommonAPI.post(user, %{"status" => "uguu"})
 +
 +      object = Pleroma.Object.get_by_ap_id(post_activity.data["object"])
 +
 +      {:ok, valid_emoji_react, []} = Builder.emoji_react(user, object, "👌")
 +
 +      %{user: user, post_activity: post_activity, valid_emoji_react: valid_emoji_react}
 +    end
 +
 +    test "it validates a valid EmojiReact", %{valid_emoji_react: valid_emoji_react} do
 +      assert {:ok, _, _} = ObjectValidator.validate(valid_emoji_react, [])
 +    end
 +
 +    test "it is not valid without a 'content' field", %{valid_emoji_react: valid_emoji_react} do
 +      without_content =
 +        valid_emoji_react
 +        |> Map.delete("content")
 +
 +      {:error, cng} = ObjectValidator.validate(without_content, [])
 +
 +      refute cng.valid?
 +      assert {:content, {"can't be blank", [validation: :required]}} in cng.errors
 +    end
 +
 +    test "it is not valid with a non-emoji content field", %{valid_emoji_react: valid_emoji_react} do
 +      without_emoji_content =
 +        valid_emoji_react
 +        |> Map.put("content", "x")
 +
 +      {:error, cng} = ObjectValidator.validate(without_emoji_content, [])
 +
 +      refute cng.valid?
 +
 +      assert {:content, {"must be a single character emoji", []}} in cng.errors
 +    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
      setup do
        user = insert(:user)
index 9271d5ba14a3f845d56f0996491a59bc4dc6d139,a9598d7b36ecbdc2c21507c177fd7aa2a4b52710..404b129ea752e850efa3b79c047d512a31585f0b
  # SPDX-License-Identifier: AGPL-3.0-only
  
  defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
+   use Oban.Testing, repo: Pleroma.Repo
    use Pleroma.DataCase
  
+   alias Pleroma.Activity
    alias Pleroma.Notification
    alias Pleroma.Object
    alias Pleroma.Repo
+   alias Pleroma.Tests.ObanHelpers
+   alias Pleroma.User
    alias Pleroma.Web.ActivityPub.ActivityPub
    alias Pleroma.Web.ActivityPub.Builder
    alias Pleroma.Web.ActivityPub.SideEffects
    alias Pleroma.Web.CommonAPI
  
    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 "EmojiReact objects" do
 +    setup do
 +      poster = insert(:user)
 +      user = insert(:user)
 +
 +      {:ok, post} = CommonAPI.post(poster, %{"status" => "hey"})
 +
 +      {:ok, emoji_react_data, []} = Builder.emoji_react(user, post.object, "👌")
 +      {:ok, emoji_react, _meta} = ActivityPub.persist(emoji_react_data, local: true)
 +
 +      %{emoji_react: emoji_react, user: user, poster: poster}
 +    end
 +
 +    test "adds the reaction to the object", %{emoji_react: emoji_react, user: user} do
 +      {:ok, emoji_react, _} = SideEffects.handle(emoji_react)
 +      object = Object.get_by_ap_id(emoji_react.data["object"])
 +
 +      assert object.data["reaction_count"] == 1
 +      assert ["👌", [user.ap_id]] in object.data["reactions"]
 +    end
 +
 +    test "creates a notification", %{emoji_react: emoji_react, poster: poster} do
 +      {:ok, emoji_react, _} = SideEffects.handle(emoji_react)
 +      assert Repo.get_by(Notification, user_id: poster.id, activity_id: emoji_react.id)
 +    end
 +  end
 +
    describe "like objects" do
      setup do
        poster = insert(:user)
index 7deac29094d2a29a4ae096ec35012124e5d9cb84,6d43c3365e49daa4d596c4635c486fad66d3364e..336ddb32303c5ec259016f11faec25f529828516
@@@ -325,11 -325,48 +325,11 @@@ defmodule Pleroma.Web.ActivityPub.Trans
        assert object_data["cc"] == to
      end
  
 -    test "it works for incoming emoji reactions" do
 -      user = insert(:user)
 -      {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
 -
 -      data =
 -        File.read!("test/fixtures/emoji-reaction.json")
 -        |> Poison.decode!()
 -        |> Map.put("object", activity.data["object"])
 -
 -      {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
 -
 -      assert data["actor"] == "http://mastodon.example.org/users/admin"
 -      assert data["type"] == "EmojiReact"
 -      assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2"
 -      assert data["object"] == activity.data["object"]
 -      assert data["content"] == "👌"
 -    end
 -
 -    test "it reject invalid emoji reactions" do
 -      user = insert(:user)
 -      {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
 -
 -      data =
 -        File.read!("test/fixtures/emoji-reaction-too-long.json")
 -        |> Poison.decode!()
 -        |> Map.put("object", activity.data["object"])
 -
 -      assert :error = Transmogrifier.handle_incoming(data)
 -
 -      data =
 -        File.read!("test/fixtures/emoji-reaction-no-emoji.json")
 -        |> Poison.decode!()
 -        |> Map.put("object", activity.data["object"])
 -
 -      assert :error = Transmogrifier.handle_incoming(data)
 -    end
 -
      test "it works for incoming emoji reaction undos" do
        user = insert(:user)
  
        {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
 -      {:ok, reaction_activity, _object} = CommonAPI.react_with_emoji(activity.id, user, "👌")
 +      {:ok, reaction_activity} = CommonAPI.react_with_emoji(activity.id, user, "👌")
  
        data =
          File.read!("test/fixtures/mastodon-undo-like.json")
      test "it strips internal reactions" do
        user = insert(:user)
        {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
 -      {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "📢")
 +      {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "📢")
  
        %{object: object} = Activity.get_by_id_with_object(activity.id)
        assert Map.has_key?(object.data, "reactions")
        assert user.locked == true
      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
        user = insert(:user)
        {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
index 74171fcd997ce6d1ee7f1f22da9d5ebfd3448864,62a2665b639682841c2ee78731c84723001dc234..e5f7e3ef8afeeb6649f83fb4fdcd9cc7c451de26
@@@ -9,11 -9,13 +9,13 @@@ defmodule Pleroma.Web.CommonAPITest d
    alias Pleroma.Object
    alias Pleroma.User
    alias Pleroma.Web.ActivityPub.ActivityPub
+   alias Pleroma.Web.ActivityPub.Transmogrifier
    alias Pleroma.Web.ActivityPub.Visibility
    alias Pleroma.Web.AdminAPI.AccountView
    alias Pleroma.Web.CommonAPI
  
    import Pleroma.Factory
+   import Mock
  
    require Pleroma.Constants
  
    setup do: clear_config([:instance, :limit])
    setup do: clear_config([:instance, :max_pinned_statuses])
  
+   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
      user = insert(:user)
      users_serial = insert_list(10, :user)
  
        {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
  
 -      {:ok, reaction, _} = CommonAPI.react_with_emoji(activity.id, user, "👍")
 +      {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
  
        assert reaction.data["actor"] == user.ap_id
        assert reaction.data["content"] == "👍"
        other_user = insert(:user)
  
        {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
 -      {:ok, reaction, _} = CommonAPI.react_with_emoji(activity.id, user, "👍")
 +      {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
  
        {:ok, unreaction, _} = CommonAPI.unreact_with_emoji(activity.id, user, "👍")
  
index b64370c3feaa01efa52f7bdec323604542c8de21,451723e6017f64a42d56ea6a991701c161cfb591..b5e7dc3171bad7a27f8e3b844eb7383fad3dd4e7
@@@ -32,9 -32,9 +32,9 @@@ defmodule Pleroma.Web.MastodonAPI.Statu
      third_user = insert(:user)
      {:ok, activity} = CommonAPI.post(user, %{"status" => "dae cofe??"})
  
 -    {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "☕")
 -    {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵")
 -    {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
 +    {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "☕")
 +    {:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵")
 +    {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
      activity = Repo.get(Activity, activity.id)
      status = StatusView.render("show.json", activity: activity)
  
        pleroma: %{mime_type: "image/png"}
      }
  
+     api_spec = Pleroma.Web.ApiSpec.spec()
      assert expected == StatusView.render("attachment.json", %{attachment: object})
+     OpenApiSpex.TestAssertions.assert_schema(expected, "Attachment", api_spec)
  
      # If theres a "id", use that instead of the generated one
      object = Map.put(object, "id", 2)
-     assert %{id: "2"} = StatusView.render("attachment.json", %{attachment: object})
+     result = StatusView.render("attachment.json", %{attachment: object})
+     assert %{id: "2"} = result
+     OpenApiSpex.TestAssertions.assert_schema(result, "Attachment", api_spec)
    end
  
    test "put the url advertised in the Activity in to the url attribute" do