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