Merge branch 'openapi/search' into 'develop'

Add OpenAPI spec for SearchController

See merge request pleroma/pleroma!2484
This commit is contained in:
lain 2020-05-07 13:38:34 +00:00
commit 516ff945ce
6 changed files with 293 additions and 60 deletions

View File

@ -556,11 +556,12 @@ defp update_creadentials_request do
} }
end end
defp array_of_accounts do def array_of_accounts do
%Schema{ %Schema{
title: "ArrayOfAccounts", title: "ArrayOfAccounts",
type: :array, type: :array,
items: Account items: Account,
example: [Account.schema().example]
} }
end end

View File

@ -0,0 +1,207 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.SearchOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.AccountOperation
alias Pleroma.Web.ApiSpec.Schemas.Account
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.Status
alias Pleroma.Web.ApiSpec.Schemas.Tag
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def account_search_operation do
%Operation{
tags: ["Search"],
summary: "Search for matching accounts by username or display name",
operationId: "SearchController.account_search",
parameters: [
Operation.parameter(:q, :query, %Schema{type: :string}, "What to search for",
required: true
),
Operation.parameter(
:limit,
:query,
%Schema{type: :integer, default: 40},
"Maximum number of results"
),
Operation.parameter(
:resolve,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Attempt WebFinger lookup. Use this when `q` is an exact address."
),
Operation.parameter(
:following,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Only include accounts that the user is following"
)
],
responses: %{
200 =>
Operation.response(
"Array of Account",
"application/json",
AccountOperation.array_of_accounts()
)
}
}
end
def search_operation do
%Operation{
tags: ["Search"],
summary: "Search results",
security: [%{"oAuth" => ["read:search"]}],
operationId: "SearchController.search",
deprecated: true,
parameters: [
Operation.parameter(
:account_id,
:query,
FlakeID,
"If provided, statuses returned will be authored only by this account"
),
Operation.parameter(
:type,
:query,
%Schema{type: :string, enum: ["accounts", "hashtags", "statuses"]},
"Search type"
),
Operation.parameter(:q, :query, %Schema{type: :string}, "The search query", required: true),
Operation.parameter(
:resolve,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Attempt WebFinger lookup"
),
Operation.parameter(
:following,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Only include accounts that the user is following"
),
Operation.parameter(
:offset,
:query,
%Schema{type: :integer},
"Offset"
)
| pagination_params()
],
responses: %{
200 => Operation.response("Results", "application/json", results())
}
}
end
def search2_operation do
%Operation{
tags: ["Search"],
summary: "Search results",
security: [%{"oAuth" => ["read:search"]}],
operationId: "SearchController.search2",
parameters: [
Operation.parameter(
:account_id,
:query,
FlakeID,
"If provided, statuses returned will be authored only by this account"
),
Operation.parameter(
:type,
:query,
%Schema{type: :string, enum: ["accounts", "hashtags", "statuses"]},
"Search type"
),
Operation.parameter(:q, :query, %Schema{type: :string}, "What to search for",
required: true
),
Operation.parameter(
:resolve,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Attempt WebFinger lookup"
),
Operation.parameter(
:following,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Only include accounts that the user is following"
)
| pagination_params()
],
responses: %{
200 => Operation.response("Results", "application/json", results2())
}
}
end
defp results2 do
%Schema{
title: "SearchResults",
type: :object,
properties: %{
accounts: %Schema{
type: :array,
items: Account,
description: "Accounts which match the given query"
},
statuses: %Schema{
type: :array,
items: Status,
description: "Statuses which match the given query"
},
hashtags: %Schema{
type: :array,
items: Tag,
description: "Hashtags which match the given query"
}
},
example: %{
"accounts" => [Account.schema().example],
"statuses" => [Status.schema().example],
"hashtags" => [Tag.schema().example]
}
}
end
defp results do
%Schema{
title: "SearchResults",
type: :object,
properties: %{
accounts: %Schema{
type: :array,
items: Account,
description: "Accounts which match the given query"
},
statuses: %Schema{
type: :array,
items: Status,
description: "Statuses which match the given query"
},
hashtags: %Schema{
type: :array,
items: %Schema{type: :string},
description: "Hashtags which match the given query"
}
},
example: %{
"accounts" => [Account.schema().example],
"statuses" => [Status.schema().example],
"hashtags" => ["cofe"]
}
}
end
end

View File

