Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into remake-remodel-dms
authorlain <lain@soykaf.club>
Wed, 6 May 2020 15:36:08 +0000 (17:36 +0200)
committerlain <lain@soykaf.club>
Wed, 6 May 2020 15:36:08 +0000 (17:36 +0200)
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/web/activity_pub/object_validator_test.exs
test/web/activity_pub/side_effects_test.exs
test/web/common_api/common_api_test.exs

index 67e65c7b990049f0b1425ebedabc49fe14527b11,1345a3a3e2cf2430e42990f0d8b758f1d7a20266..6e3a375e7a33f153cc771b922c122985ede9b3f0
@@@ -11,42 -10,33 +11,69 @@@ defmodule Pleroma.Web.ActivityPub.Build
    alias Pleroma.Web.ActivityPub.Utils
    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
 +    {:ok,
 +     %{
 +       "id" => Utils.generate_activity_id(),
 +       "actor" => actor.ap_id,
 +       "to" => recipients,
 +       "object" => object,
 +       "type" => "Create",
 +       "published" => DateTime.utc_now() |> DateTime.to_iso8601()
 +     }, []}
 +  end
 +
 +  def chat_message(actor, recipient, content, opts \\ []) do
 +    basic = %{
 +      "id" => Utils.generate_object_id(),
 +      "actor" => actor.ap_id,
 +      "type" => "ChatMessage",
 +      "to" => [recipient],
 +      "content" => content,
 +      "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
 +      "emoji" => Emoji.Formatter.get_emoji_map(content)
 +    }
 +
 +    case opts[:attachment] do
 +      %Object{data: attachment_data} ->
 +        {
 +          :ok,
 +          Map.put(basic, "attachment", attachment_data),
 +          []
 +        }
 +
 +      _ ->
 +        {:ok, basic, []}
 +    end
 +  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 d6c14f7b8248873ed9a3419fd41b381f2823e820,479f922f51296a5a55e8bf0fb4c8c2937f9320d3..cc5ca1d9ec63600ebe7053f77f655eb720c6c84a
@@@ -11,20 -11,27 +11,31 @@@ defmodule Pleroma.Web.ActivityPub.Objec
  
    alias Pleroma.Object
    alias Pleroma.User
 +  alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
 +  alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
+   alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
    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
 -      object = stringify_keys(object |> Map.from_struct())
 +           object
 +           |> LikeValidator.cast_and_validate()
 +           |> Ecto.Changeset.apply_action(:insert) do
 +      object = stringify_keys(object)
        {:ok, object, meta}
      end
    end
