Merge develop
[akkoma] / lib / pleroma / user.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.User do
6 use Ecto.Schema
7
8 import Ecto.Changeset
9 import Ecto.Query
10
11 alias Comeonin.Pbkdf2
12 alias Ecto.Multi
13 alias Pleroma.Activity
14 alias Pleroma.Keys
15 alias Pleroma.Notification
16 alias Pleroma.Object
17 alias Pleroma.Registration
18 alias Pleroma.Repo
19 alias Pleroma.RepoStreamer
20 alias Pleroma.User
21 alias Pleroma.Web
22 alias Pleroma.Web.ActivityPub.ActivityPub
23 alias Pleroma.Web.ActivityPub.Utils
24 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
25 alias Pleroma.Web.OAuth
26 alias Pleroma.Web.OStatus
27 alias Pleroma.Web.RelMe
28 alias Pleroma.Web.Websub
29
30 require Logger
31
32 @type t :: %__MODULE__{}
33
34 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
35
36 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
37 @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
38
39 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
40 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
41
42 schema "users" do
43 field(:bio, :string)
44 field(:email, :string)
45 field(:name, :string)
46 field(:nickname, :string)
47 field(:password_hash, :string)
48 field(:password, :string, virtual: true)
49 field(:password_confirmation, :string, virtual: true)
50 field(:following, {:array, :string}, default: [])
51 field(:ap_id, :string)
52 field(:avatar, :map)
53 field(:local, :boolean, default: true)
54 field(:follower_address, :string)
55 field(:search_rank, :float, virtual: true)
56 field(:search_type, :integer, virtual: true)
57 field(:tags, {:array, :string}, default: [])
58 field(:last_refreshed_at, :naive_datetime_usec)
59 field(:last_digest_emailed_at, :naive_datetime)
60 has_many(:notifications, Notification)
61 has_many(:registrations, Registration)
62 embeds_one(:info, User.Info)
63
64 timestamps()
65 end
66
67 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
68 do: !Pleroma.Config.get([:instance, :account_activation_required])
69
70 def auth_active?(%User{}), do: true
71
72 def visible_for?(user, for_user \\ nil)
73
74 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
75
76 def visible_for?(%User{} = user, for_user) do
77 auth_active?(user) || superuser?(for_user)
78 end
79
80 def visible_for?(_, _), do: false
81
82 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
83 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
84 def superuser?(_), do: false
85
86 def avatar_url(user, options \\ []) do
87 case user.avatar do
88 %{"url" => [%{"href" => href} | _]} -> href
89 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
90 end
91 end
92
93 def banner_url(user, options \\ []) do
94 case user.info.banner do
95 %{"url" => [%{"href" => href} | _]} -> href
96 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
97 end
98 end
99
100 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
101 def profile_url(%User{ap_id: ap_id}), do: ap_id
102 def profile_url(_), do: nil
103
104 def ap_id(%User{nickname: nickname}) do
105 "#{Web.base_url()}/users/#{nickname}"
106 end
107
108 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
109 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
110
111 def user_info(%User{} = user, args \\ %{}) do
112 following_count =
113 if args[:following_count], do: args[:following_count], else: following_count(user)
114
115 follower_count =
116 if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
117
118 %{
119 note_count: user.info.note_count,
120 locked: user.info.locked,
121 confirmation_pending: user.info.confirmation_pending,
122 default_scope: user.info.default_scope
123 }
124 |> Map.put(:following_count, following_count)
125 |> Map.put(:follower_count, follower_count)
126 end
127
128 def set_info_cache(user, args) do
129 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
130 end
131
132 def restrict_deactivated(query) do
133 from(u in query,
134 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
135 )
136 end
137
138 def following_count(%User{following: []}), do: 0
139
140 def following_count(%User{} = user) do
141 user
142 |> get_friends_query()
143 |> Repo.aggregate(:count, :id)
144 end
145
146 def remote_user_creation(params) do
147 params =
148 params
149 |> Map.put(:info, params[:info] || %{})
150
151 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
152
153 changes =
154 %User{}
155 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
156 |> validate_required([:name, :ap_id])
157 |> unique_constraint(:nickname)
158 |> validate_format(:nickname, @email_regex)
159 |> validate_length(:bio, max: 5000)
160 |> validate_length(:name, max: 100)
161 |> put_change(:local, false)
162 |> put_embed(:info, info_cng)
163
164 if changes.valid? do
165 case info_cng.changes[:source_data] do
166 %{"followers" => followers} ->
167 changes
168 |> put_change(:follower_address, followers)
169
170 _ ->
171 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
172
173 changes
174 |> put_change(:follower_address, followers)
175 end
176 else
177 changes
178 end
179 end
180
181 def update_changeset(struct, params \\ %{}) do
182 struct
183 |> cast(params, [:bio, :name, :avatar, :following])
184 |> unique_constraint(:nickname)
185 |> validate_format(:nickname, local_nickname_regex())
186 |> validate_length(:bio, max: 5000)
187 |> validate_length(:name, min: 1, max: 100)
188 end
189
190 def upgrade_changeset(struct, params \\ %{}) do
191 params =
192 params
193 |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
194
195 info_cng =
196 struct.info
197 |> User.Info.user_upgrade(params[:info])
198
199 struct
200 |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
201 |> unique_constraint(:nickname)
202 |> validate_format(:nickname, local_nickname_regex())
203 |> validate_length(:bio, max: 5000)
204 |> validate_length(:name, max: 100)
205 |> put_embed(:info, info_cng)
206 end
207
208 def password_update_changeset(struct, params) do
209 struct
210 |> cast(params, [:password, :password_confirmation])
211 |> validate_required([:password, :password_confirmation])
212 |> validate_confirmation(:password)
213 |> put_password_hash
214 end
215
216 def reset_password(%User{id: user_id} = user, data) do
217 multi =
218 Multi.new()
219 |> Multi.update(:user, password_update_changeset(user, data))
220 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
221 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
222
223 case Repo.transaction(multi) do
224 {:ok, %{user: user} = _} -> set_cache(user)
225 {:error, _, changeset, _} -> {:error, changeset}
226 end
227 end
228
229 def register_changeset(struct, params \\ %{}, opts \\ []) do
230 need_confirmation? =
231 if is_nil(opts[:need_confirmation]) do
232 Pleroma.Config.get([:instance, :account_activation_required])
233 else
234 opts[:need_confirmation]
235 end
236
237 info_change =
238 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
239
240 changeset =
241 struct
242 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
243 |> validate_required([:name, :nickname, :password, :password_confirmation])
244 |> validate_confirmation(:password)
245 |> unique_constraint(:email)
246 |> unique_constraint(:nickname)
247 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
248 |> validate_format(:nickname, local_nickname_regex())
249 |> validate_format(:email, @email_regex)
250 |> validate_length(:bio, max: 1000)
251 |> validate_length(:name, min: 1, max: 100)
252 |> put_change(:info, info_change)
253
254 changeset =
255 if opts[:external] do
256 changeset
257 else
258 validate_required(changeset, [:email])
259 end
260
261 if changeset.valid? do
262 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
263 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
264
265 changeset
266 |> put_password_hash
267 |> put_change(:ap_id, ap_id)
268 |> unique_constraint(:ap_id)
269 |> put_change(:following, [followers])
270 |> put_change(:follower_address, followers)
271 else
272 changeset
273 end
274 end
275
276 defp autofollow_users(user) do
277 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
278
279 autofollowed_users =
280 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
281 |> Repo.all()
282
283 follow_all(user, autofollowed_users)
284 end
285
286 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
287 def register(%Ecto.Changeset{} = changeset) do
288 with {:ok, user} <- Repo.insert(changeset),
289 {:ok, user} <- autofollow_users(user),
290 {:ok, user} <- set_cache(user),
291 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
292 {:ok, _} <- try_send_confirmation_email(user) do
293 {:ok, user}
294 end
295 end
296
297 def try_send_confirmation_email(%User{} = user) do
298 if user.info.confirmation_pending &&
299 Pleroma.Config.get([:instance, :account_activation_required]) do
300 user
301 |> Pleroma.Emails.UserEmail.account_confirmation_email()
302 |> Pleroma.Emails.Mailer.deliver_async()
303
304 {:ok, :enqueued}
305 else
306 {:ok, :noop}
307 end
308 end
309
310 def needs_update?(%User{local: true}), do: false
311
312 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
313
314 def needs_update?(%User{local: false} = user) do
315 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
316 end
317
318 def needs_update?(_), do: true
319
320 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
321 {:ok, follower}
322 end
323
324 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
325 follow(follower, followed)
326 end
327
328 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
329 if not User.ap_enabled?(followed) do
330 follow(follower, followed)
331 else
332 {:ok, follower}
333 end
334 end
335
336 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
337 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
338 def follow_all(follower, followeds) do
339 followed_addresses =
340 followeds
341 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
342 |> Enum.map(fn %{follower_address: fa} -> fa end)
343
344 q =
345 from(u in User,
346 where: u.id == ^follower.id,
347 update: [
348 set: [
349 following:
350 fragment(
351 "array(select distinct unnest (array_cat(?, ?)))",
352 u.following,
353 ^followed_addresses
354 )
355 ]
356 ],
357 select: u
358 )
359
360 {1, [follower]} = Repo.update_all(q, [])
361
362 Enum.each(followeds, fn followed ->
363 update_follower_count(followed)
364 end)
365
366 set_cache(follower)
367 end
368
369 def follow(%User{} = follower, %User{info: info} = followed) do
370 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
371 ap_followers = followed.follower_address
372
373 cond do
374 info.deactivated ->
375 {:error, "Could not follow user: You are deactivated."}
376
377 deny_follow_blocked and blocks?(followed, follower) ->
378 {:error, "Could not follow user: #{followed.nickname} blocked you."}
379
380 true ->
381 if !followed.local && follower.local && !ap_enabled?(followed) do
382 Websub.subscribe(follower, followed)
383 end
384
385 q =
386 from(u in User,
387 where: u.id == ^follower.id,
388 update: [push: [following: ^ap_followers]],
389 select: u
390 )
391
392 {1, [follower]} = Repo.update_all(q, [])
393
394 {:ok, _} = update_follower_count(followed)
395
396 set_cache(follower)
397 end
398 end
399
400 def unfollow(%User{} = follower, %User{} = followed) do
401 ap_followers = followed.follower_address
402
403 if following?(follower, followed) and follower.ap_id != followed.ap_id do
404 q =
405 from(u in User,
406 where: u.id == ^follower.id,
407 update: [pull: [following: ^ap_followers]],
408 select: u
409 )
410
411 {1, [follower]} = Repo.update_all(q, [])
412
413 {:ok, followed} = update_follower_count(followed)
414
415 set_cache(follower)
416
417 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
418 else
419 {:error, "Not subscribed!"}
420 end
421 end
422
423 @spec following?(User.t(), User.t()) :: boolean
424 def following?(%User{} = follower, %User{} = followed) do
425 Enum.member?(follower.following, followed.follower_address)
426 end
427
428 def locked?(%User{} = user) do
429 user.info.locked || false
430 end
431
432 def get_by_id(id) do
433 Repo.get_by(User, id: id)
434 end
435
436 def get_by_ap_id(ap_id) do
437 Repo.get_by(User, ap_id: ap_id)
438 end
439
440 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
441 # of the ap_id and the domain and tries to get that user
442 def get_by_guessed_nickname(ap_id) do
443 domain = URI.parse(ap_id).host
444 name = List.last(String.split(ap_id, "/"))
445 nickname = "#{name}@#{domain}"
446
447 get_cached_by_nickname(nickname)
448 end
449
450 def set_cache({:ok, user}), do: set_cache(user)
451 def set_cache({:error, err}), do: {:error, err}
452
453 def set_cache(%User{} = user) do
454 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
455 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
456 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
457 {:ok, user}
458 end
459
460 def update_and_set_cache(changeset) do
461 with {:ok, user} <- Repo.update(changeset) do
462 set_cache(user)
463 else
464 e -> e
465 end
466 end
467
468 def invalidate_cache(user) do
469 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
470 Cachex.del(:user_cache, "nickname:#{user.nickname}")
471 Cachex.del(:user_cache, "user_info:#{user.id}")
472 end
473
474 def get_cached_by_ap_id(ap_id) do
475 key = "ap_id:#{ap_id}"
476 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
477 end
478
479 def get_cached_by_id(id) do
480 key = "id:#{id}"
481
482 ap_id =
483 Cachex.fetch!(:user_cache, key, fn _ ->
484 user = get_by_id(id)
485
486 if user do
487 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
488 {:commit, user.ap_id}
489 else
490 {:ignore, ""}
491 end
492 end)
493
494 get_cached_by_ap_id(ap_id)
495 end
496
497 def get_cached_by_nickname(nickname) do
498 key = "nickname:#{nickname}"
499
500 Cachex.fetch!(:user_cache, key, fn ->
501 user_result = get_or_fetch_by_nickname(nickname)
502
503 case user_result do
504 {:ok, user} -> {:commit, user}
505 {:error, _error} -> {:ignore, nil}
506 end
507 end)
508 end
509
510 def get_cached_by_nickname_or_id(nickname_or_id) do
511 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
512 end
513
514 def get_by_nickname(nickname) do
515 Repo.get_by(User, nickname: nickname) ||
516 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
517 Repo.get_by(User, nickname: local_nickname(nickname))
518 end
519 end
520
521 def get_by_email(email), do: Repo.get_by(User, email: email)
522
523 def get_by_nickname_or_email(nickname_or_email) do
524 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
525 end
526
527 def get_cached_user_info(user) do
528 key = "user_info:#{user.id}"
529 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
530 end
531
532 def fetch_by_nickname(nickname) do
533 ap_try = ActivityPub.make_user_from_nickname(nickname)
534
535 case ap_try do
536 {:ok, user} -> {:ok, user}
537 _ -> OStatus.make_user(nickname)
538 end
539 end
540
541 def get_or_fetch_by_nickname(nickname) do
542 with %User{} = user <- get_by_nickname(nickname) do
543 {:ok, user}
544 else
545 _e ->
546 with [_nick, _domain] <- String.split(nickname, "@"),
547 {:ok, user} <- fetch_by_nickname(nickname) do
548 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
549 fetch_initial_posts(user)
550 end
551
552 {:ok, user}
553 else
554 _e -> {:error, "not found " <> nickname}
555 end
556 end
557 end
558
559 @doc "Fetch some posts when the user has just been federated with"
560 def fetch_initial_posts(user),
561 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
562
563 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
564 def get_followers_query(%User{} = user, nil) do
565 User.Query.build(%{followers: user, deactivated: false})
566 end
567
568 def get_followers_query(user, page) do
569 from(u in get_followers_query(user, nil))
570 |> User.Query.paginate(page, 20)
571 end
572
573 @spec get_followers_query(User.t()) :: Ecto.Query.t()
574 def get_followers_query(user), do: get_followers_query(user, nil)
575
576 def get_followers(user, page \\ nil) do
577 q = get_followers_query(user, page)
578
579 {:ok, Repo.all(q)}
580 end
581
582 def get_followers_ids(user, page \\ nil) do
583 q = get_followers_query(user, page)
584
585 Repo.all(from(u in q, select: u.id))
586 end
587
588 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
589 def get_friends_query(%User{} = user, nil) do
590 User.Query.build(%{friends: user, deactivated: false})
591 end
592
593 def get_friends_query(user, page) do
594 from(u in get_friends_query(user, nil))
595 |> User.Query.paginate(page, 20)
596 end
597
598 @spec get_friends_query(User.t()) :: Ecto.Query.t()
599 def get_friends_query(user), do: get_friends_query(user, nil)
600
601 def get_friends(user, page \\ nil) do
602 q = get_friends_query(user, page)
603
604 {:ok, Repo.all(q)}
605 end
606
607 def get_friends_ids(user, page \\ nil) do
608 q = get_friends_query(user, page)
609
610 Repo.all(from(u in q, select: u.id))
611 end
612
613 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
614 def get_follow_requests(%User{} = user) do
615 users =
616 Activity.follow_requests_for_actor(user)
617 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
618 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
619 |> group_by([a, u], u.id)
620 |> select([a, u], u)
621 |> Repo.all()
622
623 {:ok, users}
624 end
625
626 def increase_note_count(%User{} = user) do
627 User
628 |> where(id: ^user.id)
629 |> update([u],
630 set: [
631 info:
632 fragment(
633 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
634 u.info,
635 u.info
636 )
637 ]
638 )
639 |> select([u], u)
640 |> Repo.update_all([])
641 |> case do
642 {1, [user]} -> set_cache(user)
643 _ -> {:error, user}
644 end
645 end
646
647 def decrease_note_count(%User{} = user) do
648 User
649 |> where(id: ^user.id)
650 |> update([u],
651 set: [
652 info:
653 fragment(
654 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
655 u.info,
656 u.info
657 )
658 ]
659 )
660 |> select([u], u)
661 |> Repo.update_all([])
662 |> case do
663 {1, [user]} -> set_cache(user)
664 _ -> {:error, user}
665 end
666 end
667
668 def update_note_count(%User{} = user) do
669 note_count_query =
670 from(
671 a in Object,
672 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
673 select: count(a.id)
674 )
675
676 note_count = Repo.one(note_count_query)
677
678 info_cng = User.Info.set_note_count(user.info, note_count)
679
680 user
681 |> change()
682 |> put_embed(:info, info_cng)
683 |> update_and_set_cache()
684 end
685
686 def update_follower_count(%User{} = user) do
687 follower_count_query =
688 User.Query.build(%{followers: user, deactivated: false})
689 |> select([u], %{count: count(u.id)})
690
691 User
692 |> where(id: ^user.id)
693 |> join(:inner, [u], s in subquery(follower_count_query))
694 |> update([u, s],
695 set: [
696 info:
697 fragment(
698 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
699 u.info,
700 s.count
701 )
702 ]
703 )
704 |> select([u], u)
705 |> Repo.update_all([])
706 |> case do
707 {1, [user]} -> set_cache(user)
708 _ -> {:error, user}
709 end
710 end
711
712 def remove_duplicated_following(%User{following: following} = user) do
713 uniq_following = Enum.uniq(following)
714
715 if length(following) == length(uniq_following) do
716 {:ok, user}
717 else
718 user
719 |> update_changeset(%{following: uniq_following})
720 |> update_and_set_cache()
721 end
722 end
723
724 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
725 def get_users_from_set(ap_ids, local_only \\ true) do
726 criteria = %{ap_id: ap_ids, deactivated: false}
727 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
728
729 User.Query.build(criteria)
730 |> Repo.all()
731 end
732
733 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
734 def get_recipients_from_activity(%Activity{recipients: to}) do
735 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
736 |> Repo.all()
737 end
738
739 def mute(muter, %User{ap_id: ap_id}) do
740 info_cng =
741 muter.info
742 |> User.Info.add_to_mutes(ap_id)
743
744 cng =
745 change(muter)
746 |> put_embed(:info, info_cng)
747
748 update_and_set_cache(cng)
749 end
750
751 def unmute(muter, %{ap_id: ap_id}) do
752 info_cng =
753 muter.info
754 |> User.Info.remove_from_mutes(ap_id)
755
756 cng =
757 change(muter)
758 |> put_embed(:info, info_cng)
759
760 update_and_set_cache(cng)
761 end
762
763 def subscribe(subscriber, %{ap_id: ap_id}) do
764 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
765
766 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
767 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
768
769 if blocked do
770 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
771 else
772 info_cng =
773 subscribed.info
774 |> User.Info.add_to_subscribers(subscriber.ap_id)
775
776 change(subscribed)
777 |> put_embed(:info, info_cng)
778 |> update_and_set_cache()
779 end
780 end
781 end
782
783 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
784 with %User{} = user <- get_cached_by_ap_id(ap_id) do
785 info_cng =
786 user.info
787 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
788
789 change(user)
790 |> put_embed(:info, info_cng)
791 |> update_and_set_cache()
792 end
793 end
794
795 def block(blocker, %User{ap_id: ap_id} = blocked) do
796 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
797 blocker =
798 if following?(blocker, blocked) do
799 {:ok, blocker, _} = unfollow(blocker, blocked)
800 blocker
801 else
802 blocker
803 end
804
805 blocker =
806 if subscribed_to?(blocked, blocker) do
807 {:ok, blocker} = unsubscribe(blocked, blocker)
808 blocker
809 else
810 blocker
811 end
812
813 if following?(blocked, blocker) do
814 unfollow(blocked, blocker)
815 end
816
817 {:ok, blocker} = update_follower_count(blocker)
818
819 info_cng =
820 blocker.info
821 |> User.Info.add_to_block(ap_id)
822
823 cng =
824 change(blocker)
825 |> put_embed(:info, info_cng)
826
827 update_and_set_cache(cng)
828 end
829
830 # helper to handle the block given only an actor's AP id
831 def block(blocker, %{ap_id: ap_id}) do
832 block(blocker, get_cached_by_ap_id(ap_id))
833 end
834
835 def unblock(blocker, %{ap_id: ap_id}) do
836 info_cng =
837 blocker.info
838 |> User.Info.remove_from_block(ap_id)
839
840 cng =
841 change(blocker)
842 |> put_embed(:info, info_cng)
843
844 update_and_set_cache(cng)
845 end
846
847 def mutes?(nil, _), do: false
848 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
849
850 def blocks?(%User{info: info} = _user, %{ap_id: ap_id}) do
851 blocks = info.blocks
852 domain_blocks = info.domain_blocks
853 %{host: host} = URI.parse(ap_id)
854
855 Enum.member?(blocks, ap_id) || Enum.any?(domain_blocks, &(&1 == host))
856 end
857
858 def subscribed_to?(user, %{ap_id: ap_id}) do
859 with %User{} = target <- get_cached_by_ap_id(ap_id) do
860 Enum.member?(target.info.subscribers, user.ap_id)
861 end
862 end
863
864 @spec muted_users(User.t()) :: [User.t()]
865 def muted_users(user) do
866 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
867 |> Repo.all()
868 end
869
870 @spec blocked_users(User.t()) :: [User.t()]
871 def blocked_users(user) do
872 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
873 |> Repo.all()
874 end
875
876 @spec subscribers(User.t()) :: [User.t()]
877 def subscribers(user) do
878 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
879 |> Repo.all()
880 end
881
882 def block_domain(user, domain) do
883 info_cng =
884 user.info
885 |> User.Info.add_to_domain_block(domain)
886
887 cng =
888 change(user)
889 |> put_embed(:info, info_cng)
890
891 update_and_set_cache(cng)
892 end
893
894 def unblock_domain(user, domain) do
895 info_cng =
896 user.info
897 |> User.Info.remove_from_domain_block(domain)
898
899 cng =
900 change(user)
901 |> put_embed(:info, info_cng)
902
903 update_and_set_cache(cng)
904 end
905
906 def deactivate_async(user, status \\ true) do
907 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
908 end
909
910 def deactivate(%User{} = user, status \\ true) do
911 info_cng = User.Info.set_activation_status(user.info, status)
912
913 with {:ok, friends} <- User.get_friends(user),
914 {:ok, followers} <- User.get_followers(user),
915 {:ok, user} <-
916 user
917 |> change()
918 |> put_embed(:info, info_cng)
919 |> update_and_set_cache() do
920 Enum.each(followers, &invalidate_cache(&1))
921 Enum.each(friends, &update_follower_count(&1))
922
923 {:ok, user}
924 end
925 end
926
927 def update_notification_settings(%User{} = user, settings \\ %{}) do
928 info_changeset = User.Info.update_notification_settings(user.info, settings)
929
930 change(user)
931 |> put_embed(:info, info_changeset)
932 |> update_and_set_cache()
933 end
934
935 @spec delete(User.t()) :: :ok
936 def delete(%User{} = user),
937 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
938
939 @spec perform(atom(), User.t()) :: {:ok, User.t()}
940 def perform(:delete, %User{} = user) do
941 # Remove all relationships
942 {:ok, followers} = User.get_followers(user)
943
944 Enum.each(followers, fn follower ->
945 ActivityPub.unfollow(follower, user)
946 User.unfollow(follower, user)
947 end)
948
949 {:ok, friends} = User.get_friends(user)
950
951 Enum.each(friends, fn followed ->
952 ActivityPub.unfollow(user, followed)
953 User.unfollow(user, followed)
954 end)
955
956 delete_user_activities(user)
957
958 {:ok, _user} = Repo.delete(user)
959 end
960
961 @spec perform(atom(), User.t()) :: {:ok, User.t()}
962 def perform(:fetch_initial_posts, %User{} = user) do
963 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
964
965 Enum.each(
966 # Insert all the posts in reverse order, so they're in the right order on the timeline
967 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
968 &Pleroma.Web.Federator.incoming_ap_doc/1
969 )
970
971 {:ok, user}
972 end
973
974 def perform(:deactivate_async, user, status), do: deactivate(user, status)
975
976 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
977 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
978 when is_list(blocked_identifiers) do
979 Enum.map(
980 blocked_identifiers,
981 fn blocked_identifier ->
982 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
983 {:ok, blocker} <- block(blocker, blocked),
984 {:ok, _} <- ActivityPub.block(blocker, blocked) do
985 blocked
986 else
987 err ->
988 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
989 err
990 end
991 end
992 )
993 end
994
995 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
996 def perform(:follow_import, %User{} = follower, followed_identifiers)
997 when is_list(followed_identifiers) do
998 Enum.map(
999 followed_identifiers,
1000 fn followed_identifier ->
1001 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1002 {:ok, follower} <- maybe_direct_follow(follower, followed),
1003 {:ok, _} <- ActivityPub.follow(follower, followed) do
1004 followed
1005 else
1006 err ->
1007 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1008 err
1009 end
1010 end
1011 )
1012 end
1013
1014 @spec sync_follow_counter() :: :ok
1015 def sync_follow_counter,
1016 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:sync_follow_counters])
1017
1018 @spec perform(:sync_follow_counters) :: :ok
1019 def perform(:sync_follow_counters) do
1020 {:ok, _pid} = Agent.start_link(fn -> %{} end, name: :domain_errors)
1021 config = Pleroma.Config.get([:instance, :external_user_synchronization])
1022
1023 :ok = sync_follow_counters(config)
1024 Agent.stop(:domain_errors)
1025 end
1026
1027 @spec sync_follow_counters(keyword()) :: :ok
1028 def sync_follow_counters(opts \\ []) do
1029 users = external_users(opts)
1030
1031 if length(users) > 0 do
1032 errors = Agent.get(:domain_errors, fn state -> state end)
1033 {last, updated_errors} = User.Synchronization.call(users, errors, opts)
1034 Agent.update(:domain_errors, fn _state -> updated_errors end)
1035 sync_follow_counters(max_id: last.id, limit: opts[:limit])
1036 else
1037 :ok
1038 end
1039 end
1040
1041 @spec external_users(keyword()) :: [User.t()]
1042 def external_users(opts \\ []) do
1043 query =
1044 User.Query.build(%{
1045 external: true,
1046 active: true,
1047 order_by: :id,
1048 select: [:id, :ap_id, :info]
1049 })
1050
1051 query =
1052 if opts[:max_id],
1053 do: where(query, [u], u.id > ^opts[:max_id]),
1054 else: query
1055
1056 query =
1057 if opts[:limit],
1058 do: limit(query, ^opts[:limit]),
1059 else: query
1060
1061 Repo.all(query)
1062 end
1063
1064 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1065 do:
1066 PleromaJobQueue.enqueue(:background, __MODULE__, [
1067 :blocks_import,
1068 blocker,
1069 blocked_identifiers
1070 ])
1071
1072 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1073 do:
1074 PleromaJobQueue.enqueue(:background, __MODULE__, [
1075 :follow_import,
1076 follower,
1077 followed_identifiers
1078 ])
1079
1080 def delete_user_activities(%User{ap_id: ap_id} = user) do
1081 ap_id
1082 |> Activity.query_by_actor()
1083 |> RepoStreamer.chunk_stream(50)
1084 |> Stream.each(fn activities ->
1085 Enum.each(activities, &delete_activity(&1))
1086 end)
1087 |> Stream.run()
1088
1089 {:ok, user}
1090 end
1091
1092 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1093 activity
1094 |> Object.normalize()
1095 |> ActivityPub.delete()
1096 end
1097
1098 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1099 user = get_cached_by_ap_id(activity.actor)
1100 object = Object.normalize(activity)
1101
1102 ActivityPub.unlike(user, object)
1103 end
1104
1105 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1106 user = get_cached_by_ap_id(activity.actor)
1107 object = Object.normalize(activity)
1108
1109 ActivityPub.unannounce(user, object)
1110 end
1111
1112 defp delete_activity(_activity), do: "Doing nothing"
1113
1114 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1115 Pleroma.HTML.Scrubber.TwitterText
1116 end
1117
1118 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1119
1120 def fetch_by_ap_id(ap_id) do
1121 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1122
1123 case ap_try do
1124 {:ok, user} ->
1125 {:ok, user}
1126
1127 _ ->
1128 case OStatus.make_user(ap_id) do
1129 {:ok, user} -> {:ok, user}
1130 _ -> {:error, "Could not fetch by AP id"}
1131 end
1132 end
1133 end
1134
1135 def get_or_fetch_by_ap_id(ap_id) do
1136 user = get_cached_by_ap_id(ap_id)
1137
1138 if !is_nil(user) and !User.needs_update?(user) do
1139 {:ok, user}
1140 else
1141 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1142 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1143
1144 resp = fetch_by_ap_id(ap_id)
1145
1146 if should_fetch_initial do
1147 with {:ok, %User{} = user} <- resp do
1148 fetch_initial_posts(user)
1149 end
1150 end
1151
1152 resp
1153 end
1154 end
1155
1156 def get_or_create_instance_user do
1157 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1158
1159 if user = get_cached_by_ap_id(relay_uri) do
1160 user
1161 else
1162 changes =
1163 %User{info: %User.Info{}}
1164 |> cast(%{}, [:ap_id, :nickname, :local])
1165 |> put_change(:ap_id, relay_uri)
1166 |> put_change(:nickname, nil)
1167 |> put_change(:local, true)
1168 |> put_change(:follower_address, relay_uri <> "/followers")
1169
1170 {:ok, user} = Repo.insert(changes)
1171 user
1172 end
1173 end
1174
1175 # AP style
1176 def public_key_from_info(%{
1177 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1178 }) do
1179 key =
1180 public_key_pem
1181 |> :public_key.pem_decode()
1182 |> hd()
1183 |> :public_key.pem_entry_decode()
1184
1185 {:ok, key}
1186 end
1187
1188 # OStatus Magic Key
1189 def public_key_from_info(%{magic_key: magic_key}) do
1190 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1191 end
1192
1193 def get_public_key_for_ap_id(ap_id) do
1194 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1195 {:ok, public_key} <- public_key_from_info(user.info) do
1196 {:ok, public_key}
1197 else
1198 _ -> :error
1199 end
1200 end
1201
1202 defp blank?(""), do: nil
1203 defp blank?(n), do: n
1204
1205 def insert_or_update_user(data) do
1206 data
1207 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1208 |> remote_user_creation()
1209 |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
1210 |> set_cache()
1211 end
1212
1213 def ap_enabled?(%User{local: true}), do: true
1214 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1215 def ap_enabled?(_), do: false
1216
1217 @doc "Gets or fetch a user by uri or nickname."
1218 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1219 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1220 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1221
1222 # wait a period of time and return newest version of the User structs
1223 # this is because we have synchronous follow APIs and need to simulate them
1224 # with an async handshake
1225 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1226 with %User{} = a <- User.get_cached_by_id(a.id),
1227 %User{} = b <- User.get_cached_by_id(b.id) do
1228 {:ok, a, b}
1229 else
1230 _e ->
1231 :error
1232 end
1233 end
1234
1235 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1236 with :ok <- :timer.sleep(timeout),
1237 %User{} = a <- User.get_cached_by_id(a.id),
1238 %User{} = b <- User.get_cached_by_id(b.id) do
1239 {:ok, a, b}
1240 else
1241 _e ->
1242 :error
1243 end
1244 end
1245
1246 def parse_bio(bio) when is_binary(bio) and bio != "" do
1247 bio
1248 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1249 |> elem(0)
1250 end
1251
1252 def parse_bio(_), do: ""
1253
1254 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1255 # TODO: get profile URLs other than user.ap_id
1256 profile_urls = [user.ap_id]
1257
1258 bio
1259 |> CommonUtils.format_input("text/plain",
1260 mentions_format: :full,
1261 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1262 )
1263 |> elem(0)
1264 end
1265
1266 def parse_bio(_, _), do: ""
1267
1268 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1269 Repo.transaction(fn ->
1270 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1271 end)
1272 end
1273
1274 def tag(nickname, tags) when is_binary(nickname),
1275 do: tag(get_by_nickname(nickname), tags)
1276
1277 def tag(%User{} = user, tags),
1278 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1279
1280 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1281 Repo.transaction(fn ->
1282 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1283 end)
1284 end
1285
1286 def untag(nickname, tags) when is_binary(nickname),
1287 do: untag(get_by_nickname(nickname), tags)
1288
1289 def untag(%User{} = user, tags),
1290 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1291
1292 defp update_tags(%User{} = user, new_tags) do
1293 {:ok, updated_user} =
1294 user
1295 |> change(%{tags: new_tags})
1296 |> update_and_set_cache()
1297
1298 updated_user
1299 end
1300
1301 defp normalize_tags(tags) do
1302 [tags]
1303 |> List.flatten()
1304 |> Enum.map(&String.downcase(&1))
1305 end
1306
1307 defp local_nickname_regex do
1308 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1309 @extended_local_nickname_regex
1310 else
1311 @strict_local_nickname_regex
1312 end
1313 end
1314
1315 def local_nickname(nickname_or_mention) do
1316 nickname_or_mention
1317 |> full_nickname()
1318 |> String.split("@")
1319 |> hd()
1320 end
1321
1322 def full_nickname(nickname_or_mention),
1323 do: String.trim_leading(nickname_or_mention, "@")
1324
1325 def error_user(ap_id) do
1326 %User{
1327 name: ap_id,
1328 ap_id: ap_id,
1329 info: %User.Info{},
1330 nickname: "erroruser@example.com",
1331 inserted_at: NaiveDateTime.utc_now()
1332 }
1333 end
1334
1335 @spec all_superusers() :: [User.t()]
1336 def all_superusers do
1337 User.Query.build(%{super_users: true, local: true, deactivated: false})
1338 |> Repo.all()
1339 end
1340
1341 def showing_reblogs?(%User{} = user, %User{} = target) do
1342 target.ap_id not in user.info.muted_reblogs
1343 end
1344
1345 @doc """
1346 The function returns a query to get users with no activity for given interval of days.
1347 Inactive users are those who didn't read any notification, or had any activity where
1348 the user is the activity's actor, during `inactivity_threshold` days.
1349 Deactivated users will not appear in this list.
1350
1351 ## Examples
1352
1353 iex> Pleroma.User.list_inactive_users()
1354 %Ecto.Query{}
1355 """
1356 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1357 def list_inactive_users_query(inactivity_threshold \\ 7) do
1358 negative_inactivity_threshold = -inactivity_threshold
1359 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1360 # Subqueries are not supported in `where` clauses, join gets too complicated.
1361 has_read_notifications =
1362 from(n in Pleroma.Notification,
1363 where: n.seen == true,
1364 group_by: n.id,
1365 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1366 select: n.user_id
1367 )
1368 |> Pleroma.Repo.all()
1369
1370 from(u in Pleroma.User,
1371 left_join: a in Pleroma.Activity,
1372 on: u.ap_id == a.actor,
1373 where: not is_nil(u.nickname),
1374 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1375 where: u.id not in ^has_read_notifications,
1376 group_by: u.id,
1377 having:
1378 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1379 is_nil(max(a.inserted_at))
1380 )
1381 end
1382
1383 @doc """
1384 Enable or disable email notifications for user
1385
1386 ## Examples
1387
1388 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1389 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1390
1391 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1392 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1393 """
1394 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1395 {:ok, t()} | {:error, Ecto.Changeset.t()}
1396 def switch_email_notifications(user, type, status) do
1397 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1398
1399 change(user)
1400 |> put_embed(:info, info)
1401 |> update_and_set_cache()
1402 end
1403
1404 @doc """
1405 Set `last_digest_emailed_at` value for the user to current time
1406 """
1407 @spec touch_last_digest_emailed_at(t()) :: t()
1408 def touch_last_digest_emailed_at(user) do
1409 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1410
1411 {:ok, updated_user} =
1412 user
1413 |> change(%{last_digest_emailed_at: now})
1414 |> update_and_set_cache()
1415
1416 updated_user
1417 end
1418
1419 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1420 def toggle_confirmation(%User{} = user) do
1421 need_confirmation? = !user.info.confirmation_pending
1422
1423 info_changeset =
1424 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1425
1426 user
1427 |> change()
1428 |> put_embed(:info, info_changeset)
1429 |> update_and_set_cache()
1430 end
1431
1432 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1433 mascot
1434 end
1435
1436 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1437 # use instance-default
1438 config = Pleroma.Config.get([:assets, :mascots])
1439 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1440 mascot = Keyword.get(config, default_mascot)
1441
1442 %{
1443 "id" => "default-mascot",
1444 "url" => mascot[:url],
1445 "preview_url" => mascot[:url],
1446 "pleroma" => %{
1447 "mime_type" => mascot[:mime_type]
1448 }
1449 }
1450 end
1451
1452 def ensure_keys_present(user) do
1453 info = user.info
1454
1455 if info.keys do
1456 {:ok, user}
1457 else
1458 {:ok, pem} = Keys.generate_rsa_pem()
1459
1460 info_cng =
1461 info
1462 |> User.Info.set_keys(pem)
1463
1464 cng =
1465 Ecto.Changeset.change(user)
1466 |> Ecto.Changeset.put_embed(:info, info_cng)
1467
1468 update_and_set_cache(cng)
1469 end
1470 end
1471
1472 def get_ap_ids_by_nicknames(nicknames) do
1473 from(u in User,
1474 where: u.nickname in ^nicknames,
1475 select: u.ap_id
1476 )
1477 |> Repo.all()
1478 end
1479
1480 defdelegate search(query, opts \\ []), to: User.Search
1481
1482 defp put_password_hash(
1483 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1484 ) do
1485 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1486 end
1487
1488 defp put_password_hash(changeset), do: changeset
1489 end