From 1ee3ba5fd3d553658aef38ae290b0f711f75148d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Thu, 16 Nov 2023 01:18:09 +0100 Subject: [PATCH] Optionally filter local timelines according to domain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- config/description.exs | 5 +++ lib/pleroma/web/activity_pub/activity_pub.ex | 19 +++++++++ .../controllers/timeline_controller.ex | 18 ++++++++ .../20230618190919_create_domains.exs | 2 + .../controllers/timeline_controller_test.exs | 42 +++++++++++++++++++ 5 files changed, 86 insertions(+) diff --git a/config/description.exs b/config/description.exs index 1cd4d2d24f..cea74a9b3b 100644 --- a/config/description.exs +++ b/config/description.exs @@ -1071,6 +1071,11 @@ key: :enabled, type: :boolean, description: "Enables allowing multiple Webfinger domains" + }, + %{ + key: :separate_timelines, + type: :boolean, + description: "Only display posts from own domain on local timeline" } ] } diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 32d1a1037a..2b04d2dcfb 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -926,6 +926,24 @@ defp restrict_local(query, %{local_only: true}) do defp restrict_local(query, _), do: query + defp restrict_domain(query, %{domain_id: 0}) do + query + |> join(:inner, [activity], u in User, + as: :domain_user, + on: activity.actor == u.ap_id and is_nil(u.domain_id) + ) + end + + defp restrict_domain(query, %{domain_id: domain_id}) do + query + |> join(:inner, [activity], u in User, + as: :domain_user, + on: activity.actor == u.ap_id and u.domain_id == ^domain_id + ) + end + + defp restrict_domain(query, _), do: query + defp restrict_remote(query, %{remote: true}) do from(activity in query, where: activity.local == false) end @@ -1404,6 +1422,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_replies(opts) |> restrict_since(opts) |> restrict_local(opts) + |> restrict_domain(opts) |> restrict_remote(opts) |> restrict_actor(opts) |> restrict_type(opts) diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 293c61b41c..599f2742ba 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -50,6 +50,7 @@ def home(%{assigns: %{user: user}} = conn, params) do |> Map.put(:user, user) |> Map.put(:local_only, params[:local]) |> Map.delete(:local) + |> ActivityPub.fetch_public_activities() activities = [user.ap_id | User.following(user)] @@ -114,6 +115,7 @@ def public(%{assigns: %{user: user}} = conn, params) do |> Map.put(:instance, params[:instance]) # Restricts unfederated content to authenticated users |> Map.put(:includes_local_public, not is_nil(user)) + |> maybe_put_domain_id(user) |> ActivityPub.fetch_public_activities() conn @@ -150,6 +152,7 @@ defp hashtag_fetching(params, user, local_only) do |> Map.put(:tag, tags_any) |> Map.put(:tag_all, tag_all) |> Map.put(:tag_reject, tag_reject) + |> maybe_put_domain_id(user) |> ActivityPub.fetch_public_activities() end @@ -183,6 +186,7 @@ def list(%{assigns: %{user: user}} = conn, %{list_id: id} = params) do |> Map.put(:user, user) |> Map.put(:muting_user, user) |> Map.put(:local_only, params[:local]) + |> ActivityPub.fetch_public_activities() # we must filter the following list for the user to avoid leaking statuses the user # does not actually have permission to see (for more info, peruse security issue #270). @@ -207,4 +211,18 @@ def list(%{assigns: %{user: user}} = conn, %{list_id: id} = params) do _e -> render_error(conn, :forbidden, "Error.") end end + + defp maybe_put_domain_id(%{local_only: true} = params, user) do + separate_timelines = Config.get([:instance, :multitenancy, :separate_timelines]) + domain_id = if(user, do: user.domain_id || 0, else: 0) + + if separate_timelines do + params + |> Map.put(:domain_id, domain_id) + else + params + end + end + + defp maybe_put_domain_id(params, _user), do: params end diff --git a/priv/repo/migrations/20230618190919_create_domains.exs b/priv/repo/migrations/20230618190919_create_domains.exs index 78a0ade589..9994a46829 100644 --- a/priv/repo/migrations/20230618190919_create_domains.exs +++ b/priv/repo/migrations/20230618190919_create_domains.exs @@ -16,5 +16,7 @@ def change do alter table(:users) do add(:domain_id, references(:domains)) end + + create_if_not_exists(index(:users, [:domain_id])) end end diff --git a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs index c120dd53c2..8cb0543475 100644 --- a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs @@ -408,6 +408,48 @@ test "should not return local-only posts for anonymous users" do assert [] = result end + + test "filtering local posts basing on domain", %{conn: conn} do + clear_config([:instance, :multitenancy], %{separate_timelines: false}) + + {:ok, domain} = Pleroma.Domain.create(%{domain: "pleroma.example.org"}) + + user1 = insert(:user) + + user2 = insert(:user, %{domain_id: domain.id}) + + %{id: note1} = insert(:note_activity, user: user1) + %{id: note2} = insert(:note_activity, user: user2) + + assert [ + %{"id" => ^note2}, + %{"id" => ^note1} + ] = + conn + |> get("/api/v1/timelines/public?local=true") + |> json_response_and_validate_schema(200) + + clear_config([:instance, :multitenancy], %{separate_timelines: true}) + + assert [%{"id" => ^note1}] = + conn + |> get("/api/v1/timelines/public?local=true") + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^note1}] = + conn + |> assign(:user, user1) + |> assign(:token, insert(:oauth_token, user: user1, scopes: ["read:statuses"])) + |> get("/api/v1/timelines/public?local=true") + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^note2}] = + conn + |> assign(:user, user2) + |> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"])) + |> get("/api/v1/timelines/public?local=true") + |> json_response_and_validate_schema(200) + end end defp local_and_remote_activities do