forked from AkkomaGang/akkoma
Merge branch 'feature/addressable-lists' into 'develop'
[#802] Add addressable lists See merge request pleroma/pleroma!1113
This commit is contained in:
commit
d725ccfd3a
18 changed files with 373 additions and 53 deletions
|
@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Added synchronization of following/followers counters for external users
|
- Added synchronization of following/followers counters for external users
|
||||||
- Configuration: `enabled` option for `Pleroma.Emails.Mailer`, defaulting to `false`.
|
- Configuration: `enabled` option for `Pleroma.Emails.Mailer`, defaulting to `false`.
|
||||||
- Configuration: Pleroma.Plugs.RateLimiter `bucket_name`, `params` options.
|
- Configuration: Pleroma.Plugs.RateLimiter `bucket_name`, `params` options.
|
||||||
|
- Addressable lists
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
|
- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
|
||||||
|
|
|
@ -16,9 +16,11 @@ Adding the parameter `with_muted=true` to the timeline queries will also return
|
||||||
|
|
||||||
## Statuses
|
## Statuses
|
||||||
|
|
||||||
|
- `visibility`: has an additional possible value `list`
|
||||||
|
|
||||||
Has these additional fields under the `pleroma` object:
|
Has these additional fields under the `pleroma` object:
|
||||||
|
|
||||||
- `local`: true if the post was made on the local instance.
|
- `local`: true if the post was made on the local instance
|
||||||
- `conversation_id`: the ID of the conversation the status is associated with (if any)
|
- `conversation_id`: the ID of the conversation the status is associated with (if any)
|
||||||
- `in_reply_to_account_acct`: the `acct` property of User entity for replied user (if any)
|
- `in_reply_to_account_acct`: the `acct` property of User entity for replied user (if any)
|
||||||
- `content`: a map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
|
- `content`: a map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
|
||||||
|
@ -72,6 +74,7 @@ Additional parameters can be added to the JSON body/Form data:
|
||||||
- `preview`: boolean, if set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example.
|
- `preview`: boolean, if set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example.
|
||||||
- `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`.
|
||||||
|
|
||||||
## PATCH `/api/v1/update_credentials`
|
## PATCH `/api/v1/update_credentials`
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ defmodule Pleroma.List do
|
||||||
belongs_to(:user, User, type: Pleroma.FlakeId)
|
belongs_to(:user, User, type: Pleroma.FlakeId)
|
||||||
field(:title, :string)
|
field(:title, :string)
|
||||||
field(:following, {:array, :string}, default: [])
|
field(:following, {:array, :string}, default: [])
|
||||||
|
field(:ap_id, :string)
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
@ -55,6 +56,10 @@ def get(id, %{id: user_id} = _user) do
|
||||||
Repo.one(query)
|
Repo.one(query)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_by_ap_id(ap_id) do
|
||||||
|
Repo.get_by(__MODULE__, ap_id: ap_id)
|
||||||
|
end
|
||||||
|
|
||||||
def get_following(%Pleroma.List{following: following} = _list) do
|
def get_following(%Pleroma.List{following: following} = _list) do
|
||||||
q =
|
q =
|
||||||
from(
|
from(
|
||||||
|
@ -105,7 +110,14 @@ def rename(%Pleroma.List{} = list, title) do
|
||||||
|
|
||||||
def create(title, %User{} = creator) do
|
def create(title, %User{} = creator) do
|
||||||
list = %Pleroma.List{user_id: creator.id, title: title}
|
list = %Pleroma.List{user_id: creator.id, title: title}
|
||||||
Repo.insert(list)
|
|
||||||
|
Repo.transaction(fn ->
|
||||||
|
list = Repo.insert!(list)
|
||||||
|
|
||||||
|
list
|
||||||
|
|> change(ap_id: "#{creator.ap_id}/lists/#{list.id}")
|
||||||
|
|> Repo.update!()
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
def follow(%Pleroma.List{following: following} = list, %User{} = followed) do
|
def follow(%Pleroma.List{following: following} = list, %User{} = followed) do
|
||||||
|
@ -125,4 +137,19 @@ def update_follows(%Pleroma.List{} = list, attrs) do
|
||||||
|> follow_changeset(attrs)
|
|> follow_changeset(attrs)
|
||||||
|> Repo.update()
|
|> Repo.update()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def memberships(%User{follower_address: follower_address}) do
|
||||||
|
Pleroma.List
|
||||||
|
|> where([l], ^follower_address in l.following)
|
||||||
|
|> select([l], l.ap_id)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
|
def memberships(_), do: []
|
||||||
|
|
||||||
|
def member?(%Pleroma.List{following: following}, %User{follower_address: follower_address}) do
|
||||||
|
Enum.member?(following, follower_address)
|
||||||
|
end
|
||||||
|
|
||||||
|
def member?(_, _), do: false
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,19 +27,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
# For Announce activities, we filter the recipients based on following status for any actors
|
# For Announce activities, we filter the recipients based on following status for any actors
|
||||||
# that match actual users. See issue #164 for more information about why this is necessary.
|
# that match actual users. See issue #164 for more information about why this is necessary.
|
||||||
defp get_recipients(%{"type" => "Announce"} = data) do
|
defp get_recipients(%{"type" => "Announce"} = data) do
|
||||||
to = data["to"] || []
|
to = Map.get(data, "to", [])
|
||||||
cc = data["cc"] || []
|
cc = Map.get(data, "cc", [])
|
||||||
|
bcc = Map.get(data, "bcc", [])
|
||||||
actor = User.get_cached_by_ap_id(data["actor"])
|
actor = User.get_cached_by_ap_id(data["actor"])
|
||||||
|
|
||||||
recipients =
|
recipients =
|
||||||
(to ++ cc)
|
Enum.filter(Enum.concat([to, cc, bcc]), fn recipient ->
|
||||||
|> Enum.filter(fn recipient ->
|
|
||||||
case User.get_cached_by_ap_id(recipient) do
|
case User.get_cached_by_ap_id(recipient) do
|
||||||
nil ->
|
nil -> true
|
||||||
true
|
user -> User.following?(user, actor)
|
||||||
|
|
||||||
user ->
|
|
||||||
User.following?(user, actor)
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
@ -47,17 +44,19 @@ defp get_recipients(%{"type" => "Announce"} = data) do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_recipients(%{"type" => "Create"} = data) do
|
defp get_recipients(%{"type" => "Create"} = data) do
|
||||||
to = data["to"] || []
|
to = Map.get(data, "to", [])
|
||||||
cc = data["cc"] || []
|
cc = Map.get(data, "cc", [])
|
||||||
actor = data["actor"] || []
|
bcc = Map.get(data, "bcc", [])
|
||||||
recipients = (to ++ cc ++ [actor]) |> Enum.uniq()
|
actor = Map.get(data, "actor", [])
|
||||||
|
recipients = [to, cc, bcc, [actor]] |> Enum.concat() |> Enum.uniq()
|
||||||
{recipients, to, cc}
|
{recipients, to, cc}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_recipients(data) do
|
defp get_recipients(data) do
|
||||||
to = data["to"] || []
|
to = Map.get(data, "to", [])
|
||||||
cc = data["cc"] || []
|
cc = Map.get(data, "cc", [])
|
||||||
recipients = to ++ cc
|
bcc = Map.get(data, "bcc", [])
|
||||||
|
recipients = Enum.concat([to, cc, bcc])
|
||||||
{recipients, to, cc}
|
{recipients, to, cc}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -898,13 +897,11 @@ defp maybe_order(query, %{order: :asc}) do
|
||||||
defp maybe_order(query, _), do: query
|
defp maybe_order(query, _), do: query
|
||||||
|
|
||||||
def fetch_activities_query(recipients, opts \\ %{}) do
|
def fetch_activities_query(recipients, opts \\ %{}) do
|
||||||
base_query = from(activity in Activity)
|
|
||||||
|
|
||||||
config = %{
|
config = %{
|
||||||
skip_thread_containment: Config.get([:instance, :skip_thread_containment])
|
skip_thread_containment: Config.get([:instance, :skip_thread_containment])
|
||||||
}
|
}
|
||||||
|
|
||||||
base_query
|
Activity
|
||||||
|> maybe_preload_objects(opts)
|
|> maybe_preload_objects(opts)
|
||||||
|> maybe_preload_bookmarks(opts)
|
|> maybe_preload_bookmarks(opts)
|
||||||
|> maybe_set_thread_muted_field(opts)
|
|> maybe_set_thread_muted_field(opts)
|
||||||
|
@ -933,11 +930,31 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_activities(recipients, opts \\ %{}) do
|
def fetch_activities(recipients, opts \\ %{}) do
|
||||||
fetch_activities_query(recipients, opts)
|
list_memberships = Pleroma.List.memberships(opts["user"])
|
||||||
|
|
||||||
|
fetch_activities_query(recipients ++ list_memberships, opts)
|
||||||
|> Pagination.fetch_paginated(opts)
|
|> Pagination.fetch_paginated(opts)
|
||||||
|> Enum.reverse()
|
|> Enum.reverse()
|
||||||
|
|> maybe_update_cc(list_memberships, opts["user"])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id})
|
||||||
|
when is_list(list_memberships) and length(list_memberships) > 0 do
|
||||||
|
Enum.map(activities, fn
|
||||||
|
%{data: %{"bcc" => bcc}} = activity when is_list(bcc) and length(bcc) > 0 ->
|
||||||
|
if Enum.any?(bcc, &(&1 in list_memberships)) do
|
||||||
|
update_in(activity.data["cc"], &[user_ap_id | &1])
|
||||||
|
else
|
||||||
|
activity
|
||||||
|
end
|
||||||
|
|
||||||
|
activity ->
|
||||||
|
activity
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_update_cc(activities, _, _), do: activities
|
||||||
|
|
||||||
def fetch_activities_bounded_query(query, recipients, recipients_with_public) do
|
def fetch_activities_bounded_query(query, recipients, recipients_with_public) do
|
||||||
from(activity in query,
|
from(activity in query,
|
||||||
where:
|
where:
|
||||||
|
|
|
@ -92,18 +92,68 @@ defp should_federate?(inbox, public) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
defp recipients(actor, activity) do
|
||||||
Publishes an activity to all relevant peers.
|
followers =
|
||||||
"""
|
|
||||||
def publish(%User{} = actor, %Activity{} = activity) do
|
|
||||||
remote_followers =
|
|
||||||
if actor.follower_address in activity.recipients do
|
if actor.follower_address in activity.recipients do
|
||||||
{:ok, followers} = User.get_followers(actor)
|
{:ok, followers} = User.get_followers(actor)
|
||||||
followers |> Enum.filter(&(!&1.local))
|
Enum.filter(followers, &(!&1.local))
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Pleroma.Web.Salmon.remote_users(actor, activity) ++ followers
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_cc_ap_ids(ap_id, recipients) do
|
||||||
|
host = Map.get(URI.parse(ap_id), :host)
|
||||||
|
|
||||||
|
recipients
|
||||||
|
|> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end)
|
||||||
|
|> Enum.map(& &1.ap_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Publishes an activity with BCC to all relevant peers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def publish(actor, %{data: %{"bcc" => bcc}} = activity) when is_list(bcc) and bcc != [] do
|
||||||
|
public = is_public?(activity)
|
||||||
|
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
|
||||||
|
|
||||||
|
recipients = recipients(actor, activity)
|
||||||
|
|
||||||
|
recipients
|
||||||
|
|> Enum.filter(&User.ap_enabled?/1)
|
||||||
|
|> Enum.map(fn %{info: %{source_data: data}} -> data["inbox"] end)
|
||||||
|
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|
||||||
|
|> Instances.filter_reachable()
|
||||||
|
|> Enum.each(fn {inbox, unreachable_since} ->
|
||||||
|
%User{ap_id: ap_id} =
|
||||||
|
Enum.find(recipients, fn %{info: %{source_data: data}} -> data["inbox"] == inbox end)
|
||||||
|
|
||||||
|
# Get all the recipients on the same host and add them to cc. Otherwise it a remote
|
||||||
|
# instance would only accept a first message for the first recipient and ignore the rest.
|
||||||
|
cc = get_cc_ap_ids(ap_id, recipients)
|
||||||
|
|
||||||
|
json =
|
||||||
|
data
|
||||||
|
|> Map.put("cc", cc)
|
||||||
|
|> Jason.encode!()
|
||||||
|
|
||||||
|
Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{
|
||||||
|
inbox: inbox,
|
||||||
|
json: json,
|
||||||
|
actor: actor,
|
||||||
|
id: activity.data["id"],
|
||||||
|
unreachable_since: unreachable_since
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Publishes an activity to all relevant peers.
|
||||||
|
"""
|
||||||
|
def publish(%User{} = actor, %Activity{} = activity) do
|
||||||
public = is_public?(activity)
|
public = is_public?(activity)
|
||||||
|
|
||||||
if public && Config.get([:instance, :allow_relay]) do
|
if public && Config.get([:instance, :allow_relay]) do
|
||||||
|
@ -114,7 +164,7 @@ def publish(%User{} = actor, %Activity{} = activity) do
|
||||||
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
|
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
|
||||||
json = Jason.encode!(data)
|
json = Jason.encode!(data)
|
||||||
|
|
||||||
(Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers)
|
recipients(actor, activity)
|
||||||
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|
||||||
|> Enum.map(fn %{info: %{source_data: data}} ->
|
|> Enum.map(fn %{info: %{source_data: data}} ->
|
||||||
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
|
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
|
||||||
|
|
|
@ -814,13 +814,16 @@ def prepare_object(object) do
|
||||||
|
|
||||||
def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
|
def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
|
||||||
object =
|
object =
|
||||||
Object.normalize(object_id).data
|
object_id
|
||||||
|
|> Object.normalize()
|
||||||
|
|> Map.get(:data)
|
||||||
|> prepare_object
|
|> prepare_object
|
||||||
|
|
||||||
data =
|
data =
|
||||||
data
|
data
|
||||||
|> Map.put("object", object)
|
|> Map.put("object", object)
|
||||||
|> Map.merge(Utils.make_json_ld_header())
|
|> Map.merge(Utils.make_json_ld_header())
|
||||||
|
|> Map.delete("bcc")
|
||||||
|
|
||||||
{:ok, data}
|
{:ok, data}
|
||||||
end
|
end
|
||||||
|
|
|
@ -34,6 +34,20 @@ def is_direct?(activity) do
|
||||||
!is_public?(activity) && !is_private?(activity)
|
!is_public?(activity) && !is_private?(activity)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def is_list?(%{data: %{"listMessage" => _}}), do: true
|
||||||
|
def is_list?(_), do: false
|
||||||
|
|
||||||
|
def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true
|
||||||
|
|
||||||
|
def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do
|
||||||
|
user.ap_id in activity.data["to"] ||
|
||||||
|
list_ap_id
|
||||||
|
|> Pleroma.List.get_by_ap_id()
|
||||||
|
|> Pleroma.List.member?(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false
|
||||||
|
|
||||||
def visible_for_user?(activity, nil) do
|
def visible_for_user?(activity, nil) do
|
||||||
is_public?(activity)
|
is_public?(activity)
|
||||||
end
|
end
|
||||||
|
@ -73,6 +87,9 @@ def get_visibility(object) do
|
||||||
object.data["directMessage"] == true ->
|
object.data["directMessage"] == true ->
|
||||||
"direct"
|
"direct"
|
||||||
|
|
||||||
|
is_binary(object.data["listMessage"]) ->
|
||||||
|
"list"
|
||||||
|
|
||||||
length(cc) > 0 ->
|
length(cc) > 0 ->
|
||||||
"private"
|
"private"
|
||||||
|
|
||||||
|
|
|
@ -176,6 +176,11 @@ 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
|
||||||
|
visibility = {:list, String.to_integer(list_id)}
|
||||||
|
{visibility, get_replied_to_visibility(in_reply_to)}
|
||||||
|
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}
|
||||||
|
@ -236,19 +241,18 @@ def post(user, %{"status" => status} = data) do
|
||||||
"emoji",
|
"emoji",
|
||||||
Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji)
|
Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji)
|
||||||
) do
|
) do
|
||||||
res =
|
preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
|
||||||
ActivityPub.create(
|
direct? = visibility == "direct"
|
||||||
%{
|
|
||||||
to: to,
|
|
||||||
actor: user,
|
|
||||||
context: context,
|
|
||||||
object: object,
|
|
||||||
additional: %{"cc" => cc, "directMessage" => visibility == "direct"}
|
|
||||||
},
|
|
||||||
Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
|
|
||||||
)
|
|
||||||
|
|
||||||
res
|
%{
|
||||||
|
to: to,
|
||||||
|
actor: user,
|
||||||
|
context: context,
|
||||||
|
object: object,
|
||||||
|
additional: %{"cc" => cc, "directMessage" => direct?}
|
||||||
|
}
|
||||||
|
|> maybe_add_list_data(user, visibility)
|
||||||
|
|> ActivityPub.create(preview?)
|
||||||
else
|
else
|
||||||
{:private_to_public, true} ->
|
{:private_to_public, true} ->
|
||||||
{:error, dgettext("errors", "The message visibility must be direct")}
|
{:error, dgettext("errors", "The message visibility must be direct")}
|
||||||
|
|
|
@ -100,12 +100,29 @@ def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct") do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
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)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_addressed_users(mentioned_users, _), do: mentioned_users
|
def get_addressed_users(mentioned_users, _), do: mentioned_users
|
||||||
|
|
||||||
|
def maybe_add_list_data(activity_params, user, {:list, list_id}) do
|
||||||
|
case Pleroma.List.get(list_id, user) do
|
||||||
|
%Pleroma.List{} = list ->
|
||||||
|
activity_params
|
||||||
|
|> put_in([:additional, "bcc"], [list.ap_id])
|
||||||
|
|> put_in([:additional, "listMessage"], list.ap_id)
|
||||||
|
|> put_in([:object, "listMessage"], list.ap_id)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
activity_params
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_add_list_data(activity_params, _, _), do: activity_params
|
||||||
|
|
||||||
def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data)
|
def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data)
|
||||||
when is_list(options) do
|
when is_list(options) do
|
||||||
%{max_expiration: max_expiration, min_expiration: min_expiration} =
|
%{max_expiration: max_expiration, min_expiration: min_expiration} =
|
||||||
|
|
|
@ -123,11 +123,26 @@ def encode(private_key, doc) do
|
||||||
{:ok, salmon}
|
{:ok, salmon}
|
||||||
end
|
end
|
||||||
|
|
||||||
def remote_users(%{data: %{"to" => to} = data}) do
|
def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do
|
||||||
to = to ++ (data["cc"] || [])
|
cc = Map.get(data, "cc", [])
|
||||||
|
|
||||||
to
|
bcc =
|
||||||
|> Enum.map(fn id -> User.get_cached_by_ap_id(id) end)
|
data
|
||||||
|
|> Map.get("bcc", [])
|
||||||
|
|> Enum.reduce([], fn ap_id, bcc ->
|
||||||
|
case Pleroma.List.get_by_ap_id(ap_id) do
|
||||||
|
%Pleroma.List{user_id: ^user_id} = list ->
|
||||||
|
{:ok, following} = Pleroma.List.get_following(list)
|
||||||
|
bcc ++ Enum.map(following, & &1.ap_id)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
bcc
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
[to, cc, bcc]
|
||||||
|
|> Enum.concat()
|
||||||
|
|> Enum.map(&User.get_cached_by_ap_id/1)
|
||||||
|> Enum.filter(fn user -> user && !user.local end)
|
|> Enum.filter(fn user -> user && !user.local end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -191,7 +206,7 @@ def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity
|
||||||
{:ok, private, _} = Keys.keys_from_pem(keys)
|
{:ok, private, _} = Keys.keys_from_pem(keys)
|
||||||
{:ok, feed} = encode(private, feed)
|
{:ok, feed} = encode(private, feed)
|
||||||
|
|
||||||
remote_users = remote_users(activity)
|
remote_users = remote_users(user, activity)
|
||||||
|
|
||||||
salmon_urls = Enum.map(remote_users, & &1.info.salmon)
|
salmon_urls = Enum.map(remote_users, & &1.info.salmon)
|
||||||
reachable_urls_metadata = Instances.filter_reachable(salmon_urls)
|
reachable_urls_metadata = Instances.filter_reachable(salmon_urls)
|
||||||
|
|
26
priv/repo/migrations/20190516112144_add_ap_id_to_lists.exs
Normal file
26
priv/repo/migrations/20190516112144_add_ap_id_to_lists.exs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
defmodule Pleroma.Repo.Migrations.AddApIdToLists do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
alter table(:lists) do
|
||||||
|
add(:ap_id, :string)
|
||||||
|
end
|
||||||
|
|
||||||
|
execute("""
|
||||||
|
UPDATE lists
|
||||||
|
SET ap_id = u.ap_id || '/lists/' || lists.id
|
||||||
|
FROM users AS u
|
||||||
|
WHERE lists.user_id = u.id
|
||||||
|
""")
|
||||||
|
|
||||||
|
create(unique_index(:lists, :ap_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
drop(index(:lists, [:ap_id]))
|
||||||
|
|
||||||
|
alter table(:lists) do
|
||||||
|
remove(:ap_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -20,6 +20,10 @@
|
||||||
"sensitive": "as:sensitive",
|
"sensitive": "as:sensitive",
|
||||||
"litepub": "http://litepub.social/ns#",
|
"litepub": "http://litepub.social/ns#",
|
||||||
"directMessage": "litepub:directMessage",
|
"directMessage": "litepub:directMessage",
|
||||||
|
"listMessage": {
|
||||||
|
"@id": "litepub:listMessage",
|
||||||
|
"@type": "@id"
|
||||||
|
},
|
||||||
"oauthRegistrationEndpoint": {
|
"oauthRegistrationEndpoint": {
|
||||||
"@id": "litepub:oauthRegistrationEndpoint",
|
"@id": "litepub:oauthRegistrationEndpoint",
|
||||||
"@type": "@id"
|
"@type": "@id"
|
||||||
|
|
|
@ -113,4 +113,30 @@ test "getting own lists a given user belongs to" do
|
||||||
assert owned_list in lists_2
|
assert owned_list in lists_2
|
||||||
refute not_owned_list in lists_2
|
refute not_owned_list in lists_2
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "get by ap_id" do
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, list} = Pleroma.List.create("foo", user)
|
||||||
|
assert Pleroma.List.get_by_ap_id(list.ap_id) == list
|
||||||
|
end
|
||||||
|
|
||||||
|
test "memberships" do
|
||||||
|
user = insert(:user)
|
||||||
|
member = insert(:user)
|
||||||
|
{:ok, list} = Pleroma.List.create("foo", user)
|
||||||
|
{:ok, list} = Pleroma.List.follow(list, member)
|
||||||
|
|
||||||
|
assert Pleroma.List.memberships(member) == [list.ap_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "member?" do
|
||||||
|
user = insert(:user)
|
||||||
|
member = insert(:user)
|
||||||
|
|
||||||
|
{:ok, list} = Pleroma.List.create("foo", user)
|
||||||
|
{:ok, list} = Pleroma.List.follow(list, member)
|
||||||
|
|
||||||
|
assert Pleroma.List.member?(list, member)
|
||||||
|
refute Pleroma.List.member?(list, user)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1190,6 +1190,21 @@ test "it can create a Flag activity" do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "fetch_activities/2 returns activities addressed to a list " do
|
||||||
|
user = insert(:user)
|
||||||
|
member = insert(:user)
|
||||||
|
{:ok, list} = Pleroma.List.create("foo", user)
|
||||||
|
{:ok, list} = Pleroma.List.follow(list, member)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
|
||||||
|
|
||||||
|
activity = Repo.preload(activity, :bookmark)
|
||||||
|
activity = %Activity{activity | thread_muted?: !!activity.thread_muted?}
|
||||||
|
|
||||||
|
assert ActivityPub.fetch_activities([], %{"user" => user}) == [activity]
|
||||||
|
end
|
||||||
|
|
||||||
def data_uri do
|
def data_uri do
|
||||||
File.read!("test/fixtures/avatar_data_uri")
|
File.read!("test/fixtures/avatar_data_uri")
|
||||||
end
|
end
|
||||||
|
|
|
@ -1098,6 +1098,18 @@ test "the directMessage flag is present" do
|
||||||
|
|
||||||
assert modified["directMessage"] == true
|
assert modified["directMessage"] == true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it strips BCC field" do
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, list} = Pleroma.List.create("foo", user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
|
||||||
|
|
||||||
|
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
|
||||||
|
|
||||||
|
assert is_nil(modified["bcc"])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "user upgrade" do
|
describe "user upgrade" do
|
||||||
|
|
|
@ -16,6 +16,9 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
|
||||||
following = insert(:user)
|
following = insert(:user)
|
||||||
unrelated = insert(:user)
|
unrelated = insert(:user)
|
||||||
{:ok, following} = Pleroma.User.follow(following, user)
|
{:ok, following} = Pleroma.User.follow(following, user)
|
||||||
|
{:ok, list} = Pleroma.List.create("foo", user)
|
||||||
|
|
||||||
|
Pleroma.List.follow(list, unrelated)
|
||||||
|
|
||||||
{:ok, public} =
|
{:ok, public} =
|
||||||
CommonAPI.post(user, %{"status" => "@#{mentioned.nickname}", "visibility" => "public"})
|
CommonAPI.post(user, %{"status" => "@#{mentioned.nickname}", "visibility" => "public"})
|
||||||
|
@ -29,6 +32,12 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
|
||||||
{:ok, unlisted} =
|
{:ok, unlisted} =
|
||||||
CommonAPI.post(user, %{"status" => "@#{mentioned.nickname}", "visibility" => "unlisted"})
|
CommonAPI.post(user, %{"status" => "@#{mentioned.nickname}", "visibility" => "unlisted"})
|
||||||
|
|
||||||
|
{:ok, list} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "@#{mentioned.nickname}",
|
||||||
|
"visibility" => "list:#{list.id}"
|
||||||
|
})
|
||||||
|
|
||||||
%{
|
%{
|
||||||
public: public,
|
public: public,
|
||||||
private: private,
|
private: private,
|
||||||
|
@ -37,29 +46,65 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
|
||||||
user: user,
|
user: user,
|
||||||
mentioned: mentioned,
|
mentioned: mentioned,
|
||||||
following: following,
|
following: following,
|
||||||
unrelated: unrelated
|
unrelated: unrelated,
|
||||||
|
list: list
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "is_direct?", %{public: public, private: private, direct: direct, unlisted: unlisted} do
|
test "is_direct?", %{
|
||||||
|
public: public,
|
||||||
|
private: private,
|
||||||
|
direct: direct,
|
||||||
|
unlisted: unlisted,
|
||||||
|
list: list
|
||||||
|
} do
|
||||||
assert Visibility.is_direct?(direct)
|
assert Visibility.is_direct?(direct)
|
||||||
refute Visibility.is_direct?(public)
|
refute Visibility.is_direct?(public)
|
||||||
refute Visibility.is_direct?(private)
|
refute Visibility.is_direct?(private)
|
||||||
refute Visibility.is_direct?(unlisted)
|
refute Visibility.is_direct?(unlisted)
|
||||||
|
assert Visibility.is_direct?(list)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "is_public?", %{public: public, private: private, direct: direct, unlisted: unlisted} do
|
test "is_public?", %{
|
||||||
|
public: public,
|
||||||
|
private: private,
|
||||||
|
direct: direct,
|
||||||
|
unlisted: unlisted,
|
||||||
|
list: list
|
||||||
|
} do
|
||||||
refute Visibility.is_public?(direct)
|
refute Visibility.is_public?(direct)
|
||||||
assert Visibility.is_public?(public)
|
assert Visibility.is_public?(public)
|
||||||
refute Visibility.is_public?(private)
|
refute Visibility.is_public?(private)
|
||||||
assert Visibility.is_public?(unlisted)
|
assert Visibility.is_public?(unlisted)
|
||||||
|
refute Visibility.is_public?(list)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "is_private?", %{public: public, private: private, direct: direct, unlisted: unlisted} do
|
test "is_private?", %{
|
||||||
|
public: public,
|
||||||
|
private: private,
|
||||||
|
direct: direct,
|
||||||
|
unlisted: unlisted,
|
||||||
|
list: list
|
||||||
|
} do
|
||||||
refute Visibility.is_private?(direct)
|
refute Visibility.is_private?(direct)
|
||||||
refute Visibility.is_private?(public)
|
refute Visibility.is_private?(public)
|
||||||
assert Visibility.is_private?(private)
|
assert Visibility.is_private?(private)
|
||||||
refute Visibility.is_private?(unlisted)
|
refute Visibility.is_private?(unlisted)
|
||||||
|
refute Visibility.is_private?(list)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "is_list?", %{
|
||||||
|
public: public,
|
||||||
|
private: private,
|
||||||
|
direct: direct,
|
||||||
|
unlisted: unlisted,
|
||||||
|
list: list
|
||||||
|
} do
|
||||||
|
refute Visibility.is_list?(direct)
|
||||||
|
refute Visibility.is_list?(public)
|
||||||
|
refute Visibility.is_list?(private)
|
||||||
|
refute Visibility.is_list?(unlisted)
|
||||||
|
assert Visibility.is_list?(list)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "visible_for_user?", %{
|
test "visible_for_user?", %{
|
||||||
|
@ -70,7 +115,8 @@ test "visible_for_user?", %{
|
||||||
user: user,
|
user: user,
|
||||||
mentioned: mentioned,
|
mentioned: mentioned,
|
||||||
following: following,
|
following: following,
|
||||||
unrelated: unrelated
|
unrelated: unrelated,
|
||||||
|
list: list
|
||||||
} do
|
} do
|
||||||
# All visible to author
|
# All visible to author
|
||||||
|
|
||||||
|
@ -78,6 +124,7 @@ test "visible_for_user?", %{
|
||||||
assert Visibility.visible_for_user?(private, user)
|
assert Visibility.visible_for_user?(private, user)
|
||||||
assert Visibility.visible_for_user?(unlisted, user)
|
assert Visibility.visible_for_user?(unlisted, user)
|
||||||
assert Visibility.visible_for_user?(direct, user)
|
assert Visibility.visible_for_user?(direct, user)
|
||||||
|
assert Visibility.visible_for_user?(list, user)
|
||||||
|
|
||||||
# All visible to a mentioned user
|
# All visible to a mentioned user
|
||||||
|
|
||||||
|
@ -85,6 +132,7 @@ test "visible_for_user?", %{
|
||||||
assert Visibility.visible_for_user?(private, mentioned)
|
assert Visibility.visible_for_user?(private, mentioned)
|
||||||
assert Visibility.visible_for_user?(unlisted, mentioned)
|
assert Visibility.visible_for_user?(unlisted, mentioned)
|
||||||
assert Visibility.visible_for_user?(direct, mentioned)
|
assert Visibility.visible_for_user?(direct, mentioned)
|
||||||
|
assert Visibility.visible_for_user?(list, mentioned)
|
||||||
|
|
||||||
# DM not visible for just follower
|
# DM not visible for just follower
|
||||||
|
|
||||||
|
@ -92,6 +140,7 @@ test "visible_for_user?", %{
|
||||||
assert Visibility.visible_for_user?(private, following)
|
assert Visibility.visible_for_user?(private, following)
|
||||||
assert Visibility.visible_for_user?(unlisted, following)
|
assert Visibility.visible_for_user?(unlisted, following)
|
||||||
refute Visibility.visible_for_user?(direct, following)
|
refute Visibility.visible_for_user?(direct, following)
|
||||||
|
refute Visibility.visible_for_user?(list, following)
|
||||||
|
|
||||||
# Public and unlisted visible for unrelated user
|
# Public and unlisted visible for unrelated user
|
||||||
|
|
||||||
|
@ -99,6 +148,9 @@ test "visible_for_user?", %{
|
||||||
assert Visibility.visible_for_user?(unlisted, unrelated)
|
assert Visibility.visible_for_user?(unlisted, unrelated)
|
||||||
refute Visibility.visible_for_user?(private, unrelated)
|
refute Visibility.visible_for_user?(private, unrelated)
|
||||||
refute Visibility.visible_for_user?(direct, unrelated)
|
refute Visibility.visible_for_user?(direct, unrelated)
|
||||||
|
|
||||||
|
# Visible for a list member
|
||||||
|
assert Visibility.visible_for_user?(list, unrelated)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "doesn't die when the user doesn't exist",
|
test "doesn't die when the user doesn't exist",
|
||||||
|
@ -115,18 +167,24 @@ test "get_visibility", %{
|
||||||
public: public,
|
public: public,
|
||||||
private: private,
|
private: private,
|
||||||
direct: direct,
|
direct: direct,
|
||||||
unlisted: unlisted
|
unlisted: unlisted,
|
||||||
|
list: list
|
||||||
} do
|
} do
|
||||||
assert Visibility.get_visibility(public) == "public"
|
assert Visibility.get_visibility(public) == "public"
|
||||||
assert Visibility.get_visibility(private) == "private"
|
assert Visibility.get_visibility(private) == "private"
|
||||||
assert Visibility.get_visibility(direct) == "direct"
|
assert Visibility.get_visibility(direct) == "direct"
|
||||||
assert Visibility.get_visibility(unlisted) == "unlisted"
|
assert Visibility.get_visibility(unlisted) == "unlisted"
|
||||||
|
assert Visibility.get_visibility(list) == "list"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "get_visibility with directMessage flag" do
|
test "get_visibility with directMessage flag" do
|
||||||
assert Visibility.get_visibility(%{data: %{"directMessage" => true}}) == "direct"
|
assert Visibility.get_visibility(%{data: %{"directMessage" => true}}) == "direct"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "get_visibility with listMessage flag" do
|
||||||
|
assert Visibility.get_visibility(%{data: %{"listMessage" => ""}}) == "list"
|
||||||
|
end
|
||||||
|
|
||||||
describe "entire_thread_visible_for_user?/2" do
|
describe "entire_thread_visible_for_user?/2" do
|
||||||
test "returns false if not found activity", %{user: user} do
|
test "returns false if not found activity", %{user: user} do
|
||||||
refute Visibility.entire_thread_visible_for_user?(%Activity{}, user)
|
refute Visibility.entire_thread_visible_for_user?(%Activity{}, user)
|
||||||
|
|
|
@ -129,6 +129,18 @@ test "it does not allow replies to direct messages that are not direct messages
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it allows to address a list" do
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, list} = Pleroma.List.create("foo", user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
|
||||||
|
|
||||||
|
assert activity.data["bcc"] == [list.ap_id]
|
||||||
|
assert activity.recipients == [list.ap_id, user.ap_id]
|
||||||
|
assert activity.data["listMessage"] == list.ap_id
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "reactions" do
|
describe "reactions" do
|
||||||
|
|
|
@ -541,4 +541,17 @@ test "embeds a relationship in the account in reposts" do
|
||||||
assert result[:reblog][:account][:pleroma][:relationship] ==
|
assert result[:reblog][:account][:pleroma][:relationship] ==
|
||||||
AccountView.render("relationship.json", %{user: user, target: user})
|
AccountView.render("relationship.json", %{user: user, target: user})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "visibility/list" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, list} = Pleroma.List.create("foo", user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
|
||||||
|
|
||||||
|
status = StatusView.render("status.json", activity: activity)
|
||||||
|
|
||||||
|
assert status.visibility == "list"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue