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