Bring chat and shoutbox back

This commit is contained in:
Zero 2022-11-29 13:30:48 -05:00 committed by Sam Therapy
parent 8e99b530c4
commit 8bb003a6ca
Signed by: sam
GPG key ID: 4D8B07C18F31ACBD
102 changed files with 6666 additions and 77 deletions

View file

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

View file

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

View file

@ -956,7 +956,7 @@
key: :privileged_staff,
type: :boolean,
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,
@ -1000,6 +1000,35 @@
}
]
},
%{
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,
type: :keyword,
@ -2651,6 +2680,27 @@
}
]
},
%{
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,
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.
## :shout
* `enabled` - Enables the backend Shoutbox chat feature. Defaults to `true`.
* `limit` - Shout character limit. Defaults to `5_000`
## :instance
* `name`: The instances name.
* `email`: Email used to reach an Administrator/Moderator of the instance.
@ -74,6 +79,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`.
* `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.
* `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.
* `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.

View file

@ -1031,6 +1031,7 @@ Most of the settings will be applied in `runtime`, this means that you don't nee
- `:hackney_pools`
- `:connections_pool`
- `:pools`
- `:chat`
- partially settings inside these keys:
- `:seconds_valid` in `Pleroma.Captcha`
- `:proxy_remote` in `Pleroma.Upload`
@ -1410,6 +1411,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 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_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`
- `chat_token`: The token needed for Akkoma shoutbox. Only returned in `/api/v1/accounts/verify_credentials`
- `deactivated`: boolean, true when the user is deactivated
- `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_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.
- `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
### Source
@ -161,6 +163,15 @@ The `type` value is `pleroma:emoji_reaction`. Has these fields:
- `account`: The account of the user who reacted
- `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)
This notification has to be requested explicitly.
@ -175,7 +186,7 @@ The `type` value is `pleroma:report`
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`.
- `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`
@ -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.
- `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.
- `accepts_chat_messages` - if false, this account will reject all chat messages.
- `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.
@ -292,6 +304,7 @@ Has these additional parameters (which are the same as in Akkoma-API):
`GET /api/v1/instance` has additional fields
- `max_toot_chars`: The maximum characters per post
- `chat_limit`: The maximum characters per chat message
- `description_limit`: The maximum characters per image description
- `poll_limits`: The limits of polls
- `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:
- pleroma:chat_mention
- pleroma:emoji_reaction
## Markers
@ -322,6 +336,10 @@ Has these additional fields under the `pleroma` object:
## 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
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",
"multifetch",
"pleroma:api/v1/notifications:include_types_filter",
"chat",
"shout",
"relay",
"pleroma_emoji_reactions"
"pleroma_emoji_reactions",
"pleroma_chat_messages"
],
"federation":{
"enabled":true,
@ -201,8 +204,11 @@ See also [the Nodeinfo standard](https://nodeinfo.diaspora.software/).
"shareable_emoji_packs",
"multifetch",
"pleroma:api/v1/notifications:include_types_filter",
"chat",
"shout",
"relay",
"pleroma_emoji_reactions"
"pleroma_emoji_reactions",
"pleroma_chat_messages"
],
"federation":{
"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.
## 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

@ -77,7 +77,8 @@ def start(_type, _args) do
] ++
elasticsearch_children() ++
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
# for other strategies and supported options
@ -92,16 +93,11 @@ def start(_type, _args) do
end
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
set_postgres_server_version()
{:ok, data}
else
e ->
Logger.error("Failed to start!")
Logger.error("#{inspect(e)}")
e
end
set_postgres_server_version()
result
end
defp set_postgres_server_version do
@ -156,6 +152,10 @@ defp cachex_children do
build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10),
build_cachex("failed_proxy_url", limit: 2500),
build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000),
build_cachex("chat_message_id_idempotency_key",
expiration: chat_message_id_idempotency_key_expiration(),
limit: 500_000
),
build_cachex("translations", default_ttl: :timer.hours(24 * 30), 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)
@ -168,6 +168,9 @@ defp emoji_packs_expiration,
defp idempotency_expiration,
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,
do: :timer.seconds(Config.get!([Pleroma.Captcha, :seconds_valid]))
@ -179,6 +182,8 @@ def build_cachex(type, opts),
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(_) do
@ -198,6 +203,15 @@ defp background_migrators do
]
end
defp shout_child(true) do
[
Pleroma.Web.ShoutChannel.ShoutChannelState,
{Phoenix.PubSub, [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2]}
]
end
defp shout_child(_), 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

@ -178,6 +178,7 @@ def warn do
check_activity_expiration_config(),
check_remote_ip_plug_name(),
check_uploders_s3_public_endpoint(),
check_old_chat_shoutbox(),
check_quarantined_instances_tuples(),
check_transparency_exclusions_tuples(),
check_simple_policy_tuples()
@ -309,4 +310,27 @@ def check_uploders_s3_public_endpoint do
:ok
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

View file

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

View file

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

View file

