ChatMessages: Add attachments.
authorlain <lain@soykaf.club>
Wed, 6 May 2020 14:12:36 +0000 (16:12 +0200)
committerlain <lain@soykaf.club>
Wed, 6 May 2020 14:12:36 +0000 (16:12 +0200)
14 files changed:
lib/pleroma/web/activity_pub/builder.ex
lib/pleroma/web/activity_pub/object_validator.ex
lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex
lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/operations/chat_operation.ex
lib/pleroma/web/api_spec/schemas/chat_message.ex
lib/pleroma/web/common_api/common_api.ex
lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
lib/pleroma/web/pleroma_api/views/chat_message_view.ex
test/web/activity_pub/object_validator_test.exs
test/web/activity_pub/object_validators/types/object_id_test.exs
test/web/pleroma_api/controllers/chat_controller_test.exs
test/web/pleroma_api/views/chat_message_view_test.exs

index 7f9c071b33727abb852038a918f13d809ff8498a..67e65c7b990049f0b1425ebedabc49fe14527b11 100644 (file)
@@ -23,17 +23,28 @@ defmodule Pleroma.Web.ActivityPub.Builder do
      }, []}
   end
 
-  def chat_message(actor, recipient, content) do
-    {:ok,
-     %{
-       "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)
-     }, []}
+  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()}
index 20c7cceb64982f765f3a0b0a8f0d610edd334fe8..d6c14f7b8248873ed9a3419fd41b381f2823e820 100644 (file)
@@ -63,11 +63,18 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
     |> stringify_keys
   end
 
-  def stringify_keys(object) do
+  def stringify_keys(object) when is_map(object) do
     object
-    |> Map.new(fn {key, val} -> {to_string(key), val} end)
+    |> Map.new(fn {key, val} -> {to_string(key), stringify_keys(val)} end)
   end
 
+  def stringify_keys(object) when is_list(object) do
+    object
+    |> Enum.map(&stringify_keys/1)
+  end
+
+  def stringify_keys(object), do: object
+
   def fetch_actor(object) do
     with {:ok, actor} <- Types.ObjectID.cast(object["actor"]) do
       User.get_or_fetch_by_ap_id(actor)
diff --git a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
new file mode 100644 (file)
index 0000000..16ed490
--- /dev/null
@@ -0,0 +1,72 @@
+# 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.AttachmentValidator do
+  use Ecto.Schema
+
+  alias Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator
+
+  import Ecto.Changeset
+
+  @primary_key false
+  embedded_schema do
+    field(:type, :string)
+    field(:mediaType, :string)
+    field(:name, :string)
+
+    embeds_many(:url, UrlObjectValidator)
+  end
+
+  def cast_and_validate(data) do
+    data
+    |> cast_data()
+    |> validate_data()
+  end
+
+  def cast_data(data) do
+    %__MODULE__{}
+    |> changeset(data)
+  end
+
+  def changeset(struct, data) do
+    data =
+      data
+      |> fix_media_type()
+      |> fix_url()
+
+    struct
+    |> cast(data, [:type, :mediaType, :name])
+    |> cast_embed(:url, required: true)
+  end
+
+  def fix_media_type(data) do
+    data
+    |> Map.put_new("mediaType", data["mimeType"])
+  end
+
+  def fix_url(data) do
+    case data["url"] do
+      url when is_binary(url) ->
+        data
+        |> Map.put(
+          "url",
+          [
+            %{
+              "href" => url,
+              "type" => "Link",
+              "mediaType" => data["mediaType"]
+            }
+          ]
+        )
+
+      _ ->
+        data
+    end
+  end
+
+  def validate_data(cng) do
+    cng
+    |> validate_required([:mediaType, :url, :type])
+  end
+end
index e87c1ac2e217bdb48cac07f3a6c2775a4a463531..99ffeba28b88d12b635562faa067c9b2d110aceb 100644 (file)
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do
 
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+  alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
 
   import Ecto.Changeset
   import Pleroma.Web.ActivityPub.Transmogrifier, only: [fix_emoji: 1]
