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