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\/(?\w+)\/lists\/(?\d+)/ + schema "lists" do belongs_to(:user, User, type: Pleroma.FlakeId) field(:title, :string) @@ -32,6 +34,12 @@ def follow_changeset(list, attrs \\ %{}) 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 @@ def get(id, %{id: user_id} = _user) 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 @@ def update_follows(%Pleroma.List{} = list, attrs) 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 84d7f47b1..3b71e0369 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -783,9 +783,7 @@ defp maybe_preload_objects(query, _) do end def fetch_activities_query(recipients, opts \\ %{}) do - base_query = from(activity in Activity) - - base_query + Activity |> maybe_preload_objects(opts) |> restrict_recipients(recipients, opts["user"]) |> restrict_tag(opts) @@ -807,9 +805,29 @@ def fetch_activities_query(recipients, opts \\ %{}) 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 @@ -917,13 +935,23 @@ def should_federate?(inbox, public) do end defp recipients(actor, activity) do - Pleroma.Web.Salmon.remote_users(activity) ++ + 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 def publish(actor, %{data: %{"bcc" => bcc}} = activity) when is_list(bcc) and bcc != [] do @@ -938,12 +966,14 @@ def publish(actor, %{data: %{"bcc" => bcc}} = activity) when is_list(bcc) and bc |> Enum.filter(fn inbox -> should_federate?(inbox, public) end) |> Instances.filter_reachable() |> Enum.each(fn {inbox, unreachable_since} -> - %User{ap_id: cc} = + %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("cc", cc) |> Map.put("directMessage", true) |> Jason.encode!() diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 4ca59110f..d47d5788c 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -153,7 +153,7 @@ def post(user, %{"status" => status} = data) do visibility ), {to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility), - {:ok, bcc} <- bcc_for_list(user, visibility), + bcc <- bcc_for_list(user, visibility), context <- make_context(in_reply_to), cw <- data["spoiler_text"], full_payload <- String.trim(status <> (data["spoiler_text"] || "")), @@ -197,7 +197,7 @@ def update(user) 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 @@ -230,7 +230,7 @@ def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) 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 @@ -247,7 +247,7 @@ def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) 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 83a745b58..32c3b4b98 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -8,7 +8,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do alias Pleroma.Activity alias Pleroma.Config alias Pleroma.Formatter - alias Pleroma.List alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User @@ -106,16 +105,10 @@ def to_for_user_and_mentions(_user, mentions, inReplyTo, "direct") do def to_for_user_and_mentions(_user, _mentions, _inReplyTo, _), do: {[], []} def bcc_for_list(user, {:list, list_id}) do - with {_, %List{} = list} <- {:list, List.get(list_id, user)}, - {:ok, following} <- List.get_following(list) do - {:ok, Enum.map(following, & &1.ap_id)} - else - {:list, _} -> {:error, "List not found"} - err -> err - end + [Pleroma.List.ap_id(user, list_id)] end - def bcc_for_list(_, _), do: {:ok, []} + def bcc_for_list(_, _), do: [] def make_content_html( status, diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex index 4d89f4bdb..ca51255f3 100644 --- a/lib/pleroma/web/salmon/salmon.ex +++ b/lib/pleroma/web/salmon/salmon.ex @@ -156,9 +156,22 @@ def encode(private_key, doc) do {:ok, salmon} end - def remote_users(%{data: %{"to" => to} = data}) do + def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do cc = Map.get(data, "cc", []) - bcc = Map.get(data, "bcc", []) + + 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, cc, bcc] |> Enum.concat() @@ -220,7 +233,7 @@ def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity {: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)