Merge branch 'pleroma-conversations' into 'develop'

Extended Pleroma Conversations

See merge request pleroma/pleroma!1535
This commit is contained in:
lain 2019-08-16 12:55:33 +00:00
commit 7ab2dbbdb6
24 changed files with 655 additions and 99 deletions

View File

@ -46,6 +46,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- MRF: fix use of unserializable keyword lists in describe() implementations - MRF: fix use of unserializable keyword lists in describe() implementations
### Added ### Added
- Conversations: Add Pleroma-specific conversation endpoints and status posting extensions. Run the `bump_all_conversations` task again to create the necessary data.
- **Breaking:** MRF describe API, which adds support for exposing configuration information about MRF policies to NodeInfo. - **Breaking:** MRF describe API, which adds support for exposing configuration information about MRF policies to NodeInfo.
Custom modules will need to be updated by adding, at the very least, `def describe, do: {:ok, %{}}` to the MRF policy modules. Custom modules will need to be updated by adding, at the very least, `def describe, do: {:ok, %{}}` to the MRF policy modules.
- MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`) - MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`)

View File

@ -59,12 +59,19 @@ Has these additional fields under the `pleroma` object:
- `show_role`: boolean, nullable, true when the user wants his role (e.g admin, moderator) to be shown - `show_role`: boolean, nullable, true when the user wants his role (e.g admin, moderator) to be shown
- `no_rich_text` - boolean, nullable, true when html tags are stripped from all statuses requested from the API - `no_rich_text` - boolean, nullable, true when html tags are stripped from all statuses requested from the API
## Conversations
Has an additional field under the `pleroma` object:
- `recipients`: The list of the recipients of this Conversation. These will be addressed when replying to this conversation.
## Account Search ## Account Search
Behavior has changed: Behavior has changed:
- `/api/v1/accounts/search`: Does not require authentication - `/api/v1/accounts/search`: Does not require authentication
## Notifications ## Notifications
Has these additional fields under the `pleroma` object: Has these additional fields under the `pleroma` object:
@ -79,6 +86,7 @@ Additional parameters can be added to the JSON body/Form data:
- `content_type`: string, contain the MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint. - `content_type`: string, contain the MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint.
- `to`: A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply. - `to`: A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply.
- `visibility`: string, besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`. - `visibility`: string, besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`.
- `in_reply_to_conversation_id`: Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`.
## PATCH `/api/v1/update_credentials` ## PATCH `/api/v1/update_credentials`

View File

@ -319,3 +319,38 @@ See [Admin-API](Admin-API.md)
"healthy": true # Instance state "healthy": true # Instance state
} }
``` ```
# Pleroma Conversations
Pleroma Conversations have the same general structure that Mastodon Conversations have. The behavior differs in the following ways when using these endpoints:
1. Pleroma Conversations never add or remove recipients, unless explicitly changed by the user.
2. Pleroma Conversations statuses can be requested by Conversation id.
3. Pleroma Conversations can be replied to.
Conversations have the additional field "recipients" under the "pleroma" key. This holds a list of all the accounts that will receive a message in this conversation.
The status posting endpoint takes an additional parameter, `in_reply_to_conversation_id`, which, when set, will set the visiblity to direct and address only the people who are the recipients of that Conversation.
## `GET /api/v1/pleroma/conversations/:id/statuses`
### Timeline for a given conversation
* Method `GET`
* Authentication: required
* Params: Like other timelines
* Response: JSON, statuses (200 - healthy, 503 unhealthy).
## `GET /api/v1/pleroma/conversations/:id`
### The conversation with the given ID.
* Method `GET`
* Authentication: required
* Params: None
* Response: JSON, statuses (200 - healthy, 503 unhealthy).
## `PATCH /api/v1/pleroma/conversations/:id`
### Update a conversation. Used to change the set of recipients.
* Method `PATCH`
* Authentication: required
* Params:
* `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.
* Response: JSON, statuses (200 - healthy, 503 unhealthy)

View File

