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