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