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