total rebase
[anni] / lib / pleroma / user / query.ex
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
5 defmodule Pleroma.User.Query do
6   @moduledoc """
7   User query builder module. Builds query from new query or another user query.
8
9     ## Example:
10         query = Pleroma.User.Query.build(%{nickname: "nickname"})
11         another_query = Pleroma.User.Query.build(query, %{email: "email@example.com"})
12         Pleroma.Repo.all(query)
13         Pleroma.Repo.all(another_query)
14
15   Adding new rules:
16     - *ilike criteria*
17       - add field to @ilike_criteria list
18       - pass non empty string
19       - e.g. Pleroma.User.Query.build(%{nickname: "nickname"})
20     - *equal criteria*
21       - add field to @equal_criteria list
22       - pass non empty string
23       - e.g. Pleroma.User.Query.build(%{email: "email@example.com"})
24     - *contains criteria*
25       - add field to @contains_criteria list
26       - pass values list
27       - e.g. Pleroma.User.Query.build(%{ap_id: ["http://ap_id1", "http://ap_id2"]})
28   """
29   import Ecto.Query
30   import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
31
32   alias Pleroma.Config
33   alias Pleroma.FollowingRelationship
34   alias Pleroma.User
35
36   @type criteria ::
37           %{
38             query: String.t(),
39             tags: [String.t()],
40             name: String.t(),
41             email: String.t(),
42             local: boolean(),
43             external: boolean(),
44             active: boolean(),
45             deactivated: boolean(),
46             need_approval: boolean(),
47             unconfirmed: boolean(),
48             is_admin: boolean(),
49             is_moderator: boolean(),
50             is_suggested: boolean(),
51             is_discoverable: boolean(),
52             super_users: boolean(),
53             is_privileged: atom(),
54             invisible: boolean(),
55             internal: boolean(),
56             followers: User.t(),
57             friends: User.t(),
58             recipients_from_activity: [String.t()],
59             nickname: [String.t()] | String.t(),
60             ap_id: [String.t()],
61             order_by: term(),
62             select: term(),
63             limit: pos_integer(),
64             actor_types: [String.t()],
65             birthday_day: pos_integer(),
66             birthday_month: pos_integer()
67           }
68           | map()
69
70   @ilike_criteria [:nickname, :name, :query]
71   @equal_criteria [:email]
72   @contains_criteria [:ap_id, :nickname]
73
74   @spec build(Ecto.Query.t(), criteria()) :: Ecto.Query.t()
75   def build(query \\ base_query(), criteria) do
76     prepare_query(query, criteria)
77   end
78
79   @spec paginate(Ecto.Query.t(), pos_integer(), pos_integer()) :: Ecto.Query.t()
80   def paginate(query, page, page_size) do
81     from(u in query,
82       limit: ^page_size,
83       offset: ^((page - 1) * page_size)
84     )
85   end
86
87   defp base_query do
88     from(u in User)
89   end
90
91   defp prepare_query(query, criteria) do
92     criteria
93     |> Map.put_new(:internal, false)
94     |> Enum.reduce(query, &compose_query/2)
95   end
96
97   defp compose_query({key, value}, query)
98        when key in @ilike_criteria and not_empty_string(value) do
99     # hack for :query key
100     key = if key == :query, do: :nickname, else: key
101     where(query, [u], ilike(field(u, ^key), ^"%#{value}%"))
102   end
103
104   defp compose_query({:invisible, bool}, query) when is_boolean(bool) do
105     where(query, [u], u.invisible == ^bool)
106   end
107
108   defp compose_query({key, value}, query)
109        when key in @equal_criteria and not_empty_string(value) do
110     where(query, [u], ^[{key, value}])
111   end
112
113   defp compose_query({key, values}, query) when key in @contains_criteria and is_list(values) do
114     where(query, [u], field(u, ^key) in ^values)
115   end
116
117   defp compose_query({:tags, tags}, query) when is_list(tags) and length(tags) > 0 do
118     where(query, [u], fragment("? && ?", u.tags, ^tags))
119   end
120
121   defp compose_query({:is_admin, bool}, query) do
122     where(query, [u], u.is_admin == ^bool)
123   end
124
125   defp compose_query({:actor_types, actor_types}, query) when is_list(actor_types) do
126     where(query, [u], u.actor_type in ^actor_types)
127   end
128
129   defp compose_query({:is_moderator, bool}, query) do
130     where(query, [u], u.is_moderator == ^bool)
131   end
132
133   defp compose_query({:super_users, _}, query) do
134     where(
135       query,
136       [u],
137       u.is_admin or u.is_moderator
138     )
139   end
140
141   defp compose_query({:is_privileged, privilege}, query) do
142     moderator_privileged = privilege in Config.get([:instance, :moderator_privileges])
143     admin_privileged = privilege in Config.get([:instance, :admin_privileges])
144
145     query = compose_query({:active, true}, query)
146     query = compose_query({:local, true}, query)
147
148     case {admin_privileged, moderator_privileged} do
149       {false, false} ->
150         where(
151           query,
152           false
153         )
154
155       {true, true} ->
156         where(
157           query,
158           [u],
159           u.is_admin or u.is_moderator
160         )
161
162       {true, false} ->
163         where(
164           query,
165           [u],
166           u.is_admin
167         )
168
169       {false, true} ->
170         where(
171           query,
172           [u],
173           u.is_moderator
174         )
175     end
176   end
177
178   defp compose_query({:local, _}, query), do: location_query(query, true)
179
180   defp compose_query({:external, _}, query), do: location_query(query, false)
181
182   defp compose_query({:active, _}, query) do
183     where(query, [u], u.is_active == true)
184     |> where([u], u.is_approved == true)
185     |> where([u], u.is_confirmed == true)
186   end
187
188   defp compose_query({:legacy_active, _}, query) do
189     query
190     |> where([u], fragment("not (?->'deactivated' @> 'true')", u.info))
191   end
192
193   defp compose_query({:deactivated, false}, query) do
194     where(query, [u], u.is_active == true)
195   end
196
197   defp compose_query({:deactivated, true}, query) do
198     where(query, [u], u.is_active == false)
199   end
200
201   defp compose_query({:confirmation_pending, bool}, query) do
202     where(query, [u], u.is_confirmed != ^bool)
203   end
204
205   defp compose_query({:need_approval, _}, query) do
206     where(query, [u], u.is_approved == false)
207   end
208
209   defp compose_query({:unconfirmed, _}, query) do
210     where(query, [u], u.is_confirmed == false)
211   end
212
213   defp compose_query({:is_suggested, bool}, query) do
214     where(query, [u], u.is_suggested == ^bool)
215   end
216
217   defp compose_query({:is_discoverable, bool}, query) do
218     where(query, [u], u.is_discoverable == ^bool)
219   end
220
221   defp compose_query({:followers, %User{id: id}}, query) do
222     query
223     |> where([u], u.id != ^id)
224     |> join(:inner, [u], r in FollowingRelationship,
225       as: :relationships,
226       on: r.following_id == ^id and r.follower_id == u.id
227     )
228     |> where([relationships: r], r.state == ^:follow_accept)
229   end
230
231   defp compose_query({:friends, %User{id: id}}, query) do
232     query
233     |> where([u], u.id != ^id)
234     |> join(:inner, [u], r in FollowingRelationship,
235       as: :relationships,
236       on: r.following_id == u.id and r.follower_id == ^id
237     )
238     |> where([relationships: r], r.state == ^:follow_accept)
239   end
240
241   defp compose_query({:recipients_from_activity, to}, query) do
242     following_query =
243       from(u in User,
244         join: f in FollowingRelationship,
245         on: u.id == f.following_id,
246         where: f.state == ^:follow_accept,
247         where: u.follower_address in ^to,
248         select: f.follower_id
249       )
250
251     from(u in query,
252       where: u.ap_id in ^to or u.id in subquery(following_query)
253     )
254   end
255
256   defp compose_query({:order_by, key}, query) do
257     order_by(query, [u], field(u, ^key))
258   end
259
260   defp compose_query({:select, keys}, query) do
261     select(query, [u], ^keys)
262   end
263
264   defp compose_query({:limit, limit}, query) do
265     limit(query, ^limit)
266   end
267
268   defp compose_query({:internal, false}, query) do
269     query
270     |> where([u], not is_nil(u.nickname))
271     |> where([u], not like(u.nickname, "internal.%"))
272   end
273
274   defp compose_query({:birthday_day, day}, query) do
275     query
276     |> where([u], u.show_birthday == true)
277     |> where([u], not is_nil(u.birthday))
278     |> where([u], fragment("date_part('day', ?)", u.birthday) == ^day)
279   end
280
281   defp compose_query({:birthday_month, month}, query) do
282     query
283     |> where([u], u.show_birthday == true)
284     |> where([u], not is_nil(u.birthday))
285     |> where([u], fragment("date_part('month', ?)", u.birthday) == ^month)
286   end
287
288   defp compose_query(_unsupported_param, query), do: query
289
290   defp location_query(query, local) do
291     where(query, [u], u.local == ^local)
292   end
293 end