@ -237,6 +237,17 @@ def insert_log(%{actor: %User{}, action: action, target: target} = attrs)
insert_log_entry_with_message(%ModerationLog{data: data})
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) :: {:ok, ModerationLog} | {:error, any}
defp insert_log_entry_with_message(entry) do
entry.data["message"]
@ -543,6 +554,16 @@ def get_log_entry_message(%ModerationLog{
"@#{actor_nickname} updated users: #{users_to_nicknames_string(subjects)}"
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{
data: %{
"actor" => %{"nickname" => actor_nickname},

View file

@ -68,6 +68,7 @@ def unread_notifications_count(%User{id: user_id}) do
follow_request
mention
move
pleroma:chat_mention
pleroma:emoji_reaction
pleroma:report
reblog
@ -463,7 +464,16 @@ defp type_from_activity(%{data: %{"type" => type}} = activity) do
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.
def create_notification(%Activity{} = activity, %User{} = user, opts \\ []) do

View file

@ -145,6 +145,7 @@ defmodule Pleroma.User do
field(:also_known_as, {:array, ObjectValidators.ObjectID}, default: [])
field(:inbox, :string)
field(:shared_inbox, :string)
field(:accepts_chat_messages, :boolean, default: nil)
field(:last_active_at, :naive_datetime)
field(:disclose_client, :boolean, default: true)
field(:pinned_objects, :map, default: %{})
@ -195,6 +196,17 @@ defmodule Pleroma.User do
has_many(incoming_relation_source, through: [incoming_relation, :source])
end
# `:blocks` is deprecated (replaced with `blocked_users` relation)
field(:blocks, {:array, :string}, default: [])
# `:mutes` is deprecated (replaced with `muted_users` relation)
field(:mutes, {:array, :string}, default: [])
# `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
field(:muted_reblogs, {:array, :string}, default: [])
# `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
field(:muted_notifications, {:array, :string}, default: [])
# `:subscribers` is deprecated (replaced with `subscriber_users` relation)
field(:subscribers, {:array, :string}, default: [])
embeds_one(
:multi_factor_authentication_settings,
MFA.Settings,
@ -456,6 +468,7 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
:invisible,
:actor_type,
:also_known_as,
:accepts_chat_messages,
:pinned_objects
]
)
@ -516,6 +529,7 @@ def update_changeset(struct, params \\ %{}) do
:pleroma_settings_store,
:is_discoverable,
:actor_type,
:accepts_chat_messages,
:disclose_client
]
)
@ -692,6 +706,7 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
bio_limit = Config.get([:instance, :user_bio_length], 5000)
name_limit = Config.get([:instance, :user_name_length], 100)
reason_limit = Config.get([:instance, :registration_reason_length], 500)
params = Map.put_new(params, :accepts_chat_messages, true)
confirmed? =
if is_nil(opts[:confirmed]) do
@ -719,6 +734,7 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do
:password,
:password_confirmation,
:emoji,
:accepts_chat_messages,
:registration_reason,
:language
])
@ -827,7 +843,8 @@ def post_register_action(%User{is_approved: true, is_confirmed: true} = user) do
{:ok, user} <- set_cache(user),
{:ok, _} <- maybe_send_registration_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}
end
end
@ -861,6 +878,15 @@ defp maybe_send_welcome_message(user) do
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
if User.WelcomeEmail.enabled?() do
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 @@ defp increase_replies_count_if_reply(%{
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 ChatMessage Question Answer Audio Video Event Article Note Page]
@impl true
def persist(%{"type" => type} = object, meta) when type in @object_types do
with {:ok, object} <- Object.create(object) do
@ -1247,6 +1247,30 @@ defp exclude_poll_votes(query, _) do
end
end
defp exclude_chat_messages(query, %{include_chat_messages: true}), do: query
defp exclude_chat_messages(query, _) do
if has_named_binding?(query, :object) do
from([activity, object: o] in query,
where: fragment("not(?->>'type' = ?)", o.data, "ChatMessage")
)
else
query
end
end
defp exclude_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, %{type: "Flag"}), do: query
defp exclude_invisible_actors(query, %{invisible_actors: true}), do: query
@ -1386,7 +1410,9 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_announce_object_actor(opts)
|> restrict_filtered(opts)
|> maybe_restrict_deactivated_users(opts)
|> exclude_chat_messages(opts)
|> exclude_poll_votes(opts)
|> exclude_chat_messages(opts)
|> exclude_invisible_actors(opts)
|> exclude_visibility(opts)
@ -1508,6 +1534,8 @@ defp object_to_user_data(data, additional) do
end)
is_locked = data["manuallyApprovesFollowers"] || false
capabilities = data["capabilities"] || %{}
accepts_chat_messages = capabilities["acceptsChatMessages"]
data = Transmogrifier.maybe_fix_user_object(data)
is_discoverable = data["discoverable"] || false
invisible = data["invisible"] || false
@ -1563,6 +1591,7 @@ defp object_to_user_data(data, additional) do
public_key: public_key,
inbox: data["inbox"],
shared_inbox: shared_inbox,
accepts_chat_messages: accepts_chat_messages,
pinned_objects: pinned_objects,
nickname: nickname
}

View file

@ -231,6 +231,30 @@ defp add_in_reply_to(object, in_reply_to) do
end
end
def chat_message(actor, recipient, content, opts \\ []) do
basic = %{
"id" => Utils.generate_object_id(),
"actor" => actor.ap_id,
"type" => "ChatMessage",
"to" => [recipient],
"content" => content,
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"emoji" => Emoji.Formatter.get_emoji_map(content)
}
case opts[:attachment] do
%Object{data: attachment_data} ->
{
:ok,
Map.put(basic, "attachment", attachment_data),
[]
}
_ ->
{:ok, basic, []}
end
end
defp add_quote(object, nil), do: object
defp add_quote(object, quote) do

View file

