forked from YokaiRick/akkoma
Merge branch 'develop' into fix/csp-mediaproxy-base-url
This commit is contained in:
commit
8a59fde0e5
81 changed files with 3930 additions and 148 deletions
|
@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- **Breaking:** removed `with_move` parameter from notifications timeline.
|
||||
|
||||
### Added
|
||||
- Chats: Added support for federated chats. For details, see the docs.
|
||||
- ActivityPub: Added support for existing AP ids for instances migrated from Mastodon.
|
||||
- Instance: Add `background_image` to configuration and `/api/v1/instance`
|
||||
- Instance: Extend `/api/v1/instance` with Pleroma-specific information.
|
||||
|
|
248
docs/API/chats.md
Normal file
248
docs/API/chats.md
Normal file
|
@ -0,0 +1,248 @@
|
|||
# Chats
|
||||
|
||||
Chats are a way to represent an IM-style conversation between two actors. They are not the same as direct messages and they are not `Status`es, even though they have a lot in common.
|
||||
|
||||
## Why Chats?
|
||||
|
||||
There are no 'visibility levels' in ActivityPub, their definition is purely a Mastodon convention. Direct Messaging between users on the fediverse has mostly been modeled by using ActivityPub addressing following Mastodon conventions on normal `Note` objects. In this case, a 'direct message' would be a message that has no followers addressed and also does not address the special public actor, but just the recipients in the `to` field. It would still be a `Note` and is presented with other `Note`s as a `Status` in the API.
|
||||
|
||||
This is an awkward setup for a few reasons:
|
||||
|
||||
- As DMs generally still follow the usual `Status` conventions, it is easy to accidentally pull somebody into a DM thread by mentioning them. (e.g. "I hate @badguy so much")
|
||||
- It is possible to go from a publicly addressed `Status` to a DM reply, back to public, then to a 'followers only' reply, and so on. This can be become very confusing, as it is unclear which user can see which part of the conversation.
|
||||
- The standard `Status` format of implicit addressing also leads to rather ugly results if you try to display the messages as a chat, because all the recipients are always mentioned by name in the message.
|
||||
- As direct messages are posted with the same api call (and usually same frontend component) as public messages, accidentally making a public message private or vice versa can happen easily. Client bugs can also lead to this, accidentally making private messages public.
|
||||
|
||||
As a measure to improve this situation, the `Conversation` concept and related Pleroma extensions were introduced. While it made it possible to work around a few of the issues, many of the problems remained and it didn't see much adoption because it was too complicated to use correctly.
|
||||
|
||||
## Chats explained
|
||||
For this reasons, Chats are a new and different entity, both in the API as well as in ActivityPub. A quick overview:
|
||||
|
||||
- Chats are meant to represent an instant message conversation between two actors. For now these are only 1-on-1 conversations, but the other actor can be a group in the future.
|
||||
- Chat messages have the ActivityPub type `ChatMessage`. They are not `Note`s. Servers that don't understand them will just drop them.
|
||||
- The only addressing allowed in `ChatMessage`s is one single ActivityPub actor in the `to` field.
|
||||
- There's always only one Chat between two actors. If you start chatting with someone and later start a 'new' Chat, the old Chat will be continued.
|
||||
- `ChatMessage`s are posted with a different api, making it very hard to accidentally send a message to the wrong person.
|
||||
- `ChatMessage`s don't show up in the existing timelines.
|
||||
- Chats can never go from private to public. They are always private between the two actors.
|
||||
|
||||
## Caveats
|
||||
|
||||
- Chats are NOT E2E encrypted (yet). Security is still the same as email.
|
||||
|
||||
## API
|
||||
|
||||
In general, the way to send a `ChatMessage` is to first create a `Chat`, then post a message to that `Chat`. `Group`s will later be supported by making them a sub-type of `Account`.
|
||||
|
||||
This is the overview of using the API. The API is also documented via OpenAPI, so you can view it and play with it by pointing SwaggerUI or a similar OpenAPI tool to `https://yourinstance.tld/api/openapi`.
|
||||
|
||||
### Creating or getting a chat.
|
||||
|
||||
To create or get an existing Chat for a certain recipient (identified by Account ID)
|
||||
you can call:
|
||||
|
||||
`POST /api/v1/pleroma/chats/by-account-id/:account_id`
|
||||
|
||||
The account id is the normal FlakeId of the user
|
||||
```
|
||||
POST /api/v1/pleroma/chats/by-account-id/someflakeid
|
||||
```
|
||||
|
||||
If you already have the id of a chat, you can also use
|
||||
|
||||
```
|
||||
GET /api/v1/pleroma/chats/:id
|
||||
```
|
||||
|
||||
There will only ever be ONE Chat for you and a given recipient, so this call
|
||||
will return the same Chat if you already have one with that user.
|
||||
|
||||
Returned data:
|
||||
|
||||
```json
|
||||
{
|
||||
"account": {
|
||||
"id": "someflakeid",
|
||||
"username": "somenick",
|
||||
...
|
||||
},
|
||||
"id" : "1",
|
||||
"unread" : 2,
|
||||
"last_message" : {...}, // The last message in that chat
|
||||
"updated_at": "2020-04-21T15:11:46.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Marking a chat as read
|
||||
|
||||
To mark a number of messages in a chat up to a certain message as read, you can use
|
||||
|
||||
`POST /api/v1/pleroma/chats/:id/read`
|
||||
|
||||
|
||||
Parameters:
|
||||
- last_read_id: Given this id, all chat messages until this one will be marked as read. Required.
|
||||
|
||||
|
||||
Returned data:
|
||||
|
||||
```json
|
||||
{
|
||||
"account": {
|
||||
"id": "someflakeid",
|
||||
"username": "somenick",
|
||||
...
|
||||
},
|
||||
"id" : "1",
|
||||
"unread" : 0,
|
||||
"updated_at": "2020-04-21T15:11:46.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Marking a single chat message as read
|
||||
|
||||
To set the `unread` property of a message to `false`
|
||||
|
||||
`POST /api/v1/pleroma/chats/:id/messages/:message_id/read`
|
||||
|
||||
Returned data:
|
||||
|
||||
The modified chat message
|
||||
|
||||
### Getting a list of Chats
|
||||
|
||||
`GET /api/v1/pleroma/chats`
|
||||
|
||||
This will return a list of chats that you have been involved in, sorted by their
|
||||
last update (so new chats will be at the top).
|
||||
|
||||
Returned data:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"account": {
|
||||
"id": "someflakeid",
|
||||
"username": "somenick",
|
||||
...
|
||||
},
|
||||
"id" : "1",
|
||||
"unread" : 2,
|
||||
"last_message" : {...}, // The last message in that chat
|
||||
"updated_at": "2020-04-21T15:11:46.000Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
The recipient of messages that are sent to this chat is given by their AP ID.
|
||||
No pagination is implemented for now.
|
||||
|
||||
### Getting the messages for a Chat
|
||||
|
||||
For a given Chat id, you can get the associated messages with
|
||||
|
||||
`GET /api/v1/pleroma/chats/:id/messages`
|
||||
|
||||
This will return all messages, sorted by most recent to least recent. The usual
|
||||
pagination options are implemented.
|
||||
|
||||
Returned data:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"account_id": "someflakeid",
|
||||
"chat_id": "1",
|
||||
"content": "Check this out :firefox:",
|
||||
"created_at": "2020-04-21T15:11:46.000Z",
|
||||
"emojis": [
|
||||
{
|
||||
"shortcode": "firefox",
|
||||
"static_url": "https://dontbulling.me/emoji/Firefox.gif",
|
||||
"url": "https://dontbulling.me/emoji/Firefox.gif",
|
||||
"visible_in_picker": false
|
||||
}
|
||||
],
|
||||
"id": "13",
|
||||
"unread": true
|
||||
},
|
||||
{
|
||||
"account_id": "someflakeid",
|
||||
"chat_id": "1",
|
||||
"content": "Whats' up?",
|
||||
"created_at": "2020-04-21T15:06:45.000Z",
|
||||
"emojis": [],
|
||||
"id": "12",
|
||||
"unread": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Posting a chat message
|
||||
|
||||
Posting a chat message for given Chat id works like this:
|
||||
|
||||
`POST /api/v1/pleroma/chats/:id/messages`
|
||||
|
||||
Parameters:
|
||||
- content: The text content of the message. Optional if media is attached.
|
||||
- media_id: The id of an upload that will be attached to the message.
|
||||
|
||||
Currently, no formatting beyond basic escaping and emoji is implemented.
|
||||
|
||||
Returned data:
|
||||
|
||||
```json
|
||||
{
|
||||
"account_id": "someflakeid",
|
||||
"chat_id": "1",
|
||||
"content": "Check this out :firefox:",
|
||||
"created_at": "2020-04-21T15:11:46.000Z",
|
||||
"emojis": [
|
||||
{
|
||||
"shortcode": "firefox",
|
||||
"static_url": "https://dontbulling.me/emoji/Firefox.gif",
|
||||
"url": "https://dontbulling.me/emoji/Firefox.gif",
|
||||
"visible_in_picker": false
|
||||
}
|
||||
],
|
||||
"id": "13",
|
||||
"unread": false
|
||||
}
|
||||
```
|
||||
|
||||
### Deleting a chat message
|
||||
|
||||
Deleting a chat message for given Chat id works like this:
|
||||
|
||||
`DELETE /api/v1/pleroma/chats/:chat_id/messages/:message_id`
|
||||
|
||||
Returned data is the deleted message.
|
||||
|
||||
### Notifications
|
||||
|
||||
There's a new `pleroma:chat_mention` notification, which has this form. It is not given out in the notifications endpoint by default, you need to explicitly request it with `include_types[]=pleroma:chat_mention`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "someid",
|
||||
"type": "pleroma:chat_mention",
|
||||
"account": { ... } // User account of the sender,
|
||||
"chat_message": {
|
||||
"chat_id": "1",
|
||||
"id": "10",
|
||||
"content": "Hello",
|
||||
"account_id": "someflakeid",
|
||||
"unread": false
|
||||
},
|
||||
"created_at": "somedate"
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming
|
||||
|
||||
There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field.
|
||||
|
||||
### Web Push
|
||||
|
||||
If you want to receive push messages for this type, you'll need to add the `pleroma:chat_mention` type to your alerts in the push subscription.
|
|
@ -230,3 +230,7 @@ Has theses additional parameters (which are the same as in Pleroma-API):
|
|||
Has these additional fields under the `pleroma` object:
|
||||
|
||||
- `unread_count`: contains number unread notifications
|
||||
|
||||
## Streaming
|
||||
|
||||
There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field.
|
||||
|
|
35
docs/ap_extensions.md
Normal file
35
docs/ap_extensions.md
Normal file
|
@ -0,0 +1,35 @@
|
|||
# ChatMessages
|
||||
|
||||
ChatMessages are the messages sent in 1-on-1 chats. They are similar to
|
||||
`Note`s, but the addresing is done by having a single AP actor in the `to`
|
||||
field. Addressing multiple actors is not allowed. These messages are always
|
||||
private, there is no public version of them. They are created with a `Create`
|
||||
activity.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"actor": "http://2hu.gensokyo/users/raymoo",
|
||||
"id": "http://2hu.gensokyo/objects/1",
|
||||
"object": {
|
||||
"attributedTo": "http://2hu.gensokyo/users/raymoo",
|
||||
"content": "You expected a cute girl? Too bad.",
|
||||
"id": "http://2hu.gensokyo/objects/2",
|
||||
"published": "2020-02-12T14:08:20Z",
|
||||
"to": [
|
||||
"http://2hu.gensokyo/users/marisa"
|
||||
],
|
||||
"type": "ChatMessage"
|
||||
},
|
||||
"published": "2018-02-12T14:08:20Z",
|
||||
"to": [
|
||||
"http://2hu.gensokyo/users/marisa"
|
||||
],
|
||||
"type": "Create"
|
||||
}
|
||||
```
|
||||
|
||||
This setup does not prevent multi-user chats, but these will have to go through
|
||||
a `Group`, which will be the recipient of the messages and then `Announce` them
|
||||
to the users in the `Group`.
|
|
@ -24,16 +24,6 @@ defmodule Pleroma.Activity do
|
|||
|
||||
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
|
||||
|
||||
# https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
|
||||
@mastodon_notification_types %{
|
||||
"Create" => "mention",
|
||||
"Follow" => ["follow", "follow_request"],
|
||||
"Announce" => "reblog",
|
||||
"Like" => "favourite",
|
||||
"Move" => "move",
|
||||
"EmojiReact" => "pleroma:emoji_reaction"
|
||||
}
|
||||
|
||||
schema "activities" do
|
||||
field(:data, :map)
|
||||
field(:local, :boolean, default: true)
|
||||
|
@ -300,32 +290,6 @@ def follow_accepted?(
|
|||
|
||||
def follow_accepted?(_), do: false
|
||||
|
||||
@spec mastodon_notification_type(Activity.t()) :: String.t() | nil
|
||||
|
||||
for {ap_type, type} <- @mastodon_notification_types, not is_list(type) do
|
||||
def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
|
||||
do: unquote(type)
|
||||
end
|
||||
|
||||
def mastodon_notification_type(%Activity{data: %{"type" => "Follow"}} = activity) do
|
||||
if follow_accepted?(activity) do
|
||||
"follow"
|
||||
else
|
||||
"follow_request"
|
||||
end
|
||||
end
|
||||
|
||||
def mastodon_notification_type(%Activity{}), do: nil
|
||||
|
||||
@spec from_mastodon_notification_type(String.t()) :: String.t() | nil
|
||||
@doc "Converts Mastodon notification type to AR activity type"
|
||||
def from_mastodon_notification_type(type) do
|
||||
with {k, _v} <-
|
||||
Enum.find(@mastodon_notification_types, fn {_k, v} -> type in List.wrap(v) end) do
|
||||
k
|
||||
end
|
||||
end
|
||||
|
||||
def all_by_actor_and_id(actor, status_ids \\ [])
|
||||
def all_by_actor_and_id(_actor, []), do: []
|
||||
|
||||
|
|
72
lib/pleroma/chat.ex
Normal file
72
lib/pleroma/chat.ex
Normal file
|
@ -0,0 +1,72 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Chat do
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
||||
@moduledoc """
|
||||
Chat keeps a reference to ChatMessage conversations between a user and an recipient. The recipient can be a user (for now) or a group (not implemented yet).
|
||||
|
||||
It is a helper only, to make it easy to display a list of chats with other people, ordered by last bump. The actual messages are retrieved by querying the recipients of the ChatMessages.
|
||||
"""
|
||||
|
||||
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
|
||||
|
||||
schema "chats" do
|
||||
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||
field(:recipient, :string)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def changeset(struct, params) do
|
||||
struct
|
||||
|> cast(params, [:user_id, :recipient])
|
||||
|> validate_change(:recipient, fn
|
||||
:recipient, recipient ->
|
||||
case User.get_cached_by_ap_id(recipient) do
|
||||
nil -> [recipient: "must be an existing user"]
|
||||
_ -> []
|
||||
end
|
||||
end)
|
||||
|> validate_required([:user_id, :recipient])
|
||||
|> unique_constraint(:user_id, name: :chats_user_id_recipient_index)
|
||||
end
|
||||
|
||||
def get_by_id(id) do
|
||||
__MODULE__
|
||||
|> Repo.get(id)
|
||||
end
|
||||
|
||||
def get(user_id, recipient) do
|
||||
__MODULE__
|
||||
|> Repo.get_by(user_id: user_id, recipient: recipient)
|
||||
end
|
||||
|
||||
def get_or_create(user_id, recipient) do
|
||||
%__MODULE__{}
|
||||
|> changeset(%{user_id: user_id, recipient: recipient})
|
||||
|> Repo.insert(
|
||||
# Need to set something, otherwise we get nothing back at all
|
||||
on_conflict: [set: [recipient: recipient]],
|
||||
returning: true,
|
||||
conflict_target: [:user_id, :recipient]
|
||||
)
|
||||
end
|
||||
|
||||
def bump_or_create(user_id, recipient) do
|
||||
%__MODULE__{}
|
||||
|> changeset(%{user_id: user_id, recipient: recipient})
|
||||
|> Repo.insert(
|
||||
on_conflict: [set: [updated_at: NaiveDateTime.utc_now()]],
|
||||
returning: true,
|
||||
conflict_target: [:user_id, :recipient]
|
||||
)
|
||||
end
|
||||
end
|
117
lib/pleroma/chat/message_reference.ex
Normal file
117
lib/pleroma/chat/message_reference.ex
Normal file
|
@ -0,0 +1,117 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Chat.MessageReference do
|
||||
@moduledoc """
|
||||
A reference that builds a relation between an AP chat message that a user can see and whether it has been seen
|
||||
by them, or should be displayed to them. Used to build the chat view that is presented to the user.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.Chat
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
|
||||
@primary_key {:id, FlakeId.Ecto.Type, autogenerate: true}
|
||||
|
||||
schema "chat_message_references" do
|
||||
belongs_to(:object, Object)
|
||||
belongs_to(:chat, Chat, type: FlakeId.Ecto.CompatType)
|
||||
|
||||
field(:unread, :boolean, default: true)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def changeset(struct, params) do
|
||||
struct
|
||||
|> cast(params, [:object_id, :chat_id, :unread])
|
||||
|> validate_required([:object_id, :chat_id, :unread])
|
||||
end
|
||||
|
||||
def get_by_id(id) do
|
||||
__MODULE__
|
||||
|> Repo.get(id)
|
||||
|> Repo.preload(:object)
|
||||
end
|
||||
|
||||
def delete(cm_ref) do
|
||||
cm_ref
|
||||
|> Repo.delete()
|
||||
end
|
||||
|
||||
def delete_for_object(%{id: object_id}) do
|
||||
from(cr in __MODULE__,
|
||||
where: cr.object_id == ^object_id
|
||||
)
|
||||
|> Repo.delete_all()
|
||||
end
|
||||
|
||||
def for_chat_and_object(%{id: chat_id}, %{id: object_id}) do
|
||||
__MODULE__
|
||||
|> Repo.get_by(chat_id: chat_id, object_id: object_id)
|
||||
|> Repo.preload(:object)
|
||||
end
|
||||
|
||||
def for_chat_query(chat) do
|
||||
from(cr in __MODULE__,
|
||||
where: cr.chat_id == ^chat.id,
|
||||
order_by: [desc: :id],
|
||||
preload: [:object]
|
||||
)
|
||||
end
|
||||
|
||||
def last_message_for_chat(chat) do
|
||||
chat
|
||||
|> for_chat_query()
|
||||
|> limit(1)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
def create(chat, object, unread) do
|
||||
params = %{
|
||||
chat_id: chat.id,
|
||||
object_id: object.id,
|
||||
unread: unread
|
||||
}
|
||||
|
||||
%__MODULE__{}
|
||||
|> changeset(params)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
def unread_count_for_chat(chat) do
|
||||
chat
|
||||
|> for_chat_query()
|
||||
|> where([cmr], cmr.unread == true)
|
||||
|> Repo.aggregate(:count)
|
||||
end
|
||||
|
||||
def mark_as_read(cm_ref) do
|
||||
cm_ref
|
||||
|> changeset(%{unread: false})
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def set_all_seen_for_chat(chat, last_read_id \\ nil) do
|
||||
query =
|
||||
chat
|
||||
|> for_chat_query()
|
||||
|> exclude(:order_by)
|
||||
|> exclude(:preload)
|
||||
|> where([cmr], cmr.unread == true)
|
||||
|
||||
if last_read_id do
|
||||
query
|
||||
|> where([cmr], cmr.id <= ^last_read_id)
|
||||
else
|
||||
query
|
||||
end
|
||||
|> Repo.update_all(set: [unread: false])
|
||||
end
|
||||
end
|
85
lib/pleroma/migration_helper/notification_backfill.ex
Normal file
85
lib/pleroma/migration_helper/notification_backfill.ex
Normal file
|
@ -0,0 +1,85 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MigrationHelper.NotificationBackfill do
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
def fill_in_notification_types do
|
||||
query =
|
||||
from(n in Pleroma.Notification,
|
||||
where: is_nil(n.type),
|
||||
preload: :activity
|
||||
)
|
||||
|
||||
query
|
||||
|> Repo.all()
|
||||
|> Enum.each(fn notification ->
|
||||
type =
|
||||
notification.activity
|
||||
|> type_from_activity()
|
||||
|
||||
notification
|
||||
|> Notification.changeset(%{type: type})
|
||||
|> Repo.update()
|
||||
end)
|
||||
end
|
||||
|
||||
# This is copied over from Notifications to keep this stable.
|
||||
defp type_from_activity(%{data: %{"type" => type}} = activity) do
|
||||
case type do
|
||||
"Follow" ->
|
||||
accepted_function = fn activity ->
|
||||
with %User{} = follower <- User.get_by_ap_id(activity.data["actor"]),
|
||||
%User{} = followed <- User.get_by_ap_id(activity.data["object"]) do
|
||||
Pleroma.FollowingRelationship.following?(follower, followed)
|
||||
end
|
||||
end
|
||||
|
||||
if accepted_function.(activity) do
|
||||
"follow"
|
||||
else
|
||||
"follow_request"
|
||||
end
|
||||
|
||||
"Announce" ->
|
||||
"reblog"
|
||||
|
||||
"Like" ->
|
||||
"favourite"
|
||||
|
||||
"Move" ->
|
||||
"move"
|
||||
|
||||
"EmojiReact" ->
|
||||
"pleroma:emoji_reaction"
|
||||
|
||||
# Compatibility with old reactions
|
||||
"EmojiReaction" ->
|
||||
"pleroma:emoji_reaction"
|
||||
|
||||
"Create" ->
|
||||
activity
|
||||
|> type_from_activity_object()
|
||||
|
||||
t ->
|
||||
raise "No notification type for activity type #{t}"
|
||||
end
|
||||
end
|
||||
|
||||
defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention"
|
||||
|
||||
defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do
|
||||
object = Object.get_by_ap_id(activity.data["object"])
|
||||
|
||||
case object && object.data["type"] do
|
||||
"ChatMessage" -> "pleroma:chat_mention"
|
||||
_ -> "mention"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -30,12 +30,29 @@ defmodule Pleroma.Notification do
|
|||
|
||||
schema "notifications" do
|
||||
field(:seen, :boolean, default: false)
|
||||
# This is an enum type in the database. If you add a new notification type,
|
||||
# remember to add a migration to add it to the `notifications_type` enum
|
||||
# as well.
|
||||
field(:type, :string)
|
||||
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def update_notification_type(user, activity) do
|
||||
with %__MODULE__{} = notification <-
|
||||
Repo.get_by(__MODULE__, user_id: user.id, activity_id: activity.id) do
|
||||
type =
|
||||
activity
|
||||
|> type_from_activity()
|
||||
|
||||
notification
|
||||
|> changeset(%{type: type})
|
||||
|> Repo.update()
|
||||
end
|
||||
end
|
||||
|
||||
@spec unread_notifications_count(User.t()) :: integer()
|
||||
def unread_notifications_count(%User{id: user_id}) do
|
||||
from(q in __MODULE__,
|
||||
|
@ -44,9 +61,21 @@ def unread_notifications_count(%User{id: user_id}) do
|
|||
|> Repo.aggregate(:count, :id)
|
||||
end
|
||||
|
||||
@notification_types ~w{
|
||||
favourite
|
||||
follow
|
||||
follow_request
|
||||
mention
|
||||
move
|
||||
pleroma:chat_mention
|
||||
pleroma:emoji_reaction
|
||||
reblog
|
||||
}
|
||||
|
||||
def changeset(%Notification{} = notification, attrs) do
|
||||
notification
|
||||
|> cast(attrs, [:seen])
|
||||
|> cast(attrs, [:seen, :type])
|
||||
|> validate_inclusion(:type, @notification_types)
|
||||
end
|
||||
|
||||
@spec last_read_query(User.t()) :: Ecto.Queryable.t()
|
||||
|
@ -300,42 +329,95 @@ def dismiss(%{id: user_id} = _user, id) do
|
|||
end
|
||||
end
|
||||
|
||||
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
|
||||
object = Object.normalize(activity)
|
||||
def create_notifications(activity, options \\ [])
|
||||
|
||||
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
|
||||
object = Object.normalize(activity, false)
|
||||
|
||||
if object && object.data["type"] == "Answer" do
|
||||
{:ok, []}
|
||||
else
|
||||
do_create_notifications(activity)
|
||||
do_create_notifications(activity, options)
|
||||
end
|
||||
end
|
||||
|
||||
def create_notifications(%Activity{data: %{"type" => type}} = activity)
|
||||
def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
|
||||
when type in ["Follow", "Like", "Announce", "Move", "EmojiReact"] do
|
||||
do_create_notifications(activity)
|
||||
do_create_notifications(activity, options)
|
||||
end
|
||||
|
||||
def create_notifications(_), do: {:ok, []}
|
||||
def create_notifications(_, _), do: {:ok, []}
|
||||
|
||||
defp do_create_notifications(%Activity{} = activity, options) do
|
||||
do_send = Keyword.get(options, :do_send, true)
|
||||
|
||||
defp do_create_notifications(%Activity{} = activity) do
|
||||
{enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
|
||||
potential_receivers = enabled_receivers ++ disabled_receivers
|
||||
|
||||
notifications =
|
||||
Enum.map(potential_receivers, fn user ->
|
||||
do_send = user in enabled_receivers
|
||||
do_send = do_send && user in enabled_receivers
|
||||
create_notification(activity, user, do_send)
|
||||
end)
|
||||
|
||||
{:ok, notifications}
|
||||
end
|
||||
|
||||
defp type_from_activity(%{data: %{"type" => type}} = activity) do
|
||||
case type do
|
||||
"Follow" ->
|
||||
if Activity.follow_accepted?(activity) do
|
||||
"follow"
|
||||
else
|
||||
"follow_request"
|
||||
end
|
||||
|
||||
"Announce" ->
|
||||
"reblog"
|
||||
|
||||
"Like" ->
|
||||
"favourite"
|
||||
|
||||
"Move" ->
|
||||
"move"
|
||||
|
||||
"EmojiReact" ->
|
||||
"pleroma:emoji_reaction"
|
||||
|
||||
# Compatibility with old reactions
|
||||
"EmojiReaction" ->
|
||||
"pleroma:emoji_reaction"
|
||||
|
||||
"Create" ->
|
||||
activity
|
||||
|> type_from_activity_object()
|
||||
|
||||
t ->
|
||||
raise "No notification type for activity type #{t}"
|
||||
end
|
||||
end
|
||||
|
||||
defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention"
|
||||
|
||||
defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do
|
||||
object = Object.get_by_ap_id(activity.data["object"])
|
||||
|
||||
case object && object.data["type"] do
|
||||
"ChatMessage" -> "pleroma:chat_mention"
|
||||
_ -> "mention"
|
||||
end
|
||||
end
|
||||
|
||||
# TODO move to sql, too.
|
||||
def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
|
||||
unless skip?(activity, user) do
|
||||
{:ok, %{notification: notification}} =
|
||||
Multi.new()
|
||||
|> Multi.insert(:notification, %Notification{user_id: user.id, activity: activity})
|
||||
|> Multi.insert(:notification, %Notification{
|
||||
user_id: user.id,
|
||||
activity: activity,
|
||||
type: type_from_activity(activity)
|
||||
})
|
||||
|> Marker.multi_set_last_read_id(user, "notifications")
|
||||
|> Repo.transaction()
|
||||
|
||||
|
@ -527,4 +609,12 @@ def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity,
|
|||
end
|
||||
|
||||
def skip?(_, _, _), do: false
|
||||
|
||||
def for_user_and_activity(user, activity) do
|
||||
from(n in __MODULE__,
|
||||
where: n.user_id == ^user.id,
|
||||
where: n.activity_id == ^activity.id
|
||||
)
|
||||
|> Repo.one()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -67,6 +67,7 @@ def store(upload, opts \\ []) do
|
|||
{:ok,
|
||||
%{
|
||||
"type" => opts.activity_type,
|
||||
"mediaType" => upload.content_type,
|
||||
"url" => [
|
||||
%{
|
||||
"type" => "Link",
|
||||
|
|
|
@ -112,7 +112,14 @@ defp increase_poll_votes_if_vote(%{
|
|||
|
||||
defp increase_poll_votes_if_vote(_create_data), do: :noop
|
||||
|
||||
@object_types ["ChatMessage"]
|
||||
@spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
|
||||
def persist(%{"type" => type} = object, meta) when type in @object_types do
|
||||
with {:ok, object} <- Object.create(object) do
|
||||
{:ok, object, meta}
|
||||
end
|
||||
end
|
||||
|
||||
def persist(object, meta) do
|
||||
with local <- Keyword.fetch!(meta, :local),
|
||||
{recipients, _, _} <- get_recipients(object),
|
||||
|
@ -344,20 +351,21 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
|
|||
end
|
||||
end
|
||||
|
||||
@spec follow(User.t(), User.t(), String.t() | nil, boolean()) ::
|
||||
@spec follow(User.t(), User.t(), String.t() | nil, boolean(), keyword()) ::
|
||||
{:ok, Activity.t()} | {:error, any()}
|
||||
def follow(follower, followed, activity_id \\ nil, local \\ true) do
|
||||
def follow(follower, followed, activity_id \\ nil, local \\ true, opts \\ []) do
|
||||
with {:ok, result} <-
|
||||
Repo.transaction(fn -> do_follow(follower, followed, activity_id, local) end) do
|
||||
Repo.transaction(fn -> do_follow(follower, followed, activity_id, local, opts) end) do
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
defp do_follow(follower, followed, activity_id, local) do
|
||||
defp do_follow(follower, followed, activity_id, local, opts) do
|
||||
skip_notify_and_stream = Keyword.get(opts, :skip_notify_and_stream, false)
|
||||
data = make_follow_data(follower, followed, activity_id)
|
||||
|
||||
with {:ok, activity} <- insert(data, local),
|
||||
_ <- notify_and_stream(activity),
|
||||
_ <- skip_notify_and_stream || notify_and_stream(activity),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
else
|
||||
|
@ -1000,6 +1008,18 @@ 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_invisible_actors(query, %{invisible_actors: true}), do: query
|
||||
|
||||
defp exclude_invisible_actors(query, _opts) do
|
||||
|
@ -1115,6 +1135,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|
|||
|> restrict_instance(opts)
|
||||
|> Activity.restrict_deactivated_users()
|
||||
|> exclude_poll_votes(opts)
|
||||
|> exclude_chat_messages(opts)
|
||||
|> exclude_invisible_actors(opts)
|
||||
|> exclude_visibility(opts)
|
||||
end
|
||||
|
|
|
@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
This module encodes our addressing policies and general shape of our objects.
|
||||
"""
|
||||
|
||||
alias Pleroma.Emoji
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Relay
|
||||
|
@ -65,6 +66,42 @@ def delete(actor, object_id) do
|
|||
}, []}
|
||||
end
|
||||
|
||||
def create(actor, object, recipients) do
|
||||
{:ok,
|
||||
%{
|
||||
"id" => Utils.generate_activity_id(),
|
||||
"actor" => actor.ap_id,
|
||||
"to" => recipients,
|
||||
"object" => object,
|
||||
"type" => "Create",
|
||||
"published" => DateTime.utc_now() |> DateTime.to_iso8601()
|
||||
}, []}
|
||||
end
|
||||
|
||||
def chat_message(actor, recipient, content, opts \\ []) do
|
||||
basic = %{
|
||||
"id" => Utils.generate_object_id(),
|
||||
"actor" => actor.ap_id,
|
||||
"type" => "ChatMessage",
|
||||
"to" => [recipient],
|
||||
"content" => content,
|
||||
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
|
||||
"emoji" => Emoji.Formatter.get_emoji_map(content)
|
||||
}
|
||||
|
||||
case opts[:attachment] do
|
||||
%Object{data: attachment_data} ->
|
||||
{
|
||||
:ok,
|
||||
Map.put(basic, "attachment", attachment_data),
|
||||
[]
|
||||
}
|
||||
|
||||
_ ->
|
||||
{:ok, basic, []}
|
||||
end
|
||||
end
|
||||
|
||||
@spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
|
||||
def tombstone(actor, id) do
|
||||
{:ok,
|
||||
|
|
|
@ -12,6 +12,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
|
||||
|
@ -43,8 +45,20 @@ def validate(%{"type" => "Delete"} = object, meta) do
|
|||
|
||||
def validate(%{"type" => "Like"} = object, meta) do
|
||||
with {:ok, object} <-
|
||||
object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do
|
||||
object = stringify_keys(object |> Map.from_struct())
|
||||
object
|
||||
|> LikeValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert) do
|
||||
object = stringify_keys(object)
|
||||
{:ok, object, meta}
|
||||
end
|
||||
end
|
||||
|
||||
def validate(%{"type" => "ChatMessage"} = object, meta) do
|
||||
with {:ok, object} <-
|
||||
object
|
||||
|> ChatMessageValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert) do
|
||||
object = stringify_keys(object)
|
||||
{:ok, object, meta}
|
||||
end
|
||||
end
|
||||
|
@ -59,6 +73,18 @@ def validate(%{"type" => "EmojiReact"} = object, meta) do
|
|||
end
|
||||
end
|
||||
|
||||
def validate(%{"type" => "Create", "object" => object} = create_activity, meta) do
|
||||
with {:ok, object_data} <- cast_and_apply(object),
|
||||
meta = Keyword.put(meta, :object_data, object_data |> stringify_keys),
|
||||
{:ok, create_activity} <-
|
||||
create_activity
|
||||
|> CreateChatMessageValidator.cast_and_validate(meta)
|
||||
|> Ecto.Changeset.apply_action(:insert) do
|
||||
create_activity = stringify_keys(create_activity)
|
||||
{:ok, create_activity, meta}
|
||||
end
|
||||
end
|
||||
|
||||
def validate(%{"type" => "Announce"} = object, meta) do
|
||||
with {:ok, object} <-
|
||||
object
|
||||
|
@ -69,17 +95,30 @@ def validate(%{"type" => "Announce"} = object, meta) do
|
|||
end
|
||||
end
|
||||
|
||||
def cast_and_apply(%{"type" => "ChatMessage"} = object) do
|
||||
ChatMessageValidator.cast_and_apply(object)
|
||||
end
|
||||
|
||||
def cast_and_apply(o), do: {:error, {:validator_not_set, o}}
|
||||
|
||||
def stringify_keys(%{__struct__: _} = object) do
|
||||
object
|
||||
|> Map.from_struct()
|
||||
|> stringify_keys
|
||||
end
|
||||
|
||||
def stringify_keys(object) do
|
||||
def stringify_keys(object) when is_map(object) do
|
||||
object
|
||||
|> Map.new(fn {key, val} -> {to_string(key), val} end)
|
||||
|> Map.new(fn {key, val} -> {to_string(key), stringify_keys(val)} end)
|
||||
end
|
||||
|
||||
def stringify_keys(object) when is_list(object) do
|
||||
object
|
||||
|> Enum.map(&stringify_keys/1)
|
||||
end
|
||||
|
||||
def stringify_keys(object), do: object
|
||||
|
||||
def fetch_actor(object) do
|
||||
with {:ok, actor} <- Types.ObjectID.cast(object["actor"]) do
|
||||
User.get_or_fetch_by_ap_id(actor)
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key false
|
||||
embedded_schema do
|
||||
field(:type, :string)
|
||||
field(:mediaType, :string, default: "application/octet-stream")
|
||||
field(:name, :string)
|
||||
|
||||
embeds_many(:url, UrlObjectValidator)
|
||||
end
|
||||
|
||||
def cast_and_validate(data) do
|
||||
data
|
||||
|> cast_data()
|
||||
|> validate_data()
|
||||
end
|
||||
|
||||
def cast_data(data) do
|
||||
%__MODULE__{}
|
||||
|> changeset(data)
|
||||
end
|
||||
|
||||
def changeset(struct, data) do
|
||||
data =
|
||||
data
|
||||
|> fix_media_type()
|
||||
|> fix_url()
|
||||
|
||||
struct
|
||||
|> cast(data, [:type, :mediaType, :name])
|
||||
|> cast_embed(:url, required: true)
|
||||
end
|
||||
|
||||
def fix_media_type(data) do
|
||||
data =
|
||||
data
|
||||
|> Map.put_new("mediaType", data["mimeType"])
|
||||
|
||||
if MIME.valid?(data["mediaType"]) do
|
||||
data
|
||||
else
|
||||
data
|
||||
|> Map.put("mediaType", "application/octet-stream")
|
||||
end
|
||||
end
|
||||
|
||||
def fix_url(data) do
|
||||
case data["url"] do
|
||||
url when is_binary(url) ->
|
||||
data
|
||||
|> Map.put(
|
||||
"url",
|
||||
[
|
||||
%{
|
||||
"href" => url,
|
||||
"type" => "Link",
|
||||
"mediaType" => data["mediaType"]
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
_ ->
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
def validate_data(cng) do
|
||||
cng
|
||||
|> validate_required([:mediaType, :url, :type])
|
||||
end
|
||||
end
|
|
@ -0,0 +1,123 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||
|
||||
import Ecto.Changeset
|
||||
import Pleroma.Web.ActivityPub.Transmogrifier, only: [fix_emoji: 1]
|
||||
|
||||
@primary_key false
|
||||
@derive Jason.Encoder
|
||||
|
||||
embedded_schema do
|
||||
field(:id, Types.ObjectID, primary_key: true)
|
||||
field(:to, Types.Recipients, default: [])
|
||||
field(:type, :string)
|
||||
field(:content, Types.SafeText)
|
||||
field(:actor, Types.ObjectID)
|
||||
field(:published, Types.DateTime)
|
||||
field(:emoji, :map, default: %{})
|
||||
|
||||
embeds_one(:attachment, AttachmentValidator)
|
||||
end
|
||||
|
||||
def cast_and_apply(data) do
|
||||
data
|
||||
|> cast_data
|
||||
|> apply_action(:insert)
|
||||
end
|
||||
|
||||
def cast_and_validate(data) do
|
||||
data
|
||||
|> cast_data()
|
||||
|> validate_data()
|
||||
end
|
||||
|
||||
def cast_data(data) do
|
||||
%__MODULE__{}
|
||||
|> changeset(data)
|
||||
end
|
||||
|
||||
def fix(data) do
|
||||
data
|
||||
|> fix_emoji()
|
||||
|> fix_attachment()
|
||||
|> Map.put_new("actor", data["attributedTo"])
|
||||
end
|
||||
|
||||
# Throws everything but the first one away
|
||||
def fix_attachment(%{"attachment" => [attachment | _]} = data) do
|
||||
data
|
||||
|> Map.put("attachment", attachment)
|
||||
end
|
||||
|
||||
def fix_attachment(data), do: data
|
||||
|
||||
def changeset(struct, data) do
|
||||
data = fix(data)
|
||||
|
||||
struct
|
||||
|> cast(data, List.delete(__schema__(:fields), :attachment))
|
||||
|> cast_embed(:attachment)
|
||||
end
|
||||
|
||||
def validate_data(data_cng) do
|
||||
data_cng
|
||||
|> validate_inclusion(:type, ["ChatMessage"])
|
||||
|> validate_required([:id, :actor, :to, :type, :published])
|
||||
|> validate_content_or_attachment()
|
||||
|> validate_length(:to, is: 1)
|
||||
|> validate_length(:content, max: Pleroma.Config.get([:instance, :remote_limit]))
|
||||
|> validate_local_concern()
|
||||
end
|
||||
|
||||
def validate_content_or_attachment(cng) do
|
||||
attachment = get_field(cng, :attachment)
|
||||
|
||||
if attachment do
|
||||
cng
|
||||
else
|
||||
cng
|
||||
|> validate_required([:content])
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Validates the following
|
||||
- If both users are in our system
|
||||
- If at least one of the users in this ChatMessage is a local user
|
||||
- If the recipient is not blocking the actor
|
||||
"""
|
||||
def validate_local_concern(cng) do
|
||||
with actor_ap <- get_field(cng, :actor),
|
||||
{_, %User{} = actor} <- {:find_actor, User.get_cached_by_ap_id(actor_ap)},
|
||||
{_, %User{} = recipient} <-
|
||||
{:find_recipient, User.get_cached_by_ap_id(get_field(cng, :to) |> hd())},
|
||||
{_, false} <- {:blocking_actor?, User.blocks?(recipient, actor)},
|
||||
{_, true} <- {:local?, Enum.any?([actor, recipient], & &1.local)} do
|
||||
cng
|
||||
else
|
||||
{:blocking_actor?, true} ->
|
||||
cng
|
||||
|> add_error(:actor, "actor is blocked by recipient")
|
||||
|
||||
{:local?, false} ->
|
||||
cng
|
||||
|> add_error(:actor, "actor and recipient are both remote")
|
||||
|
||||
{:find_actor, _} ->
|
||||
cng
|
||||
|> add_error(:actor, "can't find user")
|
||||
|
||||
{:find_recipient, _} ->
|
||||
cng
|
||||
|> add_error(:to, "can't find user")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,91 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
# NOTES
|
||||
# - Can probably be a generic create validator
|
||||
# - doesn't embed, will only get the object id
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||
|
||||
import Ecto.Changeset
|
||||
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:id, Types.ObjectID, primary_key: true)
|
||||
field(:actor, Types.ObjectID)
|
||||
field(:type, :string)
|
||||
field(:to, Types.Recipients, default: [])
|
||||
field(:object, Types.ObjectID)
|
||||
end
|
||||
|
||||
def cast_and_apply(data) do
|
||||
data
|
||||
|> cast_data
|
||||
|> apply_action(:insert)
|
||||
end
|
||||
|
||||
def cast_data(data) do
|
||||
cast(%__MODULE__{}, data, __schema__(:fields))
|
||||
end
|
||||
|
||||
def cast_and_validate(data, meta \\ []) do
|
||||
cast_data(data)
|
||||
|> validate_data(meta)
|
||||
end
|
||||
|
||||
def validate_data(cng, meta \\ []) do
|
||||
cng
|
||||
|> validate_required([:id, :actor, :to, :type, :object])
|
||||
|> validate_inclusion(:type, ["Create"])
|
||||
|> validate_actor_presence()
|
||||
|> validate_recipients_match(meta)
|
||||
|> validate_actors_match(meta)
|
||||
|> validate_object_nonexistence()
|
||||
end
|
||||
|
||||
def validate_object_nonexistence(cng) do
|
||||
cng
|
||||
|> validate_change(:object, fn :object, object_id ->
|
||||
if Object.get_cached_by_ap_id(object_id) do
|
||||
[{:object, "The object to create already exists"}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def validate_actors_match(cng, meta) do
|
||||
object_actor = meta[:object_data]["actor"]
|
||||
|
||||
cng
|
||||
|> validate_change(:actor, fn :actor, actor ->
|
||||
if actor == object_actor do
|
||||
[]
|
||||
else
|
||||
[{:actor, "Actor doesn't match with object actor"}]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def validate_recipients_match(cng, meta) do
|
||||
object_recipients = meta[:object_data]["to"] || []
|
||||
|
||||
cng
|
||||
|> validate_change(:to, fn :to, recipients ->
|
||||
activity_set = MapSet.new(recipients)
|
||||
object_set = MapSet.new(object_recipients)
|
||||
|
||||
if MapSet.equal?(activity_set, object_set) do
|
||||
[]
|
||||
else
|
||||
[{:to, "Recipients don't match with object recipients"}]
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
|
@ -46,12 +46,13 @@ def add_deleted_activity_id(cng) do
|
|||
Answer
|
||||
Article
|
||||
Audio
|
||||
ChatMessage
|
||||
Event
|
||||
Note
|
||||
Page
|
||||
Question
|
||||
Video
|
||||
Tombstone
|
||||
Video
|
||||
}
|
||||
def validate_data(cng) do
|
||||
cng
|
||||
|
|
|
@ -11,11 +11,13 @@ def cast(object) when is_binary(object) do
|
|||
|
||||
def cast(data) when is_list(data) do
|
||||
data
|
||||
|> Enum.reduce({:ok, []}, fn element, acc ->
|
||||
case {acc, ObjectID.cast(element)} do
|
||||
{:error, _} -> :error
|
||||
{_, :error} -> :error
|
||||
{{:ok, list}, {:ok, id}} -> {:ok, [id | list]}
|
||||
|> Enum.reduce_while({:ok, []}, fn element, {:ok, list} ->
|
||||
case ObjectID.cast(element) do
|
||||
{:ok, id} ->
|
||||
{:cont, {:ok, [id | list]}}
|
||||
|
||||
_ ->
|
||||
{:halt, :error}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.SafeText do
|
||||
use Ecto.Type
|
||||
|
||||
alias Pleroma.HTML
|
||||
|
||||
def type, do: :string
|
||||
|
||||
def cast(str) when is_binary(str) do
|
||||
{:ok, HTML.filter_tags(str)}
|
||||
end
|
||||
|
||||
def cast(_), do: :error
|
||||
|
||||
def dump(data) do
|
||||
{:ok, data}
|
||||
end
|
||||
|
||||
def load(data) do
|
||||
{:ok, data}
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.UrlObjectValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
|
||||
|
||||
import Ecto.Changeset
|
||||
@primary_key false
|
||||
|
||||
embedded_schema do
|
||||
field(:type, :string)
|
||||
field(:href, Types.Uri)
|
||||
field(:mediaType, :string)
|
||||
end
|
||||
|
||||
def changeset(struct, data) do
|
||||
struct
|
||||
|> cast(data, __schema__(:fields))
|
||||
|> validate_required([:type, :href, :mediaType])
|
||||
end
|
||||
end
|
|
@ -17,6 +17,10 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
|
|||
{:ok, Activity.t() | Object.t(), keyword()} | {:error, any()}
|
||||
def common_pipeline(object, meta) do
|
||||
case Repo.transaction(fn -> do_common_pipeline(object, meta) end) do
|
||||
{:ok, {:ok, activity, meta}} ->
|
||||
SideEffects.handle_after_transaction(meta)
|
||||
{:ok, activity, meta}
|
||||
|
||||
{:ok, value} ->
|
||||
value
|
||||
|
||||
|
|
|
@ -6,12 +6,17 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
collection, and so on.
|
||||
"""
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Chat
|
||||
alias Pleroma.Chat.MessageReference
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Pipeline
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.Push
|
||||
alias Pleroma.Web.Streamer
|
||||
|
||||
def handle(object, meta \\ [])
|
||||
|
||||
|
@ -27,6 +32,24 @@ def handle(%{data: %{"type" => "Like"}} = object, meta) do
|
|||
{:ok, object, meta}
|
||||
end
|
||||
|
||||
# Tasks this handles
|
||||
# - Actually create object
|
||||
# - Rollback if we couldn't create it
|
||||
# - Set up notifications
|
||||
def handle(%{data: %{"type" => "Create"}} = activity, meta) do
|
||||
with {:ok, _object, meta} <- handle_object_creation(meta[:object_data], meta) do
|
||||
{:ok, notifications} = Notification.create_notifications(activity, do_send: false)
|
||||
|
||||
meta =
|
||||
meta
|
||||
|> add_notifications(notifications)
|
||||
|
||||
{:ok, activity, meta}
|
||||
else
|
||||
e -> Repo.rollback(e)
|
||||
end
|
||||
end
|
||||
|
||||
# Tasks this handles:
|
||||
# - Add announce to object
|
||||
# - Set up notification
|
||||
|
@ -88,6 +111,8 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object,
|
|||
Object.decrease_replies_count(in_reply_to)
|
||||
end
|
||||
|
||||
MessageReference.delete_for_object(deleted_object)
|
||||
|
||||
ActivityPub.stream_out(object)
|
||||
ActivityPub.stream_out_participations(deleted_object, user)
|
||||
:ok
|
||||
|
@ -112,6 +137,39 @@ def handle(object, meta) do
|
|||
{:ok, object, meta}
|
||||
end
|
||||
|
||||
def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do
|
||||
with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
|
||||
actor = User.get_cached_by_ap_id(object.data["actor"])
|
||||
recipient = User.get_cached_by_ap_id(hd(object.data["to"]))
|
||||
|
||||
streamables =
|
||||
[[actor, recipient], [recipient, actor]]
|
||||
|> Enum.map(fn [user, other_user] ->
|
||||
if user.local do
|
||||
{:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
|
||||
{:ok, cm_ref} = MessageReference.create(chat, object, user.ap_id != actor.ap_id)
|
||||
|
||||
{
|
||||
["user", "user:pleroma_chat"],
|
||||
{user, %{cm_ref | chat: chat, object: object}}
|
||||
}
|
||||
end
|
||||
end)
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
meta =
|
||||
meta
|
||||
|> add_streamables(streamables)
|
||||
|
||||
{:ok, object, meta}
|
||||
end
|
||||
end
|
||||
|
||||
# Nothing to do
|
||||
def handle_object_creation(object) do
|
||||
{:ok, object}
|
||||
end
|
||||
|
||||
def handle_undoing(%{data: %{"type" => "Like"}} = object) do
|
||||
with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
|
||||
{:ok, _} <- Utils.remove_like_from_object(object, liked_object),
|
||||
|
@ -148,4 +206,43 @@ def handle_undoing(
|
|||
end
|
||||
|
||||
def handle_undoing(object), do: {:error, ["don't know how to handle", object]}
|
||||
|
||||
defp send_notifications(meta) do
|
||||
Keyword.get(meta, :notifications, [])
|
||||
|> Enum.each(fn notification ->
|
||||
Streamer.stream(["user", "user:notification"], notification)
|
||||
Push.send(notification)
|
||||
end)
|
||||
|
||||
meta
|
||||
end
|
||||
|
||||
defp send_streamables(meta) do
|
||||
Keyword.get(meta, :streamables, [])
|
||||
|> Enum.each(fn {topics, items} ->
|
||||
Streamer.stream(topics, items)
|
||||
end)
|
||||
|
||||
meta
|
||||
end
|
||||
|
||||
defp add_streamables(meta, streamables) do
|
||||
existing = Keyword.get(meta, :streamables, [])
|
||||
|
||||
meta
|
||||
|> Keyword.put(:streamables, streamables ++ existing)
|
||||
end
|
||||
|
||||
defp add_notifications(meta, notifications) do
|
||||
existing = Keyword.get(meta, :notifications, [])
|
||||
|
||||
meta
|
||||
|> Keyword.put(:notifications, notifications ++ existing)
|
||||
end
|
||||
|
||||
def handle_after_transaction(meta) do
|
||||
meta
|
||||
|> send_notifications()
|
||||
|> send_streamables()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
alias Pleroma.EarmarkRenderer
|
||||
alias Pleroma.FollowingRelationship
|
||||
alias Pleroma.Maps
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Object.Containment
|
||||
alias Pleroma.Repo
|
||||
|
@ -527,7 +528,8 @@ def handle_incoming(
|
|||
User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
|
||||
{:ok, %User{} = follower} <-
|
||||
User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
|
||||
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
|
||||
{:ok, activity} <-
|
||||
ActivityPub.follow(follower, followed, id, false, skip_notify_and_stream: true) do
|
||||
with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
|
||||
{_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
|
||||
{_, false} <- {:user_locked, User.locked?(followed)},
|
||||
|
@ -570,6 +572,7 @@ def handle_incoming(
|
|||
:noop
|
||||
end
|
||||
|
||||
ActivityPub.notify_and_stream(activity)
|
||||
{:ok, activity}
|
||||
else
|
||||
_e ->
|
||||
|
@ -590,6 +593,8 @@ def handle_incoming(
|
|||
User.update_follower_count(followed)
|
||||
User.update_following_count(follower)
|
||||
|
||||
Notification.update_notification_type(followed, follow_activity)
|
||||
|
||||
ActivityPub.accept(%{
|
||||
to: follow_activity.data["to"],
|
||||
type: "Accept",
|
||||
|
@ -657,6 +662,16 @@ def handle_incoming(
|
|||
|> handle_incoming(options)
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
|
||||
_options
|
||||
) do
|
||||
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
|
||||
{:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(%{"type" => type} = data, _options)
|
||||
when type in ["Like", "EmojiReact", "Announce"] do
|
||||
with :ok <- ObjectValidator.fetch_actor_and_object(data),
|
||||
|
@ -1108,6 +1123,9 @@ 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
|
||||
|
|
355
lib/pleroma/web/api_spec/operations/chat_operation.ex
Normal file
355
lib/pleroma/web/api_spec/operations/chat_operation.ex
Normal file
|
@ -0,0 +1,355 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.ChatOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Chat
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ChatMessage
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
@spec open_api_operation(atom) :: Operation.t()
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def mark_as_read_operation do
|
||||
%Operation{
|
||||
tags: ["chat"],
|
||||
summary: "Mark all messages in the chat as read",
|
||||
operationId: "ChatController.mark_as_read",
|
||||
parameters: [Operation.parameter(:id, :path, :string, "The ID of the Chat")],
|
||||
requestBody: request_body("Parameters", mark_as_read()),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response(
|
||||
"The updated chat",
|
||||
"application/json",
|
||||
Chat
|
||||
)
|
||||
},
|
||||
security: [
|
||||
%{
|
||||
"oAuth" => ["write:chats"]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def mark_message_as_read_operation do
|
||||
%Operation{
|
||||
tags: ["chat"],
|
||||
summary: "Mark one message in the chat as read",
|
||||
operationId: "ChatController.mark_message_as_read",
|
||||
parameters: [
|
||||
Operation.parameter(:id, :path, :string, "The ID of the Chat"),
|
||||
Operation.parameter(:message_id, :path, :string, "The ID of the message")
|
||||
],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response(
|
||||
"The read ChatMessage",
|
||||
"application/json",
|
||||
ChatMessage
|
||||
)
|
||||
},
|
||||
security: [
|
||||
%{
|
||||
"oAuth" => ["write:chats"]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["chat"],
|
||||
summary: "Create a chat",
|
||||
operationId: "ChatController.show",
|
||||
parameters: [
|
||||
Operation.parameter(
|
||||
:id,
|
||||
:path,
|
||||
:string,
|
||||
"The id of the chat",
|
||||
required: true,
|
||||
example: "1234"
|
||||
)
|
||||
],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response(
|
||||
"The existing chat",
|
||||
"application/json",
|
||||
Chat
|
||||
)
|
||||
},
|
||||
security: [
|
||||
%{
|
||||
"oAuth" => ["read"]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["chat"],
|
||||
summary: "Create a chat",
|
||||
operationId: "ChatController.create",
|
||||
parameters: [
|
||||
Operation.parameter(
|
||||
:id,
|
||||
:path,
|
||||
:string,
|
||||
"The account id of the recipient of this chat",
|
||||
required: true,
|
||||
example: "someflakeid"
|
||||
)
|
||||
],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response(
|
||||
"The created or existing chat",
|
||||
"application/json",
|
||||
Chat
|
||||
)
|
||||
},
|
||||
security: [
|
||||
%{
|
||||
"oAuth" => ["write:chats"]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["chat"],
|
||||
summary: "Get a list of chats that you participated in",
|
||||
operationId: "ChatController.index",
|
||||
parameters: pagination_params(),
|
||||
responses: %{
|
||||
200 => Operation.response("The chats of the user", "application/json", chats_response())
|
||||
},
|
||||
security: [
|
||||
%{
|
||||
"oAuth" => ["read:chats"]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def messages_operation do
|
||||
%Operation{
|
||||
tags: ["chat"],
|
||||
summary: "Get the most recent messages of the chat",
|
||||
operationId: "ChatController.messages",
|
||||
parameters:
|
||||
[Operation.parameter(:id, :path, :string, "The ID of the Chat")] ++
|
||||
pagination_params(),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response(
|
||||
"The messages in the chat",
|
||||
"application/json",
|
||||
chat_messages_response()
|
||||
)
|
||||
},
|
||||
security: [
|
||||
%{
|
||||
"oAuth" => ["read:chats"]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def post_chat_message_operation do
|
||||
%Operation{
|
||||
tags: ["chat"],
|
||||
summary: "Post a message to the chat",
|
||||
operationId: "ChatController.post_chat_message",
|
||||
parameters: [
|
||||
Operation.parameter(:id, :path, :string, "The ID of the Chat")
|
||||
],
|
||||
requestBody: request_body("Parameters", chat_message_create()),
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response(
|
||||
"The newly created ChatMessage",
|
||||
"application/json",
|
||||
ChatMessage
|
||||
),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError)
|
||||
},
|
||||
security: [
|
||||
%{
|
||||
"oAuth" => ["write:chats"]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def delete_message_operation do
|
||||
%Operation{
|
||||
tags: ["chat"],
|
||||
summary: "delete_message",
|
||||
operationId: "ChatController.delete_message",
|
||||
parameters: [
|
||||
Operation.parameter(:id, :path, :string, "The ID of the Chat"),
|
||||
Operation.parameter(:message_id, :path, :string, "The ID of the message")
|
||||
],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response(
|
||||
"The deleted ChatMessage",
|
||||
"application/json",
|
||||
ChatMessage
|
||||
)
|
||||
},
|
||||
security: [
|
||||
%{
|
||||
"oAuth" => ["write:chats"]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def chats_response do
|
||||
%Schema{
|
||||
title: "ChatsResponse",
|
||||
description: "Response schema for multiple Chats",
|
||||
type: :array,
|
||||
items: Chat,
|
||||
example: [
|
||||
%{
|
||||
"account" => %{
|
||||
"pleroma" => %{
|
||||
"is_admin" => false,
|
||||
"confirmation_pending" => false,
|
||||
"hide_followers_count" => false,
|
||||
"is_moderator" => false,
|
||||
"hide_favorites" => true,
|
||||
"ap_id" => "https://dontbulling.me/users/lain",
|
||||
"hide_follows_count" => false,
|
||||
"hide_follows" => false,
|
||||
"background_image" => nil,
|
||||
"skip_thread_containment" => false,
|
||||
"hide_followers" => false,
|
||||
"relationship" => %{},
|
||||
"tags" => []
|
||||
},
|
||||
"avatar" =>
|
||||
"https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg",
|
||||
"following_count" => 0,
|
||||
"header_static" => "https://originalpatchou.li/images/banner.png",
|
||||
"source" => %{
|
||||
"sensitive" => false,
|
||||
"note" => "lain",
|
||||
"pleroma" => %{
|
||||
"discoverable" => false,
|
||||
"actor_type" => "Person"
|
||||
},
|
||||
"fields" => []
|
||||
},
|
||||
"statuses_count" => 1,
|
||||
"locked" => false,
|
||||
"created_at" => "2020-04-16T13:40:15.000Z",
|
||||
"display_name" => "lain",
|
||||
"fields" => [],
|
||||
"acct" => "lain@dontbulling.me",
|
||||
"id" => "9u6Qw6TAZANpqokMkK",
|
||||
"emojis" => [],
|
||||
"avatar_static" =>
|
||||
"https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg",
|
||||
"username" => "lain",
|
||||
"followers_count" => 0,
|
||||
"header" => "https://originalpatchou.li/images/banner.png",
|
||||
"bot" => false,
|
||||
"note" => "lain",
|
||||
"url" => "https://dontbulling.me/users/lain"
|
||||
},
|
||||
"id" => "1",
|
||||
"unread" => 2
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def chat_messages_response do
|
||||
%Schema{
|
||||
title: "ChatMessagesResponse",
|
||||
description: "Response schema for multiple ChatMessages",
|
||||
type: :array,
|
||||
items: ChatMessage,
|
||||
example: [
|
||||
%{
|
||||
"emojis" => [
|
||||
%{
|
||||
"static_url" => "https://dontbulling.me/emoji/Firefox.gif",
|
||||
"visible_in_picker" => false,
|
||||
"shortcode" => "firefox",
|
||||
"url" => "https://dontbulling.me/emoji/Firefox.gif"
|
||||
}
|
||||
],
|
||||
"created_at" => "2020-04-21T15:11:46.000Z",
|
||||
"content" => "Check this out :firefox:",
|
||||
"id" => "13",
|
||||
"chat_id" => "1",
|
||||
"actor_id" => "someflakeid",
|
||||
"unread" => false
|
||||
},
|
||||
%{
|
||||
"actor_id" => "someflakeid",
|
||||
"content" => "Whats' up?",
|
||||
"id" => "12",
|
||||
"chat_id" => "1",
|
||||
"emojis" => [],
|
||||
"created_at" => "2020-04-21T15:06:45.000Z",
|
||||
"unread" => false
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def chat_message_create do
|
||||
%Schema{
|
||||
title: "ChatMessageCreateRequest",
|
||||
description: "POST body for creating an chat message",
|
||||
type: :object,
|
||||
properties: %{
|
||||
content: %Schema{
|
||||
type: :string,
|
||||
description: "The content of your message. Optional if media_id is present"
|
||||
},
|
||||
media_id: %Schema{type: :string, description: "The id of an upload"}
|
||||
},
|
||||
example: %{
|
||||
"content" => "Hey wanna buy feet pics?",
|
||||
"media_id" => "134234"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def mark_as_read do
|
||||
%Schema{
|
||||
title: "MarkAsReadRequest",
|
||||
description: "POST body for marking a number of chat messages as read",
|
||||
type: :object,
|
||||
required: [:last_read_id],
|
||||
properties: %{
|
||||
last_read_id: %Schema{
|
||||
type: :string,
|
||||
description: "The content of your message."
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"last_read_id" => "abcdef12456"
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
|
@ -185,6 +185,7 @@ defp notification_type do
|
|||
"mention",
|
||||
"poll",
|
||||
"pleroma:emoji_reaction",
|
||||
"pleroma:chat_mention",
|
||||
"move",
|
||||
"follow_request"
|
||||
],
|
||||
|
|
|
@ -141,6 +141,11 @@ defp create_request do
|
|||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Receive poll notifications?"
|
||||
},
|
||||
"pleroma:chat_mention": %Schema{
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Receive chat notifications?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
75
lib/pleroma/web/api_spec/schemas/chat.ex
Normal file
75
lib/pleroma/web/api_spec/schemas/chat.ex
Normal file
|
@ -0,0 +1,75 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Schemas.Chat do
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ChatMessage
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "Chat",
|
||||
description: "Response schema for a Chat",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
account: %Schema{type: :object},
|
||||
unread: %Schema{type: :integer},
|
||||
last_message: ChatMessage,
|
||||
updated_at: %Schema{type: :string, format: :"date-time"}
|
||||
},
|
||||
example: %{
|
||||
"account" => %{
|
||||
"pleroma" => %{
|
||||
"is_admin" => false,
|
||||
"confirmation_pending" => false,
|
||||
"hide_followers_count" => false,
|
||||
"is_moderator" => false,
|
||||
"hide_favorites" => true,
|
||||
"ap_id" => "https://dontbulling.me/users/lain",
|
||||
"hide_follows_count" => false,
|
||||
"hide_follows" => false,
|
||||
"background_image" => nil,
|
||||
"skip_thread_containment" => false,
|
||||
"hide_followers" => false,
|
||||
"relationship" => %{},
|
||||
"tags" => []
|
||||
},
|
||||
"avatar" =>
|
||||
"https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg",
|
||||
"following_count" => 0,
|
||||
"header_static" => "https://originalpatchou.li/images/banner.png",
|
||||
"source" => %{
|
||||
"sensitive" => false,
|
||||
"note" => "lain",
|
||||
"pleroma" => %{
|
||||
"discoverable" => false,
|
||||
"actor_type" => "Person"
|
||||
},
|
||||
"fields" => []
|
||||
},
|
||||
"statuses_count" => 1,
|
||||
"locked" => false,
|
||||
"created_at" => "2020-04-16T13:40:15.000Z",
|
||||
"display_name" => "lain",
|
||||
"fields" => [],
|
||||
"acct" => "lain@dontbulling.me",
|
||||
"id" => "9u6Qw6TAZANpqokMkK",
|
||||
"emojis" => [],
|
||||
"avatar_static" =>
|
||||
"https://dontbulling.me/media/065a4dd3c6740dab13ff9c71ec7d240bb9f8be9205c9e7467fb2202117da1e32.jpg",
|
||||
"username" => "lain",
|
||||
"followers_count" => 0,
|
||||
"header" => "https://originalpatchou.li/images/banner.png",
|
||||
"bot" => false,
|
||||
"note" => "lain",
|
||||
"url" => "https://dontbulling.me/users/lain"
|
||||
},
|
||||
"id" => "1",
|
||||
"unread" => 2,
|
||||
"last_message" => ChatMessage.schema().example(),
|
||||
"updated_at" => "2020-04-21T15:06:45.000Z"
|
||||
}
|
||||
})
|
||||
end
|
41
lib/pleroma/web/api_spec/schemas/chat_message.ex
Normal file
41
lib/pleroma/web/api_spec/schemas/chat_message.ex
Normal file
|
@ -0,0 +1,41 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Schemas.ChatMessage do
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "ChatMessage",
|
||||
description: "Response schema for a ChatMessage",
|
||||
nullable: true,
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
account_id: %Schema{type: :string, description: "The Mastodon API id of the actor"},
|
||||
chat_id: %Schema{type: :string},
|
||||
content: %Schema{type: :string, nullable: true},
|
||||
created_at: %Schema{type: :string, format: :"date-time"},
|
||||
emojis: %Schema{type: :array},
|
||||
attachment: %Schema{type: :object, nullable: true}
|
||||
},
|
||||
example: %{
|
||||
"account_id" => "someflakeid",
|
||||
"chat_id" => "1",
|
||||
"content" => "hey you again",
|
||||
"created_at" => "2020-04-21T15:06:45.000Z",
|
||||
"emojis" => [
|
||||
%{
|
||||
"static_url" => "https://dontbulling.me/emoji/Firefox.gif",
|
||||
"visible_in_picker" => false,
|
||||
"shortcode" => "firefox",
|
||||
"url" => "https://dontbulling.me/emoji/Firefox.gif"
|
||||
}
|
||||
],
|
||||
"id" => "14",
|
||||
"attachment" => nil
|
||||
}
|
||||
})
|
||||
end
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
alias Pleroma.ActivityExpiration
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.FollowingRelationship
|
||||
alias Pleroma.Formatter
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.ThreadMute
|
||||
|
@ -24,6 +25,53 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
require Pleroma.Constants
|
||||
require Logger
|
||||
|
||||
def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
|
||||
with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
|
||||
:ok <- validate_chat_content_length(content, !!maybe_attachment),
|
||||
{_, {:ok, chat_message_data, _meta}} <-
|
||||
{:build_object,
|
||||
Builder.chat_message(
|
||||
user,
|
||||
recipient.ap_id,
|
||||
content |> format_chat_content,
|
||||
attachment: maybe_attachment
|
||||
)},
|
||||
{_, {:ok, create_activity_data, _meta}} <-
|
||||
{:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
|
||||
{_, {:ok, %Activity{} = activity, _meta}} <-
|
||||
{:common_pipeline,
|
||||
Pipeline.common_pipeline(create_activity_data,
|
||||
local: true
|
||||
)} do
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
defp format_chat_content(nil), do: nil
|
||||
|
||||
defp format_chat_content(content) do
|
||||
{text, _, _} =
|
||||
content
|
||||
|> Formatter.html_escape("text/plain")
|
||||
|> Formatter.linkify()
|
||||
|> (fn {text, mentions, tags} ->
|
||||
{String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
|
||||
end).()
|
||||
|
||||
text
|
||||
end
|
||||
|
||||
defp validate_chat_content_length(_, true), do: :ok
|
||||
defp validate_chat_content_length(nil, false), do: {:error, :no_content}
|
||||
|
||||
defp validate_chat_content_length(content, _) do
|
||||
if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
|
||||
:ok
|
||||
else
|
||||
{:error, :content_too_long}
|
||||
end
|
||||
end
|
||||
|
||||
def unblock(blocker, blocked) do
|
||||
with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
|
||||
{:ok, unblock_data, _} <- Builder.undo(blocker, block),
|
||||
|
@ -73,6 +121,7 @@ def accept_follow_request(follower, followed) do
|
|||
object: follow_activity.data["id"],
|
||||
type: "Accept"
|
||||
}) do
|
||||
Notification.update_notification_type(followed, follow_activity)
|
||||
{:ok, follower}
|
||||
end
|
||||
end
|
||||
|
@ -427,12 +476,13 @@ def remove_mute(user, activity) do
|
|||
{:ok, activity}
|
||||
end
|
||||
|
||||
def thread_muted?(%{id: nil} = _user, _activity), do: false
|
||||
|
||||
def thread_muted?(user, activity) do
|
||||
ThreadMute.exists?(user.id, activity.data["context"])
|
||||
def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
|
||||
when is_binary("context") do
|
||||
ThreadMute.exists?(user_id, context)
|
||||
end
|
||||
|
||||
def thread_muted?(_, _), do: false
|
||||
|
||||
def report(user, data) do
|
||||
with {:ok, account} <- get_reported_account(data.account_id),
|
||||
{:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
|
||||
|
|
|
@ -429,7 +429,7 @@ def maybe_notify_mentioned_recipients(
|
|||
%Activity{data: %{"to" => _to, "type" => type} = data} = activity
|
||||
)
|
||||
when type == "Create" do
|
||||
object = Object.normalize(activity)
|
||||
object = Object.normalize(activity, false)
|
||||
|
||||
object_data =
|
||||
cond do
|
||||
|
|
|
@ -42,8 +42,20 @@ def index(conn, %{account_id: account_id} = params) do
|
|||
end
|
||||
end
|
||||
|
||||
@default_notification_types ~w{
|
||||
mention
|
||||
follow
|
||||
follow_request
|
||||
reblog
|
||||
favourite
|
||||
move
|
||||
pleroma:emoji_reaction
|
||||
}
|
||||
def index(%{assigns: %{user: user}} = conn, params) do
|
||||
params = Map.new(params, fn {k, v} -> {to_string(k), v} end)
|
||||
params =
|
||||
Map.new(params, fn {k, v} -> {to_string(k), v} end)
|
||||
|> Map.put_new("include_types", @default_notification_types)
|
||||
|
||||
notifications = MastodonAPI.get_notifications(user, params)
|
||||
|
||||
conn
|
||||
|
|
|
@ -124,6 +124,7 @@ defp resource_search(:v1, "hashtags", query, _options) do
|
|||
defp prepare_tags(query, add_joined_tag \\ true) do
|
||||
tags =
|
||||
query
|
||||
|> preprocess_uri_query()
|
||||
|> String.split(~r/[^#\w]+/u, trim: true)
|
||||
|> Enum.uniq_by(&String.downcase/1)
|
||||
|
||||
|
@ -147,6 +148,19 @@ defp prepare_tags(query, add_joined_tag \\ true) do
|
|||
end
|
||||
end
|
||||
|
||||
# If `query` is a URI, returns last component of its path, otherwise returns `query`
|
||||
defp preprocess_uri_query(query) do
|
||||
if query =~ ~r/https?:\/\// do
|
||||
query
|
||||
|> URI.parse()
|
||||
|> Map.get(:path)
|
||||
|> String.split("/")
|
||||
|> Enum.at(-1)
|
||||
else
|
||||
query
|
||||
end
|
||||
end
|
||||
|
||||
defp joined_tag(tags) do
|
||||
tags
|
||||
|> Enum.map(fn tag -> String.capitalize(tag) end)
|
||||
|
|
|
@ -6,7 +6,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
|
|||
import Ecto.Query
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Pagination
|
||||
alias Pleroma.ScheduledActivity
|
||||
|
@ -82,15 +81,11 @@ defp cast_params(params) do
|
|||
end
|
||||
|
||||
defp restrict(query, :include_types, %{include_types: mastodon_types = [_ | _]}) do
|
||||
ap_types = convert_and_filter_mastodon_types(mastodon_types)
|
||||
|
||||
where(query, [q, a], fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
|
||||
where(query, [n], n.type in ^mastodon_types)
|
||||
end
|
||||
|
||||
defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do
|
||||
ap_types = convert_and_filter_mastodon_types(mastodon_types)
|
||||
|
||||
where(query, [q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
|
||||
where(query, [n], n.type not in ^mastodon_types)
|
||||
end
|
||||
|
||||
defp restrict(query, :account_ap_id, %{account_ap_id: account_ap_id}) do
|
||||
|
@ -98,10 +93,4 @@ defp restrict(query, :account_ap_id, %{account_ap_id: account_ap_id}) do
|
|||
end
|
||||
|
||||
defp restrict(query, _, _), do: query
|
||||
|
||||
defp convert_and_filter_mastodon_types(types) do
|
||||
types
|
||||
|> Enum.map(&Activity.from_mastodon_notification_type/1)
|
||||
|> Enum.filter(& &1)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -235,6 +235,7 @@ defp do_render("show.json", %{user: user} = opts) do
|
|||
|
||||
# Pleroma extension
|
||||
pleroma: %{
|
||||
ap_id: user.ap_id,
|
||||
confirmation_pending: user.confirmation_pending,
|
||||
tags: user.tags,
|
||||
hide_followers_count: user.hide_followers_count,
|
||||
|
|
|
@ -69,7 +69,8 @@ def features do
|
|||
if Config.get([:instance, :safe_dm_mentions]) do
|
||||
"safe_dm_mentions"
|
||||
end,
|
||||
"pleroma_emoji_reactions"
|
||||
"pleroma_emoji_reactions",
|
||||
"pleroma_chat_messages"
|
||||
]
|
||||
|> Enum.filter(& &1)
|
||||
end
|
||||
|
|
|
@ -6,26 +6,28 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
|||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Chat.MessageReference
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.UserRelationship
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
alias Pleroma.Web.MastodonAPI.NotificationView
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
|
||||
|
||||
@parent_types ~w{Like Announce EmojiReact}
|
||||
|
||||
def render("index.json", %{notifications: notifications, for: reading_user} = opts) do
|
||||
activities = Enum.map(notifications, & &1.activity)
|
||||
|
||||
parent_activities =
|
||||
activities
|
||||
|> Enum.filter(
|
||||
&(Activity.mastodon_notification_type(&1) in [
|
||||
"favourite",
|
||||
"reblog",
|
||||
"pleroma:emoji_reaction"
|
||||
])
|
||||
)
|
||||
|> Enum.filter(fn
|
||||
%{data: %{"type" => type}} ->
|
||||
type in @parent_types
|
||||
end)
|
||||
|> Enum.map(& &1.data["object"])
|
||||
|> Activity.create_by_object_ap_id()
|
||||
|> Activity.with_preloaded_object(:left)
|
||||
|
@ -42,7 +44,7 @@ def render("index.json", %{notifications: notifications, for: reading_user} = op
|
|||
true ->
|
||||
move_activities_targets =
|
||||
activities
|
||||
|> Enum.filter(&(Activity.mastodon_notification_type(&1) == "move"))
|
||||
|> Enum.filter(&(&1.data["type"] == "Move"))
|
||||
|> Enum.map(&User.get_cached_by_ap_id(&1.data["target"]))
|
||||
|
||||
actors =
|
||||
|
@ -79,8 +81,6 @@ def render(
|
|||
end
|
||||
end
|
||||
|
||||
mastodon_type = Activity.mastodon_notification_type(activity)
|
||||
|
||||
# Note: :relationships contain user mutes (needed for :muted flag in :status)
|
||||
status_render_opts = %{relationships: opts[:relationships]}
|
||||
|
||||
|
@ -91,7 +91,7 @@ def render(
|
|||
) do
|
||||
response = %{
|
||||
id: to_string(notification.id),
|
||||
type: mastodon_type,
|
||||
type: notification.type,
|
||||
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
|
||||
account: account,
|
||||
pleroma: %{
|
||||
|
@ -99,7 +99,7 @@ def render(
|
|||
}
|
||||
}
|
||||
|
||||
case mastodon_type do
|
||||
case notification.type do
|
||||
"mention" ->
|
||||
put_status(response, activity, reading_user, status_render_opts)
|
||||
|
||||
|
@ -117,6 +117,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)
|
||||
|
||||
type when type in ["follow", "follow_request"] ->
|
||||
response
|
||||
|
||||
|
@ -132,6 +135,17 @@ defp put_emoji(response, activity) do
|
|||
Map.put(response, :emoji, activity.data["content"])
|
||||
end
|
||||
|
||||
defp put_chat_message(response, activity, reading_user, opts) do
|
||||
object = Object.normalize(activity)
|
||||
author = User.get_cached_by_ap_id(object.data["actor"])
|
||||
chat = Pleroma.Chat.get(reading_user.id, author.ap_id)
|
||||
cm_ref = MessageReference.for_chat_and_object(chat, object)
|
||||
render_opts = Map.merge(opts, %{for: reading_user, chat_message_reference: cm_ref})
|
||||
chat_message_render = MessageReferenceView.render("show.json", render_opts)
|
||||
|
||||
Map.put(response, :chat_message, chat_message_render)
|
||||
end
|
||||
|
||||
defp put_status(response, activity, reading_user, opts) do
|
||||
status_render_opts = Map.merge(opts, %{activity: activity, for: reading_user})
|
||||
status_render = StatusView.render("show.json", status_render_opts)
|
||||
|
|
174
lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
Normal file
174
lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
Normal file
|
@ -0,0 +1,174 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
defmodule Pleroma.Web.PleromaAPI.ChatController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Chat
|
||||
alias Pleroma.Chat.MessageReference
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Pagination
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
|
||||
alias Pleroma.Web.PleromaAPI.ChatView
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["write:chats"]}
|
||||
when action in [
|
||||
:post_chat_message,
|
||||
:create,
|
||||
:mark_as_read,
|
||||
:mark_message_as_read,
|
||||
:delete_message
|
||||
]
|
||||
)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read:chats"]} when action in [:messages, :index, :show]
|
||||
)
|
||||
|
||||
plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation
|
||||
|
||||
def delete_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{
|
||||
message_id: message_id,
|
||||
id: chat_id
|
||||
}) do
|
||||
with %MessageReference{} = cm_ref <-
|
||||
MessageReference.get_by_id(message_id),
|
||||
^chat_id <- cm_ref.chat_id |> to_string(),
|
||||
%Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id),
|
||||
{:ok, _} <- remove_or_delete(cm_ref, user) do
|
||||
conn
|
||||
|> put_view(MessageReferenceView)
|
||||
|> render("show.json", chat_message_reference: cm_ref)
|
||||
else
|
||||
_e ->
|
||||
{:error, :could_not_delete}
|
||||
end
|
||||
end
|
||||
|
||||
defp remove_or_delete(
|
||||
%{object: %{data: %{"actor" => actor, "id" => id}}},
|
||||
%{ap_id: actor} = user
|
||||
) do
|
||||
with %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
|
||||
CommonAPI.delete(activity.id, user)
|
||||
end
|
||||
end
|
||||
|
||||
defp remove_or_delete(cm_ref, _) do
|
||||
cm_ref
|
||||
|> MessageReference.delete()
|
||||
end
|
||||
|
||||
def post_chat_message(
|
||||
%{body_params: params, assigns: %{user: %{id: user_id} = user}} = conn,
|
||||
%{
|
||||
id: id
|
||||
}
|
||||
) do
|
||||
with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id),
|
||||
%User{} = recipient <- User.get_cached_by_ap_id(chat.recipient),
|
||||
{:ok, activity} <-
|
||||
CommonAPI.post_chat_message(user, recipient, params[:content],
|
||||
media_id: params[:media_id]
|
||||
),
|
||||
message <- Object.normalize(activity, false),
|
||||
cm_ref <- MessageReference.for_chat_and_object(chat, message) do
|
||||
conn
|
||||
|> put_view(MessageReferenceView)
|
||||
|> render("show.json", for: user, chat_message_reference: cm_ref)
|
||||
end
|
||||
end
|
||||
|
||||
def mark_message_as_read(%{assigns: %{user: %{id: user_id} = user}} = conn, %{
|
||||
id: chat_id,
|
||||
message_id: message_id
|
||||
}) do
|
||||
with %MessageReference{} = cm_ref <-
|
||||
MessageReference.get_by_id(message_id),
|
||||
^chat_id <- cm_ref.chat_id |> to_string(),
|
||||
%Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id),
|
||||
{:ok, cm_ref} <- MessageReference.mark_as_read(cm_ref) do
|
||||
conn
|
||||
|> put_view(MessageReferenceView)
|
||||
|> render("show.json", for: user, chat_message_reference: cm_ref)
|
||||
end
|
||||
end
|
||||
|
||||
def mark_as_read(
|
||||
%{body_params: %{last_read_id: last_read_id}, assigns: %{user: %{id: user_id}}} = conn,
|
||||
%{id: id}
|
||||
) do
|
||||
with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id),
|
||||
{_n, _} <-
|
||||
MessageReference.set_all_seen_for_chat(chat, last_read_id) do
|
||||
conn
|
||||
|> put_view(ChatView)
|
||||
|> render("show.json", chat: chat)
|
||||
end
|
||||
end
|
||||
|
||||
def messages(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: id} = params) do
|
||||
with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do
|
||||
cm_refs =
|
||||
chat
|
||||
|> MessageReference.for_chat_query()
|
||||
|> Pagination.fetch_paginated(params)
|
||||
|
||||
conn
|
||||
|> put_view(MessageReferenceView)
|
||||
|> render("index.json", for: user, chat_message_references: cm_refs)
|
||||
else
|
||||
_ ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "not found"})
|
||||
end
|
||||
end
|
||||
|
||||
def index(%{assigns: %{user: %{id: user_id} = user}} = conn, _params) do
|
||||
blocked_ap_ids = User.blocked_users_ap_ids(user)
|
||||
|
||||
chats =
|
||||
from(c in Chat,
|
||||
where: c.user_id == ^user_id,
|
||||
where: c.recipient not in ^blocked_ap_ids,
|
||||
order_by: [desc: c.updated_at]
|
||||
)
|
||||
|> Repo.all()
|
||||
|
||||
conn
|
||||
|> put_view(ChatView)
|
||||
|> render("index.json", chats: chats)
|
||||
end
|
||||
|
||||
def create(%{assigns: %{user: user}} = conn, params) do
|
||||
with %User{ap_id: recipient} <- User.get_by_id(params[:id]),
|
||||
{:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do
|
||||
conn
|
||||
|> put_view(ChatView)
|
||||
|> render("show.json", chat: chat)
|
||||
end
|
||||
end
|
||||
|
||||
def show(%{assigns: %{user: user}} = conn, params) do
|
||||
with %Chat{} = chat <- Repo.get_by(Chat, user_id: user.id, id: params[:id]) do
|
||||
conn
|
||||
|> put_view(ChatView)
|
||||
|> render("show.json", chat: chat)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,45 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
|
||||
def render(
|
||||
"show.json",
|
||||
%{
|
||||
chat_message_reference: %{
|
||||
id: id,
|
||||
object: %{data: chat_message},
|
||||
chat_id: chat_id,
|
||||
unread: unread
|
||||
}
|
||||
}
|
||||
) do
|
||||
%{
|
||||
id: id |> to_string(),
|
||||
content: chat_message["content"],
|
||||
chat_id: chat_id |> to_string(),
|
||||
account_id: User.get_cached_by_ap_id(chat_message["actor"]).id,
|
||||
created_at: Utils.to_masto_date(chat_message["published"]),
|
||||
emojis: StatusView.build_emojis(chat_message["emoji"]),
|
||||
attachment:
|
||||
chat_message["attachment"] &&
|
||||
StatusView.render("attachment.json", attachment: chat_message["attachment"]),
|
||||
unread: unread
|
||||
}
|
||||
end
|
||||
|
||||
def render("index.json", opts) do
|
||||
render_many(
|
||||
opts[:chat_message_references],
|
||||
__MODULE__,
|
||||
"show.json",
|
||||
Map.put(opts, :as, :chat_message_reference)
|
||||
)
|
||||
end
|
||||
end
|
33
lib/pleroma/web/pleroma_api/views/chat_view.ex
Normal file
33
lib/pleroma/web/pleroma_api/views/chat_view.ex
Normal file
|
@ -0,0 +1,33 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.PleromaAPI.ChatView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.Chat
|
||||
alias Pleroma.Chat.MessageReference
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
|
||||
|
||||
def render("show.json", %{chat: %Chat{} = chat} = opts) do
|
||||
recipient = User.get_cached_by_ap_id(chat.recipient)
|
||||
last_message = opts[:last_message] || MessageReference.last_message_for_chat(chat)
|
||||
|
||||
%{
|
||||
id: chat.id |> to_string(),
|
||||
account: AccountView.render("show.json", Map.put(opts, :user, recipient)),
|
||||
unread: MessageReference.unread_count_for_chat(chat),
|
||||
last_message:
|
||||
last_message &&
|
||||
MessageReferenceView.render("show.json", chat_message_reference: last_message),
|
||||
updated_at: Utils.to_masto_date(chat.updated_at)
|
||||
}
|
||||
end
|
||||
|
||||
def render("index.json", %{chats: chats}) do
|
||||
render_many(chats, __MODULE__, "show.json")
|
||||
end
|
||||
end
|
|
@ -16,8 +16,6 @@ defmodule Pleroma.Web.Push.Impl do
|
|||
require Logger
|
||||
import Ecto.Query
|
||||
|
||||
defdelegate mastodon_notification_type(activity), to: Activity
|
||||
|
||||
@types ["Create", "Follow", "Announce", "Like", "Move"]
|
||||
|
||||
@doc "Performs sending notifications for user subscriptions"
|
||||
|
@ -31,10 +29,10 @@ def perform(
|
|||
when activity_type in @types do
|
||||
actor = User.get_cached_by_ap_id(notification.activity.data["actor"])
|
||||
|
||||
mastodon_type = mastodon_notification_type(notification.activity)
|
||||
mastodon_type = notification.type
|
||||
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
|
||||
avatar_url = User.avatar_url(actor)
|
||||
object = Object.normalize(activity)
|
||||
object = Object.normalize(activity, false)
|
||||
user = User.get_cached_by_id(user_id)
|
||||
direct_conversation_id = Activity.direct_conversation_id(activity, user)
|
||||
|
||||
|
@ -116,7 +114,7 @@ def build_content(
|
|||
end
|
||||
|
||||
def build_content(notification, actor, object, mastodon_type) do
|
||||
mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
|
||||
mastodon_type = mastodon_type || notification.type
|
||||
|
||||
%{
|
||||
title: format_title(notification, mastodon_type),
|
||||
|
@ -126,6 +124,13 @@ 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", "content" => content}}, _) do
|
||||
case content do
|
||||
nil -> "@#{actor.nickname}: (Attachment)"
|
||||
content -> "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
|
||||
end
|
||||
end
|
||||
|
||||
def format_body(
|
||||
%{activity: %{data: %{"type" => "Create"}}},
|
||||
actor,
|
||||
|
@ -151,7 +156,7 @@ def format_body(
|
|||
mastodon_type
|
||||
)
|
||||
when type in ["Follow", "Like"] do
|
||||
mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
|
||||
mastodon_type = mastodon_type || notification.type
|
||||
|
||||
case mastodon_type do
|
||||
"follow" -> "@#{actor.nickname} has followed you"
|
||||
|
@ -166,15 +171,14 @@ def format_title(%{activity: %{data: %{"directMessage" => true}}}, _mastodon_typ
|
|||
"New Direct Message"
|
||||
end
|
||||
|
||||
def format_title(%{activity: activity}, mastodon_type) do
|
||||
mastodon_type = mastodon_type || mastodon_notification_type(activity)
|
||||
|
||||
case mastodon_type do
|
||||
def format_title(%{type: type}, mastodon_type) do
|
||||
case mastodon_type || type do
|
||||
"mention" -> "New Mention"
|
||||
"follow" -> "New Follower"
|
||||
"follow_request" -> "New Follow Request"
|
||||
"reblog" -> "New Repeat"
|
||||
"favourite" -> "New Favorite"
|
||||
"pleroma:chat_mention" -> "New Chat Message"
|
||||
type -> "New #{String.capitalize(type || "event")}"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,7 +25,7 @@ defmodule Pleroma.Web.Push.Subscription do
|
|||
timestamps()
|
||||
end
|
||||
|
||||
@supported_alert_types ~w[follow favourite mention reblog]a
|
||||
@supported_alert_types ~w[follow favourite mention reblog pleroma:chat_mention]a
|
||||
|
||||
defp alerts(%{data: %{alerts: alerts}}) do
|
||||
alerts = Map.take(alerts, @supported_alert_types)
|
||||
|
|
|
@ -306,6 +306,15 @@ defmodule Pleroma.Web.Router do
|
|||
scope [] do
|
||||
pipe_through(:authenticated_api)
|
||||
|
||||
post("/chats/by-account-id/:id", ChatController, :create)
|
||||
get("/chats", ChatController, :index)
|
||||
get("/chats/:id", ChatController, :show)
|
||||
get("/chats/:id/messages", ChatController, :messages)
|
||||
post("/chats/:id/messages", ChatController, :post_chat_message)
|
||||
delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
|
||||
post("/chats/:id/read", ChatController, :mark_as_read)
|
||||
post("/chats/:id/messages/:message_id/read", ChatController, :mark_message_as_read)
|
||||
|
||||
get("/conversations/:id/statuses", ConversationController, :statuses)
|
||||
get("/conversations/:id", ConversationController, :show)
|
||||
post("/conversations/read", ConversationController, :mark_as_read)
|
||||
|
|
|
@ -6,6 +6,7 @@ defmodule Pleroma.Web.Streamer do
|
|||
require Logger
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Chat.MessageReference
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Notification
|
||||
|
@ -22,7 +23,7 @@ defmodule Pleroma.Web.Streamer do
|
|||
def registry, do: @registry
|
||||
|
||||
@public_streams ["public", "public:local", "public:media", "public:local:media"]
|
||||
@user_streams ["user", "user:notification", "direct"]
|
||||
@user_streams ["user", "user:notification", "direct", "user:pleroma_chat"]
|
||||
|
||||
@doc "Expands and authorizes a stream, and registers the process for streaming."
|
||||
@spec get_topic_and_add_socket(stream :: String.t(), User.t() | nil, Map.t() | nil) ::
|
||||
|
@ -89,34 +90,20 @@ def remove_socket(topic) do
|
|||
if should_env_send?(), do: Registry.unregister(@registry, topic)
|
||||
end
|
||||
|
||||
def stream(topics, item) when is_list(topics) do
|
||||
def stream(topics, items) do
|
||||
if should_env_send?() do
|
||||
Enum.each(topics, fn t ->
|
||||
spawn(fn -> do_stream(t, item) end)
|
||||
List.wrap(topics)
|
||||
|> Enum.each(fn topic ->
|
||||
List.wrap(items)
|
||||
|> Enum.each(fn item ->
|
||||
spawn(fn -> do_stream(topic, item) end)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def stream(topic, items) when is_list(items) do
|
||||
if should_env_send?() do
|
||||
Enum.each(items, fn i ->
|
||||
spawn(fn -> do_stream(topic, i) end)
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def stream(topic, item) do
|
||||
if should_env_send?() do
|
||||
spawn(fn -> do_stream(topic, item) end)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def filtered_by_user?(%User{} = user, %Activity{} = item) do
|
||||
%{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} =
|
||||
User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute])
|
||||
|
@ -200,6 +187,19 @@ 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")
|
||||
|
||||
|
|
|
@ -51,6 +51,29 @@ def render("update.json", %Activity{} = activity) do
|
|||
|> Jason.encode!()
|
||||
end
|
||||
|
||||
def render("chat_update.json", %{chat_message_reference: cm_ref}) do
|
||||
# Explicitly giving the cmr for the object here, so we don't accidentally
|
||||
# send a later 'last_message' that was inserted between inserting this and
|
||||
# streaming it out
|
||||
#
|
||||
# It also contains the chat with a cache of the correct unread count
|
||||
Logger.debug("Trying to stream out #{inspect(cm_ref)}")
|
||||
|
||||
representation =
|
||||
Pleroma.Web.PleromaAPI.ChatView.render(
|
||||
"show.json",
|
||||
%{last_message: cm_ref, chat: cm_ref.chat}
|
||||
)
|
||||
|
||||
%{
|
||||
event: "pleroma:chat_update",
|
||||
payload:
|
||||
representation
|
||||
|> Jason.encode!()
|
||||
}
|
||||
|> Jason.encode!()
|
||||
end
|
||||
|
||||
def render("conversation.json", %Participation{} = participation) do
|
||||
%{
|
||||
event: "conversation",
|
||||
|
|
16
priv/repo/migrations/20200309123730_create_chats.exs
Normal file
16
priv/repo/migrations/20200309123730_create_chats.exs
Normal file
|
@ -0,0 +1,16 @@
|
|||
defmodule Pleroma.Repo.Migrations.CreateChats do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:chats) do
|
||||
add(:user_id, references(:users, type: :uuid))
|
||||
# Recipient is an ActivityPub id, to future-proof for group support.
|
||||
add(:recipient, :string)
|
||||
add(:unread, :integer, default: 0)
|
||||
timestamps()
|
||||
end
|
||||
|
||||
# There's only one chat between a user and a recipient.
|
||||
create(index(:chats, [:user_id, :recipient], unique: true))
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
defmodule Pleroma.Repo.Migrations.AddTypeToNotifications do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:notifications) do
|
||||
add(:type, :string)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,10 @@
|
|||
defmodule Pleroma.Repo.Migrations.BackfillNotificationTypes do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
Pleroma.MigrationHelper.NotificationBackfill.fill_in_notification_types()
|
||||
end
|
||||
|
||||
def down do
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Repo.Migrations.CreateChatMessageReference do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:chat_message_references, primary_key: false) do
|
||||
add(:id, :uuid, primary_key: true)
|
||||
add(:chat_id, references(:chats, on_delete: :delete_all), null: false)
|
||||
add(:object_id, references(:objects, on_delete: :delete_all), null: false)
|
||||
add(:seen, :boolean, default: false, null: false)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create(index(:chat_message_references, [:chat_id, "id desc"]))
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
defmodule Pleroma.Repo.Migrations.AddUniqueIndexToChatMessageReferences do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create(unique_index(:chat_message_references, [:object_id, :chat_id]))
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
defmodule Pleroma.Repo.Migrations.RemoveUnreadFromChats do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:chats) do
|
||||
remove(:unread, :integer, default: 0)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
defmodule Pleroma.Repo.Migrations.AddSeenIndexToChatMessageReferences do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create(
|
||||
index(:chat_message_references, [:chat_id],
|
||||
where: "seen = false",
|
||||
name: "unseen_messages_count_index"
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,30 @@
|
|||
defmodule Pleroma.Repo.Migrations.MigrateSeenToUnreadInChatMessageReferences do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
drop(
|
||||
index(:chat_message_references, [:chat_id],
|
||||
where: "seen = false",
|
||||
name: "unseen_messages_count_index"
|
||||
)
|
||||
)
|
||||
|
||||
alter table(:chat_message_references) do
|
||||
add(:unread, :boolean, default: true)
|
||||
end
|
||||
|
||||
execute("update chat_message_references set unread = not seen")
|
||||
|
||||
alter table(:chat_message_references) do
|
||||
modify(:unread, :boolean, default: true, null: false)
|
||||
remove(:seen, :boolean, default: false, null: false)
|
||||
end
|
||||
|
||||
create(
|
||||
index(:chat_message_references, [:chat_id],
|
||||
where: "unread = true",
|
||||
name: "unread_messages_count_index"
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,36 @@
|
|||
defmodule Pleroma.Repo.Migrations.ChangeTypeToEnumForNotifications do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
"""
|
||||
create type notification_type as enum (
|
||||
'follow',
|
||||
'follow_request',
|
||||
'mention',
|
||||
'move',
|
||||
'pleroma:emoji_reaction',
|
||||
'pleroma:chat_mention',
|
||||
'reblog',
|
||||
'favourite'
|
||||
)
|
||||
"""
|
||||
|> execute()
|
||||
|
||||
"""
|
||||
alter table notifications
|
||||
alter column type type notification_type using (type::notification_type)
|
||||
"""
|
||||
|> execute()
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:notifications) do
|
||||
modify(:type, :string)
|
||||
end
|
||||
|
||||
"""
|
||||
drop type notification_type
|
||||
"""
|
||||
|> execute()
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
defmodule Pleroma.Repo.Migrations.ChangeChatIdToFlake do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
execute("""
|
||||
alter table chats
|
||||
drop constraint chats_pkey cascade,
|
||||
alter column id drop default,
|
||||
alter column id set data type uuid using cast( lpad( to_hex(id), 32, '0') as uuid),
|
||||
add primary key (id)
|
||||
""")
|
||||
|
||||
execute("""
|
||||
alter table chat_message_references
|
||||
alter column chat_id set data type uuid using cast( lpad( to_hex(chat_id), 32, '0') as uuid),
|
||||
add constraint chat_message_references_chat_id_fkey foreign key (chat_id) references chats(id) on delete cascade
|
||||
""")
|
||||
end
|
||||
|
||||
def down do
|
||||
:ok
|
||||
end
|
||||
end
|
|
@ -30,6 +30,7 @@
|
|||
"@type": "@id"
|
||||
},
|
||||
"EmojiReact": "litepub:EmojiReact",
|
||||
"ChatMessage": "litepub:ChatMessage",
|
||||
"alsoKnownAs": {
|
||||
"@id": "as:alsoKnownAs",
|
||||
"@type": "@id"
|
||||
|
|
29
test/chat/message_reference_test.exs
Normal file
29
test/chat/message_reference_test.exs
Normal file
|
@ -0,0 +1,29 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Chat.MessageReferenceTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.Chat
|
||||
alias Pleroma.Chat.MessageReference
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
describe "messages" do
|
||||
test "it returns the last message in a chat" do
|
||||
user = insert(:user)
|
||||
recipient = insert(:user)
|
||||
|
||||
{:ok, _message_1} = CommonAPI.post_chat_message(user, recipient, "hey")
|
||||
{:ok, _message_2} = CommonAPI.post_chat_message(recipient, user, "ho")
|
||||
|
||||
{:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id)
|
||||
|
||||
message = MessageReference.last_message_for_chat(chat)
|
||||
|
||||
assert message.object.data["content"] == "ho"
|
||||
end
|
||||
end
|
||||
end
|
61
test/chat_test.exs
Normal file
61
test/chat_test.exs
Normal file
|
@ -0,0 +1,61 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.ChatTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.Chat
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
describe "creation and getting" do
|
||||
test "it only works if the recipient is a valid user (for now)" do
|
||||
user = insert(:user)
|
||||
|
||||
assert {:error, _chat} = Chat.bump_or_create(user.id, "http://some/nonexisting/account")
|
||||
assert {:error, _chat} = Chat.get_or_create(user.id, "http://some/nonexisting/account")
|
||||
end
|
||||
|
||||
test "it creates a chat for a user and recipient" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
|
||||
|
||||
assert chat.id
|
||||
end
|
||||
|
||||
test "it returns and bumps a chat for a user and recipient if it already exists" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
|
||||
{:ok, chat_two} = Chat.bump_or_create(user.id, other_user.ap_id)
|
||||
|
||||
assert chat.id == chat_two.id
|
||||
end
|
||||
|
||||
test "it returns a chat for a user and recipient if it already exists" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
|
||||
{:ok, chat_two} = Chat.get_or_create(user.id, other_user.ap_id)
|
||||
|
||||
assert chat.id == chat_two.id
|
||||
end
|
||||
|
||||
test "a returning chat will have an updated `update_at` field" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
|
||||
:timer.sleep(1500)
|
||||
{:ok, chat_two} = Chat.bump_or_create(user.id, other_user.ap_id)
|
||||
|
||||
assert chat.id == chat_two.id
|
||||
assert chat.updated_at != chat_two.updated_at
|
||||
end
|
||||
end
|
||||
end
|
31
test/fixtures/create-chat-message.json
vendored
Normal file
31
test/fixtures/create-chat-message.json
vendored
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"actor": "http://2hu.gensokyo/users/raymoo",
|
||||
"id": "http://2hu.gensokyo/objects/1",
|
||||
"object": {
|
||||
"attributedTo": "http://2hu.gensokyo/users/raymoo",
|
||||
"content": "You expected a cute girl? Too bad. <script>alert('XSS')</script>",
|
||||
"id": "http://2hu.gensokyo/objects/2",
|
||||
"published": "2020-02-12T14:08:20Z",
|
||||
"to": [
|
||||
"http://2hu.gensokyo/users/marisa"
|
||||
],
|
||||
"tag": [
|
||||
{
|
||||
"icon": {
|
||||
"type": "Image",
|
||||
"url": "http://2hu.gensokyo/emoji/Firefox.gif"
|
||||
},
|
||||
"id": "http://2hu.gensokyo/emoji/Firefox.gif",
|
||||
"name": ":firefox:",
|
||||
"type": "Emoji",
|
||||
"updated": "1970-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"type": "ChatMessage"
|
||||
},
|
||||
"published": "2018-02-12T14:08:20Z",
|
||||
"to": [
|
||||
"http://2hu.gensokyo/users/marisa"
|
||||
],
|
||||
"type": "Create"
|
||||
}
|
56
test/migration_helper/notification_backfill_test.exs
Normal file
56
test/migration_helper/notification_backfill_test.exs
Normal file
|
@ -0,0 +1,56 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.MigrationHelper.NotificationBackfillTest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.MigrationHelper.NotificationBackfill
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
describe "fill_in_notification_types" do
|
||||
test "it fills in missing notification types" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, post} = CommonAPI.post(user, %{status: "yeah, @#{other_user.nickname}"})
|
||||
{:ok, chat} = CommonAPI.post_chat_message(user, other_user, "yo")
|
||||
{:ok, react} = CommonAPI.react_with_emoji(post.id, other_user, "☕")
|
||||
{:ok, like} = CommonAPI.favorite(other_user, post.id)
|
||||
{:ok, react_2} = CommonAPI.react_with_emoji(post.id, other_user, "☕")
|
||||
|
||||
data =
|
||||
react_2.data
|
||||
|> Map.put("type", "EmojiReaction")
|
||||
|
||||
{:ok, react_2} =
|
||||
react_2
|
||||
|> Activity.change(%{data: data})
|
||||
|> Repo.update()
|
||||
|
||||
assert {5, nil} = Repo.update_all(Notification, set: [type: nil])
|
||||
|
||||
NotificationBackfill.fill_in_notification_types()
|
||||
|
||||
assert %{type: "mention"} =
|
||||
Repo.get_by(Notification, user_id: other_user.id, activity_id: post.id)
|
||||
|
||||
assert %{type: "favourite"} =
|
||||
Repo.get_by(Notification, user_id: user.id, activity_id: like.id)
|
||||
|
||||
assert %{type: "pleroma:emoji_reaction"} =
|
||||
Repo.get_by(Notification, user_id: user.id, activity_id: react.id)
|
||||
|
||||
assert %{type: "pleroma:emoji_reaction"} =
|
||||
Repo.get_by(Notification, user_id: user.id, activity_id: react_2.id)
|
||||
|
||||
assert %{type: "pleroma:chat_mention"} =
|
||||
Repo.get_by(Notification, user_id: other_user.id, activity_id: chat.id)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,6 +10,7 @@ defmodule Pleroma.NotificationTest do
|
|||
|
||||
alias Pleroma.FollowingRelationship
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Tests.ObanHelpers
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
|
@ -31,6 +32,7 @@ test "creates a notification for an emoji reaction" do
|
|||
{:ok, [notification]} = Notification.create_notifications(activity)
|
||||
|
||||
assert notification.user_id == user.id
|
||||
assert notification.type == "pleroma:emoji_reaction"
|
||||
end
|
||||
|
||||
test "notifies someone when they are directly addressed" do
|
||||
|
@ -48,6 +50,7 @@ test "notifies someone when they are directly addressed" do
|
|||
notified_ids = Enum.sort([notification.user_id, other_notification.user_id])
|
||||
assert notified_ids == [other_user.id, third_user.id]
|
||||
assert notification.activity_id == activity.id
|
||||
assert notification.type == "mention"
|
||||
assert other_notification.activity_id == activity.id
|
||||
|
||||
assert [%Pleroma.Marker{unread_count: 2}] =
|
||||
|
@ -335,9 +338,12 @@ test "it creates `follow_request` notification for pending Follow activity" do
|
|||
# After request is accepted, the same notification is rendered with type "follow":
|
||||
assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user)
|
||||
|
||||
notification_id = notification.id
|
||||
assert [%{id: ^notification_id}] = Notification.for_user(followed_user)
|
||||
assert %{type: "follow"} = NotificationView.render("show.json", render_opts)
|
||||
notification =
|
||||
Repo.get(Notification, notification.id)
|
||||
|> Repo.preload(:activity)
|
||||
|
||||
assert %{type: "follow"} =
|
||||
NotificationView.render("show.json", notification: notification, for: followed_user)
|
||||
end
|
||||
|
||||
test "it doesn't create a notification for follow-unfollow-follow chains" do
|
||||
|
|
|
@ -54,6 +54,7 @@ test "it returns file" do
|
|||
%{
|
||||
"name" => "image.jpg",
|
||||
"type" => "Document",
|
||||
"mediaType" => "image/jpeg",
|
||||
"url" => [
|
||||
%{
|
||||
"href" => "http://localhost:4001/media/post-process-file.jpg",
|
||||
|
|
|
@ -2,14 +2,264 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
|
|||
use Pleroma.DataCase
|
||||
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Builder
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
describe "attachments" do
|
||||
test "works with honkerific attachments" do
|
||||
attachment = %{
|
||||
"mediaType" => "",
|
||||
"name" => "",
|
||||
"summary" => "298p3RG7j27tfsZ9RQ.jpg",
|
||||
"type" => "Document",
|
||||
"url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg"
|
||||
}
|
||||
|
||||
assert {:ok, attachment} =
|
||||
AttachmentValidator.cast_and_validate(attachment)
|
||||
|> Ecto.Changeset.apply_action(:insert)
|
||||
|
||||
assert attachment.mediaType == "application/octet-stream"
|
||||
end
|
||||
|
||||
test "it turns mastodon attachments into our attachments" do
|
||||
attachment = %{
|
||||
"url" =>
|
||||
"http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg",
|
||||
"type" => "Document",
|
||||
"name" => nil,
|
||||
"mediaType" => "image/jpeg"
|
||||
}
|
||||
|
||||
{:ok, attachment} =
|
||||
AttachmentValidator.cast_and_validate(attachment)
|
||||
|> Ecto.Changeset.apply_action(:insert)
|
||||
|
||||
assert [
|
||||
%{
|
||||
href:
|
||||
"http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg",
|
||||
type: "Link",
|
||||
mediaType: "image/jpeg"
|
||||
}
|
||||
] = attachment.url
|
||||
|
||||
assert attachment.mediaType == "image/jpeg"
|
||||
end
|
||||
|
||||
test "it handles our own uploads" do
|
||||
user = insert(:user)
|
||||
|
||||
file = %Plug.Upload{
|
||||
content_type: "image/jpg",
|
||||
path: Path.absname("test/fixtures/image.jpg"),
|
||||
filename: "an_image.jpg"
|
||||
}
|
||||
|
||||
{:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
|
||||
|
||||
{:ok, attachment} =
|
||||
attachment.data
|
||||
|> AttachmentValidator.cast_and_validate()
|
||||
|> Ecto.Changeset.apply_action(:insert)
|
||||
|
||||
assert attachment.mediaType == "image/jpeg"
|
||||
end
|
||||
end
|
||||
|
||||
describe "chat message create activities" do
|
||||
test "it is invalid if the object already exists" do
|
||||
user = insert(:user)
|
||||
recipient = insert(:user)
|
||||
{:ok, activity} = CommonAPI.post_chat_message(user, recipient, "hey")
|
||||
object = Object.normalize(activity, false)
|
||||
|
||||
{:ok, create_data, _} = Builder.create(user, object.data, [recipient.ap_id])
|
||||
|
||||
{:error, cng} = ObjectValidator.validate(create_data, [])
|
||||
|
||||
assert {:object, {"The object to create already exists", []}} in cng.errors
|
||||
end
|
||||
|
||||
test "it is invalid if the object data has a different `to` or `actor` field" do
|
||||
user = insert(:user)
|
||||
recipient = insert(:user)
|
||||
{:ok, object_data, _} = Builder.chat_message(recipient, user.ap_id, "Hey")
|
||||
|
||||
{:ok, create_data, _} = Builder.create(user, object_data, [recipient.ap_id])
|
||||
|
||||
{:error, cng} = ObjectValidator.validate(create_data, [])
|
||||
|
||||
assert {:to, {"Recipients don't match with object recipients", []}} in cng.errors
|
||||
assert {:actor, {"Actor doesn't match with object actor", []}} in cng.errors
|
||||
end
|
||||
end
|
||||
|
||||
describe "chat messages" do
|
||||
setup do
|
||||
clear_config([:instance, :remote_limit])
|
||||
user = insert(:user)
|
||||
recipient = insert(:user, local: false)
|
||||
|
||||
{:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey :firefox:")
|
||||
|
||||
%{user: user, recipient: recipient, valid_chat_message: valid_chat_message}
|
||||
end
|
||||
|
||||
test "let's through some basic html", %{user: user, recipient: recipient} do
|
||||
{:ok, valid_chat_message, _} =
|
||||
Builder.chat_message(
|
||||
user,
|
||||
recipient.ap_id,
|
||||
"hey <a href='https://example.org'>example</a> <script>alert('uguu')</script>"
|
||||
)
|
||||
|
||||
assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
|
||||
|
||||
assert object["content"] ==
|
||||
"hey <a href=\"https://example.org\">example</a> alert('uguu')"
|
||||
end
|
||||
|
||||
test "validates for a basic object we build", %{valid_chat_message: valid_chat_message} do
|
||||
assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
|
||||
|
||||
assert Map.put(valid_chat_message, "attachment", nil) == object
|
||||
end
|
||||
|
||||
test "validates for a basic object with an attachment", %{
|
||||
valid_chat_message: valid_chat_message,
|
||||
user: user
|
||||
} do
|
||||
file = %Plug.Upload{
|
||||
content_type: "image/jpg",
|
||||
path: Path.absname("test/fixtures/image.jpg"),
|
||||
filename: "an_image.jpg"
|
||||
}
|
||||
|
||||
{:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
|
||||
|
||||
valid_chat_message =
|
||||
valid_chat_message
|
||||
|> Map.put("attachment", attachment.data)
|
||||
|
||||
assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
|
||||
|
||||
assert object["attachment"]
|
||||
end
|
||||
|
||||
test "validates for a basic object with an attachment in an array", %{
|
||||
valid_chat_message: valid_chat_message,
|
||||
user: user
|
||||
} do
|
||||
file = %Plug.Upload{
|
||||
content_type: "image/jpg",
|
||||
path: Path.absname("test/fixtures/image.jpg"),
|
||||
filename: "an_image.jpg"
|
||||
}
|
||||
|
||||
{:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
|
||||
|
||||
valid_chat_message =
|
||||
valid_chat_message
|
||||
|> Map.put("attachment", [attachment.data])
|
||||
|
||||
assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
|
||||
|
||||
assert object["attachment"]
|
||||
end
|
||||
|
||||
test "validates for a basic object with an attachment but without content", %{
|
||||
valid_chat_message: valid_chat_message,
|
||||
user: user
|
||||
} do
|
||||
file = %Plug.Upload{
|
||||
content_type: "image/jpg",
|
||||
path: Path.absname("test/fixtures/image.jpg"),
|
||||
filename: "an_image.jpg"
|
||||
}
|
||||
|
||||
{:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id)
|
||||
|
||||
valid_chat_message =
|
||||
valid_chat_message
|
||||
|> Map.put("attachment", attachment.data)
|
||||
|> Map.delete("content")
|
||||
|
||||
assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
|
||||
|
||||
assert object["attachment"]
|
||||
end
|
||||
|
||||
test "does not validate if the message has no content", %{
|
||||
valid_chat_message: valid_chat_message
|
||||
} do
|
||||
contentless =
|
||||
valid_chat_message
|
||||
|> Map.delete("content")
|
||||
|
||||
refute match?({:ok, _object, _meta}, ObjectValidator.validate(contentless, []))
|
||||
end
|
||||
|
||||
test "does not validate if the message is longer than the remote_limit", %{
|
||||
valid_chat_message: valid_chat_message
|
||||
} do
|
||||
Pleroma.Config.put([:instance, :remote_limit], 2)
|
||||
refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, []))
|
||||
end
|
||||
|
||||
test "does not validate if the recipient is blocking the actor", %{
|
||||
valid_chat_message: valid_chat_message,
|
||||
user: user,
|
||||
recipient: recipient
|
||||
} do
|
||||
Pleroma.User.block(recipient, user)
|
||||
refute match?({:ok, _object, _meta}, ObjectValidator.validate(valid_chat_message, []))
|
||||
end
|
||||
|
||||
test "does not validate if the actor or the recipient is not in our system", %{
|
||||
valid_chat_message: valid_chat_message
|
||||
} do
|
||||
chat_message =
|
||||
valid_chat_message
|
||||
|> Map.put("actor", "https://raymoo.com/raymoo")
|
||||
|
||||
{:error, _} = ObjectValidator.validate(chat_message, [])
|
||||
|
||||
chat_message =
|
||||
valid_chat_message
|
||||
|> Map.put("to", ["https://raymoo.com/raymoo"])
|
||||
|
||||
{:error, _} = ObjectValidator.validate(chat_message, [])
|
||||
end
|
||||
|
||||
test "does not validate for a message with multiple recipients", %{
|
||||
valid_chat_message: valid_chat_message,
|
||||
user: user,
|
||||
recipient: recipient
|
||||
} do
|
||||
chat_message =
|
||||
valid_chat_message
|
||||
|> Map.put("to", [user.ap_id, recipient.ap_id])
|
||||
|
||||
assert {:error, _} = ObjectValidator.validate(chat_message, [])
|
||||
end
|
||||
|
||||
test "does not validate if it doesn't concern local users" do
|
||||
user = insert(:user, local: false)
|
||||
recipient = insert(:user, local: false)
|
||||
|
||||
{:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "hey")
|
||||
assert {:error, _} = ObjectValidator.validate(valid_chat_message, [])
|
||||
end
|
||||
end
|
||||
|
||||
describe "EmojiReacts" do
|
||||
setup do
|
||||
user = insert(:user)
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ObjectValidators.Types.ObjectIDTest do
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID
|
||||
use Pleroma.DataCase
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.SafeTextTest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.Types.SafeText
|
||||
|
||||
test "it lets normal text go through" do
|
||||
text = "hey how are you"
|
||||
assert {:ok, text} == SafeText.cast(text)
|
||||
end
|
||||
|
||||
test "it removes html tags from text" do
|
||||
text = "hey look xss <script>alert('foo')</script>"
|
||||
assert {:ok, "hey look xss alert('foo')"} == SafeText.cast(text)
|
||||
end
|
||||
|
||||
test "it keeps basic html tags" do
|
||||
text = "hey <a href='http://gensokyo.2hu'>look</a> xss <script>alert('foo')</script>"
|
||||
|
||||
assert {:ok, "hey <a href=\"http://gensokyo.2hu\">look</a> xss alert('foo')"} ==
|
||||
SafeText.cast(text)
|
||||
end
|
||||
|
||||
test "errors for non-text" do
|
||||
assert :error == SafeText.cast(1)
|
||||
end
|
||||
end
|
|
@ -33,7 +33,10 @@ test "it goes through validation, filtering, persisting, side effects and federa
|
|||
{
|
||||
Pleroma.Web.ActivityPub.SideEffects,
|
||||
[],
|
||||
[handle: fn o, m -> {:ok, o, m} end]
|
||||
[
|
||||
handle: fn o, m -> {:ok, o, m} end,
|
||||
handle_after_transaction: fn m -> m end
|
||||
]
|
||||
},
|
||||
{
|
||||
Pleroma.Web.Federator,
|
||||
|
@ -71,7 +74,7 @@ test "it goes through validation, filtering, persisting, side effects without fe
|
|||
{
|
||||
Pleroma.Web.ActivityPub.SideEffects,
|
||||
[],
|
||||
[handle: fn o, m -> {:ok, o, m} end]
|
||||
[handle: fn o, m -> {:ok, o, m} end, handle_after_transaction: fn m -> m end]
|
||||
},
|
||||
{
|
||||
Pleroma.Web.Federator,
|
||||
|
@ -110,7 +113,7 @@ test "it goes through validation, filtering, persisting, side effects without fe
|
|||
{
|
||||
Pleroma.Web.ActivityPub.SideEffects,
|
||||
[],
|
||||
[handle: fn o, m -> {:ok, o, m} end]
|
||||
[handle: fn o, m -> {:ok, o, m} end, handle_after_transaction: fn m -> m end]
|
||||
},
|
||||
{
|
||||
Pleroma.Web.Federator,
|
||||
|
|
|
@ -7,6 +7,8 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
|
|||
use Pleroma.DataCase
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Chat
|
||||
alias Pleroma.Chat.MessageReference
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
|
@ -20,6 +22,48 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
|
|||
import Pleroma.Factory
|
||||
import Mock
|
||||
|
||||
describe "handle_after_transaction" do
|
||||
test "it streams out notifications and streams" do
|
||||
author = insert(:user, local: true)
|
||||
recipient = insert(:user, local: true)
|
||||
|
||||
{:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
|
||||
|
||||
{:ok, create_activity_data, _meta} =
|
||||
Builder.create(author, chat_message_data["id"], [recipient.ap_id])
|
||||
|
||||
{:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
|
||||
|
||||
{:ok, _create_activity, meta} =
|
||||
SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
|
||||
|
||||
assert [notification] = meta[:notifications]
|
||||
|
||||
with_mocks([
|
||||
{
|
||||
Pleroma.Web.Streamer,
|
||||
[],
|
||||
[
|
||||
stream: fn _, _ -> nil end
|
||||
]
|
||||
},
|
||||
{
|
||||
Pleroma.Web.Push,
|
||||
[],
|
||||
[
|
||||
send: fn _ -> nil end
|
||||
]
|
||||
}
|
||||
]) do
|
||||
SideEffects.handle_after_transaction(meta)
|
||||
|
||||
assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification))
|
||||
assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_))
|
||||
assert called(Pleroma.Web.Push.send(notification))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete objects" do
|
||||
setup do
|
||||
user = insert(:user)
|
||||
|
@ -290,6 +334,147 @@ 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)
|
||||
|
|
153
test/web/activity_pub/transmogrifier/chat_message_test.exs
Normal file
153
test/web/activity_pub/transmogrifier/chat_message_test.exs
Normal file
|
@ -0,0 +1,153 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.Transmogrifier.ChatMessageTest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Chat
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
|
||||
describe "handle_incoming" do
|
||||
test "handles chonks with attachment" do
|
||||
data = %{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"actor" => "https://honk.tedunangst.com/u/tedu",
|
||||
"id" => "https://honk.tedunangst.com/u/tedu/honk/x6gt8X8PcyGkQcXxzg1T",
|
||||
"object" => %{
|
||||
"attachment" => [
|
||||
%{
|
||||
"mediaType" => "image/jpeg",
|
||||
"name" => "298p3RG7j27tfsZ9RQ.jpg",
|
||||
"summary" => "298p3RG7j27tfsZ9RQ.jpg",
|
||||
"type" => "Document",
|
||||
"url" => "https://honk.tedunangst.com/d/298p3RG7j27tfsZ9RQ.jpg"
|
||||
}
|
||||
],
|
||||
"attributedTo" => "https://honk.tedunangst.com/u/tedu",
|
||||
"content" => "",
|
||||
"id" => "https://honk.tedunangst.com/u/tedu/chonk/26L4wl5yCbn4dr4y1b",
|
||||
"published" => "2020-05-18T01:13:03Z",
|
||||
"to" => [
|
||||
"https://dontbulling.me/users/lain"
|
||||
],
|
||||
"type" => "ChatMessage"
|
||||
},
|
||||
"published" => "2020-05-18T01:13:03Z",
|
||||
"to" => [
|
||||
"https://dontbulling.me/users/lain"
|
||||
],
|
||||
"type" => "Create"
|
||||
}
|
||||
|
||||
_user = insert(:user, ap_id: data["actor"])
|
||||
_user = insert(:user, ap_id: hd(data["to"]))
|
||||
|
||||
assert {:ok, _activity} = Transmogrifier.handle_incoming(data)
|
||||
end
|
||||
|
||||
test "it rejects messages that don't contain content" do
|
||||
data =
|
||||
File.read!("test/fixtures/create-chat-message.json")
|
||||
|> Poison.decode!()
|
||||
|
||||
object =
|
||||
data["object"]
|
||||
|> Map.delete("content")
|
||||
|
||||
data =
|
||||
data
|
||||
|> Map.put("object", object)
|
||||
|
||||
_author =
|
||||
insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now())
|
||||
|
||||
_recipient =
|
||||
insert(:user,
|
||||
ap_id: List.first(data["to"]),
|
||||
local: true,
|
||||
last_refreshed_at: DateTime.utc_now()
|
||||
)
|
||||
|
||||
{:error, _} = Transmogrifier.handle_incoming(data)
|
||||
end
|
||||
|
||||
test "it rejects messages that don't concern local users" do
|
||||
data =
|
||||
File.read!("test/fixtures/create-chat-message.json")
|
||||
|> Poison.decode!()
|
||||
|
||||
_author =
|
||||
insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now())
|
||||
|
||||
_recipient =
|
||||
insert(:user,
|
||||
ap_id: List.first(data["to"]),
|
||||
local: false,
|
||||
last_refreshed_at: DateTime.utc_now()
|
||||
)
|
||||
|
||||
{:error, _} = Transmogrifier.handle_incoming(data)
|
||||
end
|
||||
|
||||
test "it rejects messages where the `to` field of activity and object don't match" do
|
||||
data =
|
||||
File.read!("test/fixtures/create-chat-message.json")
|
||||
|> Poison.decode!()
|
||||
|
||||
author = insert(:user, ap_id: data["actor"])
|
||||
_recipient = insert(:user, ap_id: List.first(data["to"]))
|
||||
|
||||
data =
|
||||
data
|
||||
|> Map.put("to", author.ap_id)
|
||||
|
||||
assert match?({:error, _}, Transmogrifier.handle_incoming(data))
|
||||
refute Object.get_by_ap_id(data["object"]["id"])
|
||||
end
|
||||
|
||||
test "it fetches the actor if they aren't in our system" do
|
||||
Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||
|
||||
data =
|
||||
File.read!("test/fixtures/create-chat-message.json")
|
||||
|> Poison.decode!()
|
||||
|> Map.put("actor", "http://mastodon.example.org/users/admin")
|
||||
|> put_in(["object", "actor"], "http://mastodon.example.org/users/admin")
|
||||
|
||||
_recipient = insert(:user, ap_id: List.first(data["to"]), local: true)
|
||||
|
||||
{:ok, %Activity{} = _activity} = Transmogrifier.handle_incoming(data)
|
||||
end
|
||||
|
||||
test "it inserts it and creates a chat" do
|
||||
data =
|
||||
File.read!("test/fixtures/create-chat-message.json")
|
||||
|> Poison.decode!()
|
||||
|
||||
author =
|
||||
insert(:user, ap_id: data["actor"], local: false, last_refreshed_at: DateTime.utc_now())
|
||||
|
||||
recipient = insert(:user, ap_id: List.first(data["to"]), local: true)
|
||||
|
||||
{:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(data)
|
||||
assert activity.local == false
|
||||
|
||||
assert activity.actor == author.ap_id
|
||||
assert activity.recipients == [recipient.ap_id, author.ap_id]
|
||||
|
||||
%Object{} = object = Object.get_by_ap_id(activity.data["object"])
|
||||
|
||||
assert object
|
||||
assert object.data["content"] == "You expected a cute girl? Too bad. alert('XSS')"
|
||||
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
|
|
@ -5,6 +5,7 @@
|
|||
defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
|
||||
use Pleroma.DataCase
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
|
@ -12,6 +13,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
|
|||
|
||||
import Pleroma.Factory
|
||||
import Ecto.Query
|
||||
import Mock
|
||||
|
||||
setup_all do
|
||||
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||
|
@ -57,9 +59,12 @@ test "it works for incoming follow requests" do
|
|||
activity = Repo.get(Activity, activity.id)
|
||||
assert activity.data["state"] == "accept"
|
||||
assert User.following?(User.get_cached_by_ap_id(data["actor"]), user)
|
||||
|
||||
[notification] = Notification.for_user(user)
|
||||
assert notification.type == "follow"
|
||||
end
|
||||
|
||||
test "with locked accounts, it does not create a follow or an accept" do
|
||||
test "with locked accounts, it does create a Follow, but not an Accept" do
|
||||
user = insert(:user, locked: true)
|
||||
|
||||
data =
|
||||
|
@ -81,6 +86,9 @@ test "with locked accounts, it does not create a follow or an accept" do
|
|||
|> Repo.all()
|
||||
|
||||
assert Enum.empty?(accepts)
|
||||
|
||||
[notification] = Notification.for_user(user)
|
||||
assert notification.type == "follow_request"
|
||||
end
|
||||
|
||||
test "it works for follow requests when you are already followed, creating a new accept activity" do
|
||||
|
@ -144,6 +152,23 @@ test "it rejects incoming follow requests from blocked users when deny_follow_bl
|
|||
assert activity.data["state"] == "reject"
|
||||
end
|
||||
|
||||
test "it rejects incoming follow requests if the following errors for some reason" do
|
||||
user = insert(:user)
|
||||
|
||||
data =
|
||||
File.read!("test/fixtures/mastodon-follow-activity.json")
|
||||
|> Poison.decode!()
|
||||
|> Map.put("object", user.ap_id)
|
||||
|
||||
with_mock Pleroma.User, [:passthrough], follow: fn _, _ -> {:error, :testing} end do
|
||||
{:ok, %Activity{data: %{"id" => id}}} = Transmogrifier.handle_incoming(data)
|
||||
|
||||
%Activity{} = activity = Activity.get_by_ap_id(id)
|
||||
|
||||
assert activity.data["state"] == "reject"
|
||||
end
|
||||
end
|
||||
|
||||
test "it works for incoming follow requests from hubzilla" do
|
||||
user = insert(:user)
|
||||
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
defmodule Pleroma.Web.CommonAPITest do
|
||||
use Pleroma.DataCase
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Chat
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
|
@ -23,6 +25,150 @@ defmodule Pleroma.Web.CommonAPITest do
|
|||
setup do: clear_config([:instance, :limit])
|
||||
setup do: clear_config([:instance, :max_pinned_statuses])
|
||||
|
||||
describe "posting chat messages" do
|
||||
setup do: clear_config([:instance, :chat_limit])
|
||||
|
||||
test "it posts a chat message without content but with an attachment" do
|
||||
author = insert(:user)
|
||||
recipient = insert(:user)
|
||||
|
||||
file = %Plug.Upload{
|
||||
content_type: "image/jpg",
|
||||
path: Path.absname("test/fixtures/image.jpg"),
|
||||
filename: "an_image.jpg"
|
||||
}
|
||||
|
||||
{:ok, upload} = ActivityPub.upload(file, actor: author.ap_id)
|
||||
|
||||
with_mocks([
|
||||
{
|
||||
Pleroma.Web.Streamer,
|
||||
[],
|
||||
[
|
||||
stream: fn _, _ ->
|
||||
nil
|
||||
end
|
||||
]
|
||||
},
|
||||
{
|
||||
Pleroma.Web.Push,
|
||||
[],
|
||||
[
|
||||
send: fn _ -> nil end
|
||||
]
|
||||
}
|
||||
]) do
|
||||
{:ok, activity} =
|
||||
CommonAPI.post_chat_message(
|
||||
author,
|
||||
recipient,
|
||||
nil,
|
||||
media_id: upload.id
|
||||
)
|
||||
|
||||
notification =
|
||||
Notification.for_user_and_activity(recipient, activity)
|
||||
|> Repo.preload(:activity)
|
||||
|
||||
assert called(Pleroma.Web.Push.send(notification))
|
||||
assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification))
|
||||
assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_))
|
||||
|
||||
assert activity
|
||||
end
|
||||
end
|
||||
|
||||
test "it adds html newlines" do
|
||||
author = insert(:user)
|
||||
recipient = insert(:user)
|
||||
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post_chat_message(
|
||||
author,
|
||||
recipient,
|
||||
"uguu\nuguuu"
|
||||
)
|
||||
|
||||
assert other_user.ap_id not in activity.recipients
|
||||
|
||||
object = Object.normalize(activity, false)
|
||||
|
||||
assert object.data["content"] == "uguu<br/>uguuu"
|
||||
end
|
||||
|
||||
test "it linkifies" do
|
||||
author = insert(:user)
|
||||
recipient = insert(:user)
|
||||
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post_chat_message(
|
||||
author,
|
||||
recipient,
|
||||
"https://example.org is the site of @#{other_user.nickname} #2hu"
|
||||
)
|
||||
|
||||
assert other_user.ap_id not in activity.recipients
|
||||
|
||||
object = Object.normalize(activity, false)
|
||||
|
||||
assert object.data["content"] ==
|
||||
"<a href=\"https://example.org\" rel=\"ugc\">https://example.org</a> is the site of <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{
|
||||
other_user.id
|
||||
}\" href=\"#{other_user.ap_id}\" rel=\"ugc\">@<span>#{other_user.nickname}</span></a></span> <a class=\"hashtag\" data-tag=\"2hu\" href=\"http://localhost:4001/tag/2hu\">#2hu</a>"
|
||||
end
|
||||
|
||||
test "it posts a chat message" do
|
||||
author = insert(:user)
|
||||
recipient = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post_chat_message(
|
||||
author,
|
||||
recipient,
|
||||
"a test message <script>alert('uuu')</script> :firefox:"
|
||||
)
|
||||
|
||||
assert activity.data["type"] == "Create"
|
||||
assert activity.local
|
||||
object = Object.normalize(activity)
|
||||
|
||||
assert object.data["type"] == "ChatMessage"
|
||||
assert object.data["to"] == [recipient.ap_id]
|
||||
|
||||
assert object.data["content"] ==
|
||||
"a test message <script>alert('uuu')</script> :firefox:"
|
||||
|
||||
assert object.data["emoji"] == %{
|
||||
"firefox" => "http://localhost:4001/emoji/Firefox.gif"
|
||||
}
|
||||
|
||||
assert Chat.get(author.id, recipient.ap_id)
|
||||
assert Chat.get(recipient.id, author.ap_id)
|
||||
|
||||
assert :ok == Pleroma.Web.Federator.perform(:publish, activity)
|
||||
end
|
||||
|
||||
test "it reject messages over the local limit" do
|
||||
Pleroma.Config.put([:instance, :chat_limit], 2)
|
||||
|
||||
author = insert(:user)
|
||||
recipient = insert(:user)
|
||||
|
||||
{:error, message} =
|
||||
CommonAPI.post_chat_message(
|
||||
author,
|
||||
recipient,
|
||||
"123"
|
||||
)
|
||||
|
||||
assert message == :content_too_long
|
||||
end
|
||||
end
|
||||
|
||||
describe "unblocking" do
|
||||
test "it works even without an existing block activity" do
|
||||
blocked = insert(:user)
|
||||
|
|
|
@ -54,6 +54,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 "getting a single notification" do
|
||||
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||
other_user = insert(:user)
|
||||
|
|
|
@ -111,6 +111,15 @@ test "constructs hashtags from search query", %{conn: conn} do
|
|||
%{"name" => "prone", "url" => "#{Web.base_url()}/tag/prone"},
|
||||
%{"name" => "AccidentProne", "url" => "#{Web.base_url()}/tag/AccidentProne"}
|
||||
]
|
||||
|
||||
results =
|
||||
conn
|
||||
|> get("/api/v2/search?#{URI.encode_query(%{q: "https://shpposter.club/users/shpuld"})}")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert results["hashtags"] == [
|
||||
%{"name" => "shpuld", "url" => "#{Web.base_url()}/tag/shpuld"}
|
||||
]
|
||||
end
|
||||
|
||||
test "excludes a blocked users from search results", %{conn: conn} do
|
||||
|
|
|
@ -58,7 +58,9 @@ test "successful creation", %{conn: conn} do
|
|||
result =
|
||||
conn
|
||||
|> post("/api/v1/push/subscription", %{
|
||||
"data" => %{"alerts" => %{"mention" => true, "test" => true}},
|
||||
"data" => %{
|
||||
"alerts" => %{"mention" => true, "test" => true, "pleroma:chat_mention" => true}
|
||||
},
|
||||
"subscription" => @sub
|
||||
})
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
@ -66,7 +68,7 @@ test "successful creation", %{conn: conn} do
|
|||
[subscription] = Pleroma.Repo.all(Subscription)
|
||||
|
||||
assert %{
|
||||
"alerts" => %{"mention" => true},
|
||||
"alerts" => %{"mention" => true, "pleroma:chat_mention" => true},
|
||||
"endpoint" => subscription.endpoint,
|
||||
"id" => to_string(subscription.id),
|
||||
"server_key" => @server_key
|
||||
|
|
|
@ -72,6 +72,7 @@ test "Represent a user account" do
|
|||
fields: []
|
||||
},
|
||||
pleroma: %{
|
||||
ap_id: user.ap_id,
|
||||
background_image: "https://example.com/images/asuka_hospital.png",
|
||||
confirmation_pending: false,
|
||||
tags: [],
|
||||
|
@ -148,6 +149,7 @@ test "Represent a Service(bot) account" do
|
|||
fields: []
|
||||
},
|
||||
pleroma: %{
|
||||
ap_id: user.ap_id,
|
||||
background_image: nil,
|
||||
confirmation_pending: false,
|
||||
tags: [],
|
||||
|
|
|
@ -6,7 +6,10 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
|
|||
use Pleroma.DataCase
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Chat
|
||||
alias Pleroma.Chat.MessageReference
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
@ -14,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
|
|||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
alias Pleroma.Web.MastodonAPI.NotificationView
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
|
||||
import Pleroma.Factory
|
||||
|
||||
defp test_notifications_rendering(notifications, user, expected_result) do
|
||||
|
@ -31,6 +35,30 @@ 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)
|
||||
chat = Chat.get(recipient.id, user.ap_id)
|
||||
|
||||
cm_ref = MessageReference.for_chat_and_object(chat, object)
|
||||
|
||||
expected = %{
|
||||
id: to_string(notification.id),
|
||||
pleroma: %{is_seen: false},
|
||||
type: "pleroma:chat_mention",
|
||||
account: AccountView.render("show.json", %{user: user, for: recipient}),
|
||||
chat_message: MessageReferenceView.render("show.json", %{chat_message_reference: cm_ref}),
|
||||
created_at: Utils.to_masto_date(notification.inserted_at)
|
||||
}
|
||||
|
||||
test_notifications_rendering([notification], recipient, [expected])
|
||||
end
|
||||
|
||||
test "Mention notification" do
|
||||
user = insert(:user)
|
||||
mentioned_user = insert(:user)
|
||||
|
|
|
@ -145,7 +145,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?(
|
||||
|
|
336
test/web/pleroma_api/controllers/chat_controller_test.exs
Normal file
336
test/web/pleroma_api/controllers/chat_controller_test.exs
Normal file
|
@ -0,0 +1,336 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do
|
||||
use Pleroma.Web.ConnCase, async: true
|
||||
|
||||
alias Pleroma.Chat
|
||||
alias Pleroma.Chat.MessageReference
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
describe "POST /api/v1/pleroma/chats/:id/messages/:message_id/read" do
|
||||
setup do: oauth_access(["write:chats"])
|
||||
|
||||
test "it marks one message as read", %{conn: conn, user: user} do
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup")
|
||||
{:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2")
|
||||
{:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
|
||||
object = Object.normalize(create, false)
|
||||
cm_ref = MessageReference.for_chat_and_object(chat, object)
|
||||
|
||||
assert cm_ref.unread == true
|
||||
|
||||
result =
|
||||
conn
|
||||
|> post("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}/read")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert result["unread"] == false
|
||||
|
||||
cm_ref = MessageReference.for_chat_and_object(chat, object)
|
||||
|
||||
assert cm_ref.unread == false
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /api/v1/pleroma/chats/:id/read" do
|
||||
setup do: oauth_access(["write:chats"])
|
||||
|
||||
test "given a `last_read_id`, it marks everything until then as read", %{
|
||||
conn: conn,
|
||||
user: user
|
||||
} do
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, create} = CommonAPI.post_chat_message(other_user, user, "sup")
|
||||
{:ok, _create} = CommonAPI.post_chat_message(other_user, user, "sup part 2")
|
||||
{:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
|
||||
object = Object.normalize(create, false)
|
||||
cm_ref = MessageReference.for_chat_and_object(chat, object)
|
||||
|
||||
assert cm_ref.unread == true
|
||||
|
||||
result =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/v1/pleroma/chats/#{chat.id}/read", %{"last_read_id" => cm_ref.id})
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert result["unread"] == 1
|
||||
|
||||
cm_ref = MessageReference.for_chat_and_object(chat, object)
|
||||
|
||||
assert cm_ref.unread == false
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /api/v1/pleroma/chats/:id/messages" do
|
||||
setup do: oauth_access(["write:chats"])
|
||||
|
||||
test "it posts a message to the chat", %{conn: conn, user: user} do
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
|
||||
|
||||
result =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{"content" => "Hallo!!"})
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert result["content"] == "Hallo!!"
|
||||
assert result["chat_id"] == chat.id |> to_string()
|
||||
end
|
||||
|
||||
test "it fails if there is no content", %{conn: conn, user: user} do
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
|
||||
|
||||
result =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/v1/pleroma/chats/#{chat.id}/messages")
|
||||
|> json_response_and_validate_schema(400)
|
||||
|
||||
assert result
|
||||
end
|
||||
|
||||
test "it works with an attachment", %{conn: conn, user: user} do
|
||||
file = %Plug.Upload{
|
||||
content_type: "image/jpg",
|
||||
path: Path.absname("test/fixtures/image.jpg"),
|
||||
filename: "an_image.jpg"
|
||||
}
|
||||
|
||||
{:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
|
||||
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
|
||||
|
||||
result =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/v1/pleroma/chats/#{chat.id}/messages", %{
|
||||
"media_id" => to_string(upload.id)
|
||||
})
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert result["attachment"]
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE /api/v1/pleroma/chats/:id/messages/:message_id" do
|
||||
setup do: oauth_access(["write:chats"])
|
||||
|
||||
test "it deletes a message from the chat", %{conn: conn, user: user} do
|
||||
recipient = insert(:user)
|
||||
|
||||
{:ok, message} =
|
||||
CommonAPI.post_chat_message(user, recipient, "Hello darkness my old friend")
|
||||
|
||||
{:ok, other_message} = CommonAPI.post_chat_message(recipient, user, "nico nico ni")
|
||||
|
||||
object = Object.normalize(message, false)
|
||||
|
||||
chat = Chat.get(user.id, recipient.ap_id)
|
||||
|
||||
cm_ref = MessageReference.for_chat_and_object(chat, object)
|
||||
|
||||
# Deleting your own message removes the message and the reference
|
||||
result =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert result["id"] == cm_ref.id
|
||||
refute MessageReference.get_by_id(cm_ref.id)
|
||||
assert %{data: %{"type" => "Tombstone"}} = Object.get_by_id(object.id)
|
||||
|
||||
# Deleting other people's messages just removes the reference
|
||||
object = Object.normalize(other_message, false)
|
||||
cm_ref = MessageReference.for_chat_and_object(chat, object)
|
||||
|
||||
result =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> delete("/api/v1/pleroma/chats/#{chat.id}/messages/#{cm_ref.id}")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert result["id"] == cm_ref.id
|
||||
refute MessageReference.get_by_id(cm_ref.id)
|
||||
assert Object.get_by_id(object.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/v1/pleroma/chats/:id/messages" do
|
||||
setup do: oauth_access(["read:chats"])
|
||||
|
||||
test "it paginates", %{conn: conn, user: user} do
|
||||
recipient = insert(:user)
|
||||
|
||||
Enum.each(1..30, fn _ ->
|
||||
{:ok, _} = CommonAPI.post_chat_message(user, recipient, "hey")
|
||||
end)
|
||||
|
||||
chat = Chat.get(user.id, recipient.ap_id)
|
||||
|
||||
result =
|
||||
conn
|
||||
|> get("/api/v1/pleroma/chats/#{chat.id}/messages")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert length(result) == 20
|
||||
|
||||
result =
|
||||
conn
|
||||
|> get("/api/v1/pleroma/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert length(result) == 10
|
||||
end
|
||||
|
||||
test "it returns the messages for a given chat", %{conn: conn, user: user} do
|
||||
other_user = insert(:user)
|
||||
third_user = insert(:user)
|
||||
|
||||
{:ok, _} = CommonAPI.post_chat_message(user, other_user, "hey")
|
||||
{:ok, _} = CommonAPI.post_chat_message(user, third_user, "hey")
|
||||
{:ok, _} = CommonAPI.post_chat_message(user, other_user, "how are you?")
|
||||
{:ok, _} = CommonAPI.post_chat_message(other_user, user, "fine, how about you?")
|
||||
|
||||
chat = Chat.get(user.id, other_user.ap_id)
|
||||
|
||||
result =
|
||||
conn
|
||||
|> get("/api/v1/pleroma/chats/#{chat.id}/messages")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
result
|
||||
|> Enum.each(fn message ->
|
||||
assert message["chat_id"] == chat.id |> to_string()
|
||||
end)
|
||||
|
||||
assert length(result) == 3
|
||||
|
||||
# Trying to get the chat of a different user
|
||||
result =
|
||||
conn
|
||||
|> assign(:user, other_user)
|
||||
|> get("/api/v1/pleroma/chats/#{chat.id}/messages")
|
||||
|
||||
assert result |> json_response(404)
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /api/v1/pleroma/chats/by-account-id/:id" do
|
||||
setup do: oauth_access(["write:chats"])
|
||||
|
||||
test "it creates or returns a chat", %{conn: conn} do
|
||||
other_user = insert(:user)
|
||||
|
||||
result =
|
||||
conn
|
||||
|> post("/api/v1/pleroma/chats/by-account-id/#{other_user.id}")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert result["id"]
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/v1/pleroma/chats/:id" do
|
||||
setup do: oauth_access(["read:chats"])
|
||||
|
||||
test "it returns a chat", %{conn: conn, user: user} do
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, chat} = Chat.get_or_create(user.id, other_user.ap_id)
|
||||
|
||||
result =
|
||||
conn
|
||||
|> get("/api/v1/pleroma/chats/#{chat.id}")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert result["id"] == to_string(chat.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/v1/pleroma/chats" do
|
||||
setup do: oauth_access(["read:chats"])
|
||||
|
||||
test "it does not return chats with users you blocked", %{conn: conn, user: user} do
|
||||
recipient = insert(:user)
|
||||
|
||||
{:ok, _} = Chat.get_or_create(user.id, recipient.ap_id)
|
||||
|
||||
result =
|
||||
conn
|
||||
|> get("/api/v1/pleroma/chats")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert length(result) == 1
|
||||
|
||||
User.block(user, recipient)
|
||||
|
||||
result =
|
||||
conn
|
||||
|> get("/api/v1/pleroma/chats")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert length(result) == 0
|
||||
end
|
||||
|
||||
test "it returns all chats", %{conn: conn, user: user} do
|
||||
Enum.each(1..30, fn _ ->
|
||||
recipient = insert(:user)
|
||||
{:ok, _} = Chat.get_or_create(user.id, recipient.ap_id)
|
||||
end)
|
||||
|
||||
result =
|
||||
conn
|
||||
|> get("/api/v1/pleroma/chats")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert length(result) == 30
|
||||
end
|
||||
|
||||
test "it return a list of chats the current user is participating in, in descending order of updates",
|
||||
%{conn: conn, user: user} do
|
||||
har = insert(:user)
|
||||
jafnhar = insert(:user)
|
||||
tridi = insert(:user)
|
||||
|
||||
{:ok, chat_1} = Chat.get_or_create(user.id, har.ap_id)
|
||||
:timer.sleep(1000)
|
||||
{:ok, _chat_2} = Chat.get_or_create(user.id, jafnhar.ap_id)
|
||||
:timer.sleep(1000)
|
||||
{:ok, chat_3} = Chat.get_or_create(user.id, tridi.ap_id)
|
||||
:timer.sleep(1000)
|
||||
|
||||
# bump the second one
|
||||
{:ok, chat_2} = Chat.bump_or_create(user.id, jafnhar.ap_id)
|
||||
|
||||
result =
|
||||
conn
|
||||
|> get("/api/v1/pleroma/chats")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
ids = Enum.map(result, & &1["id"])
|
||||
|
||||
assert ids == [
|
||||
chat_2.id |> to_string(),
|
||||
chat_3.id |> to_string(),
|
||||
chat_1.id |> to_string()
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,61 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceViewTest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
alias Pleroma.Chat
|
||||
alias Pleroma.Chat.MessageReference
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
test "it displays a chat message" do
|
||||
user = insert(:user)
|
||||
recipient = insert(:user)
|
||||
|
||||
file = %Plug.Upload{
|
||||
content_type: "image/jpg",
|
||||
path: Path.absname("test/fixtures/image.jpg"),
|
||||
filename: "an_image.jpg"
|
||||
}
|
||||
|
||||
{:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
|
||||
{:ok, activity} = CommonAPI.post_chat_message(user, recipient, "kippis :firefox:")
|
||||
|
||||
chat = Chat.get(user.id, recipient.ap_id)
|
||||
|
||||
object = Object.normalize(activity)
|
||||
|
||||
cm_ref = MessageReference.for_chat_and_object(chat, object)
|
||||
|
||||
chat_message = MessageReferenceView.render("show.json", chat_message_reference: cm_ref)
|
||||
|
||||
assert chat_message[:id] == cm_ref.id
|
||||
assert chat_message[:content] == "kippis :firefox:"
|
||||
assert chat_message[:account_id] == user.id
|
||||
assert chat_message[:chat_id]
|
||||
assert chat_message[:created_at]
|
||||
assert chat_message[:unread] == false
|
||||
assert match?([%{shortcode: "firefox"}], chat_message[:emojis])
|
||||
|
||||
{:ok, activity} = CommonAPI.post_chat_message(recipient, user, "gkgkgk", media_id: upload.id)
|
||||
|
||||
object = Object.normalize(activity)
|
||||
|
||||
cm_ref = MessageReference.for_chat_and_object(chat, object)
|
||||
|
||||
chat_message_two = MessageReferenceView.render("show.json", chat_message_reference: cm_ref)
|
||||
|
||||
assert chat_message_two[:id] == cm_ref.id
|
||||
assert chat_message_two[:content] == "gkgkgk"
|
||||
assert chat_message_two[:account_id] == recipient.id
|
||||
assert chat_message_two[:chat_id] == chat_message[:chat_id]
|
||||
assert chat_message_two[:attachment]
|
||||
assert chat_message_two[:unread] == true
|
||||
end
|
||||
end
|
48
test/web/pleroma_api/views/chat_view_test.exs
Normal file
48
test/web/pleroma_api/views/chat_view_test.exs
Normal file
|
@ -0,0 +1,48 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.PleromaAPI.ChatViewTest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
alias Pleroma.Chat
|
||||
alias Pleroma.Chat.MessageReference
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
|
||||
alias Pleroma.Web.PleromaAPI.ChatView
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
test "it represents a chat" do
|
||||
user = insert(:user)
|
||||
recipient = insert(:user)
|
||||
|
||||
{:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id)
|
||||
|
||||
represented_chat = ChatView.render("show.json", chat: chat)
|
||||
|
||||
assert represented_chat == %{
|
||||
id: "#{chat.id}",
|
||||
account: AccountView.render("show.json", user: recipient),
|
||||
unread: 0,
|
||||
last_message: nil,
|
||||
updated_at: Utils.to_masto_date(chat.updated_at)
|
||||
}
|
||||
|
||||
{:ok, chat_message_creation} = CommonAPI.post_chat_message(user, recipient, "hello")
|
||||
|
||||
chat_message = Object.normalize(chat_message_creation, false)
|
||||
|
||||
{:ok, chat} = Chat.get_or_create(user.id, recipient.ap_id)
|
||||
|
||||
represented_chat = ChatView.render("show.json", chat: chat)
|
||||
|
||||
cm_ref = MessageReference.for_chat_and_object(chat, chat_message)
|
||||
|
||||
assert represented_chat[:last_message] ==
|
||||
MessageReferenceView.render("show.json", chat_message_reference: cm_ref)
|
||||
end
|
||||
end
|
|
@ -5,8 +5,10 @@
|
|||
defmodule Pleroma.Web.Push.ImplTest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.Push.Impl
|
||||
alias Pleroma.Web.Push.Subscription
|
||||
|
@ -60,7 +62,8 @@ test "performs sending notifications" do
|
|||
notif =
|
||||
insert(:notification,
|
||||
user: user,
|
||||
activity: activity
|
||||
activity: activity,
|
||||
type: "mention"
|
||||
)
|
||||
|
||||
assert Impl.perform(notif) == {:ok, [:ok, :ok]}
|
||||
|
@ -126,7 +129,7 @@ test "renders title and body for create activity" do
|
|||
) ==
|
||||
"@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..."
|
||||
|
||||
assert Impl.format_title(%{activity: activity}) ==
|
||||
assert Impl.format_title(%{activity: activity, type: "mention"}) ==
|
||||
"New Mention"
|
||||
end
|
||||
|
||||
|
@ -136,9 +139,10 @@ test "renders title and body for follow activity" do
|
|||
{:ok, _, _, activity} = CommonAPI.follow(user, other_user)
|
||||
object = Object.normalize(activity, false)
|
||||
|
||||
assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has followed you"
|
||||
assert Impl.format_body(%{activity: activity, type: "follow"}, user, object) ==
|
||||
"@Bob has followed you"
|
||||
|
||||
assert Impl.format_title(%{activity: activity}) ==
|
||||
assert Impl.format_title(%{activity: activity, type: "follow"}) ==
|
||||
"New Follower"
|
||||
end
|
||||
|
||||
|
@ -157,7 +161,7 @@ test "renders title and body for announce activity" do
|
|||
assert Impl.format_body(%{activity: announce_activity}, user, object) ==
|
||||
"@#{user.nickname} repeated: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..."
|
||||
|
||||
assert Impl.format_title(%{activity: announce_activity}) ==
|
||||
assert Impl.format_title(%{activity: announce_activity, type: "reblog"}) ==
|
||||
"New Repeat"
|
||||
end
|
||||
|
||||
|
@ -173,9 +177,10 @@ test "renders title and body for like activity" do
|
|||
{:ok, activity} = CommonAPI.favorite(user, activity.id)
|
||||
object = Object.normalize(activity)
|
||||
|
||||
assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has favorited your post"
|
||||
assert Impl.format_body(%{activity: activity, type: "favourite"}, user, object) ==
|
||||
"@Bob has favorited your post"
|
||||
|
||||
assert Impl.format_title(%{activity: activity}) ==
|
||||
assert Impl.format_title(%{activity: activity, type: "favourite"}) ==
|
||||
"New Favorite"
|
||||
end
|
||||
|
||||
|
@ -193,6 +198,46 @@ 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, false)
|
||||
[notification] = Notification.for_user(recipient)
|
||||
|
||||
res = Impl.build_content(notification, user, object)
|
||||
|
||||
assert res == %{
|
||||
body: "@#{user.nickname}: hey",
|
||||
title: "New Chat Message"
|
||||
}
|
||||
end
|
||||
|
||||
test "builds content for chat messages with no content" do
|
||||
user = insert(:user)
|
||||
recipient = insert(:user)
|
||||
|
||||
file = %Plug.Upload{
|
||||
content_type: "image/jpg",
|
||||
path: Path.absname("test/fixtures/image.jpg"),
|
||||
filename: "an_image.jpg"
|
||||
}
|
||||
|
||||
{:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
|
||||
|
||||
{:ok, chat} = CommonAPI.post_chat_message(user, recipient, nil, media_id: upload.id)
|
||||
object = Object.normalize(chat, false)
|
||||
[notification] = Notification.for_user(recipient)
|
||||
|
||||
res = Impl.build_content(notification, user, object)
|
||||
|
||||
assert res == %{
|
||||
body: "@#{user.nickname}: (Attachment)",
|
||||
title: "New Chat Message"
|
||||
}
|
||||
end
|
||||
|
||||
test "hides details for notifications when privacy option enabled" do
|
||||
user = insert(:user, nickname: "Bob")
|
||||
user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: true})
|
||||
|
@ -218,7 +263,7 @@ test "hides details for notifications when privacy option enabled" do
|
|||
status: "<Lorem ipsum dolor sit amet."
|
||||
})
|
||||
|
||||
notif = insert(:notification, user: user2, activity: activity)
|
||||
notif = insert(:notification, user: user2, activity: activity, type: "mention")
|
||||
|
||||
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
|
||||
object = Object.normalize(activity)
|
||||
|
@ -229,7 +274,7 @@ test "hides details for notifications when privacy option enabled" do
|
|||
|
||||
{:ok, activity} = CommonAPI.favorite(user, activity.id)
|
||||
|
||||
notif = insert(:notification, user: user2, activity: activity)
|
||||
notif = insert(:notification, user: user2, activity: activity, type: "favourite")
|
||||
|
||||
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
|
||||
object = Object.normalize(activity)
|
||||
|
@ -268,7 +313,7 @@ test "returns regular content for notifications with privacy option disabled" do
|
|||
"<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis."
|
||||
})
|
||||
|
||||
notif = insert(:notification, user: user2, activity: activity)
|
||||
notif = insert(:notification, user: user2, activity: activity, type: "mention")
|
||||
|
||||
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
|
||||
object = Object.normalize(activity)
|
||||
|
@ -281,7 +326,7 @@ test "returns regular content for notifications with privacy option disabled" do
|
|||
|
||||
{:ok, activity} = CommonAPI.favorite(user, activity.id)
|
||||
|
||||
notif = insert(:notification, user: user2, activity: activity)
|
||||
notif = insert(:notification, user: user2, activity: activity, type: "favourite")
|
||||
|
||||
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
|
||||
object = Object.normalize(activity)
|
||||
|
|
|
@ -7,11 +7,15 @@ defmodule Pleroma.Web.StreamerTest do
|
|||
|
||||
import Pleroma.Factory
|
||||
|
||||
alias Pleroma.Chat
|
||||
alias Pleroma.Chat.MessageReference
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.List
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.Streamer
|
||||
alias Pleroma.Web.StreamerView
|
||||
|
||||
@moduletag needs_streamer: true, capture_log: true
|
||||
|
||||
|
@ -145,6 +149,57 @@ test "it sends notify to in the 'user:notification' stream", %{user: user, notif
|
|||
refute Streamer.filtered_by_user?(user, notify)
|
||||
end
|
||||
|
||||
test "it sends chat messages to the 'user:pleroma_chat' stream", %{user: user} do
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno")
|
||||
object = Object.normalize(create_activity, false)
|
||||
chat = Chat.get(user.id, other_user.ap_id)
|
||||
cm_ref = MessageReference.for_chat_and_object(chat, object)
|
||||
cm_ref = %{cm_ref | chat: chat, object: object}
|
||||
|
||||
Streamer.get_topic_and_add_socket("user:pleroma_chat", user)
|
||||
Streamer.stream("user:pleroma_chat", {user, cm_ref})
|
||||
|
||||
text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref})
|
||||
|
||||
assert text =~ "hey cirno"
|
||||
assert_receive {:text, ^text}
|
||||
end
|
||||
|
||||
test "it sends chat messages to the 'user' stream", %{user: user} do
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey cirno")
|
||||
object = Object.normalize(create_activity, false)
|
||||
chat = Chat.get(user.id, other_user.ap_id)
|
||||
cm_ref = MessageReference.for_chat_and_object(chat, object)
|
||||
cm_ref = %{cm_ref | chat: chat, object: object}
|
||||
|
||||
Streamer.get_topic_and_add_socket("user", user)
|
||||
Streamer.stream("user", {user, cm_ref})
|
||||
|
||||
text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref})
|
||||
|
||||
assert text =~ "hey cirno"
|
||||
assert_receive {:text, ^text}
|
||||
end
|
||||
|
||||
test "it sends chat message notifications to the 'user:notification' stream", %{user: user} do
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, create_activity} = CommonAPI.post_chat_message(other_user, user, "hey")
|
||||
|
||||
notify =
|
||||
Repo.get_by(Pleroma.Notification, user_id: user.id, activity_id: create_activity.id)
|
||||
|> Repo.preload(:activity)
|
||||
|
||||
Streamer.get_topic_and_add_socket("user:notification", user)
|
||||
Streamer.stream("user:notification", notify)
|
||||
assert_receive {:render_with_user, _, _, ^notify}
|
||||
refute Streamer.filtered_by_user?(user, notify)
|
||||
end
|
||||
|
||||
test "it doesn't send notify to the 'user:notification' stream when a user is blocked", %{
|
||||
user: user
|
||||
} do
|
||||
|
|
Loading…
Reference in a new issue