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