@ -4,6 +4,7 @@
defmodule Pleroma.Conversation do defmodule Pleroma.Conversation do
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Conversation.Participation.RecipientShip
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
use Ecto.Schema use Ecto.Schema
@ -39,6 +40,15 @@ defmodule Pleroma.Conversation do
Repo.get_by(__MODULE__, ap_id: ap_id) Repo.get_by(__MODULE__, ap_id: ap_id)
end end
def maybe_create_recipientships(participation, activity) do
participation = Repo.preload(participation, :recipients)
if participation.recipients |> Enum.empty?() do
recipients = User.get_all_by_ap_id(activity.recipients)
RecipientShip.create(recipients, participation)
end
end
@doc """ @doc """
This will This will
1. Create a conversation if there isn't one already 1. Create a conversation if there isn't one already
@ -60,6 +70,7 @@ defmodule Pleroma.Conversation do
{:ok, participation} = {:ok, participation} =
Participation.create_for_user_and_conversation(user, conversation, opts) Participation.create_for_user_and_conversation(user, conversation, opts)
maybe_create_recipientships(participation, activity)
participation participation
end) end)

View File

@ -5,6 +5,7 @@
defmodule Pleroma.Conversation.Participation do defmodule Pleroma.Conversation.Participation do
use Ecto.Schema use Ecto.Schema
alias Pleroma.Conversation alias Pleroma.Conversation
alias Pleroma.Conversation.Participation.RecipientShip
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
@ -17,6 +18,9 @@ defmodule Pleroma.Conversation.Participation do
field(:read, :boolean, default: false) field(:read, :boolean, default: false)
field(:last_activity_id, Pleroma.FlakeId, virtual: true) field(:last_activity_id, Pleroma.FlakeId, virtual: true)
has_many(:recipient_ships, RecipientShip)
has_many(:recipients, through: [:recipient_ships, :user])
timestamps() timestamps()
end end
@ -65,6 +69,14 @@ defmodule Pleroma.Conversation.Participation do
|> Pleroma.Pagination.fetch_paginated(params) |> Pleroma.Pagination.fetch_paginated(params)
end end
def for_user_and_conversation(user, conversation) do
from(p in __MODULE__,
where: p.user_id == ^user.id,
where: p.conversation_id == ^conversation.id
)
|> Repo.one()
end
def for_user_with_last_activity_id(user, params \\ %{}) do def for_user_with_last_activity_id(user, params \\ %{}) do
for_user(user, params) for_user(user, params)
|> Enum.map(fn participation -> |> Enum.map(fn participation ->
@ -81,4 +93,46 @@ defmodule Pleroma.Conversation.Participation do
end) end)
|> Enum.filter(& &1.last_activity_id) |> Enum.filter(& &1.last_activity_id)
end end
def get(_, _ \\ [])
def get(nil, _), do: nil
def get(id, params) do
query =
if preload = params[:preload] do
from(p in __MODULE__,
preload: ^preload
)
else
__MODULE__
end
Repo.get(query, id)
end
def set_recipients(participation, user_ids) do
user_ids =
[participation.user_id | user_ids]
|> Enum.uniq()
Repo.transaction(fn ->
query =
from(r in RecipientShip,
where: r.participation_id == ^participation.id
)
Repo.delete_all(query)
users =
from(u in User,
where: u.id in ^user_ids
)
|> Repo.all()
RecipientShip.create(users, participation)
:ok
end)
{:ok, Repo.preload(participation, :recipients, force: true)}
end
end end

View File

@ -0,0 +1,34 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Conversation.Participation.RecipientShip do
use Ecto.Schema
alias Pleroma.Conversation.Participation
alias Pleroma.Repo
alias Pleroma.User
import Ecto.Changeset
schema "conversation_participation_recipient_ships" do
belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:participation, Participation)
end
def creation_cng(struct, params) do
struct
|> cast(params, [:user_id, :participation_id])
|> validate_required([:user_id, :participation_id])
end
def create(%User{} = user, participation), do: create([user], participation)
def create(users, participation) do
Enum.each(users, fn user ->
%__MODULE__{}
|> creation_cng(%{user_id: user.id, participation_id: participation.id})
|> Repo.insert!()
end)
end
end

View File

