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