Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into remake-remodel-dms
authorlain <lain@soykaf.club>
Fri, 8 May 2020 11:13:37 +0000 (13:13 +0200)
committerlain <lain@soykaf.club>
Fri, 8 May 2020 11:13:37 +0000 (13:13 +0200)
13 files changed:
1  2 
lib/pleroma/notification.ex
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
lib/pleroma/web/mastodon_api/views/account_view.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
test/web/mastodon_api/views/account_view_test.exs
test/web/mastodon_api/views/notification_view_test.exs

Simple merge
index 6e3a375e7a33f153cc771b922c122985ede9b3f0,922a444a9b7704080afcaeca04bdc1c8be5e981d..2a21a38119628e263582de341943dc6c8e07997d
@@@ -38,44 -62,19 +63,55 @@@ defmodule Pleroma.Web.ActivityPub.Build
       }, []}
    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
+     with {:ok, data, meta} <- object_action(actor, object) do
+       data =
+         data
+         |> Map.put("type", "Like")
+       {:ok, data, meta}
+     end
+   end
+   @spec object_action(User.t(), Object.t()) :: {:ok, map(), keyword()}
+   defp object_action(actor, object) do
      object_actor = User.get_cached_by_ap_id(object.data["actor"])
  
      # Address the actor of the object, and our actor's follower collection if the post is public.
index cc5ca1d9ec63600ebe7053f77f655eb720c6c84a,549e5e761e7b5f83983861f3291e019b8fc9a4b9..7f1e0171cfc828f1e6f15c8aff3a644cef853744
@@@ -11,11 -11,11 +11,13 @@@ 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.EmojiReactValidator
    alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
    alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+   alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
  
    @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
    def validate(object, meta)
      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 validate(%{"type" => "Create", "object" => object} = create_activity, meta) do
 +    with {:ok, object_data} <- cast_and_apply(object),
 +         meta = Keyword.put(meta, :object_data, object_data |> stringify_keys),
 +         {:ok, create_activity} <-
 +           create_activity
 +           |> CreateChatMessageValidator.cast_and_validate(meta)
 +           |> Ecto.Changeset.apply_action(:insert) do
 +      create_activity = stringify_keys(create_activity)
 +      {:ok, create_activity, meta}
 +    end
 +  end
 +
 +  def cast_and_apply(%{"type" => "ChatMessage"} = object) do
 +    ChatMessageValidator.cast_and_apply(object)
 +  end
 +
 +  def cast_and_apply(o), do: {:error, {:validator_not_set, o}}
 +
    def stringify_keys(%{__struct__: _} = object) do
      object
      |> Map.from_struct()
index 8bdc433ff745a5ebe670fb8d87370011d6fc09da,bfc2ab845d7aac444c7ce8b8a563e768a6f3cfd9..28b5194322cb775170b352a7771a8cbaa2399126
@@@ -5,7 -5,7 +5,8 @@@ 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.Activity
    alias Pleroma.Notification
    alias Pleroma.Object
    alias Pleroma.Repo
      {: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
 +
+   def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, meta) do
+     with undone_object <- Activity.get_by_ap_id(undone_object),
+          :ok <- handle_undoing(undone_object) do
+       {:ok, object, meta}
+     end
+   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
      {:ok, object, meta}
    end
  
 +  def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do
 +    with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
 +      actor = User.get_cached_by_ap_id(object.data["actor"])
 +      recipient = User.get_cached_by_ap_id(hd(object.data["to"]))
 +
 +      [[actor, recipient], [recipient, actor]]
 +      |> Enum.each(fn [user, other_user] ->
 +        if user.local do
 +          Chat.bump_or_create(user.id, other_user.ap_id)
 +        end
 +      end)
 +
 +      {:ok, object, meta}
 +    end
 +  end
 +
 +  # Nothing to do
 +  def handle_object_creation(object) do
 +    {:ok, object}
 +  end
++
+   def handle_undoing(%{data: %{"type" => "Like"}} = object) do
+     with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
+          {:ok, _} <- Utils.remove_like_from_object(object, liked_object),
+          {:ok, _} <- Repo.delete(object) do
+       :ok
+     end
+   end
+   def handle_undoing(%{data: %{"type" => "EmojiReact"}} = object) do
+     with %Object{} = reacted_object <- Object.get_by_ap_id(object.data["object"]),
+          {:ok, _} <- Utils.remove_emoji_reaction_from_object(object, reacted_object),
+          {:ok, _} <- Repo.delete(object) do
+       :ok
+     end
+   end
+   def handle_undoing(%{data: %{"type" => "Announce"}} = object) do
+     with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
+          {:ok, _} <- Utils.remove_announce_from_object(object, liked_object),
+          {:ok, _} <- Repo.delete(object) do
+       :ok
+     end
+   end
+   def handle_undoing(
+         %{data: %{"type" => "Block", "actor" => blocker, "object" => blocked}} = object
+       ) do
+     with %User{} = blocker <- User.get_cached_by_ap_id(blocker),
+          %User{} = blocked <- User.get_cached_by_ap_id(blocked),
+          {:ok, _} <- User.unblock(blocker, blocked),
+          {:ok, _} <- Repo.delete(object) do
+       :ok
+     end
+   end
+   def handle_undoing(object), do: {:error, ["don't know how to handle", object]}
  end
