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