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