Merge branch 'feature/hide-follows-remote' into 'develop'
[akkoma] / lib / pleroma / user.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.User do
6 use Ecto.Schema
7
8 import Ecto.Changeset
9 import Ecto.Query
10
11 alias Comeonin.Pbkdf2
12 alias Ecto.Multi
13 alias Pleroma.Activity
14 alias Pleroma.Keys
15 alias Pleroma.Notification
16 alias Pleroma.Object
17 alias Pleroma.Registration
18 alias Pleroma.Repo
19 alias Pleroma.RepoStreamer
20 alias Pleroma.User
21 alias Pleroma.Web
22 alias Pleroma.Web.ActivityPub.ActivityPub
23 alias Pleroma.Web.ActivityPub.Utils
24 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
25 alias Pleroma.Web.OAuth
26 alias Pleroma.Web.OStatus
27 alias Pleroma.Web.RelMe
28 alias Pleroma.Web.Websub
29
30 require Logger
31
32 @type t :: %__MODULE__{}
33
34 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
35
36 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
37 @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
38
39 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
40 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
41
42 schema "users" do
43 field(:bio, :string)
44 field(:email, :string)
45 field(:name, :string)
46 field(:nickname, :string)
47 field(:password_hash, :string)
48 field(:password, :string, virtual: true)
49 field(:password_confirmation, :string, virtual: true)
50 field(:following, {:array, :string}, default: [])
51 field(:ap_id, :string)
52 field(:avatar, :map)
53 field(:local, :boolean, default: true)
54 field(:follower_address, :string)
55 field(: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 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
462 # of the ap_id and the domain and tries to get that user
463 def get_by_guessed_nickname(ap_id) do
464 domain = URI.parse(ap_id).host
465 name = List.last(String.split(ap_id, "/"))
466 nickname = "#{name}@#{domain}"
467
468 get_cached_by_nickname(nickname)
469 end
470
471 def set_cache({:ok, user}), do: set_cache(user)
472 def set_cache({:error, err}), do: {:error, err}
473
474 def set_cache(%User{} = user) do
475 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
476 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
477 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
478 {:ok, user}
479 end
480
481 def update_and_set_cache(changeset) do
482 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
483 set_cache(user)
484 else
485 e -> e
486 end
487 end
488
489 def invalidate_cache(user) do
490 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
491 Cachex.del(:user_cache, "nickname:#{user.nickname}")
492 Cachex.del(:user_cache, "user_info:#{user.id}")
493 end
494
495 def get_cached_by_ap_id(ap_id) do
496 key = "ap_id:#{ap_id}"
497 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
498 end
499
500 def get_cached_by_id(id) do
501 key = "id:#{id}"
502
503 ap_id =
504 Cachex.fetch!(:user_cache, key, fn _ ->
505 user = get_by_id(id)
506
507 if user do
508 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
509 {:commit, user.ap_id}
510 else
511 {:ignore, ""}
512 end
513 end)
514
515 get_cached_by_ap_id(ap_id)
516 end
517
518 def get_cached_by_nickname(nickname) do
519 key = "nickname:#{nickname}"
520
521 Cachex.fetch!(:user_cache, key, fn ->
522 user_result = get_or_fetch_by_nickname(nickname)
523
524 case user_result do
525 {:ok, user} -> {:commit, user}
526 {:error, _error} -> {:ignore, nil}
527 end
528 end)
529 end
530
531 def get_cached_by_nickname_or_id(nickname_or_id) do
532 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
533 end
534
535 def get_by_nickname(nickname) do
536 Repo.get_by(User, nickname: nickname) ||
537 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
538 Repo.get_by(User, nickname: local_nickname(nickname))
539 end
540 end
541
542 def get_by_email(email), do: Repo.get_by(User, email: email)
543
544 def get_by_nickname_or_email(nickname_or_email) do
545 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
546 end
547
548 def get_cached_user_info(user) do
549 key = "user_info:#{user.id}"
550 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
551 end
552
553 def fetch_by_nickname(nickname) do
554 ap_try = ActivityPub.make_user_from_nickname(nickname)
555
556 case ap_try do
557 {:ok, user} -> {:ok, user}
558 _ -> OStatus.make_user(nickname)
559 end
560 end
561
562 def get_or_fetch_by_nickname(nickname) do
563 with %User{} = user <- get_by_nickname(nickname) do
564 {:ok, user}
565 else
566 _e ->
567 with [_nick, _domain] <- String.split(nickname, "@"),
568 {:ok, user} <- fetch_by_nickname(nickname) do
569 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
570 fetch_initial_posts(user)
571 end
572
573 {:ok, user}
574 else
575 _e -> {:error, "not found " <> nickname}
576 end
577 end
578 end
579
580 @doc "Fetch some posts when the user has just been federated with"
581 def fetch_initial_posts(user),
582 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
583
584 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
585 def get_followers_query(%User{} = user, nil) do
586 User.Query.build(%{followers: user, deactivated: false})
587 end
588
589 def get_followers_query(user, page) do
590 from(u in get_followers_query(user, nil))
591 |> User.Query.paginate(page, 20)
592 end
593
594 @spec get_followers_query(User.t()) :: Ecto.Query.t()
595 def get_followers_query(user), do: get_followers_query(user, nil)
596
597 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
598 def get_followers(user, page \\ nil) do
599 q = get_followers_query(user, page)
600
601 {:ok, Repo.all(q)}
602 end
603
604 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
605 def get_external_followers(user, page \\ nil) do
606 q =
607 user
608 |> get_followers_query(page)
609 |> User.Query.build(%{external: true})
610
611 {:ok, Repo.all(q)}
612 end
613
614 def get_followers_ids(user, page \\ nil) do
615 q = get_followers_query(user, page)
616
617 Repo.all(from(u in q, select: u.id))
618 end
619
620 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
621 def get_friends_query(%User{} = user, nil) do
622 User.Query.build(%{friends: user, deactivated: false})
623 end
624
625 def get_friends_query(user, page) do
626 from(u in get_friends_query(user, nil))
627 |> User.Query.paginate(page, 20)
628 end
629
630 @spec get_friends_query(User.t()) :: Ecto.Query.t()
631 def get_friends_query(user), do: get_friends_query(user, nil)
632
633 def get_friends(user, page \\ nil) do
634 q = get_friends_query(user, page)
635
636 {:ok, Repo.all(q)}
637 end
638
639 def get_friends_ids(user, page \\ nil) do
640 q = get_friends_query(user, page)
641
642 Repo.all(from(u in q, select: u.id))
643 end
644
645 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
646 def get_follow_requests(%User{} = user) do
647 users =
648 Activity.follow_requests_for_actor(user)
649 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
650 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
651 |> group_by([a, u], u.id)
652 |> select([a, u], u)
653 |> Repo.all()
654
655 {:ok, users}
656 end
657
658 def increase_note_count(%User{} = user) do
659 User
660 |> where(id: ^user.id)
661 |> update([u],
662 set: [
663 info:
664 fragment(
665 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
666 u.info,
667 u.info
668 )
669 ]
670 )
671 |> select([u], u)
672 |> Repo.update_all([])
673 |> case do
674 {1, [user]} -> set_cache(user)
675 _ -> {:error, user}
676 end
677 end
678
679 def decrease_note_count(%User{} = user) do
680 User
681 |> where(id: ^user.id)
682 |> update([u],
683 set: [
684 info:
685 fragment(
686 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
687 u.info,
688 u.info
689 )
690 ]
691 )
692 |> select([u], u)
693 |> Repo.update_all([])
694 |> case do
695 {1, [user]} -> set_cache(user)
696 _ -> {:error, user}
697 end
698 end
699
700 def update_note_count(%User{} = user) do
701 note_count_query =
702 from(
703 a in Object,
704 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
705 select: count(a.id)
706 )
707
708 note_count = Repo.one(note_count_query)
709
710 info_cng = User.Info.set_note_count(user.info, note_count)
711
712 user
713 |> change()
714 |> put_embed(:info, info_cng)
715 |> update_and_set_cache()
716 end
717
718 def maybe_fetch_follow_information(user) do
719 with {:ok, user} <- fetch_follow_information(user) do
720 user
721 else
722 e ->
723 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
724
725 user
726 end
727 end
728
729 def fetch_follow_information(user) do
730 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
731 info_cng = User.Info.follow_information_update(user.info, info)
732
733 changeset =
734 user
735 |> change()
736 |> put_embed(:info, info_cng)
737
738 update_and_set_cache(changeset)
739 else
740 {:error, _} = e -> e
741 e -> {:error, e}
742 end
743 end
744
745 def update_follower_count(%User{} = user) do
746 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
747 follower_count_query =
748 User.Query.build(%{followers: user, deactivated: false})
749 |> select([u], %{count: count(u.id)})
750
751 User
752 |> where(id: ^user.id)
753 |> join(:inner, [u], s in subquery(follower_count_query))
754 |> update([u, s],
755 set: [
756 info:
757 fragment(
758 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
759 u.info,
760 s.count
761 )
762 ]
763 )
764 |> select([u], u)
765 |> Repo.update_all([])
766 |> case do
767 {1, [user]} -> set_cache(user)
768 _ -> {:error, user}
769 end
770 else
771 {:ok, maybe_fetch_follow_information(user)}
772 end
773 end
774
775 def maybe_update_following_count(%User{local: false} = user) do
776 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
777 {:ok, maybe_fetch_follow_information(user)}
778 else
779 user
780 end
781 end
782
783 def maybe_update_following_count(user), do: user
784
785 def remove_duplicated_following(%User{following: following} = user) do
786 uniq_following = Enum.uniq(following)
787
788 if length(following) == length(uniq_following) do
789 {:ok, user}
790 else
791 user
792 |> update_changeset(%{following: uniq_following})
793 |> update_and_set_cache()
794 end
795 end
796
797 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
798 def get_users_from_set(ap_ids, local_only \\ true) do
799 criteria = %{ap_id: ap_ids, deactivated: false}
800 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
801
802 User.Query.build(criteria)
803 |> Repo.all()
804 end
805
806 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
807 def get_recipients_from_activity(%Activity{recipients: to}) do
808 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
809 |> Repo.all()
810 end
811
812 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
813 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
814 info = muter.info
815
816 info_cng =
817 User.Info.add_to_mutes(info, ap_id)
818 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
819
820 cng =
821 change(muter)
822 |> put_embed(:info, info_cng)
823
824 update_and_set_cache(cng)
825 end
826
827 def unmute(muter, %{ap_id: ap_id}) do
828 info = muter.info
829
830 info_cng =
831 User.Info.remove_from_mutes(info, ap_id)
832 |> User.Info.remove_from_muted_notifications(info, ap_id)
833
834 cng =
835 change(muter)
836 |> put_embed(:info, info_cng)
837
838 update_and_set_cache(cng)
839 end
840
841 def subscribe(subscriber, %{ap_id: ap_id}) do
842 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
843
844 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
845 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
846
847 if blocked do
848 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
849 else
850 info_cng =
851 subscribed.info
852 |> User.Info.add_to_subscribers(subscriber.ap_id)
853
854 change(subscribed)
855 |> put_embed(:info, info_cng)
856 |> update_and_set_cache()
857 end
858 end
859 end
860
861 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
862 with %User{} = user <- get_cached_by_ap_id(ap_id) do
863 info_cng =
864 user.info
865 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
866
867 change(user)
868 |> put_embed(:info, info_cng)
869 |> update_and_set_cache()
870 end
871 end
872
873 def block(blocker, %User{ap_id: ap_id} = blocked) do
874 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
875 blocker =
876 if following?(blocker, blocked) do
877 {:ok, blocker, _} = unfollow(blocker, blocked)
878 blocker
879 else
880 blocker
881 end
882
883 blocker =
884 if subscribed_to?(blocked, blocker) do
885 {:ok, blocker} = unsubscribe(blocked, blocker)
886 blocker
887 else
888 blocker
889 end
890
891 if following?(blocked, blocker) do
892 unfollow(blocked, blocker)
893 end
894
895 {:ok, blocker} = update_follower_count(blocker)
896
897 info_cng =
898 blocker.info
899 |> User.Info.add_to_block(ap_id)
900
901 cng =
902 change(blocker)
903 |> put_embed(:info, info_cng)
904
905 update_and_set_cache(cng)
906 end
907
908 # helper to handle the block given only an actor's AP id
909 def block(blocker, %{ap_id: ap_id}) do
910 block(blocker, get_cached_by_ap_id(ap_id))
911 end
912
913 def unblock(blocker, %{ap_id: ap_id}) do
914 info_cng =
915 blocker.info
916 |> User.Info.remove_from_block(ap_id)
917
918 cng =
919 change(blocker)
920 |> put_embed(:info, info_cng)
921
922 update_and_set_cache(cng)
923 end
924
925 def mutes?(nil, _), do: false
926 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
927
928 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
929 def muted_notifications?(nil, _), do: false
930
931 def muted_notifications?(user, %{ap_id: ap_id}),
932 do: Enum.member?(user.info.muted_notifications, ap_id)
933
934 def blocks?(%User{} = user, %User{} = target) do
935 blocks_ap_id?(user, target) || blocks_domain?(user, target)
936 end
937
938 def blocks?(nil, _), do: false
939
940 def blocks_ap_id?(%User{} = user, %User{} = target) do
941 Enum.member?(user.info.blocks, target.ap_id)
942 end
943
944 def blocks_ap_id?(_, _), do: false
945
946 def blocks_domain?(%User{} = user, %User{} = target) do
947 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
948 %{host: host} = URI.parse(target.ap_id)
949 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
950 end
951
952 def blocks_domain?(_, _), do: false
953
954 def subscribed_to?(user, %{ap_id: ap_id}) do
955 with %User{} = target <- get_cached_by_ap_id(ap_id) do
956 Enum.member?(target.info.subscribers, user.ap_id)
957 end
958 end
959
960 @spec muted_users(User.t()) :: [User.t()]
961 def muted_users(user) do
962 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
963 |> Repo.all()
964 end
965
966 @spec blocked_users(User.t()) :: [User.t()]
967 def blocked_users(user) do
968 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
969 |> Repo.all()
970 end
971
972 @spec subscribers(User.t()) :: [User.t()]
973 def subscribers(user) do
974 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
975 |> Repo.all()
976 end
977
978 def block_domain(user, domain) do
979 info_cng =
980 user.info
981 |> User.Info.add_to_domain_block(domain)
982
983 cng =
984 change(user)
985 |> put_embed(:info, info_cng)
986
987 update_and_set_cache(cng)
988 end
989
990 def unblock_domain(user, domain) do
991 info_cng =
992 user.info
993 |> User.Info.remove_from_domain_block(domain)
994
995 cng =
996 change(user)
997 |> put_embed(:info, info_cng)
998
999 update_and_set_cache(cng)
1000 end
1001
1002 def deactivate_async(user, status \\ true) do
1003 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
1004 end
1005
1006 def deactivate(%User{} = user, status \\ true) do
1007 info_cng = User.Info.set_activation_status(user.info, status)
1008
1009 with {:ok, friends} <- User.get_friends(user),
1010 {:ok, followers} <- User.get_followers(user),
1011 {:ok, user} <-
1012 user
1013 |> change()
1014 |> put_embed(:info, info_cng)
1015 |> update_and_set_cache() do
1016 Enum.each(followers, &invalidate_cache(&1))
1017 Enum.each(friends, &update_follower_count(&1))
1018
1019 {:ok, user}
1020 end
1021 end
1022
1023 def update_notification_settings(%User{} = user, settings \\ %{}) do
1024 info_changeset = User.Info.update_notification_settings(user.info, settings)
1025
1026 change(user)
1027 |> put_embed(:info, info_changeset)
1028 |> update_and_set_cache()
1029 end
1030
1031 @spec delete(User.t()) :: :ok
1032 def delete(%User{} = user),
1033 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1034
1035 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1036 def perform(:delete, %User{} = user) do
1037 {:ok, _user} = ActivityPub.delete(user)
1038
1039 # Remove all relationships
1040 {:ok, followers} = User.get_followers(user)
1041
1042 Enum.each(followers, fn follower ->
1043 ActivityPub.unfollow(follower, user)
1044 User.unfollow(follower, user)
1045 end)
1046
1047 {:ok, friends} = User.get_friends(user)
1048
1049 Enum.each(friends, fn followed ->
1050 ActivityPub.unfollow(user, followed)
1051 User.unfollow(user, followed)
1052 end)
1053
1054 delete_user_activities(user)
1055 invalidate_cache(user)
1056 Repo.delete(user)
1057 end
1058
1059 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1060 def perform(:fetch_initial_posts, %User{} = user) do
1061 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1062
1063 Enum.each(
1064 # Insert all the posts in reverse order, so they're in the right order on the timeline
1065 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1066 &Pleroma.Web.Federator.incoming_ap_doc/1
1067 )
1068
1069 {:ok, user}
1070 end
1071
1072 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1073
1074 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1075 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1076 when is_list(blocked_identifiers) do
1077 Enum.map(
1078 blocked_identifiers,
1079 fn blocked_identifier ->
1080 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1081 {:ok, blocker} <- block(blocker, blocked),
1082 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1083 blocked
1084 else
1085 err ->
1086 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1087 err
1088 end
1089 end
1090 )
1091 end
1092
1093 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1094 def perform(:follow_import, %User{} = follower, followed_identifiers)
1095 when is_list(followed_identifiers) do
1096 Enum.map(
1097 followed_identifiers,
1098 fn followed_identifier ->
1099 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1100 {:ok, follower} <- maybe_direct_follow(follower, followed),
1101 {:ok, _} <- ActivityPub.follow(follower, followed) do
1102 followed
1103 else
1104 err ->
1105 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1106 err
1107 end
1108 end
1109 )
1110 end
1111
1112 @spec external_users_query() :: Ecto.Query.t()
1113 def external_users_query do
1114 User.Query.build(%{
1115 external: true,
1116 active: true,
1117 order_by: :id
1118 })
1119 end
1120
1121 @spec external_users(keyword()) :: [User.t()]
1122 def external_users(opts \\ []) do
1123 query =
1124 external_users_query()
1125 |> select([u], struct(u, [:id, :ap_id, :info]))
1126
1127 query =
1128 if opts[:max_id],
1129 do: where(query, [u], u.id > ^opts[:max_id]),
1130 else: query
1131
1132 query =
1133 if opts[:limit],
1134 do: limit(query, ^opts[:limit]),
1135 else: query
1136
1137 Repo.all(query)
1138 end
1139
1140 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1141 do:
1142 PleromaJobQueue.enqueue(:background, __MODULE__, [
1143 :blocks_import,
1144 blocker,
1145 blocked_identifiers
1146 ])
1147
1148 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1149 do:
1150 PleromaJobQueue.enqueue(:background, __MODULE__, [
1151 :follow_import,
1152 follower,
1153 followed_identifiers
1154 ])
1155
1156 def delete_user_activities(%User{ap_id: ap_id} = user) do
1157 ap_id
1158 |> Activity.query_by_actor()
1159 |> RepoStreamer.chunk_stream(50)
1160 |> Stream.each(fn activities ->
1161 Enum.each(activities, &delete_activity(&1))
1162 end)
1163 |> Stream.run()
1164
1165 {:ok, user}
1166 end
1167
1168 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1169 activity
1170 |> Object.normalize()
1171 |> ActivityPub.delete()
1172 end
1173
1174 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1175 user = get_cached_by_ap_id(activity.actor)
1176 object = Object.normalize(activity)
1177
1178 ActivityPub.unlike(user, object)
1179 end
1180
1181 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1182 user = get_cached_by_ap_id(activity.actor)
1183 object = Object.normalize(activity)
1184
1185 ActivityPub.unannounce(user, object)
1186 end
1187
1188 defp delete_activity(_activity), do: "Doing nothing"
1189
1190 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1191 Pleroma.HTML.Scrubber.TwitterText
1192 end
1193
1194 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1195
1196 def fetch_by_ap_id(ap_id) do
1197 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1198
1199 case ap_try do
1200 {:ok, user} ->
1201 {:ok, user}
1202
1203 _ ->
1204 case OStatus.make_user(ap_id) do
1205 {:ok, user} -> {:ok, user}
1206 _ -> {:error, "Could not fetch by AP id"}
1207 end
1208 end
1209 end
1210
1211 def get_or_fetch_by_ap_id(ap_id) do
1212 user = get_cached_by_ap_id(ap_id)
1213
1214 if !is_nil(user) and !User.needs_update?(user) do
1215 {:ok, user}
1216 else
1217 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1218 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1219
1220 resp = fetch_by_ap_id(ap_id)
1221
1222 if should_fetch_initial do
1223 with {:ok, %User{} = user} <- resp do
1224 fetch_initial_posts(user)
1225 end
1226 end
1227
1228 resp
1229 end
1230 end
1231
1232 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1233 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1234 if user = get_cached_by_ap_id(uri) do
1235 user
1236 else
1237 changes =
1238 %User{info: %User.Info{}}
1239 |> cast(%{}, [:ap_id, :nickname, :local])
1240 |> put_change(:ap_id, uri)
1241 |> put_change(:nickname, nickname)
1242 |> put_change(:local, true)
1243 |> put_change(:follower_address, uri <> "/followers")
1244
1245 {:ok, user} = Repo.insert(changes)
1246 user
1247 end
1248 end
1249
1250 # AP style
1251 def public_key_from_info(%{
1252 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1253 }) do
1254 key =
1255 public_key_pem
1256 |> :public_key.pem_decode()
1257 |> hd()
1258 |> :public_key.pem_entry_decode()
1259
1260 {:ok, key}
1261 end
1262
1263 # OStatus Magic Key
1264 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1265 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1266 end
1267
1268 def public_key_from_info(_), do: {:error, "not found key"}
1269
1270 def get_public_key_for_ap_id(ap_id) do
1271 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1272 {:ok, public_key} <- public_key_from_info(user.info) do
1273 {:ok, public_key}
1274 else
1275 _ -> :error
1276 end
1277 end
1278
1279 defp blank?(""), do: nil
1280 defp blank?(n), do: n
1281
1282 def insert_or_update_user(data) do
1283 data
1284 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1285 |> remote_user_creation()
1286 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1287 |> set_cache()
1288 end
1289
1290 def ap_enabled?(%User{local: true}), do: true
1291 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1292 def ap_enabled?(_), do: false
1293
1294 @doc "Gets or fetch a user by uri or nickname."
1295 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1296 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1297 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1298
1299 # wait a period of time and return newest version of the User structs
1300 # this is because we have synchronous follow APIs and need to simulate them
1301 # with an async handshake
1302 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1303 with %User{} = a <- User.get_cached_by_id(a.id),
1304 %User{} = b <- User.get_cached_by_id(b.id) do
1305 {:ok, a, b}
1306 else
1307 _e ->
1308 :error
1309 end
1310 end
1311
1312 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1313 with :ok <- :timer.sleep(timeout),
1314 %User{} = a <- User.get_cached_by_id(a.id),
1315 %User{} = b <- User.get_cached_by_id(b.id) do
1316 {:ok, a, b}
1317 else
1318 _e ->
1319 :error
1320 end
1321 end
1322
1323 def parse_bio(bio) when is_binary(bio) and bio != "" do
1324 bio
1325 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1326 |> elem(0)
1327 end
1328
1329 def parse_bio(_), do: ""
1330
1331 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1332 # TODO: get profile URLs other than user.ap_id
1333 profile_urls = [user.ap_id]
1334
1335 bio
1336 |> CommonUtils.format_input("text/plain",
1337 mentions_format: :full,
1338 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1339 )
1340 |> elem(0)
1341 end
1342
1343 def parse_bio(_, _), do: ""
1344
1345 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1346 Repo.transaction(fn ->
1347 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1348 end)
1349 end
1350
1351 def tag(nickname, tags) when is_binary(nickname),
1352 do: tag(get_by_nickname(nickname), tags)
1353
1354 def tag(%User{} = user, tags),
1355 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1356
1357 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1358 Repo.transaction(fn ->
1359 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1360 end)
1361 end
1362
1363 def untag(nickname, tags) when is_binary(nickname),
1364 do: untag(get_by_nickname(nickname), tags)
1365
1366 def untag(%User{} = user, tags),
1367 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1368
1369 defp update_tags(%User{} = user, new_tags) do
1370 {:ok, updated_user} =
1371 user
1372 |> change(%{tags: new_tags})
1373 |> update_and_set_cache()
1374
1375 updated_user
1376 end
1377
1378 defp normalize_tags(tags) do
1379 [tags]
1380 |> List.flatten()
1381 |> Enum.map(&String.downcase(&1))
1382 end
1383
1384 defp local_nickname_regex do
1385 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1386 @extended_local_nickname_regex
1387 else
1388 @strict_local_nickname_regex
1389 end
1390 end
1391
1392 def local_nickname(nickname_or_mention) do
1393 nickname_or_mention
1394 |> full_nickname()
1395 |> String.split("@")
1396 |> hd()
1397 end
1398
1399 def full_nickname(nickname_or_mention),
1400 do: String.trim_leading(nickname_or_mention, "@")
1401
1402 def error_user(ap_id) do
1403 %User{
1404 name: ap_id,
1405 ap_id: ap_id,
1406 info: %User.Info{},
1407 nickname: "erroruser@example.com",
1408 inserted_at: NaiveDateTime.utc_now()
1409 }
1410 end
1411
1412 @spec all_superusers() :: [User.t()]
1413 def all_superusers do
1414 User.Query.build(%{super_users: true, local: true, deactivated: false})
1415 |> Repo.all()
1416 end
1417
1418 def showing_reblogs?(%User{} = user, %User{} = target) do
1419 target.ap_id not in user.info.muted_reblogs
1420 end
1421
1422 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1423 def toggle_confirmation(%User{} = user) do
1424 need_confirmation? = !user.info.confirmation_pending
1425
1426 info_changeset =
1427 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1428
1429 user
1430 |> change()
1431 |> put_embed(:info, info_changeset)
1432 |> update_and_set_cache()
1433 end
1434
1435 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1436 mascot
1437 end
1438
1439 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1440 # use instance-default
1441 config = Pleroma.Config.get([:assets, :mascots])
1442 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1443 mascot = Keyword.get(config, default_mascot)
1444
1445 %{
1446 "id" => "default-mascot",
1447 "url" => mascot[:url],
1448 "preview_url" => mascot[:url],
1449 "pleroma" => %{
1450 "mime_type" => mascot[:mime_type]
1451 }
1452 }
1453 end
1454
1455 def ensure_keys_present(%User{info: info} = user) do
1456 if info.keys do
1457 {:ok, user}
1458 else
1459 {:ok, pem} = Keys.generate_rsa_pem()
1460
1461 user
1462 |> Ecto.Changeset.change()
1463 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1464 |> update_and_set_cache()
1465 end
1466 end
1467
1468 def get_ap_ids_by_nicknames(nicknames) do
1469 from(u in User,
1470 where: u.nickname in ^nicknames,
1471 select: u.ap_id
1472 )
1473 |> Repo.all()
1474 end
1475
1476 defdelegate search(query, opts \\ []), to: User.Search
1477
1478 defp put_password_hash(
1479 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1480 ) do
1481 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1482 end
1483
1484 defp put_password_hash(changeset), do: changeset
1485
1486 def is_internal_user?(%User{nickname: nil}), do: true
1487 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1488 def is_internal_user?(_), do: false
1489 end