diff options
6 files changed, 293 insertions, 60 deletions
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 470fc0215..70069d6f9 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -556,11 +556,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do      }    end -  defp array_of_accounts do +  def array_of_accounts do      %Schema{        title: "ArrayOfAccounts",        type: :array, -      items: Account +      items: Account, +      example: [Account.schema().example]      }    end diff --git a/lib/pleroma/web/api_spec/operations/search_operation.ex b/lib/pleroma/web/api_spec/operations/search_operation.ex new file mode 100644 index 000000000..6ea00a9a8 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/search_operation.ex @@ -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 diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 7a804461f..2572c9641 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do    alias Pleroma.Web.ApiSpec.Schemas.Emoji    alias Pleroma.Web.ApiSpec.Schemas.FlakeID    alias Pleroma.Web.ApiSpec.Schemas.Poll +  alias Pleroma.Web.ApiSpec.Schemas.Tag    alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope    require OpenApiSpex @@ -106,16 +107,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do        replies_count: %Schema{type: :integer},        sensitive: %Schema{type: :boolean},        spoiler_text: %Schema{type: :string}, -      tags: %Schema{ -        type: :array, -        items: %Schema{ -          type: :object, -          properties: %{ -            name: %Schema{type: :string}, -            url: %Schema{type: :string, format: :uri} -          } -        } -      }, +      tags: %Schema{type: :array, items: Tag},        uri: %Schema{type: :string, format: :uri},        url: %Schema{type: :string, nullable: true, format: :uri},        visibility: VisibilityScope diff --git a/lib/pleroma/web/api_spec/schemas/tag.ex b/lib/pleroma/web/api_spec/schemas/tag.ex new file mode 100644 index 000000000..e693fb83e --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/tag.ex @@ -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 diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index cd49da6ad..0e0d54ba4 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -5,7 +5,7 @@  defmodule Pleroma.Web.MastodonAPI.SearchController do    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.Plugs.OAuthScopesPlug @@ -18,6 +18,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do    require Logger +  plug(Pleroma.Web.ApiSpec.CastAndValidate) +    # Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)    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]) -  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))      conn @@ -36,7 +40,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do    def search2(conn, params), do: do_search(:v2, 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)      timeout = Keyword.get(Repo.config(), :timeout, 15_000)      default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []} @@ -44,7 +48,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do      result =        default_values        |> 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}          else            {resource, fn -> default_value end} @@ -68,11 +72,11 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do    defp search_options(params, user) do      [        skip_relationships: skip_relationships?(params), -      resolve: params["resolve"] == "true", -      following: params["following"] == "true", -      limit: fetch_integer_param(params, "limit"), -      offset: fetch_integer_param(params, "offset"), -      type: params["type"], +      resolve: params[:resolve], +      following: params[:following], +      limit: params[:limit], +      offset: params[:offset], +      type: params[:type],        author: get_author(params),        for_user: user      ] @@ -135,7 +139,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do      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)    defp get_author(_params), do: nil diff --git a/test/web/mastodon_api/controllers/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs index 11133ff66..02476acb6 100644 --- a/test/web/mastodon_api/controllers/search_controller_test.exs +++ b/test/web/mastodon_api/controllers/search_controller_test.exs @@ -27,8 +27,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do          capture_log(fn ->            results =              conn -            |> get("/api/v2/search", %{"q" => "2hu"}) -            |> json_response(200) +            |> get("/api/v2/search?q=2hu") +            |> json_response_and_validate_schema(200)            assert results["accounts"] == []            assert results["statuses"] == [] @@ -54,8 +54,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do        results =          conn -        |> get("/api/v2/search", %{"q" => "2hu #private"}) -        |> json_response(200) +        |> get("/api/v2/search?#{URI.encode_query(%{q: "2hu #private"})}") +        |> json_response_and_validate_schema(200)        [account | _] = results["accounts"]        assert account["id"] == to_string(user_three.id) @@ -68,8 +68,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do        assert status["id"] == to_string(activity.id)        results = -        get(conn, "/api/v2/search", %{"q" => "天子"}) -        |> json_response(200) +        get(conn, "/api/v2/search?q=天子") +        |> json_response_and_validate_schema(200)        [status] = results["statuses"]        assert status["id"] == to_string(activity.id) @@ -89,8 +89,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do          conn          |> assign(:user, user)          |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"])) -        |> get("/api/v2/search", %{"q" => "Agent"}) -        |> json_response(200) +        |> get("/api/v2/search?q=Agent") +        |> json_response_and_validate_schema(200)        status_ids = Enum.map(results["statuses"], fn g -> g["id"] end) @@ -107,8 +107,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do        results =          conn -        |> get("/api/v1/accounts/search", %{"q" => "shp"}) -        |> json_response(200) +        |> get("/api/v1/accounts/search?q=shp") +        |> json_response_and_validate_schema(200)        result_ids = for result <- results, do: result["acct"] @@ -117,8 +117,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do        results =          conn -        |> get("/api/v1/accounts/search", %{"q" => "2hu"}) -        |> json_response(200) +        |> get("/api/v1/accounts/search?q=2hu") +        |> json_response_and_validate_schema(200)        result_ids = for result <- results, do: result["acct"] @@ -130,8 +130,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do        results =          conn -        |> get("/api/v1/accounts/search", %{"q" => "shp@shitposter.club xxx "}) -        |> json_response(200) +        |> get("/api/v1/accounts/search?q=shp@shitposter.club xxx") +        |> json_response_and_validate_schema(200)        assert length(results) == 1      end @@ -146,8 +146,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do          capture_log(fn ->            results =              conn -            |> get("/api/v1/search", %{"q" => "2hu"}) -            |> json_response(200) +            |> get("/api/v1/search?q=2hu") +            |> json_response_and_validate_schema(200)            assert results["accounts"] == []            assert results["statuses"] == [] @@ -173,8 +173,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do        results =          conn -        |> get("/api/v1/search", %{"q" => "2hu"}) -        |> json_response(200) +        |> get("/api/v1/search?q=2hu") +        |> json_response_and_validate_schema(200)        [account | _] = results["accounts"]        assert account["id"] == to_string(user_three.id) @@ -194,8 +194,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do          results =            conn -          |> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"}) -          |> json_response(200) +          |> get("/api/v1/search?q=https://shitposter.club/notice/2827873") +          |> json_response_and_validate_schema(200)          [status, %{"id" => ^activity_id}] = results["statuses"] @@ -212,10 +212,12 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do          })        capture_log(fn -> +        q = Object.normalize(activity).data["id"] +          results =            conn -          |> get("/api/v1/search", %{"q" => Object.normalize(activity).data["id"]}) -          |> json_response(200) +          |> get("/api/v1/search?q=#{q}") +          |> json_response_and_validate_schema(200)          [] = results["statuses"]        end) @@ -228,8 +230,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do          conn          |> assign(:user, user)          |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"])) -        |> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "true"}) -        |> json_response(200) +        |> get("/api/v1/search?q=mike@osada.macgirvin.com&resolve=true") +        |> json_response_and_validate_schema(200)        [account] = results["accounts"]        assert account["acct"] == "mike@osada.macgirvin.com" @@ -238,8 +240,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do      test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do        results =          conn -        |> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "false"}) -        |> json_response(200) +        |> get("/api/v1/search?q=mike@osada.macgirvin.com&resolve=false") +        |> json_response_and_validate_schema(200)        assert [] == results["accounts"]      end @@ -254,16 +256,16 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do        result =          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 [_] = results["accounts"]        results =          conn -        |> get("/api/v1/search", %{"q" => "2hu", "limit" => 1, "offset" => 1}) -        |> json_response(200) +        |> get("/api/v1/search?q=2hu&limit=1&offset=1") +        |> json_response_and_validate_schema(200)        assert [%{"id" => activity_id2}] = results["statuses"]        assert [] = results["accounts"] @@ -279,13 +281,13 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do        assert %{"statuses" => [_activity], "accounts" => [], "hashtags" => []} =                 conn -               |> get("/api/v1/search", %{"q" => "2hu", "type" => "statuses"}) -               |> json_response(200) +               |> get("/api/v1/search?q=2hu&type=statuses") +               |> json_response_and_validate_schema(200)        assert %{"statuses" => [], "accounts" => [_user_two], "hashtags" => []} =                 conn -               |> get("/api/v1/search", %{"q" => "2hu", "type" => "accounts"}) -               |> json_response(200) +               |> get("/api/v1/search?q=2hu&type=accounts") +               |> json_response_and_validate_schema(200)      end      test "search uses account_id to filter statuses by the author", %{conn: conn} do @@ -297,8 +299,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do        results =          conn -        |> get("/api/v1/search", %{"q" => "2hu", "account_id" => user.id}) -        |> json_response(200) +        |> get("/api/v1/search?q=2hu&account_id=#{user.id}") +        |> json_response_and_validate_schema(200)        assert [%{"id" => activity_id1}] = results["statuses"]        assert activity_id1 == activity1.id @@ -306,8 +308,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do        results =          conn -        |> get("/api/v1/search", %{"q" => "2hu", "account_id" => user_two.id}) -        |> json_response(200) +        |> get("/api/v1/search?q=2hu&account_id=#{user_two.id}") +        |> json_response_and_validate_schema(200)        assert [%{"id" => activity_id2}] = results["statuses"]        assert activity_id2 == activity2.id  | 
