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