@@ -22,6 +23,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do
     field(:actor, Types.ObjectID)
     field(:published, Types.DateTime)
     field(:emoji, :map, default: %{})
+
+    embeds_one(:attachment, AttachmentValidator)
   end
 
   def cast_and_apply(data) do
@@ -51,7 +54,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do
     data = fix(data)
 
     struct
-    |> cast(data, __schema__(:fields))
+    |> cast(data, List.delete(__schema__(:fields), :attachment))
+    |> cast_embed(:attachment)
   end
 
   def validate_data(data_cng) do
diff --git a/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex b/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex
new file mode 100644 (file)
index 0000000..47e2311
--- /dev/null
@@ -0,0 +1,20 @@
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator do
+  use Ecto.Schema
+
+  alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+
+  import Ecto.Changeset
+  @primary_key false
+
+  embedded_schema do
+    field(:type, :string)
+    field(:href, Types.Uri)
+    field(:mediaType, :string)
+  end
+
+  def changeset(struct, data) do
+    struct
+    |> cast(data, __schema__(:fields))
+    |> validate_required([:type, :href, :mediaType])
+  end
+end
index 0fe0e07b202116d3359389f40ab6cf93a07ccef8..8b9dc2e44792a0f960f8eb9c327b0642e7ee5dc8 100644 (file)
@@ -236,7 +236,8 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
       description: "POST body for creating an chat message",
       type: :object,
       properties: %{
-        content: %Schema{type: :string, description: "The content of your message"}
+        content: %Schema{type: :string, description: "The content of your message"},
+        media_id: %Schema{type: :string, description: "The id of an upload"}
       },
       required: [:content],
       example: %{
index 7c93b0c839b45418e839c7cfdecbb04eb8869aa0..89e062dddd09f480b9a4f0f543b80e551efcac0e 100644 (file)
@@ -17,7 +17,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
       chat_id: %Schema{type: :string},
       content: %Schema{type: :string},
       created_at: %Schema{type: :string, format: :"date-time"},
-      emojis: %Schema{type: :array}
+      emojis: %Schema{type: :array},
+      attachment: %Schema{type: :object, nullable: true}
     },
     example: %{
       "account_id" => "someflakeid",
@@ -32,7 +33,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
           "url" => "https://dontbulling.me/emoji/Firefox.gif"
         }
       ],
-      "id" => "14"
+      "id" => "14",
+      "attachment" => nil
     }
   })
 end
index e428cc17dd323a753d4be52cc082b9f25874528b..38b5c6f7c8e65b93bf960f3e92f39079700c5068 100644 (file)
@@ -25,14 +25,16 @@ defmodule Pleroma.Web.CommonAPI do
   require Pleroma.Constants
   require Logger
 
-  def post_chat_message(%User{} = user, %User{} = recipient, content) do
+  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")
+              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])},
index bedae73bd8439b8c1c123d15a02ea66856409c36..450d85332c7dc45d3340e5ed4bd1785e413281ac 100644 (file)
@@ -36,14 +36,16 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
   defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation
 
   def post_chat_message(
-        %{body_params: %{content: content}, assigns: %{user: %{id: user_id} = user}} = conn,
+        %{body_params: %{content: content} = params, assigns: %{user: %{id: user_id} = user}} =
+          conn,
         %{
           id: id
         }
       ) do
     with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id),
          %User{} = recipient <- User.get_cached_by_ap_id(chat.recipient),
-         {:ok, activity} <- CommonAPI.post_chat_message(user, recipient, content),
+         {:ok, activity} <-
+           CommonAPI.post_chat_message(user, recipient, content, media_id: params[:media_id]),
          message <- Object.normalize(activity) do
       conn
       |> put_view(ChatMessageView)
index a821479ab7a3fac98a0bbc73707b01fb3c4c8a46..b088a87346fd0cee75b13b30daeb29eb54d43ffd 100644 (file)
@@ -23,7 +23,10 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageView do
       chat_id: chat_id |> to_string(),
       account_id: User.get_cached_by_ap_id(chat_message["actor"]).id,
       created_at: Utils.to_masto_date(chat_message["published"]),
