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