diff --git a/changelog.d/outgoing-follow-requests.add b/changelog.d/outgoing-follow-requests.add
new file mode 100644
index 000000000..a898bcf6e
--- /dev/null
+++ b/changelog.d/outgoing-follow-requests.add
@@ -0,0 +1 @@
+Add /api/v1/pleroma/outgoing_follow_requests
diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex
index 495488dfd..653feb32f 100644
--- a/lib/pleroma/following_relationship.ex
+++ b/lib/pleroma/following_relationship.ex
@@ -157,6 +157,16 @@ defmodule Pleroma.FollowingRelationship do
|> Repo.all()
end
+ def get_outgoing_follow_requests(%User{id: id}) do
+ __MODULE__
+ |> join(:inner, [r], f in assoc(r, :following))
+ |> where([r], r.state == ^:follow_pending)
+ |> where([r], r.follower_id == ^id)
+ |> where([r, f], f.is_active == true)
+ |> select([r, f], f)
+ |> Repo.all()
+ end
+
def following?(%User{id: follower_id}, %User{id: followed_id}) do
__MODULE__
|> where(follower_id: ^follower_id, following_id: ^followed_id, state: ^:follow_accept)
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 7a36ece77..5586ac6b1 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -280,6 +280,7 @@ defmodule Pleroma.User do
defdelegate following?(follower, followed), to: FollowingRelationship
defdelegate following_ap_ids(user), to: FollowingRelationship
defdelegate get_follow_requests(user), to: FollowingRelationship
+ defdelegate get_outgoing_follow_requests(user), to: FollowingRelationship
defdelegate search(query, opts \\ []), to: User.Search
@doc """
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_follow_request_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_follow_request_operation.ex
new file mode 100644
index 000000000..ff0dbb8e8
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_follow_request_operation.ex
@@ -0,0 +1,31 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.PleromaFollowRequestOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.Account
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Follow requests"],
+ summary: "Retrieve outgoing follow requests",
+ security: [%{"oAuth" => ["read:follows", "follow"]}],
+ operationId: "PleromaFollowRequestController.index",
+ responses: %{
+ 200 =>
+ Operation.response("Array of Account", "application/json", %Schema{
+ type: :array,
+ items: Account,
+ example: [Account.schema().example]
+ })
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/follow_request_controller.ex b/lib/pleroma/web/pleroma_api/controllers/follow_request_controller.ex
new file mode 100644
index 000000000..8967c378f
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/follow_request_controller.ex
@@ -0,0 +1,25 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.FollowRequestController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.User
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ plug(OAuthScopesPlug, %{scopes: ["follow", "read:follows"]})
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaFollowRequestOperation
+
+ @doc "GET /api/v1/pleroma/outgoing_follow_requests"
+ def index(%{assigns: %{user: follower}} = conn, _params) do
+ follow_requests = User.get_outgoing_follow_requests(follower)
+
+ render(conn, "index.json", for: follower, users: follow_requests, as: :user)
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/views/follow_request_view.ex b/lib/pleroma/web/pleroma_api/views/follow_request_view.ex
new file mode 100644
index 000000000..17edd1bcc
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/follow_request_view.ex
@@ -0,0 +1,10 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.FollowRequestView do
+ use Pleroma.Web, :view
+ alias Pleroma.Web.MastodonAPI
+
+ def render(view, opts), do: MastodonAPI.AccountView.render(view, opts)
+end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 0423ca9e2..d1088a808 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -602,6 +602,8 @@ defmodule Pleroma.Web.Router do
post("/bookmark_folders", BookmarkFolderController, :create)
patch("/bookmark_folders/:id", BookmarkFolderController, :update)
delete("/bookmark_folders/:id", BookmarkFolderController, :delete)
+
+ get("/outgoing_follow_requests", FollowRequestController, :index)
end
scope [] do
diff --git a/test/pleroma/web/pleroma_api/controllers/follow_request_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/follow_request_controller_test.exs
new file mode 100644
index 000000000..46109e35e
--- /dev/null
+++ b/test/pleroma/web/pleroma_api/controllers/follow_request_controller_test.exs
@@ -0,0 +1,27 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.FollowRequestControllerTest do
+ use Pleroma.Web.ConnCase, async: true
+
+ alias Pleroma.Web.CommonAPI
+
+ import Pleroma.Factory
+
+ test "/api/v1/pleroma/outgoing_follow_requests works" do
+ %{conn: conn, user: user} = oauth_access(["read:follows"])
+
+ other_user1 = insert(:user)
+ other_user2 = insert(:user, is_locked: true)
+ _other_user3 = insert(:user)
+
+ {:ok, _, _, _} = CommonAPI.follow(other_user1, user)
+ {:ok, _, _, _} = CommonAPI.follow(other_user2, user)
+
+ conn = get(conn, "/api/v1/pleroma/outgoing_follow_requests")
+
+ assert [relationship] = json_response_and_validate_schema(conn, 200)
+ assert to_string(other_user2.id) == relationship["id"]
+ end
+end