Fork of Pleroma with site-specific changes and feature branches https://git.pleroma.social/pleroma/pleroma
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.

262 líneas
7.0KB

  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.User.Search do
  5. alias Pleroma.EctoType.ActivityPub.ObjectValidators.Uri, as: UriType
  6. alias Pleroma.Pagination
  7. alias Pleroma.User
  8. import Ecto.Query
  9. @limit 20
  10. def search(query_string, opts \\ []) do
  11. resolve = Keyword.get(opts, :resolve, false)
  12. following = Keyword.get(opts, :following, false)
  13. result_limit = Keyword.get(opts, :limit, @limit)
  14. offset = Keyword.get(opts, :offset, 0)
  15. for_user = Keyword.get(opts, :for_user)
  16. query_string = format_query(query_string)
  17. # If this returns anything, it should bounce to the top
  18. maybe_resolved = maybe_resolve(resolve, for_user, query_string)
  19. top_user_ids =
  20. []
  21. |> maybe_add_resolved(maybe_resolved)
  22. |> maybe_add_ap_id_match(query_string)
  23. |> maybe_add_uri_match(query_string)
  24. results =
  25. query_string
  26. |> search_query(for_user, following, top_user_ids)
  27. |> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
  28. results
  29. end
  30. defp maybe_add_resolved(list, {:ok, %User{} = user}) do
  31. [user.id | list]
  32. end
  33. defp maybe_add_resolved(list, _), do: list
  34. defp maybe_add_ap_id_match(list, query) do
  35. if user = User.get_cached_by_ap_id(query) do
  36. [user.id | list]
  37. else
  38. list
  39. end
  40. end
  41. defp maybe_add_uri_match(list, query) do
  42. with {:ok, query} <- UriType.cast(query),
  43. q = from(u in User, where: u.uri == ^query, select: u.id),
  44. users = Pleroma.Repo.all(q) do
  45. users ++ list
  46. else
  47. _ -> list
  48. end
  49. end
  50. defp format_query(query_string) do
  51. # Strip the beginning @ off if there is a query
  52. query_string = String.trim_leading(query_string, "@")
  53. with [name, domain] <- String.split(query_string, "@") do
  54. encoded_domain =
  55. domain
  56. |> String.replace(~r/[!-\-|@|[-`|{-~|\/|:|\s]+/, "")
  57. |> String.to_charlist()
  58. |> :idna.encode()
  59. |> to_string()
  60. name <> "@" <> encoded_domain
  61. else
  62. _ -> query_string
  63. end
  64. end
  65. defp search_query(query_string, for_user, following, top_user_ids) do
  66. for_user
  67. |> base_query(following)
  68. |> filter_blocked_user(for_user)
  69. |> filter_invisible_users()
  70. |> filter_internal_users()
  71. |> filter_blocked_domains(for_user)
  72. |> fts_search(query_string)
  73. |> select_top_users(top_user_ids)
  74. |> trigram_rank(query_string)
  75. |> boost_search_rank(for_user, top_user_ids)
  76. |> subquery()
  77. |> order_by(desc: :search_rank)
  78. |> maybe_restrict_local(for_user)
  79. end
  80. defp select_top_users(query, top_user_ids) do
  81. from(u in query,
  82. or_where: u.id in ^top_user_ids
  83. )
  84. end
  85. defp fts_search(query, query_string) do
  86. query_string = to_tsquery(query_string)
  87. from(
  88. u in query,
  89. where:
  90. fragment(
  91. # The fragment must _exactly_ match `users_fts_index`, otherwise the index won't work
  92. """
  93. (
  94. setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
  95. setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')
  96. ) @@ to_tsquery('simple', ?)
  97. """,
  98. u.nickname,
  99. u.name,
  100. ^query_string
  101. )
  102. )
  103. end
  104. defp to_tsquery(query_string) do
  105. String.trim_trailing(query_string, "@" <> local_domain())
  106. |> String.replace(~r/[!-\/|@|[-`|{-~|:-?]+/, " ")
  107. |> String.trim()
  108. |> String.split()
  109. |> Enum.map(&(&1 <> ":*"))
  110. |> Enum.join(" | ")
  111. end
  112. # Considers nickname match, localized nickname match, name match; preferences nickname match
  113. defp trigram_rank(query, query_string) do
  114. from(
  115. u in query,
  116. select_merge: %{
  117. search_rank:
  118. fragment(
  119. """
  120. similarity(?, ?) +
  121. similarity(?, regexp_replace(?, '@.+', '')) +
  122. similarity(?, trim(coalesce(?, '')))
  123. """,
  124. ^query_string,
  125. u.nickname,
  126. ^query_string,
  127. u.nickname,
  128. ^query_string,
  129. u.name
  130. )
  131. }
  132. )
  133. end
  134. defp base_query(%User{} = user, true), do: User.get_friends_query(user)
  135. defp base_query(_user, _following), do: User
  136. defp filter_invisible_users(query) do
  137. from(q in query, where: q.invisible == false)
  138. end
  139. defp filter_internal_users(query) do
  140. from(q in query, where: q.actor_type != "Application")
  141. end
  142. defp filter_blocked_user(query, %User{} = blocker) do
  143. query
  144. |> join(:left, [u], b in Pleroma.UserRelationship,
  145. as: :blocks,
  146. on: b.relationship_type == ^:block and b.source_id == ^blocker.id and u.id == b.target_id
  147. )
  148. |> where([blocks: b], is_nil(b.target_id))
  149. end
  150. defp filter_blocked_user(query, _), do: query
  151. defp filter_blocked_domains(query, %User{domain_blocks: domain_blocks})
  152. when length(domain_blocks) > 0 do
  153. domains = Enum.join(domain_blocks, ",")
  154. from(
  155. q in query,
  156. where: fragment("substring(ap_id from '.*://([^/]*)') NOT IN (?)", ^domains)
  157. )
  158. end
  159. defp filter_blocked_domains(query, _), do: query
  160. defp maybe_resolve(true, user, query) do
  161. case {limit(), user} do
  162. {:all, _} -> :noop
  163. {:unauthenticated, %User{}} -> User.get_or_fetch(query)
  164. {:unauthenticated, _} -> :noop
  165. {false, _} -> User.get_or_fetch(query)
  166. end
  167. end
  168. defp maybe_resolve(_, _, _), do: :noop
  169. defp maybe_restrict_local(q, user) do
  170. case {limit(), user} do
  171. {:all, _} -> restrict_local(q)
  172. {:unauthenticated, %User{}} -> q
  173. {:unauthenticated, _} -> restrict_local(q)
  174. {false, _} -> q
  175. end
  176. end
  177. defp limit, do: Pleroma.Config.get([:instance, :limit_to_local_content], :unauthenticated)
  178. defp restrict_local(q), do: where(q, [u], u.local == true)
  179. defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
  180. defp boost_search_rank(query, %User{} = for_user, top_user_ids) do
  181. friends_ids = User.get_friends_ids(for_user)
  182. followers_ids = User.get_followers_ids(for_user)
  183. from(u in subquery(query),
  184. select_merge: %{
  185. search_rank:
  186. fragment(
  187. """
  188. CASE WHEN (?) THEN (?) * 1.5
  189. WHEN (?) THEN (?) * 1.3
  190. WHEN (?) THEN (?) * 1.1
  191. WHEN (?) THEN 9001
  192. ELSE (?) END
  193. """,
  194. u.id in ^friends_ids and u.id in ^followers_ids,
  195. u.search_rank,
  196. u.id in ^friends_ids,
  197. u.search_rank,
  198. u.id in ^followers_ids,
  199. u.search_rank,
  200. u.id in ^top_user_ids,
  201. u.search_rank
  202. )
  203. }
  204. )
  205. end
  206. defp boost_search_rank(query, _for_user, top_user_ids) do
  207. from(u in subquery(query),
  208. select_merge: %{
  209. search_rank:
  210. fragment(
  211. """
  212. CASE WHEN (?) THEN 9001
  213. ELSE (?) END
  214. """,
  215. u.id in ^top_user_ids,
  216. u.search_rank
  217. )
  218. }
  219. )
  220. end
  221. end