Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into remake-remodel-dms
authorlain <lain@soykaf.club>
Mon, 27 Apr 2020 10:07:08 +0000 (12:07 +0200)
committerlain <lain@soykaf.club>
Mon, 27 Apr 2020 10:07:08 +0000 (12:07 +0200)
41 files changed:
docs/ap_extensions.md [new file with mode: 0644]
lib/pleroma/chat.ex [new file with mode: 0644]
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/object_validators/chat_message_validator.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/object_validators/common_validations.ex
lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex [moved from lib/pleroma/web/activity_pub/object_validators/create_validator.ex with 100% similarity]
lib/pleroma/web/activity_pub/object_validators/types/recipients.ex [new file with mode: 0644]
lib/pleroma/web/activity_pub/side_effects.ex
lib/pleroma/web/activity_pub/transmogrifier.ex
lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/operations/chat_operation.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/chat_message_create_request.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/chat_message_response.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/chat_messages_response.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/chat_response.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/chats_response.ex [new file with mode: 0644]
lib/pleroma/web/common_api/common_api.ex
lib/pleroma/web/mastodon_api/views/account_view.ex
lib/pleroma/web/mastodon_api/views/notification_view.ex
lib/pleroma/web/pleroma_api/controllers/chat_controller.ex [new file with mode: 0644]
lib/pleroma/web/pleroma_api/views/chat_message_view.ex [new file with mode: 0644]
lib/pleroma/web/pleroma_api/views/chat_view.ex [new file with mode: 0644]
lib/pleroma/web/router.ex
priv/repo/migrations/20200309123730_create_chats.exs [new file with mode: 0644]
priv/static/schemas/litepub-0.1.jsonld
test/chat_test.exs [new file with mode: 0644]
test/fixtures/create-chat-message.json [new file with mode: 0644]
test/web/activity_pub/object_validator_test.exs
test/web/activity_pub/object_validators/types/recipients_test.exs [new file with mode: 0644]
test/web/activity_pub/side_effects_test.exs
test/web/activity_pub/transmogrifier/chat_message_test.exs [new file with mode: 0644]
test/web/common_api/common_api_test.exs
test/web/mastodon_api/controllers/timeline_controller_test.exs
test/web/mastodon_api/views/account_view_test.exs
test/web/mastodon_api/views/notification_view_test.exs
test/web/pleroma_api/controllers/chat_controller_test.exs [new file with mode: 0644]
test/web/pleroma_api/views/chat_message_view_test.exs [new file with mode: 0644]
test/web/pleroma_api/views/chat_view_test.exs [new file with mode: 0644]

diff --git a/docs/ap_extensions.md b/docs/ap_extensions.md
new file mode 100644 (file)
index 0000000..c4550a1
--- /dev/null
@@ -0,0 +1,35 @@
+# ChatMessages
+
+ChatMessages are the messages sent in 1-on-1 chats. They are similar to
+`Note`s, but the addresing is done by having a single AP actor in the `to`
+field. Addressing multiple actors is not allowed. These messages are always
+private, there is no public version of them. They are created with a `Create`
+activity.
+
+Example:
+
+```json
+{
+  "actor": "http://2hu.gensokyo/users/raymoo",
+  "id": "http://2hu.gensokyo/objects/1",
+  "object": {
+    "attributedTo": "http://2hu.gensokyo/users/raymoo",
+    "content": "You expected a cute girl? Too bad.",
+    "id": "http://2hu.gensokyo/objects/2",
+    "published": "2020-02-12T14:08:20Z",
+    "to": [
+      "http://2hu.gensokyo/users/marisa"
+    ],
+    "type": "ChatMessage"
+  },
+  "published": "2018-02-12T14:08:20Z",
+  "to": [
+    "http://2hu.gensokyo/users/marisa"
+  ],
+  "type": "Create"
+}
+```
+
+This setup does not prevent multi-user chats, but these will have to go through
+a `Group`, which will be the recipient of the messages and then `Announce` them
+to the users in the `Group`.
diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex
new file mode 100644 (file)
index 0000000..b854506
--- /dev/null
@@ -0,0 +1,63 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Chat do
+  use Ecto.Schema
+  import Ecto.Changeset
+
+  alias Pleroma.Repo
+  alias Pleroma.User
+
+  @moduledoc """
+  Chat keeps a reference to ChatMessage conversations between a user and an recipient. The recipient can be a user (for now) or a group (not implemented yet).
+
+  It is a helper only, to make it easy to display a list of chats with other people, ordered by last bump. The actual messages are retrieved by querying the recipients of the ChatMessages.
+  """
+
+  schema "chats" do
+    belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
+    field(:recipient, :string)
+    field(:unread, :integer, default: 0, read_after_writes: true)
+
+    timestamps()
+  end
+
+  def creation_cng(struct, params) do
+    struct
+    |> cast(params, [:user_id, :recipient, :unread])
+    |> validate_change(:recipient, fn
+      :recipient, recipient ->
+        case User.get_cached_by_ap_id(recipient) do
+          nil -> [recipient: "must a an existing user"]
+          _ -> []
+        end
+    end)
+    |> validate_required([:user_id, :recipient])
+    |> unique_constraint(:user_id, name: :chats_user_id_recipient_index)
+  end
+
+  def get(user_id, recipient) do
+    __MODULE__
+    |> Repo.get_by(user_id: user_id, recipient: recipient)
+  end
+
+  def get_or_create(user_id, recipient) do
+    %__MODULE__{}
+    |> creation_cng(%{user_id: user_id, recipient: recipient})
+    |> Repo.insert(
+      on_conflict: :nothing,
+      returning: true,
+      conflict_target: [:user_id, :recipient]
+    )
+  end
+
+  def bump_or_create(user_id, recipient) do
+    %__MODULE__{}
+    |> creation_cng(%{user_id: user_id, recipient: recipient, unread: 1})
+    |> Repo.insert(
+      on_conflict: [set: [updated_at: NaiveDateTime.utc_now()], inc: [unread: 1]],
+      conflict_target: [:user_id, :recipient]
+    )
+  end
+end
index 61a4960a0801501e66a70e7dbb3d69999c9f604a..3528526e0732204c786b26f7979d95ffa9474547 100644 (file)
@@ -1213,6 +1213,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     end
   end
 
+  defp exclude_chat_messages(query, %{"include_chat_messages" => true}), do: query
+
+  defp exclude_chat_messages(query, _) do
+    if has_named_binding?(query, :object) do
+      from([activity, object: o] in query,
+        where: fragment("not(?->>'type' = ?)", o.data, "ChatMessage")
+      )
+    else
+      query
+    end
+  end
+
   defp exclude_id(query, %{"exclude_id" => id}) when is_binary(id) do
     from(activity in query, where: activity.id != ^id)
   end
@@ -1318,6 +1330,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     |> restrict_instance(opts)
     |> Activity.restrict_deactivated_users()
     |> exclude_poll_votes(opts)
+    |> exclude_chat_messages(opts)
     |> exclude_visibility(opts)
   end
 
