summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorrinpatch <rinpatch@sdf.org>2020-06-10 12:05:45 +0000
committerrinpatch <rinpatch@sdf.org>2020-06-10 12:05:45 +0000
commit7aa6c82937090ca6f2298dee0ef894954ca2f129 (patch)
tree850f22f06eccd0babae8b805fa55109b536f324c
parent1b746cfbbb55cd3411e809ead246c752a43361d0 (diff)
parent064c4f86f32b626a626fbc238f09434617f57a90 (diff)
Merge branch 'remake-remodel-dms' into 'develop'
Chats / ChatMessages See merge request pleroma/pleroma!2429
-rw-r--r--CHANGELOG.md1
-rw-r--r--docs/API/chats.md248
-rw-r--r--docs/API/differences_in_mastoapi_responses.md4
-rw-r--r--docs/ap_extensions.md35
-rw-r--r--lib/pleroma/activity.ex36
-rw-r--r--lib/pleroma/chat.ex72
-rw-r--r--lib/pleroma/chat/message_reference.ex117
-rw-r--r--lib/pleroma/migration_helper/notification_backfill.ex85
-rw-r--r--lib/pleroma/notification.ex110
-rw-r--r--lib/pleroma/upload.ex1
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex31
-rw-r--r--lib/pleroma/web/activity_pub/builder.ex37
-rw-r--r--lib/pleroma/web/activity_pub/object_validator.ex47
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex80
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex123
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex91
-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/delete_validator.ex3
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/types/recipients.ex12
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex25
-rw-r--r--lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex20
-rw-r--r--lib/pleroma/web/activity_pub/pipeline.ex4
-rw-r--r--lib/pleroma/web/activity_pub/side_effects.ex97
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex20
-rw-r--r--lib/pleroma/web/api_spec/operations/chat_operation.ex355
-rw-r--r--lib/pleroma/web/api_spec/operations/notification_operation.ex1
-rw-r--r--lib/pleroma/web/api_spec/operations/subscription_operation.ex5
-rw-r--r--lib/pleroma/web/api_spec/schemas/chat.ex75
-rw-r--r--lib/pleroma/web/api_spec/schemas/chat_message.ex41
-rw-r--r--lib/pleroma/web/common_api/common_api.ex58
-rw-r--r--lib/pleroma/web/common_api/utils.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/notification_controller.ex14
-rw-r--r--lib/pleroma/web/mastodon_api/mastodon_api.ex15
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex1
-rw-r--r--lib/pleroma/web/mastodon_api/views/instance_view.ex3
-rw-r--r--lib/pleroma/web/mastodon_api/views/notification_view.ex38
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/chat_controller.ex174
-rw-r--r--lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex45
-rw-r--r--lib/pleroma/web/pleroma_api/views/chat_view.ex33
-rw-r--r--lib/pleroma/web/push/impl.ex24
-rw-r--r--lib/pleroma/web/push/subscription.ex2
-rw-r--r--lib/pleroma/web/router.ex9
-rw-r--r--lib/pleroma/web/streamer/streamer.ex44
-rw-r--r--lib/pleroma/web/views/streamer_view.ex23
-rw-r--r--priv/repo/migrations/20200309123730_create_chats.exs16
-rw-r--r--priv/repo/migrations/20200602094828_add_type_to_notifications.exs9
-rw-r--r--priv/repo/migrations/20200602125218_backfill_notification_types.exs10
-rw-r--r--priv/repo/migrations/20200602150528_create_chat_message_reference.exs20
-rw-r--r--priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs7
-rw-r--r--priv/repo/migrations/20200603120448_remove_unread_from_chats.exs9
-rw-r--r--priv/repo/migrations/20200603122732_add_seen_index_to_chat_message_references.exs12
-rw-r--r--priv/repo/migrations/20200604150318_migrate_seen_to_unread_in_chat_message_references.exs30
-rw-r--r--priv/repo/migrations/20200606105430_change_type_to_enum_for_notifications.exs36
-rw-r--r--priv/repo/migrations/20200607112923_change_chat_id_to_flake.exs23
-rw-r--r--priv/static/schemas/litepub-0.1.jsonld1
-rw-r--r--test/chat/message_reference_test.exs29
-rw-r--r--test/chat_test.exs61
-rw-r--r--test/fixtures/create-chat-message.json31
-rw-r--r--test/migration_helper/notification_backfill_test.exs56
-rw-r--r--test/notification_test.exs12
-rw-r--r--test/upload_test.exs1
-rw-r--r--test/web/activity_pub/object_validator_test.exs250
-rw-r--r--test/web/activity_pub/object_validators/types/object_id_test.exs4
-rw-r--r--test/web/activity_pub/object_validators/types/safe_text_test.exs30
-rw-r--r--test/web/activity_pub/pipeline_test.exs9
-rw-r--r--test/web/activity_pub/side_effects_test.exs185
-rw-r--r--test/web/activity_pub/transmogrifier/chat_message_test.exs153
-rw-r--r--test/web/activity_pub/transmogrifier/follow_handling_test.exs27
-rw-r--r--test/web/common_api/common_api_test.exs146
-rw-r--r--test/web/mastodon_api/controllers/notification_controller_test.exs21
-rw-r--r--test/web/mastodon_api/controllers/subscription_controller_test.exs6
-rw-r--r--test/web/mastodon_api/views/account_view_test.exs2
-rw-r--r--test/web/mastodon_api/views/notification_view_test.exs28
-rw-r--r--test/web/node_info_test.exs3
-rw-r--r--test/web/pleroma_api/controllers/chat_controller_test.exs336
-rw-r--r--test/web/pleroma_api/views/chat/message_reference_view_test.exs61
-rw-r--r--test/web/pleroma_api/views/chat_view_test.exs48
-rw-r--r--test/web/push/impl_test.exs67
-rw-r--r--test/web/streamer/streamer_test.exs55
79 files changed, 3907 insertions, 148 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 839bf90ab..1cf2210f5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- **Breaking:** removed `with_move` parameter from notifications timeline.
### Added
+- Chats: Added support for federated chats. For details, see the docs.
- ActivityPub: Added support for existing AP ids for instances migrated from Mastodon.
- Instance: Add `background_image` to configuration and `/api/v1/instance`
- Instance: Extend `/api/v1/instance` with Pleroma-specific information.
diff --git a/docs/API/chats.md b/docs/API/chats.md
new file mode 100644
index 000000000..aa6119670
--- /dev/null
+++ b/docs/API/chats.md
@@ -0,0 +1,248 @@
+# Chats
+
+Chats are a way to represent an IM-style conversation between two actors. They are not the same as direct messages and they are not `Status`es, even though they have a lot in common.
+
+## Why Chats?
+
+There are no 'visibility levels' in ActivityPub, their definition is purely a Mastodon convention. Direct Messaging between users on the fediverse has mostly been modeled by using ActivityPub addressing following Mastodon conventions on normal `Note` objects. In this case, a 'direct message' would be a message that has no followers addressed and also does not address the special public actor, but just the recipients in the `to` field. It would still be a `Note` and is presented with other `Note`s as a `Status` in the API.
+
+This is an awkward setup for a few reasons:
+
+- As DMs generally still follow the usual `Status` conventions, it is easy to accidentally pull somebody into a DM thread by mentioning them. (e.g. "I hate @badguy so much")
+- It is possible to go from a publicly addressed `Status` to a DM reply, back to public, then to a 'followers only' reply, and so on. This can be become very confusing, as it is unclear which user can see which part of the conversation.
+- The standard `Status` format of implicit addressing also leads to rather ugly results if you try to display the messages as a chat, because all the recipients are always mentioned by name in the message.
+- As direct messages are posted with the same api call (and usually same frontend component) as public messages, accidentally making a public message private or vice versa can happen easily. Client bugs can also lead to this, accidentally making private messages public.
+
+As a measure to improve this situation, the `Conversation` concept and related Pleroma extensions were introduced. While it made it possible to work around a few of the issues, many of the problems remained and it didn't see much adoption because it was too complicated to use correctly.
+
+## Chats explained
+For this reasons, Chats are a new and different entity, both in the API as well as in ActivityPub. A quick overview:
+
+- Chats are meant to represent an instant message conversation between two actors. For now these are only 1-on-1 conversations, but the other actor can be a group in the future.
+- Chat messages have the ActivityPub type `ChatMessage`. They are not `Note`s. Servers that don't understand them will just drop them.
+- The only addressing allowed in `ChatMessage`s is one single ActivityPub actor in the `to` field.
+- There's always only one Chat between two actors. If you start chatting with someone and later start a 'new' Chat, the old Chat will be continued.
+- `ChatMessage`s are posted with a different api, making it very hard to accidentally send a message to the wrong person.
+- `ChatMessage`s don't show up in the existing timelines.
+- Chats can never go from private to public. They are always private between the two actors.
+
+## Caveats
+
+- Chats are NOT E2E encrypted (yet). Security is still the same as email.
+
+## API
+
+In general, the way to send a `ChatMessage` is to first create a `Chat`, then post a message to that `Chat`. `Group`s will later be supported by making them a sub-type of `Account`.
+
+This is the overview of using the API. The API is also documented via OpenAPI, so you can view it and play with it by pointing SwaggerUI or a similar OpenAPI tool to `https://yourinstance.tld/api/openapi`.
+
+### Creating or getting a chat.
+
+To create or get an existing Chat for a certain recipient (identified by Account ID)
+you can call:
+
+`POST /api/v1/pleroma/chats/by-account-id/:account_id`
+
+The account id is the normal FlakeId of the user
+```
+POST /api/v1/pleroma/chats/by-account-id/someflakeid
+```
+
+If you already have the id of a chat, you can also use
+
+```
+GET /api/v1/pleroma/chats/:id
+```
+
+There will only ever be ONE Chat for you and a given recipient, so this call
+will return the same Chat if you already have one with that user.
+
+Returned data:
+
+```json
+{
+ "account": {
+ "id": "someflakeid",
+ "username": "somenick",
+ ...
+ },
+ "id" : "1",
+ "unread" : 2,
+ "last_message" : {...}, // The last message in that chat
+ "updated_at": "2020-04-21T15:11:46.000Z"
+}
+```
+
+### Marking a chat as read
+
+To mark a number of messages in a chat up to a certain message as read, you can use
+
+`POST /api/v1/pleroma/chats/:id/read`
+
+
+Parameters:
+- last_read_id: Given this id, all chat messages until this one will be marked as read. Required.
+
+
+Returned data:
+
+```json
+{
+ "account": {
+ "id": "someflakeid",
+ "username": "somenick",
+ ...
+ },
+ "id" : "1",
+ "unread" : 0,
+ "updated_at": "2020-04-21T15:11:46.000Z"
+}
+```
+
+### Marking a single chat message as read
+
+To set the `unread` property of a message to `false`
+
+`POST /api/v1/pleroma/chats/:id/messages/:message_id/read`
+
+Returned data:
+
+The modified chat message
+
+### Getting a list of Chats
+
+`GET /api/v1/pleroma/chats`
+
+This will return a list of chats that you have been involved in, sorted by their
+last update (so new chats will be at the top).
+
+Returned data:
+
+```json
+[
+ {
+ "account": {
+ "id": "someflakeid",
+ "username": "somenick",
+ ...
+ },
+ "id" : "1",
+ "unread" : 2,
+ "last_message" : {...}, // The last message in that chat
+ "updated_at": "2020-04-21T15:11:46.000Z"
+ }
+]
+```
+
+The recipient of messages that are sent to this chat is given by their AP ID.
+No pagination is implemented for now.
+
+### Getting the messages for a Chat
+
+For a given Chat id, you can get the associated messages with
+
+`GET /api/v1/pleroma/chats/:id/messages`
+
+This will return all messages, sorted by most recent to least recent. The usual
+pagination options are implemented.
+
+Returned data:
+
+```json
+[
+ {
+ "account_id": "someflakeid",
+ "chat_id": "1",
+ "content": "Check this out :firefox:",
+ "created_at": "2020-04-21T15:11:46.000Z",
+ "emojis": [
+ {
+ "shortcode": "firefox",
+ "static_url": "https://dontbulling.me/emoji/Firefox.gif",
+ "url": "https://dontbulling.me/emoji/Firefox.gif",
+ "visible_in_picker": false
+ }
+ ],
+ "id": "13",
+ "unread": true
+ },
+ {
+ "account_id": "someflakeid",
+ "chat_id": "1",
+ "content": "Whats' up?",
+ "created_at": "2020-04-21T15:06:45.000Z",
+ "emojis": [],
+ "id": "12",
+ "unread": false
+ }
+]
+```
+
+### Posting a chat message
+
+Posting a chat message for given Chat id works like this:
+
+`POST /api/v1/pleroma/chats/:id/messages`
+
+Parameters:
+- content: The text content of the message. Optional if media is attached.
+- media_id: The id of an upload that will be attached to the message.
+
+Currently, no formatting beyond basic escaping and emoji is implemented.
+
+Returned data:
+
+```json
+{
+ "account_id": "someflakeid",
+ "chat_id": "1",
+ "content": "Check this out :firefox:",
+ "created_at": "2020-04-21T15:11:46.000Z",
+ "emojis": [
+ {
+ "shortcode": "firefox",
+ "static_url": "https://dontbulling.me/emoji/Firefox.gif",
+ "url": "https://dontbulling.me/emoji/Firefox.gif",
+ "visible_in_picker": false
+ }
+ ],
+ "id": "13",
+ "unread": false
+}
+```
+
+### Deleting a chat message
+
+Deleting a chat message for given Chat id works like this:
+
+`DELETE /api/v1/pleroma/chats/:chat_id/messages/:message_id`
+
+Returned data is the deleted message.
+
+### Notifications
+
+There's a new `pleroma:chat_mention` notification, which has this form. It is not given out in the notifications endpoint by default, you need to explicitly request it with `include_types[]=pleroma:chat_mention`:
+
+```json
+{
+ "id": "someid",
+ "type": "pleroma:chat_mention",
+ "account": { ... } // User account of the sender,
+ "chat_message": {
+ "chat_id": "1",
+ "id": "10",
+ "content": "Hello",
+ "account_id": "someflakeid",
+ "unread": false
+ },
+ "created_at": "somedate"
+}
+```
+
+### Streaming
+
+There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field.
+
+### Web Push
+
+If you want to receive push messages for this type, you'll need to add the `pleroma:chat_mention` type to your alerts in the push subscription.
diff --git a/docs/API/differences_in_mastoapi_responses.md b/docs/API/differences_in_mastoapi_responses.md
index 434ade9a4..be3c802af 100644
--- a/docs/API/differences_in_mastoapi_responses.md
+++ b/docs/API/differences_in_mastoapi_responses.md
@@ -230,3 +230,7 @@ Has theses additional parameters (which are the same as in Pleroma-API):
Has these additional fields under the `pleroma` object:
- `unread_count`: contains number unread notifications
+
+## Streaming
+
+There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field.
diff --git a/docs/ap_extensions.md b/docs/ap_extensions.md
new file mode 100644
index 000000000..c4550a1ac
--- /dev/null
+++ b/docs/ap_extensions.md
@@ -0,0 +1,35 @@
+# ChatMessages
+
+ChatMessages are the messages sent in 1-on-1 chats. They are similar to
+`Note`s, but the addresing is done by having a single AP actor in the `to`
+field. Addressing multiple actors is not allowed. These messages are always
+private, there is no public version of them. They are created with a `Create`
+activity.
+
+Example:
+
+```json
+{
+ "actor": "http://2hu.gensokyo/users/raymoo",
+ "id": "http://2hu.gensokyo/objects/1",
+ "object": {
+ "attributedTo": "http://2hu.gensokyo/users/raymoo",
+ "content": "You expected a cute girl? Too bad.",
+ "id": "http://2hu.gensokyo/objects/2",
+ "published": "2020-02-12T14:08:20Z",
+ "to": [
+ "http://2hu.gensokyo/users/marisa"
+ ],
+ "type": "ChatMessage"
+ },
+ "published": "2018-02-12T14:08:20Z",
+ "to": [
+ "http://2hu.gensokyo/users/marisa"
+ ],
+ "type": "Create"
+}
+```
+
+This setup does not prevent multi-user chats, but these will have to go through
+a `Group`, which will be the recipient of the messages and then `Announce` them
+to the users in the `Group`.
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index 6213d0eb7..da1be20b3 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -24,16 +24,6 @@ defmodule Pleroma.Activity do
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
- # https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
- @mastodon_notification_types %{
- "Create" => "mention",
- "Follow" => ["follow", "follow_request"],
- "Announce" => "reblog",
- "Like" => "favourite",
- "Move" => "move",
- "EmojiReact" => "pleroma:emoji_reaction"
- }
-
schema "activities" do
field(:data, :map)
field(:local, :boolean, default: true)
@@ -300,32 +290,6 @@ defmodule Pleroma.Activity do
def follow_accepted?(_), do: false
- @spec mastodon_notification_type(Activity.t()) :: String.t() | nil
-
- for {ap_type, type} <- @mastodon_notification_types, not is_list(type) do
- def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
- do: unquote(type)
- end
-
- def mastodon_notification_type(%Activity{data: %{"type" => "Follow"}} = activity) do
- if follow_accepted?(activity) do
- "follow"
- else
- "follow_request"
- end
- end
-
- def mastodon_notification_type(%Activity{}), do: nil
-
- @spec from_mastodon_notification_type(String.t()) :: String.t() | nil
- @doc "Converts Mastodon notification type to AR activity type"
- def from_mastodon_notification_type(type) do
- with {k, _v} <-
- Enum.find(@mastodon_notification_types, fn {_k, v} -> type in List.wrap(v) end) do
- k
- end
- end
-
def all_by_actor_and_id(actor, status_ids \\ [])
def all_by_actor_and_id(_actor, []), do: []
diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex
new file mode 100644
index 000000000..24a86371e
--- /dev/null
+++ b/lib/pleroma/chat.ex
@@ -0,0 +1,72 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Chat do
+ use Ecto.Schema
+
+ import Ecto.Changeset
+
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ @moduledoc """
+ Chat keeps a reference to ChatMessage conversations between a user and an recipient. The recipient can be a user (for now) or a group (not implemented yet).
+
+ It is a helper only, to make it easy to display a list of chats with other people, ordered by last bump. The actual messages are retrieved by querying the recipients of the ChatMessages.
+ """
+
+ @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
+
+ schema "chats" do
+ belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
+ field(:recipient, :string)
+
+ timestamps()
+ end
+
+ def changeset(struct, params) do
+ struct
+ |> cast(params, [:user_id, :recipient])
+ |> validate_change(:recipient, fn
+ :recipient, recipient ->
+ case User.get_cached_by_ap_id(recipient) do
+ nil -> [recipient: "must be an existing user"]
+ _ -> []
+ end
+ end)
+ |> validate_required([:user_id, :recipient])
+ |> unique_constraint(:user_id, name: :chats_user_id_recipient_index)
+ end
+
+ def get_by_id(id) do
+ __MODULE__
+ |> Repo.get(id)
+ 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__{}
+ |> changeset(%{user_id: user_id, recipient: recipient})
+ |> Repo.insert(
+ # Need to set something, otherwise we get nothing back at all
+ on_conflict: [set: [recipient: recipient]],
+ returning: true,
+ conflict_target: [:user_id, :recipient]
+ )
+ end
+
+ def bump_or_create(user_id, recipient) do
+ %__MODULE__{}
+ |> changeset(%{user_id: user_id, recipient: recipient})
+ |> Repo.insert(
+ on_conflict: [set: [updated_at: NaiveDateTime.utc_now()]],
+ returning: true,
+ conflict_target: [:user_id, :recipient]
+ )
+ end
+end
diff --git a/lib/pleroma/chat/message_reference.ex b/lib/pleroma/chat/message_reference.ex
new file mode 100644
index 000000000..131ae0186
--- /dev/null
+++ b/lib/pleroma/chat/message_reference.ex
@@ -0,0 +1,117 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Chat.MessageReference do
+ @moduledoc """
+ A reference that builds a relation between an AP chat message that a user can see and whether it has been seen
+ by them, or should be displayed to them. Used to build the chat view that is presented to the user.
+ """
+
+ use Ecto.Schema
+
+ alias Pleroma.Chat
+ alias Pleroma.Object
+ alias Pleroma.Repo
+
+ import Ecto.Changeset
+ import Ecto.Query
+
+ @primary_key {:id, FlakeId.Ecto.Type, autogenerate: true}
+
+ schema "chat_message_references" do
+ belongs_to(:object, Object)
+ belongs_to(:chat, Chat, type: FlakeId.Ecto.CompatType)
+
+ field(:unread, :boolean, default: true)
+
+ timestamps()
+ end
+
+ def changeset(struct, params) do
+ struct
+ |> cast(params, [:object_id, :chat_id, :unread])
+ |> validate_required([:object_id, :chat_id, :unread])
+ end
+
+ def get_by_id(id) do
+ __MODULE__
+ |> Repo.get(id)
+ |> Repo.preload(:object)
+ end
+
+ def delete(cm_ref) do
+ cm_ref
+ |> Repo.delete()
+ end
+
+ def delete_for_object(%{id: object_id}) do
+ from(cr in __MODULE__,
+ where: cr.object_id == ^object_id
+ )
+ |> Repo.delete_all()
+ end
+
+ def for_chat_and_object(%{id: chat_id}, %{id: object_id}) do
+ __MODULE__
+ |> Repo.get_by(chat_id: chat_id, object_id: object_id)
+ |> Repo.preload(:object)
+ end
+
+ def for_chat_query(chat) do
+ from(cr in __MODULE__,
+ where: cr.chat_id == ^chat.id,
+ order_by: [desc: :id],
+ preload: [:object]
+ )
+ end
+
+ def last_message_for_chat(chat) do
+ chat
+ |> for_chat_query()
+ |> limit(1)
+ |> Repo.one()
+ end
+
+ def create(chat, object, unread) do
+ params = %{
+ chat_id: chat.id,
+ object_id: object.id,
+ unread: unread
+ }
+
+ %__MODULE__{}
+ |> changeset(params)
+ |> Repo.insert()
+ end
+
+ def unread_count_for_chat(chat) do
+ chat
+ |> for_chat_query()
+ |> where([cmr], cmr.unread == true)
+ |> Repo.aggregate(:count)
+ end
+
+ def mark_as_read(cm_ref) do
+ cm_ref
+ |> changeset(%{unread: false})
+ |> Repo.update()
+ end
+
+ def set_all_seen_for_chat(chat, last_read_id \\ nil) do
+ query =
+ chat
+ |> for_chat_query()
+ |> exclude(:order_by)
+ |> exclude(:preload)
+ |> where([cmr], cmr.unread == true)
+
+ if last_read_id do
+ query
+ |> where([cmr], cmr.id <= ^last_read_id)
+ else
+ query
+ end
+ |> Repo.update_all(set: [unread: false])
+ end
+end
diff --git a/lib/pleroma/migration_helper/notification_backfill.ex b/lib/pleroma/migration_helper/notification_backfill.ex
new file mode 100644
index 000000000..09647d12a
--- /dev/null
+++ b/lib/pleroma/migration_helper/notification_backfill.ex
@@ -0,0 +1,85 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MigrationHelper.NotificationBackfill do
+ alias Pleroma.Notification
+ alias Pleroma.Object
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ import Ecto.Query
+
+ def fill_in_notification_types do
+ query =
+ from(n in Pleroma.Notification,
+ where: is_nil(n.type),
+ preload: :activity
+ )
+
+ query
+ |> Repo.all()
+ |> Enum.each(fn notification ->
+ type =
+ notification.activity
+ |> type_from_activity()
+
+ notification
+ |> Notification.changeset(%{type: type})
+ |> Repo.update()
+ end)
+ end
+
+ # This is copied over from Notifications to keep this stable.
+ defp type_from_activity(%{data: %{"type" => type}} = activity) do
+ case type do
+ "Follow" ->
+ accepted_function = fn activity ->
+ with %User{} = follower <- User.get_by_ap_id(activity.data["actor"]),
+ %User{} = followed <- User.get_by_ap_id(activity.data["object"]) do
+ Pleroma.FollowingRelationship.following?(follower, followed)
+ end
+ end
+
+ if accepted_function.(activity) do
+ "follow"
+ else
+ "follow_request"
+ end
+
+ "Announce" ->
+ "reblog"
+
+ "Like" ->
+ "favourite"
+
+ "Move" ->
+ "move"
+
+ "EmojiReact" ->
+ "pleroma:emoji_reaction"
+
+ # Compatibility with old reactions
+ "EmojiReaction" ->
+ "pleroma:emoji_reaction"
+
+ "Create" ->
+ activity
+ |> type_from_activity_object()
+
+ t ->
+ raise "No notification type for activity type #{t}"
+ end
+ end
+
+ defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention"
+
+ defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do
+ object = Object.get_by_ap_id(activity.data["object"])
+
+ case object && object.data["type"] do
+ "ChatMessage" -> "pleroma:chat_mention"
+ _ -> "mention"
+ end
+ end
+end
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index 7eca55ac9..3386a1933 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -30,12 +30,29 @@ defmodule Pleroma.Notification do
schema "notifications" do
field(:seen, :boolean, default: false)
+ # This is an enum type in the database. If you add a new notification type,
+ # remember to add a migration to add it to the `notifications_type` enum
+ # as well.
+ field(:type, :string)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
timestamps()
end
+ def update_notification_type(user, activity) do
+ with %__MODULE__{} = notification <-
+ Repo.get_by(__MODULE__, user_id: user.id, activity_id: activity.id) do
+ type =
+ activity
+ |> type_from_activity()
+
+ notification
+ |> changeset(%{type: type})
+ |> Repo.update()
+ end
+ end
+
@spec unread_notifications_count(User.t()) :: integer()
def unread_notifications_count(%User{id: user_id}) do
from(q in __MODULE__,
@@ -44,9 +61,21 @@ defmodule Pleroma.Notification do
|> Repo.aggregate(:count, :id)
end
+ @notification_types ~w{
+ favourite
+ follow
+ follow_request
+ mention
+ move
+ pleroma:chat_mention
+ pleroma:emoji_reaction
+ reblog
+ }
+
def changeset(%Notification{} = notification, attrs) do
notification
- |> cast(attrs, [:seen])
+ |> cast(attrs, [:seen, :type])
+ |> validate_inclusion(:type, @notification_types)
end
@spec last_read_query(User.t()) :: Ecto.Queryable.t()
@@ -300,42 +329,95 @@ defmodule Pleroma.Notification do
end
end
- def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
- object = Object.normalize(activity)
+ def create_notifications(activity, options \\ [])
+
+ def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
+ object = Object.normalize(activity, false)
if object && object.data["type"] == "Answer" do
{:ok, []}
else
- do_create_notifications(activity)
+ do_create_notifications(activity, options)
end
end
- def create_notifications(%Activity{data: %{"type" => type}} = activity)
+ def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
when type in ["Follow", "Like", "Announce", "Move", "EmojiReact"] do
- do_create_notifications(activity)
+ do_create_notifications(activity, options)
end
- def create_notifications(_), do: {:ok, []}
+ def create_notifications(_, _), do: {:ok, []}
+
+ defp do_create_notifications(%Activity{} = activity, options) do
+ do_send = Keyword.get(options, :do_send, true)
- defp do_create_notifications(%Activity{} = activity) do
{enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
potential_receivers = enabled_receivers ++ disabled_receivers
notifications =
Enum.map(potential_receivers, fn user ->
- do_send = user in enabled_receivers
+ do_send = do_send && user in enabled_receivers
create_notification(activity, user, do_send)
end)
{:ok, notifications}
end
+ defp type_from_activity(%{data: %{"type" => type}} = activity) do
+ case type do
+ "Follow" ->
+ if Activity.follow_accepted?(activity) do
+ "follow"
+ else
+ "follow_request"
+ end
+
+ "Announce" ->
+ "reblog"
+
+ "Like" ->
+ "favourite"
+
+ "Move" ->
+ "move"
+
+ "EmojiReact" ->
+ "pleroma:emoji_reaction"
+
+ # Compatibility with old reactions
+ "EmojiReaction" ->
+ "pleroma:emoji_reaction"
+
+ "Create" ->
+ activity
+ |> type_from_activity_object()
+
+ t ->
+ raise "No notification type for activity type #{t}"
+ end
+ end
+
+ defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention"
+
+ defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do
+ object = Object.get_by_ap_id(activity.data["object"])
+
+ case object && object.data["type"] do
+ "ChatMessage" -> "pleroma:chat_mention"
+ _ -> "mention"
+ end
+ end
+
# TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
unless skip?(activity, user) do
{:ok, %{notification: notification}} =
Multi.new()
- |> Multi.insert(:notification, %Notification{user_id: user.id, activity: activity})
+ |> Multi.insert(:notification, %Notification{
+ user_id: user.id,
+ activity: activity,
+ type: type_from_activity(activity)
+ })
|> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction()
@@ -527,4 +609,12 @@ defmodule Pleroma.Notification do
end
def skip?(_, _, _), do: false
+
+ def for_user_and_activity(user, activity) do
+ from(n in __MODULE__,
+ where: n.user_id == ^user.id,
+ where: n.activity_id == ^activity.id
+ )
+ |> Repo.one()
+ end
end
diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex
index 1be1a3a5b..797555bff 100644
--- a/lib/pleroma/upload.ex
+++ b/lib/pleroma/upload.ex
@@ -67,6 +67,7 @@ defmodule Pleroma.Upload do
{:ok,
%{
"type" => opts.activity_type,
+ "mediaType" => upload.content_type,
"url" => [
%{
"type" => "Link",
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index eb73c95fe..aeec4beae 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -112,7 +112,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp increase_poll_votes_if_vote(_create_data), do: :noop
+ @object_types ["ChatMessage"]
@spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
+ def persist(%{"type" => type} = object, meta) when type in @object_types do
+ with {:ok, object} <- Object.create(object) do
+ {:ok, object, meta}
+ end
+ end
+
def persist(object, meta) do
with local <- Keyword.fetch!(meta, :local),
{recipients, _, _} <- get_recipients(object),
@@ -344,20 +351,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
- @spec follow(User.t(), User.t(), String.t() | nil, boolean()) ::
+ @spec follow(User.t(), User.t(), String.t() | nil, boolean(), keyword()) ::
{:ok, Activity.t()} | {:error, any()}
- def follow(follower, followed, activity_id \\ nil, local \\ true) do
+ def follow(follower, followed, activity_id \\ nil, local \\ true, opts \\ []) do
with {:ok, result} <-
- Repo.transaction(fn -> do_follow(follower, followed, activity_id, local) end) do
+ Repo.transaction(fn -> do_follow(follower, followed, activity_id, local, opts) end) do
result
end
end
- defp do_follow(follower, followed, activity_id, local) do
+ defp do_follow(follower, followed, activity_id, local, opts) do
+ skip_notify_and_stream = Keyword.get(opts, :skip_notify_and_stream, false)
data = make_follow_data(follower, followed, activity_id)
with {:ok, activity} <- insert(data, local),
- _ <- notify_and_stream(activity),
+ _ <- skip_notify_and_stream || notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
@@ -1000,6 +1008,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_invisible_actors(query, %{invisible_actors: true}), do: query
defp exclude_invisible_actors(query, _opts) do
@@ -1115,6 +1135,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_instance(opts)
|> Activity.restrict_deactivated_users()
|> exclude_poll_votes(opts)
+ |> exclude_chat_messages(opts)
|> exclude_invisible_actors(opts)
|> exclude_visibility(opts)
end
diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex
index 51b74414a..1aac62c69 100644
--- a/lib/pleroma/web/activity_pub/builder.ex
+++ b/lib/pleroma/web/activity_pub/builder.ex
@@ -5,6 +5,7 @@ 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.Relay
@@ -65,6 +66,42 @@ defmodule Pleroma.Web.ActivityPub.Builder do
}, []}
end
+ def create(actor, object, recipients) do
+ {:ok,
+ %{
+ "id" => Utils.generate_activity_id(),
+ "actor" => actor.ap_id,
+ "to" => recipients,
+ "object" => object,
+ "type" => "Create",
+ "published" => DateTime.utc_now() |> DateTime.to_iso8601()
+ }, []}
+ end
+
+ def chat_message(actor, recipient, content, opts \\ []) do
+ basic = %{
+ "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)
+ }
+
+ case opts[:attachment] do
+ %Object{data: attachment_data} ->
+ {
+ :ok,
+ Map.put(basic, "attachment", attachment_data),
+ []
+ }
+
+ _ ->
+ {:ok, basic, []}
+ end
+ end
+
@spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
def tombstone(actor, id) do
{:ok,
diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex
index 2599067a8..c01c5f780 100644
--- a/lib/pleroma/web/activity_pub/object_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -12,6 +12,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
@@ -43,8 +45,20 @@ 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 = stringify_keys(object |> Map.from_struct())
+ object
+ |> LikeValidator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert) do
+ object = stringify_keys(object)
+ {: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
@@ -59,6 +73,18 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end
end
+ def validate(%{"type" => "Create", "object" => object} = create_activity, meta) do
+ with {:ok, object_data} <- cast_and_apply(object),
+ meta = Keyword.put(meta, :object_data, object_data |> stringify_keys),
+ {:ok, create_activity} <-
+ create_activity
+ |> CreateChatMessageValidator.cast_and_validate(meta)
+ |> Ecto.Changeset.apply_action(:insert) do
+ create_activity = stringify_keys(create_activity)
+ {:ok, create_activity, meta}
+ end
+ end
+
def validate(%{"type" => "Announce"} = object, meta) do
with {:ok, object} <-
object
@@ -69,17 +95,30 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end
end
+ def cast_and_apply(%{"type" => "ChatMessage"} = object) do
+ ChatMessageValidator.cast_and_apply(object)
+ end
+
+ def cast_and_apply(o), do: {:error, {:validator_not_set, o}}
+
def stringify_keys(%{__struct__: _} = object) do
object
|> Map.from_struct()
|> stringify_keys
end
- def stringify_keys(object) do
+ def stringify_keys(object) when is_map(object) do
object
- |> Map.new(fn {key, val} -> {to_string(key), val} end)
+ |> Map.new(fn {key, val} -> {to_string(key), stringify_keys(val)} end)
end
+ def stringify_keys(object) when is_list(object) do
+ object
+ |> Enum.map(&stringify_keys/1)
+ end
+
+ def stringify_keys(object), do: object
+
def fetch_actor(object) do
with {:ok, actor} <- Types.ObjectID.cast(object["actor"]) do
User.get_or_fetch_by_ap_id(actor)
diff --git a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
new file mode 100644
index 000000000..f53bb02be
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
@@ -0,0 +1,80 @@
+# 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.AttachmentValidator do
+ use Ecto.Schema
+
+ alias Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator
+
+ import Ecto.Changeset
+
+ @primary_key false
+ embedded_schema do
+ field(:type, :string)
+ field(:mediaType, :string, default: "application/octet-stream")
+ field(:name, :string)
+
+ embeds_many(:url, UrlObjectValidator)
+ end
+
+ def cast_and_validate(data) do
+ data
+ |> cast_data()
+ |> validate_data()
+ end
+
+ def cast_data(data) do
+ %__MODULE__{}
+ |> changeset(data)
+ end
+
+ def changeset(struct, data) do
+ data =
+ data
+ |> fix_media_type()
+ |> fix_url()
+
+ struct
+ |> cast(data, [:type, :mediaType, :name])
+ |> cast_embed(:url, required: true)
+ end
+
+ def fix_media_type(data) do
+ data =
+ data
+ |> Map.put_new("mediaType", data["mimeType"])
+
+ if MIME.valid?(data["mediaType"]) do
+ data
+ else
+ data
+ |> Map.put("mediaType", "application/octet-stream")
+ end
+ end
+
+ def fix_url(data) do
+ case data["url"] do
+ url when is_binary(url) ->
+ data
+ |> Map.put(
+ "url",
+ [
+ %{
+ "href" => url,
+ "type" => "Link",
+ "mediaType" => data["mediaType"]
+ }
+ ]
+ )
+
+ _ ->
+ data
+ end
+ end
+
+ def validate_data(cng) do
+ cng
+ |> validate_required([:mediaType, :url, :type])
+ end
+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..138736f23
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex
@@ -0,0 +1,123 @@
+# 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.AttachmentValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+
+ import Ecto.Changeset
+ import Pleroma.Web.ActivityPub.Transmogrifier, only: [fix_emoji: 1]
+
+ @primary_key false
+ @derive Jason.Encoder
+
+ embedded_schema do
+ field(:id, Types.ObjectID, primary_key: true)
+ field(:to, Types.Recipients, default: [])
+ field(:type, :string)
+ field(:content, Types.SafeText)
+ field(:actor, Types.ObjectID)
+ field(:published, Types.DateTime)
+ field(:emoji, :map, default: %{})
+
+ embeds_one(:attachment, AttachmentValidator)
+ end
+
+ def cast_and_apply(data) do
+ data
+ |> cast_data
+ |> apply_action(:insert)
+ end
+
+ def cast_and_validate(data) do
+ data
+ |> cast_data()
+ |> validate_data()
+ end
+
+ def cast_data(data) do
+ %__MODULE__{}
+ |> changeset(data)
+ end
+
+ def fix(data) do
+ data
+ |> fix_emoji()
+ |> fix_attachment()
+ |> Map.put_new("actor", data["attributedTo"])
+ end
+
+ # Throws everything but the first one away
+ def fix_attachment(%{"attachment" => [attachment | _]} = data) do
+ data
+ |> Map.put("attachment", attachment)
+ end
+
+ def fix_attachment(data), do: data
+
+ def changeset(struct, data) do
+ data = fix(data)
+
+ struct
+ |> cast(data, List.delete(__schema__(:fields), :attachment))
+ |> cast_embed(:attachment)
+ end
+
+ def validate_data(data_cng) do
+ data_cng
+ |> validate_inclusion(:type, ["ChatMessage"])
+ |> validate_required([:id, :actor, :to, :type, :published])
+ |> validate_content_or_attachment()
+ |> validate_length(:to, is: 1)
+ |> validate_length(:content, max: Pleroma.Config.get([:instance, :remote_limit]))
+ |> validate_local_concern()
+ end
+
+ def validate_content_or_attachment(cng) do
+ attachment = get_field(cng, :attachment)
+
+ if attachment do
+ cng
+ else
+ cng
+ |> validate_required([:content])
+ end
+ end
+
+ @doc """
+ Validates the following
+ - If both users are in our system
+ - If at least one of the users in this ChatMessage is a local user
+ - If the recipient is not blocking the actor
+ """
+ def validate_local_concern(cng) do
+ with actor_ap <- get_field(cng, :actor),
+ {_, %User{} = actor} <- {:find_actor, User.get_cached_by_ap_id(actor_ap)},
+ {_, %User{} = recipient} <-
+ {:find_recipient, User.get_cached_by_ap_id(get_field(cng, :to) |> hd())},
+ {_, false} <- {:blocking_actor?, User.blocks?(recipient, actor)},
+ {_, true} <- {:local?, Enum.any?([actor, recipient], & &1.local)} do
+ cng
+ else
+ {:blocking_actor?, true} ->
+ cng
+ |> add_error(:actor, "actor is blocked by recipient")
+
+ {:local?, false} ->
+ cng
+ |> add_error(:actor, "actor and recipient are both remote")
+
+ {:find_actor, _} ->
+ cng
+ |> add_error(:actor, "can't find user")
+
+ {:find_recipient, _} ->
+ cng
+ |> add_error(:to, "can't find user")
+ end
+ end
+end
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..fc582400b
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/create_chat_message_validator.ex
@@ -0,0 +1,91 @@
+# 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
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator do
+ use Ecto.Schema
+
+ alias Pleroma.Object
+ alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+
+ import Ecto.Changeset
+ import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+ @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
+
+ def cast_and_validate(data, meta \\ []) do
+ cast_data(data)
+ |> validate_data(meta)
+ end
+
+ def validate_data(cng, meta \\ []) do
+ cng
+ |> validate_required([:id, :actor, :to, :type, :object])
+ |> validate_inclusion(:type, ["Create"])
+ |> validate_actor_presence()
+ |> validate_recipients_match(meta)
+ |> validate_actors_match(meta)
+ |> validate_object_nonexistence()
+ end
+
+ def validate_object_nonexistence(cng) do
+ cng
+ |> validate_change(:object, fn :object, object_id ->
+ if Object.get_cached_by_ap_id(object_id) do
+ [{:object, "The object to create already exists"}]
+ else
+ []
+ end
+ end)
+ end
+
+ def validate_actors_match(cng, meta) do
+ object_actor = meta[:object_data]["actor"]
+
+ cng
+ |> validate_change(:actor, fn :actor, actor ->
+ if actor == object_actor do
+ []
+ else
+ [{:actor, "Actor doesn't match with object actor"}]
+ end
+ end)
+ end
+
+ def validate_recipients_match(cng, meta) do
+ object_recipients = meta[:object_data]["to"] || []
+
+ cng
+ |> validate_change(:to, fn :to, recipients ->
+ activity_set = MapSet.new(recipients)
+ object_set = MapSet.new(object_recipients)
+
+ if MapSet.equal?(activity_set, object_set) do
+ []
+ else
+ [{:to, "Recipients don't match with object recipients"}]
+ end
+ end)
+ 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/delete_validator.ex b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
index f42c03510..e5d08eb5c 100644
--- a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
@@ -46,12 +46,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
Answer
Article
Audio
+ ChatMessage
Event
Note
Page
Question
- Video
Tombstone
+ Video
}
def validate_data(cng) do
cng
diff --git a/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex
index 48fe61e1a..408e0f6ee 100644
--- a/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex
+++ b/lib/pleroma/web/activity_pub/object_validators/types/recipients.ex
@@ -11,11 +11,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients do
def cast(data) when is_list(data) do
data
- |> Enum.reduce({:ok, []}, fn element, acc ->
- case {acc, ObjectID.cast(element)} do
- {:error, _} -> :error
- {_, :error} -> :error
- {{:ok, list}, {:ok, id}} -> {:ok, [id | list]}
+ |> Enum.reduce_while({:ok, []}, fn element, {:ok, list} ->
+ case ObjectID.cast(element) do
+ {:ok, id} ->
+ {:cont, {:ok, [id | list]}}
+
+ _ ->
+ {:halt, :error}
end
end)
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex b/lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex
new file mode 100644
index 000000000..95c948123
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/types/safe_text.ex
@@ -0,0 +1,25 @@
+# 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.Types.SafeText do
+ use Ecto.Type
+
+ alias Pleroma.HTML
+
+ def type, do: :string
+
+ def cast(str) when is_binary(str) do
+ {:ok, HTML.filter_tags(str)}
+ end
+
+ def cast(_), do: :error
+
+ def dump(data) do
+ {:ok, data}
+ end
+
+ def load(data) do
+ {:ok, data}
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex b/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex
new file mode 100644
index 000000000..47e231150
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/object_validators/url_object_validator.ex
@@ -0,0 +1,20 @@
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator do
+ use Ecto.Schema
+
+ alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+
+ import Ecto.Changeset
+ @primary_key false
+
+ embedded_schema do
+ field(:type, :string)
+ field(:href, Types.Uri)
+ field(:mediaType, :string)
+ end
+
+ def changeset(struct, data) do
+ struct
+ |> cast(data, __schema__(:fields))
+ |> validate_required([:type, :href, :mediaType])
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex
index 0c54c4b23..6875c47f6 100644
--- a/lib/pleroma/web/activity_pub/pipeline.ex
+++ b/lib/pleroma/web/activity_pub/pipeline.ex
@@ -17,6 +17,10 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
{:ok, Activity.t() | Object.t(), keyword()} | {:error, any()}
def common_pipeline(object, meta) do
case Repo.transaction(fn -> do_common_pipeline(object, meta) end) do
+ {:ok, {:ok, activity, meta}} ->
+ SideEffects.handle_after_transaction(meta)
+ {:ok, activity, meta}
+
{:ok, value} ->
value
diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex
index fb6275450..1a1cc675c 100644
--- a/lib/pleroma/web/activity_pub/side_effects.ex
+++ b/lib/pleroma/web/activity_pub/side_effects.ex
@@ -6,12 +6,17 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
collection, and so on.
"""
alias Pleroma.Activity
+ alias Pleroma.Chat
+ alias Pleroma.Chat.MessageReference
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.Push
+ alias Pleroma.Web.Streamer
def handle(object, meta \\ [])
@@ -27,6 +32,24 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
{:ok, object, meta}
end
+ # Tasks this handles
+ # - Actually create object
+ # - Rollback if we couldn't create it
+ # - Set up notifications
+ def handle(%{data: %{"type" => "Create"}} = activity, meta) do
+ with {:ok, _object, meta} <- handle_object_creation(meta[:object_data], meta) do
+ {:ok, notifications} = Notification.create_notifications(activity, do_send: false)
+
+ meta =
+ meta
+ |> add_notifications(notifications)
+
+ {:ok, activity, meta}
+ else
+ e -> Repo.rollback(e)
+ end
+ end
+
# Tasks this handles:
# - Add announce to object
# - Set up notification
@@ -88,6 +111,8 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
Object.decrease_replies_count(in_reply_to)
end
+ MessageReference.delete_for_object(deleted_object)
+
ActivityPub.stream_out(object)
ActivityPub.stream_out_participations(deleted_object, user)
:ok
@@ -112,6 +137,39 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
{:ok, object, meta}
end
+ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do
+ with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
+ actor = User.get_cached_by_ap_id(object.data["actor"])
+ recipient = User.get_cached_by_ap_id(hd(object.data["to"]))
+
+ streamables =
+ [[actor, recipient], [recipient, actor]]
+ |> Enum.map(fn [user, other_user] ->
+ if user.local do
+ {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
+ {:ok, cm_ref} = MessageReference.create(chat, object, user.ap_id != actor.ap_id)
+
+ {
+ ["user", "user:pleroma_chat"],
+ {user, %{cm_ref | chat: chat, object: object}}
+ }
+ end
+ end)
+ |> Enum.filter(& &1)
+
+ meta =
+ meta
+ |> add_streamables(streamables)
+
+ {:ok, object, meta}
+ end
+ end
+
+ # Nothing to do
+ def handle_object_creation(object) do
+ {:ok, object}
+ end
+
def handle_undoing(%{data: %{"type" => "Like"}} = object) do
with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
{:ok, _} <- Utils.remove_like_from_object(object, liked_object),
@@ -148,4 +206,43 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
end
def handle_undoing(object), do: {:error, ["don't know how to handle", object]}
+
+ defp send_notifications(meta) do
+ Keyword.get(meta, :notifications, [])
+ |> Enum.each(fn notification ->
+ Streamer.stream(["user", "user:notification"], notification)
+ Push.send(notification)
+ end)
+
+ meta
+ end
+
+ defp send_streamables(meta) do
+ Keyword.get(meta, :streamables, [])
+ |> Enum.each(fn {topics, items} ->
+ Streamer.stream(topics, items)
+ end)
+
+ meta
+ end
+
+ defp add_streamables(meta, streamables) do
+ existing = Keyword.get(meta, :streamables, [])
+
+ meta
+ |> Keyword.put(:streamables, streamables ++ existing)
+ end
+
+ defp add_notifications(meta, notifications) do
+ existing = Keyword.get(meta, :notifications, [])
+
+ meta
+ |> Keyword.put(:notifications, notifications ++ existing)
+ end
+
+ def handle_after_transaction(meta) do
+ meta
+ |> send_notifications()
+ |> send_streamables()
+ end
end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 543972ae9..985921aa0 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
alias Pleroma.EarmarkRenderer
alias Pleroma.FollowingRelationship
alias Pleroma.Maps
+ alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Repo
@@ -527,7 +528,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
{:ok, %User{} = follower} <-
User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
- {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
+ {:ok, activity} <-
+ ActivityPub.follow(follower, followed, id, false, skip_notify_and_stream: true) do
with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
{_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
{_, false} <- {:user_locked, User.locked?(followed)},
@@ -570,6 +572,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
:noop
end
+ ActivityPub.notify_and_stream(activity)
{:ok, activity}
else
_e ->
@@ -590,6 +593,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
User.update_follower_count(followed)
User.update_following_count(follower)
+ Notification.update_notification_type(followed, follow_activity)
+
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
@@ -657,6 +662,16 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> handle_incoming(options)
end
+ def handle_incoming(
+ %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
+ _options
+ ) do
+ with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
+ {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
+ {:ok, activity}
+ end
+ end
+
def handle_incoming(%{"type" => type} = data, _options)
when type in ["Like", "EmojiReact", "Announce"] do
with :ok <- ObjectValidator.fetch_actor_and_object(data),
@@ -1108,6 +1123,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
Map.put(object, "attributedTo", attributed_to)
end
+ # TODO: Revisit this
+ def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
+
def prepare_attachments(object) do
attachments =
object
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..cf299bfc2
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/chat_operation.ex
@@ -0,0 +1,355 @@
+# 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
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.Chat
+ alias Pleroma.Web.ApiSpec.Schemas.ChatMessage
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ @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 mark_as_read_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Mark all messages in the chat as read",
+ operationId: "ChatController.mark_as_read",
+ parameters: [Operation.parameter(:id, :path, :string, "The ID of the Chat")],
+ requestBody: request_body("Parameters", mark_as_read()),
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The updated chat",
+ "application/json",
+ Chat
+ )
+ },
+ security: [
+ %{
+ "oAuth" => ["write:chats"]
+ }
+ ]
+ }
+ end
+
+ def mark_message_as_read_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Mark one message in the chat as read",
+ operationId: "ChatController.mark_message_as_read",
+ parameters: [
+ Operation.parameter(:id, :path, :string, "The ID of the Chat"),
+ Operation.parameter(:message_id, :path, :string, "The ID of the message")
+ ],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The read ChatMessage",
+ "application/json",
+ ChatMessage
+ )
+ },
+ security: [
+ %{
+ "oAuth" => ["write:chats"]
+ }
+ ]
+ }
+ end
+
+ def show_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Create a chat",
+ operationId: "ChatController.show",
+ parameters: [
+ Operation.parameter(
+ :id,
+ :path,
+ :string,
+ "The id of the chat",
+ required: true,
+ example: "1234"
+ )
+ ],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The existing chat",
+ "application/json",
+ Chat
+ )
+ },
+ security: [
+ %{
+ "oAuth" => ["read"]
+ }
+ ]
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Create a chat",
+ operationId: "ChatController.create",
+ parameters: [
+ Operation.parameter(
+ :id,
+ :path,
+ :string,
+ "The account id of the recipient of this chat",
+ required: true,
+ example: "someflakeid"
+ )
+ ],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The created or existing chat",
+ "application/json",
+ Chat
+ )
+ },
+ security: [
+ %{
+ "oAuth" => ["write:chats"]
+ }
+ ]
+ }
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Get a list of chats that you participated in",
+ operationId: "ChatController.index",
+ parameters: pagination_params(),
+ responses: %{
+ 200 => Operation.response("The chats of the user", "application/json", chats_response())
+ },
+ security: [
+ %{
+ "oAuth" => ["read:chats"]
+ }
+ ]
+ }
+ end
+
+ def messages_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Get the most recent messages of the chat",
+ operationId: "ChatController.messages",
+ parameters:
+ [Operation.parameter(:id, :path, :string, "The ID of the Chat")] ++
+ pagination_params(),
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The messages in the chat",
+ "application/json",
+ chat_messages_response()
+ )
+ },
+ security: [
+ %{
+ "oAuth" => ["read:chats"]
+ }
+ ]
+ }
+ end
+
+ def post_chat_message_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "Post a message to the chat",
+ operationId: "ChatController.post_chat_message",
+ parameters: [
+ Operation.parameter(:id, :path, :string, "The ID of the Chat")
+ ],
+ requestBody: request_body("Parameters", chat_message_create()),
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The newly created ChatMessage",
+ "application/json",
+ ChatMessage
+ ),
+ 400 => Operation.response("Bad Request", "application/json", ApiError)
+ },
+ security: [
+ %{
+ "oAuth" => ["write:chats"]
+ }
+ ]
+ }
+ end
+
+ def delete_message_operation do
+ %Operation{
+ tags: ["chat"],
+ summary: "delete_message",
+ operationId: "ChatController.delete_message",
+ parameters: [
+ Operation.parameter(:id, :path, :string, "The ID of the Chat"),
+ Operation.parameter(:message_id, :path, :string, "The ID of the message")
+ ],
+ responses: %{
+ 200 =>
+ Operation.response(
+ "The deleted ChatMessage",
+ "application/json",
+ ChatMessage
+ )
+ },
+ security: [
+ %{
+ "oAuth" => ["write:chats"]
+ }
+ ]
+ }
+ end
+
+ def chats_response do
+ %Schema{
+ title: "ChatsResponse",
+ description: "Response schema for multiple Chats",
+ type: :array,
+ items: Chat,
+ example: [
+ %{
+ "account" => %{
+ "pleroma" => %{
+ "is_admin" => false,
+ "confirmation_pending" => false,
+ "hide_followers_count" => false,
+ "is_moderator" => false,
+ "hide_favorites" => true,
+ "ap_id" => "https://dontbulling.me/users/lain",
+ "hide_follows_count" => false,
+ "hide_follows" => false,
+ "background_image" => nil,
+ "skip_thread_containment" => false,
+ "hide_followers" => false,
+ "relationship" => %{},
+ "tags" => []
+ },
+ "avatar" =>
+ "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg",
+ "following_count" => 0,
+ "header_static" => "https://originalpatchou.li/images/banner.png",
+ "source" => %{
+ "sensitive" => false,
+ "note" => "lain",
+ "pleroma" => %{
+ "discoverable" => false,
+ "actor_type" => "Person"
+ },
+ "fields" => []
+ },
+ "statuses_count" => 1,
+ "locked" => false,
+ "created_at" => "2020-04-16T13:40:15.000Z",
+ "display_name" => "lain",
+ "fields" => [],
+ "acct" => "lain@dontbulling.me",
+ "id" => "9u6Qw6TAZANpqokMkK",
+ "emojis" => [],
+ "avatar_static" =>
+ "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg",
+ "username" => "lain",
+ "followers_count" => 0,
+ "header" => "https://originalpatchou.li/images/banner.png",
+ "bot" => false,
+ "note" => "lain",
+ "url" => "https://dontbulling.me/users/lain"
+ },
+ "id" => "1",
+ "unread" => 2
+ }
+ ]
+ }
+ end
+
+ def chat_messages_response do
+ %Schema{
+ title: "ChatMessagesResponse",
+ description: "Response schema for multiple ChatMessages",
+ type: :array,
+ items: ChatMessage,
+ example: [
+ %{
+ "emojis" => [
+ %{
+ "static_url" => "https://dontbulling.me/emoji/Firefox.gif",
+ "visible_in_picker" => false,
+ "shortcode" => "firefox",
+ "url" => "https://dontbulling.me/emoji/Firefox.gif"
+ }
+ ],
+ "created_at" => "2020-04-21T15:11:46.000Z",
+ "content" => "Check this out :firefox:",
+ "id" => "13",
+ "chat_id" => "1",
+ "actor_id" => "someflakeid",
+ "unread" => false
+ },
+ %{
+ "actor_id" => "someflakeid",
+ "content" => "Whats' up?",
+ "id" => "12",
+ "chat_id" => "1",
+ "emojis" => [],
+ "created_at" => "2020-04-21T15:06:45.000Z",
+ "unread" => false
+ }
+ ]
+ }
+ end
+
+ def chat_message_create do
+ %Schema{
+ title: "ChatMessageCreateRequest",
+ description: "POST body for creating an chat message",
+ type: :object,
+ properties: %{
+ content: %Schema{
+ type: :string,
+ description: "The content of your message. Optional if media_id is present"
+ },
+ media_id: %Schema{type: :string, description: "The id of an upload"}
+ },
+ example: %{
+ "content" => "Hey wanna buy feet pics?",
+ "media_id" => "134234"
+ }
+ }
+ end
+
+ def mark_as_read do
+ %Schema{
+ title: "MarkAsReadRequest",
+ description: "POST body for marking a number of chat messages as read",
+ type: :object,
+ required: [:last_read_id],
+ properties: %{
+ last_read_id: %Schema{
+ type: :string,
+ description: "The content of your message."
+ }
+ },
+ example: %{
+ "last_read_id" => "abcdef12456"
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex
index 46e72f8bf..c966b553a 100644
--- a/lib/pleroma/web/api_spec/operations/notification_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex
@@ -185,6 +185,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
"mention",
"poll",
"pleroma:emoji_reaction",
+ "pleroma:chat_mention",
"move",
"follow_request"
],
diff --git a/lib/pleroma/web/api_spec/operations/subscription_operation.ex b/lib/pleroma/web/api_spec/operations/subscription_operation.ex
index c575a87e6..775dd795d 100644
--- a/lib/pleroma/web/api_spec/operations/subscription_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/subscription_operation.ex
@@ -141,6 +141,11 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do
allOf: [BooleanLike],
nullable: true,
description: "Receive poll notifications?"
+ },
+ "pleroma:chat_mention": %Schema{
+ allOf: [BooleanLike],
+ nullable: true,
+ description: "Receive chat notifications?"
}
}
}
diff --git a/lib/pleroma/web/api_spec/schemas/chat.ex b/lib/pleroma/web/api_spec/schemas/chat.ex
new file mode 100644
index 000000000..b4986b734
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/chat.ex
@@ -0,0 +1,75 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.Chat do
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ChatMessage
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "Chat",
+ description: "Response schema for a Chat",
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ account: %Schema{type: :object},
+ unread: %Schema{type: :integer},
+ last_message: ChatMessage,
+ updated_at: %Schema{type: :string, format: :"date-time"}
+ },
+ example: %{
+ "account" => %{
+ "pleroma" => %{
+ "is_admin" => false,
+ "confirmation_pending" => false,
+ "hide_followers_count" => false,
+ "is_moderator" => false,
+ "hide_favorites" => true,
+ "ap_id" => "https://dontbulling.me/users/lain",
+ "hide_follows_count" => false,
+ "hide_follows" => false,
+ "background_image" => nil,
+ "skip_thread_containment" => false,
+ "hide_followers" => false,
+ "relationship" => %{},
+ "tags" => []
+ },
+ "avatar" =>
+ "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg",
+ "following_count" => 0,
+ "header_static" => "https://originalpatchou.li/images/banner.png",
+ "source" => %{
+ "sensitive" => false,
+ "note" => "lain",
+ "pleroma" => %{
+ "discoverable" => false,
+ "actor_type" => "Person"
+ },
+ "fields" => []
+ },
+ "statuses_count" => 1,
+ "locked" => false,
+ "created_at" => "2020-04-16T13:40:15.000Z",
+ "display_name" => "lain",
+ "fields" => [],
+ "acct" => "lain@dontbulling.me",
+ "id" => "9u6Qw6TAZANpqokMkK",
+ "emojis" => [],
+ "avatar_static" =>
+ "https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg",
+ "username" => "lain",
+ "followers_count" => 0,
+ "header" => "https://originalpatchou.li/images/banner.png",
+ "bot" => false,
+ "note" => "lain",
+ "url" => "https://dontbulling.me/users/lain"
+ },
+ "id" => "1",
+ "unread" => 2,
+ "last_message" => ChatMessage.schema().example(),
+ "updated_at" => "2020-04-21T15:06:45.000Z"
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/chat_message.ex b/lib/pleroma/web/api_spec/schemas/chat_message.ex
new file mode 100644
index 000000000..3ee85aa76
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/chat_message.ex
@@ -0,0 +1,41 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
+ alias OpenApiSpex.Schema
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "ChatMessage",
+ description: "Response schema for a ChatMessage",
+ nullable: true,
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ account_id: %Schema{type: :string, description: "The Mastodon API id of the actor"},
+ chat_id: %Schema{type: :string},
+ content: %Schema{type: :string, nullable: true},
+ created_at: %Schema{type: :string, format: :"date-time"},
+ emojis: %Schema{type: :array},
+ attachment: %Schema{type: :object, nullable: true}
+ },
+ example: %{
+ "account_id" => "someflakeid",
+ "chat_id" => "1",
+ "content" => "hey you again",
+ "created_at" => "2020-04-21T15:06:45.000Z",
+ "emojis" => [
+ %{
+ "static_url" => "https://dontbulling.me/emoji/Firefox.gif",
+ "visible_in_picker" => false,
+ "shortcode" => "firefox",
+ "url" => "https://dontbulling.me/emoji/Firefox.gif"
+ }
+ ],
+ "id" => "14",
+ "attachment" => nil
+ }
+ })
+end
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index dbb3d7ade..5a194910d 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.ActivityExpiration
alias Pleroma.Conversation.Participation
alias Pleroma.FollowingRelationship
+ alias Pleroma.Formatter
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.ThreadMute
@@ -24,6 +25,53 @@ defmodule Pleroma.Web.CommonAPI do
require Pleroma.Constants
require Logger
+ def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
+ with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
+ :ok <- validate_chat_content_length(content, !!maybe_attachment),
+ {_, {:ok, chat_message_data, _meta}} <-
+ {:build_object,
+ Builder.chat_message(
+ user,
+ recipient.ap_id,
+ content |> format_chat_content,
+ attachment: maybe_attachment
+ )},
+ {_, {:ok, create_activity_data, _meta}} <-
+ {:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
+ {_, {:ok, %Activity{} = activity, _meta}} <-
+ {:common_pipeline,
+ Pipeline.common_pipeline(create_activity_data,
+ local: true
+ )} do
+ {:ok, activity}
+ end
+ end
+
+ defp format_chat_content(nil), do: nil
+
+ defp format_chat_content(content) do
+ {text, _, _} =
+ content
+ |> Formatter.html_escape("text/plain")
+ |> Formatter.linkify()
+ |> (fn {text, mentions, tags} ->
+ {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
+ end).()
+
+ text
+ end
+
+ defp validate_chat_content_length(_, true), do: :ok
+ defp validate_chat_content_length(nil, false), do: {:error, :no_content}
+
+ defp validate_chat_content_length(content, _) do
+ if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
+ :ok
+ else
+ {:error, :content_too_long}
+ end
+ end
+
def unblock(blocker, blocked) do
with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
{:ok, unblock_data, _} <- Builder.undo(blocker, block),
@@ -73,6 +121,7 @@ defmodule Pleroma.Web.CommonAPI do
object: follow_activity.data["id"],
type: "Accept"
}) do
+ Notification.update_notification_type(followed, follow_activity)
{:ok, follower}
end
end
@@ -427,12 +476,13 @@ defmodule Pleroma.Web.CommonAPI do
{:ok, activity}
end
- def thread_muted?(%{id: nil} = _user, _activity), do: false
-
- def thread_muted?(user, activity) do
- ThreadMute.exists?(user.id, activity.data["context"])
+ def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
+ when is_binary("context") do
+ ThreadMute.exists?(user_id, context)
end
+ def thread_muted?(_, _), do: false
+
def report(user, data) do
with {:ok, account} <- get_reported_account(data.account_id),
{:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 6ec489f9a..15594125f 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -429,7 +429,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
%Activity{data: %{"to" => _to, "type" => type} = data} = activity
)
when type == "Create" do
- object = Object.normalize(activity)
+ object = Object.normalize(activity, false)
object_data =
cond do
diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
index bcd12c73f..e25cef30b 100644
--- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
@@ -42,8 +42,20 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
end
end
+ @default_notification_types ~w{
+ mention
+ follow
+ follow_request
+ reblog
+ favourite
+ move
+ pleroma:emoji_reaction
+ }
def index(%{assigns: %{user: user}} = conn, params) do
- params = Map.new(params, fn {k, v} -> {to_string(k), v} end)
+ params =
+ Map.new(params, fn {k, v} -> {to_string(k), v} end)
+ |> Map.put_new("include_types", @default_notification_types)
+
notifications = MastodonAPI.get_notifications(user, params)
conn
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex
index 70da64a7a..694bf5ca8 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex
@@ -6,7 +6,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
import Ecto.Query
import Ecto.Changeset
- alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Pagination
alias Pleroma.ScheduledActivity
@@ -82,15 +81,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
end
defp restrict(query, :include_types, %{include_types: mastodon_types = [_ | _]}) do
- ap_types = convert_and_filter_mastodon_types(mastodon_types)
-
- where(query, [q, a], fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
+ where(query, [n], n.type in ^mastodon_types)
end
defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do
- ap_types = convert_and_filter_mastodon_types(mastodon_types)
-
- where(query, [q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
+ where(query, [n], n.type not in ^mastodon_types)
end
defp restrict(query, :account_ap_id, %{account_ap_id: account_ap_id}) do
@@ -98,10 +93,4 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
end
defp restrict(query, _, _), do: query
-
- defp convert_and_filter_mastodon_types(types) do
- types
- |> Enum.map(&Activity.from_mastodon_notification_type/1)
- |> Enum.filter(& &1)
- end
end
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 04c419d2f..9fc06bf9d 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -235,6 +235,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/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index 6a630eafa..c498fe632 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -69,7 +69,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
if Config.get([:instance, :safe_dm_mentions]) do
"safe_dm_mentions"
end,
- "pleroma_emoji_reactions"
+ "pleroma_emoji_reactions",
+ "pleroma_chat_messages"
]
|> Enum.filter(& &1)
end
diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex
index c46ddcf55..b11578623 100644
--- a/lib/pleroma/web/mastodon_api/views/notification_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex
@@ -6,26 +6,28 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
use Pleroma.Web, :view
alias Pleroma.Activity
+ alias Pleroma.Chat.MessageReference
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.Chat.MessageReferenceView
+
+ @parent_types ~w{Like Announce EmojiReact}
def render("index.json", %{notifications: notifications, for: reading_user} = opts) do
activities = Enum.map(notifications, & &1.activity)
parent_activities =
activities
- |> Enum.filter(
- &(Activity.mastodon_notification_type(&1) in [
- "favourite",
- "reblog",
- "pleroma:emoji_reaction"
- ])
- )
+ |> Enum.filter(fn
+ %{data: %{"type" => type}} ->
+ type in @parent_types
+ end)
|> Enum.map(& &1.data["object"])
|> Activity.create_by_object_ap_id()
|> Activity.with_preloaded_object(:left)
@@ -42,7 +44,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
true ->
move_activities_targets =
activities
- |> Enum.filter(&(Activity.mastodon_notification_type(&1) == "move"))
+ |> Enum.filter(&(&1.data["type"] == "Move"))
|> Enum.map(&User.get_cached_by_ap_id(&1.data["target"]))
actors =
@@ -79,8 +81,6 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
end
end
- mastodon_type = Activity.mastodon_notification_type(activity)
-
# Note: :relationships contain user mutes (needed for :muted flag in :status)
status_render_opts = %{relationships: opts[:relationships]}
@@ -91,7 +91,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
) do
response = %{
id: to_string(notification.id),
- type: mastodon_type,
+ type: notification.type,
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
account: account,
pleroma: %{
@@ -99,7 +99,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
}
}
- case mastodon_type do
+ case notification.type do
"mention" ->
put_status(response, activity, reading_user, status_render_opts)
@@ -117,6 +117,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|> put_status(parent_activity_fn.(), reading_user, status_render_opts)
|> put_emoji(activity)
+ "pleroma:chat_mention" ->
+ put_chat_message(response, activity, reading_user, status_render_opts)
+
type when type in ["follow", "follow_request"] ->
response
@@ -132,6 +135,17 @@ 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)
+ cm_ref = MessageReference.for_chat_and_object(chat, object)
+ render_opts = Map.merge(opts, %{for: reading_user, chat_message_reference: cm_ref})
+ chat_message_render = MessageReferenceView.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..c8ef3d915
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
@@ -0,0 +1,174 @@
+# 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.Activity
+ alias Pleroma.Chat
+ alias Pleroma.Chat.MessageReference
+ alias Pleroma.Object
+ alias Pleroma.Pagination
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
+ alias Pleroma.Web.PleromaAPI.ChatView
+
+ import Ecto.Query
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:chats"]}
+ when action in [
+ :post_chat_message,
+ :create,
+ :mark_as_read,
+ :mark_message_as_read,
+ :delete_message
+ ]
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:chats"]} when action in [:messages, :index, :show]
+ )
+
+ plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation
+
+ def delete_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{
+ message_id: message_id,
+ id: chat_id
+ }) do
+ with %MessageReference{} = cm_ref <-
+ MessageReference.get_by_id(message_id),
+ ^chat_id <- cm_ref.chat_id |> to_string(),
+ %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id),
+ {:ok, _} <- remove_or_delete(cm_ref, user) do
+ conn
+ |> put_view(MessageReferenceView)
+ |> render("show.json", chat_message_reference: cm_ref)
+ else
+ _e ->
+ {:error, :could_not_delete}
+ end
+ end
+
+ defp remove_or_delete(
+ %{object: %{data: %{"actor" => actor, "id" => id}}},
+ %{ap_id: actor} = user
+ ) do
+ with %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+ CommonAPI.delete(activity.id, user)
+ end
+ end
+
+ defp remove_or_delete(cm_ref, _) do
+ cm_ref
+ |> MessageReference.delete()
+ end
+
+ def post_chat_message(
+ %{body_params: params, assigns: %{user: %{id: user_id} = user}} = conn,
+ %{
+ id: id
+ }
+ ) do
+ with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id),
+ %User{} = recipient <- User.get_cached_by_ap_id(chat.recipient),
+ {:ok, activity} <-
+ CommonAPI.post_chat_message(user, recipient, params[:content],
+ media_id: params[:media_id]
+ ),
+ message <- Object.normalize(activity, false),
+ cm_ref <- MessageReference.for_chat_and_object(chat, message) do
+ conn
+ |> put_view(MessageReferenceView)
+ |> render("show.json", for: user, chat_message_reference: cm_ref)
+ end
+ end
+
+ def mark_message_as_read(%{assigns: %{user: %{id: user_id} = user}} = conn, %{
+ id: chat_id,
+ message_id: message_id
+ }) do
+ with %MessageReference{} = cm_ref <-
+ MessageReference.get_by_id(message_id),
+ ^chat_id <- cm_ref.chat_id |> to_string(),
+ %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id),
+ {:ok, cm_ref} <- MessageReference.mark_as_read(cm_ref) do
+ conn
+ |> put_view(MessageReferenceView)
+ |> render("show.json", for: user, chat_message_reference: cm_ref)
+ end
+ end
+
+ def mark_as_read(
+ %{body_params: %{last_read_id: last_read_id}, assigns: %{user: %{id: user_id}}} = conn,
+ %{id: id}
+ ) do
+ with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id),
+ {_n, _} <-
+ MessageReference.set_all_seen_for_chat(chat, last_read_id) do
+ conn
+ |> put_view(ChatView)
+ |> render("show.json", 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
+ cm_refs =
+ chat
+ |> MessageReference.for_chat_query()
+ |> Pagination.fetch_paginated(params)
+
+ conn
+ |> put_view(MessageReferenceView)
+ |> render("index.json", for: user, chat_message_references: cm_refs)
+ else
+ _ ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{error: "not found"})
+ end
+ end
+
+ def index(%{assigns: %{user: %{id: user_id} = user}} = conn, _params) do
+ blocked_ap_ids = User.blocked_users_ap_ids(user)
+
+ chats =
+ from(c in Chat,
+ where: c.user_id == ^user_id,
+ where: c.recipient not in ^blocked_ap_ids,
+ order_by: [desc: c.updated_at]
+ )
+ |> Repo.all()
+
+ conn
+ |> put_view(ChatView)
+ |> render("index.json", chats: chats)
+ end
+
+ def create(%{assigns: %{user: user}} = conn, params) do
+ with %User{ap_id: recipient} <- User.get_by_id(params[:id]),
+ {:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do
+ conn
+ |> put_view(ChatView)
+ |> render("show.json", chat: chat)
+ end
+ end
+
+ def show(%{assigns: %{user: user}} = conn, params) do
+ with %Chat{} = chat <- Repo.get_by(Chat, user_id: user.id, id: params[:id]) do
+ conn
+ |> put_view(ChatView)
+ |> render("show.json", chat: chat)
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex b/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex
new file mode 100644
index 000000000..f2112a86e
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex
@@ -0,0 +1,45 @@
+# 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.Chat.MessageReferenceView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.MastodonAPI.StatusView
+
+ def render(
+ "show.json",
+ %{
+ chat_message_reference: %{
+ id: id,
+ object: %{data: chat_message},
+ chat_id: chat_id,
+ unread: unread
+ }
+ }
+ ) do
+ %{
+ id: id |> to_string(),
+ content: chat_message["content"],
+ chat_id: chat_id |> to_string(),
+ account_id: User.get_cached_by_ap_id(chat_message["actor"]).id,
+ created_at: Utils.to_masto_date(chat_message["published"]),
+ emojis: StatusView.build_emojis(chat_message["emoji"]),
+ attachment:
+ chat_message["attachment"] &&
+ StatusView.render("attachment.json", attachment: chat_message["attachment"]),
+ unread: unread
+ }
+ end
+
+ def render("index.json", opts) do
+ render_many(
+ opts[:chat_message_references],
+ __MODULE__,
+ "show.json",
+ Map.put(opts, :as, :chat_message_reference)
+ )
+ 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..1c996da11
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/chat_view.ex
@@ -0,0 +1,33 @@
+# 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.Chat.MessageReference
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.MastodonAPI.AccountView
+ alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
+
+ def render("show.json", %{chat: %Chat{} = chat} = opts) do
+ recipient = User.get_cached_by_ap_id(chat.recipient)
+ last_message = opts[:last_message] || MessageReference.last_message_for_chat(chat)
+
+ %{
+ id: chat.id |> to_string(),
+ account: AccountView.render("show.json", Map.put(opts, :user, recipient)),
+ unread: MessageReference.unread_count_for_chat(chat),
+ last_message:
+ last_message &&
+ MessageReferenceView.render("show.json", chat_message_reference: last_message),
+ updated_at: Utils.to_masto_date(chat.updated_at)
+ }
+ end
+
+ def render("index.json", %{chats: chats}) do
+ render_many(chats, __MODULE__, "show.json")
+ end
+end
diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex
index 691725702..cdb827e76 100644
--- a/lib/pleroma/web/push/impl.ex
+++ b/lib/pleroma/web/push/impl.ex
@@ -16,8 +16,6 @@ defmodule Pleroma.Web.Push.Impl do
require Logger
import Ecto.Query
- defdelegate mastodon_notification_type(activity), to: Activity
-
@types ["Create", "Follow", "Announce", "Like", "Move"]
@doc "Performs sending notifications for user subscriptions"
@@ -31,10 +29,10 @@ defmodule Pleroma.Web.Push.Impl do
when activity_type in @types do
actor = User.get_cached_by_ap_id(notification.activity.data["actor"])
- mastodon_type = mastodon_notification_type(notification.activity)
+ mastodon_type = notification.type
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
avatar_url = User.avatar_url(actor)
- object = Object.normalize(activity)
+ object = Object.normalize(activity, false)
user = User.get_cached_by_id(user_id)
direct_conversation_id = Activity.direct_conversation_id(activity, user)
@@ -116,7 +114,7 @@ defmodule Pleroma.Web.Push.Impl do
end
def build_content(notification, actor, object, mastodon_type) do
- mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
+ mastodon_type = mastodon_type || notification.type
%{
title: format_title(notification, mastodon_type),
@@ -126,6 +124,13 @@ defmodule Pleroma.Web.Push.Impl do
def format_body(activity, actor, object, mastodon_type \\ nil)
+ def format_body(_activity, actor, %{data: %{"type" => "ChatMessage", "content" => content}}, _) do
+ case content do
+ nil -> "@#{actor.nickname}: (Attachment)"
+ content -> "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
+ end
+ end
+
def format_body(
%{activity: %{data: %{"type" => "Create"}}},
actor,
@@ -151,7 +156,7 @@ defmodule Pleroma.Web.Push.Impl do
mastodon_type
)
when type in ["Follow", "Like"] do
- mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
+ mastodon_type = mastodon_type || notification.type
case mastodon_type do
"follow" -> "@#{actor.nickname} has followed you"
@@ -166,15 +171,14 @@ defmodule Pleroma.Web.Push.Impl do
"New Direct Message"
end
- def format_title(%{activity: activity}, mastodon_type) do
- mastodon_type = mastodon_type || mastodon_notification_type(activity)
-
- case mastodon_type do
+ def format_title(%{type: type}, mastodon_type) do
+ case mastodon_type || type do
"mention" -> "New Mention"
"follow" -> "New Follower"
"follow_request" -> "New Follow Request"
"reblog" -> "New Repeat"
"favourite" -> "New Favorite"
+ "pleroma:chat_mention" -> "New Chat Message"
type -> "New #{String.capitalize(type || "event")}"
end
end
diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex
index 3e401a490..5b5aa0d59 100644
--- a/lib/pleroma/web/push/subscription.ex
+++ b/lib/pleroma/web/push/subscription.ex
@@ -25,7 +25,7 @@ defmodule Pleroma.Web.Push.Subscription do
timestamps()
end
- @supported_alert_types ~w[follow favourite mention reblog]a
+ @supported_alert_types ~w[follow favourite mention reblog pleroma:chat_mention]a
defp alerts(%{data: %{alerts: alerts}}) do
alerts = Map.take(alerts, @supported_alert_types)
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index aa272540d..57570b672 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -306,6 +306,15 @@ defmodule Pleroma.Web.Router do
scope [] do
pipe_through(:authenticated_api)
+ post("/chats/by-account-id/:id", ChatController, :create)
+ get("/chats", ChatController, :index)
+ get("/chats/:id", ChatController, :show)
+ get("/chats/:id/messages", ChatController, :messages)
+ post("/chats/:id/messages", ChatController, :post_chat_message)
+ delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
+ post("/chats/:id/read", ChatController, :mark_as_read)
+ post("/chats/:id/messages/:message_id/read", ChatController, :mark_message_as_read)
+
get("/conversations/:id/statuses", ConversationController, :statuses)
get("/conversations/:id", ConversationController, :show)
post("/conversations/read", ConversationController, :mark_as_read)
diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex
index 0cf41189b..d1d2c9b9c 100644
--- a/lib/pleroma/web/streamer/streamer.ex
+++ b/lib/pleroma/web/streamer/streamer.ex
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.Streamer do
require Logger
alias Pleroma.Activity
+ alias Pleroma.Chat.MessageReference
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Notification
@@ -22,7 +23,7 @@ defmodule Pleroma.Web.Streamer do
def registry, do: @registry
@public_streams ["public", "public:local", "public:media", "public:local:media"]
- @user_streams ["user", "user:notification", "direct"]
+ @user_streams ["user", "user:notification", "direct", "user:pleroma_chat"]
@doc "Expands and authorizes a stream, and registers the process for streaming."
@spec get_topic_and_add_socket(stream :: String.t(), User.t() | nil, Map.t() | nil) ::
@@ -89,34 +90,20 @@ defmodule Pleroma.Web.Streamer do
if should_env_send?(), do: Registry.unregister(@registry, topic)
end
- def stream(topics, item) when is_list(topics) do
+ def stream(topics, items) do
if should_env_send?() do
- Enum.each(topics, fn t ->
- spawn(fn -> do_stream(t, item) end)
+ List.wrap(topics)
+ |> Enum.each(fn topic ->
+ List.wrap(items)
+ |> Enum.each(fn item ->
+ spawn(fn -> do_stream(topic, item) end)
+ end)
end)
end
:ok
end
- def stream(topic, items) when is_list(items) do
- if should_env_send?() do
- Enum.each(items, fn i ->
- spawn(fn -> do_stream(topic, i) end)
- end)
-
- :ok
- end
- end
-
- def stream(topic, item) do
- if should_env_send?() do
- spawn(fn -> do_stream(topic, item) end)
- end
-
- :ok
- end
-
def filtered_by_user?(%User{} = user, %Activity{} = item) do
%{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} =
User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute])
@@ -200,6 +187,19 @@ defmodule Pleroma.Web.Streamer do
end)
end
+ defp do_stream(topic, {user, %MessageReference{} = cm_ref})
+ when topic in ["user", "user:pleroma_chat"] do
+ topic = "#{topic}:#{user.id}"
+
+ text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref})
+
+ Registry.dispatch(@registry, topic, fn list ->
+ Enum.each(list, fn {pid, _auth} ->
+ send(pid, {:text, text})
+ end)
+ end)
+ end
+
defp do_stream("user", item) do
Logger.debug("Trying to push to users")
diff --git a/lib/pleroma/web/views/streamer_view.ex b/lib/pleroma/web/views/streamer_view.ex
index 237b29ded..476a33245 100644
--- a/lib/pleroma/web/views/streamer_view.ex
+++ b/lib/pleroma/web/views/streamer_view.ex
@@ -51,6 +51,29 @@ defmodule Pleroma.Web.StreamerView do
|> Jason.encode!()
end
+ def render("chat_update.json", %{chat_message_reference: cm_ref}) do
+ # Explicitly giving the cmr for the object here, so we don't accidentally
+ # send a later 'last_message' that was inserted between inserting this and
+ # streaming it out
+ #
+ # It also contains the chat with a cache of the correct unread count
+ Logger.debug("Trying to stream out #{inspect(cm_ref)}")
+
+ representation =
+ Pleroma.Web.PleromaAPI.ChatView.render(
+ "show.json",
+ %{last_message: cm_ref, chat: cm_ref.chat}
+ )
+
+ %{
+ event: "pleroma:chat_update",
+ payload:
+ representation
+ |> Jason.encode!()
+ }
+ |> Jason.encode!()
+ end
+
def render("conversation.json", %Participation{} = participation) do
%{
event: "conversation",
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/priv/repo/migrations/20200602094828_add_type_to_notifications.exs b/priv/repo/migrations/20200602094828_add_type_to_notifications.exs
new file mode 100644
index 000000000..19c733628
--- /dev/null
+++ b/priv/repo/migrations/20200602094828_add_type_to_notifications.exs
@@ -0,0 +1,9 @@
+defmodule Pleroma.Repo.Migrations.AddTypeToNotifications do
+ use Ecto.Migration
+
+ def change do
+ alter table(:notifications) do
+ add(:type, :string)
+ end
+ end
+end
diff --git a/priv/repo/migrations/20200602125218_backfill_notification_types.exs b/priv/repo/migrations/20200602125218_backfill_notification_types.exs
new file mode 100644
index 000000000..996d721ee
--- /dev/null
+++ b/priv/repo/migrations/20200602125218_backfill_notification_types.exs
@@ -0,0 +1,10 @@
+defmodule Pleroma.Repo.Migrations.BackfillNotificationTypes do
+ use Ecto.Migration
+
+ def up do
+ Pleroma.MigrationHelper.NotificationBackfill.fill_in_notification_types()
+ end
+
+ def down do
+ end
+end
diff --git a/priv/repo/migrations/20200602150528_create_chat_message_reference.exs b/priv/repo/migrations/20200602150528_create_chat_message_reference.exs
new file mode 100644
index 000000000..6f9148b7c
--- /dev/null
+++ b/priv/repo/migrations/20200602150528_create_chat_message_reference.exs
@@ -0,0 +1,20 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Repo.Migrations.CreateChatMessageReference do
+ use Ecto.Migration
+
+ def change do
+ create table(:chat_message_references, primary_key: false) do
+ add(:id, :uuid, primary_key: true)
+ add(:chat_id, references(:chats, on_delete: :delete_all), null: false)
+ add(:object_id, references(:objects, on_delete: :delete_all), null: false)
+ add(:seen, :boolean, default: false, null: false)
+
+ timestamps()
+ end
+
+ create(index(:chat_message_references, [:chat_id, "id desc"]))
+ end
+end
diff --git a/priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs b/priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs
new file mode 100644
index 000000000..fdf85132e
--- /dev/null
+++ b/priv/repo/migrations/20200603105113_add_unique_index_to_chat_message_references.exs
@@ -0,0 +1,7 @@
+defmodule Pleroma.Repo.Migrations.AddUniqueIndexToChatMessageReferences do
+ use Ecto.Migration
+
+ def change do
+ create(unique_index(:chat_message_references, [:object_id, :chat_id]))
+ end
+end
diff --git a/priv/repo/migrations/20200603120448_remove_unread_from_chats.exs b/priv/repo/migrations/20200603120448_remove_unread_from_chats.exs
new file mode 100644
index 000000000..6322137d5
--- /dev/null
+++ b/priv/repo/migrations/20200603120448_remove_unread_from_chats.exs
@@ -0,0 +1,9 @@
+defmodule Pleroma.Repo.Migrations.RemoveUnreadFromChats do
+ use Ecto.Migration
+
+ def change do
+ alter table(:chats) do
+ remove(:unread, :integer, default: 0)
+ end
+ end
+end
diff --git a/priv/repo/migrations/20200603122732_add_seen_index_to_chat_message_references.exs b/priv/repo/migrations/20200603122732_add_seen_index_to_chat_message_references.exs
new file mode 100644
index 000000000..a5065d612
--- /dev/null
+++ b/priv/repo/migrations/20200603122732_add_seen_index_to_chat_message_references.exs
@@ -0,0 +1,12 @@
+defmodule Pleroma.Repo.Migrations.AddSeenIndexToChatMessageReferences do
+ use Ecto.Migration
+
+ def change do
+ create(
+ index(:chat_message_references, [:chat_id],
+ where: "seen = false",
+ name: "unseen_messages_count_index"
+ )
+ )
+ end
+end
diff --git a/priv/repo/migrations/20200604150318_migrate_seen_to_unread_in_chat_message_references.exs b/priv/repo/migrations/20200604150318_migrate_seen_to_unread_in_chat_message_references.exs
new file mode 100644
index 000000000..fd6bc7bc7
--- /dev/null
+++ b/priv/repo/migrations/20200604150318_migrate_seen_to_unread_in_chat_message_references.exs
@@ -0,0 +1,30 @@
+defmodule Pleroma.Repo.Migrations.MigrateSeenToUnreadInChatMessageReferences do
+ use Ecto.Migration
+
+ def change do
+ drop(
+ index(:chat_message_references, [:chat_id],
+ where: "seen = false",
+ name: "unseen_messages_count_index"
+ )
+ )
+
+ alter table(:chat_message_references) do
+ add(:unread, :boolean, default: true)
+ end
+
+ execute("update chat_message_references set unread = not seen")
+
+ alter table(:chat_message_references) do
+ modify(:unread, :boolean, default: true, null: false)
+ remove(:seen, :boolean, default: false, null: false)
+ end
+
+ create(
+ index(:chat_message_references, [:chat_id],
+ where: "unread = true",
+ name: "unread_messages_count_index"
+ )
+ )
+ end
+end
diff --git a/priv/repo/migrations/20200606105430_change_type_to_enum_for_notifications.exs b/priv/repo/migrations/20200606105430_change_type_to_enum_for_notifications.exs
new file mode 100644
index 000000000..9ea34436b
--- /dev/null
+++ b/priv/repo/migrations/20200606105430_change_type_to_enum_for_notifications.exs
@@ -0,0 +1,36 @@
+defmodule Pleroma.Repo.Migrations.ChangeTypeToEnumForNotifications do
+ use Ecto.Migration
+
+ def up do
+ """
+ create type notification_type as enum (
+ 'follow',
+ 'follow_request',
+ 'mention',
+ 'move',
+ 'pleroma:emoji_reaction',
+ 'pleroma:chat_mention',
+ 'reblog',
+ 'favourite'
+ )
+ """
+ |> execute()
+
+ """
+ alter table notifications
+ alter column type type notification_type using (type::notification_type)
+ """
+ |> execute()
+ end
+
+ def down do
+ alter table(:notifications) do
+ modify(:type, :string)
+ end
+
+ """
+ drop type notification_type
+ """
+ |> execute()
+ end
+end
diff --git a/priv/repo/migrations/20200607112923_change_chat_id_to_flake.exs b/priv/repo/migrations/20200607112923_change_chat_id_to_flake.exs
new file mode 100644
index 000000000..f14e269ca
--- /dev/null
+++ b/priv/repo/migrations/20200607112923_change_chat_id_to_flake.exs
@@ -0,0 +1,23 @@
+defmodule Pleroma.Repo.Migrations.ChangeChatIdToFlake do
+ use Ecto.Migration
+
+ def up do
+ execute("""
+ alter table chats
+ drop constraint chats_pkey cascade,
+ alter column id drop default,
+ alter column id set data type uuid using cast( lpad( to_hex(id), 32, '0') as uuid),
+ add primary key (id)
+ """)
+
+ execute("""
+ alter table chat_message_references
+ alter column chat_id set data type uuid using cast( lpad( to_hex(chat_id), 32, '0') as uuid),
+ add constraint chat_message_references_chat_id_fkey foreign key (chat_id) references chats(id) on delete cascade
+ """)
+ end
+
+ def down do
+ :ok
+ end
+end
diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld
index 278ad2f96..7cc3fee40 100644
--- a/priv/static/schemas/litepub-0.1.jsonld
+++ b/priv/static/schemas/litepub-0.1.jsonld
@@ -30,6 +30,7 @@
"@type": "@id"
},
"EmojiReact": "litepub:EmojiReact",
+ "ChatMessage": "litepub:ChatMessage",
"alsoKnownAs": {
"@id": "as:alsoKnownAs",
"@type": "@id"
diff --git a/test/chat/message_reference_test.exs b/test/chat/message_reference_test.exs
new file mode 100644
index 000000000..aaa7c1ad4
--- /dev/null
+++ b/test/chat/message_reference_test.exs
@@ -0,0 +1,29 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Chat.MessageReferenceTest do
+ use Pleroma.DataCase, async: true
+
+ alias Pleroma.Chat
+ alias Pleroma.Chat.MessageReference
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ describe "messages" do
+ test "it returns the last message in a chat" do
+ user = insert(:user)
+ recipient = insert(:user)
+
+ {:ok, _message_1} = CommonAPI.post_chat_message(user, recipient, "hey")
+ {:ok, _message_2} = CommonAPI.post_chat_message(recipient, user, "ho")
+
+ {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id)
+
+ message = MessageReference.last_message_for_chat(chat)
+
+ assert message.object.data["content"] == "ho"
+ end
+ end
+end
diff --git a/test/chat_test.exs b/test/chat_test.exs
new file mode 100644
index 000000000..332f2180a
--- /dev/null
+++ b/test/chat_test.exs
@@ -0,0 +1,61 @@
+# 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 and bumps 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 "it returns a chat for a user and recipient if it already exists" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
+ {:ok, chat_two} = Chat.get_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" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
+ :timer.sleep(1500)
+ {:ok, chat_two} = Chat.bump_or_create(user.id, other_user.ap_id)
+
+ 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..9c23a1c9b
--- /dev/null
+++ b/test/fixtures/create-chat-message.json
@@ -0,0 +1,31 @@
+{
+ "actor": "http://2hu.gensokyo/users/raymoo",
+ "id": "http://2hu.gensokyo/objects/1",
+ "object": {
+ "attributedTo": "http://2hu.gensokyo/users/raymoo",
+ "content": "You expected a cute girl? Too bad. <script>alert('XSS')</script>",
+ "id": "http://2hu.gensokyo/objects/2",
+ "published": "2020-02-12T14:08:20Z",
+ "to": [
+ "http://2hu.gensokyo/users/marisa"
+ ],
+ "tag": [
+ {
+ "icon": {
+ "type": "Image",
+ "url": "http://2hu.gensokyo/emoji/Firefox.gif"
+ },
+ "id": "http://2hu.gensokyo/emoji/Firefox.gif",
+ "name": ":firefox:",
+ "type": "Emoji",
+ "updated": "1970-01-01T00:00:00Z"
+ }
+ ],
+ "type": "ChatMessage"
+ },
+ "published": "2018-02-12T14:08:20Z",
+ "to": [
+ "http://2hu.gensokyo/users/marisa"
+ ],
+ "type": "Create"
+}
diff --git a/test/migration_helper/notification_backfill_test.exs b/test/migration_helper/notification_backfill_test.exs
new file mode 100644
index 000000000..2a62a2b00
--- /dev/null
+++ b/test/migration_helper/notification_backfill_test.exs
@@ -0,0 +1,56 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MigrationHelper.NotificationBackfillTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Activity
+ alias Pleroma.MigrationHelper.NotificationBackfill
+ alias Pleroma.Notification
+ alias Pleroma.Repo
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ describe "fill_in_notification_types" do
+ test "it fills in missing notification types" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, post} = CommonAPI.post(user, %{status: "yeah, @#{other_user.nickname}"})
+ {:ok, chat} = CommonAPI.post_chat_message(user, other_user, "yo")
+ {:ok, react} = CommonAPI.react_with_emoji(post.id, other_user, "☕")
+ {:ok, like} = CommonAPI.favorite(other_user, post.id)
+ {:ok, react_2} = CommonAPI.react_with_emoji(post.id, other_user, "☕")
+
+ data =
+ react_2.data
+ |> Map.put("type", "EmojiReaction")
+
+ {:ok, react_2} =
+ react_2
+ |> Activity.change(%{data: data})
+ |> Repo.update()
+
+ assert {5, nil} = Repo.update_all(Notification, set: [type: nil])
+
+ NotificationBackfill.fill_in_notification_types()
+
+ assert %{type: "mention"} =
+ Repo.get_by(Notification, user_id: other_user.id, activity_id: post.id)
+
+ assert %{type: "favourite"} =
+ Repo.get_by(Notification, user_id: user.id, activity_id: like.id)
+
+ assert %{type: "pleroma:emoji_reaction"} =
+ Repo.get_by(Notification, user_id: user.id, activity_id: react.id)
+
+ assert %{type: "pleroma:emoji_reaction"} =
+ Repo.get_by(Notification, user_id: user.id, activity_id: react_2.id)
+
+ assert %{type: "pleroma:chat_mention"} =
+ Repo.get_by(Notification, user_id: other_user.id, activity_id: chat.id)
+ end
+ end
+end
diff --git a/test/notification_test.exs b/test/notification_test.exs
index 37c255fee..b9bbdceca 100644
--- a/test/notification_test.exs
+++ b/test/notification_test.exs
@@ -10,6 +10,7 @@ defmodule Pleroma.NotificationTest do
alias Pleroma.FollowingRelationship
alias Pleroma.Notification
+ alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
@@ -31,6 +32,7 @@ defmodule Pleroma.NotificationTest do
{:ok, [notification]} = Notification.create_notifications(activity)
assert notification.user_id == user.id
+ assert notification.type == "pleroma:emoji_reaction"
end
test "notifies someone when they are directly addressed" do
@@ -48,6 +50,7 @@ defmodule Pleroma.NotificationTest do
notified_ids = Enum.sort([notification.user_id, other_notification.user_id])
assert notified_ids == [other_user.id, third_user.id]
assert notification.activity_id == activity.id
+ assert notification.type == "mention"
assert other_notification.activity_id == activity.id
assert [%Pleroma.Marker{unread_count: 2}] =
@@ -335,9 +338,12 @@ defmodule Pleroma.NotificationTest do
# After request is accepted, the same notification is rendered with type "follow":
assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user)
- notification_id = notification.id
- assert [%{id: ^notification_id}] = Notification.for_user(followed_user)
- assert %{type: "follow"} = NotificationView.render("show.json", render_opts)
+ notification =
+ Repo.get(Notification, notification.id)
+ |> Repo.preload(:activity)
+
+ assert %{type: "follow"} =
+ NotificationView.render("show.json", notification: notification, for: followed_user)
end
test "it doesn't create a notification for follow-unfollow-follow chains" do
diff --git a/test/upload_test.exs b/test/upload_test.exs
index 060a940bb..2abf0edec 100644
--- a/test/upload_test.exs
+++ b/test/upload_test.exs
@@ -54,6 +54,7 @@ defmodule Pleroma.UploadTest do
%{
"name" => "image.jpg",
"type" => "Document",
+ "mediaType" => "image/jpeg",
"url" => [
%{
"href" => "http://localhost:4001/media/post-process-file.jpg",
diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs
index 7953eecf2..31224abe0 100644
--- a/test/web/activity_pub/object_validator_test.exs
+++ b/test/web/activity_pub/object_validator_test.exs
@@ -2,14 +2,264 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
use Pleroma.DataCase
alias Pleroma.Object
+ alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.ObjectValidator
+ alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
+ describe "attachments" do
+ test "works with honkerific attachments" do
+ attachment = %{
+ "mediaType" => "",
+ "name" => "",
+ "summary" => "298p3RG7j27tfsZ9RQ.jpg",
+ "type" => "Document",
+ "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg"
+ }
+
+ assert {:ok, attachment} =
+ AttachmentValidator.cast_and_validate(attachment)
+ |> Ecto.Changeset.apply_action(:insert)
+
+ assert attachment.mediaType == "application/octet-stream"
+ end
+
+ test "it turns mastodon attachments into our attachments" do
+ attachment = %{
+ "url" =>
+ "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg",
+ "type" => "Document",
+ "name" => nil,
+ "mediaType" => "image/jpeg"
+ }
+
+ {:ok, attachment} =
+ AttachmentValidator.cast_and_validate(attachment)
+ |> Ecto.Changeset.apply_action(:insert)
+
+ assert [
+ %{
+ href:
+ "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg",
+ type: "Link",
+ mediaType: "image/jpeg"
+ }
+ ] = attachment.url
+
+ assert attachment.mediaType == "image/jpeg"
+ end
+
+ test "it handles our own uploads" do
+ user = insert(:user)
+
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
+
+ {:ok, attachment} =
+ attachment.data
+ |> AttachmentValidator.cast_and_validate()
+ |> Ecto.Changeset.apply_action(:insert)
+
+ assert attachment.mediaType == "image/jpeg"
+ end
+ end
+
+ describe "chat message create activities" do
+ test "it is invalid if the object already exists" do
+ user = insert(:user)
+ recipient = insert(:user)
+ {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "hey")
+ object = Object.normalize(activity, false)
+
+ {:ok, create_data, _} = Builder.create(user, object.data, [recipient.ap_id])
+
+ {:error, cng} = ObjectValidator.validate(create_data, [])
+
+ assert {:object, {"The object to create already exists", []}} in cng.errors
+ end
+
+ test "it is invalid if the object data has a different `to` or `actor` field" do
+ user = insert(:user)
+ recipient = insert(:user)
+ {:ok, object_data, _} = Builder.chat_message(recipient, user.ap_id, "Hey")
+
+ {:ok, create_data, _} = Builder.create(user, object_data, [recipient.ap_id])
+
+ {:error, cng} = ObjectValidator.validate(create_data, [])
+
+ assert {:to, {"Recipients don't match with object recipients", []}} in cng.errors
+ assert {:actor, {"Actor doesn't match with object actor", []}} in cng.errors
+ end
+ end
+
+ describe "chat messages" do
+ setup do
+ clear_config([:instance, :remote_limit])
+ user = insert(:user)
+ recipient = insert(:user, local: false)
+
+ {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey :firefox:")
+
+ %{user: user, recipient: recipient, valid_chat_message: valid_chat_message}
+ end
+
+ test "let's through some basic html", %{user: user, recipient: recipient} do
+ {:ok, valid_chat_message, _} =
+ Builder.chat_message(
+ user,
+ recipient.ap_id,
+ "hey <a href='https://example.org'>example</a> <script>alert('uguu')</script>"
+ )
+
+ assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+ assert object["content"] ==
+ "hey <a href=\"https://example.org\">example</a> alert(&#39;uguu&#39;)"
+ end
+
+ test "validates for a basic object we build", %{valid_chat_message: valid_chat_message} do
+ assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+ assert Map.put(valid_chat_message, "attachment", nil) == object
+ end
+
+ test "validates for a basic object with an attachment", %{
+ valid_chat_message: valid_chat_message,
+ user: user
+ } do
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
+
+ valid_chat_message =
+ valid_chat_message
+ |> Map.put("attachment", attachment.data)
+
+ assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+ assert object["attachment"]
+ end
+
+ test "validates for a basic object with an attachment in an array", %{
+ valid_chat_message: valid_chat_message,
+ user: user
+ } do
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
+
+ valid_chat_message =
+ valid_chat_message
+ |> Map.put("attachment", [attachment.data])
+
+ assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+ assert object["attachment"]
+ end
+
+ test "validates for a basic object with an attachment but without content", %{
+ valid_chat_message: valid_chat_message,
+ user: user
+ } do
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
+
+ valid_chat_message =
+ valid_chat_message
+ |> Map.put("attachment", attachment.data)
+ |> Map.delete("content")
+
+ assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+ assert object["attachment"]
+ end
+
+ test "does not validate if the message has no content", %{
+ valid_chat_message: valid_chat_message
+ } do
+ contentless =
+ valid_chat_message
+ |> Map.delete("content")
+
+ refute match?({:ok, _object, _meta}, ObjectValidator.validate(contentless, []))
+ end
+
+ test "does not validate if the message is longer than the remote_limit", %{
+ valid_chat_message: valid_chat_message
+ } do
+ Pleroma.Config.put([:instance, :remote_limit], 2)
+ refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, []))
+ end
+
+ test "does not validate if the recipient is blocking the actor", %{
+ valid_chat_message: valid_chat_message,
+ user: user,
+ recipient: recipient
+ } do
+ Pleroma.User.block(recipient, user)
+ refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, []))
+ end
+
+ test "does not validate if the actor or the recipient is not in our system", %{
+ valid_chat_message: valid_chat_message
+ } do
+ chat_message =
+ valid_chat_message
+ |> Map.put("actor", "https://raymoo.com/raymoo")
+
+ {:error, _} = ObjectValidator.validate(chat_message, [])
+
+ chat_message =
+ valid_chat_message
+ |> Map.put("to", ["https://raymoo.com/raymoo"])
+
+ {:error, _} = ObjectValidator.validate(chat_message, [])
+ end
+
+ test "does not validate for a message with multiple recipients", %{
+ valid_chat_message: valid_chat_message,
+ user: user,
+ recipient: recipient
+ } do
+ chat_message =
+ valid_chat_message
+ |> Map.put("to", [user.ap_id, recipient.ap_id])
+
+ assert {:error, _} = ObjectValidator.validate(chat_message, [])
+ end
+
+ test "does not validate if it doesn't concern local users" do
+ user = insert(:user, local: false)
+ recipient = insert(:user, local: false)
+
+ {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey")
+ assert {:error, _} = ObjectValidator.validate(valid_chat_message, [])
+ end
+ end
+
describe "EmojiReacts" do
setup do
user = insert(:user)
diff --git a/test/web/activity_pub/object_validators/types/object_id_test.exs b/test/web/activity_pub/object_validators/types/object_id_test.exs
index 834213182..c8911948e 100644
--- a/test/web/activity_pub/object_validators/types/object_id_test.exs
+++ b/test/web/activity_pub/object_validators/types/object_id_test.exs
@@ -1,3 +1,7 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
defmodule Pleroma.Web.ObjectValidators.Types.ObjectIDTest do
alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID
use Pleroma.DataCase
diff --git a/test/web/activity_pub/object_validators/types/safe_text_test.exs b/test/web/activity_pub/object_validators/types/safe_text_test.exs
new file mode 100644
index 000000000..d4a574554
--- /dev/null
+++ b/test/web/activity_pub/object_validators/types/safe_text_test.exs
@@ -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.ObjectValidators.Types.SafeTextTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Web.ActivityPub.ObjectValidators.Types.SafeText
+
+ test "it lets normal text go through" do
+ text = "hey how are you"
+ assert {:ok, text} == SafeText.cast(text)
+ end
+
+ test "it removes html tags from text" do
+ text = "hey look xss <script>alert('foo')</script>"
+ assert {:ok, "hey look xss alert(&#39;foo&#39;)"} == SafeText.cast(text)
+ end
+
+ test "it keeps basic html tags" do
+ text = "hey <a href='http://gensokyo.2hu'>look</a> xss <script>alert('foo')</script>"
+
+ assert {:ok, "hey <a href=\"http://gensokyo.2hu\">look</a> xss alert(&#39;foo&#39;)"} ==
+ SafeText.cast(text)
+ end
+
+ test "errors for non-text" do
+ assert :error == SafeText.cast(1)
+ end
+end
diff --git a/test/web/activity_pub/pipeline_test.exs b/test/web/activity_pub/pipeline_test.exs
index 26557720b..8deb64501 100644
--- a/test/web/activity_pub/pipeline_test.exs
+++ b/test/web/activity_pub/pipeline_test.exs
@@ -33,7 +33,10 @@ defmodule Pleroma.Web.ActivityPub.PipelineTest do
{
Pleroma.Web.ActivityPub.SideEffects,
[],
- [handle: fn o, m -> {:ok, o, m} end]
+ [
+ handle: fn o, m -> {:ok, o, m} end,
+ handle_after_transaction: fn m -> m end
+ ]
},
{
Pleroma.Web.Federator,
@@ -71,7 +74,7 @@ defmodule Pleroma.Web.ActivityPub.PipelineTest do
{
Pleroma.Web.ActivityPub.SideEffects,
[],
- [handle: fn o, m -> {:ok, o, m} end]
+ [handle: fn o, m -> {:ok, o, m} end, handle_after_transaction: fn m -> m end]
},
{
Pleroma.Web.Federator,
@@ -110,7 +113,7 @@ defmodule Pleroma.Web.ActivityPub.PipelineTest do
{
Pleroma.Web.ActivityPub.SideEffects,
[],
- [handle: fn o, m -> {:ok, o, m} end]
+ [handle: fn o, m -> {:ok, o, m} end, handle_after_transaction: fn m -> m end]
},
{
Pleroma.Web.Federator,
diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs
index a80104ea7..6bbbaae87 100644
--- a/test/web/activity_pub/side_effects_test.exs
+++ b/test/web/activity_pub/side_effects_test.exs
@@ -7,6 +7,8 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
use Pleroma.DataCase
alias Pleroma.Activity
+ alias Pleroma.Chat
+ alias Pleroma.Chat.MessageReference
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
@@ -20,6 +22,48 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
import Pleroma.Factory
import Mock
+ describe "handle_after_transaction" do
+ test "it streams out notifications and streams" do
+ author = insert(:user, local: true)
+ recipient = insert(:user, local: true)
+
+ {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
+
+ {:ok, create_activity_data, _meta} =
+ Builder.create(author, chat_message_data["id"], [recipient.ap_id])
+
+ {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
+
+ {:ok, _create_activity, meta} =
+ SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
+
+ assert [notification] = meta[:notifications]
+
+ with_mocks([
+ {
+ Pleroma.Web.Streamer,
+ [],
+ [
+ stream: fn _, _ -> nil end
+ ]
+ },
+ {
+ Pleroma.Web.Push,
+ [],
+ [
+ send: fn _ -> nil end
+ ]
+ }
+ ]) do
+ SideEffects.handle_after_transaction(meta)
+
+ assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification))
+ assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_))
+ assert called(Pleroma.Web.Push.send(notification))
+ end
+ end
+ end
+
describe "delete objects" do
setup do
user = insert(:user)
@@ -290,6 +334,147 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
end
end
+ describe "creation of ChatMessages" do
+ test "notifies the recipient" do
+ author = insert(:user, local: false)
+ recipient = insert(:user, local: true)
+
+ {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
+
+ {:ok, create_activity_data, _meta} =
+ Builder.create(author, chat_message_data["id"], [recipient.ap_id])
+
+ {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
+
+ {:ok, _create_activity, _meta} =
+ SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
+
+ assert Repo.get_by(Notification, user_id: recipient.id, activity_id: create_activity.id)
+ end
+
+ test "it streams the created ChatMessage" do
+ author = insert(:user, local: true)
+ recipient = insert(:user, local: true)
+
+ {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
+
+ {:ok, create_activity_data, _meta} =
+ Builder.create(author, chat_message_data["id"], [recipient.ap_id])
+
+ {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
+
+ {:ok, _create_activity, meta} =
+ SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
+
+ assert [_, _] = meta[:streamables]
+ end
+
+ test "it creates a Chat and MessageReferences for the local users and bumps the unread count, except for the author" do
+ author = insert(:user, local: true)
+ recipient = insert(:user, local: true)
+
+ {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
+
+ {:ok, create_activity_data, _meta} =
+ Builder.create(author, chat_message_data["id"], [recipient.ap_id])
+
+ {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
+
+ with_mocks([
+ {
+ Pleroma.Web.Streamer,
+ [],
+ [
+ stream: fn _, _ -> nil end
+ ]
+ },
+ {
+ Pleroma.Web.Push,
+ [],
+ [
+ send: fn _ -> nil end
+ ]
+ }
+ ]) do
+ {:ok, _create_activity, meta} =
+ SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
+
+ # The notification gets created
+ assert [notification] = meta[:notifications]
+ assert notification.activity_id == create_activity.id
+
+ # But it is not sent out
+ refute called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification))
+ refute called(Pleroma.Web.Push.send(notification))
+
+ # Same for the user chat stream
+ assert [{topics, _}, _] = meta[:streamables]
+ assert topics == ["user", "user:pleroma_chat"]
+ refute called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_))
+
+ chat = Chat.get(author.id, recipient.ap_id)
+
+ [cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all()
+
+ assert cm_ref.object.data["content"] == "hey"
+ assert cm_ref.unread == false
+
+ chat = Chat.get(recipient.id, author.ap_id)
+
+ [cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all()
+
+ assert cm_ref.object.data["content"] == "hey"
+ assert cm_ref.unread == true
+ end
+ end
+
+ test "it creates a Chat for the local users and bumps the unread count" do
+ author = insert(:user, local: false)
+ recipient = insert(:user, local: true)
+
+ {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
+
+ {:ok, create_activity_data, _meta} =
+ Builder.create(author, chat_message_data["id"], [recipient.ap_id])
+
+ {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
+
+ {:ok, _create_activity, _meta} =
+ SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
+
+ # An object is created
+ assert Object.get_by_ap_id(chat_message_data["id"])
+
+ # 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, create_activity_data, _meta} =
+ Builder.create(author, chat_message_data["id"], [recipient.ap_id])
+
+ {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
+
+ {:ok, _create_activity, _meta} =
+ SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
+
+ # 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
+
describe "announce objects" do
setup do
poster = insert(:user)
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..d6736dc3e
--- /dev/null
+++ b/test/web/activity_pub/transmogrifier/chat_message_test.exs
@@ -0,0 +1,153 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageTest do
+ use Pleroma.DataCase
+
+ import Pleroma.Factory
+
+ alias Pleroma.Activity
+ alias Pleroma.Chat
+ alias Pleroma.Object
+ alias Pleroma.Web.ActivityPub.Transmogrifier
+
+ describe "handle_incoming" do
+ test "handles chonks with attachment" do
+ data = %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "actor" => "https://honk.tedunangst.com/u/tedu",
+ "id" => "https://honk.tedunangst.com/u/tedu/honk/x6gt8X8PcyGkQcXxzg1T",
+ "object" => %{
+ "attachment" => [
+ %{
+ "mediaType" => "image/jpeg",
+ "name" => "298p3RG7j27tfsZ9RQ.jpg",
+ "summary" => "298p3RG7j27tfsZ9RQ.jpg",
+ "type" => "Document",
+ "url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg"
+ }
+ ],
+ "attributedTo" => "https://honk.tedunangst.com/u/tedu",
+ "content" => "",
+ "id" => "https://honk.tedunangst.com/u/tedu/chonk/26L4wl5yCbn4dr4y1b",
+ "published" => "2020-05-18T01:13:03Z",
+ "to" => [
+ "https://dontbulling.me/users/lain"
+ ],
+ "type" => "ChatMessage"
+ },
+ "published" => "2020-05-18T01:13:03Z",
+ "to" => [
+ "https://dontbulling.me/users/lain"
+ ],
+ "type" => "Create"
+ }
+
+ _user = insert(:user, ap_id: data["actor"])
+ _user = insert(:user, ap_id: hd(data["to"]))
+
+ assert {:ok, _activity} = Transmogrifier.handle_incoming(data)
+ end
+
+ test "it rejects messages that don't contain content" do
+ data =
+ File.read!("test/fixtures/create-chat-message.json")
+ |> Poison.decode!()
+
+ object =
+ data["object"]
+ |> Map.delete("content")
+
+ data =
+ data
+ |> Map.put("object", object)
+
+ _author =
+ insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now())
+
+ _recipient =
+ insert(:user,
+ ap_id: List.first(data["to"]),
+ local: true,
+ last_refreshed_at: DateTime.utc_now()
+ )
+
+ {:error, _} = Transmogrifier.handle_incoming(data)
+ end
+
+ test "it rejects messages that don't concern local users" do
+ data =
+ File.read!("test/fixtures/create-chat-message.json")
+ |> Poison.decode!()
+
+ _author =
+ insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now())
+
+ _recipient =
+ insert(:user,
+ ap_id: List.first(data["to"]),
+ local: false,
+ last_refreshed_at: DateTime.utc_now()
+ )
+
+ {:error, _} = Transmogrifier.handle_incoming(data)
+ end
+
+ test "it rejects messages where the `to` field of activity and object don't match" do
+ data =
+ File.read!("test/fixtures/create-chat-message.json")
+ |> Poison.decode!()
+
+ author = insert(:user, ap_id: data["actor"])
+ _recipient = insert(:user, ap_id: List.first(data["to"]))
+
+ data =
+ data
+ |> Map.put("to", author.ap_id)
+
+ assert match?({:error, _}, Transmogrifier.handle_incoming(data))
+ refute Object.get_by_ap_id(data["object"]["id"])
+ end
+
+ test "it fetches the actor if they aren't in our system" do
+ Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+
+ data =
+ File.read!("test/fixtures/create-chat-message.json")
+ |> Poison.decode!()
+ |> Map.put("actor", "http://mastodon.example.org/users/admin")
+ |> put_in(["object", "actor"], "http://mastodon.example.org/users/admin")
+
+ _recipient = insert(:user, ap_id: List.first(data["to"]), local: true)
+
+ {:ok, %Activity{} = _activity} = Transmogrifier.handle_incoming(data)
+ end
+
+ test "it inserts it and creates a chat" do
+ data =
+ File.read!("test/fixtures/create-chat-message.json")
+ |> Poison.decode!()
+
+ author =
+ insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now())
+
+ recipient = insert(:user, ap_id: List.first(data["to"]), local: true)
+
+ {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(data)
+ assert activity.local == false
+
+ assert activity.actor == author.ap_id
+ assert activity.recipients == [recipient.ap_id, author.ap_id]
+
+ %Object{} = object = Object.get_by_ap_id(activity.data["object"])
+
+ assert object
+ assert object.data["content"] == "You expected a cute girl? Too bad. alert(&#39;XSS&#39;)"
+ assert match?(%{"firefox" => _}, object.data["emoji"])
+
+ refute Chat.get(author.id, recipient.ap_id)
+ assert Chat.get(recipient.id, author.ap_id)
+ end
+ end
+end
diff --git a/test/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/web/activity_pub/transmogrifier/follow_handling_test.exs
index 967389fae..06c39eed6 100644
--- a/test/web/activity_pub/transmogrifier/follow_handling_test.exs
+++ b/test/web/activity_pub/transmogrifier/follow_handling_test.exs
@@ -5,6 +5,7 @@
defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
use Pleroma.DataCase
alias Pleroma.Activity
+ alias Pleroma.Notification
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
@@ -12,6 +13,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
import Pleroma.Factory
import Ecto.Query
+ import Mock
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
@@ -57,9 +59,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
activity = Repo.get(Activity, activity.id)
assert activity.data["state"] == "accept"
assert User.following?(User.get_cached_by_ap_id(data["actor"]), user)
+
+ [notification] = Notification.for_user(user)
+ assert notification.type == "follow"
end
- test "with locked accounts, it does not create a follow or an accept" do
+ test "with locked accounts, it does create a Follow, but not an Accept" do
user = insert(:user, locked: true)
data =
@@ -81,6 +86,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
|> Repo.all()
assert Enum.empty?(accepts)
+
+ [notification] = Notification.for_user(user)
+ assert notification.type == "follow_request"
end
test "it works for follow requests when you are already followed, creating a new accept activity" do
@@ -144,6 +152,23 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
assert activity.data["state"] == "reject"
end
+ test "it rejects incoming follow requests if the following errors for some reason" do
+ user = insert(:user)
+
+ data =
+ File.read!("test/fixtures/mastodon-follow-activity.json")
+ |> Poison.decode!()
+ |> Map.put("object", user.ap_id)
+
+ with_mock Pleroma.User, [:passthrough], follow: fn _, _ -> {:error, :testing} end do
+ {:ok, %Activity{data: %{"id" => id}}} = Transmogrifier.handle_incoming(data)
+
+ %Activity{} = activity = Activity.get_by_ap_id(id)
+
+ assert activity.data["state"] == "reject"
+ end
+ end
+
test "it works for incoming follow requests from hubzilla" do
user = insert(:user)
diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs
index 2291f76dd..6bd26050e 100644
--- a/test/web/common_api/common_api_test.exs
+++ b/test/web/common_api/common_api_test.exs
@@ -5,7 +5,9 @@
defmodule Pleroma.Web.CommonAPITest do
use Pleroma.DataCase
alias Pleroma.Activity
+ alias Pleroma.Chat
alias Pleroma.Conversation.Participation
+ alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
@@ -23,6 +25,150 @@ defmodule Pleroma.Web.CommonAPITest do
setup do: clear_config([:instance, :limit])
setup do: clear_config([:instance, :max_pinned_statuses])
+ describe "posting chat messages" do
+ setup do: clear_config([:instance, :chat_limit])
+
+ test "it posts a chat message without content but with an attachment" do
+ author = insert(:user)
+ recipient = insert(:user)
+
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ {:ok, upload} = ActivityPub.upload(file, actor: author.ap_id)
+
+ with_mocks([
+ {
+ Pleroma.Web.Streamer,
+ [],
+ [
+ stream: fn _, _ ->
+ nil
+ end
+ ]
+ },
+ {
+ Pleroma.Web.Push,
+ [],
+ [
+ send: fn _ -> nil end
+ ]
+ }
+ ]) do
+ {:ok, activity} =
+ CommonAPI.post_chat_message(
+ author,
+ recipient,
+ nil,
+ media_id: upload.id
+ )
+
+ notification =
+ Notification.for_user_and_activity(recipient, activity)
+ |> Repo.preload(:activity)
+
+ assert called(Pleroma.Web.Push.send(notification))
+ assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification))
+ assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_))
+
+ assert activity
+ end
+ end
+
+ test "it adds html newlines" do
+ author = insert(:user)
+ recipient = insert(:user)
+
+ other_user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post_chat_message(
+ author,
+ recipient,
+ "uguu\nuguuu"
+ )
+
+ assert other_user.ap_id not in activity.recipients
+
+ object = Object.normalize(activity, false)
+
+ assert object.data["content"] == "uguu<br/>uguuu"
+ end
+
+ test "it linkifies" do
+ author = insert(:user)
+ recipient = insert(:user)
+
+ other_user = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post_chat_message(
+ author,
+ recipient,
+ "https://example.org is the site of @#{other_user.nickname} #2hu"
+ )
+
+ assert other_user.ap_id not in activity.recipients
+
+ object = Object.normalize(activity, false)
+
+ assert object.data["content"] ==
+ "<a href=\"https://example.org\" rel=\"ugc\">https://example.org</a> is the site of <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{
+ other_user.id
+ }\" href=\"#{other_user.ap_id}\" rel=\"ugc\">@<span>#{other_user.nickname}</span></a></span> <a class=\"hashtag\" data-tag=\"2hu\" href=\"http://localhost:4001/tag/2hu\">#2hu</a>"
+ end
+
+ test "it posts a chat message" do
+ author = insert(:user)
+ recipient = insert(:user)
+
+ {:ok, activity} =
+ CommonAPI.post_chat_message(
+ author,
+ recipient,
+ "a test message <script>alert('uuu')</script> :firefox:"
+ )
+
+ assert activity.data["type"] == "Create"
+ assert activity.local
+ object = Object.normalize(activity)
+
+ assert object.data["type"] == "ChatMessage"
+ assert object.data["to"] == [recipient.ap_id]
+
+ assert object.data["content"] ==
+ "a test message &lt;script&gt;alert(&#39;uuu&#39;)&lt;/script&gt; :firefox:"
+
+ assert object.data["emoji"] == %{
+ "firefox" => "http://localhost:4001/emoji/Firefox.gif"
+ }
+
+ assert Chat.get(author.id, recipient.ap_id)
+ assert Chat.get(recipient.id, author.ap_id)
+
+ assert :ok == Pleroma.Web.Federator.perform(:publish, activity)
+ end
+
+ test "it reject messages over the local limit" do
+ Pleroma.Config.put([:instance, :chat_limit], 2)
+
+ author = insert(:user)
+ recipient = insert(:user)
+
+ {:error, message} =
+ CommonAPI.post_chat_message(
+ author,
+ recipient,
+ "123"
+ )
+
+ assert message == :content_too_long
+ end
+ end
+
describe "unblocking" do
test "it works even without an existing block activity" do
blocked = insert(:user)
diff --git a/test/web/mastodon_api/controllers/notification_controller_test.exs b/test/web/mastodon_api/controllers/notification_controller_test.exs
index e278d61f5..698c99711 100644
--- a/test/web/mastodon_api/controllers/notification_controller_test.exs
+++ b/test/web/mastodon_api/controllers/notification_controller_test.exs
@@ -54,6 +54,27 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
assert response == expected_response
end
+ test "by default, does not contain pleroma:chat_mention" do
+ %{user: user, conn: conn} = oauth_access(["read:notifications"])
+ other_user = insert(:user)
+
+ {:ok, _activity} = CommonAPI.post_chat_message(other_user, user, "hey")
+
+ result =
+ conn
+ |> get("/api/v1/notifications")
+ |> json_response_and_validate_schema(200)
+
+ assert [] == result
+
+ result =
+ conn
+ |> get("/api/v1/notifications?include_types[]=pleroma:chat_mention")
+ |> json_response_and_validate_schema(200)
+
+ assert [_] = result
+ end
+
test "getting a single notification" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user = insert(:user)
diff --git a/test/web/mastodon_api/controllers/subscription_controller_test.exs b/test/web/mastodon_api/controllers/subscription_controller_test.exs
index 4aa260663..d36bb1ae8 100644
--- a/test/web/mastodon_api/controllers/subscription_controller_test.exs
+++ b/test/web/mastodon_api/controllers/subscription_controller_test.exs
@@ -58,7 +58,9 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do
result =
conn
|> post("/api/v1/push/subscription", %{
- "data" => %{"alerts" => %{"mention" => true, "test" => true}},
+ "data" => %{
+ "alerts" => %{"mention" => true, "test" => true, "pleroma:chat_mention" => true}
+ },
"subscription" => @sub
})
|> json_response_and_validate_schema(200)
@@ -66,7 +68,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do
[subscription] = Pleroma.Repo.all(Subscription)
assert %{
- "alerts" => %{"mention" => true},
+ "alerts" => %{"mention" => true, "pleroma:chat_mention" => true},
"endpoint" => subscription.endpoint,
"id" => to_string(subscription.id),
"server_key" => @server_key
diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs
index f91333e5c..044f088a4 100644
--- a/test/web/mastodon_api/views/account_view_test.exs
+++ b/test/web/mastodon_api/views/account_view_test.exs
@@ -72,6 +72,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
fields: []
},
pleroma: %{
+ ap_id: user.ap_id,
background_image: "https://example.com/images/asuka_hospital.png",
confirmation_pending: false,
tags: [],
@@ -148,6 +149,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
fields: []
},
pleroma: %{
+ ap_id: user.ap_id,
background_image: nil,
confirmation_pending: false,
tags: [],
diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs
index f15be1df1..b2fa5b302 100644
--- a/test/web/mastodon_api/views/notification_view_test.exs
+++ b/test/web/mastodon_api/views/notification_view_test.exs
@@ -6,7 +6,10 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
use Pleroma.DataCase
alias Pleroma.Activity
+ alias Pleroma.Chat
+ alias Pleroma.Chat.MessageReference
alias Pleroma.Notification
+ alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
@@ -14,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
import Pleroma.Factory
defp test_notifications_rendering(notifications, user, expected_result) do
@@ -31,6 +35,30 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
assert expected_result == result
end
+ test "ChatMessage notification" do
+ user = insert(:user)
+ recipient = insert(:user)
+ {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "what's up my dude")
+
+ {:ok, [notification]} = Notification.create_notifications(activity)
+
+ object = Object.normalize(activity)
+ chat = Chat.get(recipient.id, user.ap_id)
+
+ cm_ref = MessageReference.for_chat_and_object(chat, object)
+
+ expected = %{
+ id: to_string(notification.id),
+ pleroma: %{is_seen: false},
+ type: "pleroma:chat_mention",
+ account: AccountView.render("show.json", %{user: user, for: recipient}),
+ chat_message: MessageReferenceView.render("show.json", %{chat_message_reference: cm_ref}),
+ created_at: Utils.to_masto_date(notification.inserted_at)
+ }
+
+ test_notifications_rendering([notification], recipient, [expected])
+ end
+
test "Mention notification" do
user = insert(:user)
mentioned_user = insert(:user)
diff --git a/test/web/node_info_test.exs b/test/web/node_info_test.exs
index 9bcc07b37..00925caad 100644
--- a/test/web/node_info_test.exs
+++ b/test/web/node_info_test.exs
@@ -145,7 +145,8 @@ defmodule Pleroma.Web.NodeInfoTest do
"shareable_emoji_packs",
"multifetch",
"pleroma_emoji_reactions",
- "pleroma:api/v1/notifications:include_types_filter"
+ "pleroma:api/v1/notifications:include_types_filter",
+ "pleroma_chat_messages"
]
assert MapSet.subset?(
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..82e16741d
--- /dev/null
+++ b/test/web/pleroma_api/controllers/chat_controller_test.exs
@@ -0,0 +1,336 @@
+# 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.Chat.MessageReference
+ alias Pleroma.Object
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ describe "POST /api/v1/pleroma/chats/:id/messages/:message_id/read" do
+ setup do: oauth_access(["write:chats"])
+
+ test "it marks one message as read", %{conn: conn, user: user} do
+ other_user = insert(:user)
+
+ {:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup")
+ {:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2")
+ {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
+ object = Object.normalize(create, false)
+ cm_ref = MessageReference.for_chat_and_object(chat, object)
+
+ assert cm_ref.unread == true
+
+ result =
+ conn
+ |> post("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}/read")
+ |> json_response_and_validate_schema(200)
+
+ assert result["unread"] == false
+
+ cm_ref = MessageReference.for_chat_and_object(chat, object)
+
+ assert cm_ref.unread == false
+ end
+ end
+
+ describe "POST /api/v1/pleroma/chats/:id/read" do
+ setup do: oauth_access(["write:chats"])
+
+ test "given a `last_read_id`, it marks everything until then as read", %{
+ conn: conn,
+ user: user
+ } do
+ other_user = insert(:user)
+
+ {:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup")
+ {:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2")
+ {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
+ object = Object.normalize(create, false)
+ cm_ref = MessageReference.for_chat_and_object(chat, object)
+
+ assert cm_ref.unread == true
+
+ result =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/pleroma/chats/#{chat.id}/read", %{"last_read_id" => cm_ref.id})
+ |> json_response_and_validate_schema(200)
+
+ assert result["unread"] == 1
+
+ cm_ref = MessageReference.for_chat_and_object(chat, object)
+
+ assert cm_ref.unread == false
+ end
+ end
+
+ describe "POST /api/v1/pleroma/chats/:id/messages" do
+ setup do: oauth_access(["write:chats"])
+
+ test "it posts a message to the chat", %{conn: conn, user: user} do
+ other_user = insert(:user)
+
+ {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
+
+ result =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "Hallo!!"})
+ |> json_response_and_validate_schema(200)
+
+ assert result["content"] == "Hallo!!"
+ assert result["chat_id"] == chat.id |> to_string()
+ end
+
+ test "it fails if there is no content", %{conn: conn, user: user} do
+ other_user = insert(:user)
+
+ {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
+
+ result =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/pleroma/chats/#{chat.id}/messages")
+ |> json_response_and_validate_schema(400)
+
+ assert result
+ end
+
+ test "it works with an attachment", %{conn: conn, user: user} do
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
+
+ other_user = insert(:user)
+
+ {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
+
+ result =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{
+ "media_id" => to_string(upload.id)
+ })
+ |> json_response_and_validate_schema(200)
+
+ assert result["attachment"]
+ end
+ end
+
+ describe "DELETE /api/v1/pleroma/chats/:id/messages/:message_id" do
+ setup do: oauth_access(["write:chats"])
+
+ test "it deletes a message from the chat", %{conn: conn, user: user} do
+ recipient = insert(:user)
+
+ {:ok, message} =
+ CommonAPI.post_chat_message(user, recipient, "Hello darkness my old friend")
+
+ {:ok, other_message} = CommonAPI.post_chat_message(recipient, user, "nico nico ni")
+
+ object = Object.normalize(message, false)
+
+ chat = Chat.get(user.id, recipient.ap_id)
+
+ cm_ref = MessageReference.for_chat_and_object(chat, object)
+
+ # Deleting your own message removes the message and the reference
+ result =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}")
+ |> json_response_and_validate_schema(200)
+
+ assert result["id"] == cm_ref.id
+ refute MessageReference.get_by_id(cm_ref.id)
+ assert %{data: %{"type" => "Tombstone"}} = Object.get_by_id(object.id)
+
+ # Deleting other people's messages just removes the reference
+ object = Object.normalize(other_message, false)
+ cm_ref = MessageReference.for_chat_and_object(chat, object)
+
+ result =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}")
+ |> json_response_and_validate_schema(200)
+
+ assert result["id"] == cm_ref.id
+ refute MessageReference.get_by_id(cm_ref.id)
+ assert Object.get_by_id(object.id)
+ end
+ end
+
+ describe "GET /api/v1/pleroma/chats/:id/messages" do
+ setup do: oauth_access(["read:chats"])
+
+ test "it paginates", %{conn: conn, user: user} do
+ recipient = insert(:user)
+
+ Enum.each(1..30, fn _ ->
+ {:ok, _} = CommonAPI.post_chat_message(user, recipient, "hey")
+ end)
+
+ chat = Chat.get(user.id, recipient.ap_id)
+
+ result =
+ conn
+ |> get("/api/v1/pleroma/chats/#{chat.id}/messages")
+ |> json_response_and_validate_schema(200)
+
+ assert length(result) == 20
+
+ result =
+ conn
+ |> get("/api/v1/pleroma/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}")
+ |> json_response_and_validate_schema(200)
+
+ assert length(result) == 10
+ end
+
+ test "it returns the messages for a given chat", %{conn: conn, user: user} do
+ other_user = insert(:user)
+ third_user = insert(:user)
+
+ {:ok, _} = CommonAPI.post_chat_message(user, other_user, "hey")
+ {:ok, _} = CommonAPI.post_chat_message(user, third_user, "hey")
+ {:ok, _} = CommonAPI.post_chat_message(user, other_user, "how are you?")
+ {:ok, _} = CommonAPI.post_chat_message(other_user, user, "fine, how about you?")
+
+ chat = Chat.get(user.id, other_user.ap_id)
+
+ result =
+ conn
+ |> get("/api/v1/pleroma/chats/#{chat.id}/messages")
+ |> json_response_and_validate_schema(200)
+
+ result
+ |> Enum.each(fn message ->
+ assert message["chat_id"] == chat.id |> to_string()
+ end)
+
+ assert length(result) == 3
+
+ # Trying to get the chat of a different user
+ result =
+ conn
+ |> assign(:user, other_user)
+ |> get("/api/v1/pleroma/chats/#{chat.id}/messages")
+
+ assert result |> json_response(404)
+ end
+ end
+
+ describe "POST /api/v1/pleroma/chats/by-account-id/:id" do
+ setup do: oauth_access(["write:chats"])
+
+ test "it creates or returns a chat", %{conn: conn} do
+ other_user = insert(:user)
+
+ result =
+ conn
+ |> post("/api/v1/pleroma/chats/by-account-id/#{other_user.id}")
+ |> json_response_and_validate_schema(200)
+
+ assert result["id"]
+ end
+ end
+
+ describe "GET /api/v1/pleroma/chats/:id" do
+ setup do: oauth_access(["read:chats"])
+
+ test "it returns a chat", %{conn: conn, user: user} do
+ other_user = insert(:user)
+
+ {:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
+
+ result =
+ conn
+ |> get("/api/v1/pleroma/chats/#{chat.id}")
+ |> json_response_and_validate_schema(200)
+
+ assert result["id"] == to_string(chat.id)
+ end
+ end
+
+ describe "GET /api/v1/pleroma/chats" do
+ setup do: oauth_access(["read:chats"])
+
+ test "it does not return chats with users you blocked", %{conn: conn, user: user} do
+ recipient = insert(:user)
+
+ {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id)
+
+ result =
+ conn
+ |> get("/api/v1/pleroma/chats")
+ |> json_response_and_validate_schema(200)
+
+ assert length(result) == 1
+
+ User.block(user, recipient)
+
+ result =
+ conn
+ |> get("/api/v1/pleroma/chats")
+ |> json_response_and_validate_schema(200)
+
+ assert length(result) == 0
+ end
+
+ test "it returns all chats", %{conn: conn, user: user} do
+ Enum.each(1..30, fn _ ->
+ recipient = insert(:user)
+ {:ok, _} = Chat.get_or_create(user.id, recipient.ap_id)
+ end)
+
+ result =
+ conn
+ |> get("/api/v1/pleroma/chats")
+ |> json_response_and_validate_schema(200)
+
+ assert length(result) == 30
+ end
+
+ test "it return a list of chats the current user is participating in, in descending order of updates",
+ %{conn: conn, user: user} do
+ har = insert(:user)
+ jafnhar = insert(:user)
+ tridi = insert(:user)
+
+ {:ok, chat_1} = Chat.get_or_create(user.id, har.ap_id)
+ :timer.sleep(1000)
+ {:ok, _chat_2} = Chat.get_or_create(user.id, jafnhar.ap_id)
+ :timer.sleep(1000)
+ {:ok, chat_3} = Chat.get_or_create(user.id, tridi.ap_id)
+ :timer.sleep(1000)
+
+ # bump the second one
+ {:ok, chat_2} = Chat.bump_or_create(user.id, jafnhar.ap_id)
+
+ result =
+ conn
+ |> get("/api/v1/pleroma/chats")
+ |> json_response_and_validate_schema(200)
+
+ ids = Enum.map(result, & &1["id"])
+
+ assert ids == [
+ chat_2.id |> to_string(),
+ chat_3.id |> to_string(),
+ chat_1.id |> to_string()
+ ]
+ end
+ end
+end
diff --git a/test/web/pleroma_api/views/chat/message_reference_view_test.exs b/test/web/pleroma_api/views/chat/message_reference_view_test.exs
new file mode 100644
index 000000000..e5b165255
--- /dev/null
+++ b/test/web/pleroma_api/views/chat/message_reference_view_test.exs
@@ -0,0 +1,61 @@
+# 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.Chat.MessageReferenceViewTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Chat
+ alias Pleroma.Chat.MessageReference
+ alias Pleroma.Object
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
+
+ import Pleroma.Factory
+
+ test "it displays a chat message" do
+ user = insert(:user)
+ recipient = insert(:user)
+
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
+ {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "kippis :firefox:")
+
+ chat = Chat.get(user.id, recipient.ap_id)
+
+ object = Object.normalize(activity)
+
+ cm_ref = MessageReference.for_chat_and_object(chat, object)
+
+ chat_message = MessageReferenceView.render("show.json", chat_message_reference: cm_ref)
+
+ assert chat_message[:id] == cm_ref.id
+ assert chat_message[:content] == "kippis :firefox:"
+ assert chat_message[:account_id] == user.id
+ assert chat_message[:chat_id]
+ assert chat_message[:created_at]
+ assert chat_message[:unread] == false
+ assert match?([%{shortcode: "firefox"}], chat_message[:emojis])
+
+ {:ok, activity} = CommonAPI.post_chat_message(recipient, user, "gkgkgk", media_id: upload.id)
+
+ object = Object.normalize(activity)
+
+ cm_ref = MessageReference.for_chat_and_object(chat, object)
+
+ chat_message_two = MessageReferenceView.render("show.json", chat_message_reference: cm_ref)
+
+ assert chat_message_two[:id] == cm_ref.id
+ assert chat_message_two[:content] == "gkgkgk"
+ assert chat_message_two[:account_id] == recipient.id
+ assert chat_message_two[:chat_id] == chat_message[:chat_id]
+ assert chat_message_two[:attachment]
+ assert chat_message_two[:unread] == true
+ end
+end
diff --git a/test/web/pleroma_api/views/chat_view_test.exs b/test/web/pleroma_api/views/chat_view_test.exs
new file mode 100644
index 000000000..14eecb1bd
--- /dev/null
+++ b/test/web/pleroma_api/views/chat_view_test.exs
@@ -0,0 +1,48 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.ChatViewTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Chat
+ alias Pleroma.Chat.MessageReference
+ alias Pleroma.Object
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.MastodonAPI.AccountView
+ alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
+ alias Pleroma.Web.PleromaAPI.ChatView
+
+ import Pleroma.Factory
+
+ test "it represents a chat" do
+ user = insert(:user)
+ recipient = insert(:user)
+
+ {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id)
+
+ represented_chat = ChatView.render("show.json", chat: chat)
+
+ assert represented_chat == %{
+ id: "#{chat.id}",
+ account: AccountView.render("show.json", user: recipient),
+ unread: 0,
+ last_message: nil,
+ updated_at: Utils.to_masto_date(chat.updated_at)
+ }
+
+ {:ok, chat_message_creation} = CommonAPI.post_chat_message(user, recipient, "hello")
+
+ chat_message = Object.normalize(chat_message_creation, false)
+
+ {:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id)
+
+ represented_chat = ChatView.render("show.json", chat: chat)
+
+ cm_ref = MessageReference.for_chat_and_object(chat, chat_message)
+
+ assert represented_chat[:last_message] ==
+ MessageReferenceView.render("show.json", chat_message_reference: cm_ref)
+ end
+end
diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs
index a826b24c9..b48952b29 100644
--- a/test/web/push/impl_test.exs
+++ b/test/web/push/impl_test.exs
@@ -5,8 +5,10 @@
defmodule Pleroma.Web.Push.ImplTest do
use Pleroma.DataCase
+ alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Push.Impl
alias Pleroma.Web.Push.Subscription
@@ -60,7 +62,8 @@ defmodule Pleroma.Web.Push.ImplTest do
notif =
insert(:notification,
user: user,
- activity: activity
+ activity: activity,
+ type: "mention"
)
assert Impl.perform(notif) == {:ok, [:ok, :ok]}
@@ -126,7 +129,7 @@ defmodule Pleroma.Web.Push.ImplTest do
) ==
"@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..."
- assert Impl.format_title(%{activity: activity}) ==
+ assert Impl.format_title(%{activity: activity, type: "mention"}) ==
"New Mention"
end
@@ -136,9 +139,10 @@ defmodule Pleroma.Web.Push.ImplTest do
{:ok, _, _, activity} = CommonAPI.follow(user, other_user)
object = Object.normalize(activity, false)
- assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has followed you"
+ assert Impl.format_body(%{activity: activity, type: "follow"}, user, object) ==
+ "@Bob has followed you"
- assert Impl.format_title(%{activity: activity}) ==
+ assert Impl.format_title(%{activity: activity, type: "follow"}) ==
"New Follower"
end
@@ -157,7 +161,7 @@ defmodule Pleroma.Web.Push.ImplTest do
assert Impl.format_body(%{activity: announce_activity}, user, object) ==
"@#{user.nickname} repeated: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..."
- assert Impl.format_title(%{activity: announce_activity}) ==
+ assert Impl.format_title(%{activity: announce_activity, type: "reblog"}) ==
"New Repeat"
end
@@ -173,9 +177,10 @@ defmodule Pleroma.Web.Push.ImplTest do
{:ok, activity} = CommonAPI.favorite(user, activity.id)
object = Object.normalize(activity)
- assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has favorited your post"
+ assert Impl.format_body(%{activity: activity, type: "favourite"}, user, object) ==
+ "@Bob has favorited your post"
- assert Impl.format_title(%{activity: activity}) ==
+ assert Impl.format_title(%{activity: activity, type: "favourite"}) ==
"New Favorite"
end
@@ -193,6 +198,46 @@ defmodule Pleroma.Web.Push.ImplTest do
end
describe "build_content/3" do
+ test "builds content for chat messages" do
+ user = insert(:user)
+ recipient = insert(:user)
+
+ {:ok, chat} = CommonAPI.post_chat_message(user, recipient, "hey")
+ object = Object.normalize(chat, false)
+ [notification] = Notification.for_user(recipient)
+
+ res = Impl.build_content(notification, user, object)
+
+ assert res == %{
+ body: "@#{user.nickname}: hey",
+ title: "New Chat Message"
+ }
+ end
+
+ test "builds content for chat messages with no content" do
+ user = insert(:user)
+ recipient = insert(:user)
+
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
+
+ {:ok, chat} = CommonAPI.post_chat_message(user, recipient, nil, media_id: upload.id)
+ object = Object.normalize(chat, false)
+ [notification] = Notification.for_user(recipient)
+
+ res = Impl.build_content(notification, user, object)
+
+ assert res == %{
+ body: "@#{user.nickname}: (Attachment)",
+ title: "New Chat Message"
+ }
+ end
+
test "hides details for notifications when privacy option enabled" do
user = insert(:user, nickname: "Bob")
user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: true})
@@ -218,7 +263,7 @@ defmodule Pleroma.Web.Push.ImplTest do
status: "<Lorem ipsum dolor sit amet."
})
- notif = insert(:notification, user: user2, activity: activity)
+ notif = insert(:notification, user: user2, activity: activity, type: "mention")
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
object = Object.normalize(activity)
@@ -229,7 +274,7 @@ defmodule Pleroma.Web.Push.ImplTest do
{:ok, activity} = CommonAPI.favorite(user, activity.id)
- notif = insert(:notification, user: user2, activity: activity)
+ notif = insert(:notification, user: user2, activity: activity, type: "favourite")
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
object = Object.normalize(activity)
@@ -268,7 +313,7 @@ defmodule Pleroma.Web.Push.ImplTest do
"<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis."
})
- notif = insert(:notification, user: user2, activity: activity)
+ notif = insert(:notification, user: user2, activity: activity, type: "mention")
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
object = Object.normalize(activity)
@@ -281,7 +326,7 @@ defmodule Pleroma.Web.Push.ImplTest do
{:ok, activity} = CommonAPI.favorite(user, activity.id)
- notif = insert(:notification, user: user2, activity: activity)
+ notif = insert(:notification, user: user2, activity: activity, type: "favourite")
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
object = Object.normalize(activity)
diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs
index 3f012259a..245f6e63f 100644
--- a/test/web/streamer/streamer_test.exs
+++ b/test/web/streamer/streamer_test.exs
@@ -7,11 +7,15 @@ defmodule Pleroma.Web.StreamerTest do
import Pleroma.Factory
+ alias Pleroma.Chat
+ alias Pleroma.Chat.MessageReference
alias Pleroma.Conversation.Participation
alias Pleroma.List
+ alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Streamer
+ alias Pleroma.Web.StreamerView
@moduletag needs_streamer: true, capture_log: true
@@ -145,6 +149,57 @@ defmodule Pleroma.Web.StreamerTest do
refute Streamer.filtered_by_user?(user, notify)
end
+ test "it sends chat messages to the 'user:pleroma_chat' stream", %{user: user} do
+ other_user = insert(:user)
+
+ {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno")
+ object = Object.normalize(create_activity, false)
+ chat = Chat.get(user.id, other_user.ap_id)
+ cm_ref = MessageReference.for_chat_and_object(chat, object)
+ cm_ref = %{cm_ref | chat: chat, object: object}
+
+ Streamer.get_topic_and_add_socket("user:pleroma_chat", user)
+ Streamer.stream("user:pleroma_chat", {user, cm_ref})
+
+ text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref})
+
+ assert text =~ "hey cirno"
+ assert_receive {:text, ^text}
+ end
+
+ test "it sends chat messages to the 'user' stream", %{user: user} do
+ other_user = insert(:user)
+
+ {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno")
+ object = Object.normalize(create_activity, false)
+ chat = Chat.get(user.id, other_user.ap_id)
+ cm_ref = MessageReference.for_chat_and_object(chat, object)
+ cm_ref = %{cm_ref | chat: chat, object: object}
+
+ Streamer.get_topic_and_add_socket("user", user)
+ Streamer.stream("user", {user, cm_ref})
+
+ text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref})
+
+ assert text =~ "hey cirno"
+ assert_receive {:text, ^text}
+ end
+
+ test "it sends chat message notifications to the 'user:notification' stream", %{user: user} do
+ other_user = insert(:user)
+
+ {:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey")
+
+ notify =
+ Repo.get_by(Pleroma.Notification, user_id: user.id, activity_id: create_activity.id)
+ |> Repo.preload(:activity)
+
+ Streamer.get_topic_and_add_socket("user:notification", user)
+ Streamer.stream("user:notification", notify)
+ assert_receive {:render_with_user, _, _, ^notify}
+ refute Streamer.filtered_by_user?(user, notify)
+ end
+
test "it doesn't send notify to the 'user:notification' stream when a user is blocked", %{
user: user
} do