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