aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/pleroma/chat.ex63
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex1
-rw-r--r--lib/pleroma/web/activity_pub/builder.ex22
-rw-r--r--lib/pleroma/web/activity_pub/object_validator.ex30
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex58
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex35
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/create_note_validator.ex (renamed from lib/pleroma/web/activity_pub/object_validators/create_validator.ex)0
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/types/recipients.ex23
-rw-r--r--lib/pleroma/web/activity_pub/side_effects.ex29
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex7
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex30
-rw-r--r--lib/pleroma/web/common_api/common_api.ex23
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/chat_controller.ex113
-rw-r--r--lib/pleroma/web/router.ex9
-rw-r--r--priv/repo/migrations/20200309123730_create_chats.exs16
-rw-r--r--test/chat_test.exs53
-rw-r--r--test/fixtures/create-chat-message.json19
-rw-r--r--test/web/activity_pub/object_validators/types/recipients_test.exs15
-rw-r--r--test/web/activity_pub/side_effects_test.exs46
-rw-r--r--test/web/activity_pub/transmogrifier/chat_message_test.exs32
-rw-r--r--test/web/common_api/common_api_test.exs21
-rw-r--r--test/web/pleroma_api/controllers/chat_controller_test.exs100
22 files changed, 744 insertions, 1 deletions
diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex
new file mode 100644
index 000000000..c2044881f
--- /dev/null
+++ b/lib/pleroma/chat.ex
@@ -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
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 86b105b7f..4a56beb73 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/builder.ex b/lib/pleroma/web/activity_pub/builder.ex
index 429a510b8..f0a6c1e1b 100644
--- a/lib/pleroma/web/activity_pub/builder.ex
+++ b/lib/pleroma/web/activity_pub/builder.ex
@@ -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"])
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
index 926804ce7..926804ce7 100644
--- a/lib/pleroma/web/activity_pub/object_validators/create_validator.ex
+++ b/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/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex
index 666a4e310..594f32700 100644
--- a/lib/pleroma/web/activity_pub/side_effects.ex
+++ b/lib/pleroma/web/activity_pub/side_effects.ex
@@ -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
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 39feae285..ad77a5037 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
@@ -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
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/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index c56756a3d..2f13daf0c 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -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
index 000000000..972330f4e
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
@@ -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
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 5f5ec1c81..b10bf4466 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -287,6 +287,15 @@ defmodule Pleroma.Web.Router 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)
+
get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
get("/conversations/:id", PleromaAPIController, :conversation)
post("/conversations/read", PleromaAPIController, :read_conversations)
diff --git a/priv/repo/migrations/20200309123730_create_chats.exs b/priv/repo/migrations/20200309123730_create_chats.exs
new file mode 100644
index 000000000..715d798ea
--- /dev/null
+++ b/priv/repo/migrations/20200309123730_create_chats.exs
@@ -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
index 000000000..952598c87
--- /dev/null
+++ b/test/chat_test.exs
@@ -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
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/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs
index b67bd14b3..b629d0d5d 100644
--- a/test/web/activity_pub/side_effects_test.exs
+++ b/test/web/activity_pub/side_effects_test.exs
@@ -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
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
diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs
index b12be973f..168721c81 100644
--- a/test/web/common_api/common_api_test.exs
+++ b/test/web/common_api/common_api_test.exs
@@ -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
index 000000000..b4230e5ad
--- /dev/null
+++ b/test/web/pleroma_api/controllers/chat_controller_test.exs
@@ -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