Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into remake-remodel-dms
authorlain <lain@soykaf.club>
Wed, 15 Apr 2020 12:41:43 +0000 (14:41 +0200)
committerlain <lain@soykaf.club>
Wed, 15 Apr 2020 12:41:43 +0000 (14:41 +0200)
22 files changed:
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/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/common_api/common_api.ex
lib/pleroma/web/pleroma_api/controllers/chat_controller.ex [new file with mode: 0644]
lib/pleroma/web/router.ex
priv/repo/migrations/20200309123730_create_chats.exs [new file with mode: 0644]
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_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/pleroma_api/controllers/chat_controller_test.exs [new file with mode: 0644]

diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex
new file mode 100644 (file)
index 0000000..c204488
--- /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.User
+  alias Pleroma.Repo
+
+  @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 86b105b7fe8610f3ac1f0257be4fc5c845768080..4a56beb7372fb433cd50eb44f8d5f5e963479a3c 100644 (file)
@@ -397,6 +397,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     end
   end
 
+  # TODO: Is this even used now?
   # TODO: This is weird, maybe we shouldn't check here if we can make the activity.
   @spec like(User.t(), Object.t(), String.t() | nil, boolean()) ::
           {:ok, Activity.t(), Object.t()} | {:error, any()}
index 429a510b8118df4f136f614de281131fd5dd78b8..f0a6c1e1b8eca57fa1a6f4b7eb0ce13413bf705e 100644 (file)
@@ -10,6 +10,28 @@ defmodule Pleroma.Web.ActivityPub.Builder do
   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"
+     }, []}
+  end
+
+  def chat_message(actor, recipient, content) do
+    {:ok,
+     %{
+       "id" => Utils.generate_object_id(),
+       "actor" => actor.ap_id,
+       "type" => "ChatMessage",
+       "to" => [recipient],
+       "content" => 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..49cc7256127778ad738073ef443528a04840376f 100644 (file)
@@ -12,18 +12,46 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
   alias Pleroma.Object
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
+  alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
+  alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
 
   @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
   def validate(object, meta)
 
   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_apply() do
+      object = stringify_keys(object)
+      {:ok, object, meta}
+    end
+  end
+
+  def validate(%{"type" => "Create"} = object, meta) do
+    with {:ok, object} <-
+           object
+           |> CreateChatMessageValidator.cast_and_apply() 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..ab5be35
--- /dev/null
@@ -0,0 +1,58 @@
+# 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.Web.ActivityPub.ObjectValidators.Types
+
+  import Ecto.Changeset
+
+  @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)
+  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
+    |> 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])
+  end
+end
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..6593114
--- /dev/null
@@ -0,0 +1,35 @@
+# 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
+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 666a4e310a6a116c1f68e0dafc6fd30686f0bd2f..594f327009f828cf43da74349724202434b8bdeb 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 \\ [])
@@ -21,8 +23,35 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
     {:ok, object, meta}
   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)
