From 9c456018c25d8cde8579335e6f66e9ffc912c38f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Wed, 16 Mar 2022 16:51:09 +0100 Subject: [PATCH 1/5] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- lib/pleroma/user.ex | 33 +++++++++++++++ .../api_spec/operations/account_operation.ex | 42 +++++++++++++++++++ .../controllers/account_controller.ex | 31 +++++++++++++- .../web/mastodon_api/views/account_view.ex | 19 +++++++++ lib/pleroma/web/router.ex | 1 + 5 files changed, 125 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index c18364e6de..8445d9d1a5 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1325,6 +1325,39 @@ def get_friends_ids(%User{} = user, page \\ nil) do |> Repo.all() end + @spec get_familiar_followers_query(User.t(), User.t(), pos_integer() | nil) :: Ecto.Query.t() + def get_familiar_followers_query(%User{} = user, %User{} = current_user, nil) do + User.Query.build(%{is_active: true}) + |> where([u], u.id not in ^[user.id, current_user.id]) + |> join(:inner, [u], r in FollowingRelationship, + as: :followers_relationships, + on: r.following_id == ^user.id and r.follower_id == u.id + ) + |> join(:inner, [u], r in FollowingRelationship, + as: :following_relationships, + on: r.following_id == u.id and r.follower_id == ^current_user.id and not u.hide_follows + ) + |> where([followers_relationships: r], r.state == ^:follow_accept) + |> where([following_relationships: r], r.state == ^:follow_accept) + end + + def get_familiar_followers_query(%User{} = user, %User{} = current_user, page) do + user + |> get_familiar_followers_query(current_user, nil) + |> User.Query.paginate(page, 20) + end + + @spec get_familiar_followers_query(User.t(), User.t()) :: Ecto.Query.t() + def get_familiar_followers_query(%User{} = user, %User{} = current_user), + do: get_familiar_followers_query(user, current_user, nil) + + @spec get_familiar_followers(User.t(), User.t(), pos_integer() | nil) :: {:ok, list(User.t())} + def get_familiar_followers(%User{} = user, %User{} = current_user, page \\ nil) do + user + |> get_familiar_followers_query(current_user, page) + |> Repo.all() + end + def increase_note_count(%User{} = user) do User |> where(id: ^user.id) diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index b0680ee000..85c3fcc369 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -11,6 +11,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do alias Pleroma.Web.ApiSpec.Schemas.ActorType alias Pleroma.Web.ApiSpec.Schemas.ApiError alias Pleroma.Web.ApiSpec.Schemas.BooleanLike + alias Pleroma.Web.ApiSpec.Schemas.FlakeID alias Pleroma.Web.ApiSpec.Schemas.List alias Pleroma.Web.ApiSpec.Schemas.Status alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope @@ -485,6 +486,47 @@ def identity_proofs_operation do } end + def familiar_followers_operation do + %Operation{ + tags: ["Retrieve account information"], + summary: "Followers you know", + operationId: "AccountController.relationships", + description: "Returns followers of given account you know.", + security: [%{"oAuth" => ["read:follows"]}], + parameters: [ + Operation.parameter( + :id, + :query, + %Schema{ + oneOf: [%Schema{type: :array, items: %Schema{type: :string}}, %Schema{type: :string}] + }, + "Account IDs", + example: "123" + ) + ], + responses: %{ + 200 => + Operation.response("Accounts", "application/json", %Schema{ + title: "ArrayOfAccounts", + type: :array, + items: %Schema{ + title: "Account", + type: :object, + properties: %{ + id: FlakeID, + accounts: %Schema{ + title: "ArrayOfAccounts", + type: :array, + items: Account, + example: [Account.schema().example] + } + } + } + }) + } + } + end + defp create_request do %Schema{ title: "AccountCreateRequest", diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 09e182985e..2b629e4c2a 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -72,7 +72,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock] ) - plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships) + plug( + OAuthScopesPlug, + %{scopes: ["read:follows"]} when action in [:relationships, :familiar_followers] + ) plug( OAuthScopesPlug, @@ -544,6 +547,32 @@ def endorsements(%{assigns: %{user: user}} = conn, params) do ) end + @doc "GET /api/v1/accounts/familiar_followers" + def familiar_followers(%{assigns: %{user: user}} = conn, %{id: id}) do + users = + User.get_all_by_ids(List.wrap(id)) + |> Enum.map(&%{id: &1.id, accounts: get_familiar_followers(&1, user)}) + + conn + |> render("familiar_followers.json", + for: user, + users: users, + as: :user + ) + end + + defp get_familiar_followers(%{id: id} = user, %{id: id}) do + User.get_familiar_followers(user, user) + end + + defp get_familiar_followers(%{hide_followers: true}, _current_user) do + [] + end + + defp get_familiar_followers(user, current_user) do + User.get_familiar_followers(user, current_user) + end + @doc "GET /api/v1/identity_proofs" def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params) end diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 96f9d5ad8a..d369788ef2 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -193,6 +193,25 @@ def render("relationships.json", %{user: user, targets: targets} = opts) do render_many(targets, AccountView, "relationship.json", render_opts) end + def render("familiar_followers.json", %{users: users} = opts) do + opts = + opts + |> Map.merge(%{as: :user}) + |> Map.delete(:users) + + users + |> render_many(AccountView, "familiar_followers.json", opts) + end + + def render("familiar_followers.json", %{user: %{id: id, accounts: accounts}} = opts) do + accounts = + accounts + |> render_many(AccountView, "show.json", opts) + |> Enum.filter(&Enum.any?/1) + + %{id: id, accounts: accounts} + end + defp do_render("show.json", %{user: user} = opts) do user = User.sanitize_html(user, User.html_filter_policy(opts[:for])) display_name = user.name || user.nickname diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 8c7ef2401a..fee7699a42 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -510,6 +510,7 @@ defmodule Pleroma.Web.Router do patch("/accounts/update_credentials", AccountController, :update_credentials) get("/accounts/relationships", AccountController, :relationships) + get("/accounts/familiar_followers", AccountController, :familiar_followers) get("/accounts/:id/lists", AccountController, :lists) get("/accounts/:id/identity_proofs", AccountController, :identity_proofs) get("/endorsements", AccountController, :endorsements) From 72c68dd0407025b95ad69fbfe9d3c44bb26e5729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Tue, 22 Mar 2022 20:50:29 +0100 Subject: [PATCH 2/5] Add tests for familiar followers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- test/pleroma/user_test.exs | 14 ++++++ .../controllers/account_controller_test.exs | 49 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index 0fb93c2d4f..6a7c69ab62 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -2689,4 +2689,18 @@ test "should report error on non-existing alias" do assert user.ap_id in user3_updated.also_known_as end end + + describe "get_familiar_followers/3" do + test "returns familiar followers for a pair of users" do + user1 = insert(:user) + %{id: id2} = user2 = insert(:user) + user3 = insert(:user) + _user4 = insert(:user) + + User.follow(user1, user2) + User.follow(user2, user3) + + assert [%{id: ^id2}] = User.get_familiar_followers(user3, user1) + end + end end diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index 68cb0e7c2e..85d82daf63 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -1991,4 +1991,53 @@ test "max pinned accounts", %{user: user, conn: conn} do |> json_response_and_validate_schema(400) end end + + describe "familiar followers" do + setup do: oauth_access(["read:follows"]) + + test "fetch user familiar followers", %{user: user, conn: conn} do + other_user1 = insert(:user) + %{id: id2} = other_user2 = insert(:user) + _ = insert(:user) + + User.follow(user, other_user1) + User.follow(other_user1, other_user2) + + assert [%{"accounts" => [%{"id" => ^id1}], "id" => ^id2}] = + conn + |> put_req_header("content-type", "application/json") + |> get("/api/v1/accounts/familiar_followers?id[]=#{id2}") + |> json_response_and_validate_schema(200) + end + + test "returns empty array if followers are hidden", %{user: user, conn: conn} do + other_user1 = insert(:user, hide_follows: true) + %{id: id2} = other_user2 = insert(:user) + _ = insert(:user) + + User.follow(user, other_user1) + User.follow(other_user1, other_user2) + + assert [%{"accounts" => [], "id" => ^id2}] = + conn + |> put_req_header("content-type", "application/json") + |> get("/api/v1/accounts/familiar_followers?id[]=#{id2}") + |> json_response_and_validate_schema(200) + end + + test "it respects hide_followers", %{user: user, conn: conn} do + %{id: id1} = other_user1 = insert(:user) + %{id: id2} = other_user2 = insert(:user, hide_followers: true) + _ = insert(:user) + + User.follow(user, other_user1) + User.follow(other_user1, other_user2) + + assert [%{"accounts" => [], "id" => ^id2}] = + conn + |> put_req_header("content-type", "application/json") + |> get("/api/v1/accounts/familiar_followers?id[]=#{id2}") + |> json_response_and_validate_schema(200) + end + end end From ba8c734818cb0a889300a58485b3a8ca80adb6bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 9 Jul 2023 19:03:06 +0200 Subject: [PATCH 3/5] Familiar followers query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- lib/pleroma/user.ex | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index bb34a32c6b..7990033ff3 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1424,18 +1424,17 @@ def get_friends_ids(%User{} = user, page \\ nil) do @spec get_familiar_followers_query(User.t(), User.t(), pos_integer() | nil) :: Ecto.Query.t() def get_familiar_followers_query(%User{} = user, %User{} = current_user, nil) do + friends = get_friends_query(current_user) + |> select([u], u.id) + User.Query.build(%{is_active: true}) |> where([u], u.id not in ^[user.id, current_user.id]) |> join(:inner, [u], r in FollowingRelationship, as: :followers_relationships, on: r.following_id == ^user.id and r.follower_id == u.id ) - |> join(:inner, [u], r in FollowingRelationship, - as: :following_relationships, - on: r.following_id == u.id and r.follower_id == ^current_user.id and not u.hide_follows - ) |> where([followers_relationships: r], r.state == ^:follow_accept) - |> where([following_relationships: r], r.state == ^:follow_accept) + |> where([followers_relationships: r], r.follower_id in subquery(friends)) end def get_familiar_followers_query(%User{} = user, %User{} = current_user, page) do From 5f5e95ebcddab9973040ef2cdd8c89b7801f2c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 9 Jul 2023 19:10:19 +0200 Subject: [PATCH 4/5] Lint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- lib/pleroma/user.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 7990033ff3..0e06df8418 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1424,8 +1424,9 @@ def get_friends_ids(%User{} = user, page \\ nil) do @spec get_familiar_followers_query(User.t(), User.t(), pos_integer() | nil) :: Ecto.Query.t() def get_familiar_followers_query(%User{} = user, %User{} = current_user, nil) do - friends = get_friends_query(current_user) - |> select([u], u.id) + friends = + get_friends_query(current_user) + |> select([u], u.id) User.Query.build(%{is_active: true}) |> where([u], u.id not in ^[user.id, current_user.id]) From 4e0e5ce463e7ea73bc2d012c0d2a39fc8a183970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 9 Jul 2023 19:30:54 +0200 Subject: [PATCH 5/5] Fix tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- lib/pleroma/user.ex | 1 + .../web/mastodon_api/controllers/account_controller_test.exs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 0e06df8418..e663b41956 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1426,6 +1426,7 @@ def get_friends_ids(%User{} = user, page \\ nil) do def get_familiar_followers_query(%User{} = user, %User{} = current_user, nil) do friends = get_friends_query(current_user) + |> where([u], not u.hide_follows) |> select([u], u.id) User.Query.build(%{is_active: true}) diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index 750819f8ba..d57178a34c 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -2229,7 +2229,7 @@ test "max pinned accounts", %{user: user, conn: conn} do setup do: oauth_access(["read:follows"]) test "fetch user familiar followers", %{user: user, conn: conn} do - other_user1 = insert(:user) + %{id: id1} = other_user1 = insert(:user) %{id: id2} = other_user2 = insert(:user) _ = insert(:user) @@ -2259,7 +2259,7 @@ test "returns empty array if followers are hidden", %{user: user, conn: conn} do end test "it respects hide_followers", %{user: user, conn: conn} do - %{id: id1} = other_user1 = insert(:user) + other_user1 = insert(:user) %{id: id2} = other_user2 = insert(:user, hide_followers: true) _ = insert(:user)