index 55e0df28387f4820bb132765878e7f54ad0ffd44,be7b57f13bb20ad9abe2f2c29b2b131a734cf4cc..29f668cadbb0d6098738e4b56dc8c00500162d36
@@@ -656,17 -656,7 +656,17 @@@ defmodule Pleroma.Web.ActivityPub.Trans
      |> handle_incoming(options)
    end
  
-   def handle_incoming(%{"type" => "Like"} = data, _options) do
 +  def handle_incoming(
 +        %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
 +        _options
 +      ) do
 +    with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
 +         {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
 +      {:ok, activity}
 +    end
 +  end
 +
+   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
index d7d934683a0d0d6ce72a2891d5c4106b363d4f6a,c538a634f2e66c7419619e6d7bdd307a9063a763..ad2096c16135e99b1054b795eaf6fd1e68b47a6f
@@@ -25,36 -24,14 +25,44 @@@ defmodule Pleroma.Web.CommonAPI d
    require Pleroma.Constants
    require Logger
  
 +  def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
 +    with :ok <- validate_chat_content_length(content),
 +         maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
 +         {_, {:ok, chat_message_data, _meta}} <-
 +           {:build_object,
 +            Builder.chat_message(
 +              user,
 +              recipient.ap_id,
 +              content |> Formatter.html_escape("text/plain"),
 +              attachment: maybe_attachment
 +            )},
 +         {_, {:ok, create_activity_data, _meta}} <-
 +           {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
 +         {_, {:ok, %Activity{} = activity, _meta}} <-
 +           {:common_pipeline,
 +            Pipeline.common_pipeline(create_activity_data,
 +              local: true
 +            )} do
 +      {:ok, activity}
 +    end
 +  end
 +
 +  defp validate_chat_content_length(content) do
 +    if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
 +      :ok
 +    else
 +      {:error, :content_too_long}
 +    end
 +  end
 +
+   def unblock(blocker, blocked) do
+     with %Activity{} = block <- Utils.fetch_latest_block(blocker, blocked),
+          {:ok, unblock_data, _} <- Builder.undo(blocker, block),
+          {:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
+       {:ok, unblock}
+     end
+   end
    def follow(follower, followed) do
      timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
  
index c9eace8669674a8f3191e613480ea1167136f7c1,f382adf3e7e38ecdbae370507d4bb9da46ffb9fc..6164d176dfe457002af2169c582e32600f8c3dd3
@@@ -12,150 -10,86 +12,230 @@@ 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 "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 "Undos" do
+     setup do
+       user = insert(:user)
+       {:ok, post_activity} = CommonAPI.post(user, %{"status" => "uguu"})
+       {:ok, like} = CommonAPI.favorite(user, post_activity.id)
+       {:ok, valid_like_undo, []} = Builder.undo(user, like)
+       %{user: user, like: like, valid_like_undo: valid_like_undo}
+     end
+     test "it validates a basic like undo", %{valid_like_undo: valid_like_undo} do
+       assert {:ok, _, _} = ObjectValidator.validate(valid_like_undo, [])
+     end
+     test "it does not validate if the actor of the undo is not the actor of the object", %{
+       valid_like_undo: valid_like_undo
+     } do
+       other_user = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo")
+       bad_actor =
+         valid_like_undo
+         |> Map.put("actor", other_user.ap_id)
+       {:error, cng} = ObjectValidator.validate(bad_actor, [])
+       assert {:actor, {"not the same as object actor", []}} in cng.errors
+     end
+     test "it does not validate if the object is missing", %{valid_like_undo: valid_like_undo} do
+       missing_object =
+         valid_like_undo
+         |> Map.put("object", "https://gensokyo.2hu/objects/1")
+       {:error, cng} = ObjectValidator.validate(missing_object, [])
+       assert {:object, {"can't find object", []}} in cng.errors
+       assert length(cng.errors) == 1
+     end
+   end
    describe "deletes" do
      setup do
        user = insert(:user)