I wish painful death on floatingghost

This commit is contained in:
Fox 2023-03-07 00:22:46 +00:00
parent 43e6b6d275
commit 16e950e4c3
91 changed files with 6141 additions and 40 deletions

View file

@ -176,12 +176,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `/api/v1/pleroma/scrobble` - `/api/v1/pleroma/scrobble`
- `/api/v1/pleroma/accounts/{id}/scrobbles` - `/api/v1/pleroma/accounts/{id}/scrobbles`
- Deprecated endpoints - Deprecated endpoints
- `/api/v1/pleroma/chats`
- `/api/v1/notifications/dismiss` - `/api/v1/notifications/dismiss`
- `/api/v1/search` - `/api/v1/search`
- `/api/v1/statuses/{id}/card` - `/api/v1/statuses/{id}/card`
- Chats, they were half-baked. Just use PMs.
- Prometheus, it causes massive slowdown - Prometheus, it causes massive slowdown
- LDAP authenticator
## 2022.07 ## 2022.07

View file

@ -269,6 +269,11 @@ config :pleroma, :welcome,
sender_nickname: nil, sender_nickname: nil,
message: nil message: nil
], ],
chat_message: [
enabled: false,
sender_nickname: nil,
message: nil
],
email: [ email: [
enabled: false, enabled: false,
sender: nil, sender: nil,
@ -763,8 +768,7 @@ config :pleroma, :frontends,
"mastodon-fe" => %{ "mastodon-fe" => %{
"name" => "mastodon-fe", "name" => "mastodon-fe",
"git" => "https://akkoma.dev/AkkomaGang/masto-fe", "git" => "https://akkoma.dev/AkkomaGang/masto-fe",
"build_url" => "build_url" => "https://akkoma-updates.s3-website.fr-par.scw.cloud/frontend/${ref}/masto-fe.zip",
"https://akkoma-updates.s3-website.fr-par.scw.cloud/frontend/${ref}/masto-fe.zip",
"build_dir" => "distribution", "build_dir" => "distribution",
"ref" => "akkoma" "ref" => "akkoma"
}, },

View file

@ -957,7 +957,7 @@ config :pleroma, :config_description, [
key: :privileged_staff, key: :privileged_staff,
type: :boolean, type: :boolean,
description: description:
"Let moderators access sensitive data (e.g. updating user credentials, get password reset token, delete users, index and read private statuses)" "Let moderators access sensitive data (e.g. updating user credentials, get password reset token, delete users, index and read private statuses and chats)"
}, },
%{ %{
key: :local_bubble, key: :local_bubble,
@ -1006,6 +1006,35 @@ config :pleroma, :config_description, [
} }
] ]
}, },
%{
key: :chat_message,
type: :keyword,
descpiption: "Chat message settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables sending a chat message to newly registered users"
},
%{
key: :message,
type: :string,
description:
"A message that will be sent to newly registered users as a chat message",
suggestions: [
"Hello, welcome on board!"
]
},
%{
key: :sender_nickname,
type: :string,
description: "The nickname of the local user that sends a welcome chat message",
suggestions: [
"lain"
]
}
]
},
%{ %{
key: :email, key: :email,
type: :keyword, type: :keyword,
@ -2624,6 +2653,27 @@ config :pleroma, :config_description, [
} }
] ]
}, },
%{
group: :pleroma,
key: :shout,
type: :group,
description: "Pleroma shout settings",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enables the backend Shoutbox chat feature."
},
%{
key: :limit,
type: :integer,
description: "Shout message character limit.",
suggestions: [
5_000
]
}
]
},
%{ %{
group: :pleroma, group: :pleroma,
key: :http, key: :http,

View file

@ -8,6 +8,11 @@ For from source installations Akkoma configuration works by first importing the
To add configuration to your config file, you can copy it from the base config. The latest version of it can be viewed [here](https://akkoma.dev/AkkomaGang/akkoma/src/branch/develop/config/config.exs). You can also use this file if you don't know how an option is supposed to be formatted. To add configuration to your config file, you can copy it from the base config. The latest version of it can be viewed [here](https://akkoma.dev/AkkomaGang/akkoma/src/branch/develop/config/config.exs). You can also use this file if you don't know how an option is supposed to be formatted.
## :shout
* `enabled` - Enables the backend Shoutbox chat feature. Defaults to `true`.
* `limit` - Shout character limit. Defaults to `5_000`
## :instance ## :instance
* `name`: The instances name. * `name`: The instances name.
* `email`: Email used to reach an Administrator/Moderator of the instance. * `email`: Email used to reach an Administrator/Moderator of the instance.
@ -76,6 +81,10 @@ To add configuration to your config file, you can copy it from the base config.
* `enabled`: Enables the send a direct message to a newly registered user. Defaults to `false`. * `enabled`: Enables the send a direct message to a newly registered user. Defaults to `false`.
* `sender_nickname`: The nickname of the local user that sends the welcome message. * `sender_nickname`: The nickname of the local user that sends the welcome message.
* `message`: A message that will be send to a newly registered users as a direct message. * `message`: A message that will be send to a newly registered users as a direct message.
* `chat_message`: - welcome message sent as a chat message.
* `enabled`: Enables the send a chat message to a newly registered user. Defaults to `false`.
* `sender_nickname`: The nickname of the local user that sends the welcome message.
* `message`: A message that will be send to a newly registered users as a chat message.
* `email`: - welcome message sent as a email. * `email`: - welcome message sent as a email.
* `enabled`: Enables the send a welcome email to a newly registered user. Defaults to `false`. * `enabled`: Enables the send a welcome email to a newly registered user. Defaults to `false`.
* `sender`: The email address or tuple with `{nickname, email}` that will use as sender to the welcome email. * `sender`: The email address or tuple with `{nickname, email}` that will use as sender to the welcome email.

View file

@ -1031,6 +1031,7 @@ Most of the settings will be applied in `runtime`, this means that you don't nee
- `:hackney_pools` - `:hackney_pools`
- `:connections_pool` - `:connections_pool`
- `:pools` - `:pools`
- `:chat`
- partially settings inside these keys: - partially settings inside these keys:
- `:seconds_valid` in `Pleroma.Captcha` - `:seconds_valid` in `Pleroma.Captcha`
- `:proxy_remote` in `Pleroma.Upload` - `:proxy_remote` in `Pleroma.Upload`
@ -1393,6 +1394,127 @@ Loads json generated from `config/descriptions.exs`.
``` ```
## GET /api/v1/pleroma/admin/users/:nickname/chats
### List a user's chats
- Params: None
- Response:
```json
[
{
"sender": {
"id": "someflakeid",
"username": "somenick",
...
},
"receiver": {
"id": "someflakeid",
"username": "somenick",
...
},
"id" : "1",
"unread" : 2,
"last_message" : {...}, // The last message in that chat
"updated_at": "2020-04-21T15:11:46.000Z"
}
]
```
## GET /api/v1/pleroma/admin/chats/:chat_id
### View a single chat
- Params: None
- Response:
```json
{
"sender": {
"id": "someflakeid",
"username": "somenick",
...
},
"receiver": {
"id": "someflakeid",
"username": "somenick",
...
},
"id" : "1",
"unread" : 2,
"last_message" : {...}, // The last message in that chat
"updated_at": "2020-04-21T15:11:46.000Z"
}
```
## GET /api/v1/pleroma/admin/chats/:chat_id/messages
### List the messages in a chat
- Params: `max_id`, `min_id`
- Response:
```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
}
]
```
## DELETE /api/v1/pleroma/admin/chats/:chat_id/messages/:message_id
### Delete a single message
- Params: None
- Response:
```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
}
```
## `GET /api/v1/pleroma/admin/instance_document/:document_name` ## `GET /api/v1/pleroma/admin/instance_document/:document_name`
### Get an instance document ### Get an instance document

View file

@ -0,0 +1,255 @@
# 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 Akkoma 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).
Parameters:
- with_muted: Include chats from muted users (boolean).
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,
"idempotency_key": "75442486-0874-440c-9db1-a7006c25a31f"
}
]
```
- idempotency_key: The copy of the `idempotency-key` HTTP request header that can be used for optimistic message sending. Included only during the first few minutes after the message creation.
### 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.

View file

@ -103,11 +103,13 @@ Has these additional fields under the `pleroma` object:
- `hide_followers_count`: boolean, true when the user has follower stat hiding enabled - `hide_followers_count`: boolean, true when the user has follower stat hiding enabled
- `hide_follows_count`: boolean, true when the user has follow stat hiding enabled - `hide_follows_count`: boolean, true when the user has follow stat hiding enabled
- `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `/api/v1/accounts/verify_credentials` and `/api/v1/accounts/update_credentials` - `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `/api/v1/accounts/verify_credentials` and `/api/v1/accounts/update_credentials`
- `chat_token`: The token needed for Akkoma shoutbox. Only returned in `/api/v1/accounts/verify_credentials`
- `deactivated`: boolean, true when the user is deactivated - `deactivated`: boolean, true when the user is deactivated
- `allow_following_move`: boolean, true when the user allows automatically follow moved following accounts - `allow_following_move`: boolean, true when the user allows automatically follow moved following accounts
- `unread_conversation_count`: The count of unread conversations. Only returned to the account owner. - `unread_conversation_count`: The count of unread conversations. Only returned to the account owner.
- `unread_notifications_count`: The count of unread notifications. Only returned to the account owner. - `unread_notifications_count`: The count of unread notifications. Only returned to the account owner.
- `notification_settings`: object, can be absent. See `/api/v1/pleroma/notification_settings` for the parameters/keys returned. - `notification_settings`: object, can be absent. See `/api/v1/pleroma/notification_settings` for the parameters/keys returned.
- `accepts_chat_messages`: boolean, but can be null if we don't have that information about a user
- `favicon`: nullable URL string, Favicon image of the user's instance - `favicon`: nullable URL string, Favicon image of the user's instance
### Source ### Source
@ -161,6 +163,15 @@ The `type` value is `pleroma:emoji_reaction`. Has these fields:
- `account`: The account of the user who reacted - `account`: The account of the user who reacted
- `status`: The status that was reacted on - `status`: The status that was reacted on
### ChatMention Notification (not default)
This notification has to be requested explicitly.
The `type` value is `pleroma:chat_mention`
- `account`: The account who sent the message
- `chat_message`: The chat message
### Report Notification (not default) ### Report Notification (not default)
This notification has to be requested explicitly. This notification has to be requested explicitly.
@ -175,7 +186,7 @@ The `type` value is `pleroma:report`
Accepts additional parameters: Accepts additional parameters:
- `exclude_visibilities`: will exclude the notifications for activities with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`). Usage example: `GET /api/v1/notifications?exclude_visibilities[]=direct&exclude_visibilities[]=private`. - `exclude_visibilities`: will exclude the notifications for activities with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`). Usage example: `GET /api/v1/notifications?exclude_visibilities[]=direct&exclude_visibilities[]=private`.
- `include_types`: will include the notifications for activities with the given types. The parameter accepts an array of types (`mention`, `follow`, `reblog`, `favourite`, `move`, `pleroma:emoji_reaction`, `pleroma:report`). Usage example: `GET /api/v1/notifications?include_types[]=mention&include_types[]=reblog`. - `include_types`: will include the notifications for activities with the given types. The parameter accepts an array of types (`mention`, `follow`, `reblog`, `favourite`, `move`, `pleroma:emoji_reaction`, `pleroma:chat_mention`, `pleroma:report`). Usage example: `GET /api/v1/notifications?include_types[]=mention&include_types[]=reblog`.
## DELETE `/api/v1/notifications/destroy_multiple` ## DELETE `/api/v1/notifications/destroy_multiple`
@ -233,6 +244,7 @@ Additional parameters can be added to the JSON body/Form data:
- `pleroma_background_image` - sets the background image of the user. Can be set to "" (an empty string) to reset. - `pleroma_background_image` - sets the background image of the user. Can be set to "" (an empty string) to reset.
- `discoverable` - if true, external services (search bots) etc. are allowed to index / list the account (regardless of this setting, user will still appear in regular search results). - `discoverable` - if true, external services (search bots) etc. are allowed to index / list the account (regardless of this setting, user will still appear in regular search results).
- `actor_type` - the type of this account. - `actor_type` - the type of this account.
- `accepts_chat_messages` - if false, this account will reject all chat messages.
- `language` - user's preferred language for receiving emails (digest, confirmation, etc.) - `language` - user's preferred language for receiving emails (digest, confirmation, etc.)
All images (avatar, banner and background) can be reset to the default by sending an empty string ("") instead of a file. All images (avatar, banner and background) can be reset to the default by sending an empty string ("") instead of a file.
@ -292,6 +304,7 @@ Has these additional parameters (which are the same as in Akkoma-API):
`GET /api/v1/instance` has additional fields `GET /api/v1/instance` has additional fields
- `max_toot_chars`: The maximum characters per post - `max_toot_chars`: The maximum characters per post
- `chat_limit`: The maximum characters per chat message
- `description_limit`: The maximum characters per image description - `description_limit`: The maximum characters per image description
- `poll_limits`: The limits of polls - `poll_limits`: The limits of polls
- `upload_limit`: The maximum upload file size - `upload_limit`: The maximum upload file size
@ -312,6 +325,7 @@ Has these additional parameters (which are the same as in Akkoma-API):
Permits these additional alert types: Permits these additional alert types:
- pleroma:chat_mention
- pleroma:emoji_reaction - pleroma:emoji_reaction
## Markers ## Markers
@ -322,6 +336,10 @@ Has these additional fields under the `pleroma` object:
## Streaming ## Streaming
### Chats
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.
### Remote timelines ### Remote timelines
For viewing remote server timelines, there are `public:remote` and `public:remote:media` streams. Each of these accept a parameter like `?instance=lain.com`. For viewing remote server timelines, there are `public:remote` and `public:remote:media` streams. Each of these accept a parameter like `?instance=lain.com`.

View file

@ -44,8 +44,11 @@ See also [the Nodeinfo standard](https://nodeinfo.diaspora.software/).
"shareable_emoji_packs", "shareable_emoji_packs",
"multifetch", "multifetch",
"pleroma:api/v1/notifications:include_types_filter", "pleroma:api/v1/notifications:include_types_filter",
"chat",
"shout",
"relay", "relay",
"pleroma_emoji_reactions" "pleroma_emoji_reactions",
"pleroma_chat_messages"
], ],
"federation":{ "federation":{
"enabled":true, "enabled":true,
@ -201,8 +204,11 @@ See also [the Nodeinfo standard](https://nodeinfo.diaspora.software/).
"shareable_emoji_packs", "shareable_emoji_packs",
"multifetch", "multifetch",
"pleroma:api/v1/notifications:include_types_filter", "pleroma:api/v1/notifications:include_types_filter",
"chat",
"shout",
"relay", "relay",
"pleroma_emoji_reactions" "pleroma_emoji_reactions",
"pleroma_chat_messages"
], ],
"federation":{ "federation":{
"enabled":true, "enabled":true,

View file

@ -26,3 +26,40 @@ Response: HTTP 201 Created with the object into the body, no `Location` header p
The object given in the reponse should then be inserted into an Object's `attachment` field. The object given in the reponse should then be inserted into an Object's `attachment` field.
## ChatMessages
`ChatMessage`s 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.
They are part of the `litepub` namespace as `http://litepub.social/ns#ChatMessage`.
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`.

View file

@ -78,7 +78,8 @@ defmodule Pleroma.Application do
] ++ ] ++
elasticsearch_children() ++ elasticsearch_children() ++
task_children(@mix_env) ++ task_children(@mix_env) ++
dont_run_in_test(@mix_env) dont_run_in_test(@mix_env) ++
shout_child(shout_enabled?())
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options # for other strategies and supported options
@ -93,6 +94,7 @@ defmodule Pleroma.Application do
end end
opts = [strategy: :one_for_one, name: Pleroma.Supervisor, max_restarts: max_restarts] opts = [strategy: :one_for_one, name: Pleroma.Supervisor, max_restarts: max_restarts]
result = Supervisor.start_link(children, opts)
with {:ok, data} <- Supervisor.start_link(children, opts) do with {:ok, data} <- Supervisor.start_link(children, opts) do
set_postgres_server_version() set_postgres_server_version()
@ -161,6 +163,10 @@ defmodule Pleroma.Application do
build_cachex("instances", default_ttl: :timer.hours(24), ttl_interval: 1000, limit: 2500), build_cachex("instances", default_ttl: :timer.hours(24), ttl_interval: 1000, limit: 2500),
build_cachex("request_signatures", default_ttl: :timer.hours(24 * 30), limit: 3000), build_cachex("request_signatures", default_ttl: :timer.hours(24 * 30), limit: 3000),
build_cachex("rel_me", default_ttl: :timer.hours(24 * 30), limit: 300) build_cachex("rel_me", default_ttl: :timer.hours(24 * 30), limit: 300)
build_cachex("chat_message_id_idempotency_key",
expiration: chat_message_id_idempotency_key_expiration(),
limit: 500_000
)
] ]
end end
@ -170,6 +176,9 @@ defmodule Pleroma.Application do
defp idempotency_expiration, defp idempotency_expiration,
do: expiration(default: :timer.seconds(6 * 60 * 60), interval: :timer.seconds(60)) do: expiration(default: :timer.seconds(6 * 60 * 60), interval: :timer.seconds(60))
defp chat_message_id_idempotency_key_expiration,
do: expiration(default: :timer.minutes(2), interval: :timer.seconds(60))
defp seconds_valid_interval, defp seconds_valid_interval,
do: :timer.seconds(Config.get!([Pleroma.Captcha, :seconds_valid])) do: :timer.seconds(Config.get!([Pleroma.Captcha, :seconds_valid]))
@ -181,6 +190,8 @@ defmodule Pleroma.Application do
type: :worker type: :worker
} }
defp shout_enabled?, do: Config.get([:shout, :enabled])
defp dont_run_in_test(env) when env in [:test, :benchmark], do: [] defp dont_run_in_test(env) when env in [:test, :benchmark], do: []
defp dont_run_in_test(_) do defp dont_run_in_test(_) do
@ -201,6 +212,12 @@ defmodule Pleroma.Application do
end end
@spec task_children(atom()) :: [map()] @spec task_children(atom()) :: [map()]
defp shout_child(true) do
[
Pleroma.Web.ShoutChannel.ShoutChannelState,
{Phoenix.PubSub, [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2]}
]
end
defp task_children(:test) do defp task_children(:test) do
[ [

97
lib/pleroma/chat.ex Normal file
View file

@ -0,0 +1,97 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Chat do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Pleroma.Chat
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.
"""
@type t :: %__MODULE__{}
@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
@spec get_by_user_and_id(User.t(), FlakeId.Ecto.CompatType.t()) ::
{:ok, t()} | {:error, :not_found}
def get_by_user_and_id(%User{id: user_id}, id) do
from(c in __MODULE__,
where: c.id == ^id,
where: c.user_id == ^user_id
)
|> Repo.find_resource()
end
@spec get_by_id(FlakeId.Ecto.CompatType.t()) :: t() | nil
def get_by_id(id) do
Repo.get(__MODULE__, id)
end
@spec get(FlakeId.Ecto.CompatType.t(), String.t()) :: t() | nil
def get(user_id, recipient) do
Repo.get_by(__MODULE__, user_id: user_id, recipient: recipient)
end
@spec get_or_create(FlakeId.Ecto.CompatType.t(), String.t()) ::
{:ok, t()} | {:error, Ecto.Changeset.t()}
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
@spec bump_or_create(FlakeId.Ecto.CompatType.t(), String.t()) ::
{:ok, t()} | {:error, Ecto.Changeset.t()}
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
@spec for_user_query(FlakeId.Ecto.CompatType.t()) :: Ecto.Query.t()
def for_user_query(user_id) do
from(c in Chat,
where: c.user_id == ^user_id,
order_by: [desc: c.updated_at]
)
end
end

View file

@ -0,0 +1,117 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 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

View file

@ -179,6 +179,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
check_activity_expiration_config(), check_activity_expiration_config(),
check_remote_ip_plug_name(), check_remote_ip_plug_name(),
check_uploders_s3_public_endpoint(), check_uploders_s3_public_endpoint(),
check_old_chat_shoutbox(),
check_quarantined_instances_tuples(), check_quarantined_instances_tuples(),
check_transparency_exclusions_tuples(), check_transparency_exclusions_tuples(),
check_simple_policy_tuples(), check_simple_policy_tuples(),
@ -337,4 +338,27 @@ defmodule Pleroma.Config.DeprecationWarnings do
:ok :ok
end end
end end
@spec check_old_chat_shoutbox() :: :ok | nil
def check_old_chat_shoutbox do
instance_config = Pleroma.Config.get([:instance])
chat_config = Pleroma.Config.get([:chat]) || []
use_old_config =
Keyword.has_key?(instance_config, :chat_limit) or
Keyword.has_key?(chat_config, :enabled)
if use_old_config do
Logger.error("""
!!!DEPRECATION WARNING!!!
Your config is using the old namespace for the Shoutbox configuration. You need to convert to the new namespace. e.g.,
\n* `config :pleroma, :chat, enabled` and `config :pleroma, :instance, chat_limit` are now equal to:
\n* `config :pleroma, :shout, enabled` and `config :pleroma, :shout, limit`
""")
:error
else
:ok
end
end
end end

View file

@ -15,6 +15,7 @@ defmodule Pleroma.Config.TransferTask do
defp reboot_time_keys, defp reboot_time_keys,
do: [ do: [
{:pleroma, :shout},
{:pleroma, Oban}, {:pleroma, Oban},
{:pleroma, :rate_limit}, {:pleroma, :rate_limit},
{:pleroma, :markup}, {:pleroma, :markup},

View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.MigrationHelper.NotificationBackfill do defmodule Pleroma.MigrationHelper.NotificationBackfill do
alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
@ -78,5 +79,14 @@ defmodule Pleroma.MigrationHelper.NotificationBackfill do
end end
end end
defp type_from_activity_object(%{data: %{"type" => "Create"}}), do: "mention" 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 end

View file

@ -237,8 +237,20 @@ defmodule Pleroma.ModerationLog do
insert_log_entry_with_message(%ModerationLog{data: data}) insert_log_entry_with_message(%ModerationLog{data: data})
end end
def insert_log(%{actor: %User{} = actor, action: "chat_message_delete", subject_id: subject_id}) do
%ModerationLog{
data: %{
"actor" => %{"nickname" => actor.nickname},
"action" => "chat_message_delete",
"subject_id" => subject_id
}
}
|> insert_log_entry_with_message()
end
@spec insert_log_entry_with_message(ModerationLog.t()) :: @spec insert_log_entry_with_message(ModerationLog.t()) ::
{:ok, ModerationLog.t()} | {:error, any} {:ok, ModerationLog.t()} | {:error, any}
defp insert_log_entry_with_message(entry) do defp insert_log_entry_with_message(entry) do
entry.data["message"] entry.data["message"]
|> put_in(get_log_entry_message(entry)) |> put_in(get_log_entry_message(entry))
@ -544,6 +556,16 @@ defmodule Pleroma.ModerationLog do
"@#{actor_nickname} updated users: #{users_to_nicknames_string(subjects)}" "@#{actor_nickname} updated users: #{users_to_nicknames_string(subjects)}"
end end
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "chat_message_delete",
"subject_id" => subject_id
}
}) do
"@#{actor_nickname} deleted chat message ##{subject_id}"
end
def get_log_entry_message(%ModerationLog{ def get_log_entry_message(%ModerationLog{
data: %{ data: %{
"actor" => %{"nickname" => actor_nickname}, "actor" => %{"nickname" => actor_nickname},

View file

@ -68,6 +68,7 @@ defmodule Pleroma.Notification do
follow_request follow_request
mention mention
move move
pleroma:chat_mention
pleroma:emoji_reaction pleroma:emoji_reaction
pleroma:report pleroma:report
reblog reblog
@ -463,7 +464,16 @@ defmodule Pleroma.Notification do
end end
end end
defp type_from_activity_object(%{data: %{"type" => "Create"}}), do: "mention" 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. # TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user, opts \\ []) do def create_notification(%Activity{} = activity, %User{} = user, opts \\ []) do

View file

@ -151,6 +151,7 @@ defmodule Pleroma.User do
field(:also_known_as, {:array, ObjectValidators.ObjectID}, default: []) field(:also_known_as, {:array, ObjectValidators.ObjectID}, default: [])
field(:inbox, :string) field(:inbox, :string)
field(:shared_inbox, :string) field(:shared_inbox, :string)
field(:accepts_chat_messages, :boolean, default: nil)
field(:last_active_at, :naive_datetime) field(:last_active_at, :naive_datetime)
field(:disclose_client, :boolean, default: true) field(:disclose_client, :boolean, default: true)
field(:pinned_objects, :map, default: %{}) field(:pinned_objects, :map, default: %{})
@ -475,6 +476,7 @@ defmodule Pleroma.User do
:invisible, :invisible,
:actor_type, :actor_type,
:also_known_as, :also_known_as,
:accepts_chat_messages,
:pinned_objects :pinned_objects
] ]
) )
@ -537,6 +539,8 @@ defmodule Pleroma.User do
:actor_type, :actor_type,
:disclose_client, :disclose_client,
:status_ttl_days :status_ttl_days
:accepts_chat_messages,
:disclose_client
] ]
) )
|> unique_constraint(:nickname) |> unique_constraint(:nickname)
@ -728,6 +732,7 @@ defmodule Pleroma.User do
bio_limit = Config.get([:instance, :user_bio_length], 5000) bio_limit = Config.get([:instance, :user_bio_length], 5000)
name_limit = Config.get([:instance, :user_name_length], 100) name_limit = Config.get([:instance, :user_name_length], 100)
reason_limit = Config.get([:instance, :registration_reason_length], 500) reason_limit = Config.get([:instance, :registration_reason_length], 500)
params = Map.put_new(params, :accepts_chat_messages, true)
confirmed? = confirmed? =
if is_nil(opts[:confirmed]) do if is_nil(opts[:confirmed]) do
@ -755,6 +760,7 @@ defmodule Pleroma.User do
:password, :password,
:password_confirmation, :password_confirmation,
:emoji, :emoji,
:accepts_chat_messages,
:registration_reason, :registration_reason,
:language :language
]) ])
@ -865,7 +871,8 @@ defmodule Pleroma.User do
{:ok, user} <- set_cache(user), {:ok, user} <- set_cache(user),
{:ok, _} <- maybe_send_registration_email(user), {:ok, _} <- maybe_send_registration_email(user),
{:ok, _} <- maybe_send_welcome_email(user), {:ok, _} <- maybe_send_welcome_email(user),
{:ok, _} <- maybe_send_welcome_message(user) do {:ok, _} <- maybe_send_welcome_message(user),
{:ok, _} <- maybe_send_welcome_chat_message(user) do
{:ok, user} {:ok, user}
end end
end end
@ -899,6 +906,15 @@ defmodule Pleroma.User do
end end
end end
defp maybe_send_welcome_chat_message(user) do
if User.WelcomeChatMessage.enabled?() do
User.WelcomeChatMessage.post_message(user)
{:ok, :enqueued}
else
{:ok, :noop}
end
end
defp maybe_send_welcome_email(%User{email: email} = user) when is_binary(email) do defp maybe_send_welcome_email(%User{email: email} = user) when is_binary(email) do
if User.WelcomeEmail.enabled?() do if User.WelcomeEmail.enabled?() do
User.WelcomeEmail.send_email(user) User.WelcomeEmail.send_email(user)

View file

@ -0,0 +1,45 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.WelcomeChatMessage do
alias Pleroma.Config
alias Pleroma.User
alias Pleroma.Web.CommonAPI
@spec enabled?() :: boolean()
def enabled?, do: Config.get([:welcome, :chat_message, :enabled], false)
@spec post_message(User.t()) :: {:ok, Pleroma.Activity.t() | nil}
def post_message(user) do
[:welcome, :chat_message, :sender_nickname]
|> Config.get(nil)
|> fetch_sender()
|> do_post(user, welcome_message())
end
defp do_post(%User{} = sender, recipient, message)
when is_binary(message) do
CommonAPI.post_chat_message(
sender,
recipient,
message
)
end
defp do_post(_sender, _recipient, _message), do: {:ok, nil}
defp fetch_sender(nickname) when is_binary(nickname) do
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
user
else
_ -> nil
end
end
defp fetch_sender(_), do: nil
defp welcome_message do
Config.get([:welcome, :chat_message, :message], nil)
end
end

View file

@ -97,7 +97,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp increase_replies_count_if_reply(_create_data), do: :noop defp increase_replies_count_if_reply(_create_data), do: :noop
@object_types ~w[Question Answer Audio Video Event Article Note Page] @object_types ~w[ChatMessage Question Answer Audio Video Event Article Note Page]
@impl true @impl true
def persist(%{"type" => type} = object, meta) when type in @object_types do def persist(%{"type" => type} = object, meta) when type in @object_types do
with {:ok, object} <- Object.create(object) do with {:ok, object} <- Object.create(object) do
@ -1289,7 +1289,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end end
end end
defp exclude_invisible_actors(query, %{type: "Flag"}), do: query
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, %{invisible_actors: true}), do: query
defp exclude_invisible_actors(query, _opts) do defp exclude_invisible_actors(query, _opts) do
@ -1429,6 +1441,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_filtered(opts) |> restrict_filtered(opts)
|> maybe_restrict_deactivated_users(opts) |> maybe_restrict_deactivated_users(opts)
|> exclude_poll_votes(opts) |> exclude_poll_votes(opts)
|> exclude_chat_messages(opts)
|> exclude_invisible_actors(opts) |> exclude_invisible_actors(opts)
|> exclude_visibility(opts) |> exclude_visibility(opts)
@ -1554,6 +1567,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end) end)
is_locked = data["manuallyApprovesFollowers"] || false is_locked = data["manuallyApprovesFollowers"] || false
capabilities = data["capabilities"] || %{}
accepts_chat_messages = capabilities["acceptsChatMessages"]
data = Transmogrifier.maybe_fix_user_object(data) data = Transmogrifier.maybe_fix_user_object(data)
is_discoverable = data["discoverable"] || false is_discoverable = data["discoverable"] || false
invisible = data["invisible"] || false invisible = data["invisible"] || false
@ -1612,6 +1627,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
shared_inbox: shared_inbox, shared_inbox: shared_inbox,
pinned_objects: pinned_objects, pinned_objects: pinned_objects,
nickname: nickname nickname: nickname
accepts_chat_messages: accepts_chat_messages,
pinned_objects: pinned_objects
} }
end end

View file

@ -238,6 +238,28 @@ defmodule Pleroma.Web.ActivityPub.Builder do
Map.put(object, "quoteUri", quote_object.data["id"]) Map.put(object, "quoteUri", quote_object.data["id"])
else else
_ -> object _ -> object
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
end end

View file

@ -23,6 +23,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
@ -81,6 +83,21 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end end
end end
def validate(
%{"type" => "Create", "object" => %{"type" => "ChatMessage"} = 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( def validate(
%{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity, %{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity,
meta meta
@ -160,7 +177,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
def validate(%{"type" => type} = object, meta) def validate(%{"type" => type} = object, meta)
when type in ~w[Accept Reject Follow Update Like EmojiReact Announce when type in ~w[Accept Reject Follow Update Like EmojiReact Announce
Answer] do ChatMessage Answer] do
validator = validator =
case type do case type do
"Accept" -> AcceptRejectValidator "Accept" -> AcceptRejectValidator
@ -170,6 +187,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
"Like" -> LikeValidator "Like" -> LikeValidator
"EmojiReact" -> EmojiReactValidator "EmojiReact" -> EmojiReactValidator
"Announce" -> AnnounceValidator "Announce" -> AnnounceValidator
"ChatMessage" -> ChatMessageValidator
"Answer" -> AnswerValidator "Answer" -> AnswerValidator
end end
@ -203,6 +221,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end) end)
end end
def cast_and_apply(%{"type" => "ChatMessage"} = object) do
ChatMessageValidator.cast_and_apply(object)
end
def cast_and_apply(%{"type" => "Question"} = object) do def cast_and_apply(%{"type" => "Question"} = object) do
QuestionValidator.cast_and_apply(object) QuestionValidator.cast_and_apply(object)
end end

View file

@ -0,0 +1,129 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do
use Ecto.Schema
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
import Ecto.Changeset
import Pleroma.Web.ActivityPub.Transmogrifier, only: [fix_emoji: 1]
@primary_key false
@derive Jason.Encoder
embedded_schema do
field(:id, ObjectValidators.ObjectID, primary_key: true)
field(:to, ObjectValidators.Recipients, default: [])
field(:type, :string)
field(:content, ObjectValidators.SafeText)
field(:actor, ObjectValidators.ObjectID)
field(:published, ObjectValidators.DateTime)
field(:emoji, ObjectValidators.Emoji, 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
defp 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
- If the recipient is explicitly not accepting chat messages
"""
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} <- {:not_accepting_chats?, recipient.accepts_chat_messages == false},
{_, 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")
{:not_accepting_chats?, true} ->
cng
|> add_error(:to, "recipient does not accept chat messages")
{: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

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator
# Activities and Objects # Activities and Objects, except (Create)ChatMessage
defmacro message_fields do defmacro message_fields do
quote bind_quoted: binding() do quote bind_quoted: binding() do
field(:type, :string) field(:type, :string)
@ -39,7 +39,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
end end
end end
# Basically objects that aren't Answer # Basically objects that aren't ChatMessage and Answer
defmacro status_object_fields do defmacro status_object_fields do
quote bind_quoted: binding() do quote bind_quoted: binding() do
# TODO: Remove actor on objects # TODO: Remove actor on objects

View file

@ -0,0 +1,96 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 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.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Object
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@primary_key false
embedded_schema do
quote do
unquote do
import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields
activity_fields()
end
end
field(:id, ObjectValidators.ObjectID, primary_key: true)
field(:type, :string)
field(:to, ObjectValidators.Recipients, default: [])
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
defp 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

View file

@ -48,6 +48,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
Answer Answer
Article Article
Audio Audio
ChatMessage
Event Event
Note Note
Page Page

View file

@ -10,6 +10,8 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
collection, and so on. collection, and so on.
""" """
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Chat
alias Pleroma.Chat.MessageReference
alias Pleroma.FollowingRelationship alias Pleroma.FollowingRelationship
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
@ -26,6 +28,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
require Pleroma.Constants require Pleroma.Constants
require Logger require Logger
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@logger Pleroma.Config.get([:side_effects, :logger], Logger) @logger Pleroma.Config.get([:side_effects, :logger], Logger)
@behaviour Pleroma.Web.ActivityPub.SideEffects.Handling @behaviour Pleroma.Web.ActivityPub.SideEffects.Handling
@ -313,6 +316,8 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
Object.decrease_replies_count(in_reply_to) Object.decrease_replies_count(in_reply_to)
end end
MessageReference.delete_for_object(deleted_object)
ap_streamer().stream_out(object) ap_streamer().stream_out(object)
ap_streamer().stream_out_participations(deleted_object, user) ap_streamer().stream_out_participations(deleted_object, user)
:ok :ok
@ -472,8 +477,43 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
end end
end end
end end
end
{:ok, object, meta} {:ok, object, meta}
def handle_object_creation(%{"type" => "ChatMessage"} = object, _activity, 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.uniq()
|> 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)
@cachex.put(
:chat_message_id_idempotency_key_cache,
cm_ref.id,
meta[:idempotency_key]
)
{
["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 end
def handle_object_creation(%{"type" => "Question"} = object, activity, meta) do def handle_object_creation(%{"type" => "Question"} = object, activity, meta) do
@ -574,6 +614,13 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
meta meta
end 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 defp add_notifications(meta, notifications) do
existing = Keyword.get(meta, :notifications, []) existing = Keyword.get(meta, :notifications, [])

View file

@ -447,7 +447,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
%{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data, %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
options options
) )
when objtype in ~w{Question Answer Audio Video Event Article Note Page} do when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page} do
fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1) fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
object = object =
@ -927,6 +927,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
Map.put(object, "attributedTo", attributed_to) Map.put(object, "attributedTo", attributed_to)
end end
# TODO: Revisit this
def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
def prepare_attachments(object) do def prepare_attachments(object) do
attachments = attachments =
object object

View file

@ -81,7 +81,14 @@ defmodule Pleroma.Web.ActivityPub.UserView do
fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue")) fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue"))
capabilities = %{} capabilities =
if is_boolean(user.accepts_chat_messages) do
%{
"acceptsChatMessages" => user.accepts_chat_messages
}
else
%{}
end
%{ %{
"id" => user.ap_id, "id" => user.ap_id,

View file

@ -52,6 +52,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
when action in [:list_user_statuses] when action in [:list_user_statuses]
) )
plug(
OAuthScopesPlug,
%{scopes: ["admin:read:chats"]}
when action in [:list_user_chats]
)
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["admin:read"]} %{scopes: ["admin:read"]}
@ -100,6 +106,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end end
end end
def list_user_chats(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname} = _params) do
with %User{id: user_id} <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
chats =
Pleroma.Chat.for_user_query(user_id)
|> Pleroma.Repo.all()
conn
|> put_view(AdminAPI.ChatView)
|> render("index.json", chats: chats)
else
_ -> {:error, :not_found}
end
end
def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
with {:ok, _} <- User.tag(nicknames, tags) do with {:ok, _} <- User.tag(nicknames, tags) do
ModerationLog.insert_log(%{ ModerationLog.insert_log(%{

View file

@ -0,0 +1,85 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.ChatController do
use Pleroma.Web, :controller
alias Pleroma.Activity
alias Pleroma.Chat
alias Pleroma.Chat.MessageReference
alias Pleroma.ModerationLog
alias Pleroma.Pagination
alias Pleroma.Web.AdminAPI
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
alias Pleroma.Web.Plugs.OAuthScopesPlug
require Logger
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(
OAuthScopesPlug,
%{scopes: ["admin:read:chats"]} when action in [:show, :messages]
)
plug(
OAuthScopesPlug,
%{scopes: ["admin:write:chats"]} when action in [:delete_message]
)
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ChatOperation
def delete_message(%{assigns: %{user: user}} = conn, %{
message_id: message_id,
id: chat_id
}) do
with %MessageReference{object: %{data: %{"id" => object_ap_id}}} = cm_ref <-
MessageReference.get_by_id(message_id),
^chat_id <- to_string(cm_ref.chat_id),
%Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(object_ap_id),
{:ok, _} <- CommonAPI.delete(activity_id, user) do
ModerationLog.insert_log(%{
action: "chat_message_delete",
actor: user,
subject_id: message_id
})
conn
|> put_view(MessageReferenceView)
|> render("show.json", chat_message_reference: cm_ref)
else
_e ->
{:error, :could_not_delete}
end
end
def messages(conn, %{id: id} = params) do
with %Chat{} = chat <- Chat.get_by_id(id) do
cm_refs =
chat
|> MessageReference.for_chat_query()
|> Pagination.fetch_paginated(params)
conn
|> put_view(MessageReferenceView)
|> render("index.json", chat_message_references: cm_refs)
else
_ ->
conn
|> put_status(:not_found)
|> json(%{error: "not found"})
end
end
def show(conn, %{id: id}) do
with %Chat{} = chat <- Chat.get_by_id(id) do
conn
|> put_view(AdminAPI.ChatView)
|> render("show.json", chat: chat)
end
end
end

View file

@ -0,0 +1,30 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.ChatView do
use Pleroma.Web, :view
alias Pleroma.Chat
alias Pleroma.User
alias Pleroma.Web.MastodonAPI
alias Pleroma.Web.PleromaAPI
def render("index.json", %{chats: chats} = opts) do
render_many(chats, __MODULE__, "show.json", Map.delete(opts, :chats))
end
def render("show.json", %{chat: %Chat{user_id: user_id}} = opts) do
user = User.get_by_id(user_id)
sender = MastodonAPI.AccountView.render("show.json", user: user, skip_visibility_check: true)
serialized_chat = PleromaAPI.ChatView.render("show.json", opts)
serialized_chat
|> Map.put(:sender, sender)
|> Map.put(:receiver, serialized_chat[:account])
|> Map.delete(:account)
end
def render(view, opts), do: PleromaAPI.ChatView.render(view, opts)
end

View file

@ -84,6 +84,7 @@ defmodule Pleroma.Web.ApiSpec do
%{ %{
"name" => "Administration", "name" => "Administration",
"tags" => [ "tags" => [
"Chat administration",
"Emoji pack administration", "Emoji pack administration",
"Frontend managment", "Frontend managment",
"Instance configuration", "Instance configuration",
@ -113,6 +114,7 @@ defmodule Pleroma.Web.ApiSpec do
] ]
}, },
%{"name" => "Instance", "tags" => ["Custom emojis"]}, %{"name" => "Instance", "tags" => ["Custom emojis"]},
%{"name" => "Messaging", "tags" => ["Chats", "Conversations"]},
%{ %{
"name" => "Statuses", "name" => "Statuses",
"tags" => [ "tags" => [

View file

@ -613,6 +613,11 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
nullable: true, nullable: true,
description: "Whether manual approval of follow requests is required." description: "Whether manual approval of follow requests is required."
}, },
accepts_chat_messages: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Whether the user accepts receiving chat messages."
},
fields_attributes: %Schema{ fields_attributes: %Schema{
nullable: true, nullable: true,
oneOf: [ oneOf: [

View file

@ -0,0 +1,96 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Admin.ChatOperation do
alias OpenApiSpex.Operation
alias Pleroma.Web.ApiSpec.Schemas.Chat
alias Pleroma.Web.ApiSpec.Schemas.ChatMessage
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def delete_message_operation do
%Operation{
tags: ["Chat administration"],
summary: "Delete an individual chat message",
operationId: "AdminAPI.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" => ["admin:write:chats"]
}
]
}
end
def messages_operation do
%Operation{
tags: ["Chat administration"],
summary: "Get chat's messages",
operationId: "AdminAPI.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",
Pleroma.Web.ApiSpec.ChatOperation.chat_messages_response()
)
},
security: [
%{
"oAuth" => ["admin:read:chats"]
}
]
}
end
def show_operation do
%Operation{
tags: ["Chat administration"],
summary: "Create a chat",
operationId: "AdminAPI.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" => ["admin:read"]
}
]
}
end
end

View file

@ -0,0 +1,361 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 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.BooleanLike
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: ["Chats"],
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: ["Chats"],
summary: "Mark a message 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: ["Chats"],
summary: "Retrieve 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: ["Chats"],
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 index2_operation do
%Operation{
tags: ["Chats"],
summary: "Retrieve list of chats",
operationId: "ChatController.index2",
parameters: [
Operation.parameter(:with_muted, :query, BooleanLike, "Include chats from muted users")
| 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: ["Chats"],
summary: "Retrieve chat's messages",
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()
),
404 => Operation.response("Not Found", "application/json", ApiError)
},
security: [
%{
"oAuth" => ["read:chats"]
}
]
}
end
def post_chat_message_operation do
%Operation{
tags: ["Chats"],
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),
422 => Operation.response("MRF Rejection", "application/json", ApiError)
},
security: [
%{
"oAuth" => ["write:chats"]
}
]
}
end
def delete_message_operation do
%Operation{
tags: ["Chats"],
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,
"is_confirmed" => true,
"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",
"account_id" => "someflakeid",
"unread" => false
},
%{
"account_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

View file

@ -174,6 +174,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
"reblog", "reblog",
"mention", "mention",
"pleroma:emoji_reaction", "pleroma:emoji_reaction",
"pleroma:chat_mention",
"pleroma:report", "pleroma:report",
"move", "move",
"follow_request", "follow_request",
@ -189,6 +190,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
- `poll` - A poll you have voted in or created has ended - `poll` - A poll you have voted in or created has ended
- `move` - Someone moved their account - `move` - Someone moved their account
- `pleroma:emoji_reaction` - Someone reacted with emoji to your status - `pleroma:emoji_reaction` - Someone reacted with emoji to your status
- `pleroma:chat_mention` - Someone mentioned you in a chat message
- `pleroma:report` - Someone was reported - `pleroma:report` - Someone was reported
""" """
} }

View file

@ -142,6 +142,11 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do
nullable: true, nullable: true,
description: "Receive poll notifications?" description: "Receive poll notifications?"
}, },
"pleroma:chat_mention": %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Receive chat notifications?"
},
"pleroma:emoji_reaction": %Schema{ "pleroma:emoji_reaction": %Schema{
allOf: [BooleanLike], allOf: [BooleanLike],
nullable: true, nullable: true,
@ -211,6 +216,11 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do
nullable: true, nullable: true,
description: "Receive poll notifications?" description: "Receive poll notifications?"
}, },
"pleroma:chat_mention": %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Receive chat notifications?"
},
"pleroma:emoji_reaction": %Schema{ "pleroma:emoji_reaction": %Schema{
allOf: [BooleanLike], allOf: [BooleanLike],
nullable: true, nullable: true,

View file

@ -43,7 +43,7 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
tags: ["Timelines"], tags: ["Timelines"],
summary: "Direct timeline", summary: "Direct timeline",
description: description:
"View statuses with a “direct” scope addressed to the account. Using this endpoint is discouraged, please use [conversations](#tag/Conversations).", "View statuses with a “direct” scope addressed to the account. Using this endpoint is discouraged, please use [conversations](#tag/Conversations) or [chats](#tag/Chats).",
parameters: [with_muted_param() | pagination_params()], parameters: [with_muted_param() | pagination_params()],
security: [%{"oAuth" => ["read:statuses"]}], security: [%{"oAuth" => ["read:statuses"]}],
operationId: "TimelineController.direct", operationId: "TimelineController.direct",

View file

@ -47,6 +47,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
description: "whether the user allows automatically follow moved following accounts" description: "whether the user allows automatically follow moved following accounts"
}, },
background_image: %Schema{type: :string, nullable: true, format: :uri}, background_image: %Schema{type: :string, nullable: true, format: :uri},
chat_token: %Schema{type: :string},
is_confirmed: %Schema{ is_confirmed: %Schema{
type: :boolean, type: :boolean,
description: description:
@ -101,6 +102,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
description: description:
"A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`" "A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`"
}, },
accepts_chat_messages: %Schema{type: :boolean, nullable: true},
favicon: %Schema{ favicon: %Schema{
type: :string, type: :string,
format: :uri, format: :uri,
@ -179,6 +181,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
"is_admin" => false, "is_admin" => false,
"is_moderator" => false, "is_moderator" => false,
"skip_thread_containment" => false, "skip_thread_containment" => false,
"accepts_chat_messages" => true,
"chat_token" =>
"SFMyNTY.g3QAAAACZAAEZGF0YW0AAAASOXRLaTNlc2JHN09RZ1oyOTIwZAAGc2lnbmVkbgYARNplS3EB.Mb_Iaqew2bN1I1o79B_iP7encmVCpTKC4OtHZRxdjKc",
"unread_conversation_count" => 0, "unread_conversation_count" => 0,
"tags" => [], "tags" => [],
"notification_settings" => %{ "notification_settings" => %{

View file

@ -0,0 +1,75 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 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,
"is_confirmed" => true,
"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,
"is_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

View file

@ -0,0 +1,77 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Emoji
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, items: Emoji},
attachment: %Schema{type: :object, nullable: true},
card: %Schema{
type: :object,
nullable: true,
description: "Preview card for links included within status content",
required: [:url, :title, :description, :type],
properties: %{
type: %Schema{
type: :string,
enum: ["link", "photo", "video", "rich"],
description: "The type of the preview card"
},
provider_name: %Schema{
type: :string,
nullable: true,
description: "The provider of the original resource"
},
provider_url: %Schema{
type: :string,
format: :uri,
description: "A link to the provider of the original resource"
},
url: %Schema{type: :string, format: :uri, description: "Location of linked resource"},
image: %Schema{
type: :string,
nullable: true,
format: :uri,
description: "Preview thumbnail"
},
title: %Schema{type: :string, description: "Title of linked resource"},
description: %Schema{type: :string, description: "Description of preview"}
}
},
unread: %Schema{type: :boolean, description: "Whether a message has been marked as read."}
},
example: %{
"account_id" => "someflakeid",
"chat_id" => "1",
"content" => "hey you again",
"created_at" => "2020-04-21T15:06:45.000Z",
"card" => nil,
"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,
"unread" => false
}
})
end

View file

@ -0,0 +1,45 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.UserSocket do
use Phoenix.Socket
alias Pleroma.User
## Channels
# channel "room:*", Pleroma.Web.RoomChannel
channel("chat:*", Pleroma.Web.ShoutChannel)
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
# verification, you can put default assigns into
# the socket that will be set for all channels, ie
#
# {:ok, assign(socket, :user_id, verified_user_id)}
#
# To deny connection, return `:error`.
#
# See `Phoenix.Token` documentation for examples in
# performing token verification on connect.
def connect(%{"token" => token}, socket) do
with true <- Pleroma.Config.get([:shout, :enabled]),
{:ok, user_id} <- Phoenix.Token.verify(socket, "user socket", token, max_age: 84_600),
%User{} = user <- Pleroma.User.get_cached_by_id(user_id) do
{:ok, assign(socket, :user_name, user.nickname)}
else
_e -> :error
end
end
# Socket id's are topics that allow you to identify all sockets for a given user:
#
# def id(socket), do: "user_socket:#{socket.assigns.user_id}"
#
# Would allow you to broadcast a "disconnect" event and terminate
# all active sockets and channels for a given user:
#
# Pleroma.Web.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
#
# Returning `nil` makes this socket anonymous.
def id(_socket), do: nil
end

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.CommonAPI do defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Formatter
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.ThreadMute alias Pleroma.ThreadMute
alias Pleroma.User alias Pleroma.User
@ -29,6 +30,57 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
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,
idempotency_key: opts[:idempotency_key]
)} do
{:ok, activity}
else
{:common_pipeline, {:reject, _} = e} -> e
e -> e
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 def unblock(blocker, blocked) do
with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)}, with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
{:ok, unblock_data, _} <- Builder.undo(blocker, block), {:ok, unblock_data, _} <- Builder.undo(blocker, block),

View file

@ -9,6 +9,7 @@ defmodule Pleroma.Web.Endpoint do
alias Pleroma.Config alias Pleroma.Config
socket("/socket", Pleroma.Web.UserSocket)
socket("/live", Phoenix.LiveView.Socket) socket("/live", Phoenix.LiveView.Socket)
plug(Pleroma.Web.Plugs.SetLocalePlug) plug(Pleroma.Web.Plugs.SetLocalePlug)

View file

@ -154,10 +154,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "GET /api/v1/accounts/verify_credentials" @doc "GET /api/v1/accounts/verify_credentials"
def verify_credentials(%{assigns: %{user: user}} = conn, _) do def verify_credentials(%{assigns: %{user: user}} = conn, _) do
chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
render(conn, "show.json", render(conn, "show.json",
user: user, user: user,
for: user, for: user,
with_pleroma_settings: true with_pleroma_settings: true,
with_chat_token: chat_token
) )
end end
@ -191,7 +194,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
:show_role, :show_role,
:skip_thread_containment, :skip_thread_containment,
:allow_following_move, :allow_following_move,
:also_known_as :also_known_as,
:accepts_chat_messages
] ]
|> Enum.reduce(%{}, fn key, acc -> |> Enum.reduce(%{}, fn key, acc ->
Maps.put_if_present(acc, key, params[key], &{:ok, Params.truthy_param?(&1)}) Maps.put_if_present(acc, key, params[key], &{:ok, Params.truthy_param?(&1)})

View file

@ -307,6 +307,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
relationship: relationship, relationship: relationship,
skip_thread_containment: user.skip_thread_containment, skip_thread_containment: user.skip_thread_containment,
background_image: image_url(user.background) |> MediaProxy.url(), background_image: image_url(user.background) |> MediaProxy.url(),
accepts_chat_messages: user.accepts_chat_messages,
favicon: favicon favicon: favicon
} }
} }
@ -314,6 +315,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|> maybe_put_settings(user, opts[:for], opts) |> maybe_put_settings(user, opts[:for], opts)
|> maybe_put_notification_settings(user, opts[:for]) |> maybe_put_notification_settings(user, opts[:for])
|> maybe_put_settings_store(user, opts[:for], opts) |> maybe_put_settings_store(user, opts[:for], opts)
|> maybe_put_chat_token(user, opts[:for], opts)
|> maybe_put_activation_status(user, opts[:for]) |> maybe_put_activation_status(user, opts[:for])
|> maybe_put_follow_requests_count(user, opts[:for]) |> maybe_put_follow_requests_count(user, opts[:for])
|> maybe_put_allow_following_move(user, opts[:for]) |> maybe_put_allow_following_move(user, opts[:for])
@ -367,6 +369,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp maybe_put_settings_store(data, _, _, _), do: data defp maybe_put_settings_store(data, _, _, _), do: data
defp maybe_put_chat_token(data, %User{id: id}, %User{id: id}, %{
with_chat_token: token
}) do
data
|> Kernel.put_in([:pleroma, :chat_token], token)
end
defp maybe_put_chat_token(data, _, _, _), do: data
defp maybe_put_role(data, %User{show_role: true} = user, _) do defp maybe_put_role(data, %User{show_role: true} = user, _) do
data data
|> Kernel.put_in([:pleroma, :is_admin], user.is_admin) |> Kernel.put_in([:pleroma, :is_admin], user.is_admin)

View file

@ -37,6 +37,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
background_upload_limit: Keyword.get(instance, :background_upload_limit), background_upload_limit: Keyword.get(instance, :background_upload_limit),
banner_upload_limit: Keyword.get(instance, :banner_upload_limit), banner_upload_limit: Keyword.get(instance, :banner_upload_limit),
background_image: Pleroma.Web.Endpoint.url() <> Keyword.get(instance, :background_image), background_image: Pleroma.Web.Endpoint.url() <> Keyword.get(instance, :background_image),
shout_limit: Config.get([:shout, :limit]),
description_limit: Keyword.get(instance, :description_limit), description_limit: Keyword.get(instance, :description_limit),
pleroma: %{ pleroma: %{
metadata: %{ metadata: %{

View file

@ -6,7 +6,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Chat.MessageReference
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserRelationship alias Pleroma.UserRelationship
alias Pleroma.Web.AdminAPI.Report alias Pleroma.Web.AdminAPI.Report
@ -16,6 +18,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
defp object_id_for(%{data: %{"object" => %{"id" => id}}}) when is_binary(id), do: id defp object_id_for(%{data: %{"object" => %{"id" => id}}}) when is_binary(id), do: id
@ -125,6 +128,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|> put_status(parent_activity_fn.(), reading_user, status_render_opts) |> put_status(parent_activity_fn.(), reading_user, status_render_opts)
|> put_emoji(activity) |> put_emoji(activity)
"pleroma:chat_mention" ->
put_chat_message(response, activity, reading_user, status_render_opts)
"pleroma:report" -> "pleroma:report" ->
put_report(response, activity) put_report(response, activity)
@ -145,6 +151,17 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|> Map.put(:emoji_url, MediaProxy.url(Pleroma.Emoji.emoji_url(activity.data))) |> Map.put(:emoji_url, MediaProxy.url(Pleroma.Emoji.emoji_url(activity.data)))
end end
defp put_chat_message(response, activity, reading_user, opts) do
object = Object.normalize(activity, fetch: false)
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 defp put_status(response, activity, reading_user, opts) do
status_render_opts = Map.merge(opts, %{activity: activity, for: reading_user}) status_render_opts = Map.merge(opts, %{activity: activity, for: reading_user})
status_render = StatusView.render("show.json", status_render_opts) status_render = StatusView.render("show.json", status_render_opts)

View file

@ -0,0 +1,188 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.ChatController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
alias Pleroma.Activity
alias Pleroma.Chat
alias Pleroma.Chat.MessageReference
alias Pleroma.Object
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
alias Pleroma.Web.Plugs.OAuthScopesPlug
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, :index2, :show]
)
plug(Pleroma.Web.ApiSpec.CastAndValidate)
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 <- to_string(cm_ref.chat_id),
%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: MessageReference.delete(cm_ref)
def post_chat_message(
%{body_params: params, assigns: %{user: user}} = conn,
%{id: id}
) do
with {:ok, chat} <- Chat.get_by_user_and_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],
idempotency_key: idempotency_key(conn)
),
message <- Object.normalize(activity, fetch: false),
cm_ref <- MessageReference.for_chat_and_object(chat, message) do
conn
|> put_view(MessageReferenceView)
|> render("show.json", chat_message_reference: cm_ref)
else
{:reject, message} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{error: message})
{:error, message} ->
conn
|> put_status(:bad_request)
|> json(%{error: message})
end
end
def mark_message_as_read(
%{assigns: %{user: %{id: user_id}}} = conn,
%{id: chat_id, message_id: message_id}
) do
with %MessageReference{} = cm_ref <- MessageReference.get_by_id(message_id),
^chat_id <- to_string(cm_ref.chat_id),
%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", chat_message_reference: cm_ref)
end
end
def mark_as_read(
%{body_params: %{last_read_id: last_read_id}, assigns: %{user: user}} = conn,
%{id: id}
) do
with {:ok, chat} <- Chat.get_by_user_and_id(user, id),
{_n, _} <- MessageReference.set_all_seen_for_chat(chat, last_read_id) do
render(conn, "show.json", chat: chat)
end
end
def messages(%{assigns: %{user: user}} = conn, %{id: id} = params) do
with {:ok, chat} <- Chat.get_by_user_and_id(user, id) do
chat_message_refs =
chat
|> MessageReference.for_chat_query()
|> Pagination.fetch_paginated(params)
conn
|> add_link_headers(chat_message_refs)
|> put_view(MessageReferenceView)
|> render("index.json", chat_message_references: chat_message_refs)
end
end
def index(%{assigns: %{user: user}} = conn, params) do
chats =
index_query(user, params)
|> Repo.all()
render(conn, "index.json", chats: chats)
end
def index2(%{assigns: %{user: user}} = conn, params) do
chats =
index_query(user, params)
|> Pagination.fetch_paginated(params)
conn
|> add_link_headers(chats)
|> render("index.json", chats: chats)
end
defp index_query(%{id: user_id} = user, params) do
exclude_users =
User.cached_blocked_users_ap_ids(user) ++
if params[:with_muted], do: [], else: User.cached_muted_users_ap_ids(user)
user_id
|> Chat.for_user_query()
|> where([c], c.recipient not in ^exclude_users)
end
def create(%{assigns: %{user: user}} = conn, %{id: id}) do
with %User{ap_id: recipient} <- User.get_cached_by_id(id),
{:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do
render(conn, "show.json", chat: chat)
end
end
def show(%{assigns: %{user: user}} = conn, %{id: id}) do
with {:ok, chat} <- Chat.get_by_user_and_id(user, id) do
render(conn, "show.json", chat: chat)
end
end
defp idempotency_key(conn) do
case get_req_header(conn, "idempotency-key") do
[key] -> key
_ -> nil
end
end
end

View file

@ -0,0 +1,63 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 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.Maps
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.StatusView
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
def render(
"show.json",
%{
chat_message_reference: %{
id: id,
object: %{data: chat_message} = object,
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,
card:
StatusView.render(
"card.json",
Pleroma.Web.RichMedia.Helpers.fetch_data_for_object(object)
)
}
|> put_idempotency_key()
end
def render("index.json", opts) do
render_many(
opts[:chat_message_references],
__MODULE__,
"show.json",
Map.put(opts, :as, :chat_message_reference)
)
end
defp put_idempotency_key(data) do
with {:ok, idempotency_key} <- @cachex.get(:chat_message_id_idempotency_key_cache, data.id) do
data
|> Maps.put_if_present(:idempotency_key, idempotency_key)
else
_ -> data
end
end
end

View file

@ -0,0 +1,44 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 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)
account_view_opts = account_view_opts(opts, recipient)
%{
id: chat.id |> to_string(),
account: AccountView.render("show.json", account_view_opts),
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} = opts) do
render_many(chats, __MODULE__, "show.json", Map.delete(opts, :chats))
end
defp account_view_opts(opts, recipient) do
account_view_opts = Map.put(opts, :user, recipient)
if Map.has_key?(account_view_opts, :for) do
account_view_opts
else
Map.put(account_view_opts, :skip_visibility_check, true)
end
end
end

View file

@ -124,6 +124,13 @@ defmodule Pleroma.Web.Push.Impl do
def format_body(activity, actor, object, mastodon_type \\ nil) def format_body(activity, actor, object, mastodon_type \\ nil)
def format_body(_activity, actor, %{data: %{"type" => "ChatMessage"} = data}, _) do
case data["content"] do
nil -> "@#{actor.nickname}: (Attachment)"
content -> "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
end
end
def format_body( def format_body(
%{activity: %{data: %{"type" => "Create"}}}, %{activity: %{data: %{"type" => "Create"}}},
actor, actor,
@ -190,6 +197,7 @@ defmodule Pleroma.Web.Push.Impl do
"reblog" -> "New Repeat" "reblog" -> "New Repeat"
"favourite" -> "New Favorite" "favourite" -> "New Favorite"
"update" -> "New Update" "update" -> "New Update"
"pleroma:chat_mention" -> "New Chat Message"
"pleroma:emoji_reaction" -> "New Reaction" "pleroma:emoji_reaction" -> "New Reaction"
type -> "New #{String.capitalize(type || "event")}" type -> "New #{String.capitalize(type || "event")}"
end end

View file

@ -26,7 +26,7 @@ defmodule Pleroma.Web.Push.Subscription do
end end
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
@supported_alert_types ~w[follow favourite mention reblog poll pleroma:emoji_reaction]a @supported_alert_types ~w[follow favourite mention reblog poll pleroma:chat_mention pleroma:emoji_reaction]a
defp alerts(%{data: %{alerts: alerts}}) do defp alerts(%{data: %{alerts: alerts}}) do
alerts = Map.take(alerts, @supported_alert_types) alerts = Map.take(alerts, @supported_alert_types)

View file

@ -256,8 +256,12 @@ defmodule Pleroma.Web.Router do
patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials) patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses) get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
get("/users/:nickname/chats", AdminAPIController, :list_user_chats)
get("/statuses", StatusController, :index) get("/statuses", StatusController, :index)
get("/chats/:id", ChatController, :show)
get("/chats/:id/messages", ChatController, :messages)
end end
# AdminAPI: admins and mods (staff) can perform these actions # AdminAPI: admins and mods (staff) can perform these actions
@ -297,6 +301,8 @@ defmodule Pleroma.Web.Router do
post("/reload_emoji", AdminAPIController, :reload_emoji) post("/reload_emoji", AdminAPIController, :reload_emoji)
get("/stats", AdminAPIController, :stats) get("/stats", AdminAPIController, :stats)
delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
end end
scope "/api/v1/pleroma/emoji", Pleroma.Web.PleromaAPI do scope "/api/v1/pleroma/emoji", Pleroma.Web.PleromaAPI do
@ -425,6 +431,14 @@ defmodule Pleroma.Web.Router do
scope [] do scope [] do
pipe_through(:authenticated_api) pipe_through(:authenticated_api)
post("/chats/by-account-id/:id", ChatController, :create)
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/statuses", ConversationController, :statuses)
get("/conversations/:id", ConversationController, :show) get("/conversations/:id", ConversationController, :show)
post("/conversations/read", ConversationController, :mark_as_read) post("/conversations/read", ConversationController, :mark_as_read)
@ -490,6 +504,11 @@ defmodule Pleroma.Web.Router do
FrontendSettingsController, FrontendSettingsController,
:delete_profile :delete_profile
) )
scope "/api/v2/pleroma", Pleroma.Web.PleromaAPI do
scope [] do
pipe_through(:authenticated_api)
get("/chats", ChatController, :index2)
end
end end
scope "/api/v1", Pleroma.Web.MastodonAPI do scope "/api/v1", Pleroma.Web.MastodonAPI do

View file

@ -0,0 +1,59 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ShoutChannel do
use Phoenix.Channel
alias Pleroma.User
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.ShoutChannel.ShoutChannelState
def join("chat:public", _message, socket) do
send(self(), :after_join)
{:ok, socket}
end
def handle_info(:after_join, socket) do
push(socket, "messages", %{messages: ShoutChannelState.messages()})
{:noreply, socket}
end
def handle_in("new_msg", %{"text" => text}, %{assigns: %{user_name: user_name}} = socket) do
text = String.trim(text)
if String.length(text) in 1..Pleroma.Config.get([:shout, :limit]) do
author = User.get_cached_by_nickname(user_name)
author_json = AccountView.render("show.json", user: author, skip_visibility_check: true)
message = ShoutChannelState.add_message(%{text: text, author: author_json})
broadcast!(socket, "new_msg", message)
end
{:noreply, socket}
end
end
defmodule Pleroma.Web.ShoutChannel.ShoutChannelState do
use Agent
@max_messages 20
def start_link(_) do
Agent.start_link(fn -> %{max_id: 1, messages: []} end, name: __MODULE__)
end
def add_message(message) do
Agent.get_and_update(__MODULE__, fn state ->
id = state[:max_id] + 1
message = Map.put(message, "id", id)
messages = [message | state[:messages]] |> Enum.take(@max_messages)
{message, %{max_id: id, messages: messages}}
end)
end
def messages do
Agent.get(__MODULE__, fn state -> state[:messages] |> Enum.reverse() end)
end
end

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.Streamer do
require Logger require Logger
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Chat.MessageReference
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Notification alias Pleroma.Notification
@ -25,7 +26,7 @@ defmodule Pleroma.Web.Streamer do
def registry, do: @registry def registry, do: @registry
@public_streams ["public", "public:local", "public:media", "public:local:media"] @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." @doc "Expands and authorizes a stream, and registers the process for streaming."
@spec get_topic_and_add_socket( @spec get_topic_and_add_socket(
@ -246,6 +247,19 @@ defmodule Pleroma.Web.Streamer do
end) end)
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 defp do_stream("user", item) do
Logger.debug("Trying to push to users") Logger.debug("Trying to push to users")

View file

@ -73,7 +73,32 @@ defmodule Pleroma.Web.StreamerView do
def render("status_update.json", %Activity{} = activity, topic) do def render("status_update.json", %Activity{} = activity, topic) do
activity = Activity.get_create_by_object_ap_id_with_object(activity.object.data["id"]) activity = Activity.get_create_by_object_ap_id_with_object(activity.object.data["id"])
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("follow_relationships_update.json", item) do
%{ %{
stream: [topic], stream: [topic],
event: "status.update", event: "status.update",

View file

@ -8,6 +8,7 @@
], ],
"attachment" : [], "attachment" : [],
"capabilities" : { "capabilities" : {
"acceptsChatMessages" : true
}, },
"discoverable" : false, "discoverable" : false,
"endpoints" : { "endpoints" : {

View file

@ -124,6 +124,7 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
notify_email: "noreply@example.com", notify_email: "noreply@example.com",
description: "A Pleroma instance, an alternative fediverse server", description: "A Pleroma instance, an alternative fediverse server",
limit: 5_000, limit: 5_000,
chat_limit: 5_000,
remote_limit: 100_000, remote_limit: 100_000,
upload_limit: 16_000_000, upload_limit: 16_000_000,
avatar_upload_limit: 2_000_000, avatar_upload_limit: 2_000_000,
@ -183,7 +184,7 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
{:ok, file} = File.read(temp_file) {:ok, file} = File.read(temp_file)
assert file == assert file ==
"import Config\n\nconfig :pleroma, :instance,\n name: \"Pleroma\",\n email: \"example@example.com\",\n notify_email: \"noreply@example.com\",\n description: \"A Pleroma instance, an alternative fediverse server\",\n limit: 5000,\n remote_limit: 100_000,\n upload_limit: 16_000_000,\n avatar_upload_limit: 2_000_000,\n background_upload_limit: 4_000_000,\n banner_upload_limit: 4_000_000,\n poll_limits: %{\n max_expiration: 31_536_000,\n max_option_chars: 200,\n max_options: 20,\n min_expiration: 0\n },\n registrations_open: true,\n federating: true,\n federation_incoming_replies_max_depth: 100,\n federation_reachability_timeout_days: 7,\n federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],\n allow_relay: true,\n public: true,\n quarantined_instances: [],\n managed_config: true,\n static_dir: \"instance/static/\",\n allowed_post_formats: [\"text/plain\", \"text/html\", \"text/markdown\", \"text/bbcode\"],\n autofollowed_nicknames: [],\n max_pinned_statuses: 1,\n attachment_links: false,\n max_report_comment_size: 1000,\n safe_dm_mentions: false,\n healthcheck: false,\n remote_post_retention_days: 90,\n skip_thread_containment: true,\n limit_to_local_content: :unauthenticated,\n user_bio_length: 5000,\n user_name_length: 100,\n max_account_fields: 10,\n max_remote_account_fields: 20,\n account_field_name_length: 512,\n account_field_value_length: 2048,\n external_user_synchronization: true,\n extended_nickname_format: true,\n multi_factor_authentication: [\n totp: [digits: 6, period: 30],\n backup_codes: [number: 2, length: 6]\n ]\n" "import Config\n\nconfig :pleroma, :instance,\n name: \"Pleroma\",\n email: \"example@example.com\",\n notify_email: \"noreply@example.com\",\n description: \"A Pleroma instance, an alternative fediverse server\",\n limit: 5000,\n chat_limit: 5000,\n remote_limit: 100_000,\n upload_limit: 16_000_000,\n avatar_upload_limit: 2_000_000,\n background_upload_limit: 4_000_000,\n banner_upload_limit: 4_000_000,\n poll_limits: %{\n max_expiration: 31_536_000,\n max_option_chars: 200,\n max_options: 20,\n min_expiration: 0\n },\n registrations_open: true,\n federating: true,\n federation_incoming_replies_max_depth: 100,\n federation_reachability_timeout_days: 7,\n federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],\n allow_relay: true,\n public: true,\n quarantined_instances: [],\n managed_config: true,\n static_dir: \"instance/static/\",\n allowed_post_formats: [\"text/plain\", \"text/html\", \"text/markdown\", \"text/bbcode\"],\n autofollowed_nicknames: [],\n max_pinned_statuses: 1,\n attachment_links: false,\n max_report_comment_size: 1000,\n safe_dm_mentions: false,\n healthcheck: false,\n remote_post_retention_days: 90,\n skip_thread_containment: true,\n limit_to_local_content: :unauthenticated,\n user_bio_length: 5000,\n user_name_length: 100,\n max_account_fields: 10,\n max_remote_account_fields: 20,\n account_field_name_length: 512,\n account_field_value_length: 2048,\n external_user_synchronization: true,\n extended_nickname_format: true,\n multi_factor_authentication: [\n totp: [digits: 6, period: 30],\n backup_codes: [number: 2, length: 6]\n ]\n"
end end
end end

View file

@ -0,0 +1,29 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 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

View file

@ -0,0 +1,84 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 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 "deleting the user deletes the chat" do
user = insert(:user)
other_user = insert(:user)
{:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
Repo.delete(user)
refute Chat.get_by_id(chat.id)
end
test "deleting the recipient deletes the chat" do
user = insert(:user)
other_user = insert(:user)
{:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
Repo.delete(other_user)
refute Chat.get_by_id(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)
{:ok, chat} = time_travel(chat, -2)
{: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

View file

@ -289,4 +289,14 @@ defmodule Pleroma.Config.DeprecationWarningsTest do
Application.put_env(:tesla, :adapter, Tesla.Mock) Application.put_env(:tesla, :adapter, Tesla.Mock)
end end
test "check_old_chat_shoutbox/0" do
clear_config([:instance, :chat_limit], 1_000)
clear_config([:chat, :enabled], true)
assert capture_log(fn ->
DeprecationWarnings.check_old_chat_shoutbox()
end) =~
"Your config is using the old namespace for the Shoutbox configuration."
end
end end

View file

@ -140,6 +140,9 @@ defmodule Pleroma.Config.TransferTaskTest do
# That way we are sure that the previous call has finished before we continue. # That way we are sure that the previous call has finished before we continue.
Restarter.Pleroma.rebooted?() Restarter.Pleroma.rebooted?()
end) =~ "pleroma restarted" end) =~ "pleroma restarted"
clear_config(:shout)
insert(:config, key: :shout, value: [enabled: false])
assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted"
end end
test "on reboot time subkey" do test "on reboot time subkey" do
@ -164,6 +167,10 @@ defmodule Pleroma.Config.TransferTaskTest do
clear_config(Pleroma.Captcha) clear_config(Pleroma.Captcha)
insert(:config, key: :rate_limit, value: [enabled: false]) insert(:config, key: :rate_limit, value: [enabled: false])
clear_config(:shout)
clear_config(Pleroma.Captcha)
insert(:config, key: :shout, value: [enabled: false])
insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60]) insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60])
refute String.contains?( refute String.contains?(

View file

@ -19,6 +19,7 @@ defmodule Pleroma.MigrationHelper.NotificationBackfillTest do
other_user = insert(:user) other_user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "yeah, @#{other_user.nickname}"}) {: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, react} = CommonAPI.react_with_emoji(post.id, other_user, "")
{:ok, like} = CommonAPI.favorite(other_user, post.id) {:ok, like} = CommonAPI.favorite(other_user, post.id)
{:ok, react_2} = CommonAPI.react_with_emoji(post.id, other_user, "") {:ok, react_2} = CommonAPI.react_with_emoji(post.id, other_user, "")
@ -32,7 +33,7 @@ defmodule Pleroma.MigrationHelper.NotificationBackfillTest do
|> Activity.change(%{data: data}) |> Activity.change(%{data: data})
|> Repo.update() |> Repo.update()
assert {4, nil} = Repo.update_all(Notification, set: [type: nil]) assert {5, nil} = Repo.update_all(Notification, set: [type: nil])
NotificationBackfill.fill_in_notification_types() NotificationBackfill.fill_in_notification_types()
@ -47,6 +48,9 @@ defmodule Pleroma.MigrationHelper.NotificationBackfillTest do
assert %{type: "pleroma:emoji_reaction"} = assert %{type: "pleroma:emoji_reaction"} =
Repo.get_by(Notification, user_id: user.id, activity_id: react_2.id) 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 end
end end

View file

@ -0,0 +1,52 @@
defmodule Pleroma.Repo.Migrations.RenameInstanceChatTest do
use Pleroma.DataCase
import Pleroma.Factory
import Pleroma.Tests.Helpers
alias Pleroma.ConfigDB
setup do: clear_config([:instance])
setup do: clear_config([:chat])
setup_all do: require_migration("20200806175913_rename_instance_chat")
describe "up/0" do
test "migrates chat settings to shout", %{migration: migration} do
insert(:config, group: :pleroma, key: :instance, value: [chat_limit: 6000])
insert(:config, group: :pleroma, key: :chat, value: [enabled: true])
assert migration.up() == :ok
assert ConfigDB.get_by_params(%{group: :pleroma, key: :chat}) == nil
assert ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) == nil
assert ConfigDB.get_by_params(%{group: :pleroma, key: :shout}).value == [
limit: 6000,
enabled: true
]
end
test "does nothing when chat settings are not set", %{migration: migration} do
assert migration.up() == :noop
assert ConfigDB.get_by_params(%{group: :pleroma, key: :chat}) == nil
assert ConfigDB.get_by_params(%{group: :pleroma, key: :shout}) == nil
end
end
describe "down/0" do
test "migrates shout settings back to instance and chat", %{migration: migration} do
insert(:config, group: :pleroma, key: :shout, value: [limit: 42, enabled: true])
assert migration.down() == :ok
assert ConfigDB.get_by_params(%{group: :pleroma, key: :chat}).value == [enabled: true]
assert ConfigDB.get_by_params(%{group: :pleroma, key: :instance}).value == [chat_limit: 42]
assert ConfigDB.get_by_params(%{group: :pleroma, key: :shout}) == nil
end
test "does nothing when shout settings are not set", %{migration: migration} do
assert migration.down() == :noop
assert ConfigDB.get_by_params(%{group: :pleroma, key: :chat}) == nil
assert ConfigDB.get_by_params(%{group: :pleroma, key: :instance}) == nil
assert ConfigDB.get_by_params(%{group: :pleroma, key: :shout}) == nil
end
end
end

View file

@ -0,0 +1,36 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.WelcomeChatMessageTest do
use Pleroma.DataCase
alias Pleroma.User.WelcomeChatMessage
import Pleroma.Factory
setup do: clear_config([:welcome])
describe "post_message/1" do
test "send a chat welcome message" do
welcome_user = insert(:user, name: "mewmew")
user = insert(:user)
clear_config([:welcome, :chat_message, :enabled], true)
clear_config([:welcome, :chat_message, :sender_nickname], welcome_user.nickname)
clear_config(
[:welcome, :chat_message, :message],
"Hello, welcome to Blob/Cat!"
)
{:ok, %Pleroma.Activity{} = activity} = WelcomeChatMessage.post_message(user)
assert user.ap_id in activity.recipients
assert Pleroma.Object.normalize(activity, fetch: false).data["type"] == "ChatMessage"
assert Pleroma.Object.normalize(activity, fetch: false).data["content"] ==
"Hello, welcome to Blob/Cat!"
end
end
end

View file

@ -443,6 +443,22 @@ defmodule Pleroma.UserTest do
assert activity.actor == welcome_user.ap_id assert activity.actor == welcome_user.ap_id
end end
test "it sends a welcome chat message if it is set" do
welcome_user = insert(:user)
clear_config([:welcome, :chat_message, :enabled], true)
clear_config([:welcome, :chat_message, :sender_nickname], welcome_user.nickname)
clear_config([:welcome, :chat_message, :message], "Hello, this is a chat message")
cng = User.register_changeset(%User{}, @full_user_data)
{:ok, registered_user} = User.register(cng)
ObanHelpers.perform_all()
activity = Repo.one(Pleroma.Activity)
assert registered_user.ap_id in activity.recipients
assert Object.normalize(activity, fetch: false).data["content"] =~ "chat message"
assert activity.actor == welcome_user.ap_id
end
setup do: setup do:
clear_config( clear_config(
[:mrf_simple], [:mrf_simple],
@ -467,6 +483,24 @@ defmodule Pleroma.UserTest do
] ]
) )
test "it sends a welcome chat message when Simple policy applied to local instance" do
clear_config([:mrf_simple, :media_nsfw], [{"localhost", ""}])
welcome_user = insert(:user)
clear_config([:welcome, :chat_message, :enabled], true)
clear_config([:welcome, :chat_message, :sender_nickname], welcome_user.nickname)
clear_config([:welcome, :chat_message, :message], "Hello, this is a chat message")
cng = User.register_changeset(%User{}, @full_user_data)
{:ok, registered_user} = User.register(cng)
ObanHelpers.perform_all()
activity = Repo.one(Pleroma.Activity)
assert registered_user.ap_id in activity.recipients
assert Object.normalize(activity, fetch: false).data["content"] =~ "chat message"
assert activity.actor == welcome_user.ap_id
end
test "it sends a welcome email message if it is set" do test "it sends a welcome email message if it is set" do
welcome_user = insert(:user) welcome_user = insert(:user)
clear_config([:welcome, :email, :enabled], true) clear_config([:welcome, :email, :enabled], true)
@ -632,6 +666,15 @@ defmodule Pleroma.UserTest do
assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers" assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers"
end end
test "it sets the 'accepts_chat_messages' set to true" do
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
{:ok, user} = Repo.insert(changeset)
assert user.accepts_chat_messages
end
test "it creates a confirmed user" do test "it creates a confirmed user" do
changeset = User.register_changeset(%User{}, @full_user_data) changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid? assert changeset.valid?

View file

@ -187,6 +187,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert User.invisible?(user) assert User.invisible?(user)
end end
test "it returns a user that accepts chat messages" do
user_id = "http://mastodon.example.org/users/admin"
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
assert user.accepts_chat_messages
end
test "works for guppe actors" do test "works for guppe actors" do
user_id = "https://gup.pe/u/bernie2020" user_id = "https://gup.pe/u/bernie2020"

View file

@ -8,6 +8,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicyTest do
import Pleroma.Web.ActivityPub.MRF.HellthreadPolicy import Pleroma.Web.ActivityPub.MRF.HellthreadPolicy
alias Pleroma.Web.CommonAPI
setup do setup do
user = insert(:user) user = insert(:user)
@ -46,6 +48,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicyTest do
setup do: clear_config(:mrf_hellthread) setup do: clear_config(:mrf_hellthread)
test "doesn't die on chat messages" do
clear_config([:mrf_hellthread], %{delist_threshold: 2, reject_threshold: 0})
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post_chat_message(user, other_user, "moin")
assert {:ok, _} = filter(activity.data)
end
describe "reject" do describe "reject" do
test "rejects the message if the recipient count is above reject_threshold", %{ test "rejects the message if the recipient count is above reject_threshold", %{
message: message, message: message,

View file

@ -0,0 +1,212 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatValidationTest 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.CommonAPI
import Pleroma.Factory
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, fetch: 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 valid_chat_message == object
assert match?(%{"firefox" => _}, object["emoji"])
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/jpeg",
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/jpeg",
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/jpeg",
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
clear_config([: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 recipient is not accepting chat messages", %{
valid_chat_message: valid_chat_message,
recipient: recipient
} do
recipient
|> Ecto.Changeset.change(%{accepts_chat_messages: false})
|> Pleroma.Repo.update!()
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
end

View file

@ -7,6 +7,8 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
use Pleroma.DataCase use Pleroma.DataCase
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Chat
alias Pleroma.Chat.MessageReference
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
@ -15,7 +17,6 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.SideEffects alias Pleroma.Web.ActivityPub.SideEffects
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
import Mock import Mock
@ -55,22 +56,15 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
author = insert(:user, local: true) author = insert(:user, local: true)
recipient = insert(:user, local: true) recipient = insert(:user, local: true)
{:ok, note_data, _meta} = {:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
Builder.note(%Pleroma.Web.CommonAPI.ActivityDraft{
user: author,
to: [recipient.ap_id],
mentions: [recipient],
content_html: "hey",
extra: %{"id" => Utils.generate_object_id()}
})
{:ok, create_activity_data, _meta} = {:ok, create_activity_data, _meta} =
Builder.create(author, note_data["id"], [recipient.ap_id]) 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} = ActivityPub.persist(create_activity_data, local: false)
{:ok, _create_activity, meta} = {:ok, _create_activity, meta} =
SideEffects.handle(create_activity, local: false, object_data: note_data) SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
assert [notification] = meta[:notifications] assert [notification] = meta[:notifications]
@ -93,6 +87,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
SideEffects.handle_after_transaction(meta) SideEffects.handle_after_transaction(meta)
assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification)) 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)) assert called(Pleroma.Web.Push.send(notification))
end end
end end
@ -665,6 +660,147 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
end end
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 describe "announce objects" do
setup do setup do
poster = insert(:user) poster = insert(:user)

View file

@ -0,0 +1,171 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 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")
|> Jason.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")
|> Jason.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")
|> Jason.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")
|> Jason.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 doesn't work for deactivated users" do
data =
File.read!("test/fixtures/create-chat-message.json")
|> Jason.decode!()
_author =
insert(:user,
ap_id: data["actor"],
local: false,
last_refreshed_at: DateTime.utc_now(),
is_active: false
)
_recipient = insert(:user, ap_id: List.first(data["to"]), local: true)
assert {:error, _} = Transmogrifier.handle_incoming(data)
end
test "it inserts it and creates a chat" do
data =
File.read!("test/fixtures/create-chat-message.json")
|> Jason.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

View file

@ -157,4 +157,23 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user}) assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user})
end end
end end
describe "acceptsChatMessages" do
test "it returns this value if it is set" do
true_user = insert(:user, accepts_chat_messages: true)
false_user = insert(:user, accepts_chat_messages: false)
nil_user = insert(:user, accepts_chat_messages: nil)
assert %{"capabilities" => %{"acceptsChatMessages" => true}} =
UserView.render("user.json", user: true_user)
assert %{"capabilities" => %{"acceptsChatMessages" => false}} =
UserView.render("user.json", user: false_user)
refute Map.has_key?(
UserView.render("user.json", user: nil_user)["capabilities"],
"acceptsChatMessages"
)
end
end
end end

View file

@ -420,6 +420,56 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end end
end end
describe "GET /api/pleroma/admin/users/:nickname/chats" do
setup do
user = insert(:user)
recipients = insert_list(3, :user)
Enum.each(recipients, fn recipient ->
CommonAPI.post_chat_message(user, recipient, "yo")
end)
%{user: user}
end
test "renders user's chats", %{conn: conn, user: user} do
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/chats")
assert json_response(conn, 200) |> length() == 3
end
end
describe "GET /api/pleroma/admin/users/:nickname/chats unauthorized" do
setup do
user = insert(:user)
recipient = insert(:user)
CommonAPI.post_chat_message(user, recipient, "yo")
%{conn: conn} = oauth_access(["read:chats"])
%{conn: conn, user: user}
end
test "returns 403", %{conn: conn, user: user} do
conn
|> get("/api/pleroma/admin/users/#{user.nickname}/chats")
|> json_response(403)
end
end
describe "GET /api/pleroma/admin/users/:nickname/chats unauthenticated" do
setup do
user = insert(:user)
recipient = insert(:user)
CommonAPI.post_chat_message(user, recipient, "yo")
%{conn: build_conn(), user: user}
end
test "returns 403", %{conn: conn, user: user} do
conn
|> get("/api/pleroma/admin/users/#{user.nickname}/chats")
|> json_response(403)
end
end
describe "GET /api/v1/pleroma/admin/moderation_log" do describe "GET /api/v1/pleroma/admin/moderation_log" do
setup do setup do
moderator = insert(:user, is_moderator: true) moderator = insert(:user, is_moderator: true)

View file

@ -0,0 +1,218 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.ChatControllerTest do
use Pleroma.Web.ConnCase, async: true
import Pleroma.Factory
alias Pleroma.Chat
alias Pleroma.Chat.MessageReference
alias Pleroma.ModerationLog
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Web.CommonAPI
defp admin_setup do
admin = insert(:user, is_admin: true)
token = insert(:oauth_admin_token, user: admin)
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
{:ok, %{admin: admin, token: token, conn: conn}}
end
describe "DELETE /api/pleroma/admin/chats/:id/messages/:message_id" do
setup do: admin_setup()
test "it deletes a message from the chat", %{conn: conn, admin: admin} do
user = insert(:user)
recipient = insert(:user)
{:ok, message} =
CommonAPI.post_chat_message(user, recipient, "Hello darkness my old friend")
object = Object.normalize(message, fetch: false)
chat = Chat.get(user.id, recipient.ap_id)
recipient_chat = Chat.get(recipient.id, user.ap_id)
cm_ref = MessageReference.for_chat_and_object(chat, object)
recipient_cm_ref = MessageReference.for_chat_and_object(recipient_chat, object)
result =
conn
|> put_req_header("content-type", "application/json")
|> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}")
|> json_response_and_validate_schema(200)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deleted chat message ##{cm_ref.id}"
assert result["id"] == cm_ref.id
refute MessageReference.get_by_id(cm_ref.id)
refute MessageReference.get_by_id(recipient_cm_ref.id)
assert %{data: %{"type" => "Tombstone"}} = Object.get_by_id(object.id)
end
end
describe "GET /api/pleroma/admin/chats/:id/messages" do
setup do: admin_setup()
test "it paginates", %{conn: conn} do
user = insert(:user)
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/pleroma/admin/chats/#{chat.id}/messages")
|> json_response_and_validate_schema(200)
assert length(result) == 20
result =
conn
|> get("/api/pleroma/admin/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} do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, _} = CommonAPI.post_chat_message(user, other_user, "hey")
{:ok, _} = CommonAPI.post_chat_message(user, third_user, "hey")
{:ok, _} = CommonAPI.post_chat_message(user, other_user, "how are you?")
{:ok, _} = CommonAPI.post_chat_message(other_user, user, "fine, how about you?")
chat = Chat.get(user.id, other_user.ap_id)
result =
conn
|> get("/api/pleroma/admin/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
end
end
describe "GET /api/pleroma/admin/chats/:id" do
setup do: admin_setup()
test "it returns a chat", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
result =
conn
|> get("/api/pleroma/admin/chats/#{chat.id}")
|> json_response_and_validate_schema(200)
assert result["id"] == to_string(chat.id)
assert %{} = result["sender"]
assert %{} = result["receiver"]
refute result["account"]
end
end
describe "unauthorized chat moderation" do
setup do
user = insert(:user)
recipient = insert(:user)
{:ok, message} = CommonAPI.post_chat_message(user, recipient, "Yo")
object = Object.normalize(message, fetch: false)
chat = Chat.get(user.id, recipient.ap_id)
cm_ref = MessageReference.for_chat_and_object(chat, object)
%{conn: conn} = oauth_access(["read:chats", "write:chats"])
%{conn: conn, chat: chat, cm_ref: cm_ref}
end
test "DELETE /api/pleroma/admin/chats/:id/messages/:message_id", %{
conn: conn,
chat: chat,
cm_ref: cm_ref
} do
conn
|> put_req_header("content-type", "application/json")
|> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}")
|> json_response(403)
assert MessageReference.get_by_id(cm_ref.id) == cm_ref
end
test "GET /api/pleroma/admin/chats/:id/messages", %{conn: conn, chat: chat} do
conn
|> get("/api/pleroma/admin/chats/#{chat.id}/messages")
|> json_response(403)
end
test "GET /api/pleroma/admin/chats/:id", %{conn: conn, chat: chat} do
conn
|> get("/api/pleroma/admin/chats/#{chat.id}")
|> json_response(403)
end
end
describe "unauthenticated chat moderation" do
setup do
user = insert(:user)
recipient = insert(:user)
{:ok, message} = CommonAPI.post_chat_message(user, recipient, "Yo")
object = Object.normalize(message, fetch: false)
chat = Chat.get(user.id, recipient.ap_id)
cm_ref = MessageReference.for_chat_and_object(chat, object)
%{conn: build_conn(), chat: chat, cm_ref: cm_ref}
end
test "DELETE /api/pleroma/admin/chats/:id/messages/:message_id", %{
conn: conn,
chat: chat,
cm_ref: cm_ref
} do
conn
|> put_req_header("content-type", "application/json")
|> delete("/api/pleroma/admin/chats/#{chat.id}/messages/#{cm_ref.id}")
|> json_response(403)
assert MessageReference.get_by_id(cm_ref.id) == cm_ref
end
test "GET /api/pleroma/admin/chats/:id/messages", %{conn: conn, chat: chat} do
conn
|> get("/api/pleroma/admin/chats/#{chat.id}/messages")
|> json_response(403)
end
test "GET /api/pleroma/admin/chats/:id", %{conn: conn, chat: chat} do
conn
|> get("/api/pleroma/admin/chats/#{chat.id}")
|> json_response(403)
end
end
end

File diff suppressed because it is too large Load diff

View file

@ -7,11 +7,13 @@ defmodule Pleroma.Web.CommonAPITest do
use Pleroma.DataCase use Pleroma.DataCase
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Chat
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.AccountView
@ -115,6 +117,173 @@ defmodule Pleroma.Web.CommonAPITest do
end end
end end
describe "posting chat messages" do
setup do: clear_config([:instance, :chat_limit])
test "it posts a self-chat" do
author = insert(:user)
recipient = author
{:ok, activity} =
CommonAPI.post_chat_message(
author,
recipient,
"remember to buy milk when milk truk arive"
)
assert activity.data["type"] == "Create"
end
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/jpeg",
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, fetch: 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, fetch: 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, fetch: false)
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
clear_config([: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
test "it reject messages via MRF" do
clear_config([:mrf_keyword, :reject], ["GNO"])
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
author = insert(:user)
recipient = insert(:user)
assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} ==
CommonAPI.post_chat_message(author, recipient, "GNO/Linux")
end
end
describe "unblocking" do describe "unblocking" do
test "it works even without an existing block activity" do test "it works even without an existing block activity" do
blocked = insert(:user) blocked = insert(:user)

View file

@ -1711,6 +1711,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
response = json_response_and_validate_schema(conn, 200) response = json_response_and_validate_schema(conn, 200)
assert %{"id" => id, "source" => %{"privacy" => "public"}} = response assert %{"id" => id, "source" => %{"privacy" => "public"}} = response
assert response["pleroma"]["chat_token"]
assert response["pleroma"]["unread_notifications_count"] == 6 assert response["pleroma"]["unread_notifications_count"] == 6
assert id == to_string(user.id) assert id == to_string(user.id)
end end

View file

@ -39,6 +39,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
"background_upload_limit" => _, "background_upload_limit" => _,
"banner_upload_limit" => _, "banner_upload_limit" => _,
"background_image" => from_config_background, "background_image" => from_config_background,
"shout_limit" => _,
"description_limit" => _ "description_limit" => _
} = result } = result

View file

@ -52,6 +52,27 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
assert response == expected_response assert response == expected_response
end 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 "by default, does not contain pleroma:report" do test "by default, does not contain pleroma:report" do
%{user: user, conn: conn} = oauth_access(["read:notifications"]) %{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user = insert(:user) other_user = insert(:user)

View file

@ -113,6 +113,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do
"favourite" => true, "favourite" => true,
"follow" => true, "follow" => true,
"reblog" => true, "reblog" => true,
"pleroma:chat_mention" => true,
"pleroma:emoji_reaction" => true "pleroma:emoji_reaction" => true
} }
}, },
@ -128,6 +129,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do
"favourite" => true, "favourite" => true,
"follow" => true, "follow" => true,
"reblog" => true, "reblog" => true,
"pleroma:chat_mention" => true,
"pleroma:emoji_reaction" => true "pleroma:emoji_reaction" => true
}, },
"endpoint" => subscription.endpoint, "endpoint" => subscription.endpoint,
@ -183,6 +185,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do
"favourite" => true, "favourite" => true,
"follow" => true, "follow" => true,
"reblog" => true, "reblog" => true,
"pleroma:chat_mention" => true,
"pleroma:emoji_reaction" => true "pleroma:emoji_reaction" => true
} }
} }
@ -201,6 +204,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do
"favourite" => false, "favourite" => false,
"follow" => false, "follow" => false,
"reblog" => false, "reblog" => false,
"pleroma:chat_mention" => false,
"pleroma:emoji_reaction" => false "pleroma:emoji_reaction" => false
} }
} }
@ -213,6 +217,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do
"favourite" => false, "favourite" => false,
"follow" => false, "follow" => false,
"reblog" => false, "reblog" => false,
"pleroma:chat_mention" => false,
"pleroma:emoji_reaction" => false "pleroma:emoji_reaction" => false
}, },
"endpoint" => "https://example.com/example/1234", "endpoint" => "https://example.com/example/1234",

View file

@ -104,6 +104,13 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do
assert user_data["locked"] == true assert user_data["locked"] == true
end end
test "updates the user's chat acceptance status", %{conn: conn} do
conn = patch(conn, "/api/v1/accounts/update_credentials", %{accepts_chat_messages: "false"})
assert user_data = json_response_and_validate_schema(conn, 200)
assert user_data["pleroma"]["accepts_chat_messages"] == false
end
test "updates the user's allow_following_move", %{user: user, conn: conn} do test "updates the user's allow_following_move", %{user: user, conn: conn} do
assert user.allow_following_move == true assert user.allow_following_move == true

View file

@ -106,7 +106,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
hide_followers_count: false, hide_followers_count: false,
hide_follows_count: false, hide_follows_count: false,
relationship: %{}, relationship: %{},
skip_thread_containment: false skip_thread_containment: false,
accepts_chat_messages: nil
} }
} }
@ -264,7 +265,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
hide_followers_count: false, hide_followers_count: false,
hide_follows_count: false, hide_follows_count: false,
relationship: %{}, relationship: %{},
skip_thread_containment: false skip_thread_containment: false,
accepts_chat_messages: nil
} }
} }

View file

@ -6,6 +6,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
use Pleroma.DataCase use Pleroma.DataCase
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Chat
alias Pleroma.Chat.MessageReference
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
@ -20,6 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
import Pleroma.Factory import Pleroma.Factory
defp test_notifications_rendering(notifications, user, expected_result) do defp test_notifications_rendering(notifications, user, expected_result) do
@ -37,6 +40,30 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
assert expected_result == result assert expected_result == result
end 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, fetch: false)
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, is_muted: 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 test "Mention notification" do
user = insert(:user) user = insert(:user)
mentioned_user = insert(:user) mentioned_user = insert(:user)

View file

@ -0,0 +1,453 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do
use Pleroma.Web.ConnCase
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, fetch: 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, fetch: 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")
|> put_req_header("idempotency-key", "123")
|> 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()
assert result["idempotency_key"] == "123"
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 %{"error" => "no_content"} == result
end
test "it works with an attachment", %{conn: conn, user: user} do
clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
clear_config([Pleroma.Uploaders.Local, :uploads], "uploads")
file = %Plug.Upload{
content_type: "image/jpeg",
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
test "gets MRF reason when rejected", %{conn: conn, user: user} do
clear_config([:mrf_keyword, :reject], ["GNO"])
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
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" => "GNO/Linux"})
|> json_response_and_validate_schema(422)
assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} == result
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, fetch: 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, fetch: 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)
response = get(conn, "/api/v1/pleroma/chats/#{chat.id}/messages")
result = json_response_and_validate_schema(response, 200)
[next, prev] = get_resp_header(response, "link") |> hd() |> String.split(", ")
api_endpoint = "/api/v1/pleroma/chats/"
assert String.match?(
next,
~r(#{api_endpoint}.*/messages\?limit=\d+&max_id=.*; rel=\"next\"$)
)
assert String.match?(
prev,
~r(#{api_endpoint}.*/messages\?limit=\d+&min_id=.*; rel=\"prev\"$)
)
assert length(result) == 20
response =
get(conn, "/api/v1/pleroma/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}")
result = json_response_and_validate_schema(response, 200)
[next, prev] = get_resp_header(response, "link") |> hd() |> String.split(", ")
assert String.match?(
next,
~r(#{api_endpoint}.*/messages\?limit=\d+&max_id=.*; rel=\"next\"$)
)
assert String.match?(
prev,
~r(#{api_endpoint}.*/messages\?limit=\d+&max_id=.*&min_id=.*; rel=\"prev\"$)
)
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
other_user_chat = Chat.get(other_user.id, user.ap_id)
conn
|> get("/api/v1/pleroma/chats/#{other_user_chat.id}/messages")
|> json_response_and_validate_schema(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/v2/pleroma/chats" do
setup do: oauth_access(["read:chats"])
test "it does not return chats with deleted users", %{conn: conn, user: user} do
recipient = insert(:user)
{:ok, _} = Chat.get_or_create(user.id, recipient.ap_id)
Pleroma.Repo.delete(recipient)
User.invalidate_cache(recipient)
result =
conn
|> get("/api/v2/pleroma/chats")
|> json_response_and_validate_schema(200)
assert length(result) == 0
end
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/v2/pleroma/chats")
|> json_response_and_validate_schema(200)
assert length(result) == 1
User.block(user, recipient)
result =
conn
|> get("/api/v2/pleroma/chats")
|> json_response_and_validate_schema(200)
assert length(result) == 0
end
test "it does not return chats with users you muted", %{conn: conn, user: user} do
recipient = insert(:user)
{:ok, _} = Chat.get_or_create(user.id, recipient.ap_id)
result =
conn
|> get("/api/v2/pleroma/chats")
|> json_response_and_validate_schema(200)
assert length(result) == 1
User.mute(user, recipient)
result =
conn
|> get("/api/v2/pleroma/chats")
|> json_response_and_validate_schema(200)
assert length(result) == 0
result =
conn
|> get("/api/v2/pleroma/chats?with_muted=true")
|> json_response_and_validate_schema(200)
assert length(result) == 1
end
test "it paginates 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/v2/pleroma/chats")
|> json_response_and_validate_schema(200)
assert length(result) == 20
last_id = List.last(result)["id"]
result =
conn
|> get("/api/v2/pleroma/chats?max_id=#{last_id}")
|> json_response_and_validate_schema(200)
assert length(result) == 10
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)
{:ok, chat_1} = time_travel(chat_1, -3)
{:ok, chat_2} = Chat.get_or_create(user.id, jafnhar.ap_id)
{:ok, _chat_2} = time_travel(chat_2, -2)
{:ok, chat_3} = Chat.get_or_create(user.id, tridi.ap_id)
{:ok, chat_3} = time_travel(chat_3, -1)
# bump the second one
{:ok, chat_2} = Chat.bump_or_create(user.id, jafnhar.ap_id)
result =
conn
|> get("/api/v2/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
test "it is not affected by :restrict_unauthenticated setting (issue #1973)", %{
conn: conn,
user: user
} do
clear_config([:restrict_unauthenticated, :profiles, :local], true)
clear_config([:restrict_unauthenticated, :profiles, :remote], true)
user2 = insert(:user)
user3 = insert(:user, local: false)
{:ok, _chat_12} = Chat.get_or_create(user.id, user2.ap_id)
{:ok, _chat_13} = Chat.get_or_create(user.id, user3.ap_id)
result =
conn
|> get("/api/v2/pleroma/chats")
|> json_response_and_validate_schema(200)
account_ids = Enum.map(result, &get_in(&1, ["account", "id"]))
assert Enum.sort(account_ids) == Enum.sort([user2.id, user3.id])
end
end
end

View file

@ -0,0 +1,75 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceViewTest 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/jpeg",
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:", idempotency_key: "123")
chat = Chat.get(user.id, recipient.ap_id)
object = Object.normalize(activity, fetch: false)
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])
assert chat_message[:idempotency_key] == "123"
clear_config([:rich_media, :enabled], true)
Tesla.Mock.mock_global(fn
%{url: "https://example.com/ogp"} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")}
end)
{:ok, activity} =
CommonAPI.post_chat_message(recipient, user, "gkgkgk https://example.com/ogp",
media_id: upload.id
)
object = Object.normalize(activity, fetch: false)
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] == object.data["content"]
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
assert chat_message_two[:card]
end
end

View file

@ -0,0 +1,49 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.ChatViewTest do
use Pleroma.DataCase, async: true
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, skip_visibility_check: true),
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, fetch: 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

View file

@ -7,8 +7,10 @@ defmodule Pleroma.Web.Push.ImplTest do
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Push.Impl alias Pleroma.Web.Push.Impl
alias Pleroma.Web.Push.Subscription alias Pleroma.Web.Push.Subscription
@ -229,6 +231,46 @@ defmodule Pleroma.Web.Push.ImplTest do
end end
describe "build_content/3" do 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, fetch: 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/jpeg",
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, fetch: 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 contents of notifications when option enabled" do test "hides contents of notifications when option enabled" do
user = insert(:user, nickname: "Bob") user = insert(:user, nickname: "Bob")

View file

@ -0,0 +1,41 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ShoutChannelTest do
use Pleroma.Web.ChannelCase
alias Pleroma.Web.ShoutChannel
alias Pleroma.Web.UserSocket
import Pleroma.Factory
setup do
user = insert(:user)
{:ok, _, socket} =
socket(UserSocket, "", %{user_name: user.nickname})
|> subscribe_and_join(ShoutChannel, "chat:public")
{:ok, socket: socket}
end
test "it broadcasts a message", %{socket: socket} do
push(socket, "new_msg", %{"text" => "why is tenshi eating a corndog so cute?"})
assert_broadcast("new_msg", %{text: "why is tenshi eating a corndog so cute?"})
end
describe "message lengths" do
setup do: clear_config([:shout, :limit])
test "it ignores messages of length zero", %{socket: socket} do
push(socket, "new_msg", %{"text" => ""})
refute_broadcast("new_msg", %{text: ""})
end
test "it ignores messages above a certain length", %{socket: socket} do
clear_config([:shout, :limit], 2)
push(socket, "new_msg", %{"text" => "123"})
refute_broadcast("new_msg", %{text: "123"})
end
end
end

View file

@ -7,11 +7,15 @@ defmodule Pleroma.Web.StreamerTest do
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.Chat
alias Pleroma.Chat.MessageReference
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.List alias Pleroma.List
alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Streamer alias Pleroma.Web.Streamer
alias Pleroma.Web.StreamerView
@moduletag needs_streamer: true, capture_log: true @moduletag needs_streamer: true, capture_log: true
@ -76,16 +80,20 @@ defmodule Pleroma.Web.StreamerTest do
expected_user_topic = "user:#{user.id}" expected_user_topic = "user:#{user.id}"
expected_notification_topic = "user:notification:#{user.id}" expected_notification_topic = "user:notification:#{user.id}"
expected_direct_topic = "direct:#{user.id}" expected_direct_topic = "direct:#{user.id}"
expected_pleroma_chat_topic = "user:pleroma_chat:#{user.id}"
for valid_user_token <- [read_oauth_token, read_statuses_token] do for valid_user_token <- [read_oauth_token, read_statuses_token] do
assert {:ok, ^expected_user_topic} = Streamer.get_topic("user", user, valid_user_token) assert {:ok, ^expected_user_topic} = Streamer.get_topic("user", user, valid_user_token)
assert {:ok, ^expected_direct_topic} = assert {:ok, ^expected_direct_topic} =
Streamer.get_topic("direct", user, valid_user_token) Streamer.get_topic("direct", user, valid_user_token)
assert {:ok, ^expected_pleroma_chat_topic} =
Streamer.get_topic("user:pleroma_chat", user, valid_user_token)
end end
for invalid_user_token <- [read_notifications_token, badly_scoped_token], for invalid_user_token <- [read_notifications_token, badly_scoped_token],
user_topic <- ["user", "direct"] do user_topic <- ["user", "direct", "user:pleroma_chat"] do
assert {:error, :unauthorized} = Streamer.get_topic(user_topic, user, invalid_user_token) assert {:error, :unauthorized} = Streamer.get_topic(user_topic, user, invalid_user_token)
end end
@ -258,6 +266,66 @@ defmodule Pleroma.Web.StreamerTest do
refute Streamer.filtered_by_user?(user, notify) refute Streamer.filtered_by_user?(user, notify)
end end
test "it sends chat messages to the 'user:pleroma_chat' stream", %{
user: user,
token: oauth_token
} do
other_user = insert(:user)
{:ok, create_activity} =
CommonAPI.post_chat_message(other_user, user, "hey cirno", idempotency_key: "123")
object = Object.normalize(create_activity, fetch: 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, oauth_token)
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, token: oauth_token} do
other_user = insert(:user)
{:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno")
object = Object.normalize(create_activity, fetch: 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, oauth_token)
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,
token: oauth_token
} 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, oauth_token)
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", %{ test "it doesn't send notify to the 'user:notification' stream when a user is blocked", %{
user: user, user: user,
token: oauth_token token: oauth_token