From 20baa2eaf04425cf0a2eebc84760be6c12ee7f51 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 6 May 2020 16:12:36 +0200 Subject: [PATCH] ChatMessages: Add attachments. --- lib/pleroma/web/activity_pub/builder.ex | 33 ++++++--- .../web/activity_pub/object_validator.ex | 11 ++- .../object_validators/attachment_validator.ex | 72 +++++++++++++++++++ .../chat_message_validator.ex | 6 +- .../object_validators/url_object_validator.ex | 20 ++++++ .../web/api_spec/operations/chat_operation.ex | 3 +- .../web/api_spec/schemas/chat_message.ex | 6 +- lib/pleroma/web/common_api/common_api.ex | 6 +- .../controllers/chat_controller.ex | 6 +- .../pleroma_api/views/chat_message_view.ex | 5 +- .../activity_pub/object_validator_test.exs | 50 ++++++++++++- .../types/object_id_test.exs | 4 ++ .../controllers/chat_controller_test.exs | 27 +++++++ .../views/chat_message_view_test.exs | 12 +++- 14 files changed, 237 insertions(+), 24 deletions(-) create mode 100644 lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex create mode 100644 lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 7f9c071b3..67e65c7b9 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -23,17 +23,28 @@ def create(actor, object, recipients) 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()} diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index 20c7cceb6..d6c14f7b8 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -63,11 +63,18 @@ def stringify_keys(%{__struct__: _} = object) 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 index 000000000..16ed49051 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex @@ -0,0 +1,72 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# 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 diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex index e87c1ac2e..99ffeba28 100644 --- a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -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 @@ def changeset(struct, data) 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 index 000000000..47e231150 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex @@ -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 diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex index 0fe0e07b2..8b9dc2e44 100644 --- a/lib/pleroma/web/api_spec/operations/chat_operation.ex +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -236,7 +236,8 @@ def chat_message_create 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: %{ diff --git a/lib/pleroma/web/api_spec/schemas/chat_message.ex b/lib/pleroma/web/api_spec/schemas/chat_message.ex index 7c93b0c83..89e062ddd 100644 --- a/lib/pleroma/web/api_spec/schemas/chat_message.ex +++ b/lib/pleroma/web/api_spec/schemas/chat_message.ex @@ -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 diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index e428cc17d..38b5c6f7c 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -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])}, diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex index bedae73bd..450d85332 100644 --- a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -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) diff --git a/lib/pleroma/web/pleroma_api/views/chat_message_view.ex b/lib/pleroma/web/pleroma_api/views/chat_message_view.ex index a821479ab..b088a8734 100644 --- a/lib/pleroma/web/pleroma_api/views/chat_message_view.ex +++ b/lib/pleroma/web/pleroma_api/views/chat_message_view.ex @@ -23,7 +23,10 @@ def render( 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 diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index 60db7187f..951ed7800 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -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 @@ test "it is invalid if the object data has a different `to` or `actor` field" 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", %{ diff --git a/test/web/activity_pub/object_validators/types/object_id_test.exs b/test/web/activity_pub/object_validators/types/object_id_test.exs index 834213182..c8911948e 100644 --- a/test/web/activity_pub/object_validators/types/object_id_test.exs +++ b/test/web/activity_pub/object_validators/types/object_id_test.exs @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ObjectValidators.Types.ObjectIDTest do alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID use Pleroma.DataCase diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs index cdb2683c8..72a9a91ff 100644 --- a/test/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/web/pleroma_api/controllers/chat_controller_test.exs @@ -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 @@ test "it posts a message to the chat", %{conn: conn, user: user} 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 diff --git a/test/web/pleroma_api/views/chat_message_view_test.exs b/test/web/pleroma_api/views/chat_message_view_test.exs index 5c4c8b0d5..a13a41daa 100644 --- a/test/web/pleroma_api/views/chat_message_view_test.exs +++ b/test/web/pleroma_api/views/chat_message_view_test.exs @@ -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 @@ test "it displays a chat message" 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 @@ test "it displays a chat message" 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