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.

229 lines
6.5KB

  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.Query do
  5. @moduledoc """
  6. User query builder module. Builds query from new query or another user query.
  7. ## Example:
  8. query = Pleroma.User.Query.build(%{nickname: "nickname"})
  9. another_query = Pleroma.User.Query.build(query, %{email: "email@example.com"})
  10. Pleroma.Repo.all(query)
  11. Pleroma.Repo.all(another_query)
  12. Adding new rules:
  13. - *ilike criteria*
  14. - add field to @ilike_criteria list
  15. - pass non empty string
  16. - e.g. Pleroma.User.Query.build(%{nickname: "nickname"})
  17. - *equal criteria*
  18. - add field to @equal_criteria list
  19. - pass non empty string
  20. - e.g. Pleroma.User.Query.build(%{email: "email@example.com"})
  21. - *contains criteria*
  22. - add field to @containns_criteria list
  23. - pass values list
  24. - e.g. Pleroma.User.Query.build(%{ap_id: ["http://ap_id1", "http://ap_id2"]})
  25. """
  26. import Ecto.Query
  27. import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
  28. alias Pleroma.FollowingRelationship
  29. alias Pleroma.User
  30. @type criteria ::
  31. %{
  32. query: String.t(),
  33. tags: [String.t()],
  34. name: String.t(),
  35. email: String.t(),
  36. local: boolean(),
  37. external: boolean(),
  38. active: boolean(),
  39. deactivated: boolean(),
  40. need_approval: boolean(),
  41. unconfirmed: boolean(),
  42. is_admin: boolean(),
  43. is_moderator: boolean(),
  44. super_users: boolean(),
  45. invisible: boolean(),
  46. internal: boolean(),
  47. followers: User.t(),
  48. friends: User.t(),
  49. recipients_from_activity: [String.t()],
  50. nickname: [String.t()] | String.t(),
  51. ap_id: [String.t()],
  52. order_by: term(),
  53. select: term(),
  54. limit: pos_integer(),
  55. actor_types: [String.t()]
  56. }
  57. | map()
  58. @ilike_criteria [:nickname, :name, :query]
  59. @equal_criteria [:email]
  60. @contains_criteria [:ap_id, :nickname]
  61. @spec build(Query.t(), criteria()) :: Query.t()
  62. def build(query \\ base_query(), criteria) do
  63. prepare_query(query, criteria)
  64. end
  65. @spec paginate(Ecto.Query.t(), pos_integer(), pos_integer()) :: Ecto.Query.t()
  66. def paginate(query, page, page_size) do
  67. from(u in query,
  68. limit: ^page_size,
  69. offset: ^((page - 1) * page_size)
  70. )
  71. end
  72. defp base_query do
  73. from(u in User)
  74. end
  75. defp prepare_query(query, criteria) do
  76. criteria
  77. |> Map.put_new(:internal, false)
  78. |> Enum.reduce(query, &compose_query/2)
  79. end
  80. defp compose_query({key, value}, query)
  81. when key in @ilike_criteria and not_empty_string(value) do
  82. # hack for :query key
  83. key = if key == :query, do: :nickname, else: key
  84. where(query, [u], ilike(field(u, ^key), ^"%#{value}%"))
  85. end
  86. defp compose_query({:invisible, bool}, query) when is_boolean(bool) do
  87. where(query, [u], u.invisible == ^bool)
  88. end
  89. defp compose_query({key, value}, query)
  90. when key in @equal_criteria and not_empty_string(value) do
  91. where(query, [u], ^[{key, value}])
  92. end
  93. defp compose_query({key, values}, query) when key in @contains_criteria and is_list(values) do
  94. where(query, [u], field(u, ^key) in ^values)
  95. end
  96. defp compose_query({:tags, tags}, query) when is_list(tags) and length(tags) > 0 do
  97. where(query, [u], fragment("? && ?", u.tags, ^tags))
  98. end
  99. defp compose_query({:is_admin, bool}, query) do
  100. where(query, [u], u.is_admin == ^bool)
  101. end
  102. defp compose_query({:actor_types, actor_types}, query) when is_list(actor_types) do
  103. where(query, [u], u.actor_type in ^actor_types)
  104. end
  105. defp compose_query({:is_moderator, bool}, query) do
  106. where(query, [u], u.is_moderator == ^bool)
  107. end
  108. defp compose_query({:super_users, _}, query) do
  109. where(
  110. query,
  111. [u],
  112. u.is_admin or u.is_moderator
  113. )
  114. end
  115. defp compose_query({:local, _}, query), do: location_query(query, true)
  116. defp compose_query({:external, _}, query), do: location_query(query, false)
  117. defp compose_query({:active, _}, query) do
  118. where(query, [u], u.is_active == true)
  119. |> where([u], u.is_approved == true)
  120. |> where([u], u.is_confirmed == true)
  121. end
  122. defp compose_query({:legacy_active, _}, query) do
  123. query
  124. |> where([u], fragment("not (?->'deactivated' @> 'true')", u.info))
  125. end
  126. defp compose_query({:deactivated, false}, query) do
  127. where(query, [u], u.is_active == true)
  128. end
  129. defp compose_query({:deactivated, true}, query) do
  130. where(query, [u], u.is_active == false)
  131. end
  132. defp compose_query({:confirmation_pending, bool}, query) do
  133. where(query, [u], u.is_confirmed != ^bool)
  134. end
  135. defp compose_query({:need_approval, _}, query) do
  136. where(query, [u], u.is_approved == false)
  137. end
  138. defp compose_query({:unconfirmed, _}, query) do
  139. where(query, [u], u.is_confirmed == false)
  140. end
  141. defp compose_query({:followers, %User{id: id}}, query) do
  142. query
  143. |> where([u], u.id != ^id)
  144. |> join(:inner, [u], r in FollowingRelationship,
  145. as: :relationships,
  146. on: r.following_id == ^id and r.follower_id == u.id
  147. )
  148. |> where([relationships: r], r.state == ^:follow_accept)
  149. end
  150. defp compose_query({:friends, %User{id: id}}, query) do
  151. query
  152. |> where([u], u.id != ^id)
  153. |> join(:inner, [u], r in FollowingRelationship,
  154. as: :relationships,
  155. on: r.following_id == u.id and r.follower_id == ^id
  156. )
  157. |> where([relationships: r], r.state == ^:follow_accept)
  158. end
  159. defp compose_query({:recipients_from_activity, to}, query) do
  160. following_query =
  161. from(u in User,
  162. join: f in FollowingRelationship,
  163. on: u.id == f.following_id,
  164. where: f.state == ^:follow_accept,
  165. where: u.follower_address in ^to,
  166. select: f.follower_id
  167. )
  168. from(u in query,
  169. where: u.ap_id in ^to or u.id in subquery(following_query)
  170. )
  171. end
  172. defp compose_query({:order_by, key}, query) do
  173. order_by(query, [u], field(u, ^key))
  174. end
  175. defp compose_query({:select, keys}, query) do
  176. select(query, [u], ^keys)
  177. end
  178. defp compose_query({:limit, limit}, query) do
  179. limit(query, ^limit)
  180. end
  181. defp compose_query({:internal, false}, query) do
  182. query
  183. |> where([u], not is_nil(u.nickname))
  184. |> where([u], not like(u.nickname, "internal.%"))
  185. end
  186. defp compose_query(_unsupported_param, query), do: query
  187. defp location_query(query, local) do
  188. where(query, [u], u.local == ^local)
  189. end
  190. end