@ -485,6 +485,13 @@ defmodule Pleroma.User do
Repo.get_by(User, ap_id: ap_id) Repo.get_by(User, ap_id: ap_id)
end end
def get_all_by_ap_id(ap_ids) do
from(u in __MODULE__,
where: u.ap_id in ^ap_ids
)
|> Repo.all()
end
# This is mostly an SPC migration fix. This guesses the user nickname by taking the last part # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
# of the ap_id and the domain and tries to get that user # of the ap_id and the domain and tries to get that user
def get_by_guessed_nickname(ap_id) do def get_by_guessed_nickname(ap_id) do

View File

@ -4,6 +4,7 @@
defmodule Pleroma.Web.CommonAPI do defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.Formatter alias Pleroma.Formatter
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.ThreadMute alias Pleroma.ThreadMute
@ -171,21 +172,25 @@ defmodule Pleroma.Web.CommonAPI do
end) end)
end end
def get_visibility(%{"visibility" => visibility}, in_reply_to) def get_visibility(_, _, %Participation{}) do
{"direct", "direct"}
end
def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
when visibility in ~w{public unlisted private direct}, when visibility in ~w{public unlisted private direct},
do: {visibility, get_replied_to_visibility(in_reply_to)} do: {visibility, get_replied_to_visibility(in_reply_to)}
def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to) do def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
visibility = {:list, String.to_integer(list_id)} visibility = {:list, String.to_integer(list_id)}
{visibility, get_replied_to_visibility(in_reply_to)} {visibility, get_replied_to_visibility(in_reply_to)}
end end
def get_visibility(_, in_reply_to) when not is_nil(in_reply_to) do def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
visibility = get_replied_to_visibility(in_reply_to) visibility = get_replied_to_visibility(in_reply_to)
{visibility, visibility} {visibility, visibility}
end end
def get_visibility(_, in_reply_to), do: {"public", get_replied_to_visibility(in_reply_to)} def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
def get_replied_to_visibility(nil), do: nil def get_replied_to_visibility(nil), do: nil
@ -201,7 +206,9 @@ defmodule Pleroma.Web.CommonAPI do
with status <- String.trim(status), with status <- String.trim(status),
attachments <- attachments_from_ids(data), attachments <- attachments_from_ids(data),
in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]), in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]),
{visibility, in_reply_to_visibility} <- get_visibility(data, in_reply_to), in_reply_to_conversation <- Participation.get(data["in_reply_to_conversation_id"]),
{visibility, in_reply_to_visibility} <-
get_visibility(data, in_reply_to, in_reply_to_conversation),
{_, false} <- {_, false} <-
{:private_to_public, in_reply_to_visibility == "direct" && visibility != "direct"}, {:private_to_public, in_reply_to_visibility == "direct" && visibility != "direct"},
{content_html, mentions, tags} <- {content_html, mentions, tags} <-
@ -214,8 +221,9 @@ defmodule Pleroma.Web.CommonAPI do
mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id), mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id),
addressed_users <- get_addressed_users(mentioned_users, data["to"]), addressed_users <- get_addressed_users(mentioned_users, data["to"]),
{poll, poll_emoji} <- make_poll_data(data), {poll, poll_emoji} <- make_poll_data(data),
{to, cc} <- get_to_and_cc(user, addressed_users, in_reply_to, visibility), {to, cc} <-
context <- make_context(in_reply_to), get_to_and_cc(user, addressed_users, in_reply_to, visibility, in_reply_to_conversation),
context <- make_context(in_reply_to, in_reply_to_conversation),
cw <- data["spoiler_text"] || "", cw <- data["spoiler_text"] || "",
sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}), sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),
full_payload <- String.trim(status <> cw), full_payload <- String.trim(status <> cw),

View File

