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