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