@ -8,6 +8,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
alias Calendar.Strftime alias Calendar.Strftime
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Formatter alias Pleroma.Formatter
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.Plugs.AuthenticationPlug
@ -86,9 +87,21 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> Enum.filter(& &1) |> Enum.filter(& &1)
end end
@spec get_to_and_cc(User.t(), list(String.t()), Activity.t() | nil, String.t()) :: @spec get_to_and_cc(
User.t(),
list(String.t()),
Activity.t() | nil,
String.t(),
Participation.t() | nil
) ::
{list(String.t()), list(String.t())} {list(String.t()), list(String.t())}
def get_to_and_cc(user, mentioned_users, inReplyTo, "public") do
def get_to_and_cc(_, _, _, _, %Participation{} = participation) do
participation = Repo.preload(participation, :recipients)
{Enum.map(participation.recipients, & &1.ap_id), []}
end
def get_to_and_cc(user, mentioned_users, inReplyTo, "public", _) do
to = [Pleroma.Constants.as_public() | mentioned_users] to = [Pleroma.Constants.as_public() | mentioned_users]
cc = [user.follower_address] cc = [user.follower_address]
@ -99,7 +112,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end end
end end
def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted") do def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted", _) do
to = [user.follower_address | mentioned_users] to = [user.follower_address | mentioned_users]
cc = [Pleroma.Constants.as_public()] cc = [Pleroma.Constants.as_public()]
@ -110,12 +123,12 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end end
end end
def get_to_and_cc(user, mentioned_users, inReplyTo, "private") do def get_to_and_cc(user, mentioned_users, inReplyTo, "private", _) do
{to, cc} = get_to_and_cc(user, mentioned_users, inReplyTo, "direct") {to, cc} = get_to_and_cc(user, mentioned_users, inReplyTo, "direct", nil)
{[user.follower_address | to], cc} {[user.follower_address | to], cc}
end end
def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct") do def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct", _) do
if inReplyTo do if inReplyTo do
{Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []} {Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []}
else else
@ -123,7 +136,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end end
end end
def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}), do: {mentions, []} def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}, _), do: {mentions, []}
def get_addressed_users(_, to) when is_list(to) do def get_addressed_users(_, to) when is_list(to) do
User.get_ap_ids_by_nicknames(to) User.get_ap_ids_by_nicknames(to)
@ -253,8 +266,12 @@ defmodule Pleroma.Web.CommonAPI.Utils do
defp maybe_add_nsfw_tag(data, _), do: data defp maybe_add_nsfw_tag(data, _), do: data
def make_context(%Activity{data: %{"context" => context}}), do: context def make_context(_, %Participation{} = participation) do
def make_context(_), do: Utils.generate_context_id() Repo.preload(participation, :conversation).conversation.ap_id
end
def make_context(%Activity{data: %{"context" => context}}, _), do: context
def make_context(_, _), do: Utils.generate_context_id()
def maybe_add_attachments(parsed, _attachments, true = _no_links), do: parsed def maybe_add_attachments(parsed, _attachments, true = _no_links), do: parsed

View File

@ -33,4 +33,80 @@ defmodule Pleroma.Web.ControllerHelper do
end end
defp param_to_integer(_, default), do: default defp param_to_integer(_, default), do: default
def add_link_headers(
conn,
method,
activities,
param \\ nil,
params \\ %{},
func3 \\ nil,
func4 \\ nil
) do
params =
conn.params
|> Map.drop(["since_id", "max_id", "min_id"])
|> Map.merge(params)
last = List.last(activities)
func3 = func3 || (&mastodon_api_url/3)
func4 = func4 || (&mastodon_api_url/4)
if last do
max_id = last.id
limit =
params
|> Map.get("limit", "20")
|> String.to_integer()
min_id =
if length(activities) <= limit do
activities
|> List.first()
|> Map.get(:id)
else
activities
|> Enum.at(limit * -1)
|> Map.get(:id)
end
{next_url, prev_url} =
if param do
{
func4.(
Pleroma.Web.Endpoint,
method,
param,
Map.merge(params, %{max_id: max_id})
),
func4.(
Pleroma.Web.Endpoint,
method,
param,
Map.merge(params, %{min_id: min_id})
)
}
else
{
func3.(
Pleroma.Web.Endpoint,
method,
Map.merge(params, %{max_id: max_id})
),
func3.(
Pleroma.Web.Endpoint,
method,
Map.merge(params, %{min_id: min_id})
)
}
end
conn
|> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
else
conn
end
end
end end

View File

