diff --git a/changelog.d/exclusive-lists.add b/changelog.d/exclusive-lists.add new file mode 100644 index 000000000..bbd722f07 --- /dev/null +++ b/changelog.d/exclusive-lists.add @@ -0,0 +1 @@ +Support lists `exclusive` param diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex index b446b91a0..cd23bd0a9 100644 --- a/lib/pleroma/list.ex +++ b/lib/pleroma/list.ex @@ -17,13 +17,14 @@ defmodule Pleroma.List do field(:title, :string) field(:following, {:array, :string}, default: []) field(:ap_id, :string) + field(:exclusive, :boolean, default: false) timestamps() end - def title_changeset(list, attrs \\ %{}) do + def update_changeset(list, attrs \\ %{}) do list - |> cast(attrs, [:title]) + |> cast(attrs, [:title, :exclusive]) |> validate_required([:title]) end @@ -91,14 +92,14 @@ defmodule Pleroma.List do |> Repo.all() end - def rename(%Pleroma.List{} = list, title) do + def update(%Pleroma.List{} = list, params) do list - |> title_changeset(%{title: title}) + |> update_changeset(params) |> Repo.update() end - def create(title, %User{} = creator) do - changeset = title_changeset(%Pleroma.List{user_id: creator.id}, %{title: title}) + def create(params, %User{} = creator) do + changeset = update_changeset(%Pleroma.List{user_id: creator.id}, params) if changeset.valid? do Repo.transaction(fn -> @@ -149,4 +150,14 @@ defmodule Pleroma.List do end def member?(_, _), do: false + + def get_exclusive_list_members(%User{id: user_id}) do + Pleroma.List + |> where([l], l.user_id == ^user_id) + |> where([l], l.exclusive == true) + |> select([l], l.following) + |> Repo.all() + |> List.flatten() + |> Enum.uniq() + end end diff --git a/lib/pleroma/web/api_spec/operations/list_operation.ex b/lib/pleroma/web/api_spec/operations/list_operation.ex index 7d876ae2d..d2e803178 100644 --- a/lib/pleroma/web/api_spec/operations/list_operation.ex +++ b/lib/pleroma/web/api_spec/operations/list_operation.ex @@ -36,7 +36,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do summary: "Create a list", description: "Fetch the list with the given ID. Used for verifying the title of a list.", operationId: "ListController.create", - requestBody: create_update_request(), + requestBody: create_request(), security: [%{"oAuth" => ["write:lists"]}], responses: %{ 200 => Operation.response("List", "application/json", List), @@ -68,7 +68,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do description: "Change the title of a list", operationId: "ListController.update", parameters: [id_param()], - requestBody: create_update_request(), + requestBody: update_request(), security: [%{"oAuth" => ["write:lists"]}], responses: %{ 200 => Operation.response("List", "application/json", List), @@ -164,14 +164,15 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do ) end - defp create_update_request do + defp create_request do request_body( "Parameters", %Schema{ - description: "POST body for creating or updating a List", + description: "POST body for creating a List", type: :object, properties: %{ - title: %Schema{type: :string, description: "List title"} + title: %Schema{type: :string, description: "List title"}, + exclusive: %Schema{type: :boolean, description: "Whether members of the list should be removed from the “Home” feed"} }, required: [:title] }, @@ -179,6 +180,21 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do ) end + defp update_request do + request_body( + "Parameters", + %Schema{ + description: "PUT body for updating a List", + type: :object, + properties: %{ + title: %Schema{type: :string, description: "List title"}, + exclusive: %Schema{type: :boolean, description: "Whether members of the list should be removed from the “Home” feed"} + } + }, + required: true + ) + end + defp add_remove_accounts_request(required) when is_boolean(required) do request_body( "Parameters", diff --git a/lib/pleroma/web/api_spec/schemas/list.ex b/lib/pleroma/web/api_spec/schemas/list.ex index e57de7917..5df674894 100644 --- a/lib/pleroma/web/api_spec/schemas/list.ex +++ b/lib/pleroma/web/api_spec/schemas/list.ex @@ -13,7 +13,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.List do type: :object, properties: %{ id: %Schema{type: :string, description: "The internal database ID of the list"}, - title: %Schema{type: :string, description: "The user-defined title of the list"} + title: %Schema{type: :string, description: "The user-defined title of the list"}, + exclusive: %Schema{ + type: :boolean, + description: "Whether members of the list should be removed from the “Home” feed" + } }, example: %{ "id" => "12249", diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex index 3bfc365a5..048012ae6 100644 --- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex @@ -28,27 +28,27 @@ defmodule Pleroma.Web.MastodonAPI.ListController do # POST /api/v1/lists def create( - %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: %{title: title}}}} = + %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: params}}} = conn, _ ) do - with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do + with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(params, user) do render(conn, "show.json", list: list) end end - # GET /api/v1/lists/:idOB + # GET /api/v1/lists/:id def show(%{assigns: %{list: list}} = conn, _) do render(conn, "show.json", list: list) end # PUT /api/v1/lists/:id def update( - %{assigns: %{list: list}, private: %{open_api_spex: %{body_params: %{title: title}}}} = + %{assigns: %{list: list}, private: %{open_api_spex: %{body_params: params}}} = conn, _ ) do - with {:ok, list} <- Pleroma.List.rename(list, title) do + with {:ok, list} <- Pleroma.List.update(list, params) do render(conn, "show.json", list: list) end end diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 5ee74a80e..99a5b6957 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -45,6 +45,10 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do |> User.followed_hashtags() |> Enum.map(& &1.id) + excluded_list_members = + user + |> Pleroma.List.get_exclusive_list_members() + params = params |> Map.put(:type, ["Create", "Announce"]) @@ -58,7 +62,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do |> Map.delete(:local) activities = - [user.ap_id | User.following(user)] + [user.ap_id | User.following(user) -- excluded_list_members] |> ActivityPub.fetch_activities(params) |> Enum.reverse() diff --git a/lib/pleroma/web/mastodon_api/views/list_view.ex b/lib/pleroma/web/mastodon_api/views/list_view.ex index a7ae7c5f7..740f458d4 100644 --- a/lib/pleroma/web/mastodon_api/views/list_view.ex +++ b/lib/pleroma/web/mastodon_api/views/list_view.ex @@ -13,7 +13,8 @@ defmodule Pleroma.Web.MastodonAPI.ListView do def render("show.json", %{list: list}) do %{ id: to_string(list.id), - title: list.title + title: list.title, + exclusive: list.exclusive } end end diff --git a/priv/repo/migrations/20260218000000_add_exclusive_to_lists.exs b/priv/repo/migrations/20260218000000_add_exclusive_to_lists.exs new file mode 100644 index 000000000..253a8acb7 --- /dev/null +++ b/priv/repo/migrations/20260218000000_add_exclusive_to_lists.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.AddExclusiveToLists do + use Ecto.Migration + + def change do + alter table(:lists) do + add(:exclusive, :boolean, default: false) + end + end +end diff --git a/test/pleroma/list_test.exs b/test/pleroma/list_test.exs index a68146b0d..d44310689 100644 --- a/test/pleroma/list_test.exs +++ b/test/pleroma/list_test.exs @@ -10,22 +10,23 @@ defmodule Pleroma.ListTest do test "creating a list" do user = insert(:user) - {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user) - %Pleroma.List{title: title} = Pleroma.List.get(list.id, user) + {:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title"}, user) + %Pleroma.List{title: title, exclusive: exclusive} = Pleroma.List.get(list.id, user) assert title == "title" + assert exclusive == false end test "validates title" do user = insert(:user) - assert {:error, changeset} = Pleroma.List.create("", user) + assert {:error, changeset} = Pleroma.List.create(%{title: ""}, user) assert changeset.errors == [title: {"can't be blank", [validation: :required]}] end test "getting a list not belonging to the user" do user = insert(:user) other_user = insert(:user) - {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user) + {:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title"}, user) ret = Pleroma.List.get(list.id, other_user) assert is_nil(ret) end @@ -33,7 +34,7 @@ defmodule Pleroma.ListTest do test "adding an user to a list" do user = insert(:user) other_user = insert(:user) - {:ok, list} = Pleroma.List.create("title", user) + {:ok, list} = Pleroma.List.create(%{title: "title"}, user) {:ok, %{following: following}} = Pleroma.List.follow(list, other_user) assert [other_user.follower_address] == following end @@ -41,7 +42,7 @@ defmodule Pleroma.ListTest do test "removing an user from a list" do user = insert(:user) other_user = insert(:user) - {:ok, list} = Pleroma.List.create("title", user) + {:ok, list} = Pleroma.List.create(%{title: "title"}, user) {:ok, %{following: _following}} = Pleroma.List.follow(list, other_user) {:ok, %{following: following}} = Pleroma.List.unfollow(list, other_user) assert [] == following @@ -49,14 +50,27 @@ defmodule Pleroma.ListTest do test "renaming a list" do user = insert(:user) - {:ok, list} = Pleroma.List.create("title", user) - {:ok, %{title: title}} = Pleroma.List.rename(list, "new") + {:ok, list} = Pleroma.List.create(%{title: "title"}, user) + {:ok, %{title: title}} = Pleroma.List.update(list, %{title: "new"}) assert "new" == title end + test "updating a list exclusivity" do + user = insert(:user) + + {:ok, %{exclusive: exclusive} = list} = + Pleroma.List.create(%{title: "title", exclusive: true}, user) + + assert exclusive == true + {:ok, %{exclusive: exclusive} = list} = Pleroma.List.update(list, %{exclusive: false}) + assert exclusive == false + {:ok, %{exclusive: exclusive}} = Pleroma.List.update(list, %{exclusive: true}) + assert exclusive == true + end + test "deleting a list" do user = insert(:user) - {:ok, list} = Pleroma.List.create("title", user) + {:ok, list} = Pleroma.List.create(%{title: "title"}, user) {:ok, list} = Pleroma.List.delete(list) assert is_nil(Repo.get(Pleroma.List, list.id)) end @@ -65,7 +79,7 @@ defmodule Pleroma.ListTest do user = insert(:user) other_user = insert(:user) third_user = insert(:user) - {:ok, list} = Pleroma.List.create("title", user) + {:ok, list} = Pleroma.List.create(%{title: "title"}, user) {:ok, list} = Pleroma.List.follow(list, other_user) {:ok, list} = Pleroma.List.follow(list, third_user) {:ok, following} = Pleroma.List.get_following(list) @@ -76,9 +90,9 @@ defmodule Pleroma.ListTest do test "getting all lists by an user" do user = insert(:user) other_user = insert(:user) - {:ok, list_one} = Pleroma.List.create("title", user) - {:ok, list_two} = Pleroma.List.create("other title", user) - {:ok, list_three} = Pleroma.List.create("third title", other_user) + {:ok, list_one} = Pleroma.List.create(%{title: "title"}, user) + {:ok, list_two} = Pleroma.List.create(%{title: "other title"}, user) + {:ok, list_three} = Pleroma.List.create(%{title: "third title"}, other_user) lists = Pleroma.List.for_user(user, %{}) assert list_one in lists assert list_two in lists @@ -88,9 +102,9 @@ defmodule Pleroma.ListTest do test "getting all lists the user is a member of" do user = insert(:user) other_user = insert(:user) - {:ok, list_one} = Pleroma.List.create("title", user) - {:ok, list_two} = Pleroma.List.create("other title", user) - {:ok, list_three} = Pleroma.List.create("third title", other_user) + {:ok, list_one} = Pleroma.List.create(%{title: "title"}, user) + {:ok, list_two} = Pleroma.List.create(%{title: "other title"}, user) + {:ok, list_three} = Pleroma.List.create(%{title: "third title"}, other_user) {:ok, list_one} = Pleroma.List.follow(list_one, other_user) {:ok, list_two} = Pleroma.List.follow(list_two, other_user) {:ok, list_three} = Pleroma.List.follow(list_three, user) @@ -106,8 +120,8 @@ defmodule Pleroma.ListTest do not_owner = insert(:user) member_1 = insert(:user) member_2 = insert(:user) - {:ok, owned_list} = Pleroma.List.create("owned", owner) - {:ok, not_owned_list} = Pleroma.List.create("not owned", not_owner) + {:ok, owned_list} = Pleroma.List.create(%{title: "owned"}, owner) + {:ok, not_owned_list} = Pleroma.List.create(%{title: "not owned"}, not_owner) {:ok, owned_list} = Pleroma.List.follow(owned_list, member_1) {:ok, owned_list} = Pleroma.List.follow(owned_list, member_2) {:ok, not_owned_list} = Pleroma.List.follow(not_owned_list, member_1) @@ -123,14 +137,14 @@ defmodule Pleroma.ListTest do test "get by ap_id" do user = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.create(%{title: "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.create(%{title: "foo"}, user) {:ok, list} = Pleroma.List.follow(list, member) assert Pleroma.List.memberships(member) == [list.ap_id] @@ -140,7 +154,7 @@ defmodule Pleroma.ListTest do user = insert(:user) member = insert(:user) - {:ok, list} = Pleroma.List.create("foo", user) + {:ok, list} = Pleroma.List.create(%{title: "foo"}, user) {:ok, list} = Pleroma.List.follow(list, member) assert Pleroma.List.member?(list, member) diff --git a/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs index 430b8b89d..bfda48be4 100644 --- a/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/list_controller_test.exs @@ -56,7 +56,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do %{user: user, conn: conn} = oauth_access(["write:lists"]) other_user = insert(:user) third_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) assert %{} == conn @@ -77,7 +77,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do other_user = insert(:user) third_user = insert(:user) fourth_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) {:ok, list} = Pleroma.List.follow(list, other_user) {:ok, list} = Pleroma.List.follow(list, third_user) {:ok, list} = Pleroma.List.follow(list, fourth_user) @@ -98,7 +98,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do %{user: user, conn: conn} = oauth_access(["write:lists"]) other_user = insert(:user) third_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) {:ok, list} = Pleroma.List.follow(list, other_user) {:ok, list} = Pleroma.List.follow(list, third_user) @@ -115,7 +115,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do test "listing users in a list" do %{user: user, conn: conn} = oauth_access(["read:lists"]) other_user = insert(:user) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) {:ok, list} = Pleroma.List.follow(list, other_user) conn = @@ -129,7 +129,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do test "retrieving a list" do %{user: user, conn: conn} = oauth_access(["read:lists"]) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) conn = conn @@ -150,7 +150,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do test "renaming a list" do %{user: user, conn: conn} = oauth_access(["write:lists"]) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) assert %{"title" => "newname"} = conn @@ -161,7 +161,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do test "validates title when renaming a list" do %{user: user, conn: conn} = oauth_access(["write:lists"]) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) conn = conn @@ -175,7 +175,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do test "deleting a list" do %{user: user, conn: conn} = oauth_access(["write:lists"]) - {:ok, list} = Pleroma.List.create("name", user) + {:ok, list} = Pleroma.List.create(%{title: "name"}, user) conn = delete(conn, "/api/v1/lists/#{list.id}") 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 4d646509c..9808652d2 100644 --- a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs @@ -149,6 +149,31 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do |> get("/api/v1/timelines/home?remote=true&local=true") |> json_response_and_validate_schema(200) == [] end + + test "the home timeline excludes posts from users in exclusive lists", %{ + user: user, + conn: conn + } do + other_user1 = insert(:user) + other_user2 = insert(:user) + + {:ok, user, other_user1} = User.follow(user, other_user1) + {:ok, user, other_user2} = User.follow(user, other_user2) + + {:ok, list} = Pleroma.List.create(%{title: "foo", exclusive: true}, user) + {:ok, _list} = Pleroma.List.follow(list, other_user1) + + {:ok, _activity} = CommonAPI.post(other_user1, %{status: "hi"}) + {:ok, %{id: activity2_id}} = CommonAPI.post(other_user2, %{status: "hi too"}) + + response = + conn + |> assign(:user, user) + |> get("/api/v1/timelines/home") + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^activity2_id}] = response + end end describe "public" do