diff options
Diffstat (limited to 'lib/pleroma/web')
19 files changed, 618 insertions, 3 deletions
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index eedea08a2..f644897c2 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()} @@ -1206,6 +1207,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 @@ -1311,6 +1324,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 diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 429a510b8..7576ed278 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -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"]) diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index dc4bce059..03db681ec 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -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 index 000000000..2feb65f29 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex @@ -0,0 +1,86 @@ +# 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 + + @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 + |> 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 if at least one of the users in this ChatMessage is a local user, otherwise we don't want the message in our system. It also validates the presence of both users in our system." + 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())}, + {_, true} <- {:local?, Enum.any?([actor, recipient], & &1.local)} do + cng + else + {: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 diff --git a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex index b479c3918..02f3a6438 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_validations.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_validations.ex @@ -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 index 000000000..ce52d5623 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex @@ -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/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 6a8f1af96..2effb74f8 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 \\ []) @@ -23,8 +25,37 @@ 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) + + 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 diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 09119137b..66975cf7d 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..cfe3b767b --- /dev/null +++ b/lib/pleroma/web/activity_pub/transmogrifier/chat_message_handling.ex @@ -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 index 000000000..038ebb29d --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex @@ -0,0 +1,81 @@ +# 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 OpenApiSpex.Schema + + @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", + responses: %{ + 200 => + Operation.response("Chat", "application/json", %Schema{ + type: :object, + description: "A created chat is returned", + properties: %{ + id: %Schema{type: :integer} + } + }) + } + } + end + + def index_operation do + %Operation{ + tags: ["chat"], + summary: "Get a list of chats that you participated in", + responses: %{ + 200 => + Operation.response("Chats", "application/json", %Schema{ + type: :array, + description: "A list of chats", + items: %Schema{ + type: :object, + description: "A chat" + } + }) + } + } + end + + def messages_operation do + %Operation{ + tags: ["chat"], + summary: "Get the most recent messages of the chat", + responses: %{ + 200 => + Operation.response("Messages", "application/json", %Schema{ + type: :array, + description: "A list of chat messages", + items: %Schema{ + type: :object, + description: "A chat message" + } + }) + } + } + end + + def post_chat_message_operation do + %Operation{ + tags: ["chat"], + summary: "Post a message to the chat", + responses: %{ + 200 => + Operation.response("Message", "application/json", %Schema{ + type: :object, + description: "A chat message" + }) + } + } + end +end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index f50a909aa..9e25f4c2f 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -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]) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index b4b61e74c..c46517e49 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -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, diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 4da1ab67f..2a9951831 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -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 index 000000000..9d8b9b3cf --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex @@ -0,0 +1,104 @@ +# 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 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] + ) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation + + 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 + 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) + + 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) + + conn + |> put_view(ChatView) + |> render("index.json", chats: 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 + 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 index 000000000..b40ab92a0 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/views/chat_message_view.ex @@ -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 index 000000000..1e9ef4356 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/views/chat_view.ex @@ -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 diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 153802a43..0c56318ee 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -275,6 +275,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) |