@ -5,7 +5,8 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [json_response: 3] import Pleroma.Web.ControllerHelper,
only: [json_response: 3, add_link_headers: 5, add_link_headers: 4, add_link_headers: 3]
alias Ecto.Changeset alias Ecto.Changeset
alias Pleroma.Activity alias Pleroma.Activity
@ -342,71 +343,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
json(conn, mastodon_emoji) json(conn, mastodon_emoji)
end end
defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
params =
conn.params
|> Map.drop(["since_id", "max_id", "min_id"])
|> Map.merge(params)
last = List.last(activities)
if last do
max_id = last.id
limit =
params
|> Map.get("limit", "20")
|> String.to_integer()
min_id =
if length(activities) <= limit do
activities
|> List.first()
|> Map.get(:id)
else
activities
|> Enum.at(limit * -1)
|> Map.get(:id)
end
{next_url, prev_url} =
if param do
{
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
param,
Map.merge(params, %{max_id: max_id})
),
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
param,
Map.merge(params, %{min_id: min_id})
)
}
else
{
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
Map.merge(params, %{max_id: max_id})
),
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
Map.merge(params, %{min_id: min_id})
)
}
end
conn
|> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
else
conn
end
end
def home_timeline(%{assigns: %{user: user}} = conn, params) do def home_timeline(%{assigns: %{user: user}} = conn, params) do
params = params =
params params
@ -1797,7 +1733,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
conversations = conversations =
Enum.map(participations, fn participation -> Enum.map(participations, fn participation ->
ConversationView.render("participation.json", %{participation: participation, user: user}) ConversationView.render("participation.json", %{participation: participation, for: user})
end) end)
conn conn
@ -1810,7 +1746,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
Repo.get_by(Participation, id: participation_id, user_id: user.id), Repo.get_by(Participation, id: participation_id, user_id: user.id),
{:ok, participation} <- Participation.mark_as_read(participation) do {:ok, participation} <- Participation.mark_as_read(participation) do
participation_view = participation_view =
ConversationView.render("participation.json", %{participation: participation, user: user}) ConversationView.render("participation.json", %{participation: participation, for: user})
conn conn
|> json(participation_view) |> json(participation_view)

View File

@ -11,8 +11,8 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
def render("participation.json", %{participation: participation, user: user}) do def render("participation.json", %{participation: participation, for: user}) do
participation = Repo.preload(participation, conversation: :users) participation = Repo.preload(participation, conversation: [], recipients: [])
last_activity_id = last_activity_id =
with nil <- participation.last_activity_id do with nil <- participation.last_activity_id do
@ -28,7 +28,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
# Conversations return all users except the current user. # Conversations return all users except the current user.
users = users =
participation.conversation.users participation.recipients
|> Enum.reject(&(&1.id == user.id)) |> Enum.reject(&(&1.id == user.id))
accounts = accounts =

View File

@ -8,6 +8,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
require Pleroma.Constants require Pleroma.Constants
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Conversation
alias Pleroma.Conversation.Participation
alias Pleroma.HTML alias Pleroma.HTML
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
@ -235,6 +237,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
object.data["url"] || object.data["external_url"] || object.data["id"] object.data["url"] || object.data["external_url"] || object.data["id"]
end end
direct_conversation_id =
with {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
{_, %User{} = for_user} <- {:for_user, opts[:for]},
%{data: %{"context" => context}} when is_binary(context) <- activity,
%Conversation{} = conversation <- Conversation.get_for_ap_id(context),
%Participation{id: participation_id} <-
Participation.for_user_and_conversation(for_user, conversation) do
participation_id
else
_e ->
nil
end
%{ %{
id: to_string(activity.id), id: to_string(activity.id),
uri: object.data["id"], uri: object.data["id"],
@ -272,7 +287,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
conversation_id: get_context_id(activity), conversation_id: get_context_id(activity),
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
content: %{"text/plain" => content_plaintext}, content: %{"text/plain" => content_plaintext},
spoiler_text: %{"text/plain" => summary_plaintext} spoiler_text: %{"text/plain" => summary_plaintext},
direct_conversation_id: direct_conversation_id
} }
} }
end end

View File

