logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://hacktivis.me/git/pleroma.git

search_controller.ex (5436B)


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