Mastdon API: Add ability to get a remote account by nickname to
[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, opts \\ []) do
573 if is_integer(nickname_or_id) or Pleroma.FlakeId.is_flake_id?(nickname_or_id) do
574 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
575 else
576 unless opts[:restrict_remote_nicknames], do: get_cached_by_nickname(nickname_or_id)
577 end
578 end
579
580 def get_by_nickname(nickname) do
581 Repo.get_by(User, nickname: nickname) ||
582 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
583 Repo.get_by(User, nickname: local_nickname(nickname))
584 end
585 end
586
587 def get_by_email(email), do: Repo.get_by(User, email: email)
588
589 def get_by_nickname_or_email(nickname_or_email) do
590 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
591 end
592
593 def get_cached_user_info(user) do
594 key = "user_info:#{user.id}"
595 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
596 end
597
598 def fetch_by_nickname(nickname) do
599 ap_try = ActivityPub.make_user_from_nickname(nickname)
600
601 case ap_try do
602 {:ok, user} -> {:ok, user}
603 _ -> OStatus.make_user(nickname)
604 end
605 end
606
607 def get_or_fetch_by_nickname(nickname) do
608 with %User{} = user <- get_by_nickname(nickname) do
609 {:ok, user}
610 else
611 _e ->
612 with [_nick, _domain] <- String.split(nickname, "@"),
613 {:ok, user} <- fetch_by_nickname(nickname) do
614 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
615 fetch_initial_posts(user)
616 end
617
618 {:ok, user}
619 else
620 _e -> {:error, "not found " <> nickname}
621 end
622 end
623 end
624
625 @doc "Fetch some posts when the user has just been federated with"
626 def fetch_initial_posts(user),
627 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
628
629 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
630 def get_followers_query(%User{} = user, nil) do
631 User.Query.build(%{followers: user, deactivated: false})
632 end
633
634 def get_followers_query(user, page) do
635 from(u in get_followers_query(user, nil))
636 |> User.Query.paginate(page, 20)
637 end
638
639 @spec get_followers_query(User.t()) :: Ecto.Query.t()
640 def get_followers_query(user), do: get_followers_query(user, nil)
641
642 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
643 def get_followers(user, page \\ nil) do
644 q = get_followers_query(user, page)
645
646 {:ok, Repo.all(q)}
647 end
648
649 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
650 def get_external_followers(user, page \\ nil) do
651 q =
652 user
653 |> get_followers_query(page)
654 |> User.Query.build(%{external: true})
655
656 {:ok, Repo.all(q)}
657 end
658
659 def get_followers_ids(user, page \\ nil) do
660 q = get_followers_query(user, page)
661
662 Repo.all(from(u in q, select: u.id))
663 end
664
665 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
666 def get_friends_query(%User{} = user, nil) do
667 User.Query.build(%{friends: user, deactivated: false})
668 end
669
670 def get_friends_query(user, page) do
671 from(u in get_friends_query(user, nil))
672 |> User.Query.paginate(page, 20)
673 end
674
675 @spec get_friends_query(User.t()) :: Ecto.Query.t()
676 def get_friends_query(user), do: get_friends_query(user, nil)
677
678 def get_friends(user, page \\ nil) do
679 q = get_friends_query(user, page)
680
681 {:ok, Repo.all(q)}
682 end
683
684 def get_friends_ids(user, page \\ nil) do
685 q = get_friends_query(user, page)
686
687 Repo.all(from(u in q, select: u.id))
688 end
689
690 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
691 def get_follow_requests(%User{} = user) do
692 users =
693 Activity.follow_requests_for_actor(user)
694 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
695 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
696 |> group_by([a, u], u.id)
697 |> select([a, u], u)
698 |> Repo.all()
699
700 {:ok, users}
701 end
702
703 def increase_note_count(%User{} = user) do
704 User
705 |> where(id: ^user.id)
706 |> update([u],
707 set: [
708 info:
709 fragment(
710 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
711 u.info,
712 u.info
713 )
714 ]
715 )
716 |> select([u], u)
717 |> Repo.update_all([])
718 |> case do
719 {1, [user]} -> set_cache(user)
720 _ -> {:error, user}
721 end
722 end
723
724 def decrease_note_count(%User{} = user) do
725 User
726 |> where(id: ^user.id)
727 |> update([u],
728 set: [
729 info:
730 fragment(
731 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
732 u.info,
733 u.info
734 )
735 ]
736 )
737 |> select([u], u)
738 |> Repo.update_all([])
739 |> case do
740 {1, [user]} -> set_cache(user)
741 _ -> {:error, user}
742 end
743 end
744
745 def update_note_count(%User{} = user) do
746 note_count_query =
747 from(
748 a in Object,
749 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
750 select: count(a.id)
751 )
752
753 note_count = Repo.one(note_count_query)
754
755 info_cng = User.Info.set_note_count(user.info, note_count)
756
757 user
758 |> change()
759 |> put_embed(:info, info_cng)
760 |> update_and_set_cache()
761 end
762
763 @spec maybe_fetch_follow_information(User.t()) :: User.t()
764 def maybe_fetch_follow_information(user) do
765 with {:ok, user} <- fetch_follow_information(user) do
766 user
767 else
768 e ->
769 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
770
771 user
772 end
773 end
774
775 def fetch_follow_information(user) do
776 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
777 info_cng = User.Info.follow_information_update(user.info, info)
778
779 changeset =
780 user
781 |> change()
782 |> put_embed(:info, info_cng)
783
784 update_and_set_cache(changeset)
785 else
786 {:error, _} = e -> e
787 e -> {:error, e}
788 end
789 end
790
791 def update_follower_count(%User{} = user) do
792 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
793 follower_count_query =
794 User.Query.build(%{followers: user, deactivated: false})
795 |> select([u], %{count: count(u.id)})
796
797 User
798 |> where(id: ^user.id)
799 |> join(:inner, [u], s in subquery(follower_count_query))
800 |> update([u, s],
801 set: [
802 info:
803 fragment(
804 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
805 u.info,
806 s.count
807 )
808 ]
809 )
810 |> select([u], u)
811 |> Repo.update_all([])
812 |> case do
813 {1, [user]} -> set_cache(user)
814 _ -> {:error, user}
815 end
816 else
817 {:ok, maybe_fetch_follow_information(user)}
818 end
819 end
820
821 @spec maybe_update_following_count(User.t()) :: User.t()
822 def maybe_update_following_count(%User{local: false} = user) do
823 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
824 maybe_fetch_follow_information(user)
825 else
826 user
827 end
828 end
829
830 def maybe_update_following_count(user), do: user
831
832 def remove_duplicated_following(%User{following: following} = user) do
833 uniq_following = Enum.uniq(following)
834
835 if length(following) == length(uniq_following) do
836 {:ok, user}
837 else
838 user
839 |> update_changeset(%{following: uniq_following})
840 |> update_and_set_cache()
841 end
842 end
843
844 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
845 def get_users_from_set(ap_ids, local_only \\ true) do
846 criteria = %{ap_id: ap_ids, deactivated: false}
847 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
848
849 User.Query.build(criteria)
850 |> Repo.all()
851 end
852
853 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
854 def get_recipients_from_activity(%Activity{recipients: to}) do
855 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
856 |> Repo.all()
857 end
858
859 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
860 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
861 info = muter.info
862
863 info_cng =
864 User.Info.add_to_mutes(info, ap_id)
865 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
866
867 cng =
868 change(muter)
869 |> put_embed(:info, info_cng)
870
871 update_and_set_cache(cng)
872 end
873
874 def unmute(muter, %{ap_id: ap_id}) do
875 info = muter.info
876
877 info_cng =
878 User.Info.remove_from_mutes(info, ap_id)
879 |> User.Info.remove_from_muted_notifications(info, ap_id)
880
881 cng =
882 change(muter)
883 |> put_embed(:info, info_cng)
884
885 update_and_set_cache(cng)
886 end
887
888 def subscribe(subscriber, %{ap_id: ap_id}) do
889 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
890
891 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
892 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
893
894 if blocked do
895 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
896 else
897 info_cng =
898 subscribed.info
899 |> User.Info.add_to_subscribers(subscriber.ap_id)
900
901 change(subscribed)
902 |> put_embed(:info, info_cng)
903 |> update_and_set_cache()
904 end
905 end
906 end
907
908 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
909 with %User{} = user <- get_cached_by_ap_id(ap_id) do
910 info_cng =
911 user.info
912 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
913
914 change(user)
915 |> put_embed(:info, info_cng)
916 |> update_and_set_cache()
917 end
918 end
919
920 def block(blocker, %User{ap_id: ap_id} = blocked) do
921 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
922 blocker =
923 if following?(blocker, blocked) do
924 {:ok, blocker, _} = unfollow(blocker, blocked)
925 blocker
926 else
927 blocker
928 end
929
930 # clear any requested follows as well
931 blocked =
932 case CommonAPI.reject_follow_request(blocked, blocker) do
933 {:ok, %User{} = updated_blocked} -> updated_blocked
934 nil -> blocked
935 end
936
937 blocker =
938 if subscribed_to?(blocked, blocker) do
939 {:ok, blocker} = unsubscribe(blocked, blocker)
940 blocker
941 else
942 blocker
943 end
944
945 if following?(blocked, blocker) do
946 unfollow(blocked, blocker)
947 end
948
949 {:ok, blocker} = update_follower_count(blocker)
950
951 info_cng =
952 blocker.info
953 |> User.Info.add_to_block(ap_id)
954
955 cng =
956 change(blocker)
957 |> put_embed(:info, info_cng)
958
959 update_and_set_cache(cng)
960 end
961
962 # helper to handle the block given only an actor's AP id
963 def block(blocker, %{ap_id: ap_id}) do
964 block(blocker, get_cached_by_ap_id(ap_id))
965 end
966
967 def unblock(blocker, %{ap_id: ap_id}) do
968 info_cng =
969 blocker.info
970 |> User.Info.remove_from_block(ap_id)
971
972 cng =
973 change(blocker)
974 |> put_embed(:info, info_cng)
975
976 update_and_set_cache(cng)
977 end
978
979 def mutes?(nil, _), do: false
980 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
981
982 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
983 def muted_notifications?(nil, _), do: false
984
985 def muted_notifications?(user, %{ap_id: ap_id}),
986 do: Enum.member?(user.info.muted_notifications, ap_id)
987
988 def blocks?(%User{} = user, %User{} = target) do
989 blocks_ap_id?(user, target) || blocks_domain?(user, target)
990 end
991
992 def blocks?(nil, _), do: false
993
994 def blocks_ap_id?(%User{} = user, %User{} = target) do
995 Enum.member?(user.info.blocks, target.ap_id)
996 end
997
998 def blocks_ap_id?(_, _), do: false
999
1000 def blocks_domain?(%User{} = user, %User{} = target) do
1001 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1002 %{host: host} = URI.parse(target.ap_id)
1003 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1004 end
1005
1006 def blocks_domain?(_, _), do: false
1007
1008 def subscribed_to?(user, %{ap_id: ap_id}) do
1009 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1010 Enum.member?(target.info.subscribers, user.ap_id)
1011 end
1012 end
1013
1014 @spec muted_users(User.t()) :: [User.t()]
1015 def muted_users(user) do
1016 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1017 |> Repo.all()
1018 end
1019
1020 @spec blocked_users(User.t()) :: [User.t()]
1021 def blocked_users(user) do
1022 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1023 |> Repo.all()
1024 end
1025
1026 @spec subscribers(User.t()) :: [User.t()]
1027 def subscribers(user) do
1028 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1029 |> Repo.all()
1030 end
1031
1032 def block_domain(user, domain) do
1033 info_cng =
1034 user.info
1035 |> User.Info.add_to_domain_block(domain)
1036
1037 cng =
1038 change(user)
1039 |> put_embed(:info, info_cng)
1040
1041 update_and_set_cache(cng)
1042 end
1043
1044 def unblock_domain(user, domain) do
1045 info_cng =
1046 user.info
1047 |> User.Info.remove_from_domain_block(domain)
1048
1049 cng =
1050 change(user)
1051 |> put_embed(:info, info_cng)
1052
1053 update_and_set_cache(cng)
1054 end
1055
1056 def deactivate_async(user, status \\ true) do
1057 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
1058 end
1059
1060 def deactivate(%User{} = user, status \\ true) do
1061 info_cng = User.Info.set_activation_status(user.info, status)
1062
1063 with {:ok, friends} <- User.get_friends(user),
1064 {:ok, followers} <- User.get_followers(user),
1065 {:ok, user} <-
1066 user
1067 |> change()
1068 |> put_embed(:info, info_cng)
1069 |> update_and_set_cache() do
1070 Enum.each(followers, &invalidate_cache(&1))
1071 Enum.each(friends, &update_follower_count(&1))
1072
1073 {:ok, user}
1074 end
1075 end
1076
1077 def update_notification_settings(%User{} = user, settings \\ %{}) do
1078 info_changeset = User.Info.update_notification_settings(user.info, settings)
1079
1080 change(user)
1081 |> put_embed(:info, info_changeset)
1082 |> update_and_set_cache()
1083 end
1084
1085 @spec delete(User.t()) :: :ok
1086 def delete(%User{} = user),
1087 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1088
1089 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1090 def perform(:delete, %User{} = user) do
1091 {:ok, _user} = ActivityPub.delete(user)
1092
1093 # Remove all relationships
1094 {:ok, followers} = User.get_followers(user)
1095
1096 Enum.each(followers, fn follower ->
1097 ActivityPub.unfollow(follower, user)
1098 User.unfollow(follower, user)
1099 end)
1100
1101 {:ok, friends} = User.get_friends(user)
1102
1103 Enum.each(friends, fn followed ->
1104 ActivityPub.unfollow(user, followed)
1105 User.unfollow(user, followed)
1106 end)
1107
1108 delete_user_activities(user)
1109 invalidate_cache(user)
1110 Repo.delete(user)
1111 end
1112
1113 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1114 def perform(:fetch_initial_posts, %User{} = user) do
1115 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1116
1117 Enum.each(
1118 # Insert all the posts in reverse order, so they're in the right order on the timeline
1119 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1120 &Pleroma.Web.Federator.incoming_ap_doc/1
1121 )
1122
1123 {:ok, user}
1124 end
1125
1126 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1127
1128 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1129 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1130 when is_list(blocked_identifiers) do
1131 Enum.map(
1132 blocked_identifiers,
1133 fn blocked_identifier ->
1134 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1135 {:ok, blocker} <- block(blocker, blocked),
1136 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1137 blocked
1138 else
1139 err ->
1140 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1141 err
1142 end
1143 end
1144 )
1145 end
1146
1147 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1148 def perform(:follow_import, %User{} = follower, followed_identifiers)
1149 when is_list(followed_identifiers) do
1150 Enum.map(
1151 followed_identifiers,
1152 fn followed_identifier ->
1153 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1154 {:ok, follower} <- maybe_direct_follow(follower, followed),
1155 {:ok, _} <- ActivityPub.follow(follower, followed) do
1156 followed
1157 else
1158 err ->
1159 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1160 err
1161 end
1162 end
1163 )
1164 end
1165
1166 @spec external_users_query() :: Ecto.Query.t()
1167 def external_users_query do
1168 User.Query.build(%{
1169 external: true,
1170 active: true,
1171 order_by: :id
1172 })
1173 end
1174
1175 @spec external_users(keyword()) :: [User.t()]
1176 def external_users(opts \\ []) do
1177 query =
1178 external_users_query()
1179 |> select([u], struct(u, [:id, :ap_id, :info]))
1180
1181 query =
1182 if opts[:max_id],
1183 do: where(query, [u], u.id > ^opts[:max_id]),
1184 else: query
1185
1186 query =
1187 if opts[:limit],
1188 do: limit(query, ^opts[:limit]),
1189 else: query
1190
1191 Repo.all(query)
1192 end
1193
1194 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1195 do:
1196 PleromaJobQueue.enqueue(:background, __MODULE__, [
1197 :blocks_import,
1198 blocker,
1199 blocked_identifiers
1200 ])
1201
1202 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1203 do:
1204 PleromaJobQueue.enqueue(:background, __MODULE__, [
1205 :follow_import,
1206 follower,
1207 followed_identifiers
1208 ])
1209
1210 def delete_user_activities(%User{ap_id: ap_id} = user) do
1211 ap_id
1212 |> Activity.query_by_actor()
1213 |> RepoStreamer.chunk_stream(50)
1214 |> Stream.each(fn activities ->
1215 Enum.each(activities, &delete_activity(&1))
1216 end)
1217 |> Stream.run()
1218
1219 {:ok, user}
1220 end
1221
1222 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1223 activity
1224 |> Object.normalize()
1225 |> ActivityPub.delete()
1226 end
1227
1228 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1229 user = get_cached_by_ap_id(activity.actor)
1230 object = Object.normalize(activity)
1231
1232 ActivityPub.unlike(user, object)
1233 end
1234
1235 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1236 user = get_cached_by_ap_id(activity.actor)
1237 object = Object.normalize(activity)
1238
1239 ActivityPub.unannounce(user, object)
1240 end
1241
1242 defp delete_activity(_activity), do: "Doing nothing"
1243
1244 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1245 Pleroma.HTML.Scrubber.TwitterText
1246 end
1247
1248 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1249
1250 def fetch_by_ap_id(ap_id) do
1251 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1252
1253 case ap_try do
1254 {:ok, user} ->
1255 {:ok, user}
1256
1257 _ ->
1258 case OStatus.make_user(ap_id) do
1259 {:ok, user} -> {:ok, user}
1260 _ -> {:error, "Could not fetch by AP id"}
1261 end
1262 end
1263 end
1264
1265 def get_or_fetch_by_ap_id(ap_id) do
1266 user = get_cached_by_ap_id(ap_id)
1267
1268 if !is_nil(user) and !User.needs_update?(user) do
1269 {:ok, user}
1270 else
1271 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1272 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1273
1274 resp = fetch_by_ap_id(ap_id)
1275
1276 if should_fetch_initial do
1277 with {:ok, %User{} = user} <- resp do
1278 fetch_initial_posts(user)
1279 end
1280 end
1281
1282 resp
1283 end
1284 end
1285
1286 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1287 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1288 if user = get_cached_by_ap_id(uri) do
1289 user
1290 else
1291 changes =
1292 %User{info: %User.Info{}}
1293 |> cast(%{}, [:ap_id, :nickname, :local])
1294 |> put_change(:ap_id, uri)
1295 |> put_change(:nickname, nickname)
1296 |> put_change(:local, true)
1297 |> put_change(:follower_address, uri <> "/followers")
1298
1299 {:ok, user} = Repo.insert(changes)
1300 user
1301 end
1302 end
1303
1304 # AP style
1305 def public_key_from_info(%{
1306 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1307 }) do
1308 key =
1309 public_key_pem
1310 |> :public_key.pem_decode()
1311 |> hd()
1312 |> :public_key.pem_entry_decode()
1313
1314 {:ok, key}
1315 end
1316
1317 # OStatus Magic Key
1318 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1319 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1320 end
1321
1322 def public_key_from_info(_), do: {:error, "not found key"}
1323
1324 def get_public_key_for_ap_id(ap_id) do
1325 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1326 {:ok, public_key} <- public_key_from_info(user.info) do
1327 {:ok, public_key}
1328 else
1329 _ -> :error
1330 end
1331 end
1332
1333 defp blank?(""), do: nil
1334 defp blank?(n), do: n
1335
1336 def insert_or_update_user(data) do
1337 data
1338 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1339 |> remote_user_creation()
1340 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1341 |> set_cache()
1342 end
1343
1344 def ap_enabled?(%User{local: true}), do: true
1345 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1346 def ap_enabled?(_), do: false
1347
1348 @doc "Gets or fetch a user by uri or nickname."
1349 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1350 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1351 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1352
1353 # wait a period of time and return newest version of the User structs
1354 # this is because we have synchronous follow APIs and need to simulate them
1355 # with an async handshake
1356 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1357 with %User{} = a <- User.get_cached_by_id(a.id),
1358 %User{} = b <- User.get_cached_by_id(b.id) do
1359 {:ok, a, b}
1360 else
1361 _e ->
1362 :error
1363 end
1364 end
1365
1366 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1367 with :ok <- :timer.sleep(timeout),
1368 %User{} = a <- User.get_cached_by_id(a.id),
1369 %User{} = b <- User.get_cached_by_id(b.id) do
1370 {:ok, a, b}
1371 else
1372 _e ->
1373 :error
1374 end
1375 end
1376
1377 def parse_bio(bio) when is_binary(bio) and bio != "" do
1378 bio
1379 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1380 |> elem(0)
1381 end
1382
1383 def parse_bio(_), do: ""
1384
1385 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1386 # TODO: get profile URLs other than user.ap_id
1387 profile_urls = [user.ap_id]
1388
1389 bio
1390 |> CommonUtils.format_input("text/plain",
1391 mentions_format: :full,
1392 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1393 )
1394 |> elem(0)
1395 end
1396
1397 def parse_bio(_, _), do: ""
1398
1399 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1400 Repo.transaction(fn ->
1401 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1402 end)
1403 end
1404
1405 def tag(nickname, tags) when is_binary(nickname),
1406 do: tag(get_by_nickname(nickname), tags)
1407
1408 def tag(%User{} = user, tags),
1409 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1410
1411 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1412 Repo.transaction(fn ->
1413 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1414 end)
1415 end
1416
1417 def untag(nickname, tags) when is_binary(nickname),
1418 do: untag(get_by_nickname(nickname), tags)
1419
1420 def untag(%User{} = user, tags),
1421 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1422
1423 defp update_tags(%User{} = user, new_tags) do
1424 {:ok, updated_user} =
1425 user
1426 |> change(%{tags: new_tags})
1427 |> update_and_set_cache()
1428
1429 updated_user
1430 end
1431
1432 defp normalize_tags(tags) do
1433 [tags]
1434 |> List.flatten()
1435 |> Enum.map(&String.downcase(&1))
1436 end
1437
1438 defp local_nickname_regex do
1439 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1440 @extended_local_nickname_regex
1441 else
1442 @strict_local_nickname_regex
1443 end
1444 end
1445
1446 def local_nickname(nickname_or_mention) do
1447 nickname_or_mention
1448 |> full_nickname()
1449 |> String.split("@")
1450 |> hd()
1451 end
1452
1453 def full_nickname(nickname_or_mention),
1454 do: String.trim_leading(nickname_or_mention, "@")
1455
1456 def error_user(ap_id) do
1457 %User{
1458 name: ap_id,
1459 ap_id: ap_id,
1460 info: %User.Info{},
1461 nickname: "erroruser@example.com",
1462 inserted_at: NaiveDateTime.utc_now()
1463 }
1464 end
1465
1466 @spec all_superusers() :: [User.t()]
1467 def all_superusers do
1468 User.Query.build(%{super_users: true, local: true, deactivated: false})
1469 |> Repo.all()
1470 end
1471
1472 def showing_reblogs?(%User{} = user, %User{} = target) do
1473 target.ap_id not in user.info.muted_reblogs
1474 end
1475
1476 @doc """
1477 The function returns a query to get users with no activity for given interval of days.
1478 Inactive users are those who didn't read any notification, or had any activity where
1479 the user is the activity's actor, during `inactivity_threshold` days.
1480 Deactivated users will not appear in this list.
1481
1482 ## Examples
1483
1484 iex> Pleroma.User.list_inactive_users()
1485 %Ecto.Query{}
1486 """
1487 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1488 def list_inactive_users_query(inactivity_threshold \\ 7) do
1489 negative_inactivity_threshold = -inactivity_threshold
1490 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1491 # Subqueries are not supported in `where` clauses, join gets too complicated.
1492 has_read_notifications =
1493 from(n in Pleroma.Notification,
1494 where: n.seen == true,
1495 group_by: n.id,
1496 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1497 select: n.user_id
1498 )
1499 |> Pleroma.Repo.all()
1500
1501 from(u in Pleroma.User,
1502 left_join: a in Pleroma.Activity,
1503 on: u.ap_id == a.actor,
1504 where: not is_nil(u.nickname),
1505 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1506 where: u.id not in ^has_read_notifications,
1507 group_by: u.id,
1508 having:
1509 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1510 is_nil(max(a.inserted_at))
1511 )
1512 end
1513
1514 @doc """
1515 Enable or disable email notifications for user
1516
1517 ## Examples
1518
1519 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1520 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1521
1522 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1523 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1524 """
1525 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1526 {:ok, t()} | {:error, Ecto.Changeset.t()}
1527 def switch_email_notifications(user, type, status) do
1528 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1529
1530 change(user)
1531 |> put_embed(:info, info)
1532 |> update_and_set_cache()
1533 end
1534
1535 @doc """
1536 Set `last_digest_emailed_at` value for the user to current time
1537 """
1538 @spec touch_last_digest_emailed_at(t()) :: t()
1539 def touch_last_digest_emailed_at(user) do
1540 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1541
1542 {:ok, updated_user} =
1543 user
1544 |> change(%{last_digest_emailed_at: now})
1545 |> update_and_set_cache()
1546
1547 updated_user
1548 end
1549
1550 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1551 def toggle_confirmation(%User{} = user) do
1552 need_confirmation? = !user.info.confirmation_pending
1553
1554 info_changeset =
1555 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1556
1557 user
1558 |> change()
1559 |> put_embed(:info, info_changeset)
1560 |> update_and_set_cache()
1561 end
1562
1563 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1564 mascot
1565 end
1566
1567 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1568 # use instance-default
1569 config = Pleroma.Config.get([:assets, :mascots])
1570 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1571 mascot = Keyword.get(config, default_mascot)
1572
1573 %{
1574 "id" => "default-mascot",
1575 "url" => mascot[:url],
1576 "preview_url" => mascot[:url],
1577 "pleroma" => %{
1578 "mime_type" => mascot[:mime_type]
1579 }
1580 }
1581 end
1582
1583 def ensure_keys_present(%User{info: info} = user) do
1584 if info.keys do
1585 {:ok, user}
1586 else
1587 {:ok, pem} = Keys.generate_rsa_pem()
1588
1589 user
1590 |> Ecto.Changeset.change()
1591 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1592 |> update_and_set_cache()
1593 end
1594 end
1595
1596 def get_ap_ids_by_nicknames(nicknames) do
1597 from(u in User,
1598 where: u.nickname in ^nicknames,
1599 select: u.ap_id
1600 )
1601 |> Repo.all()
1602 end
1603
1604 defdelegate search(query, opts \\ []), to: User.Search
1605
1606 defp put_password_hash(
1607 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1608 ) do
1609 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1610 end
1611
1612 defp put_password_hash(changeset), do: changeset
1613
1614 def is_internal_user?(%User{nickname: nil}), do: true
1615 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1616 def is_internal_user?(_), do: false
1617 end