@ -0,0 +1,73 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 7]
alias Pleroma.Conversation.Participation
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.MastodonAPI.ConversationView
alias Pleroma.Web.MastodonAPI.StatusView
def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
with %Participation{} = participation <- Participation.get(participation_id),
true <- user.id == participation.user_id do
conn
|> put_view(ConversationView)
|> render("participation.json", %{participation: participation, for: user})
end
end
def conversation_statuses(
%{assigns: %{user: user}} = conn,
%{"id" => participation_id} = params
) do
params =
params
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
participation =
participation_id
|> Participation.get(preload: [:conversation])
if user.id == participation.user_id do
activities =
participation.conversation.ap_id
|> ActivityPub.fetch_activities_for_context(params)
|> Enum.reverse()
conn
|> add_link_headers(
:conversation_statuses,
activities,
participation_id,
params,
nil,
&pleroma_api_url/4
)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
end
def update_conversation(
%{assigns: %{user: user}} = conn,
%{"id" => participation_id, "recipients" => recipients}
) do
participation =
participation_id
|> Participation.get()
with true <- user.id == participation.user_id,
{:ok, participation} <- Participation.set_recipients(participation, recipients) do
conn
|> put_view(ConversationView)
|> render("participation.json", %{participation: participation, for: user})
end
end
end

View File

@ -259,6 +259,21 @@ defmodule Pleroma.Web.Router do
end end
end end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:authenticated_api)
scope [] do
pipe_through(:oauth_read)
get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
get("/conversations/:id", PleromaAPIController, :conversation)
end
scope [] do
pipe_through(:oauth_write)
patch("/conversations/:id", PleromaAPIController, :update_conversation)
end
end
scope "/api/v1", Pleroma.Web.MastodonAPI do scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:authenticated_api) pipe_through(:authenticated_api)

View File

@ -202,7 +202,7 @@ defmodule Pleroma.Web.Streamer do
payload: payload:
Pleroma.Web.MastodonAPI.ConversationView.render("participation.json", %{ Pleroma.Web.MastodonAPI.ConversationView.render("participation.json", %{
participation: participation, participation: participation,
user: participation.user for: participation.user
}) })
|> Jason.encode!() |> Jason.encode!()
} }

View File

@ -0,0 +1,13 @@
defmodule Pleroma.Repo.Migrations.CreateConversationParticipationRecipientShips do
use Ecto.Migration
def change do
create_if_not_exists table(:conversation_participation_recipient_ships) do
add(:user_id, references(:users, type: :uuid, on_delete: :delete_all))
add(:participation_id, references(:conversation_participations, on_delete: :delete_all))
end
create_if_not_exists index(:conversation_participation_recipient_ships, [:user_id])
create_if_not_exists index(:conversation_participation_recipient_ships, [:participation_id])
end
end

View File

@ -8,6 +8,50 @@ defmodule Pleroma.Conversation.ParticipationTest do
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
test "getting a participation will also preload things" do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} =
CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
[participation] = Participation.for_user(user)
participation = Participation.get(participation.id, preload: [:conversation])
assert %Pleroma.Conversation{} = participation.conversation
end
test "for a new conversation, it sets the recipents of the participation" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
[participation] = Participation.for_user(user)
participation = Pleroma.Repo.preload(participation, :recipients)
assert length(participation.recipients) == 2
assert user in participation.recipients
assert other_user in participation.recipients
# Mentioning another user in the same conversation will not add a new recipients.
{:ok, _activity} =
CommonAPI.post(user, %{
"in_reply_to_status_id" => activity.id,
"status" => "Hey @#{third_user.nickname}.",
"visibility" => "direct"
})
[participation] = Participation.for_user(user)
participation = Pleroma.Repo.preload(participation, :recipients)
assert length(participation.recipients) == 2
end
test "it creates a participation for a conversation and a user" do test "it creates a participation for a conversation and a user" do
user = insert(:user) user = insert(:user)
conversation = insert(:conversation) conversation = insert(:conversation)
@ -102,4 +146,23 @@ defmodule Pleroma.Conversation.ParticipationTest do
[] = Participation.for_user_with_last_activity_id(user) [] = Participation.for_user_with_last_activity_id(user)
end end
test "it sets recipients, always keeping the owner of the participation even when not explicitly set" do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
[participation] = Participation.for_user_with_last_activity_id(user)
participation = Repo.preload(participation, :recipients)
assert participation.recipients |> length() == 1
assert user in participation.recipients
{:ok, participation} = Participation.set_recipients(participation, [other_user.id])
assert participation.recipients |> length() == 2
assert user in participation.recipients
assert other_user in participation.recipients
end
end end

