aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--docs/api/differences_in_mastoapi_responses.md1
-rw-r--r--lib/pleroma/list.ex32
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex57
-rw-r--r--lib/pleroma/web/activity_pub/publisher.ex63
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex5
-rw-r--r--lib/pleroma/web/common_api/common_api.ex34
-rw-r--r--lib/pleroma/web/common_api/utils.ex8
-rw-r--r--lib/pleroma/web/salmon/salmon.ex25
-rw-r--r--test/list_test.exs29
-rw-r--r--test/web/activity_pub/activity_pub_test.exs14
-rw-r--r--test/web/activity_pub/transmogrifier_test.exs12
-rw-r--r--test/web/common_api/common_api_test.exs13
13 files changed, 245 insertions, 49 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c563c39da..d7536d67f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -32,6 +32,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Metadata: RelMe provider
- OAuth: added support for refresh tokens
- Emoji packs and emoji pack manager
+- Addressable lists
### Changed
- **Breaking:** Configuration: move from Pleroma.Mailer to Pleroma.Emails.Mailer
diff --git a/docs/api/differences_in_mastoapi_responses.md b/docs/api/differences_in_mastoapi_responses.md
index 36b47608e..946e0e885 100644
--- a/docs/api/differences_in_mastoapi_responses.md
+++ b/docs/api/differences_in_mastoapi_responses.md
@@ -69,6 +69,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.
- `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.
+- `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`
diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex
index a5b1cad68..81b842e9c 100644
--- a/lib/pleroma/list.ex
+++ b/lib/pleroma/list.ex
@@ -12,6 +12,8 @@ defmodule Pleroma.List do
alias Pleroma.Repo
alias Pleroma.User
+ @ap_id_regex ~r/^\/users\/(?<nickname>\w+)\/lists\/(?<list_id>\d+)/
+
schema "lists" do
belongs_to(:user, User, type: Pleroma.FlakeId)
field(:title, :string)
@@ -32,6 +34,12 @@ defmodule Pleroma.List do
|> validate_required([:following])
end
+ def ap_id(%User{nickname: nickname}, list_id) do
+ Pleroma.Web.Endpoint.url() <> "/users/#{nickname}/lists/#{list_id}"
+ end
+
+ def ap_id({nickname, list_id}), do: ap_id(%User{nickname: nickname}, list_id)
+
def for_user(user, _opts) do
query =
from(
@@ -55,6 +63,19 @@ defmodule Pleroma.List do
Repo.one(query)
end
+ def get_by_ap_id(ap_id) do
+ host = Pleroma.Web.Endpoint.host()
+
+ with %{host: ^host, path: path} <- URI.parse(ap_id),
+ %{"list_id" => list_id, "nickname" => nickname} <-
+ Regex.named_captures(@ap_id_regex, path),
+ %User{} = user <- User.get_cached_by_nickname(nickname) do
+ get(list_id, user)
+ else
+ _ -> nil
+ end
+ end
+
def get_following(%Pleroma.List{following: following} = _list) do
q =
from(
@@ -125,4 +146,15 @@ defmodule Pleroma.List do
|> follow_changeset(attrs)
|> Repo.update()
end
+
+ def memberships(%User{follower_address: follower_address}) do
+ Pleroma.List
+ |> where([l], ^follower_address in l.following)
+ |> join(:inner, [l], u in User, on: l.user_id == u.id)
+ |> select([l, u], {u.nickname, l.id})
+ |> Repo.all()
+ |> Enum.map(&ap_id/1)
+ end
+
+ def memberships(_), do: []
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 233fee4fa..b1ce5ae52 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -25,19 +25,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
# 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.
defp get_recipients(%{"type" => "Announce"} = data) do
- to = data["to"] || []
- cc = data["cc"] || []
+ to = Map.get(data, "to", [])
+ cc = Map.get(data, "cc", [])
+ bcc = Map.get(data, "bcc", [])
actor = User.get_cached_by_ap_id(data["actor"])
recipients =
- (to ++ cc)
- |> Enum.filter(fn recipient ->
+ Enum.filter(Enum.concat([to, cc, bcc]), fn recipient ->
case User.get_cached_by_ap_id(recipient) do
- nil ->
- true
-
- user ->
- User.following?(user, actor)
+ nil -> true
+ user -> User.following?(user, actor)
end
end)
@@ -45,17 +42,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
defp get_recipients(%{"type" => "Create"} = data) do
- to = data["to"] || []
- cc = data["cc"] || []
- actor = data["actor"] || []
- recipients = (to ++ cc ++ [actor]) |> Enum.uniq()
+ to = Map.get(data, "to", [])
+ cc = Map.get(data, "cc", [])
+ bcc = Map.get(data, "bcc", [])
+ actor = Map.get(data, "actor", [])
+ recipients = [to, cc, bcc, [actor]] |> Enum.concat() |> Enum.uniq()
{recipients, to, cc}
end
defp get_recipients(data) do
- to = data["to"] || []
- cc = data["cc"] || []
- recipients = to ++ cc
+ to = Map.get(data, "to", [])
+ cc = Map.get(data, "cc", [])
+ bcc = Map.get(data, "bcc", [])
+ recipients = Enum.concat([to, cc, bcc])
{recipients, to, cc}
end
@@ -829,9 +828,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp maybe_order(query, _), do: query
def fetch_activities_query(recipients, opts \\ %{}) do
- base_query = from(activity in Activity)
-
- base_query
+ Activity
|> maybe_preload_objects(opts)
|> maybe_preload_bookmarks(opts)
|> maybe_order(opts)
@@ -856,9 +853,29 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
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)
|> Enum.reverse()
+ |> maybe_update_cc(list_memberships, opts["user"])
+ end
+
+ defp maybe_update_cc(activities, [], _), do: activities
+ defp maybe_update_cc(activities, _, nil), do: activities
+
+ defp maybe_update_cc(activities, list_memberships, user) do
+ Enum.map(activities, fn
+ %{data: %{"bcc" => bcc}} = activity when is_list(bcc) ->
+ 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
def fetch_activities_bounded(recipients_to, recipients_cc, opts \\ %{}) do
diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex
index 8e3af0a81..036ac892e 100644
--- a/lib/pleroma/web/activity_pub/publisher.ex
+++ b/lib/pleroma/web/activity_pub/publisher.ex
@@ -93,18 +93,67 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
end
end
- @doc """
- Publishes an activity to all relevant peers.
- """
- def publish(%User{} = actor, %Activity{} = activity) do
- remote_followers =
+ defp recipients(actor, activity) do
+ followers =
if actor.follower_address in activity.recipients do
{:ok, followers} = User.get_followers(actor)
- followers |> Enum.filter(&(!&1.local))
+ Enum.filter(followers, &(!&1.local))
else
[]
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)
+
+ cc = get_cc_ap_ids(ap_id, recipients)
+
+ json =
+ data
+ |> Map.put("cc", cc)
+ |> Map.put("directMessage", true)
+ |> 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)
if public && Config.get([:instance, :allow_relay]) do
@@ -115,7 +164,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
{:ok, data} = Transmogrifier.prepare_outgoing(activity.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.map(fn %{info: %{source_data: data}} ->
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 508f3532f..c4cc77a95 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -741,13 +741,16 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
object =
- Object.normalize(object_id).data
+ object_id
+ |> Object.normalize()
+ |> Map.get(:data)
|> prepare_object
data =
data
|> Map.put("object", object)
|> Map.merge(Utils.make_json_ld_header())
+ |> Map.delete("bcc")
{:ok, data}
end
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index b53869c75..ed2c0017f 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -120,6 +120,10 @@ defmodule Pleroma.Web.CommonAPI do
when visibility in ~w{public unlisted private direct},
do: visibility
+ def get_visibility(%{"visibility" => "list:" <> list_id}) do
+ {:list, String.to_integer(list_id)}
+ end
+
def get_visibility(%{"in_reply_to_status_id" => status_id}) when not is_nil(status_id) do
case get_replied_to_activity(status_id) do
nil ->
@@ -150,6 +154,7 @@ defmodule Pleroma.Web.CommonAPI do
visibility
),
{to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility),
+ bcc <- bcc_for_list(user, visibility),
context <- make_context(in_reply_to),
cw <- data["spoiler_text"] || "",
full_payload <- String.trim(status <> cw),
@@ -172,19 +177,16 @@ defmodule Pleroma.Web.CommonAPI do
"emoji",
Formatter.get_emoji_map(full_payload)
) do
- res =
- ActivityPub.create(
- %{
- to: to,
- actor: user,
- context: context,
- object: object,
- additional: %{"cc" => cc, "directMessage" => visibility == "direct"}
- },
- Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
- )
-
- res
+ ActivityPub.create(
+ %{
+ to: to,
+ actor: user,
+ context: context,
+ object: object,
+ additional: %{"cc" => cc, "bcc" => bcc, "directMessage" => visibility == "direct"}
+ },
+ Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
+ )
end
end
@@ -193,7 +195,7 @@ defmodule Pleroma.Web.CommonAPI do
user =
with emoji <- emoji_from_profile(user),
source_data <- (user.info.source_data || %{}) |> Map.put("tag", emoji),
- info_cng <- Pleroma.User.Info.set_source_data(user.info, source_data),
+ info_cng <- User.Info.set_source_data(user.info, source_data),
change <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
{:ok, user} <- User.update_and_set_cache(change) do
user
@@ -226,7 +228,7 @@ defmodule Pleroma.Web.CommonAPI do
} = activity <- get_by_id_or_ap_id(id_or_ap_id),
true <- Enum.member?(object_to, "https://www.w3.org/ns/activitystreams#Public"),
%{valid?: true} = info_changeset <-
- Pleroma.User.Info.add_pinnned_activity(user.info, activity),
+ User.Info.add_pinnned_activity(user.info, activity),
changeset <-
Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
{:ok, _user} <- User.update_and_set_cache(changeset) do
@@ -243,7 +245,7 @@ defmodule Pleroma.Web.CommonAPI do
def unpin(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
%{valid?: true} = info_changeset <-
- Pleroma.User.Info.remove_pinnned_activity(user.info, activity),
+ User.Info.remove_pinnned_activity(user.info, activity),
changeset <-
Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
{:ok, _user} <- User.update_and_set_cache(changeset) do
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 1dfe50b40..f082b77d8 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -102,6 +102,14 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
+ def to_for_user_and_mentions(_user, _mentions, _inReplyTo, _), do: {[], []}
+
+ def bcc_for_list(user, {:list, list_id}) do
+ [Pleroma.List.ap_id(user, list_id)]
+ end
+
+ def bcc_for_list(_, _), do: []
+
def make_content_html(
status,
attachments,
diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex
index 42709ab47..80c3a3190 100644
--- a/lib/pleroma/web/salmon/salmon.ex
+++ b/lib/pleroma/web/salmon/salmon.ex
@@ -162,11 +162,26 @@ defmodule Pleroma.Web.Salmon do
{:ok, salmon}
end
- def remote_users(%{data: %{"to" => to} = data}) do
- to = to ++ (data["cc"] || [])
+ def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do
+ cc = Map.get(data, "cc", [])
+
+ bcc =
+ 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
- |> Enum.map(fn id -> User.get_cached_by_ap_id(id) end)
+ [to, cc, bcc]
+ |> Enum.concat()
+ |> Enum.map(&User.get_cached_by_ap_id/1)
|> Enum.filter(fn user -> user && !user.local end)
end
@@ -230,7 +245,7 @@ defmodule Pleroma.Web.Salmon do
{:ok, private, _} = keys_from_pem(keys)
{: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)
reachable_urls_metadata = Instances.filter_reachable(salmon_urls)
diff --git a/test/list_test.exs b/test/list_test.exs
index 1909c0cd9..0e72b6660 100644
--- a/test/list_test.exs
+++ b/test/list_test.exs
@@ -113,4 +113,33 @@ defmodule Pleroma.ListTest do
assert owned_list in lists_2
refute not_owned_list in lists_2
end
+
+ test "get ap_id by user nickname and list id" do
+ nickname = "foo"
+ list_id = 42
+
+ expected = Pleroma.Web.Endpoint.url() <> "/users/#{nickname}/lists/#{list_id}"
+
+ assert Pleroma.List.ap_id(%Pleroma.User{nickname: nickname}, list_id) == expected
+ assert Pleroma.List.ap_id({nickname, list_id}) == expected
+ end
+
+ test "get by ap_id" do
+ user = insert(:user)
+ {:ok, list} = Pleroma.List.create("foo", user)
+ ap_id = Pleroma.List.ap_id(user, list.id)
+
+ assert Pleroma.List.get_by_ap_id(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)
+
+ list_ap_id = Pleroma.List.ap_id(user, list.id)
+
+ assert Pleroma.List.memberships(member) == [list_ap_id]
+ end
end
diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs
index 0f90aa1ac..e38de388b 100644
--- a/test/web/activity_pub/activity_pub_test.exs
+++ b/test/web/activity_pub/activity_pub_test.exs
@@ -1156,6 +1156,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
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)
+
+ assert ActivityPub.fetch_activities([], %{"user" => user}) == [activity]
+ end
+
def data_uri do
File.read!("test/fixtures/avatar_data_uri")
end
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index c24b50f8c..e93189df6 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -1028,6 +1028,18 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert modified["directMessage"] == true
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
describe "user upgrade" do
diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs
index a5b07c446..11f3c8357 100644
--- a/test/web/common_api/common_api_test.exs
+++ b/test/web/common_api/common_api_test.exs
@@ -87,6 +87,19 @@ defmodule Pleroma.Web.CommonAPITest do
assert object.data["content"] == "<p><b>2hu</b></p>alert('xss')"
end
+
+ test "it allows to address a list" do
+ user = insert(:user)
+ {:ok, list} = Pleroma.List.create("foo", user)
+
+ list_ap_id = Pleroma.List.ap_id(user, list.id)
+
+ {: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]
+ end
end
describe "reactions" do