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 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1162 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1163 if user = get_cached_by_ap_id(uri) do
1164 user
1165 else
1166 changes =
1167 %User{info: %User.Info{}}
1168 |> cast(%{}, [:ap_id, :nickname, :local])
1169 |> put_change(:ap_id, uri)
1170 |> put_change(:nickname, nickname)
1171 |> put_change(:local, true)
1172 |> put_change(:follower_address, uri <> "/followers")
1173
1174 {:ok, user} = Repo.insert(changes)
1175 user
1176 end
1177 end
1178
1179 # AP style
1180 def public_key_from_info(%{
1181 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1182 }) do
1183 key =
1184 public_key_pem
1185 |> :public_key.pem_decode()
1186 |> hd()
1187 |> :public_key.pem_entry_decode()
1188
1189 {:ok, key}
1190 end
1191
1192 # OStatus Magic Key
1193 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1194 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1195 end
1196
1197 def public_key_from_info(_), do: {:error, "not found key"}
1198
1199 def get_public_key_for_ap_id(ap_id) do
1200 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1201 {:ok, public_key} <- public_key_from_info(user.info) do
1202 {:ok, public_key}
1203 else
1204 _ -> :error
1205 end
1206 end
1207
1208 defp blank?(""), do: nil
1209 defp blank?(n), do: n
1210
1211 def insert_or_update_user(data) do
1212 data
1213 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1214 |> remote_user_creation()
1215 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1216 |> set_cache()
1217 end
1218
1219 def ap_enabled?(%User{local: true}), do: true
1220 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1221 def ap_enabled?(_), do: false
1222
1223 @doc "Gets or fetch a user by uri or nickname."
1224 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1225 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1226 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1227
1228 # wait a period of time and return newest version of the User structs
1229 # this is because we have synchronous follow APIs and need to simulate them
1230 # with an async handshake
1231 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1232 with %User{} = a <- User.get_cached_by_id(a.id),
1233 %User{} = b <- User.get_cached_by_id(b.id) do
1234 {:ok, a, b}
1235 else
1236 _e ->
1237 :error
1238 end
1239 end
1240
1241 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1242 with :ok <- :timer.sleep(timeout),
1243 %User{} = a <- User.get_cached_by_id(a.id),
1244 %User{} = b <- User.get_cached_by_id(b.id) do
1245 {:ok, a, b}
1246 else
1247 _e ->
1248 :error
1249 end
1250 end
1251
1252 def parse_bio(bio) when is_binary(bio) and bio != "" do
1253 bio
1254 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1255 |> elem(0)
1256 end
1257
1258 def parse_bio(_), do: ""
1259
1260 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1261 # TODO: get profile URLs other than user.ap_id
1262 profile_urls = [user.ap_id]
1263
1264 bio
1265 |> CommonUtils.format_input("text/plain",
1266 mentions_format: :full,
1267 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1268 )
1269 |> elem(0)
1270 end
1271
1272 def parse_bio(_, _), do: ""
1273
1274 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1275 Repo.transaction(fn ->
1276 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1277 end)
1278 end
1279
1280 def tag(nickname, tags) when is_binary(nickname),
1281 do: tag(get_by_nickname(nickname), tags)
1282
1283 def tag(%User{} = user, tags),
1284 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1285
1286 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1287 Repo.transaction(fn ->
1288 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1289 end)
1290 end
1291
1292 def untag(nickname, tags) when is_binary(nickname),
1293 do: untag(get_by_nickname(nickname), tags)
1294
1295 def untag(%User{} = user, tags),
1296 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1297
1298 defp update_tags(%User{} = user, new_tags) do
1299 {:ok, updated_user} =
1300 user
1301 |> change(%{tags: new_tags})
1302 |> update_and_set_cache()
1303
1304 updated_user
1305 end
1306
1307 defp normalize_tags(tags) do
1308 [tags]
1309 |> List.flatten()
1310 |> Enum.map(&String.downcase(&1))
1311 end
1312
1313 defp local_nickname_regex do
1314 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1315 @extended_local_nickname_regex
1316 else
1317 @strict_local_nickname_regex
1318 end
1319 end
1320
1321 def local_nickname(nickname_or_mention) do
1322 nickname_or_mention
1323 |> full_nickname()
1324 |> String.split("@")
1325 |> hd()
1326 end
1327
1328 def full_nickname(nickname_or_mention),
1329 do: String.trim_leading(nickname_or_mention, "@")
1330
1331 def error_user(ap_id) do
1332 %User{
1333 name: ap_id,
1334 ap_id: ap_id,
1335 info: %User.Info{},
1336 nickname: "erroruser@example.com",
1337 inserted_at: NaiveDateTime.utc_now()
1338 }
1339 end
1340
1341 @spec all_superusers() :: [User.t()]
1342 def all_superusers do
1343 User.Query.build(%{super_users: true, local: true, deactivated: false})
1344 |> Repo.all()
1345 end
1346
1347 def showing_reblogs?(%User{} = user, %User{} = target) do
1348 target.ap_id not in user.info.muted_reblogs
1349 end
1350
1351 @doc """
1352 The function returns a query to get users with no activity for given interval of days.
1353 Inactive users are those who didn't read any notification, or had any activity where
1354 the user is the activity's actor, during `inactivity_threshold` days.
1355 Deactivated users will not appear in this list.
1356
1357 ## Examples
1358
1359 iex> Pleroma.User.list_inactive_users()
1360 %Ecto.Query{}
1361 """
1362 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1363 def list_inactive_users_query(inactivity_threshold \\ 7) do
1364 negative_inactivity_threshold = -inactivity_threshold
1365 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1366 # Subqueries are not supported in `where` clauses, join gets too complicated.
1367 has_read_notifications =
1368 from(n in Pleroma.Notification,
1369 where: n.seen == true,
1370 group_by: n.id,
1371 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1372 select: n.user_id
1373 )
1374 |> Pleroma.Repo.all()
1375
1376 from(u in Pleroma.User,
1377 left_join: a in Pleroma.Activity,
1378 on: u.ap_id == a.actor,
1379 where: not is_nil(u.nickname),
1380 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1381 where: u.id not in ^has_read_notifications,
1382 group_by: u.id,
1383 having:
1384 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1385 is_nil(max(a.inserted_at))
1386 )
1387 end
1388
1389 @doc """
1390 Enable or disable email notifications for user
1391
1392 ## Examples
1393
1394 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1395 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1396
1397 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1398 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1399 """
1400 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1401 {:ok, t()} | {:error, Ecto.Changeset.t()}
1402 def switch_email_notifications(user, type, status) do
1403 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1404
1405 change(user)
1406 |> put_embed(:info, info)
1407 |> update_and_set_cache()
1408 end
1409
1410 @doc """
1411 Set `last_digest_emailed_at` value for the user to current time
1412 """
1413 @spec touch_last_digest_emailed_at(t()) :: t()
1414 def touch_last_digest_emailed_at(user) do
1415 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1416
1417 {:ok, updated_user} =
1418 user
1419 |> change(%{last_digest_emailed_at: now})
1420 |> update_and_set_cache()
1421
1422 updated_user
1423 end
1424
1425 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1426 def toggle_confirmation(%User{} = user) do
1427 need_confirmation? = !user.info.confirmation_pending
1428
1429 info_changeset =
1430 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1431
1432 user
1433 |> change()
1434 |> put_embed(:info, info_changeset)
1435 |> update_and_set_cache()
1436 end
1437
1438 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1439 mascot
1440 end
1441
1442 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1443 # use instance-default
1444 config = Pleroma.Config.get([:assets, :mascots])
1445 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1446 mascot = Keyword.get(config, default_mascot)
1447
1448 %{
1449 "id" => "default-mascot",
1450 "url" => mascot[:url],
1451 "preview_url" => mascot[:url],
1452 "pleroma" => %{
1453 "mime_type" => mascot[:mime_type]
1454 }
1455 }
1456 end
1457
1458 def ensure_keys_present(%User{info: info} = user) do
1459 if info.keys do
1460 {:ok, user}
1461 else
1462 {:ok, pem} = Keys.generate_rsa_pem()
1463
1464 user
1465 |> Ecto.Changeset.change()
1466 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1467 |> update_and_set_cache()
1468 end
1469 end
1470
1471 def get_ap_ids_by_nicknames(nicknames) do
1472 from(u in User,
1473 where: u.nickname in ^nicknames,
1474 select: u.ap_id
1475 )
1476 |> Repo.all()
1477 end
1478
1479 defdelegate search(query, opts \\ []), to: User.Search
1480
1481 defp put_password_hash(
1482 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1483 ) do
1484 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1485 end
1486
1487 defp put_password_hash(changeset), do: changeset
1488
1489 def is_internal_user?(%User{nickname: nil}), do: true
1490 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1491 def is_internal_user?(_), do: false
1492 end