View File

@ -5,13 +5,58 @@
defmodule Pleroma.Web.CommonAPITest do defmodule Pleroma.Web.CommonAPITest do
use Pleroma.DataCase use Pleroma.DataCase
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
import Pleroma.Factory import Pleroma.Factory
test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
[participation] = Participation.for_user(user)
{:ok, convo_reply} =
CommonAPI.post(user, %{"status" => ".", "in_reply_to_conversation_id" => participation.id})
assert Visibility.is_direct?(convo_reply)
assert activity.data["context"] == convo_reply.data["context"]
end
test "when replying to a conversation / participation, it only mentions the recipients explicitly declared in the participation" do
har = insert(:user)
jafnhar = insert(:user)
tridi = insert(:user)
{:ok, activity} =
CommonAPI.post(har, %{
"status" => "@#{jafnhar.nickname} hey",
"visibility" => "direct"
})
assert har.ap_id in activity.recipients
assert jafnhar.ap_id in activity.recipients
[participation] = Participation.for_user(har)
{:ok, activity} =
CommonAPI.post(har, %{
"status" => "I don't really like @#{tridi.nickname}",
"visibility" => "direct",
"in_reply_to_status_id" => activity.id,
"in_reply_to_conversation_id" => participation.id
})
assert har.ap_id in activity.recipients
assert jafnhar.ap_id in activity.recipients
refute tridi.ap_id in activity.recipients
end
test "with the safe_dm_mention option set, it does not mention people beyond the initial tags" do test "with the safe_dm_mention option set, it does not mention people beyond the initial tags" do
har = insert(:user) har = insert(:user)
jafnhar = insert(:user) jafnhar = insert(:user)

View File

@ -239,7 +239,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
mentioned_user = insert(:user) mentioned_user = insert(:user)
mentions = [mentioned_user.ap_id] mentions = [mentioned_user.ap_id]
{to, cc} = Utils.get_to_and_cc(user, mentions, nil, "public") {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "public", nil)
assert length(to) == 2 assert length(to) == 2
assert length(cc) == 1 assert length(cc) == 1
@ -256,7 +256,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
{:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"}) {:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"})
mentions = [mentioned_user.ap_id] mentions = [mentioned_user.ap_id]
{to, cc} = Utils.get_to_and_cc(user, mentions, activity, "public") {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "public", nil)
assert length(to) == 3 assert length(to) == 3
assert length(cc) == 1 assert length(cc) == 1
@ -272,7 +272,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
mentioned_user = insert(:user) mentioned_user = insert(:user)
mentions = [mentioned_user.ap_id] mentions = [mentioned_user.ap_id]
{to, cc} = Utils.get_to_and_cc(user, mentions, nil, "unlisted") {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "unlisted", nil)
assert length(to) == 2 assert length(to) == 2
assert length(cc) == 1 assert length(cc) == 1
@ -289,7 +289,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
{:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"}) {:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"})
mentions = [mentioned_user.ap_id] mentions = [mentioned_user.ap_id]
{to, cc} = Utils.get_to_and_cc(user, mentions, activity, "unlisted") {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "unlisted", nil)
assert length(to) == 3 assert length(to) == 3
assert length(cc) == 1 assert length(cc) == 1
@ -305,7 +305,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
mentioned_user = insert(:user) mentioned_user = insert(:user)
mentions = [mentioned_user.ap_id] mentions = [mentioned_user.ap_id]
{to, cc} = Utils.get_to_and_cc(user, mentions, nil, "private") {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "private", nil)
assert length(to) == 2 assert length(to) == 2
assert length(cc) == 0 assert length(cc) == 0
@ -320,7 +320,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
{:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"}) {:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"})
mentions = [mentioned_user.ap_id] mentions = [mentioned_user.ap_id]
{to, cc} = Utils.get_to_and_cc(user, mentions, activity, "private") {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "private", nil)
assert length(to) == 3 assert length(to) == 3
assert length(cc) == 0 assert length(cc) == 0
@ -335,7 +335,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
mentioned_user = insert(:user) mentioned_user = insert(:user)
mentions = [mentioned_user.ap_id] mentions = [mentioned_user.ap_id]
{to, cc} = Utils.get_to_and_cc(user, mentions, nil, "direct") {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "direct", nil)
assert length(to) == 1 assert length(to) == 1
assert length(cc) == 0 assert length(cc) == 0
@ -350,7 +350,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
{:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"}) {:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"})
mentions = [mentioned_user.ap_id] mentions = [mentioned_user.ap_id]
{to, cc} = Utils.get_to_and_cc(user, mentions, activity, "direct") {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "direct", nil)
assert length(to) == 2 assert length(to) == 2
assert length(cc) == 0 assert length(cc) == 0