index e394c75d7d36bce85e606df8929c4a3da5d7699a,7b53abeafc61de0f69bfef89b81c14f8265e8aaf..8bdc433ff745a5ebe670fb8d87370011d6fc09da
@@@ -5,12 -5,10 +5,13 @@@ defmodule Pleroma.Web.ActivityPub.SideE
    liked object, a `Follow` activity will add the user to the follower
    collection, and so on.
    """
 +  alias Pleroma.Chat
    alias Pleroma.Notification
    alias Pleroma.Object
 +  alias Pleroma.Repo
    alias Pleroma.User
+   alias Pleroma.Web.ActivityPub.ActivityPub
 +  alias Pleroma.Web.ActivityPub.Pipeline
    alias Pleroma.Web.ActivityPub.Utils
  
    def handle(object, meta \\ [])
      {:ok, object, meta}
    end
  
 +  # Tasks this handles
 +  # - Actually create object
 +  # - Rollback if we couldn't create it
 +  # - Set up notifications
 +  def handle(%{data: %{"type" => "Create"}} = activity, meta) do
 +    with {:ok, _object, _meta} <- handle_object_creation(meta[:object_data], meta) do
 +      Notification.create_notifications(activity)
 +      {:ok, activity, meta}
 +    else
 +      e -> Repo.rollback(e)
 +    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
    def handle(object, meta) do
      {:ok, object, meta}
index fcc54c8a1652a1d9e55f2b5f609e560454f67aa2,744c46781d33dab03549624f1b3db52873eba764..c9eace8669674a8f3191e613480ea1167136f7c1
@@@ -12,150 -10,98 +12,242 @@@ defmodule Pleroma.Web.ActivityPub.Objec
  
    import Pleroma.Factory
  
 +  describe "attachments" do
 +    test "it turns mastodon attachments into our attachments" do
 +      attachment = %{
 +        "url" =>
 +          "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg",
 +        "type" => "Document",
 +        "name" => nil,
 +        "mediaType" => "image/jpeg"
 +      }
 +
 +      {:ok, attachment} =
 +        AttachmentValidator.cast_and_validate(attachment)
 +        |> Ecto.Changeset.apply_action(:insert)
 +
 +      assert [
 +               %{
 +                 href:
 +                   "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg",
 +                 type: "Link",
 +                 mediaType: "image/jpeg"
 +               }
 +             ] = attachment.url
 +    end
 +  end
 +
 +  describe "chat message create activities" do
 +    test "it is invalid if the object already exists" do
 +      user = insert(:user)
 +      recipient = insert(:user)
 +      {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "hey")
 +      object = Object.normalize(activity, false)
 +
 +      {:ok, create_data, _} = Builder.create(user, object.data, [recipient.ap_id])
 +
 +      {:error, cng} = ObjectValidator.validate(create_data, [])
 +
 +      assert {:object, {"The object to create already exists", []}} in cng.errors
 +    end
 +
 +    test "it is invalid if the object data has a different `to` or `actor` field" do
 +      user = insert(:user)
 +      recipient = insert(:user)
 +      {:ok, object_data, _} = Builder.chat_message(recipient, user.ap_id, "Hey")
 +
 +      {:ok, create_data, _} = Builder.create(user, object_data, [recipient.ap_id])
 +
 +      {:error, cng} = ObjectValidator.validate(create_data, [])
 +
 +      assert {:to, {"Recipients don't match with object recipients", []}} in cng.errors
 +      assert {:actor, {"Actor doesn't match with object actor", []}} in cng.errors
 +    end
 +  end
 +
 +  describe "chat messages" do
 +    setup do
 +      clear_config([:instance, :remote_limit])
 +      user = insert(:user)
 +      recipient = insert(:user, local: false)
 +
 +      {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey :firefox:")
 +
 +      %{user: user, recipient: recipient, valid_chat_message: valid_chat_message}
 +    end
 +
 +    test "validates for a basic object we build", %{valid_chat_message: valid_chat_message} do
 +      assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
 +
 +      assert Map.put(valid_chat_message, "attachment", nil) == object
 +    end
 +
 +    test "validates for a basic object with an attachment", %{
 +      valid_chat_message: valid_chat_message,
 +      user: user
 +    } do
 +      file = %Plug.Upload{
 +        content_type: "image/jpg",
 +        path: Path.absname("test/fixtures/image.jpg"),
 +        filename: "an_image.jpg"
 +      }
 +
 +      {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
 +
 +      valid_chat_message =
 +        valid_chat_message
 +        |> Map.put("attachment", attachment.data)
 +
 +      assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
 +
 +      assert object["attachment"]
 +    end
 +
 +    test "does not validate if the message is longer than the remote_limit", %{
 +      valid_chat_message: valid_chat_message
 +    } do
 +      Pleroma.Config.put([:instance, :remote_limit], 2)
 +      refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, []))
 +    end
 +
 +    test "does not validate if the recipient is blocking the actor", %{
 +      valid_chat_message: valid_chat_message,
 +      user: user,
 +      recipient: recipient
 +    } do
 +      Pleroma.User.block(recipient, user)
 +      refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, []))
 +    end
 +
 +    test "does not validate if the actor or the recipient is not in our system", %{
 +      valid_chat_message: valid_chat_message
 +    } do
 +      chat_message =
 +        valid_chat_message
 +        |> Map.put("actor", "https://raymoo.com/raymoo")
 +
 +      {:error, _} = ObjectValidator.validate(chat_message, [])
 +
 +      chat_message =
 +        valid_chat_message
 +        |> Map.put("to", ["https://raymoo.com/raymoo"])
 +
 +      {:error, _} = ObjectValidator.validate(chat_message, [])
 +    end
 +
 +    test "does not validate for a message with multiple recipients", %{
 +      valid_chat_message: valid_chat_message,
 +      user: user,
 +      recipient: recipient
 +    } do
 +      chat_message =
 +        valid_chat_message
 +        |> Map.put("to", [user.ap_id, recipient.ap_id])
 +
 +      assert {:error, _} = ObjectValidator.validate(chat_message, [])
 +    end
 +
 +    test "does not validate if it doesn't concern local users" do
 +      user = insert(:user, local: false)
 +      recipient = insert(:user, local: false)
 +
 +      {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey")
 +      assert {:error, _} = ObjectValidator.validate(valid_chat_message, [])
 +    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 19abac6a65d268116c98cdd107c34c4ea60b25e6,a9598d7b36ecbdc2c21507c177fd7aa2a4b52710..a631e5c6b4fef279f66ea0f151b4ab58e2b8fd7b
@@@ -3,9 -3,10 +3,11 @@@
  # 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.Chat
    alias Pleroma.Notification
    alias Pleroma.Object
    alias Pleroma.Repo
index 58ffda72f744130aff079957431900d2c3264ce1,62a2665b639682841c2ee78731c84723001dc234..ef7c479c05eded7961f0f3abce386cacfda0ab23
@@@ -22,55 -23,84 +24,133 @@@ defmodule Pleroma.Web.CommonAPITest d
    setup do: clear_config([:instance, :limit])
    setup do: clear_config([:instance, :max_pinned_statuses])
  
 +  describe "posting chat messages" do
 +    setup do: clear_config([:instance, :chat_limit])
 +
 +    test "it posts a chat message" do
 +      author = insert(:user)
 +      recipient = insert(:user)
 +
 +      {:ok, activity} =
 +        CommonAPI.post_chat_message(
 +          author,
 +          recipient,
 +          "a test message <script>alert('uuu')</script> :firefox:"
 +        )
 +
 +      assert activity.data["type"] == "Create"
 +      assert activity.local
 +      object = Object.normalize(activity)
 +
 +      assert object.data["type"] == "ChatMessage"
 +      assert object.data["to"] == [recipient.ap_id]
 +
 +      assert object.data["content"] ==
 +               "a test message &lt;script&gt;alert(&#39;uuu&#39;)&lt;/script&gt; :firefox:"
 +
 +      assert object.data["emoji"] == %{
 +               "firefox" => "http://localhost:4001/emoji/Firefox.gif"
 +             }
 +
 +      assert Chat.get(author.id, recipient.ap_id)
 +      assert Chat.get(recipient.id, author.ap_id)
 +    end
 +
 +    test "it reject messages over the local limit" do
 +      Pleroma.Config.put([:instance, :chat_limit], 2)
 +
 +      author = insert(:user)
 +      recipient = insert(:user)
 +
 +      {:error, message} =
 +        CommonAPI.post_chat_message(
 +          author,
 +          recipient,
 +          "123"
 +        )
 +
 +      assert message == :content_too_long
 +    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
      user = insert(:user)
      users_serial = insert_list(10, :user)