@ -9,6 +9,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
alias Pleroma.Web.ApiSpec.Schemas.Emoji alias Pleroma.Web.ApiSpec.Schemas.Emoji
alias Pleroma.Web.ApiSpec.Schemas.FlakeID alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.Poll alias Pleroma.Web.ApiSpec.Schemas.Poll
alias Pleroma.Web.ApiSpec.Schemas.Tag
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
require OpenApiSpex require OpenApiSpex
@ -106,16 +107,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
replies_count: %Schema{type: :integer}, replies_count: %Schema{type: :integer},
sensitive: %Schema{type: :boolean}, sensitive: %Schema{type: :boolean},
spoiler_text: %Schema{type: :string}, spoiler_text: %Schema{type: :string},
tags: %Schema{ tags: %Schema{type: :array, items: Tag},
type: :array,
items: %Schema{
type: :object,
properties: %{
name: %Schema{type: :string},
url: %Schema{type: :string, format: :uri}
}
}
},
uri: %Schema{type: :string, format: :uri}, uri: %Schema{type: :string, format: :uri},
url: %Schema{type: :string, nullable: true, format: :uri}, url: %Schema{type: :string, nullable: true, format: :uri},
visibility: VisibilityScope visibility: VisibilityScope

View File

@ -0,0 +1,27 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.Tag do
alias OpenApiSpex.Schema
require OpenApiSpex
OpenApiSpex.schema(%{
title: "Tag",
description: "Represents a hashtag used within the content of a status",
type: :object,
properties: %{
name: %Schema{type: :string, description: "The value of the hashtag after the # sign"},
url: %Schema{
type: :string,
format: :uri,
description: "A link to the hashtag on the instance"
}
},
example: %{
name: "cofe",
url: "https://lain.com/tag/cofe"
}
})
end

View File