View File

@ -0,0 +1,34 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ConversationViewTest do
use Pleroma.DataCase
alias Pleroma.Conversation.Participation
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.ConversationView
import Pleroma.Factory
test "represents a Mastodon Conversation entity" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}", "visibility" => "direct"})
[participation] = Participation.for_user_with_last_activity_id(user)
assert participation
conversation =
ConversationView.render("participation.json", %{participation: participation, for: user})
assert conversation.id == participation.id |> to_string()
assert conversation.last_status.id == activity.id
assert [account] = conversation.accounts
assert account.id == other_user.id
end
end

View File

@ -23,6 +23,21 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
:ok :ok
end end
test "returns the direct conversation id when given the `with_conversation_id` option" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
status =
StatusView.render("status.json",
activity: activity,
with_direct_conversation_id: true,
for: user
)
assert status[:pleroma][:direct_conversation_id]
end
test "returns a temporary ap_id based user for activities missing db users" do test "returns a temporary ap_id based user for activities missing db users" do
user = insert(:user) user = insert(:user)
@ -133,7 +148,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
conversation_id: convo_id, conversation_id: convo_id,
in_reply_to_account_acct: nil, in_reply_to_account_acct: nil,
content: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["content"])}, content: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["content"])},
spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])} spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])},
direct_conversation_id: nil
} }
} }

View File

@ -0,0 +1,94 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Conversation.Participation
alias Pleroma.Repo
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
test "/api/v1/pleroma/conversations/:id", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} =
CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}!", "visibility" => "direct"})
[participation] = Participation.for_user(other_user)
result =
conn
|> assign(:user, other_user)
|> get("/api/v1/pleroma/conversations/#{participation.id}")
|> json_response(200)
assert result["id"] == participation.id |> to_string()
end
test "/api/v1/pleroma/conversations/:id/statuses", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, _activity} =
CommonAPI.post(user, %{"status" => "Hi @#{third_user.nickname}!", "visibility" => "direct"})
{:ok, activity} =
CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}!", "visibility" => "direct"})
[participation] = Participation.for_user(other_user)
{:ok, activity_two} =
CommonAPI.post(other_user, %{
"status" => "Hi!",
"in_reply_to_status_id" => activity.id,
"in_reply_to_conversation_id" => participation.id
})
result =
conn
|> assign(:user, other_user)
|> get("/api/v1/pleroma/conversations/#{participation.id}/statuses")
|> json_response(200)
assert length(result) == 2
id_one = activity.id
id_two = activity_two.id
assert [%{"id" => ^id_one}, %{"id" => ^id_two}] = result
end
test "PATCH /api/v1/pleroma/conversations/:id", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} = CommonAPI.post(user, %{"status" => "Hi", "visibility" => "direct"})
[participation] = Participation.for_user(user)
participation = Repo.preload(participation, :recipients)
assert [user] == participation.recipients
assert other_user not in participation.recipients
result =
conn
|> assign(:user, user)
|> patch("/api/v1/pleroma/conversations/#{participation.id}", %{
"recipients" => [user.id, other_user.id]
})
|> json_response(200)
assert result["id"] == participation.id |> to_string
[participation] = Participation.for_user(user)
participation = Repo.preload(participation, :recipients)
assert user in participation.recipients
assert other_user in participation.recipients
end
end