-      emojis: StatusView.build_emojis(chat_message["emoji"])
+      emojis: StatusView.build_emojis(chat_message["emoji"]),
+      attachment:
+        chat_message["attachment"] &&
+          StatusView.render("attachment.json", attachment: chat_message["attachment"])
     }
   end
 
index 60db7187fc55b960f68bbf5a3c466d4d9cb06fc2..951ed780081704ce7c70a33be10d806ed78d29ab 100644 (file)
@@ -2,14 +2,41 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
   use Pleroma.DataCase
 
   alias Pleroma.Object
+  alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.Builder
   alias Pleroma.Web.ActivityPub.ObjectValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
+  alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.CommonAPI
 
   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)
@@ -52,7 +79,28 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
     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 object == 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", %{
index 8342131828b8c76441ba05c44ea0801d64e22b99..c8911948e7379e8f25e3a4c33df0f3e79800aa36 100644 (file)
@@ -1,3 +1,7 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
 defmodule Pleroma.Web.ObjectValidators.Types.ObjectIDTest do
   alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID
   use Pleroma.DataCase
index cdb2683c8b4f74b754271213913268faeab17a16..72a9a91ff3d50a2f5c1ff93337bc71f5c3cde655 100644 (file)
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do
 
   alias Pleroma.Chat
   alias Pleroma.Web.CommonAPI
+  alias Pleroma.Web.ActivityPub.ActivityPub
 
   import Pleroma.Factory
 
@@ -49,6 +50,32 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do
       assert result["content"] == "Hallo!!"
       assert result["chat_id"] == chat.id |> to_string()
     end
+
+    test "it works with an attachment", %{conn: conn, user: user} do
+      file = %Plug.Upload{
+        content_type: "image/jpg",
+        path: Path.absname("test/fixtures/image.jpg"),
+        filename: "an_image.jpg"
+      }
+
+      {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
+
+      other_user = insert(:user)
+
+      {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
+
+      result =
+        conn
+        |> put_req_header("content-type", "application/json")
+        |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{
+          "content" => "Hallo!!",
+          "media_id" => to_string(upload.id)
+        })
+        |> json_response_and_validate_schema(200)
+
+      assert result["content"] == "Hallo!!"
+      assert result["chat_id"] == chat.id |> to_string()
+    end
   end
 
   describe "GET /api/v1/pleroma/chats/:id/messages" do
index 5c4c8b0d59839f58406f7f3e45a0e5011aea0761..a13a41daa361d77d429ec94fed583b49f8e93155 100644 (file)
@@ -9,12 +9,21 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageViewTest do
   alias Pleroma.Object
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.PleromaAPI.ChatMessageView
+  alias Pleroma.Web.ActivityPub.ActivityPub
 
   import Pleroma.Factory
 
   test "it displays a chat message" do
     user = insert(:user)
     recipient = insert(:user)
+
+    file = %Plug.Upload{
+      content_type: "image/jpg",
+      path: Path.absname("test/fixtures/image.jpg"),
+      filename: "an_image.jpg"
+    }
+
+    {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
     {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "kippis :firefox:")
 
     chat = Chat.get(user.id, recipient.ap_id)
@@ -30,7 +39,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageViewTest do
     assert chat_message[:created_at]
     assert match?([%{shortcode: "firefox"}], chat_message[:emojis])
 
-    {:ok, activity} = CommonAPI.post_chat_message(recipient, user, "gkgkgk")
+    {:ok, activity} = CommonAPI.post_chat_message(recipient, user, "gkgkgk", media_id: upload.id)
 
     object = Object.normalize(activity)
 
@@ -40,5 +49,6 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageViewTest do
     assert chat_message_two[:content] == "gkgkgk"
     assert chat_message_two[:account_id] == recipient.id
     assert chat_message_two[:chat_id] == chat_message[:chat_id]
+    assert chat_message_two[:attachment]
   end
 end