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