+
+    {: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 39feae285593400e358116a99fd9ffcd17de8840..ad77a5037acac6d40b6356f92b6cf8789af8072a 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..b584373
--- /dev/null
@@ -0,0 +1,30 @@
+# 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),
+         {_, {: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}
+    end
+  end
+end
index c56756a3dd7e00e12c9d42320da1367b953784de..2f13daf0ccbd899e2de3f0028bc9b5843f056932 100644 (file)
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.CommonAPI do
   alias Pleroma.Conversation.Participation
   alias Pleroma.FollowingRelationship
   alias Pleroma.Object
+  alias Pleroma.Repo
   alias Pleroma.ThreadMute
   alias Pleroma.User
   alias Pleroma.UserRelationship
@@ -23,6 +24,28 @@ defmodule Pleroma.Web.CommonAPI do
   require Pleroma.Constants
   require Logger
 
+  def post_chat_message(user, recipient, content) do
+    transaction =
+      Repo.transaction(fn ->
+        with {_, {:ok, chat_message_data, _meta}} <-
+               {:build_object, Builder.chat_message(user, recipient.ap_id, content)},
+             {_, {: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}
+        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])
 
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..972330f
--- /dev/null
@@ -0,0 +1,113 @@
+# 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.Repo
+  alias Pleroma.User
+  alias Pleroma.Web.CommonAPI
+
+  import Ecto.Query
+
+  # TODO
+  # - Oauth stuff
+  # - Views / Representers
+  # - Error handling
+
+  def post_chat_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{
+        "id" => id,
+        "content" => content
+      }) 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
+      represented_message = %{
+        actor: message.data["actor"],
+        id: message.id,
+        content: message.data["content"]
+      }
+
+      conn
+      |> json(represented_message)
+    end
+  end
+
+  def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{"id" => id}) 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]
+            ),
+          order_by: [desc: o.id]
+        )
+        |> Repo.all()
+
+      represented_messages =
+        messages
+        |> Enum.map(fn message ->
+          %{
+            actor: message.data["actor"],
+            id: message.id,
+            content: message.data["content"]
+          }
+        end)
+
+      conn
+      |> json(represented_messages)
+    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]
+      )
+      |> Repo.all()
+
+    represented_chats =
+      Enum.map(chats, fn chat ->
+        %{
+          id: chat.id,
+          recipient: chat.recipient,
+          unread: chat.unread
+        }
+      end)
+
+    conn
+    |> json(represented_chats)
+  end
+
+  def create(%{assigns: %{user: user}} = conn, params) do
+    recipient = params["ap_id"] |> URI.decode_www_form()
+
+    with {:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do
+      represented_chat = %{
+        id: chat.id,
+        recipient: chat.recipient,
+        unread: chat.unread
+      }
+
+      conn
+      |> json(represented_chat)
+    end
+  end
+end
index 5f5ec1c81c3673b5074fd93fe086341004e9fe1b..b10bf4466164c66857238168fc0ef21e9f26f75e 100644 (file)
@@ -284,6 +284,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
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..4aa17f4
--- /dev/null
@@ -0,0 +1,19 @@
+{
+    "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"
+}
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 b67bd14b36e08f07c57629aefa378a7991acdcb7..b629d0d5d4b44ed5937b5229477786029f963651 100644 (file)
@@ -5,6 +5,7 @@
 defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
   use Pleroma.DataCase
 
+  alias Pleroma.Chat
   alias Pleroma.Object
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.Builder
@@ -31,4 +32,49 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
       assert user.ap_id in object.data["likes"]
     end
   end
+
+  describe "creation of ChatMessages" do
+    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..aed62c5
--- /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.ActivityPub.Transmogrifier.ChatMessageTest do
+  use Pleroma.DataCase
+
+  import Pleroma.Factory
+
+  alias Pleroma.Activity
+  alias Pleroma.Object
+  alias Pleroma.Web.ActivityPub.Transmogrifier
+
+  describe "handle_incoming" do
+    test "it insert it" 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)
+
+      {: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
+    end
+  end
+end
index b12be973f41de23ca7a78b3dbd346b3974050af4..168721c81f54f67abccf4e46b66b01bfbc615f0b 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,26 @@ defmodule Pleroma.Web.CommonAPITest do
   setup do: clear_config([:instance, :limit])
   setup do: clear_config([:instance, :max_pinned_statuses])
 
+  describe "posting chat messages" do
+    test "it posts a chat message" do
+      author = insert(:user)
+      recipient = insert(:user)
+
+      {:ok, activity} = CommonAPI.post_chat_message(author, recipient, "a test message")
+
+      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"
+
+      assert Chat.get(author.id, recipient.ap_id)
+      assert Chat.get(recipient.id, author.ap_id)
+    end
+  end
+
   test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do
     user = insert(:user)
     {:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
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..b4230e5
--- /dev/null
@@ -0,0 +1,100 @@
+# 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.CommonAPI
+
+  import Pleroma.Factory
+
+  describe "POST /api/v1/pleroma/chats/:id/messages" do
+    test "it posts a message to the chat", %{conn: conn} do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
+
+      result =
+        conn
+        |> assign(:user, user)
+        |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "Hallo!!"})
+        |> json_response(200)
+
+      assert result["content"] == "Hallo!!"
+    end
+  end
+
+  describe "GET /api/v1/pleroma/chats/:id/messages" do
+    # TODO
+    # - Test that statuses don't show
+    # - Test the case where it's not the user's chat
+    # - Test the returned data
+    test "it returns the messages for a given chat", %{conn: conn} do
+      user = insert(:user)
+      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
+        |> assign(:user, user)
+        |> get("/api/v1/pleroma/chats/#{chat.id}/messages")
+        |> json_response(200)
+
+      assert length(result) == 3
+    end
+  end
+
+  describe "POST /api/v1/pleroma/chats/by-ap-id/:id" do
+    test "it creates or returns a chat", %{conn: conn} do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      result =
+        conn
+        |> assign(:user, user)
+        |> post("/api/v1/pleroma/chats/by-ap-id/#{URI.encode_www_form(other_user.ap_id)}")
+        |> json_response(200)
+
+      assert result["id"]
+    end
+  end
+
+  describe "GET /api/v1/pleroma/chats" do
+    test "it return a list of chats the current user is participating in, in descending order of updates",
+         %{conn: conn} do
+      user = insert(:user)
+      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
+        |> assign(:user, user)
+        |> get("/api/v1/pleroma/chats")
+        |> json_response(200)
+
+      ids = Enum.map(result, & &1["id"])
+
+      assert ids == [chat_2.id, chat_3.id, chat_1.id]
+    end
+  end
+end