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