Fork of Pleroma with site-specific changes and feature branches https://git.pleroma.social/pleroma/pleroma
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

193 lines
5.1KB

  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.Web.MastodonAPI.SearchController do
  5. use Pleroma.Web, :controller
  6. alias Pleroma.Activity
  7. alias Pleroma.Repo
  8. alias Pleroma.User
  9. alias Pleroma.Web.ControllerHelper
  10. alias Pleroma.Web.Endpoint
  11. alias Pleroma.Web.MastodonAPI.AccountView
  12. alias Pleroma.Web.MastodonAPI.StatusView
  13. alias Pleroma.Web.Plugs.OAuthScopesPlug
  14. alias Pleroma.Web.Plugs.RateLimiter
  15. require Logger
  16. plug(Pleroma.Web.ApiSpec.CastAndValidate)
  17. # Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)
  18. plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated})
  19. # Note: on private instances auth is required (EnsurePublicOrAuthenticatedPlug is not skipped)
  20. plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search])
  21. defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.SearchOperation
  22. def account_search(%{assigns: %{user: user}} = conn, %{q: query} = params) do
  23. accounts = User.search(query, search_options(params, user))
  24. conn
  25. |> put_view(AccountView)
  26. |> render("index.json",
  27. users: accounts,
  28. for: user,
  29. as: :user
  30. )
  31. end
  32. def search2(conn, params), do: do_search(:v2, conn, params)
  33. def search(conn, params), do: do_search(:v1, conn, params)
  34. defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do
  35. query = String.trim(query)
  36. options = search_options(params, user)
  37. timeout = Keyword.get(Repo.config(), :timeout, 15_000)
  38. default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []}
  39. result =
  40. default_values
  41. |> Enum.map(fn {resource, default_value} ->
  42. if params[:type] in [nil, resource] do
  43. {resource, fn -> resource_search(version, resource, query, options) end}
  44. else
  45. {resource, fn -> default_value end}
  46. end
  47. end)
  48. |> Task.async_stream(fn {resource, f} -> {resource, with_fallback(f)} end,
  49. timeout: timeout,
  50. on_timeout: :kill_task
  51. )
  52. |> Enum.reduce(default_values, fn
  53. {:ok, {resource, result}}, acc ->
  54. Map.put(acc, resource, result)
  55. _error, acc ->
  56. acc
  57. end)
  58. json(conn, result)
  59. end
  60. defp search_options(params, user) do
  61. [
  62. resolve: params[:resolve],
  63. following: params[:following],
  64. limit: params[:limit],
  65. offset: params[:offset],
  66. type: params[:type],
  67. author: get_author(params),
  68. embed_relationships: ControllerHelper.embed_relationships?(params),
  69. for_user: user
  70. ]
  71. |> Enum.filter(&elem(&1, 1))
  72. end
  73. defp resource_search(_, "accounts", query, options) do
  74. accounts = with_fallback(fn -> User.search(query, options) end)
  75. AccountView.render("index.json",
  76. users: accounts,
  77. for: options[:for_user],
  78. embed_relationships: options[:embed_relationships]
  79. )
  80. end
  81. defp resource_search(_, "statuses", query, options) do
  82. statuses = with_fallback(fn -> Activity.search(options[:for_user], query, options) end)
  83. StatusView.render("index.json",
  84. activities: statuses,
  85. for: options[:for_user],
  86. as: :activity
  87. )
  88. end
  89. defp resource_search(:v2, "hashtags", query, options) do
  90. tags_path = Endpoint.url() <> "/tag/"
  91. query
  92. |> prepare_tags(options)
  93. |> Enum.map(fn tag ->
  94. %{name: tag, url: tags_path <> tag}
  95. end)
  96. end
  97. defp resource_search(:v1, "hashtags", query, options) do
  98. prepare_tags(query, options)
  99. end
  100. defp prepare_tags(query, options) do
  101. tags =
  102. query
  103. |> preprocess_uri_query()
  104. |> String.split(~r/[^#\w]+/u, trim: true)
  105. |> Enum.uniq_by(&String.downcase/1)
  106. explicit_tags = Enum.filter(tags, fn tag -> String.starts_with?(tag, "#") end)
  107. tags =
  108. if Enum.any?(explicit_tags) do
  109. explicit_tags
  110. else
  111. tags
  112. end
  113. tags = Enum.map(tags, fn tag -> String.trim_leading(tag, "#") end)
  114. tags =
  115. if Enum.empty?(explicit_tags) && !options[:skip_joined_tag] do
  116. add_joined_tag(tags)
  117. else
  118. tags
  119. end
  120. Pleroma.Pagination.paginate(tags, options)
  121. end
  122. defp add_joined_tag(tags) do
  123. tags
  124. |> Kernel.++([joined_tag(tags)])
  125. |> Enum.uniq_by(&String.downcase/1)
  126. end
  127. # If `query` is a URI, returns last component of its path, otherwise returns `query`
  128. defp preprocess_uri_query(query) do
  129. if query =~ ~r/https?:\/\// do
  130. query
  131. |> String.trim_trailing("/")
  132. |> URI.parse()
  133. |> Map.get(:path)
  134. |> String.split("/")
  135. |> Enum.at(-1)
  136. else
  137. query
  138. end
  139. end
  140. defp joined_tag(tags) do
  141. tags
  142. |> Enum.map(fn tag -> String.capitalize(tag) end)
  143. |> Enum.join()
  144. end
  145. defp with_fallback(f, fallback \\ []) do
  146. try do
  147. f.()
  148. rescue
  149. error ->
  150. Logger.error("#{__MODULE__} search error: #{inspect(error)}")
  151. fallback
  152. end
  153. end
  154. defp get_author(%{account_id: account_id}) when is_binary(account_id),
  155. do: User.get_cached_by_id(account_id)
  156. defp get_author(_params), do: nil
  157. end