From 3775683a04e9b819f88bfba533b755bbd5b3c2df Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Wed, 8 Apr 2020 15:55:43 +0200
Subject: [PATCH] ChatMessage: Basic incoming handling.

---
 lib/pleroma/chat.ex                           |  2 +-
 lib/pleroma/web/activity_pub/activity_pub.ex  |  1 +
 .../web/activity_pub/object_validator.ex      | 30 +++++++++-
 .../chat_message_validator.ex                 | 58 +++++++++++++++++++
 .../create_chat_message_validator.ex          | 35 +++++++++++
 ..._validator.ex => create_note_validator.ex} |  0
 .../object_validators/types/recipients.ex     | 23 ++++++++
 .../web/activity_pub/transmogrifier.ex        |  7 +++
 .../transmogrifier/chat_message_handling.ex   | 30 ++++++++++
 test/fixtures/create-chat-message.json        | 19 ++++++
 .../types/recipients_test.exs                 | 15 +++++
 .../transmogrifier/chat_message_test.exs      | 32 ++++++++++
 12 files changed, 250 insertions(+), 2 deletions(-)
 create mode 100644 lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex
 create mode 100644 lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex
 rename lib/pleroma/web/activity_pub/object_validators/{create_validator.ex => create_note_validator.ex} (100%)
 create mode 100644 lib/pleroma/web/activity_pub/object_validators/types/recipients.ex
 create mode 100644 lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex
 create mode 100644 test/fixtures/create-chat-message.json
 create mode 100644 test/web/activity_pub/object_validators/types/recipients_test.exs
 create mode 100644 test/web/activity_pub/transmogrifier/chat_message_test.exs

diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex
index e2a8b8eba..07ad62b97 100644
--- a/lib/pleroma/chat.ex
+++ b/lib/pleroma/chat.ex
@@ -10,7 +10,7 @@ defmodule Pleroma.Chat do
   alias Pleroma.Repo
 
   @moduledoc """
-  Chat keeps a reference to DirectMessage conversations between a user and an recipient. The recipient can be a user (for now) or a group (not implemented yet).
+  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.
   """
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 19286fd01..0b4892501 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -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()}
diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex
index dc4bce059..49cc72561 100644
--- a/lib/pleroma/web/activity_pub/object_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -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
index 000000000..ab5be3596
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex
@@ -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
index 000000000..659311480
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex
@@ -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/create_validator.ex b/lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex
similarity index 100%
rename from lib/pleroma/web/activity_pub/object_validators/create_validator.ex
rename to lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex
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
index 000000000..5a3040842
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex
@@ -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
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 0a8ad62ad..becc35ea3 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -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
@@ -612,6 +613,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
index 000000000..b5843736f
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex
@@ -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
diff --git a/test/fixtures/create-chat-message.json b/test/fixtures/create-chat-message.json
new file mode 100644
index 000000000..4aa17f4a5
--- /dev/null
+++ b/test/fixtures/create-chat-message.json
@@ -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
index 000000000..2f9218774
--- /dev/null
+++ b/test/web/activity_pub/object_validators/types/recipients_test.exs
@@ -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
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
index 000000000..aed62c520
--- /dev/null
+++ b/test/web/activity_pub/transmogrifier/chat_message_test.exs
@@ -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
-- 
2.49.0