ChatMessages: Add attachments.

This commit is contained in:
lain 2020-05-06 16:12:36 +02:00
parent 205313e541
commit 20baa2eaf0
14 changed files with 237 additions and 24 deletions

View File

@ -23,9 +23,8 @@ def create(actor, object, recipients) do
}, []} }, []}
end end
def chat_message(actor, recipient, content) do def chat_message(actor, recipient, content, opts \\ []) do
{:ok, basic = %{
%{
"id" => Utils.generate_object_id(), "id" => Utils.generate_object_id(),
"actor" => actor.ap_id, "actor" => actor.ap_id,
"type" => "ChatMessage", "type" => "ChatMessage",
@ -33,7 +32,19 @@ def chat_message(actor, recipient, content) do
"content" => content, "content" => content,
"published" => DateTime.utc_now() |> DateTime.to_iso8601(), "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"emoji" => Emoji.Formatter.get_emoji_map(content) "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 end
@spec like(User.t(), Object.t()) :: {:ok, map(), keyword()} @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}

View File

@ -63,11 +63,18 @@ def stringify_keys(%{__struct__: _} = object) do
|> stringify_keys |> stringify_keys
end end
def stringify_keys(object) do def stringify_keys(object) when is_map(object) do
object object
|> Map.new(fn {key, val} -> {to_string(key), val} end) |> Map.new(fn {key, val} -> {to_string(key), stringify_keys(val)} end)
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 def fetch_actor(object) do
with {:ok, actor} <- Types.ObjectID.cast(object["actor"]) do with {:ok, actor} <- Types.ObjectID.cast(object["actor"]) do
User.get_or_fetch_by_ap_id(actor) User.get_or_fetch_by_ap_id(actor)

View File

@ -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

View File

@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.Types alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
import Ecto.Changeset import Ecto.Changeset
import Pleroma.Web.ActivityPub.Transmogrifier, only: [fix_emoji: 1] 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(:actor, Types.ObjectID)
field(:published, Types.DateTime) field(:published, Types.DateTime)
field(:emoji, :map, default: %{}) field(:emoji, :map, default: %{})
embeds_one(:attachment, AttachmentValidator)
end end
def cast_and_apply(data) do def cast_and_apply(data) do
@ -51,7 +54,8 @@ def changeset(struct, data) do
data = fix(data) data = fix(data)
struct struct
|> cast(data, __schema__(:fields)) |> cast(data, List.delete(__schema__(:fields), :attachment))
|> cast_embed(:attachment)
end end
def validate_data(data_cng) do def validate_data(data_cng) do

View File

@ -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

View File

@ -236,7 +236,8 @@ def chat_message_create do
description: "POST body for creating an chat message", description: "POST body for creating an chat message",
type: :object, type: :object,
properties: %{ 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], required: [:content],
example: %{ example: %{

View File

@ -17,7 +17,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
chat_id: %Schema{type: :string}, chat_id: %Schema{type: :string},
content: %Schema{type: :string}, content: %Schema{type: :string},
created_at: %Schema{type: :string, format: :"date-time"}, created_at: %Schema{type: :string, format: :"date-time"},
emojis: %Schema{type: :array} emojis: %Schema{type: :array},
attachment: %Schema{type: :object, nullable: true}
}, },
example: %{ example: %{
"account_id" => "someflakeid", "account_id" => "someflakeid",
@ -32,7 +33,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
"url" => "https://dontbulling.me/emoji/Firefox.gif" "url" => "https://dontbulling.me/emoji/Firefox.gif"
} }
], ],
"id" => "14" "id" => "14",
"attachment" => nil
} }
}) })
end end

View File

@ -25,14 +25,16 @@ defmodule Pleroma.Web.CommonAPI do
require Pleroma.Constants require Pleroma.Constants
require Logger 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), with :ok <- validate_chat_content_length(content),
maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
{_, {:ok, chat_message_data, _meta}} <- {_, {:ok, chat_message_data, _meta}} <-
{:build_object, {:build_object,
Builder.chat_message( Builder.chat_message(
user, user,
recipient.ap_id, recipient.ap_id,
content |> Formatter.html_escape("text/plain") content |> Formatter.html_escape("text/plain"),
attachment: maybe_attachment
)}, )},
{_, {:ok, create_activity_data, _meta}} <- {_, {:ok, create_activity_data, _meta}} <-
{:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])}, {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},

View File

@ -36,14 +36,16 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation
def post_chat_message( 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 id: id
} }
) do ) do
with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id), with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id),
%User{} = recipient <- User.get_cached_by_ap_id(chat.recipient), %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 message <- Object.normalize(activity) do
conn conn
|> put_view(ChatMessageView) |> put_view(ChatMessageView)

View File

@ -23,7 +23,10 @@ def render(
chat_id: chat_id |> to_string(), chat_id: chat_id |> to_string(),
account_id: User.get_cached_by_ap_id(chat_message["actor"]).id, account_id: User.get_cached_by_ap_id(chat_message["actor"]).id,
created_at: Utils.to_masto_date(chat_message["published"]), 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 end

View File

@ -2,14 +2,41 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
use Pleroma.DataCase use Pleroma.DataCase
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
import Pleroma.Factory 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 describe "chat message create activities" do
test "it is invalid if the object already exists" do test "it is invalid if the object already exists" do
user = insert(:user) 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 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 {: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 end
test "does not validate if the message is longer than the remote_limit", %{ test "does not validate if the message is longer than the remote_limit", %{

View 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 defmodule Pleroma.Web.ObjectValidators.Types.ObjectIDTest do
alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID
use Pleroma.DataCase use Pleroma.DataCase

View File

@ -6,6 +6,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do
alias Pleroma.Chat alias Pleroma.Chat
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.ActivityPub.ActivityPub
import Pleroma.Factory 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["content"] == "Hallo!!"
assert result["chat_id"] == chat.id |> to_string() assert result["chat_id"] == chat.id |> to_string()
end 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 end
describe "GET /api/v1/pleroma/chats/:id/messages" do describe "GET /api/v1/pleroma/chats/:id/messages" do

View File

@ -9,12 +9,21 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageViewTest do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.PleromaAPI.ChatMessageView alias Pleroma.Web.PleromaAPI.ChatMessageView
alias Pleroma.Web.ActivityPub.ActivityPub
import Pleroma.Factory import Pleroma.Factory
test "it displays a chat message" do test "it displays a chat message" do
user = insert(:user) user = insert(:user)
recipient = 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:") {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "kippis :firefox:")
chat = Chat.get(user.id, recipient.ap_id) 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 chat_message[:created_at]
assert match?([%{shortcode: "firefox"}], chat_message[:emojis]) 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) 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[:content] == "gkgkgk"
assert chat_message_two[:account_id] == recipient.id assert chat_message_two[:account_id] == recipient.id
assert chat_message_two[:chat_id] == chat_message[:chat_id] assert chat_message_two[:chat_id] == chat_message[:chat_id]
assert chat_message_two[:attachment]
end end
end end