@ -23,6 +23,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator
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.DeleteValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
@ -81,6 +83,21 @@ def validate(%{"type" => "Delete"} = object, meta) do
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(
%{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity,
meta
@ -160,7 +177,7 @@ def validate(
def validate(%{"type" => type} = object, meta)
when type in ~w[Accept Reject Follow Update Like EmojiReact Announce
Answer] do
ChatMessage Answer] do
validator =
case type do
"Accept" -> AcceptRejectValidator
@ -170,6 +187,7 @@ def validate(%{"type" => type} = object, meta)
"Like" -> LikeValidator
"EmojiReact" -> EmojiReactValidator
"Announce" -> AnnounceValidator
"ChatMessage" -> ChatMessageValidator
"Answer" -> AnswerValidator
end
@ -194,6 +212,10 @@ def validate(%{"type" => type} = object, meta) when type in ~w(Add Remove) do
def validate(o, m), do: {:error, {:validator_not_set, {o, m}}}
def cast_and_apply(%{"type" => "ChatMessage"} = object) do
ChatMessageValidator.cast_and_apply(object)
end
def cast_and_apply_and_stringify_with_history(object) do
do_separate_with_history(object, fn object ->
with {:ok, object_data} <- cast_and_apply(object),

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.TagValidator
# Activities and Objects
# Activities and Objects, except (Create)ChatMessage
defmacro message_fields do
quote bind_quoted: binding() do
field(:type, :string)
@ -39,7 +39,7 @@ defmacro object_fields do
end
end
# Basically objects that aren't Answer
# Basically objects that aren't ChatMessage and Answer
defmacro status_object_fields do
quote bind_quoted: binding() do
# 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 @@ def add_deleted_activity_id(cng) do
Answer
Article
Audio
ChatMessage
Event
Note
Page

View file

@ -10,6 +10,8 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
collection, and so on.
"""
alias Pleroma.Activity
alias Pleroma.Chat
alias Pleroma.Chat.MessageReference
alias Pleroma.FollowingRelationship
alias Pleroma.Notification
alias Pleroma.Object
@ -26,6 +28,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
require Pleroma.Constants
require Logger
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@logger Pleroma.Config.get([:side_effects, :logger], Logger)
@behaviour Pleroma.Web.ActivityPub.SideEffects.Handling
@ -314,6 +317,8 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object,
Object.decrease_replies_count(in_reply_to)
end
MessageReference.delete_for_object(deleted_object)
ap_streamer().stream_out(object)
ap_streamer().stream_out_participations(deleted_object, user)
:ok
@ -404,6 +409,41 @@ def handle(object, meta) do
{:ok, object, meta}
end
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
defp handle_update_user(
%{data: %{"type" => "Update", "object" => updated_object}} = object,
meta
@ -575,6 +615,13 @@ defp send_streamables(meta) do
meta
end
defp add_streamables(meta, streamables) do
existing = Keyword.get(meta, :streamables, [])
meta
|> Keyword.put(:streamables, streamables ++ existing)
end
defp add_notifications(meta, notifications) do
existing = Keyword.get(meta, :notifications, [])

View file

@ -442,7 +442,7 @@ def handle_incoming(
%{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
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)
object =
@ -922,6 +922,9 @@ def add_attributed_to(object) do
Map.put(object, "attributedTo", attributed_to)
end
# TODO: Revisit this
def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
def prepare_attachments(object) do
attachments =
object

View file

@ -81,7 +81,14 @@ def render("user.json", %{user: user}) do
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,

View file

@ -52,6 +52,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
when action in [:list_user_statuses]
)
plug(
OAuthScopesPlug,
%{scopes: ["admin:read:chats"]}
when action in [:list_user_chats]
)
plug(
OAuthScopesPlug,
%{scopes: ["admin:read"]}
@ -100,6 +106,20 @@ def list_user_statuses(%{assigns: %{user: admin}} = conn, %{"nickname" => nickna
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
with {:ok, _} <- User.tag(nicknames, tags) do
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 @@ def spec(opts \\ []) do
%{
"name" => "Administration",
"tags" => [
"Chat administration",
"Emoji pack administration",
"Frontend managment",
"Instance configuration",
@ -113,6 +114,7 @@ def spec(opts \\ []) do
]
},
%{"name" => "Instance", "tags" => ["Custom emojis"]},
%{"name" => "Messaging", "tags" => ["Chats", "Conversations"]},
%{
"name" => "Statuses",
"tags" => [

View file

@ -611,6 +611,11 @@ defp update_credentials_request do
nullable: true,
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{
nullable: true,
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,383 @@
# 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 index_operation do
%Operation{
tags: ["Chats"],
summary: "Retrieve list of chats (unpaginated)",
deprecated: true,
description:
"Deprecated due to no support for pagination. Using [/api/v2/pleroma/chats](#operation/ChatController.index2) instead is recommended.",
operationId: "ChatController.index",
parameters: [
Operation.parameter(:with_muted, :query, BooleanLike, "Include chats from muted users")
],
responses: %{
200 => Operation.response("The chats of the user", "application/json", chats_response())
},
security: [
%{
"oAuth" => ["read: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

@ -108,6 +108,24 @@ def dismiss_operation do
}
end
def dismiss_via_body_operation do
%Operation{
tags: ["Notifications"],
summary: "Dismiss a single notification",
deprecated: true,
description: "Clear a single notification from the server.",
operationId: "NotificationController.dismiss_via_body",
requestBody:
request_body(
"Parameters",
%Schema{type: :object, properties: %{id: %Schema{type: :string}}},
required: true
),
security: [%{"oAuth" => ["write:notifications"]}],
responses: %{200 => empty_object_response()}
}
end
def destroy_multiple_operation do
%Operation{
tags: ["Notifications"],
@ -174,6 +192,7 @@ defp notification_type do
"reblog",
"mention",
"pleroma:emoji_reaction",
"pleroma:chat_mention",
"pleroma:report",
"move",
"follow_request",
@ -189,6 +208,7 @@ defp notification_type do
- `poll` - A poll you have voted in or created has ended
- `move` - Someone moved their account
- `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
"""
}

View file

@ -59,6 +59,53 @@ def account_search_operation do
}
end
def search_operation do
%Operation{
tags: ["Search"],
summary: "Search results",
security: [%{"oAuth" => ["read:search"]}],
operationId: "SearchController.search",
deprecated: true,
parameters: [
Operation.parameter(
:account_id,
:query,
FlakeID,
"If provided, statuses returned will be authored only by this account"
),
Operation.parameter(
:type,
:query,
%Schema{type: :string, enum: ["accounts", "hashtags", "statuses"]},
"Search type"
),
Operation.parameter(:q, :query, %Schema{type: :string}, "The search query", required: true),
Operation.parameter(
:resolve,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Attempt WebFinger lookup"
),
Operation.parameter(
:following,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Only include accounts that the user is following"
),
Operation.parameter(
:offset,
:query,
%Schema{type: :integer},
"Offset"
),
with_relationships_param() | pagination_params()
],
responses: %{
200 => Operation.response("Results", "application/json", results())
}
}
end
def search2_operation do
%Operation{
tags: ["Search"],
@ -129,4 +176,33 @@ defp results2 do
}
}
end
defp results do
%Schema{
title: "SearchResults",
type: :object,
properties: %{
accounts: %Schema{
type: :array,
items: Account,
description: "Accounts which match the given query"
},
statuses: %Schema{
type: :array,
items: Status,
description: "Statuses which match the given query"
},
hashtags: %Schema{
type: :array,
items: %Schema{type: :string},
description: "Hashtags which match the given query"
}
},
example: %{
"accounts" => [Account.schema().example],
"statuses" => [Status.schema().example],
"hashtags" => ["cofe"]
}
}
end
end

View file

