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