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