@ -327,6 +327,34 @@ def unmute_conversation_operation do
}
end
def card_operation do
%Operation{
tags: ["Retrieve status information"],
deprecated: true,
summary: "Preview card",
description: "Deprecated in favor of card property inlined on Status entity",
operationId: "StatusController.card",
parameters: [id_param()],
security: [%{"oAuth" => ["read:statuses"]}],
responses: %{
200 =>
Operation.response("Card", "application/json", %Schema{
type: :object,
nullable: true,
properties: %{
type: %Schema{type: :string, enum: ["link", "photo", "video", "rich"]},
provider_name: %Schema{type: :string, nullable: true},
provider_url: %Schema{type: :string, format: :uri},
url: %Schema{type: :string, format: :uri},
image: %Schema{type: :string, nullable: true, format: :uri},
title: %Schema{type: :string},
description: %Schema{type: :string}
}
})
}
}
end
def favourited_by_operation do
%Operation{
tags: ["Retrieve status information"],

View file

@ -142,6 +142,11 @@ defp create_request do
nullable: true,
description: "Receive poll notifications?"
},
"pleroma:chat_mention": %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Receive chat notifications?"
},
"pleroma:emoji_reaction": %Schema{
allOf: [BooleanLike],
nullable: true,
@ -211,6 +216,11 @@ defp update_request do
nullable: true,
description: "Receive poll notifications?"
},
"pleroma:chat_mention": %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Receive chat notifications?"
},
"pleroma:emoji_reaction": %Schema{
allOf: [BooleanLike],
nullable: true,

View file

@ -43,7 +43,7 @@ def direct_operation do
tags: ["Timelines"],
summary: "Direct timeline",
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()],
security: [%{"oAuth" => ["read:statuses"]}],
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"
},
background_image: %Schema{type: :string, nullable: true, format: :uri},
chat_token: %Schema{type: :string},
is_confirmed: %Schema{
type: :boolean,
description:
@ -101,6 +102,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
description:
"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{
type: :string,
format: :uri,
@ -173,6 +175,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
"is_admin" => false,
"is_moderator" => false,
"skip_thread_containment" => false,
"accepts_chat_messages" => true,
"chat_token" =>
"SFMyNTY.g3QAAAACZAAEZGF0YW0AAAASOXRLaTNlc2JHN09RZ1oyOTIwZAAGc2lnbmVkbgYARNplS3EB.Mb_Iaqew2bN1I1o79B_iP7encmVCpTKC4OtHZRxdjKc",
"unread_conversation_count" => 0,
"tags" => [],
"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

@ -30,7 +30,7 @@ def handle_error(plug, error),
def auth_template do
# Note: `config :pleroma, :auth_template, "..."` support is deprecated
implementation().auth_template() ||
Pleroma.Config.get(:auth_template) ||
Pleroma.Config.get([:auth, :auth_template], Pleroma.Config.get(:auth_template)) ||
"show.html"
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
alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.ThreadMute
alias Pleroma.User
@ -29,6 +30,57 @@ def block(blocker, blocked) do
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
with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
{:ok, unblock_data, _} <- Builder.undo(blocker, block),

View file

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

View file

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

View file

@ -99,6 +99,11 @@ def dismiss(%{assigns: %{user: user}} = conn, %{id: id} = _params) do
end
end
# POST /api/v1/notifications/dismiss (deprecated)
def dismiss_via_body(%{body_params: params} = conn, _) do
dismiss(conn, params)
end
# DELETE /api/v1/notifications/destroy_multiple
def destroy_multiple(%{assigns: %{user: user}} = conn, %{ids: ids} = _params) do
Notification.destroy_multiple(user, ids)

View file

@ -25,7 +25,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
# Note: on private instances auth is required (EnsurePublicOrAuthenticatedPlug is not skipped)
plug(RateLimiter, [name: :search] when action in [:search2, :account_search])
plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search])
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.SearchOperation
@ -42,6 +42,7 @@ def account_search(%{assigns: %{user: user}} = conn, %{q: query} = params) do
end
def search2(conn, params), do: do_search(:v2, conn, params)
def search(conn, params), do: do_search(:v1, conn, params)
defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do
query = String.trim(query)
@ -117,6 +118,10 @@ defp resource_search(:v2, "hashtags", query, options) do
end)
end
defp resource_search(:v1, "hashtags", query, options) do
prepare_tags(query, options)
end
defp prepare_tags(query, options) do
tags =
query

View file