index 429a510b8118df4f136f614de281131fd5dd78b8..7576ed2782f689c2a05bcdc523f3bd77f50f6646 100644 (file)
@@ -5,11 +5,37 @@ defmodule Pleroma.Web.ActivityPub.Builder do
   This module encodes our addressing policies and general shape of our objects.
   """
 
+  alias Pleroma.Emoji
   alias Pleroma.Object
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.ActivityPub.Visibility
 
+  def create(actor, object_id, recipients) do
+    {:ok,
+     %{
+       "id" => Utils.generate_activity_id(),
+       "actor" => actor.ap_id,
+       "to" => recipients,
+       "object" => object_id,
+       "type" => "Create",
+       "published" => DateTime.utc_now() |> DateTime.to_iso8601()
+     }, []}
+  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)
+     }, []}
+  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 dc4bce0595a12c409475206b2aed0e7222b7ce18..03db681ec89a034f74796828b43499a48141a3ae 100644 (file)
@@ -11,6 +11,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
 
   alias Pleroma.Object
   alias Pleroma.User
+  alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
+  alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
 
   @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
@@ -18,12 +20,40 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
 
   def validate(%{"type" => "Like"} = object, meta) do
     with {:ok, object} <-
-           object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do
+           object
+           |> LikeValidator.cast_and_validate()
+           |> Ecto.Changeset.apply_action(:insert) do
       object = stringify_keys(object |> Map.from_struct())
       {:ok, object, meta}
     end
   end
 
+  def validate(%{"type" => "ChatMessage"} = object, meta) do
+    with {:ok, object} <-
+           object
+           |> ChatMessageValidator.cast_and_validate()
+           |> Ecto.Changeset.apply_action(:insert) do
+      object = stringify_keys(object)
+      {:ok, object, meta}
+    end
+  end
+
+  def validate(%{"type" => "Create"} = object, meta) do
+    with {:ok, object} <-
+           object
+           |> CreateChatMessageValidator.cast_and_validate()
+           |> Ecto.Changeset.apply_action(:insert) do
+      object = stringify_keys(object)
+      {:ok, object, meta}
+    end
+  end
+
+  def stringify_keys(%{__struct__: _} = object) do
+    object
+    |> Map.from_struct()
+    |> stringify_keys
+  end
+
   def stringify_keys(object) do
     object
     |> Map.new(fn {key, val} -> {to_string(key), val} 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
new file mode 100644 (file)
index 0000000..f07045d
--- /dev/null
@@ -0,0 +1,98 @@
+# 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.ChatMessageValidator do
+  use Ecto.Schema
+
+  alias Pleroma.User
+  alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+
+  import Ecto.Changeset
+  import Pleroma.Web.ActivityPub.Transmogrifier, only: [fix_emoji: 1]
+
+  @primary_key false
+  @derive Jason.Encoder
+
+  embedded_schema do
+    field(:id, Types.ObjectID, primary_key: true)
+    field(:to, Types.Recipients, default: [])
+    field(:type, :string)
+    field(:content, :string)
+    field(:actor, Types.ObjectID)
+    field(:published, Types.DateTime)
+    field(:emoji, :map, default: %{})
+  end
+
+  def cast_and_apply(data) do
+    data
+    |> cast_data
+    |> apply_action(:insert)
+  end
+
+  def cast_and_validate(data) do
+    data
+    |> cast_data()
+    |> validate_data()
+  end
+
+  def cast_data(data) do
+    %__MODULE__{}
+    |> changeset(data)
+  end
+
+  def fix(data) do
+    data
+    |> fix_emoji()
+    |> Map.put_new("actor", data["attributedTo"])
+  end
+
+  def changeset(struct, data) do
+    data = fix(data)
+
+    struct
+    |> cast(data, __schema__(:fields))
+  end
+
+  def validate_data(data_cng) do
+    data_cng
+    |> validate_inclusion(:type, ["ChatMessage"])
+    |> validate_required([:id, :actor, :to, :type, :content, :published])
+    |> validate_length(:to, is: 1)
+    |> validate_length(:content, max: Pleroma.Config.get([:instance, :remote_limit]))
+    |> validate_local_concern()
+  end
+
+  @doc """
+  Validates the following
+  - If both users are in our system
+  - If at least one of the users in this ChatMessage is a local user
+  - If the recipient is not blocking the actor
+  """
+  def validate_local_concern(cng) do
+    with actor_ap <- get_field(cng, :actor),
+         {_, %User{} = actor} <- {:find_actor, User.get_cached_by_ap_id(actor_ap)},
+         {_, %User{} = recipient} <-
+           {:find_recipient, User.get_cached_by_ap_id(get_field(cng, :to) |> hd())},
+         {_, false} <- {:blocking_actor?, User.blocks?(recipient, actor)},
+         {_, true} <- {:local?, Enum.any?([actor, recipient], & &1.local)} do
+      cng
+    else
+      {:blocking_actor?, true} ->
+        cng
+        |> add_error(:actor, "actor is blocked by recipient")
+
+      {:local?, false} ->
+        cng
+        |> add_error(:actor, "actor and recipient are both remote")
+
+      {:find_actor, _} ->
+        cng
+        |> add_error(:actor, "can't find user")
+
+      {:find_recipient, _} ->
+        cng
+        |> add_error(:to, "can't find user")
+    end
+  end
+end
index b479c391837f1ccf20dfabd5134b562054e4dbb8..02f3a6438b5bfb4a19a6710d4505d7b6c28cbd79 100644 (file)
@@ -8,7 +8,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
   alias Pleroma.Object
   alias Pleroma.User
 
-  def validate_actor_presence(cng, field_name \\ :actor) do
+  def validate_actor_presence(cng) do
+    validate_user_presence(cng, :actor)
+  end
+
+  def validate_user_presence(cng, field_name) do
     cng
     |> validate_change(field_name, fn field_name, actor ->
       if User.get_cached_by_ap_id(actor) do
diff --git a/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex
new file mode 100644 (file)
index 0000000..ce52d56
--- /dev/null
@@ -0,0 +1,40 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+# NOTES
+# - Can probably be a generic create validator
+# - doesn't embed, will only get the object id
+# - object has to be validated first, maybe with some meta info from the surrounding create
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator do
+  use Ecto.Schema
+
+  alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+
+  import Ecto.Changeset
+
+  @primary_key false
+
+  embedded_schema do
+    field(:id, Types.ObjectID, primary_key: true)
+    field(:actor, Types.ObjectID)
+    field(:type, :string)
+    field(:to, Types.Recipients, default: [])
+    field(:object, Types.ObjectID)
+  end
+
+  def cast_and_apply(data) do
+    data
+    |> cast_data
+    |> apply_action(:insert)
+  end
+
+  def cast_data(data) do
+    cast(%__MODULE__{}, data, __schema__(:fields))
+  end
+
+  # No validation yet
+  def cast_and_validate(data) do
+    cast_data(data)
+  end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex
new file mode 100644 (file)
index 0000000..5a30408
--- /dev/null
@@ -0,0 +1,23 @@
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients do
+  use Ecto.Type
+
+  def type, do: {:array, :string}
+
+  def cast(object) when is_binary(object) do
+    cast([object])
+  end
+
+  def cast([_ | _] = data), do: {:ok, data}
+
+  def cast(_) do
+    :error
+  end
+
+  def dump(data) do
+    {:ok, data}
+  end
+
+  def load(data) do
+    {:ok, data}
+  end
+end
index 5981e754567e0c098340a1c77395b4402c87dc1f..ebe3071b08fc55ffe48dd5225ed950a517654489 100644 (file)
@@ -5,8 +5,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
   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.User
   alias Pleroma.Web.ActivityPub.Utils
 
   def handle(object, meta \\ [])
@@ -28,8 +30,37 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
     result
   end
 
+  def handle(%{data: %{"type" => "Create", "object" => object_id}} = activity, meta) do
+    object = Object.get_by_ap_id(object_id)
+
+    {:ok, _object} = handle_object_creation(object)
+
+    Notification.create_notifications(activity)
+
+    {:ok, activity, meta}
+  end
+
   # Nothing to do
   def handle(object, meta) do
     {:ok, object, meta}
   end
+
+  def handle_object_creation(%{data: %{"type" => "ChatMessage"}} = object) 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}
+  end
+
+  # Nothing to do
+  def handle_object_creation(object) do
+    {:ok, object}
+  end
 end
index 09119137b70ad41a0d84e7ccd2a2488e75962bc4..66975cf7db3b9a030521075c19a20815a0a4d769 100644 (file)
@@ -16,6 +16,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
   alias Pleroma.Web.ActivityPub.ObjectValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
   alias Pleroma.Web.ActivityPub.Pipeline
+  alias Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageHandling
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.ActivityPub.Visibility
   alias Pleroma.Web.Federator
@@ -643,6 +644,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
     |> handle_incoming(options)
   end
 
+  def handle_incoming(
+        %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
+        options
+      ),
+      do: ChatMessageHandling.handle_incoming(data, options)
+
   def handle_incoming(%{"type" => "Like"} = data, _options) do
     with {_, {:ok, cast_data_sym}} <-
            {:casting_data,
diff --git a/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex b/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex
new file mode 100644 (file)
index 0000000..cfe3b76
--- /dev/null
@@ -0,0 +1,37 @@
+# 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.Transmogrifier.ChatMessageHandling do
+  alias Pleroma.Object
+  alias Pleroma.Web.ActivityPub.ObjectValidator
+  alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
+  alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
+  alias Pleroma.Web.ActivityPub.Pipeline
+
+  def handle_incoming(
+        %{"type" => "Create", "object" => %{"type" => "ChatMessage"} = object_data} = data,
+        _options
+      ) do
+    with {_, {:ok, cast_data_sym}} <-
+           {:casting_data, data |> CreateChatMessageValidator.cast_and_apply()},
+         cast_data = ObjectValidator.stringify_keys(cast_data_sym),
+         {_, {:ok, object_cast_data_sym}} <-
+           {:casting_object_data, object_data |> ChatMessageValidator.cast_and_apply()},
+         object_cast_data = ObjectValidator.stringify_keys(object_cast_data_sym),
+         # For now, just strip HTML
+         stripped_content = Pleroma.HTML.strip_tags(object_cast_data["content"]),
+         object_cast_data = object_cast_data |> Map.put("content", stripped_content),
+         {_, true} <- {:to_fields_match, cast_data["to"] == object_cast_data["to"]},
+         {_, {:ok, validated_object, _meta}} <-
+           {:validate_object, ObjectValidator.validate(object_cast_data, %{})},
+         {_, {:ok, _created_object}} <- {:persist_object, Object.create(validated_object)},
+         {_, {:ok, activity, _meta}} <-
+           {:common_pipeline, Pipeline.common_pipeline(cast_data, local: false)} do
+      {:ok, activity}
+    else
+      e ->
+        {:error, e}
+    end
+  end
+end
diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex
new file mode 100644 (file)
index 0000000..dc99bd7
--- /dev/null
@@ -0,0 +1,115 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.ChatOperation do
+  alias OpenApiSpex.Operation
+  alias Pleroma.Web.ApiSpec.Helpers
+  alias Pleroma.Web.ApiSpec.Schemas.ChatMessageCreateRequest
+  alias Pleroma.Web.ApiSpec.Schemas.ChatMessageResponse
+  alias Pleroma.Web.ApiSpec.Schemas.ChatMessagesResponse
+  alias Pleroma.Web.ApiSpec.Schemas.ChatResponse
+  alias Pleroma.Web.ApiSpec.Schemas.ChatsResponse
+
+  @spec open_api_operation(atom) :: Operation.t()
+  def open_api_operation(action) do
+    operation = String.to_existing_atom("#{action}_operation")
+    apply(__MODULE__, operation, [])
+  end
+
+  def create_operation do
+    %Operation{
+      tags: ["chat"],
+      summary: "Create a chat",
+      operationId: "ChatController.create",
+      parameters: [
+        Operation.parameter(
+          :ap_id,
+          :path,
+          :string,
+          "The ActivityPub id of the recipient of this chat.",
+          required: true,
+          example: "https://lain.com/users/lain"
+        )
+      ],
+      responses: %{
+        200 =>
+          Operation.response("The created or existing chat", "application/json", ChatResponse)
+      },
+      security: [
+        %{
+          "oAuth" => ["write"]
+        }
+      ]
+    }
+  end
+
+  def index_operation do
+    %Operation{
+      tags: ["chat"],
+      summary: "Get a list of chats that you participated in",
+      operationId: "ChatController.index",
+      parameters: [
+        Operation.parameter(:limit, :query, :integer, "How many results to return", example: 20),
+        Operation.parameter(:min_id, :query, :string, "Return only chats after this id"),
+        Operation.parameter(:max_id, :query, :string, "Return only chats before this id")
+      ],
+      responses: %{
+        200 => Operation.response("The chats of the user", "application/json", ChatsResponse)
+      },
+      security: [
+        %{
+          "oAuth" => ["read"]
+        }
+      ]
+    }
+  end
+
+  def messages_operation do
+    %Operation{
+      tags: ["chat"],
+      summary: "Get the most recent messages of the chat",
+      operationId: "ChatController.messages",
+      parameters: [
+        Operation.parameter(:id, :path, :string, "The ID of the Chat"),
+        Operation.parameter(:limit, :query, :integer, "How many results to return", example: 20),
+        Operation.parameter(:min_id, :query, :string, "Return only messages after this id"),
+        Operation.parameter(:max_id, :query, :string, "Return only messages before this id")
+      ],
+      responses: %{
+        200 =>
+          Operation.response("The messages in the chat", "application/json", ChatMessagesResponse)
+      },
+      security: [
+        %{
+          "oAuth" => ["read"]
+        }
+      ]
+    }
+  end
+
+  def post_chat_message_operation do
+    %Operation{
+      tags: ["chat"],
+      summary: "Post a message to the chat",
+      operationId: "ChatController.post_chat_message",
+      parameters: [
+        Operation.parameter(:id, :path, :string, "The ID of the Chat")
+      ],
+      requestBody: Helpers.request_body("Parameters", ChatMessageCreateRequest, required: true),
+      responses: %{
+        200 =>
+          Operation.response(
+            "The newly created ChatMessage",
+            "application/json",
+            ChatMessageResponse
+          )
+      },
+      security: [
+        %{
+          "oAuth" => ["write"]
+        }
+      ]
+    }
+  end
+end
diff --git a/lib/pleroma/web/api_spec/schemas/chat_message_create_request.ex b/lib/pleroma/web/api_spec/schemas/chat_message_create_request.ex
new file mode 100644 (file)
index 0000000..4dafcda
--- /dev/null
@@ -0,0 +1,20 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessageCreateRequest do
+  alias OpenApiSpex.Schema
+  require OpenApiSpex
+
+  OpenApiSpex.schema(%{
+    title: "ChatMessageCreateRequest",
+    description: "POST body for creating an chat message",
+    type: :object,
+    properties: %{
+      content: %Schema{type: :string, description: "The content of your message"}
+    },
+    example: %{
+      "content" => "Hey wanna buy feet pics?"
+    }
+  })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/chat_message_response.ex b/lib/pleroma/web/api_spec/schemas/chat_message_response.ex
new file mode 100644 (file)
index 0000000..e94c003
--- /dev/null
@@ -0,0 +1,38 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessageResponse do
+  alias OpenApiSpex.Schema
+
+  require OpenApiSpex
+
+  OpenApiSpex.schema(%{
+    title: "ChatMessageResponse",
+    description: "Response schema for a ChatMessage",
+    type: :object,
+    properties: %{
+      id: %Schema{type: :string},
+      actor: %Schema{type: :string, description: "The ActivityPub id of the actor"},
+      chat_id: %Schema{type: :string},
+      content: %Schema{type: :string},
+      created_at: %Schema{type: :string, format: :datetime},
+      emojis: %Schema{type: :array}
+    },
+    example: %{
+      "actor" => "https://dontbulling.me/users/lain",
+      "chat_id" => "1",
+      "content" => "hey you again",
+      "created_at" => "2020-04-21T15:06:45.000Z",
+      "emojis" => [
+        %{
+          "static_url" => "https://dontbulling.me/emoji/Firefox.gif",
+          "visible_in_picker" => false,
+          "shortcode" => "firefox",
+          "url" => "https://dontbulling.me/emoji/Firefox.gif"
+        }
+      ],
+      "id" => "14"
+    }
+  })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/chat_messages_response.ex b/lib/pleroma/web/api_spec/schemas/chat_messages_response.ex
new file mode 100644 (file)
index 0000000..302bdec
--- /dev/null
@@ -0,0 +1,41 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessagesResponse do
+  alias Pleroma.Web.ApiSpec.Schemas.ChatMessageResponse
+
+  require OpenApiSpex
+
+  OpenApiSpex.schema(%{
+    title: "ChatMessagesResponse",
+    description: "Response schema for multiple ChatMessages",
+    type: :array,
+    items: ChatMessageResponse,
+    example: [
+      %{
+        "emojis" => [
+          %{
+            "static_url" => "https://dontbulling.me/emoji/Firefox.gif",
+            "visible_in_picker" => false,
+            "shortcode" => "firefox",
+            "url" => "https://dontbulling.me/emoji/Firefox.gif"
+          }
+        ],
+        "created_at" => "2020-04-21T15:11:46.000Z",
+        "content" => "Check this out :firefox:",
+        "id" => "13",
+        "chat_id" => "1",
+        "actor" => "https://dontbulling.me/users/lain"
+      },
+      %{
+        "actor" => "https://dontbulling.me/users/lain",
+        "content" => "Whats' up?",
+        "id" => "12",
+        "chat_id" => "1",
+        "emojis" => [],
+        "created_at" => "2020-04-21T15:06:45.000Z"
+      }
+    ]
+  })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/chat_response.ex b/lib/pleroma/web/api_spec/schemas/chat_response.ex
new file mode 100644 (file)
index 0000000..a80f4d1
--- /dev/null
@@ -0,0 +1,73 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.ChatResponse do
+  alias OpenApiSpex.Schema
+
+  require OpenApiSpex
+
+  OpenApiSpex.schema(%{
+    title: "ChatResponse",
+    description: "Response schema for a Chat",
+    type: :object,
+    properties: %{
+      id: %Schema{type: :string},
+      recipient: %Schema{type: :string},
+      # TODO: Make this reference the account structure.
+      recipient_account: %Schema{type: :object},
+      unread: %Schema{type: :integer}
+    },
+    example: %{
+      "recipient" => "https://dontbulling.me/users/lain",
+      "recipient_account" => %{
+        "pleroma" => %{
+          "is_admin" => false,
+          "confirmation_pending" => false,
+          "hide_followers_count" => false,
+          "is_moderator" => false,
+          "hide_favorites" => true,
+          "ap_id" => "https://dontbulling.me/users/lain",
+          "hide_follows_count" => false,
+          "hide_follows" => false,
+          "background_image" => nil,
+          "skip_thread_containment" => false,
+          "hide_followers" => false,
+          "relationship" => %{},
+          "tags" => []
+        },
+        "avatar" =>
+          "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg",
+        "following_count" => 0,
+        "header_static" => "https://originalpatchou.li/images/banner.png",
+        "source" => %{
+          "sensitive" => false,
+          "note" => "lain",
+          "pleroma" => %{
+            "discoverable" => false,
+            "actor_type" => "Person"
+          },
+          "fields" => []
+        },
+        "statuses_count" => 1,
+        "locked" => false,
+        "created_at" => "2020-04-16T13:40:15.000Z",
+        "display_name" => "lain",
+        "fields" => [],
+        "acct" => "lain@dontbulling.me",
+        "id" => "9u6Qw6TAZANpqokMkK",
+        "emojis" => [],
+        "avatar_static" =>
+          "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg",
+        "username" => "lain",
+        "followers_count" => 0,
+        "header" => "https://originalpatchou.li/images/banner.png",
+        "bot" => false,
+        "note" => "lain",
+        "url" => "https://dontbulling.me/users/lain"
+      },
+      "id" => "1",
+      "unread" => 2
+    }
+  })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/chats_response.ex b/lib/pleroma/web/api_spec/schemas/chats_response.ex
new file mode 100644 (file)
index 0000000..3349e06
--- /dev/null
@@ -0,0 +1,69 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.ChatsResponse do
+  alias Pleroma.Web.ApiSpec.Schemas.ChatResponse
+
+  require OpenApiSpex
+
+  OpenApiSpex.schema(%{
+    title: "ChatsResponse",
+    description: "Response schema for multiple Chats",
+    type: :array,
+    items: ChatResponse,
+    example: [
+      %{
+        "recipient" => "https://dontbulling.me/users/lain",
+        "recipient_account" => %{
+          "pleroma" => %{
+            "is_admin" => false,
+            "confirmation_pending" => false,
+            "hide_followers_count" => false,
+            "is_moderator" => false,
+            "hide_favorites" => true,
+            "ap_id" => "https://dontbulling.me/users/lain",
+            "hide_follows_count" => false,
+            "hide_follows" => false,
+            "background_image" => nil,
+            "skip_thread_containment" => false,
+            "hide_followers" => false,
+            "relationship" => %{},
+            "tags" => []
+          },
+          "avatar" =>
+            "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg",
+          "following_count" => 0,
+          "header_static" => "https://originalpatchou.li/images/banner.png",
+          "source" => %{
+            "sensitive" => false,
+            "note" => "lain",
+            "pleroma" => %{
+              "discoverable" => false,
+              "actor_type" => "Person"
+            },
+            "fields" => []
+          },
+          "statuses_count" => 1,
+          "locked" => false,
+          "created_at" => "2020-04-16T13:40:15.000Z",
+          "display_name" => "lain",
+          "fields" => [],
+          "acct" => "lain@dontbulling.me",
+          "id" => "9u6Qw6TAZANpqokMkK",
+          "emojis" => [],
+          "avatar_static" =>
+            "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg",
+          "username" => "lain",
+          "followers_count" => 0,
+          "header" => "https://originalpatchou.li/images/banner.png",
+          "bot" => false,
+          "note" => "lain",
+          "url" => "https://dontbulling.me/users/lain"
+        },
+        "id" => "1",
+        "unread" => 2
+      }
+    ]
+  })
+end
index d1efe0c36e0d1a19eb9377d700639c2434ef8bb0..5eb2216688deef01de303c24f9e3d5c7d14b3344 100644 (file)
@@ -7,7 +7,9 @@ defmodule Pleroma.Web.CommonAPI do
   alias Pleroma.ActivityExpiration
   alias Pleroma.Conversation.Participation
   alias Pleroma.FollowingRelationship
+  alias Pleroma.Formatter
   alias Pleroma.Object
+  alias Pleroma.Repo
   alias Pleroma.ThreadMute
   alias Pleroma.User
   alias Pleroma.UserRelationship
@@ -23,6 +25,39 @@ defmodule Pleroma.Web.CommonAPI do
   require Pleroma.Constants
   require Logger
 
+  def post_chat_message(%User{} = user, %User{} = recipient, content) do
+    transaction =
+      Repo.transaction(fn ->
+        with {_, true} <-
+               {:content_length,
+                String.length(content) <= Pleroma.Config.get([:instance, :chat_limit])},
+             {_, {:ok, chat_message_data, _meta}} <-
+               {:build_object,
+                Builder.chat_message(
+                  user,
+                  recipient.ap_id,
+                  content |> Formatter.html_escape("text/plain")
+                )},
+             {_, {:ok, chat_message_object}} <-
+               {:create_object, Object.create(chat_message_data)},
+             {_, {:ok, create_activity_data, _meta}} <-
+               {:build_create_activity,
+                Builder.create(user, chat_message_object.data["id"], [recipient.ap_id])},
+             {_, {:ok, %Activity{} = activity, _meta}} <-
+               {:common_pipeline, Pipeline.common_pipeline(create_activity_data, local: true)} do
+          {:ok, activity}
+        else
+          {:content_length, false} -> {:error, :content_too_long}
+          e -> e
+        end
+      end)
+
+    case transaction do
+      {:ok, value} -> value
+      error -> error
+    end
+  end
+
   def follow(follower, followed) do
     timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
 
index b4b61e74cfd393fe22cb9241a024c4cce661178f..c46517e49183ad5aca9ad58347365c84964c74b2 100644 (file)
@@ -232,6 +232,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
 
       # Pleroma extension
       pleroma: %{
+        ap_id: user.ap_id,
         confirmation_pending: user.confirmation_pending,
         tags: user.tags,
         hide_followers_count: user.hide_followers_count,
index 4da1ab67f58385e2548c1e923fb742f09afc5f7b..2a99518317007c6bd52cc1e5a6dedaf149a8b3b3 100644 (file)
@@ -7,12 +7,14 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
 
   alias Pleroma.Activity
   alias Pleroma.Notification
+  alias Pleroma.Object
   alias Pleroma.User
   alias Pleroma.UserRelationship
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.MastodonAPI.AccountView
   alias Pleroma.Web.MastodonAPI.NotificationView
   alias Pleroma.Web.MastodonAPI.StatusView
+  alias Pleroma.Web.PleromaAPI.ChatMessageView
 
   def render("index.json", %{notifications: notifications, for: reading_user} = opts) do
     activities = Enum.map(notifications, & &1.activity)
@@ -81,7 +83,21 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
       end
     end
 
-    mastodon_type = Activity.mastodon_notification_type(activity)
+    # This returns the notification type by activity, but both chats and statuses
+    # are in "Create" activities.
+    mastodon_type =
+      case Activity.mastodon_notification_type(activity) do
+        "mention" ->
+          object = Object.normalize(activity)
+
+          case object do
+            %{data: %{"type" => "ChatMessage"}} -> "pleroma:chat_mention"
+            _ -> "mention"
+          end
+
+        type ->
+          type
+      end
 
     render_opts = %{
       relationships: opts[:relationships],
@@ -122,6 +138,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
           |> put_status(parent_activity_fn.(), reading_user, render_opts)
           |> put_emoji(activity)
 
+        "pleroma:chat_mention" ->
+          put_chat_message(response, activity, reading_user, render_opts)
+
         type when type in ["follow", "follow_request"] ->
           response
 
@@ -137,6 +156,16 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
     Map.put(response, :emoji, activity.data["content"])
   end
 
+  defp put_chat_message(response, activity, reading_user, opts) do
+    object = Object.normalize(activity)
+    author = User.get_cached_by_ap_id(object.data["actor"])
+    chat = Pleroma.Chat.get(reading_user.id, author.ap_id)
+    render_opts = Map.merge(opts, %{object: object, for: reading_user, chat: chat})
+    chat_message_render = ChatMessageView.render("show.json", render_opts)
+
+    Map.put(response, :chat_message, chat_message_render)
+  end
+
   defp put_status(response, activity, reading_user, opts) do
     status_render_opts = Map.merge(opts, %{activity: activity, for: reading_user})
     status_render = StatusView.render("show.json", status_render_opts)
diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
new file mode 100644 (file)
index 0000000..771ad62
--- /dev/null
@@ -0,0 +1,110 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+defmodule Pleroma.Web.PleromaAPI.ChatController do
+  use Pleroma.Web, :controller
+
+  alias Pleroma.Chat
+  alias Pleroma.Object
+  alias Pleroma.Pagination
+  alias Pleroma.Plugs.OAuthScopesPlug
+  alias Pleroma.Repo
+  alias Pleroma.User
+  alias Pleroma.Web.CommonAPI
+  alias Pleroma.Web.PleromaAPI.ChatMessageView
+  alias Pleroma.Web.PleromaAPI.ChatView
+
+  import Pleroma.Web.ActivityPub.ObjectValidator, only: [stringify_keys: 1]
+
+  import Ecto.Query
+
+  # TODO
+  # - Error handling
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:statuses"]} when action in [:post_chat_message, :create]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["read:statuses"]} when action in [:messages, :index]
+  )
+
+  plug(OpenApiSpex.Plug.CastAndValidate)
+
+  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,
+        %{
+          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),
+         message <- Object.normalize(activity) do
+      conn
+      |> put_view(ChatMessageView)
+      |> render("show.json", for: user, object: message, chat: chat)
+    end
+  end
+
+  def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: id} = params) do
+    with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do
+      messages =
+        from(o in Object,
+          where: fragment("?->>'type' = ?", o.data, "ChatMessage"),
+          where:
+            fragment(
+              """
+              (?->>'actor' = ? and ?->'to' = ?) 
+              OR (?->>'actor' = ? and ?->'to' = ?) 
+              """,
+              o.data,
+              ^user.ap_id,
+              o.data,
+              ^[chat.recipient],
+              o.data,
+              ^chat.recipient,
+              o.data,
+              ^[user.ap_id]
+            )
+        )
+        |> Pagination.fetch_paginated(params |> stringify_keys())
+
+      conn
+      |> put_view(ChatMessageView)
+      |> render("index.json", for: user, objects: messages, chat: chat)
+    else
+      _ ->
+        conn
+        |> put_status(:not_found)
+        |> json(%{error: "not found"})
+    end
+  end
+
+  def index(%{assigns: %{user: %{id: user_id}}} = conn, params) do
+    chats =
+      from(c in Chat,
+        where: c.user_id == ^user_id,
+        order_by: [desc: c.updated_at]
+      )
+      |> Pagination.fetch_paginated(params |> stringify_keys)
+
+    conn
+    |> put_view(ChatView)
+    |> render("index.json", chats: chats)
+  end
+
+  def create(%{assigns: %{user: user}} = conn, params) do
+    recipient = params[:ap_id]
+
+    with {:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do
+      conn
+      |> put_view(ChatView)
+      |> render("show.json", chat: chat)
+    end
+  end
+end
diff --git a/lib/pleroma/web/pleroma_api/views/chat_message_view.ex b/lib/pleroma/web/pleroma_api/views/chat_message_view.ex
new file mode 100644 (file)
index 0000000..b40ab92
--- /dev/null
@@ -0,0 +1,32 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.ChatMessageView do
+  use Pleroma.Web, :view
+
+  alias Pleroma.Chat
+  alias Pleroma.Web.CommonAPI.Utils
+  alias Pleroma.Web.MastodonAPI.StatusView
+
+  def render(
+        "show.json",
+        %{
+          object: %{id: id, data: %{"type" => "ChatMessage"} = chat_message},
+          chat: %Chat{id: chat_id}
+        }
+      ) do
+    %{
+      id: id |> to_string(),
+      content: chat_message["content"],
+      chat_id: chat_id |> to_string(),
+      actor: chat_message["actor"],
+      created_at: Utils.to_masto_date(chat_message["published"]),
+      emojis: StatusView.build_emojis(chat_message["emoji"])
+    }
+  end
+
+  def render("index.json", opts) do
+    render_many(opts[:objects], __MODULE__, "show.json", Map.put(opts, :as, :object))
+  end
+end
diff --git a/lib/pleroma/web/pleroma_api/views/chat_view.ex b/lib/pleroma/web/pleroma_api/views/chat_view.ex
new file mode 100644 (file)
index 0000000..1e9ef43
--- /dev/null
@@ -0,0 +1,26 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.ChatView do
+  use Pleroma.Web, :view
+
+  alias Pleroma.Chat
+  alias Pleroma.User
+  alias Pleroma.Web.MastodonAPI.AccountView
+
+  def render("show.json", %{chat: %Chat{} = chat} = opts) do
+    recipient = User.get_cached_by_ap_id(chat.recipient)
+
+    %{
+      id: chat.id |> to_string(),
+      recipient: chat.recipient,
+      recipient_account: AccountView.render("show.json", Map.put(opts, :user, recipient)),
+      unread: chat.unread
+    }
+  end
+
+  def render("index.json", %{chats: chats}) do
+    render_many(chats, __MODULE__, "show.json")
+  end
+end
index 153802a432f42ff702b0684e87bce09ceab27d93..0c56318ee8c8528c238d89ebec674baef551f45d 100644 (file)
@@ -272,6 +272,15 @@ defmodule Pleroma.Web.Router do
   end
 
   scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
+    scope [] do
+      pipe_through(:authenticated_api)
+
+      post("/chats/by-ap-id/:ap_id", ChatController, :create)
+      get("/chats", ChatController, :index)
+      get("/chats/:id/messages", ChatController, :messages)
+      post("/chats/:id/messages", ChatController, :post_chat_message)
+    end
+
     scope [] do
       pipe_through(:authenticated_api)
 
diff --git a/priv/repo/migrations/20200309123730_create_chats.exs b/priv/repo/migrations/20200309123730_create_chats.exs
new file mode 100644 (file)
index 0000000..715d798
--- /dev/null
@@ -0,0 +1,16 @@
+defmodule Pleroma.Repo.Migrations.CreateChats do
+  use Ecto.Migration
+
+  def change do
+    create table(:chats) do
+      add(:user_id, references(:users, type: :uuid))
+      # Recipient is an ActivityPub id, to future-proof for group support.
+      add(:recipient, :string)
+      add(:unread, :integer, default: 0)
+      timestamps()
+    end
+
+    # There's only one chat between a user and a recipient.
+    create(index(:chats, [:user_id, :recipient], unique: true))
+  end
+end
index 278ad2f96f6a9eeb7bf86e005893bab6d2491f4b..7cc3fee40875f7d8be2c92896da7acf7e4d76fcd 100644 (file)
@@ -30,6 +30,7 @@
                 "@type": "@id"
             },
             "EmojiReact": "litepub:EmojiReact",
+            "ChatMessage": "litepub:ChatMessage",
             "alsoKnownAs": {
                 "@id": "as:alsoKnownAs",
                 "@type": "@id"
diff --git a/test/chat_test.exs b/test/chat_test.exs
new file mode 100644 (file)
index 0000000..952598c
--- /dev/null
@@ -0,0 +1,53 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ChatTest do
+  use Pleroma.DataCase, async: true
+
+  alias Pleroma.Chat
+
+  import Pleroma.Factory
+
+  describe "creation and getting" do
+    test "it only works if the recipient is a valid user (for now)" do
+      user = insert(:user)
+
+      assert {:error, _chat} = Chat.bump_or_create(user.id, "http://some/nonexisting/account")
+      assert {:error, _chat} = Chat.get_or_create(user.id, "http://some/nonexisting/account")
+    end
+
+    test "it creates a chat for a user and recipient" do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
+
+      assert chat.id
+    end
+
+    test "it returns a chat for a user and recipient if it already exists" do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
+      {:ok, chat_two} = Chat.bump_or_create(user.id, other_user.ap_id)
+
+      assert chat.id == chat_two.id
+    end
+
+    test "a returning chat will have an updated `update_at` field and an incremented unread count" do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
+      assert chat.unread == 1
+      :timer.sleep(1500)
+      {:ok, chat_two} = Chat.bump_or_create(user.id, other_user.ap_id)
+      assert chat_two.unread == 2
+
+      assert chat.id == chat_two.id
+      assert chat.updated_at != chat_two.updated_at
+    end
+  end
+end
diff --git a/test/fixtures/create-chat-message.json b/test/fixtures/create-chat-message.json
new file mode 100644 (file)
index 0000000..9c23a1c
--- /dev/null
@@ -0,0 +1,31 @@
+{
+  "actor": "http://2hu.gensokyo/users/raymoo",
+  "id": "http://2hu.gensokyo/objects/1",
+  "object": {
+    "attributedTo": "http://2hu.gensokyo/users/raymoo",
+    "content": "You expected a cute girl? Too bad. <script>alert('XSS')</script>",
+    "id": "http://2hu.gensokyo/objects/2",
+    "published": "2020-02-12T14:08:20Z",
+    "to": [
+      "http://2hu.gensokyo/users/marisa"
+    ],
+    "tag": [
+      {
+        "icon": {
+          "type": "Image",
+          "url": "http://2hu.gensokyo/emoji/Firefox.gif"
+        },
+        "id": "http://2hu.gensokyo/emoji/Firefox.gif",
+        "name": ":firefox:",
+        "type": "Emoji",
+        "updated": "1970-01-01T00:00:00Z"
+      }
+    ],
+    "type": "ChatMessage"
+  },
+  "published": "2018-02-12T14:08:20Z",
+  "to": [
+    "http://2hu.gensokyo/users/marisa"
+  ],
+  "type": "Create"
+}
index 3c5c3696e299c14e08febf8d52caf83f26ca58b6..bc2317e557b2169eeec1ca2391c2dce601b93339 100644 (file)
@@ -1,6 +1,7 @@
 defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
   use Pleroma.DataCase
 
+  alias Pleroma.Web.ActivityPub.Builder
   alias Pleroma.Web.ActivityPub.ObjectValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
   alias Pleroma.Web.ActivityPub.Utils
@@ -8,6 +9,76 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
 
   import Pleroma.Factory
 
+  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 object == valid_chat_message
+    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 "likes" do
     setup do
       user = insert(:user)
diff --git a/test/web/activity_pub/object_validators/types/recipients_test.exs b/test/web/activity_pub/object_validators/types/recipients_test.exs
new file mode 100644 (file)
index 0000000..2f92187
--- /dev/null
@@ -0,0 +1,15 @@
+defmodule Pleroma.Web.ObjectValidators.Types.RecipientsTest do
+  alias Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients
+  use Pleroma.DataCase
+
+  test "it works with a list" do
+    list = ["https://lain.com/users/lain"]
+    assert {:ok, list} == Recipients.cast(list)
+  end
+
+  test "it turns a single string into a list" do
+    recipient = "https://lain.com/users/lain"
+
+    assert {:ok, [recipient]} == Recipients.cast(recipient)
+  end
+end
index 0b6b551564723da5d3869fe1be0ca053f68d80c8..2889a577c290f2437d27f8924b546ca014bfc54c 100644 (file)
@@ -5,6 +5,7 @@
 defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
   use Pleroma.DataCase
 
+  alias Pleroma.Chat
   alias Pleroma.Notification
   alias Pleroma.Object
   alias Pleroma.Repo
@@ -39,4 +40,66 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
       assert Repo.get_by(Notification, user_id: poster.id, activity_id: like.id)
     end
   end
+
+  describe "creation of ChatMessages" do
+    test "notifies the recipient" do
+      author = insert(:user, local: false)
+      recipient = insert(:user, local: true)
+
+      {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
+      {:ok, chat_message_object} = Object.create(chat_message_data)
+
+      {:ok, create_activity_data, _meta} =
+        Builder.create(author, chat_message_object.data["id"], [recipient.ap_id])
+
+      {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
+
+      {:ok, _create_activity, _meta} = SideEffects.handle(create_activity)
+
+      assert Repo.get_by(Notification, user_id: recipient.id, activity_id: create_activity.id)
+    end
+
+    test "it creates a Chat for the local users and bumps the unread count" do
+      author = insert(:user, local: false)
+      recipient = insert(:user, local: true)
+
+      {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
+      {:ok, chat_message_object} = Object.create(chat_message_data)
+
+      {:ok, create_activity_data, _meta} =
+        Builder.create(author, chat_message_object.data["id"], [recipient.ap_id])
+
+      {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
+
+      {:ok, _create_activity, _meta} = SideEffects.handle(create_activity)
+
+      # The remote user won't get a chat
+      chat = Chat.get(author.id, recipient.ap_id)
+      refute chat
+
+      # The local user will get a chat
+      chat = Chat.get(recipient.id, author.ap_id)
+      assert chat
+
+      author = insert(:user, local: true)
+      recipient = insert(:user, local: true)
+
+      {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
+      {:ok, chat_message_object} = Object.create(chat_message_data)
+
+      {:ok, create_activity_data, _meta} =
+        Builder.create(author, chat_message_object.data["id"], [recipient.ap_id])
+
+      {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
+
+      {:ok, _create_activity, _meta} = SideEffects.handle(create_activity)
+
+      # Both users are local and get the chat
+      chat = Chat.get(author.id, recipient.ap_id)
+      assert chat
+
+      chat = Chat.get(recipient.id, author.ap_id)
+      assert chat
+    end
+  end
 end
diff --git a/test/web/activity_pub/transmogrifier/chat_message_test.exs b/test/web/activity_pub/transmogrifier/chat_message_test.exs
new file mode 100644 (file)
index 0000000..a63a31e
--- /dev/null
@@ -0,0 +1,84 @@
+# 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.Transmogrifier.ChatMessageTest do
+  use Pleroma.DataCase
+
+  import Pleroma.Factory
+
+  alias Pleroma.Activity
+  alias Pleroma.Chat
+  alias Pleroma.Object
+  alias Pleroma.Web.ActivityPub.Transmogrifier
+
+  describe "handle_incoming" do
+    test "it rejects messages that don't contain content" do
+      data =
+        File.read!("test/fixtures/create-chat-message.json")
+        |> Poison.decode!()
+
+      object =
+        data["object"]
+        |> Map.delete("content")
+
+      data =
+        data
+        |> Map.put("object", object)
+
+      _author = insert(:user, ap_id: data["actor"], local: false)
+      _recipient = insert(:user, ap_id: List.first(data["to"]), local: true)
+
+      {:error, _} = Transmogrifier.handle_incoming(data)
+    end
+
+    test "it rejects messages that don't concern local users" do
+      data =
+        File.read!("test/fixtures/create-chat-message.json")
+        |> Poison.decode!()
+
+      _author = insert(:user, ap_id: data["actor"], local: false)
+      _recipient = insert(:user, ap_id: List.first(data["to"]), local: false)
+
+      {:error, _} = Transmogrifier.handle_incoming(data)
+    end
+
+    test "it rejects messages where the `to` field of activity and object don't match" do
+      data =
+        File.read!("test/fixtures/create-chat-message.json")
+        |> Poison.decode!()
+
+      author = insert(:user, ap_id: data["actor"])
+      _recipient = insert(:user, ap_id: List.first(data["to"]))
+
+      data =
+        data
+        |> Map.put("to", author.ap_id)
+
+      {:error, _} = Transmogrifier.handle_incoming(data)
+    end
+
+    test "it inserts it and creates a chat" do
+      data =
+        File.read!("test/fixtures/create-chat-message.json")
+        |> Poison.decode!()
+
+      author = insert(:user, ap_id: data["actor"], local: false)
+      recipient = insert(:user, ap_id: List.first(data["to"]), local: true)
+
+      {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(data)
+
+      assert activity.actor == author.ap_id
+      assert activity.recipients == [recipient.ap_id, author.ap_id]
+
+      %Object{} = object = Object.get_by_ap_id(activity.data["object"])
+
+      assert object
+      assert object.data["content"] == "You expected a cute girl? Too bad. alert(&#39;XSS&#39;)"
+      assert match?(%{"firefox" => _}, object.data["emoji"])
+
+      refute Chat.get(author.id, recipient.ap_id)
+      assert Chat.get(recipient.id, author.ap_id)
+    end
+  end
+end
index 1758662b0c69b3caccd8289a33e4a916f8413dde..65bf55da1c0b350d127f6bcf5bdc376ac535fd39 100644 (file)
@@ -5,6 +5,7 @@
 defmodule Pleroma.Web.CommonAPITest do
   use Pleroma.DataCase
   alias Pleroma.Activity
+  alias Pleroma.Chat
   alias Pleroma.Conversation.Participation
   alias Pleroma.Object
   alias Pleroma.User
@@ -21,6 +22,55 @@ defmodule Pleroma.Web.CommonAPITest do
   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
+
   test "favoriting race condition" do
     user = insert(:user)
     users_serial = insert_list(10, :user)
index 06efdc901aa763ae6dd58a7628b544f5a06c0a43..a5c227991ebb0cf6eba00e30184d3f991946adcd 100644 (file)
@@ -51,6 +51,9 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
       {:ok, activity} = CommonAPI.post(third_user, %{"status" => "repeated post"})
       {:ok, _, _} = CommonAPI.repeat(activity.id, following)
 
+      # This one should not show up in the TL
+      {:ok, _activity} = CommonAPI.post_chat_message(third_user, user, ":gun:")
+
       ret_conn = get(conn, uri)
 
       assert Enum.empty?(json_response(ret_conn, :ok))
index 85fa4f6a27e1a4d924a6e95673c606e5ad4528f3..9ebb13549d3a9da2b4a39ae9dd249c5d91690a67 100644 (file)
@@ -72,6 +72,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
         fields: []
       },
       pleroma: %{
+        ap_id: user.ap_id,
         background_image: "https://example.com/images/asuka_hospital.png",
         confirmation_pending: false,
         tags: [],
@@ -141,6 +142,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
         fields: []
       },
       pleroma: %{
+        ap_id: user.ap_id,
         background_image: nil,
         confirmation_pending: false,
         tags: [],
@@ -339,6 +341,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
         fields: []
       },
       pleroma: %{
+        ap_id: user.ap_id,
         background_image: nil,
         confirmation_pending: false,
         tags: [],
index c3ec9dfecbcf3f1dc44186748c30097fc2b03c28..a48c298f2c68815b9fc3c190dd3e7bcce8200455 100644 (file)
@@ -6,7 +6,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
   use Pleroma.DataCase
 
   alias Pleroma.Activity
+  alias Pleroma.Chat
   alias Pleroma.Notification
+  alias Pleroma.Object
   alias Pleroma.Repo
   alias Pleroma.User
   alias Pleroma.Web.CommonAPI
@@ -14,6 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
   alias Pleroma.Web.MastodonAPI.AccountView
   alias Pleroma.Web.MastodonAPI.NotificationView
   alias Pleroma.Web.MastodonAPI.StatusView
+  alias Pleroma.Web.PleromaAPI.ChatMessageView
   import Pleroma.Factory
 
   defp test_notifications_rendering(notifications, user, expected_result) do
@@ -31,6 +34,29 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
     assert expected_result == result
   end
 
+  test "ChatMessage notification" do
+    user = insert(:user)
+    recipient = insert(:user)
+    {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "what's up my dude")
+
+    {:ok, [notification]} = Notification.create_notifications(activity)
+
+    object = Object.normalize(activity)
+    chat = Chat.get(recipient.id, user.ap_id)
+
+    expected = %{
+      id: to_string(notification.id),
+      pleroma: %{is_seen: false},
+      type: "pleroma:chat_mention",
+      account: AccountView.render("show.json", %{user: user, for: recipient}),
+      chat_message:
+        ChatMessageView.render("show.json", %{object: object, for: recipient, chat: chat}),
+      created_at: Utils.to_masto_date(notification.inserted_at)
+    }
+
+    test_notifications_rendering([notification], recipient, [expected])
+  end
+
   test "Mention notification" do
     user = insert(:user)
     mentioned_user = insert(:user)
diff --git a/test/web/pleroma_api/controllers/chat_controller_test.exs b/test/web/pleroma_api/controllers/chat_controller_test.exs
new file mode 100644 (file)
index 0000000..07b6980
--- /dev/null
@@ -0,0 +1,202 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do
+  use Pleroma.Web.ConnCase, async: true
+
+  alias Pleroma.Chat
+  alias Pleroma.Web.ApiSpec
+  alias Pleroma.Web.ApiSpec.Schemas.ChatMessageResponse
+  alias Pleroma.Web.ApiSpec.Schemas.ChatMessagesResponse
+  alias Pleroma.Web.ApiSpec.Schemas.ChatResponse
+  alias Pleroma.Web.ApiSpec.Schemas.ChatsResponse
+  alias Pleroma.Web.CommonAPI
+
+  import OpenApiSpex.TestAssertions
+  import Pleroma.Factory
+
+  describe "POST /api/v1/pleroma/chats/:id/messages" do
+    setup do: oauth_access(["write:statuses"])
+
+    test "it posts a message to the chat", %{conn: conn, user: user} do
+      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!!"})
+        |> json_response(200)
+
+      assert result["content"] == "Hallo!!"
+      assert result["chat_id"] == chat.id |> to_string()
+      assert_schema(result, "ChatMessageResponse", ApiSpec.spec())
+    end
+  end
+
+  describe "GET /api/v1/pleroma/chats/:id/messages" do
+    setup do: oauth_access(["read:statuses"])
+
+    test "it paginates", %{conn: conn, user: user} do
+      recipient = insert(:user)
+
+      Enum.each(1..30, fn _ ->
+        {:ok, _} = CommonAPI.post_chat_message(user, recipient, "hey")
+      end)
+
+      chat = Chat.get(user.id, recipient.ap_id)
+
+      result =
+        conn
+        |> get("/api/v1/pleroma/chats/#{chat.id}/messages")
+        |> json_response(200)
+
+      assert length(result) == 20
+      assert_schema(result, "ChatMessagesResponse", ApiSpec.spec())
+
+      result =
+        conn
+        |> get("/api/v1/pleroma/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}")
+        |> json_response(200)
+
+      assert length(result) == 10
+      assert_schema(result, "ChatMessagesResponse", ApiSpec.spec())
+    end
+
+    test "it returns the messages for a given chat", %{conn: conn, user: user} do
+      other_user = insert(:user)
+      third_user = insert(:user)
+
+      {:ok, _} = CommonAPI.post_chat_message(user, other_user, "hey")
+      {:ok, _} = CommonAPI.post_chat_message(user, third_user, "hey")
+      {:ok, _} = CommonAPI.post_chat_message(user, other_user, "how are you?")
+      {:ok, _} = CommonAPI.post_chat_message(other_user, user, "fine, how about you?")
+
+      chat = Chat.get(user.id, other_user.ap_id)
+
+      result =
+        conn
+        |> get("/api/v1/pleroma/chats/#{chat.id}/messages")
+        |> json_response(200)
+
+      result
+      |> Enum.each(fn message ->
+        assert message["chat_id"] == chat.id |> to_string()
+      end)
+
+      assert length(result) == 3
+      assert_schema(result, "ChatMessagesResponse", ApiSpec.spec())
+
+      # Trying to get the chat of a different user
+      result =
+        conn
+        |> assign(:user, other_user)
+        |> get("/api/v1/pleroma/chats/#{chat.id}/messages")
+
+      assert result |> json_response(404)
+    end
+  end
+
+  describe "POST /api/v1/pleroma/chats/by-ap-id/:id" do
+    setup do: oauth_access(["write:statuses"])
+
+    test "it creates or returns a chat", %{conn: conn} do
+      other_user = insert(:user)
+
+      result =
+        conn
+        |> post("/api/v1/pleroma/chats/by-ap-id/#{URI.encode_www_form(other_user.ap_id)}")
+        |> json_response(200)
+
+      assert result["id"]
+      assert_schema(result, "ChatResponse", ApiSpec.spec())
+    end
+  end
+
+  describe "GET /api/v1/pleroma/chats" do
+    setup do: oauth_access(["read:statuses"])
+
+    test "it paginates", %{conn: conn, user: user} do
+      Enum.each(1..30, fn _ ->
+        recipient = insert(:user)
+        {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id)
+      end)
+
+      result =
+        conn
+        |> get("/api/v1/pleroma/chats")
+        |> json_response(200)
+
+      assert length(result) == 20
+      assert_schema(result, "ChatsResponse", ApiSpec.spec())
+
+      result =
+        conn
+        |> get("/api/v1/pleroma/chats?max_id=#{List.last(result)["id"]}")
+        |> json_response(200)
+
+      assert length(result) == 10
+
+      assert_schema(result, "ChatsResponse", ApiSpec.spec())
+    end
+
+    test "it return a list of chats the current user is participating in, in descending order of updates",
+         %{conn: conn, user: user} do
+      har = insert(:user)
+      jafnhar = insert(:user)
+      tridi = insert(:user)
+
+      {:ok, chat_1} = Chat.get_or_create(user.id, har.ap_id)
+      :timer.sleep(1000)
+      {:ok, _chat_2} = Chat.get_or_create(user.id, jafnhar.ap_id)
+      :timer.sleep(1000)
+      {:ok, chat_3} = Chat.get_or_create(user.id, tridi.ap_id)
+      :timer.sleep(1000)
+
+      # bump the second one
+      {:ok, chat_2} = Chat.bump_or_create(user.id, jafnhar.ap_id)
+
+      result =
+        conn
+        |> get("/api/v1/pleroma/chats")
+        |> json_response(200)
+
+      ids = Enum.map(result, & &1["id"])
+
+      assert ids == [
+               chat_2.id |> to_string(),
+               chat_3.id |> to_string(),
+               chat_1.id |> to_string()
+             ]
+
+      assert_schema(result, "ChatsResponse", ApiSpec.spec())
+    end
+  end
+
+  describe "schemas" do
+    test "Chat example matches schema" do
+      api_spec = ApiSpec.spec()
+      schema = ChatResponse.schema()
+      assert_schema(schema.example, "ChatResponse", api_spec)
+    end
+
+    test "Chats example matches schema" do
+      api_spec = ApiSpec.spec()
+      schema = ChatsResponse.schema()
+      assert_schema(schema.example, "ChatsResponse", api_spec)
+    end
+
+    test "ChatMessage example matches schema" do
+      api_spec = ApiSpec.spec()
+      schema = ChatMessageResponse.schema()
+      assert_schema(schema.example, "ChatMessageResponse", api_spec)
+    end
+
+    test "ChatsMessage example matches schema" do
+      api_spec = ApiSpec.spec()
+      schema = ChatMessagesResponse.schema()
+      assert_schema(schema.example, "ChatMessagesResponse", api_spec)
+    end
+  end
+end
diff --git a/test/web/pleroma_api/views/chat_message_view_test.exs b/test/web/pleroma_api/views/chat_message_view_test.exs
new file mode 100644 (file)
index 0000000..115335f
--- /dev/null
@@ -0,0 +1,44 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.ChatMessageViewTest do
+  use Pleroma.DataCase
+
+  alias Pleroma.Chat
+  alias Pleroma.Object
+  alias Pleroma.Web.CommonAPI
+  alias Pleroma.Web.PleromaAPI.ChatMessageView
+
+  import Pleroma.Factory
+
+  test "it displays a chat message" do
+    user = insert(:user)
+    recipient = insert(:user)
+    {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "kippis :firefox:")
+
+    chat = Chat.get(user.id, recipient.ap_id)
+
+    object = Object.normalize(activity)
+
+    chat_message = ChatMessageView.render("show.json", object: object, for: user, chat: chat)
+
+    assert chat_message[:id] == object.id |> to_string()
+    assert chat_message[:content] == "kippis :firefox:"
+    assert chat_message[:actor] == user.ap_id
+    assert chat_message[:chat_id]
+    assert chat_message[:created_at]
+    assert match?([%{shortcode: "firefox"}], chat_message[:emojis])
+
+    {:ok, activity} = CommonAPI.post_chat_message(recipient, user, "gkgkgk")
+
+    object = Object.normalize(activity)
+
+    chat_message_two = ChatMessageView.render("show.json", object: object, for: user, chat: chat)
+
+    assert chat_message_two[:id] == object.id |> to_string()
+    assert chat_message_two[:content] == "gkgkgk"
+    assert chat_message_two[:actor] == recipient.ap_id
+    assert chat_message_two[:chat_id] == chat_message[:chat_id]
+  end
+end
diff --git a/test/web/pleroma_api/views/chat_view_test.exs b/test/web/pleroma_api/views/chat_view_test.exs
new file mode 100644 (file)
index 0000000..725da5f
--- /dev/null
@@ -0,0 +1,29 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.ChatViewTest do
+  use Pleroma.DataCase
+
+  alias Pleroma.Chat
+  alias Pleroma.Web.MastodonAPI.AccountView
+  alias Pleroma.Web.PleromaAPI.ChatView
+
+  import Pleroma.Factory
+
+  test "it represents a chat" do
+    user = insert(:user)
+    recipient = insert(:user)
+
+    {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id)
+
+    represented_chat = ChatView.render("show.json", chat: chat)
+
+    assert represented_chat == %{
+             id: "#{chat.id}",
+             recipient: recipient.ap_id,
+             recipient_account: AccountView.render("show.json", user: recipient),
+             unread: 0
+           }
+  end
+end