Fork of Pleroma with site-specific changes and feature branches https://git.pleroma.social/pleroma/pleroma
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

193 строки
5.1KB

  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2020 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
  10. alias Pleroma.Web.ControllerHelper
  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 = Web.base_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