@ -39,6 +39,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
when action in [
:index,
:show,
:card,
:context,
:translate,
:show_history,
@ -373,6 +374,18 @@ def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
end
end
@doc "GET /api/v1/statuses/:id/card"
@deprecated "https://github.com/tootsuite/mastodon/pull/11213"
def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
with %Activity{} = activity <- Activity.get_by_id(status_id),
true <- Visibility.visible_for_user?(activity, user) do
data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
render(conn, "card.json", data)
else
_ -> render_error(conn, :not_found, "Record not found")
end
end
@doc "GET /api/v1/statuses/:id/favourited_by"
def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
with true <- Pleroma.Config.get([:instance, :show_reactions]),

View file

@ -306,6 +306,7 @@ defp do_render("show.json", %{user: user} = opts) do
relationship: relationship,
skip_thread_containment: user.skip_thread_containment,
background_image: image_url(user.background) |> MediaProxy.url(),
accepts_chat_messages: user.accepts_chat_messages,
favicon: favicon
}
}
@ -313,6 +314,7 @@ defp do_render("show.json", %{user: user} = opts) do
|> maybe_put_settings(user, opts[:for], opts)
|> maybe_put_notification_settings(user, opts[:for])
|> 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_follow_requests_count(user, opts[:for])
|> maybe_put_allow_following_move(user, opts[:for])
@ -365,6 +367,15 @@ defp maybe_put_settings_store(data, %User{} = user, %User{}, %{
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
data
|> Kernel.put_in([:pleroma, :is_admin], user.is_admin)

View file

@ -37,6 +37,7 @@ def render("show.json", _) do
background_upload_limit: Keyword.get(instance, :background_upload_limit),
banner_upload_limit: Keyword.get(instance, :banner_upload_limit),
background_image: Pleroma.Web.Endpoint.url() <> Keyword.get(instance, :background_image),
shout_limit: Config.get([:shout, :limit]),
description_limit: Keyword.get(instance, :description_limit),
pleroma: %{
metadata: %{
@ -56,7 +57,6 @@ def render("show.json", _) do
def features do
[
"pleroma_api",
"akkoma_api",
"mastodon_api",
"mastodon_api_streaming",
"polls",
@ -69,6 +69,13 @@ def features do
if Config.get([:media_proxy, :enabled]) do
"media_proxy"
end,
# backwards compat
if Config.get([:shout, :enabled]) do
"chat"
end,
if Config.get([:shout, :enabled]) do
"shout"
end,
if Config.get([:instance, :allow_relay]) do
"relay"
end,
@ -76,6 +83,7 @@ def features do
"safe_dm_mentions"
end,
"pleroma_emoji_reactions",
"pleroma_chat_messages",
if Config.get([:instance, :show_reactions]) do
"exposable_reactions"
end,

View file

@ -6,7 +6,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
use Pleroma.Web, :view
alias Pleroma.Activity
alias Pleroma.Chat.MessageReference
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.AdminAPI.Report
@ -16,6 +18,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.NotificationView
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
@ -125,6 +128,9 @@ def render(
|> put_status(parent_activity_fn.(), reading_user, status_render_opts)
|> put_emoji(activity)
"pleroma:chat_mention" ->
put_chat_message(response, activity, reading_user, status_render_opts)
"pleroma:report" ->
put_report(response, activity)
@ -145,6 +151,17 @@ defp put_emoji(response, activity) do
|> Map.put(:emoji_url, MediaProxy.url(Pleroma.Emoji.emoji_url(activity.data)))
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
status_render_opts = Map.merge(opts, %{activity: activity, for: reading_user})
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 @@ def build_content(notification, actor, object, mastodon_type) do
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(
%{activity: %{data: %{"type" => "Create"}}},
actor,
@ -190,6 +197,7 @@ def format_title(%{type: type}, mastodon_type) do
"reblog" -> "New Repeat"
"favourite" -> "New Favorite"
"update" -> "New Update"
"pleroma:chat_mention" -> "New Chat Message"
"pleroma:emoji_reaction" -> "New Reaction"
type -> "New #{String.capitalize(type || "event")}"
end

View file

@ -26,7 +26,7 @@ defmodule Pleroma.Web.Push.Subscription do
end
# 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
alerts = Map.take(alerts, @supported_alert_types)

View file

@ -0,0 +1,10 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parsers.OGP do
@deprecated "OGP parser is deprecated. Use TwitterCard instead."
def parse(_html, _data) do
%{}
end
end

View file

@ -253,8 +253,12 @@ defmodule Pleroma.Web.Router do
patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
get("/users/:nickname/chats", AdminAPIController, :list_user_chats)
get("/statuses", StatusController, :index)
get("/chats/:id", ChatController, :show)
get("/chats/:id/messages", ChatController, :messages)
end
# AdminAPI: admins and mods (staff) can perform these actions
@ -294,6 +298,8 @@ defmodule Pleroma.Web.Router do
post("/reload_emoji", AdminAPIController, :reload_emoji)
get("/stats", AdminAPIController, :stats)
delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
end
scope "/api/v1/pleroma/emoji", Pleroma.Web.PleromaAPI do
@ -422,6 +428,15 @@ defmodule Pleroma.Web.Router do
scope [] do
pipe_through(:authenticated_api)
post("/chats/by-account-id/:id", ChatController, :create)
get("/chats", ChatController, :index)
get("/chats/:id", ChatController, :show)
get("/chats/:id/messages", ChatController, :messages)
post("/chats/:id/messages", ChatController, :post_chat_message)
delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
post("/chats/:id/read", ChatController, :mark_as_read)
post("/chats/:id/messages/:message_id/read", ChatController, :mark_message_as_read)
get("/conversations/:id/statuses", ConversationController, :statuses)
get("/conversations/:id", ConversationController, :show)
post("/conversations/read", ConversationController, :mark_as_read)
@ -458,6 +473,13 @@ defmodule Pleroma.Web.Router do
get("/federation_status", InstancesController, :show)
end
scope "/api/v2/pleroma", Pleroma.Web.PleromaAPI do
scope [] do
pipe_through(:authenticated_api)
get("/chats", ChatController, :index2)
end
end
scope "/api/v1", Pleroma.Web.PleromaAPI do
pipe_through(:authenticated_api)
put("/statuses/:id/emoji_reactions/:emoji", EmojiReactionController, :create)
@ -553,6 +575,8 @@ defmodule Pleroma.Web.Router do
post("/notifications/:id/dismiss", NotificationController, :dismiss)
post("/notifications/clear", NotificationController, :clear)
delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
# Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead
post("/notifications/dismiss", NotificationController, :dismiss_via_body)
post("/polls/:id/votes", PollController, :vote)
@ -618,6 +642,8 @@ defmodule Pleroma.Web.Router do
pipe_through(:api)
get("/accounts/search", SearchController, :account_search)
get("/search", SearchController, :search)
get("/accounts/lookup", AccountController, :lookup)
get("/accounts/:id/statuses", AccountController, :statuses)
@ -633,6 +659,7 @@ defmodule Pleroma.Web.Router do
get("/statuses", StatusController, :index)
get("/statuses/:id", StatusController, :show)
get("/statuses/:id/context", StatusController, :context)
get("/statuses/:id/card", StatusController, :card)
get("/statuses/:id/favourited_by", StatusController, :favourited_by)
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
get("/statuses/:id/history", StatusController, :show_history)

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
alias Pleroma.Activity
alias Pleroma.Chat.MessageReference
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Notification
@ -24,7 +25,7 @@ defmodule Pleroma.Web.Streamer do
def registry, do: @registry
@public_streams ["public", "public:local", "public:media", "public:local:media"]
@user_streams ["user", "user:notification", "direct"]
@user_streams ["user", "user:notification", "direct", "user:pleroma_chat"]
@doc "Expands and authorizes a stream, and registers the process for streaming."
@spec get_topic_and_add_socket(
@ -245,6 +246,19 @@ defp do_stream(topic, %Notification{} = item)
end)
end
defp do_stream(topic, {user, %MessageReference{} = cm_ref})
when topic in ["user", "user:pleroma_chat"] do
topic = "#{topic}:#{user.id}"
text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref})
Registry.dispatch(@registry, topic, fn list ->
Enum.each(list, fn {pid, _auth} ->
send(pid, {:text, text})
end)
end)
end
defp do_stream("user", item) do
Logger.debug("Trying to push to users")

View file

@ -71,6 +71,29 @@ def render("update.json", %Activity{} = activity, topic) do
|> Jason.encode!()
end
def render("chat_update.json", %{chat_message_reference: cm_ref}) do
# Explicitly giving the cmr for the object here, so we don't accidentally
# send a later 'last_message' that was inserted between inserting this and
# streaming it out
#
# It also contains the chat with a cache of the correct unread count
Logger.debug("Trying to stream out #{inspect(cm_ref)}")
representation =
Pleroma.Web.PleromaAPI.ChatView.render(
"show.json",
%{last_message: cm_ref, chat: cm_ref.chat}
)
%{
event: "pleroma:chat_update",
payload:
representation
|> Jason.encode!()
}
|> Jason.encode!()
end
def render("status_update.json", %Activity{} = activity, topic) do
activity = Activity.get_create_by_object_ap_id_with_object(activity.object.data["id"])

View file

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

View file

@ -129,6 +129,7 @@ test "load a settings with large values and pass to file", %{temp_file: temp_fil
notify_email: "noreply@example.com",
description: "A Pleroma instance, an alternative fediverse server",
limit: 5_000,
chat_limit: 5_000,
remote_limit: 100_000,
upload_limit: 16_000_000,
avatar_upload_limit: 2_000_000,
@ -188,7 +189,7 @@ test "load a settings with large values and pass to file", %{temp_file: temp_fil
{:ok, file} = File.read(temp_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

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

@ -279,4 +279,14 @@ test "check_uploders_s3_public_endpoint/0" do
end) =~
"Your config is using the old setting for controlling the URL of media uploaded to your S3 bucket."
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

View file

@ -150,20 +150,9 @@ test "don't restart if no reboot time settings were changed" do
end
test "on reboot time key" do
clear_config(:rate_limit)
insert(:config, key: :rate_limit, value: [enabled: false])
# Note that we don't actually restart Pleroma.
# See module Restarter.Pleroma
assert capture_log(fn ->
TransferTask.start_link([])
# TransferTask.start_link/1 is an asynchronous call.
# A GenServer will first finish the previous call before starting a new one.
# Here we do a synchronous call.
# That way we are sure that the previous call has finished before we continue.
Restarter.Pleroma.rebooted?()
end) =~ "pleroma restarted"
clear_config(:shout)
insert(:config, key: :shout, value: [enabled: false])
assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted"
end
test "on reboot time subkey" do
@ -183,11 +172,13 @@ test "on reboot time subkey" do
end) =~ "pleroma restarted"
end
test "don't restart pleroma on reboot time key and subkey if there is false flag" do
clear_config(:rate_limit)
clear_config(:shout)
clear_config(Pleroma.Captcha)
insert(:config, key: :rate_limit, value: [enabled: false])
nsert(:config, key: :shout, value: [enabled: false])
insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60])
refute String.contains?(

View file

@ -19,6 +19,7 @@ test "it fills in missing notification types" do
other_user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "yeah, @#{other_user.nickname}"})
{:ok, chat} = CommonAPI.post_chat_message(user, other_user, "yo")
{:ok, react} = CommonAPI.react_with_emoji(post.id, other_user, "")
{:ok, like} = CommonAPI.favorite(other_user, post.id)
{:ok, react_2} = CommonAPI.react_with_emoji(post.id, other_user, "")
@ -32,7 +33,7 @@ test "it fills in missing notification types" do
|> Activity.change(%{data: data})
|> 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()
@ -47,6 +48,9 @@ test "it fills in missing notification types" do
assert %{type: "pleroma:emoji_reaction"} =
Repo.get_by(Notification, user_id: user.id, activity_id: react_2.id)
assert %{type: "pleroma:chat_mention"} =
Repo.get_by(Notification, user_id: other_user.id, activity_id: chat.id)
end
end
end

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 @@ test "it sends a welcome message if it is set" do
assert activity.actor == welcome_user.ap_id
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:
clear_config(
[:mrf_simple],
@ -467,6 +483,24 @@ test "it sends a welcome message if it is set" 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
welcome_user = insert(:user)
clear_config([:welcome, :email, :enabled], true)
@ -635,6 +669,15 @@ test "it sets the password_hash, ap_id, private key and followers collection add
assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers"
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
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?

View file

@ -186,6 +186,13 @@ test "it returns a user that is invisible" do
assert User.invisible?(user)
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
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
alias Pleroma.Web.CommonAPI
setup do
user = insert(:user)
@ -31,6 +33,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicyTest do
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
test "rejects the message if the recipient count is above reject_threshold", %{
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
alias Pleroma.Activity
alias Pleroma.Chat
alias Pleroma.Chat.MessageReference
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
@ -15,7 +17,6 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.SideEffects
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
import Mock
@ -55,22 +56,15 @@ test "it streams out notifications and streams" do
author = insert(:user, local: true)
recipient = insert(:user, local: true)
{:ok, note_data, _meta} =
Builder.note(%Pleroma.Web.CommonAPI.ActivityDraft{
user: author,
to: [recipient.ap_id],
mentions: [recipient],
content_html: "hey",
extra: %{"id" => Utils.generate_object_id()}
})
{:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
{: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} =
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]
@ -93,6 +87,7 @@ test "it streams out notifications and streams" do
SideEffects.handle_after_transaction(meta)
assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification))
assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_))
assert called(Pleroma.Web.Push.send(notification))
end
end
@ -665,6 +660,147 @@ test "creates a notification", %{like: like, poster: poster} do
end
end
describe "creation of ChatMessages" do
test "notifies the recipient" do
author = insert(:user, local: false)
recipient = insert(:user, local: true)
{:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
{:ok, create_activity_data, _meta} =
Builder.create(author, chat_message_data["id"], [recipient.ap_id])
{:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
{:ok, _create_activity, _meta} =
SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
assert Repo.get_by(Notification, user_id: recipient.id, activity_id: create_activity.id)
end
test "it streams the created ChatMessage" do
author = insert(:user, local: true)
recipient = insert(:user, local: true)
{:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
{:ok, create_activity_data, _meta} =
Builder.create(author, chat_message_data["id"], [recipient.ap_id])
{:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
{:ok, _create_activity, meta} =
SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
assert [_, _] = meta[:streamables]
end
test "it creates a Chat and MessageReferences for the local users and bumps the unread count, except for the author" do
author = insert(:user, local: true)
recipient = insert(:user, local: true)
{:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
{:ok, create_activity_data, _meta} =
Builder.create(author, chat_message_data["id"], [recipient.ap_id])
{:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
with_mocks([
{
Pleroma.Web.Streamer,
[],
[
stream: fn _, _ -> nil end
]
},
{
Pleroma.Web.Push,
[],
[
send: fn _ -> nil end
]
}
]) do
{:ok, _create_activity, meta} =
SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
# The notification gets created
assert [notification] = meta[:notifications]
assert notification.activity_id == create_activity.id
# But it is not sent out
refute called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification))
refute called(Pleroma.Web.Push.send(notification))
# Same for the user chat stream
assert [{topics, _}, _] = meta[:streamables]
assert topics == ["user", "user:pleroma_chat"]
refute called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_))
chat = Chat.get(author.id, recipient.ap_id)
[cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all()
assert cm_ref.object.data["content"] == "hey"
assert cm_ref.unread == false
chat = Chat.get(recipient.id, author.ap_id)
[cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all()
assert cm_ref.object.data["content"] == "hey"
assert cm_ref.unread == true
end
end
test "it creates a Chat for the local users and bumps the unread count" do
author = insert(:user, local: false)
recipient = insert(:user, local: true)
{:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
{:ok, create_activity_data, _meta} =
Builder.create(author, chat_message_data["id"], [recipient.ap_id])
{:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
{:ok, _create_activity, _meta} =
SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
# An object is created
assert Object.get_by_ap_id(chat_message_data["id"])
# The remote user won't get a chat
chat = Chat.get(author.id, recipient.ap_id)
refute chat
# The local user will get a chat
chat = Chat.get(recipient.id, author.ap_id)
assert chat
author = insert(:user, local: true)
recipient = insert(:user, local: true)
{:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
{:ok, create_activity_data, _meta} =
Builder.create(author, chat_message_data["id"], [recipient.ap_id])
{:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
{:ok, _create_activity, _meta} =
SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
# Both users are local and get the chat
chat = Chat.get(author.id, recipient.ap_id)
assert chat
chat = Chat.get(recipient.id, author.ap_id)
assert chat
end
end
describe "announce objects" do
setup do
poster = insert(:user)

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 @@ test "sets correct totalItems when follows are hidden but the follow counter is
assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user})
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

View file

@ -419,6 +419,56 @@ test "excludes reblogs by default", %{conn: conn, user: user} do
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/pleroma/admin/moderation_log" do
setup do
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
alias Pleroma.Activity
alias Pleroma.Chat
alias Pleroma.Conversation.Participation
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.AdminAPI.AccountView
@ -114,6 +116,173 @@ test "it blocks and does not federate if outgoing blocks are disabled", %{
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
test "it works even without an existing block activity" do
blocked = insert(:user)

View file

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

View file

@ -39,6 +39,7 @@ test "get instance information", %{conn: conn} do
"background_upload_limit" => _,
"banner_upload_limit" => _,
"background_image" => from_config_background,
"shout_limit" => _,
"description_limit" => _
} = result

View file

@ -52,6 +52,27 @@ test "list of notifications" do
assert response == expected_response
end
test "by default, does not contain pleroma:chat_mention" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user = insert(:user)
{:ok, _activity} = CommonAPI.post_chat_message(other_user, user, "hey")
result =
conn
|> get("/api/v1/notifications")
|> json_response_and_validate_schema(200)
assert [] == result
result =
conn
|> get("/api/v1/notifications?include_types[]=pleroma:chat_mention")
|> json_response_and_validate_schema(200)
assert [_] = result
end
test "by default, does not contain pleroma:report" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user = insert(:user)
@ -116,6 +137,23 @@ test "getting a single notification" do
assert response == expected_response
end
test "dismissing a single notification (deprecated endpoint)" do
%{user: user, conn: conn} = oauth_access(["write:notifications"])
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/json")
|> post("/api/v1/notifications/dismiss", %{"id" => to_string(notification.id)})
assert %{} = json_response_and_validate_schema(conn, 200)
end
test "dismissing a single notification" do
%{user: user, conn: conn} = oauth_access(["write:notifications"])
other_user = insert(:user)

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Object
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint
import Pleroma.Factory
@ -220,4 +221,189 @@ test "returns account if query contains a space", %{conn: conn} do
assert length(results) == 1
end
end
describe ".search" do
test "it returns empty result if user or status search return undefined error", %{conn: conn} do
with_mocks [
{Pleroma.User, [], [search: fn _q, _o -> raise "Oops" end]},
{Pleroma.Activity, [], [search: fn _u, _q, _o -> raise "Oops" end]}
] do
capture_log(fn ->
results =
conn
|> get("/api/v1/search?q=2hu")
|> json_response_and_validate_schema(200)
assert results["accounts"] == []
assert results["statuses"] == []
end) =~
"[error] Elixir.Pleroma.Web.MastodonAPI.SearchController search error: %RuntimeError{message: \"Oops\"}"
end
end
test "search", %{conn: conn} do
user = insert(:user)
user_two = insert(:user, %{nickname: "shp@shitposter.club"})
user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
{:ok, activity} = CommonAPI.post(user, %{status: "This is about 2hu"})
{:ok, _activity} =
CommonAPI.post(user, %{
status: "This is about 2hu, but private",
visibility: "private"
})
{:ok, _} = CommonAPI.post(user_two, %{status: "This isn't"})
results =
conn
|> get("/api/v1/search?q=2hu")
|> json_response_and_validate_schema(200)
[account | _] = results["accounts"]
assert account["id"] == to_string(user_three.id)
assert results["hashtags"] == ["2hu"]
[status] = results["statuses"]
assert status["id"] == to_string(activity.id)
end
test "search fetches remote statuses and prefers them over other results", %{conn: conn} do
old_version = :persistent_term.get({Pleroma.Repo, :postgres_version})
:persistent_term.put({Pleroma.Repo, :postgres_version}, 10.0)
on_exit(fn -> :persistent_term.put({Pleroma.Repo, :postgres_version}, old_version) end)
capture_log(fn ->
{:ok, %{id: activity_id}} =
CommonAPI.post(insert(:user), %{
status: "check out http://mastodon.example.org/@admin/99541947525187367"
})
results =
conn
|> get("/api/v1/search?q=http://mastodon.example.org/@admin/99541947525187367")
|> json_response_and_validate_schema(200)
assert [
%{"url" => "http://mastodon.example.org/@admin/99541947525187367"},
%{"id" => ^activity_id}
] = results["statuses"]
end)
end
test "search doesn't show statuses that it shouldn't", %{conn: conn} do
{:ok, activity} =
CommonAPI.post(insert(:user), %{
status: "This is about 2hu, but private",
visibility: "private"
})
capture_log(fn ->
q = Object.normalize(activity, fetch: false).data["id"]
results =
conn
|> get("/api/v1/search?q=#{q}")
|> json_response_and_validate_schema(200)
[] = results["statuses"]
end)
end
test "search fetches remote accounts", %{conn: conn} do
user = insert(:user)
query = URI.encode_query(%{q: " mike@osada.macgirvin.com ", resolve: true})
results =
conn
|> assign(:user, user)
|> assign(:token, insert(:oauth_token, user: user, scopes: ["read"]))
|> get("/api/v1/search?#{query}")
|> json_response_and_validate_schema(200)
[account] = results["accounts"]
assert account["acct"] == "mike@osada.macgirvin.com"
end
test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do
results =
conn
|> get("/api/v1/search?q=mike@osada.macgirvin.com&resolve=false")
|> json_response_and_validate_schema(200)
assert [] == results["accounts"]
end
test "search with limit and offset", %{conn: conn} do
user = insert(:user)
_user_two = insert(:user, %{nickname: "shp@shitposter.club"})
_user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
{:ok, _activity1} = CommonAPI.post(user, %{status: "This is about 2hu"})
{:ok, _activity2} = CommonAPI.post(user, %{status: "This is also about 2hu"})
result =
conn
|> get("/api/v1/search?q=2hu&limit=1")
assert results = json_response_and_validate_schema(result, 200)
assert [%{"id" => activity_id1}] = results["statuses"]
assert [_] = results["accounts"]
results =
conn
|> get("/api/v1/search?q=2hu&limit=1&offset=1")
|> json_response_and_validate_schema(200)
assert [%{"id" => activity_id2}] = results["statuses"]
assert [] = results["accounts"]
assert activity_id1 != activity_id2
end
test "search returns results only for the given type", %{conn: conn} do
user = insert(:user)
_user_two = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
{:ok, _activity} = CommonAPI.post(user, %{status: "This is about 2hu"})
assert %{"statuses" => [_activity], "accounts" => [], "hashtags" => []} =
conn
|> get("/api/v1/search?q=2hu&type=statuses")
|> json_response_and_validate_schema(200)
assert %{"statuses" => [], "accounts" => [_user_two], "hashtags" => []} =
conn
|> get("/api/v1/search?q=2hu&type=accounts")
|> json_response_and_validate_schema(200)
end
test "search uses account_id to filter statuses by the author", %{conn: conn} do
user = insert(:user, %{nickname: "shp@shitposter.club"})
user_two = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
{:ok, activity1} = CommonAPI.post(user, %{status: "This is about 2hu"})
{:ok, activity2} = CommonAPI.post(user_two, %{status: "This is also about 2hu"})
results =
conn
|> get("/api/v1/search?q=2hu&account_id=#{user.id}")
|> json_response_and_validate_schema(200)
assert [%{"id" => activity_id1}] = results["statuses"]
assert activity_id1 == activity1.id
assert [_] = results["accounts"]
results =
conn
|> get("/api/v1/search?q=2hu&account_id=#{user_two.id}")
|> json_response_and_validate_schema(200)
assert [%{"id" => activity_id2}] = results["statuses"]
assert activity_id2 == activity2.id
end
end
end

View file

@ -1351,6 +1351,87 @@ test "on pin removes deletion job, on unpin reschedule deletion" do
end
end
describe "cards" do
setup do
clear_config([:rich_media, :enabled], true)
oauth_access(["read:statuses"])
end
test "returns rich-media card", %{conn: conn, user: user} do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
{:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp"})
card_data = %{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"provider_name" => "example.com",
"provider_url" => "https://example.com",
"title" => "The Rock",
"type" => "link",
"url" => "https://example.com/ogp",
"description" =>
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
"pleroma" => %{
"opengraph" => %{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"title" => "The Rock",
"type" => "video.movie",
"url" => "https://example.com/ogp",
"description" =>
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer."
}
}
}
response =
conn
|> get("/api/v1/statuses/#{activity.id}/card")
|> json_response_and_validate_schema(200)
assert response == card_data
# works with private posts
{:ok, activity} =
CommonAPI.post(user, %{status: "https://example.com/ogp", visibility: "direct"})
response_two =
conn
|> get("/api/v1/statuses/#{activity.id}/card")
|> json_response_and_validate_schema(200)
assert response_two == card_data
end
test "replaces missing description with an empty string", %{conn: conn, user: user} do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
{:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp-missing-data"})
response =
conn
|> get("/api/v1/statuses/#{activity.id}/card")
|> json_response_and_validate_schema(:ok)
assert response == %{
"type" => "link",
"title" => "Pleroma",
"description" => "",
"image" => nil,
"provider_name" => "example.com",
"provider_url" => "https://example.com",
"url" => "https://example.com/ogp-missing-data",
"pleroma" => %{
"opengraph" => %{
"title" => "Pleroma",
"type" => "website",
"url" => "https://example.com/ogp-missing-data"
}
}
}
end
end
test "bookmarks" do
bookmarks_uri = "/api/v1/bookmarks"

View file

@ -113,6 +113,7 @@ test "successful creation", %{conn: conn} do
"favourite" => true,
"follow" => true,
"reblog" => true,
"pleroma:chat_mention" => true,
"pleroma:emoji_reaction" => true
}
},
@ -128,6 +129,7 @@ test "successful creation", %{conn: conn} do
"favourite" => true,
"follow" => true,
"reblog" => true,
"pleroma:chat_mention" => true,
"pleroma:emoji_reaction" => true
},
"endpoint" => subscription.endpoint,
@ -183,6 +185,7 @@ test "returns a user subsciption", %{conn: conn, user: user, token: token} do
"favourite" => true,
"follow" => true,
"reblog" => true,
"pleroma:chat_mention" => true,
"pleroma:emoji_reaction" => true
}
}
@ -201,6 +204,7 @@ test "returns updated subsciption", %{conn: conn, subscription: subscription} do
"favourite" => false,
"follow" => false,
"reblog" => false,
"pleroma:chat_mention" => false,
"pleroma:emoji_reaction" => false
}
}
@ -213,6 +217,7 @@ test "returns updated subsciption", %{conn: conn, subscription: subscription} do
"favourite" => false,
"follow" => false,
"reblog" => false,
"pleroma:chat_mention" => false,
"pleroma:emoji_reaction" => false
},
"endpoint" => "https://example.com/example/1234",

View file

@ -104,6 +104,13 @@ test "updates the user's locking status", %{conn: conn} do
assert user_data["locked"] == true
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
assert user.allow_following_move == true

View file

@ -104,7 +104,8 @@ test "Represent a user account" do
hide_followers_count: false,
hide_follows_count: false,
relationship: %{},
skip_thread_containment: false
skip_thread_containment: false,
accepts_chat_messages: nil
}
}
@ -261,7 +262,8 @@ test "Represent a Service(bot) account" do
hide_followers_count: false,
hide_follows_count: false,
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
alias Pleroma.Activity
alias Pleroma.Chat
alias Pleroma.Chat.MessageReference
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
@ -20,6 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
import Pleroma.Factory
defp test_notifications_rendering(notifications, user, expected_result) do
@ -37,6 +40,30 @@ defp test_notifications_rendering(notifications, user, expected_result) do
assert expected_result == result
end
test "ChatMessage notification" do
user = insert(:user)
recipient = insert(:user)
{:ok, activity} = CommonAPI.post_chat_message(user, recipient, "what's up my dude")
{:ok, [notification]} = Notification.create_notifications(activity)
object = Object.normalize(activity, 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
user = insert(:user)
mentioned_user = insert(:user)

View file

@ -133,7 +133,6 @@ test "it shows default features flags", %{conn: conn} do
default_features = [
"pleroma_api",
"akkoma_api",
"mastodon_api",
"mastodon_api_streaming",
"polls",
@ -141,7 +140,8 @@ test "it shows default features flags", %{conn: conn} do
"shareable_emoji_packs",
"multifetch",
"pleroma_emoji_reactions",
"pleroma:api/v1/notifications:include_types_filter"
"pleroma:api/v1/notifications:include_types_filter",
"pleroma_chat_messages"
]
assert MapSet.subset?(

View file

@ -0,0 +1,471 @@
# 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
for tested_endpoint <- ["/api/v1/pleroma/chats", "/api/v2/pleroma/chats"] do
describe "GET #{tested_endpoint}" 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(unquote(tested_endpoint))
|> 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(unquote(tested_endpoint))
|> json_response_and_validate_schema(200)
assert length(result) == 1
User.block(user, recipient)
result =
conn
|> get(unquote(tested_endpoint))
|> 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(unquote(tested_endpoint))
|> json_response_and_validate_schema(200)
assert length(result) == 1
User.mute(user, recipient)
result =
conn
|> get(unquote(tested_endpoint))
|> json_response_and_validate_schema(200)
assert length(result) == 0
result =
conn
|> get("#{unquote(tested_endpoint)}?with_muted=true")
|> json_response_and_validate_schema(200)
assert length(result) == 1
end
if tested_endpoint == "/api/v1/pleroma/chats" do
test "it returns all chats", %{conn: conn, user: user} do
Enum.each(1..30, fn _ ->
recipient = insert(:user)
{:ok, _} = Chat.get_or_create(user.id, recipient.ap_id)
end)
result =
conn
|> get(unquote(tested_endpoint))
|> json_response_and_validate_schema(200)
assert length(result) == 30
end
else
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(unquote(tested_endpoint))
|> json_response_and_validate_schema(200)
assert length(result) == 20
last_id = List.last(result)["id"]
result =
conn
|> get(unquote(tested_endpoint) <> "?max_id=#{last_id}")
|> json_response_and_validate_schema(200)
assert length(result) == 10
end
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(unquote(tested_endpoint))
|> 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(unquote(tested_endpoint))
|> 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
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
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Push.Impl
alias Pleroma.Web.Push.Subscription
@ -229,6 +231,46 @@ test "renders title for create activity with direct visibility" do
end
describe "build_content/3" do
test "builds content for chat messages" do
user = insert(:user)
recipient = insert(:user)
{:ok, chat} = CommonAPI.post_chat_message(user, recipient, "hey")
object = Object.normalize(chat, 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
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

Some files were not shown because too many files have changed in this diff Show more