@ -5,7 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.SearchController do defmodule Pleroma.Web.MastodonAPI.SearchController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [fetch_integer_param: 2, skip_relationships?: 1] import Pleroma.Web.ControllerHelper, only: [skip_relationships?: 1]
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
@ -18,6 +18,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
require Logger require Logger
plug(Pleroma.Web.ApiSpec.CastAndValidate)
# Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search) # Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)
plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated}) plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated})
@ -25,7 +27,9 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search]) plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search])
def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.SearchOperation
def account_search(%{assigns: %{user: user}} = conn, %{q: query} = params) do
accounts = User.search(query, search_options(params, user)) accounts = User.search(query, search_options(params, user))
conn conn
@ -36,7 +40,7 @@ def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) d
def search2(conn, params), do: do_search(:v2, conn, params) def search2(conn, params), do: do_search(:v2, conn, params)
def search(conn, params), do: do_search(:v1, conn, params) def search(conn, params), do: do_search(:v1, conn, params)
defp do_search(version, %{assigns: %{user: user}} = conn, %{"q" => query} = params) do defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do
options = search_options(params, user) options = search_options(params, user)
timeout = Keyword.get(Repo.config(), :timeout, 15_000) timeout = Keyword.get(Repo.config(), :timeout, 15_000)
default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []} default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []}
@ -44,7 +48,7 @@ defp do_search(version, %{assigns: %{user: user}} = conn, %{"q" => query} = para
result = result =
default_values default_values
|> Enum.map(fn {resource, default_value} -> |> Enum.map(fn {resource, default_value} ->
if params["type"] in [nil, resource] do if params[:type] in [nil, resource] do
{resource, fn -> resource_search(version, resource, query, options) end} {resource, fn -> resource_search(version, resource, query, options) end}
else else
{resource, fn -> default_value end} {resource, fn -> default_value end}
@ -68,11 +72,11 @@ defp do_search(version, %{assigns: %{user: user}} = conn, %{"q" => query} = para
defp search_options(params, user) do defp search_options(params, user) do
[ [
skip_relationships: skip_relationships?(params), skip_relationships: skip_relationships?(params),
resolve: params["resolve"] == "true", resolve: params[:resolve],
following: params["following"] == "true", following: params[:following],
limit: fetch_integer_param(params, "limit"), limit: params[:limit],
offset: fetch_integer_param(params, "offset"), offset: params[:offset],
type: params["type"], type: params[:type],
author: get_author(params), author: get_author(params),
for_user: user for_user: user
] ]
@ -135,7 +139,7 @@ defp with_fallback(f, fallback \\ []) do
end end
end end
defp get_author(%{"account_id" => account_id}) when is_binary(account_id), defp get_author(%{account_id: account_id}) when is_binary(account_id),
do: User.get_cached_by_id(account_id) do: User.get_cached_by_id(account_id)
defp get_author(_params), do: nil defp get_author(_params), do: nil

View File

@ -27,8 +27,8 @@ test "it returns empty result if user or status search return undefined error",
capture_log(fn -> capture_log(fn ->
results = results =
conn conn
|> get("/api/v2/search", %{"q" => "2hu"}) |> get("/api/v2/search?q=2hu")
|> json_response(200) |> json_response_and_validate_schema(200)
assert results["accounts"] == [] assert results["accounts"] == []
assert results["statuses"] == [] assert results["statuses"] == []
@ -54,8 +54,8 @@ test "search", %{conn: conn} do
results = results =
conn conn
|> get("/api/v2/search", %{"q" => "2hu #private"}) |> get("/api/v2/search?#{URI.encode_query(%{q: "2hu #private"})}")
|> json_response(200) |> json_response_and_validate_schema(200)
[account | _] = results["accounts"] [account | _] = results["accounts"]
assert account["id"] == to_string(user_three.id) assert account["id"] == to_string(user_three.id)
@ -68,8 +68,8 @@ test "search", %{conn: conn} do
assert status["id"] == to_string(activity.id) assert status["id"] == to_string(activity.id)
results = results =
get(conn, "/api/v2/search", %{"q" => "天子"}) get(conn, "/api/v2/search?q=天子")
|> json_response(200) |> json_response_and_validate_schema(200)
[status] = results["statuses"] [status] = results["statuses"]
assert status["id"] == to_string(activity.id) assert status["id"] == to_string(activity.id)
@ -89,8 +89,8 @@ test "excludes a blocked users from search results", %{conn: conn} do
conn conn
|> assign(:user, user) |> assign(:user, user)
|> assign(:token, insert(:oauth_token, user: user, scopes: ["read"])) |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"]))
|> get("/api/v2/search", %{"q" => "Agent"}) |> get("/api/v2/search?q=Agent")
|> json_response(200) |> json_response_and_validate_schema(200)
status_ids = Enum.map(results["statuses"], fn g -> g["id"] end) status_ids = Enum.map(results["statuses"], fn g -> g["id"] end)
@ -107,8 +107,8 @@ test "account search", %{conn: conn} do
results = results =
conn conn
|> get("/api/v1/accounts/search", %{"q" => "shp"}) |> get("/api/v1/accounts/search?q=shp")
|> json_response(200) |> json_response_and_validate_schema(200)
result_ids = for result <- results, do: result["acct"] result_ids = for result <- results, do: result["acct"]
@ -117,8 +117,8 @@ test "account search", %{conn: conn} do
results = results =
conn conn
|> get("/api/v1/accounts/search", %{"q" => "2hu"}) |> get("/api/v1/accounts/search?q=2hu")
|> json_response(200) |> json_response_and_validate_schema(200)
result_ids = for result <- results, do: result["acct"] result_ids = for result <- results, do: result["acct"]
@ -130,8 +130,8 @@ test "returns account if query contains a space", %{conn: conn} do
results = results =
conn conn
|> get("/api/v1/accounts/search", %{"q" => "shp@shitposter.club xxx "}) |> get("/api/v1/accounts/search?q=shp@shitposter.club xxx")
|> json_response(200) |> json_response_and_validate_schema(200)
assert length(results) == 1 assert length(results) == 1
end end
@ -146,8 +146,8 @@ test "it returns empty result if user or status search return undefined error",
capture_log(fn -> capture_log(fn ->
results = results =
conn conn
|> get("/api/v1/search", %{"q" => "2hu"}) |> get("/api/v1/search?q=2hu")
|> json_response(200) |> json_response_and_validate_schema(200)
assert results["accounts"] == [] assert results["accounts"] == []
assert results["statuses"] == [] assert results["statuses"] == []
@ -173,8 +173,8 @@ test "search", %{conn: conn} do
results = results =
conn conn
|> get("/api/v1/search", %{"q" => "2hu"}) |> get("/api/v1/search?q=2hu")
|> json_response(200) |> json_response_and_validate_schema(200)
[account | _] = results["accounts"] [account | _] = results["accounts"]
assert account["id"] == to_string(user_three.id) assert account["id"] == to_string(user_three.id)
@ -194,8 +194,8 @@ test "search fetches remote statuses and prefers them over other results", %{con
results = results =
conn conn
|> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"}) |> get("/api/v1/search?q=https://shitposter.club/notice/2827873")
|> json_response(200) |> json_response_and_validate_schema(200)
[status, %{"id" => ^activity_id}] = results["statuses"] [status, %{"id" => ^activity_id}] = results["statuses"]
@ -212,10 +212,12 @@ test "search doesn't show statuses that it shouldn't", %{conn: conn} do
}) })
capture_log(fn -> capture_log(fn ->
q = Object.normalize(activity).data["id"]
results = results =
conn conn
|> get("/api/v1/search", %{"q" => Object.normalize(activity).data["id"]}) |> get("/api/v1/search?q=#{q}")
|> json_response(200) |> json_response_and_validate_schema(200)
[] = results["statuses"] [] = results["statuses"]
end) end)
@ -228,8 +230,8 @@ test "search fetches remote accounts", %{conn: conn} do
conn conn
|> assign(:user, user) |> assign(:user, user)
|> assign(:token, insert(:oauth_token, user: user, scopes: ["read"])) |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"]))
|> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "true"}) |> get("/api/v1/search?q=mike@osada.macgirvin.com&resolve=true")
|> json_response(200) |> json_response_and_validate_schema(200)
[account] = results["accounts"] [account] = results["accounts"]
assert account["acct"] == "mike@osada.macgirvin.com" assert account["acct"] == "mike@osada.macgirvin.com"
@ -238,8 +240,8 @@ test "search fetches remote accounts", %{conn: conn} do
test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do
results = results =
conn conn
|> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "false"}) |> get("/api/v1/search?q=mike@osada.macgirvin.com&resolve=false")
|> json_response(200) |> json_response_and_validate_schema(200)
assert [] == results["accounts"] assert [] == results["accounts"]
end end
@ -254,16 +256,16 @@ test "search with limit and offset", %{conn: conn} do
result = result =
conn conn
|> get("/api/v1/search", %{"q" => "2hu", "limit" => 1}) |> get("/api/v1/search?q=2hu&limit=1")
assert results = json_response(result, 200) assert results = json_response_and_validate_schema(result, 200)
assert [%{"id" => activity_id1}] = results["statuses"] assert [%{"id" => activity_id1}] = results["statuses"]
assert [_] = results["accounts"] assert [_] = results["accounts"]
results = results =
conn conn
|> get("/api/v1/search", %{"q" => "2hu", "limit" => 1, "offset" => 1}) |> get("/api/v1/search?q=2hu&limit=1&offset=1")
|> json_response(200) |> json_response_and_validate_schema(200)
assert [%{"id" => activity_id2}] = results["statuses"] assert [%{"id" => activity_id2}] = results["statuses"]
assert [] = results["accounts"] assert [] = results["accounts"]
@ -279,13 +281,13 @@ test "search returns results only for the given type", %{conn: conn} do
assert %{"statuses" => [_activity], "accounts" => [], "hashtags" => []} = assert %{"statuses" => [_activity], "accounts" => [], "hashtags" => []} =
conn conn
|> get("/api/v1/search", %{"q" => "2hu", "type" => "statuses"}) |> get("/api/v1/search?q=2hu&type=statuses")
|> json_response(200) |> json_response_and_validate_schema(200)
assert %{"statuses" => [], "accounts" => [_user_two], "hashtags" => []} = assert %{"statuses" => [], "accounts" => [_user_two], "hashtags" => []} =
conn conn
|> get("/api/v1/search", %{"q" => "2hu", "type" => "accounts"}) |> get("/api/v1/search?q=2hu&type=accounts")
|> json_response(200) |> json_response_and_validate_schema(200)
end end
test "search uses account_id to filter statuses by the author", %{conn: conn} do test "search uses account_id to filter statuses by the author", %{conn: conn} do
@ -297,8 +299,8 @@ test "search uses account_id to filter statuses by the author", %{conn: conn} do
results = results =
conn conn
|> get("/api/v1/search", %{"q" => "2hu", "account_id" => user.id}) |> get("/api/v1/search?q=2hu&account_id=#{user.id}")
|> json_response(200) |> json_response_and_validate_schema(200)
assert [%{"id" => activity_id1}] = results["statuses"] assert [%{"id" => activity_id1}] = results["statuses"]
assert activity_id1 == activity1.id assert activity_id1 == activity1.id
@ -306,8 +308,8 @@ test "search uses account_id to filter statuses by the author", %{conn: conn} do
results = results =
conn conn
|> get("/api/v1/search", %{"q" => "2hu", "account_id" => user_two.id}) |> get("/api/v1/search?q=2hu&account_id=#{user_two.id}")
|> json_response(200) |> json_response_and_validate_schema(200)
assert [%{"id" => activity_id2}] = results["statuses"] assert [%{"id" => activity_id2}] = results["statuses"]
assert activity_id2 == activity2.id assert activity_id2 == activity2.id