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