d4fc1c093d2197b56335e58e582f2a5913522dfc
[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 import Ecto, only: [assoc: 2]
11
12 alias Comeonin.Pbkdf2
13 alias Ecto.Multi
14 alias Pleroma.Activity
15 alias Pleroma.Conversation.Participation
16 alias Pleroma.Delivery
17 alias Pleroma.FollowingRelationship
18 alias Pleroma.Keys
19 alias Pleroma.Notification
20 alias Pleroma.Object
21 alias Pleroma.Registration
22 alias Pleroma.Repo
23 alias Pleroma.RepoStreamer
24 alias Pleroma.User
25 alias Pleroma.UserBlock
26 alias Pleroma.Web
27 alias Pleroma.Web.ActivityPub.ActivityPub
28 alias Pleroma.Web.ActivityPub.Utils
29 alias Pleroma.Web.CommonAPI
30 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
31 alias Pleroma.Web.OAuth
32 alias Pleroma.Web.RelMe
33 alias Pleroma.Workers.BackgroundWorker
34
35 require Logger
36
37 @type t :: %__MODULE__{}
38
39 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
40
41 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
42 @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])?)*$/
43
44 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
45 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
46
47 schema "users" do
48 field(:bio, :string)
49 field(:email, :string)
50 field(:name, :string)
51 field(:nickname, :string)
52 field(:password_hash, :string)
53 field(:password, :string, virtual: true)
54 field(:password_confirmation, :string, virtual: true)
55 field(:keys, :string)
56 field(:ap_id, :string)
57 field(:avatar, :map)
58 field(:local, :boolean, default: true)
59 field(:follower_address, :string)
60 field(:following_address, :string)
61 field(:search_rank, :float, virtual: true)
62 field(:search_type, :integer, virtual: true)
63 field(:tags, {:array, :string}, default: [])
64 field(:last_refreshed_at, :naive_datetime_usec)
65 field(:last_digest_emailed_at, :naive_datetime)
66
67 field(:banner, :map, default: %{})
68 field(:background, :map, default: %{})
69 field(:source_data, :map, default: %{})
70 field(:note_count, :integer, default: 0)
71 field(:follower_count, :integer, default: 0)
72 # Should be filled in only for remote users
73 field(:following_count, :integer, default: nil)
74 field(:locked, :boolean, default: false)
75 field(:confirmation_pending, :boolean, default: false)
76 field(:password_reset_pending, :boolean, default: false)
77 field(:confirmation_token, :string, default: nil)
78 field(:default_scope, :string, default: "public")
79 field(:domain_blocks, {:array, :string}, default: [])
80 field(:mutes, {:array, :string}, default: [])
81 field(:muted_reblogs, {:array, :string}, default: [])
82 field(:muted_notifications, {:array, :string}, default: [])
83 field(:subscribers, {:array, :string}, default: [])
84 field(:deactivated, :boolean, default: false)
85 field(:no_rich_text, :boolean, default: false)
86 field(:ap_enabled, :boolean, default: false)
87 field(:is_moderator, :boolean, default: false)
88 field(:is_admin, :boolean, default: false)
89 field(:show_role, :boolean, default: true)
90 field(:settings, :map, default: nil)
91 field(:magic_key, :string, default: nil)
92 field(:uri, :string, default: nil)
93 field(:hide_followers_count, :boolean, default: false)
94 field(:hide_follows_count, :boolean, default: false)
95 field(:hide_followers, :boolean, default: false)
96 field(:hide_follows, :boolean, default: false)
97 field(:hide_favorites, :boolean, default: true)
98 field(:unread_conversation_count, :integer, default: 0)
99 field(:pinned_activities, {:array, :string}, default: [])
100 field(:email_notifications, :map, default: %{"digest" => false})
101 field(:mascot, :map, default: nil)
102 field(:emoji, {:array, :map}, default: [])
103 field(:pleroma_settings_store, :map, default: %{})
104 field(:fields, {:array, :map}, default: [])
105 field(:raw_fields, {:array, :map}, default: [])
106 field(:discoverable, :boolean, default: false)
107 field(:invisible, :boolean, default: false)
108 field(:skip_thread_containment, :boolean, default: false)
109
110 field(:notification_settings, :map,
111 default: %{
112 "followers" => true,
113 "follows" => true,
114 "non_follows" => true,
115 "non_followers" => true
116 }
117 )
118
119 has_many(:notifications, Notification)
120 has_many(:registrations, Registration)
121 has_many(:deliveries, Delivery)
122 has_many(:blocker_blocks, UserBlock, foreign_key: :blocker_id)
123 has_many(:blockee_blocks, UserBlock, foreign_key: :blockee_id)
124 has_many(:blocked_users, through: [:blocker_blocks, :blockee])
125 has_many(:blocker_users, through: [:blockee_blocks, :blocker])
126
127 field(:info, :map, default: %{})
128
129 # `:blocks` is deprecated (replaced with `blocked_users` relation)
130 field(:blocks, {:array, :string}, default: [])
131
132 timestamps()
133 end
134
135 def auth_active?(%User{confirmation_pending: true}),
136 do: !Pleroma.Config.get([:instance, :account_activation_required])
137
138 def auth_active?(%User{}), do: true
139
140 def visible_for?(user, for_user \\ nil)
141
142 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
143
144 def visible_for?(%User{} = user, for_user) do
145 auth_active?(user) || superuser?(for_user)
146 end
147
148 def visible_for?(_, _), do: false
149
150 def superuser?(%User{local: true, is_admin: true}), do: true
151 def superuser?(%User{local: true, is_moderator: true}), do: true
152 def superuser?(_), do: false
153
154 def invisible?(%User{invisible: true}), do: true
155 def invisible?(_), do: false
156
157 def avatar_url(user, options \\ []) do
158 case user.avatar do
159 %{"url" => [%{"href" => href} | _]} -> href
160 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
161 end
162 end
163
164 def banner_url(user, options \\ []) do
165 case user.banner do
166 %{"url" => [%{"href" => href} | _]} -> href
167 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
168 end
169 end
170
171 def profile_url(%User{source_data: %{"url" => url}}), do: url
172 def profile_url(%User{ap_id: ap_id}), do: ap_id
173 def profile_url(_), do: nil
174
175 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
176
177 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
178 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
179
180 @spec ap_following(User.t()) :: Sring.t()
181 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
182 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
183
184 def user_info(%User{} = user, args \\ %{}) do
185 following_count =
186 Map.get(args, :following_count, user.following_count || following_count(user))
187
188 follower_count = Map.get(args, :follower_count, user.follower_count)
189
190 %{
191 note_count: user.note_count,
192 locked: user.locked,
193 confirmation_pending: user.confirmation_pending,
194 default_scope: user.default_scope
195 }
196 |> Map.put(:following_count, following_count)
197 |> Map.put(:follower_count, follower_count)
198 end
199
200 def follow_state(%User{} = user, %User{} = target) do
201 case Utils.fetch_latest_follow(user, target) do
202 %{data: %{"state" => state}} -> state
203 # Ideally this would be nil, but then Cachex does not commit the value
204 _ -> false
205 end
206 end
207
208 def get_cached_follow_state(user, target) do
209 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
210 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
211 end
212
213 @spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
214 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
215 Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
216 end
217
218 def set_info_cache(user, args) do
219 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
220 end
221
222 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
223 def restrict_deactivated(query) do
224 from(u in query, where: u.deactivated != ^true)
225 end
226
227 defdelegate following_count(user), to: FollowingRelationship
228
229 defp truncate_fields_param(params) do
230 if Map.has_key?(params, :fields) do
231 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
232 else
233 params
234 end
235 end
236
237 defp truncate_if_exists(params, key, max_length) do
238 if Map.has_key?(params, key) and is_binary(params[key]) do
239 {value, _chopped} = String.split_at(params[key], max_length)
240 Map.put(params, key, value)
241 else
242 params
243 end
244 end
245
246 def remote_user_creation(params) do
247 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
248 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
249
250 params =
251 params
252 |> Map.put(:info, params[:info] || %{})
253 |> truncate_if_exists(:name, name_limit)
254 |> truncate_if_exists(:bio, bio_limit)
255 |> truncate_fields_param()
256
257 changeset =
258 %User{local: false}
259 |> cast(
260 params,
261 [
262 :bio,
263 :name,
264 :ap_id,
265 :nickname,
266 :avatar,
267 :ap_enabled,
268 :source_data,
269 :banner,
270 :locked,
271 :magic_key,
272 :uri,
273 :hide_followers,
274 :hide_follows,
275 :hide_followers_count,
276 :hide_follows_count,
277 :follower_count,
278 :fields,
279 :following_count,
280 :discoverable,
281 :invisible
282 ]
283 )
284 |> validate_required([:name, :ap_id])
285 |> unique_constraint(:nickname)
286 |> validate_format(:nickname, @email_regex)
287 |> validate_length(:bio, max: bio_limit)
288 |> validate_length(:name, max: name_limit)
289 |> validate_fields(true)
290
291 case params[:source_data] do
292 %{"followers" => followers, "following" => following} ->
293 changeset
294 |> put_change(:follower_address, followers)
295 |> put_change(:following_address, following)
296
297 _ ->
298 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
299 put_change(changeset, :follower_address, followers)
300 end
301 end
302
303 def update_changeset(struct, params \\ %{}) do
304 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
305 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
306
307 struct
308 |> cast(
309 params,
310 [
311 :bio,
312 :name,
313 :avatar,
314 :locked,
315 :no_rich_text,
316 :default_scope,
317 :banner,
318 :hide_follows,
319 :hide_followers,
320 :hide_followers_count,
321 :hide_follows_count,
322 :hide_favorites,
323 :background,
324 :show_role,
325 :skip_thread_containment,
326 :fields,
327 :raw_fields,
328 :pleroma_settings_store,
329 :discoverable
330 ]
331 )
332 |> unique_constraint(:nickname)
333 |> validate_format(:nickname, local_nickname_regex())
334 |> validate_length(:bio, max: bio_limit)
335 |> validate_length(:name, min: 1, max: name_limit)
336 |> validate_fields(false)
337 end
338
339 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
340 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
341 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
342
343 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
344
345 params = if remote?, do: truncate_fields_param(params), else: params
346
347 struct
348 |> cast(
349 params,
350 [
351 :bio,
352 :name,
353 :follower_address,
354 :following_address,
355 :avatar,
356 :last_refreshed_at,
357 :ap_enabled,
358 :source_data,
359 :banner,
360 :locked,
361 :magic_key,
362 :follower_count,
363 :following_count,
364 :hide_follows,
365 :fields,
366 :hide_followers,
367 :discoverable,
368 :hide_followers_count,
369 :hide_follows_count
370 ]
371 )
372 |> unique_constraint(:nickname)
373 |> validate_format(:nickname, local_nickname_regex())
374 |> validate_length(:bio, max: bio_limit)
375 |> validate_length(:name, max: name_limit)
376 |> validate_fields(remote?)
377 end
378
379 def password_update_changeset(struct, params) do
380 struct
381 |> cast(params, [:password, :password_confirmation])
382 |> validate_required([:password, :password_confirmation])
383 |> validate_confirmation(:password)
384 |> put_password_hash()
385 |> put_change(:password_reset_pending, false)
386 end
387
388 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
389 def reset_password(%User{id: user_id} = user, data) do
390 multi =
391 Multi.new()
392 |> Multi.update(:user, password_update_changeset(user, data))
393 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
394 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
395
396 case Repo.transaction(multi) do
397 {:ok, %{user: user} = _} -> set_cache(user)
398 {:error, _, changeset, _} -> {:error, changeset}
399 end
400 end
401
402 def update_password_reset_pending(user, value) do
403 user
404 |> change()
405 |> put_change(:password_reset_pending, value)
406 |> update_and_set_cache()
407 end
408
409 def force_password_reset_async(user) do
410 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
411 end
412
413 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
414 def force_password_reset(user), do: update_password_reset_pending(user, true)
415
416 def register_changeset(struct, params \\ %{}, opts \\ []) do
417 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
418 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
419
420 need_confirmation? =
421 if is_nil(opts[:need_confirmation]) do
422 Pleroma.Config.get([:instance, :account_activation_required])
423 else
424 opts[:need_confirmation]
425 end
426
427 struct
428 |> confirmation_changeset(need_confirmation: need_confirmation?)
429 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
430 |> validate_required([:name, :nickname, :password, :password_confirmation])
431 |> validate_confirmation(:password)
432 |> unique_constraint(:email)
433 |> unique_constraint(:nickname)
434 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
435 |> validate_format(:nickname, local_nickname_regex())
436 |> validate_format(:email, @email_regex)
437 |> validate_length(:bio, max: bio_limit)
438 |> validate_length(:name, min: 1, max: name_limit)
439 |> maybe_validate_required_email(opts[:external])
440 |> put_password_hash
441 |> put_ap_id()
442 |> unique_constraint(:ap_id)
443 |> put_following_and_follower_address()
444 end
445
446 def maybe_validate_required_email(changeset, true), do: changeset
447 def maybe_validate_required_email(changeset, _), do: validate_required(changeset, [:email])
448
449 defp put_ap_id(changeset) do
450 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
451 put_change(changeset, :ap_id, ap_id)
452 end
453
454 defp put_following_and_follower_address(changeset) do
455 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
456
457 changeset
458 |> put_change(:follower_address, followers)
459 end
460
461 defp autofollow_users(user) do
462 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
463
464 autofollowed_users =
465 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
466 |> Repo.all()
467
468 follow_all(user, autofollowed_users)
469 end
470
471 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
472 def register(%Ecto.Changeset{} = changeset) do
473 with {:ok, user} <- Repo.insert(changeset) do
474 post_register_action(user)
475 end
476 end
477
478 def post_register_action(%User{} = user) do
479 with {:ok, user} <- autofollow_users(user),
480 {:ok, user} <- set_cache(user),
481 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
482 {:ok, _} <- try_send_confirmation_email(user) do
483 {:ok, user}
484 end
485 end
486
487 def try_send_confirmation_email(%User{} = user) do
488 if user.confirmation_pending &&
489 Pleroma.Config.get([:instance, :account_activation_required]) do
490 user
491 |> Pleroma.Emails.UserEmail.account_confirmation_email()
492 |> Pleroma.Emails.Mailer.deliver_async()
493
494 {:ok, :enqueued}
495 else
496 {:ok, :noop}
497 end
498 end
499
500 def needs_update?(%User{local: true}), do: false
501
502 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
503
504 def needs_update?(%User{local: false} = user) do
505 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
506 end
507
508 def needs_update?(_), do: true
509
510 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
511 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
512 follow(follower, followed, "pending")
513 end
514
515 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
516 follow(follower, followed)
517 end
518
519 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
520 if not ap_enabled?(followed) do
521 follow(follower, followed)
522 else
523 {:ok, follower}
524 end
525 end
526
527 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
528 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
529 def follow_all(follower, followeds) do
530 followeds =
531 Enum.reject(followeds, fn followed ->
532 blocks?(follower, followed) || blocks?(followed, follower)
533 end)
534
535 Enum.each(followeds, &follow(follower, &1, "accept"))
536
537 Enum.each(followeds, &update_follower_count/1)
538
539 set_cache(follower)
540 end
541
542 defdelegate following(user), to: FollowingRelationship
543
544 def follow(%User{} = follower, %User{} = followed, state \\ "accept") do
545 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
546
547 cond do
548 followed.deactivated ->
549 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
550
551 deny_follow_blocked and blocks?(followed, follower) ->
552 {:error, "Could not follow user: #{followed.nickname} blocked you."}
553
554 true ->
555 FollowingRelationship.follow(follower, followed, state)
556
557 follower = maybe_update_following_count(follower)
558
559 {:ok, _} = update_follower_count(followed)
560
561 set_cache(follower)
562 end
563 end
564
565 def unfollow(%User{} = follower, %User{} = followed) do
566 if following?(follower, followed) and follower.ap_id != followed.ap_id do
567 FollowingRelationship.unfollow(follower, followed)
568
569 follower = maybe_update_following_count(follower)
570
571 {:ok, followed} = update_follower_count(followed)
572
573 set_cache(follower)
574
575 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
576 else
577 {:error, "Not subscribed!"}
578 end
579 end
580
581 defdelegate following?(follower, followed), to: FollowingRelationship
582
583 def locked?(%User{} = user) do
584 user.locked || false
585 end
586
587 def get_by_id(id) do
588 Repo.get_by(User, id: id)
589 end
590
591 def get_by_ap_id(ap_id) do
592 Repo.get_by(User, ap_id: ap_id)
593 end
594
595 def get_all_by_ap_id(ap_ids) do
596 from(u in __MODULE__,
597 where: u.ap_id in ^ap_ids
598 )
599 |> Repo.all()
600 end
601
602 def get_all_by_ids(ids) do
603 from(u in __MODULE__, where: u.id in ^ids)
604 |> Repo.all()
605 end
606
607 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
608 # of the ap_id and the domain and tries to get that user
609 def get_by_guessed_nickname(ap_id) do
610 domain = URI.parse(ap_id).host
611 name = List.last(String.split(ap_id, "/"))
612 nickname = "#{name}@#{domain}"
613
614 get_cached_by_nickname(nickname)
615 end
616
617 def set_cache({:ok, user}), do: set_cache(user)
618 def set_cache({:error, err}), do: {:error, err}
619
620 def set_cache(%User{} = user) do
621 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
622 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
623 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
624 {:ok, user}
625 end
626
627 def update_and_set_cache(struct, params) do
628 struct
629 |> update_changeset(params)
630 |> update_and_set_cache()
631 end
632
633 def update_and_set_cache(changeset) do
634 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
635 set_cache(user)
636 end
637 end
638
639 def invalidate_cache(user) do
640 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
641 Cachex.del(:user_cache, "nickname:#{user.nickname}")
642 Cachex.del(:user_cache, "user_info:#{user.id}")
643 end
644
645 def get_cached_by_ap_id(ap_id) do
646 key = "ap_id:#{ap_id}"
647 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
648 end
649
650 def get_cached_by_id(id) do
651 key = "id:#{id}"
652
653 ap_id =
654 Cachex.fetch!(:user_cache, key, fn _ ->
655 user = get_by_id(id)
656
657 if user do
658 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
659 {:commit, user.ap_id}
660 else
661 {:ignore, ""}
662 end
663 end)
664
665 get_cached_by_ap_id(ap_id)
666 end
667
668 def get_cached_by_nickname(nickname) do
669 key = "nickname:#{nickname}"
670
671 Cachex.fetch!(:user_cache, key, fn ->
672 case get_or_fetch_by_nickname(nickname) do
673 {:ok, user} -> {:commit, user}
674 {:error, _error} -> {:ignore, nil}
675 end
676 end)
677 end
678
679 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
680 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
681
682 cond do
683 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
684 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
685
686 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
687 get_cached_by_nickname(nickname_or_id)
688
689 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
690 get_cached_by_nickname(nickname_or_id)
691
692 true ->
693 nil
694 end
695 end
696
697 def get_by_nickname(nickname) do
698 Repo.get_by(User, nickname: nickname) ||
699 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
700 Repo.get_by(User, nickname: local_nickname(nickname))
701 end
702 end
703
704 def get_by_email(email), do: Repo.get_by(User, email: email)
705
706 def get_by_nickname_or_email(nickname_or_email) do
707 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
708 end
709
710 def get_cached_user_info(user) do
711 key = "user_info:#{user.id}"
712 Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
713 end
714
715 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
716
717 def get_or_fetch_by_nickname(nickname) do
718 with %User{} = user <- get_by_nickname(nickname) do
719 {:ok, user}
720 else
721 _e ->
722 with [_nick, _domain] <- String.split(nickname, "@"),
723 {:ok, user} <- fetch_by_nickname(nickname) do
724 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
725 fetch_initial_posts(user)
726 end
727
728 {:ok, user}
729 else
730 _e -> {:error, "not found " <> nickname}
731 end
732 end
733 end
734
735 @doc "Fetch some posts when the user has just been federated with"
736 def fetch_initial_posts(user) do
737 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
738 end
739
740 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
741 def get_followers_query(%User{} = user, nil) do
742 User.Query.build(%{followers: user, deactivated: false})
743 end
744
745 def get_followers_query(user, page) do
746 user
747 |> get_followers_query(nil)
748 |> User.Query.paginate(page, 20)
749 end
750
751 @spec get_followers_query(User.t()) :: Ecto.Query.t()
752 def get_followers_query(user), do: get_followers_query(user, nil)
753
754 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
755 def get_followers(user, page \\ nil) do
756 user
757 |> get_followers_query(page)
758 |> Repo.all()
759 end
760
761 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
762 def get_external_followers(user, page \\ nil) do
763 user
764 |> get_followers_query(page)
765 |> User.Query.build(%{external: true})
766 |> Repo.all()
767 end
768
769 def get_followers_ids(user, page \\ nil) do
770 user
771 |> get_followers_query(page)
772 |> select([u], u.id)
773 |> Repo.all()
774 end
775
776 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
777 def get_friends_query(%User{} = user, nil) do
778 User.Query.build(%{friends: user, deactivated: false})
779 end
780
781 def get_friends_query(user, page) do
782 user
783 |> get_friends_query(nil)
784 |> User.Query.paginate(page, 20)
785 end
786
787 @spec get_friends_query(User.t()) :: Ecto.Query.t()
788 def get_friends_query(user), do: get_friends_query(user, nil)
789
790 def get_friends(user, page \\ nil) do
791 user
792 |> get_friends_query(page)
793 |> Repo.all()
794 end
795
796 def get_friends_ids(user, page \\ nil) do
797 user
798 |> get_friends_query(page)
799 |> select([u], u.id)
800 |> Repo.all()
801 end
802
803 defdelegate get_follow_requests(user), to: FollowingRelationship
804
805 def increase_note_count(%User{} = user) do
806 User
807 |> where(id: ^user.id)
808 |> update([u], inc: [note_count: 1])
809 |> select([u], u)
810 |> Repo.update_all([])
811 |> case do
812 {1, [user]} -> set_cache(user)
813 _ -> {:error, user}
814 end
815 end
816
817 def decrease_note_count(%User{} = user) do
818 User
819 |> where(id: ^user.id)
820 |> update([u],
821 set: [
822 note_count: fragment("greatest(0, note_count - 1)")
823 ]
824 )
825 |> select([u], u)
826 |> Repo.update_all([])
827 |> case do
828 {1, [user]} -> set_cache(user)
829 _ -> {:error, user}
830 end
831 end
832
833 def update_note_count(%User{} = user, note_count \\ nil) do
834 note_count =
835 note_count ||
836 from(
837 a in Object,
838 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
839 select: count(a.id)
840 )
841 |> Repo.one()
842
843 user
844 |> cast(%{note_count: note_count}, [:note_count])
845 |> update_and_set_cache()
846 end
847
848 @spec maybe_fetch_follow_information(User.t()) :: User.t()
849 def maybe_fetch_follow_information(user) do
850 with {:ok, user} <- fetch_follow_information(user) do
851 user
852 else
853 e ->
854 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
855
856 user
857 end
858 end
859
860 def fetch_follow_information(user) do
861 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
862 user
863 |> follow_information_changeset(info)
864 |> update_and_set_cache()
865 end
866 end
867
868 defp follow_information_changeset(user, params) do
869 user
870 |> cast(params, [
871 :hide_followers,
872 :hide_follows,
873 :follower_count,
874 :following_count,
875 :hide_followers_count,
876 :hide_follows_count
877 ])
878 end
879
880 def update_follower_count(%User{} = user) do
881 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
882 follower_count_query =
883 User.Query.build(%{followers: user, deactivated: false})
884 |> select([u], %{count: count(u.id)})
885
886 User
887 |> where(id: ^user.id)
888 |> join(:inner, [u], s in subquery(follower_count_query))
889 |> update([u, s],
890 set: [follower_count: s.count]
891 )
892 |> select([u], u)
893 |> Repo.update_all([])
894 |> case do
895 {1, [user]} -> set_cache(user)
896 _ -> {:error, user}
897 end
898 else
899 {:ok, maybe_fetch_follow_information(user)}
900 end
901 end
902
903 @spec maybe_update_following_count(User.t()) :: User.t()
904 def maybe_update_following_count(%User{local: false} = user) do
905 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
906 maybe_fetch_follow_information(user)
907 else
908 user
909 end
910 end
911
912 def maybe_update_following_count(user), do: user
913
914 def set_unread_conversation_count(%User{local: true} = user) do
915 unread_query = Participation.unread_conversation_count_for_user(user)
916
917 User
918 |> join(:inner, [u], p in subquery(unread_query))
919 |> update([u, p],
920 set: [unread_conversation_count: p.count]
921 )
922 |> where([u], u.id == ^user.id)
923 |> select([u], u)
924 |> Repo.update_all([])
925 |> case do
926 {1, [user]} -> set_cache(user)
927 _ -> {:error, user}
928 end
929 end
930
931 def set_unread_conversation_count(user), do: {:ok, user}
932
933 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
934 unread_query =
935 Participation.unread_conversation_count_for_user(user)
936 |> where([p], p.conversation_id == ^conversation.id)
937
938 User
939 |> join(:inner, [u], p in subquery(unread_query))
940 |> update([u, p],
941 inc: [unread_conversation_count: 1]
942 )
943 |> where([u], u.id == ^user.id)
944 |> where([u, p], p.count == 0)
945 |> select([u], u)
946 |> Repo.update_all([])
947 |> case do
948 {1, [user]} -> set_cache(user)
949 _ -> {:error, user}
950 end
951 end
952
953 def increment_unread_conversation_count(_, user), do: {:ok, user}
954
955 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
956 def get_users_from_set(ap_ids, local_only \\ true) do
957 criteria = %{ap_id: ap_ids, deactivated: false}
958 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
959
960 User.Query.build(criteria)
961 |> Repo.all()
962 end
963
964 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
965 def get_recipients_from_activity(%Activity{recipients: to}) do
966 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
967 |> Repo.all()
968 end
969
970 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
971 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
972 add_to_mutes(muter, ap_id, notifications?)
973 end
974
975 def unmute(muter, %{ap_id: ap_id}) do
976 remove_from_mutes(muter, ap_id)
977 end
978
979 def subscribe(subscriber, %{ap_id: ap_id}) do
980 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
981 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
982
983 if blocks?(subscribed, subscriber) and deny_follow_blocked do
984 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
985 else
986 User.add_to_subscribers(subscribed, subscriber.ap_id)
987 end
988 end
989 end
990
991 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
992 with %User{} = user <- get_cached_by_ap_id(ap_id) do
993 User.remove_from_subscribers(user, unsubscriber.ap_id)
994 end
995 end
996
997 def block(blocker, %User{} = blocked) do
998 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
999 blocker =
1000 if following?(blocker, blocked) do
1001 {:ok, blocker, _} = unfollow(blocker, blocked)
1002 blocker
1003 else
1004 blocker
1005 end
1006
1007 # clear any requested follows as well
1008 blocked =
1009 case CommonAPI.reject_follow_request(blocked, blocker) do
1010 {:ok, %User{} = updated_blocked} -> updated_blocked
1011 nil -> blocked
1012 end
1013
1014 blocker =
1015 if subscribed_to?(blocked, blocker) do
1016 {:ok, blocker} = unsubscribe(blocked, blocker)
1017 blocker
1018 else
1019 blocker
1020 end
1021
1022 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1023
1024 {:ok, blocker} = update_follower_count(blocker)
1025 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1026 add_to_block(blocker, blocked)
1027 end
1028
1029 # helper to handle the block given only an actor's AP id
1030 def block(blocker, %{ap_id: ap_id}) do
1031 block(blocker, get_cached_by_ap_id(ap_id))
1032 end
1033
1034 def unblock(blocker, %User{} = blocked) do
1035 remove_from_block(blocker, blocked)
1036 end
1037
1038 # helper to handle the block given only an actor's AP id
1039 def unblock(blocker, %{ap_id: ap_id}) do
1040 unblock(blocker, get_cached_by_ap_id(ap_id))
1041 end
1042
1043 def mutes?(nil, _), do: false
1044 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.mutes, ap_id)
1045
1046 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1047 def muted_notifications?(nil, _), do: false
1048
1049 def muted_notifications?(user, %{ap_id: ap_id}),
1050 do: Enum.member?(user.muted_notifications, ap_id)
1051
1052 def blocks?(%User{} = user, %User{} = target) do
1053 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1054 end
1055
1056 def blocks?(nil, _), do: false
1057
1058 def blocks_ap_id?(%User{} = user, %User{} = target) do
1059 UserBlock.exists?(user, target)
1060 end
1061
1062 def blocks_ap_id?(_, _), do: false
1063
1064 def blocks_domain?(%User{} = user, %User{} = target) do
1065 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1066 %{host: host} = URI.parse(target.ap_id)
1067 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1068 end
1069
1070 def blocks_domain?(_, _), do: false
1071
1072 def subscribed_to?(user, %{ap_id: ap_id}) do
1073 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1074 Enum.member?(target.subscribers, user.ap_id)
1075 end
1076 end
1077
1078 @spec muted_users(User.t()) :: [User.t()]
1079 def muted_users(user) do
1080 User.Query.build(%{ap_id: user.mutes, deactivated: false})
1081 |> Repo.all()
1082 end
1083
1084 @spec blocked_users(User.t()) :: [User.t()]
1085 def blocked_users(user) do
1086 user
1087 |> assoc(:blocked_users)
1088 |> restrict_deactivated()
1089 |> Repo.all()
1090 end
1091
1092 def blocked_ap_ids(user) do
1093 Repo.all(
1094 from(u in assoc(user, :blocked_users),
1095 select: u.ap_id
1096 )
1097 )
1098 end
1099
1100 @spec subscribers(User.t()) :: [User.t()]
1101 def subscribers(user) do
1102 User.Query.build(%{ap_id: user.subscribers, deactivated: false})
1103 |> Repo.all()
1104 end
1105
1106 def deactivate_async(user, status \\ true) do
1107 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1108 end
1109
1110 def deactivate(user, status \\ true)
1111
1112 def deactivate(users, status) when is_list(users) do
1113 Repo.transaction(fn ->
1114 for user <- users, do: deactivate(user, status)
1115 end)
1116 end
1117
1118 def deactivate(%User{} = user, status) do
1119 with {:ok, user} <- set_activation_status(user, status) do
1120 Enum.each(get_followers(user), &invalidate_cache/1)
1121
1122 # Only update local user counts, remote will be update during the next pull.
1123 user
1124 |> get_friends()
1125 |> Enum.filter(& &1.local)
1126 |> Enum.each(&update_follower_count/1)
1127
1128 {:ok, user}
1129 end
1130 end
1131
1132 def update_notification_settings(%User{} = user, settings) do
1133 settings =
1134 settings
1135 |> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end)
1136 |> Map.new()
1137
1138 notification_settings =
1139 user.notification_settings
1140 |> Map.merge(settings)
1141 |> Map.take(["followers", "follows", "non_follows", "non_followers"])
1142
1143 params = %{notification_settings: notification_settings}
1144
1145 user
1146 |> cast(params, [:notification_settings])
1147 |> validate_required([:notification_settings])
1148 |> update_and_set_cache()
1149 end
1150
1151 def delete(users) when is_list(users) do
1152 for user <- users, do: delete(user)
1153 end
1154
1155 def delete(%User{} = user) do
1156 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1157 end
1158
1159 def perform(:force_password_reset, user), do: force_password_reset(user)
1160
1161 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1162 def perform(:delete, %User{} = user) do
1163 {:ok, _user} = ActivityPub.delete(user)
1164
1165 # Remove all relationships
1166 user
1167 |> get_followers()
1168 |> Enum.each(fn follower ->
1169 ActivityPub.unfollow(follower, user)
1170 unfollow(follower, user)
1171 end)
1172
1173 user
1174 |> get_friends()
1175 |> Enum.each(fn followed ->
1176 ActivityPub.unfollow(user, followed)
1177 unfollow(user, followed)
1178 end)
1179
1180 delete_user_activities(user)
1181 invalidate_cache(user)
1182 Repo.delete(user)
1183 end
1184
1185 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1186 def perform(:fetch_initial_posts, %User{} = user) do
1187 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1188
1189 # Insert all the posts in reverse order, so they're in the right order on the timeline
1190 user.source_data["outbox"]
1191 |> Utils.fetch_ordered_collection(pages)
1192 |> Enum.reverse()
1193 |> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
1194 end
1195
1196 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1197
1198 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1199 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1200 when is_list(blocked_identifiers) do
1201 Enum.map(
1202 blocked_identifiers,
1203 fn blocked_identifier ->
1204 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1205 {:ok, _user_block} <- block(blocker, blocked),
1206 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1207 blocked
1208 else
1209 err ->
1210 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1211 err
1212 end
1213 end
1214 )
1215 end
1216
1217 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1218 def perform(:follow_import, %User{} = follower, followed_identifiers)
1219 when is_list(followed_identifiers) do
1220 Enum.map(
1221 followed_identifiers,
1222 fn followed_identifier ->
1223 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1224 {:ok, follower} <- maybe_direct_follow(follower, followed),
1225 {:ok, _} <- ActivityPub.follow(follower, followed) do
1226 followed
1227 else
1228 err ->
1229 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1230 err
1231 end
1232 end
1233 )
1234 end
1235
1236 @spec external_users_query() :: Ecto.Query.t()
1237 def external_users_query do
1238 User.Query.build(%{
1239 external: true,
1240 active: true,
1241 order_by: :id
1242 })
1243 end
1244
1245 @spec external_users(keyword()) :: [User.t()]
1246 def external_users(opts \\ []) do
1247 query =
1248 external_users_query()
1249 |> select([u], struct(u, [:id, :ap_id, :info]))
1250
1251 query =
1252 if opts[:max_id],
1253 do: where(query, [u], u.id > ^opts[:max_id]),
1254 else: query
1255
1256 query =
1257 if opts[:limit],
1258 do: limit(query, ^opts[:limit]),
1259 else: query
1260
1261 Repo.all(query)
1262 end
1263
1264 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1265 BackgroundWorker.enqueue("blocks_import", %{
1266 "blocker_id" => blocker.id,
1267 "blocked_identifiers" => blocked_identifiers
1268 })
1269 end
1270
1271 def follow_import(%User{} = follower, followed_identifiers)
1272 when is_list(followed_identifiers) do
1273 BackgroundWorker.enqueue("follow_import", %{
1274 "follower_id" => follower.id,
1275 "followed_identifiers" => followed_identifiers
1276 })
1277 end
1278
1279 def delete_user_activities(%User{ap_id: ap_id}) do
1280 ap_id
1281 |> Activity.Queries.by_actor()
1282 |> RepoStreamer.chunk_stream(50)
1283 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1284 |> Stream.run()
1285 end
1286
1287 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1288 activity
1289 |> Object.normalize()
1290 |> ActivityPub.delete()
1291 end
1292
1293 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1294 object = Object.normalize(activity)
1295
1296 activity.actor
1297 |> get_cached_by_ap_id()
1298 |> ActivityPub.unlike(object)
1299 end
1300
1301 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1302 object = Object.normalize(activity)
1303
1304 activity.actor
1305 |> get_cached_by_ap_id()
1306 |> ActivityPub.unannounce(object)
1307 end
1308
1309 defp delete_activity(_activity), do: "Doing nothing"
1310
1311 def html_filter_policy(%User{no_rich_text: true}) do
1312 Pleroma.HTML.Scrubber.TwitterText
1313 end
1314
1315 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1316
1317 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1318
1319 def get_or_fetch_by_ap_id(ap_id) do
1320 user = get_cached_by_ap_id(ap_id)
1321
1322 if !is_nil(user) and !needs_update?(user) do
1323 {:ok, user}
1324 else
1325 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1326 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1327
1328 resp = fetch_by_ap_id(ap_id)
1329
1330 if should_fetch_initial do
1331 with {:ok, %User{} = user} <- resp do
1332 fetch_initial_posts(user)
1333 end
1334 end
1335
1336 resp
1337 end
1338 end
1339
1340 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1341 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1342 with %User{} = user <- get_cached_by_ap_id(uri) do
1343 user
1344 else
1345 _ ->
1346 {:ok, user} =
1347 %User{}
1348 |> cast(%{}, [:ap_id, :nickname, :local])
1349 |> put_change(:ap_id, uri)
1350 |> put_change(:nickname, nickname)
1351 |> put_change(:local, true)
1352 |> put_change(:follower_address, uri <> "/followers")
1353 |> Repo.insert()
1354
1355 user
1356 end
1357 end
1358
1359 # AP style
1360 def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
1361 key =
1362 public_key_pem
1363 |> :public_key.pem_decode()
1364 |> hd()
1365 |> :public_key.pem_entry_decode()
1366
1367 {:ok, key}
1368 end
1369
1370 def public_key(_), do: {:error, "not found key"}
1371
1372 def get_public_key_for_ap_id(ap_id) do
1373 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1374 {:ok, public_key} <- public_key(user) do
1375 {:ok, public_key}
1376 else
1377 _ -> :error
1378 end
1379 end
1380
1381 defp blank?(""), do: nil
1382 defp blank?(n), do: n
1383
1384 def insert_or_update_user(data) do
1385 data
1386 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1387 |> remote_user_creation()
1388 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1389 |> set_cache()
1390 end
1391
1392 def ap_enabled?(%User{local: true}), do: true
1393 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1394 def ap_enabled?(_), do: false
1395
1396 @doc "Gets or fetch a user by uri or nickname."
1397 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1398 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1399 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1400
1401 # wait a period of time and return newest version of the User structs
1402 # this is because we have synchronous follow APIs and need to simulate them
1403 # with an async handshake
1404 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1405 with %User{} = a <- get_cached_by_id(a.id),
1406 %User{} = b <- get_cached_by_id(b.id) do
1407 {:ok, a, b}
1408 else
1409 nil -> :error
1410 end
1411 end
1412
1413 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1414 with :ok <- :timer.sleep(timeout),
1415 %User{} = a <- get_cached_by_id(a.id),
1416 %User{} = b <- get_cached_by_id(b.id) do
1417 {:ok, a, b}
1418 else
1419 nil -> :error
1420 end
1421 end
1422
1423 def parse_bio(bio) when is_binary(bio) and bio != "" do
1424 bio
1425 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1426 |> elem(0)
1427 end
1428
1429 def parse_bio(_), do: ""
1430
1431 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1432 # TODO: get profile URLs other than user.ap_id
1433 profile_urls = [user.ap_id]
1434
1435 bio
1436 |> CommonUtils.format_input("text/plain",
1437 mentions_format: :full,
1438 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1439 )
1440 |> elem(0)
1441 end
1442
1443 def parse_bio(_, _), do: ""
1444
1445 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1446 Repo.transaction(fn ->
1447 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1448 end)
1449 end
1450
1451 def tag(nickname, tags) when is_binary(nickname),
1452 do: tag(get_by_nickname(nickname), tags)
1453
1454 def tag(%User{} = user, tags),
1455 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1456
1457 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1458 Repo.transaction(fn ->
1459 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1460 end)
1461 end
1462
1463 def untag(nickname, tags) when is_binary(nickname),
1464 do: untag(get_by_nickname(nickname), tags)
1465
1466 def untag(%User{} = user, tags),
1467 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1468
1469 defp update_tags(%User{} = user, new_tags) do
1470 {:ok, updated_user} =
1471 user
1472 |> change(%{tags: new_tags})
1473 |> update_and_set_cache()
1474
1475 updated_user
1476 end
1477
1478 defp normalize_tags(tags) do
1479 [tags]
1480 |> List.flatten()
1481 |> Enum.map(&String.downcase/1)
1482 end
1483
1484 defp local_nickname_regex do
1485 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1486 @extended_local_nickname_regex
1487 else
1488 @strict_local_nickname_regex
1489 end
1490 end
1491
1492 def local_nickname(nickname_or_mention) do
1493 nickname_or_mention
1494 |> full_nickname()
1495 |> String.split("@")
1496 |> hd()
1497 end
1498
1499 def full_nickname(nickname_or_mention),
1500 do: String.trim_leading(nickname_or_mention, "@")
1501
1502 def error_user(ap_id) do
1503 %User{
1504 name: ap_id,
1505 ap_id: ap_id,
1506 nickname: "erroruser@example.com",
1507 inserted_at: NaiveDateTime.utc_now()
1508 }
1509 end
1510
1511 @spec all_superusers() :: [User.t()]
1512 def all_superusers do
1513 User.Query.build(%{super_users: true, local: true, deactivated: false})
1514 |> Repo.all()
1515 end
1516
1517 def showing_reblogs?(%User{} = user, %User{} = target) do
1518 target.ap_id not in user.muted_reblogs
1519 end
1520
1521 @doc """
1522 The function returns a query to get users with no activity for given interval of days.
1523 Inactive users are those who didn't read any notification, or had any activity where
1524 the user is the activity's actor, during `inactivity_threshold` days.
1525 Deactivated users will not appear in this list.
1526
1527 ## Examples
1528
1529 iex> Pleroma.User.list_inactive_users()
1530 %Ecto.Query{}
1531 """
1532 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1533 def list_inactive_users_query(inactivity_threshold \\ 7) do
1534 negative_inactivity_threshold = -inactivity_threshold
1535 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1536 # Subqueries are not supported in `where` clauses, join gets too complicated.
1537 has_read_notifications =
1538 from(n in Pleroma.Notification,
1539 where: n.seen == true,
1540 group_by: n.id,
1541 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1542 select: n.user_id
1543 )
1544 |> Pleroma.Repo.all()
1545
1546 from(u in Pleroma.User,
1547 left_join: a in Pleroma.Activity,
1548 on: u.ap_id == a.actor,
1549 where: not is_nil(u.nickname),
1550 where: u.deactivated != ^true,
1551 where: u.id not in ^has_read_notifications,
1552 group_by: u.id,
1553 having:
1554 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1555 is_nil(max(a.inserted_at))
1556 )
1557 end
1558
1559 @doc """
1560 Enable or disable email notifications for user
1561
1562 ## Examples
1563
1564 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1565 Pleroma.User{email_notifications: %{"digest" => true}}
1566
1567 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1568 Pleroma.User{email_notifications: %{"digest" => false}}
1569 """
1570 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1571 {:ok, t()} | {:error, Ecto.Changeset.t()}
1572 def switch_email_notifications(user, type, status) do
1573 User.update_email_notifications(user, %{type => status})
1574 end
1575
1576 @doc """
1577 Set `last_digest_emailed_at` value for the user to current time
1578 """
1579 @spec touch_last_digest_emailed_at(t()) :: t()
1580 def touch_last_digest_emailed_at(user) do
1581 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1582
1583 {:ok, updated_user} =
1584 user
1585 |> change(%{last_digest_emailed_at: now})
1586 |> update_and_set_cache()
1587
1588 updated_user
1589 end
1590
1591 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1592 def toggle_confirmation(%User{} = user) do
1593 user
1594 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1595 |> update_and_set_cache()
1596 end
1597
1598 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1599 mascot
1600 end
1601
1602 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1603 # use instance-default
1604 config = Pleroma.Config.get([:assets, :mascots])
1605 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1606 mascot = Keyword.get(config, default_mascot)
1607
1608 %{
1609 "id" => "default-mascot",
1610 "url" => mascot[:url],
1611 "preview_url" => mascot[:url],
1612 "pleroma" => %{
1613 "mime_type" => mascot[:mime_type]
1614 }
1615 }
1616 end
1617
1618 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1619
1620 def ensure_keys_present(%User{} = user) do
1621 with {:ok, pem} <- Keys.generate_rsa_pem() do
1622 user
1623 |> cast(%{keys: pem}, [:keys])
1624 |> validate_required([:keys])
1625 |> update_and_set_cache()
1626 end
1627 end
1628
1629 def get_ap_ids_by_nicknames(nicknames) do
1630 from(u in User,
1631 where: u.nickname in ^nicknames,
1632 select: u.ap_id
1633 )
1634 |> Repo.all()
1635 end
1636
1637 defdelegate search(query, opts \\ []), to: User.Search
1638
1639 defp put_password_hash(
1640 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1641 ) do
1642 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1643 end
1644
1645 defp put_password_hash(changeset), do: changeset
1646
1647 def is_internal_user?(%User{nickname: nil}), do: true
1648 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1649 def is_internal_user?(_), do: false
1650
1651 # A hack because user delete activities have a fake id for whatever reason
1652 # TODO: Get rid of this
1653 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1654
1655 def get_delivered_users_by_object_id(object_id) do
1656 from(u in User,
1657 inner_join: delivery in assoc(u, :deliveries),
1658 where: delivery.object_id == ^object_id
1659 )
1660 |> Repo.all()
1661 end
1662
1663 def change_email(user, email) do
1664 user
1665 |> cast(%{email: email}, [:email])
1666 |> validate_required([:email])
1667 |> unique_constraint(:email)
1668 |> validate_format(:email, @email_regex)
1669 |> update_and_set_cache()
1670 end
1671
1672 # Internal function; public one is `deactivate/2`
1673 defp set_activation_status(user, deactivated) do
1674 user
1675 |> cast(%{deactivated: deactivated}, [:deactivated])
1676 |> update_and_set_cache()
1677 end
1678
1679 def update_banner(user, banner) do
1680 user
1681 |> cast(%{banner: banner}, [:banner])
1682 |> update_and_set_cache()
1683 end
1684
1685 def update_background(user, background) do
1686 user
1687 |> cast(%{background: background}, [:background])
1688 |> update_and_set_cache()
1689 end
1690
1691 def update_source_data(user, source_data) do
1692 user
1693 |> cast(%{source_data: source_data}, [:source_data])
1694 |> update_and_set_cache()
1695 end
1696
1697 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1698 %{
1699 admin: is_admin,
1700 moderator: is_moderator
1701 }
1702 end
1703
1704 # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
1705 # For example: [{"name": "Pronoun", "value": "she/her"}, …]
1706 def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
1707 limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
1708
1709 attachment
1710 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
1711 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
1712 |> Enum.take(limit)
1713 end
1714
1715 def fields(%{fields: nil}), do: []
1716
1717 def fields(%{fields: fields}), do: fields
1718
1719 def validate_fields(changeset, remote? \\ false) do
1720 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
1721 limit = Pleroma.Config.get([:instance, limit_name], 0)
1722
1723 changeset
1724 |> validate_length(:fields, max: limit)
1725 |> validate_change(:fields, fn :fields, fields ->
1726 if Enum.all?(fields, &valid_field?/1) do
1727 []
1728 else
1729 [fields: "invalid"]
1730 end
1731 end)
1732 end
1733
1734 defp valid_field?(%{"name" => name, "value" => value}) do
1735 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
1736 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
1737
1738 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
1739 String.length(value) <= value_limit
1740 end
1741
1742 defp valid_field?(_), do: false
1743
1744 defp truncate_field(%{"name" => name, "value" => value}) do
1745 {name, _chopped} =
1746 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
1747
1748 {value, _chopped} =
1749 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
1750
1751 %{"name" => name, "value" => value}
1752 end
1753
1754 def admin_api_update(user, params) do
1755 user
1756 |> cast(params, [
1757 :is_moderator,
1758 :is_admin,
1759 :show_role
1760 ])
1761 |> update_and_set_cache()
1762 end
1763
1764 def mascot_update(user, url) do
1765 user
1766 |> cast(%{mascot: url}, [:mascot])
1767 |> validate_required([:mascot])
1768 |> update_and_set_cache()
1769 end
1770
1771 def mastodon_settings_update(user, settings) do
1772 user
1773 |> cast(%{settings: settings}, [:settings])
1774 |> validate_required([:settings])
1775 |> update_and_set_cache()
1776 end
1777
1778 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
1779 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
1780 params =
1781 if need_confirmation? do
1782 %{
1783 confirmation_pending: true,
1784 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
1785 }
1786 else
1787 %{
1788 confirmation_pending: false,
1789 confirmation_token: nil
1790 }
1791 end
1792
1793 cast(user, params, [:confirmation_pending, :confirmation_token])
1794 end
1795
1796 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
1797 if id not in user.pinned_activities do
1798 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
1799 params = %{pinned_activities: user.pinned_activities ++ [id]}
1800
1801 user
1802 |> cast(params, [:pinned_activities])
1803 |> validate_length(:pinned_activities,
1804 max: max_pinned_statuses,
1805 message: "You have already pinned the maximum number of statuses"
1806 )
1807 else
1808 change(user)
1809 end
1810 |> update_and_set_cache()
1811 end
1812
1813 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
1814 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
1815
1816 user
1817 |> cast(params, [:pinned_activities])
1818 |> update_and_set_cache()
1819 end
1820
1821 def update_email_notifications(user, settings) do
1822 email_notifications =
1823 user.email_notifications
1824 |> Map.merge(settings)
1825 |> Map.take(["digest"])
1826
1827 params = %{email_notifications: email_notifications}
1828 fields = [:email_notifications]
1829
1830 user
1831 |> cast(params, fields)
1832 |> validate_required(fields)
1833 |> update_and_set_cache()
1834 end
1835
1836 defp set_subscribers(user, subscribers) do
1837 params = %{subscribers: subscribers}
1838
1839 user
1840 |> cast(params, [:subscribers])
1841 |> validate_required([:subscribers])
1842 |> update_and_set_cache()
1843 end
1844
1845 def add_to_subscribers(user, subscribed) do
1846 set_subscribers(user, Enum.uniq([subscribed | user.subscribers]))
1847 end
1848
1849 def remove_from_subscribers(user, subscribed) do
1850 set_subscribers(user, List.delete(user.subscribers, subscribed))
1851 end
1852
1853 defp set_domain_blocks(user, domain_blocks) do
1854 params = %{domain_blocks: domain_blocks}
1855
1856 user
1857 |> cast(params, [:domain_blocks])
1858 |> validate_required([:domain_blocks])
1859 |> update_and_set_cache()
1860 end
1861
1862 def block_domain(user, domain_blocked) do
1863 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
1864 end
1865
1866 def unblock_domain(user, domain_blocked) do
1867 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
1868 end
1869
1870 @spec add_to_block(User.t(), User.t()) :: {:ok, UserBlock.t()} | {:error, Ecto.Changeset.t()}
1871 defp add_to_block(%User{} = user, %User{} = blocked) do
1872 UserBlock.create(user, blocked)
1873 end
1874
1875 @spec add_to_block(User.t(), User.t()) ::
1876 {:ok, UserBlock.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
1877 defp remove_from_block(%User{} = user, %User{} = blocked) do
1878 UserBlock.delete(user, blocked)
1879 end
1880
1881 defp set_mutes(user, mutes) do
1882 params = %{mutes: mutes}
1883
1884 user
1885 |> cast(params, [:mutes])
1886 |> validate_required([:mutes])
1887 |> update_and_set_cache()
1888 end
1889
1890 def add_to_mutes(user, muted, notifications?) do
1891 with {:ok, user} <- set_mutes(user, Enum.uniq([muted | user.mutes])) do
1892 set_notification_mutes(
1893 user,
1894 Enum.uniq([muted | user.muted_notifications]),
1895 notifications?
1896 )
1897 end
1898 end
1899
1900 def remove_from_mutes(user, muted) do
1901 with {:ok, user} <- set_mutes(user, List.delete(user.mutes, muted)) do
1902 set_notification_mutes(
1903 user,
1904 List.delete(user.muted_notifications, muted),
1905 true
1906 )
1907 end
1908 end
1909
1910 defp set_notification_mutes(user, _muted_notifications, false = _notifications?) do
1911 {:ok, user}
1912 end
1913
1914 defp set_notification_mutes(user, muted_notifications, true = _notifications?) do
1915 params = %{muted_notifications: muted_notifications}
1916
1917 user
1918 |> cast(params, [:muted_notifications])
1919 |> validate_required([:muted_notifications])
1920 |> update_and_set_cache()
1921 end
1922
1923 def add_reblog_mute(user, ap_id) do
1924 params = %{muted_reblogs: user.muted_reblogs ++ [ap_id]}
1925
1926 user
1927 |> cast(params, [:muted_reblogs])
1928 |> update_and_set_cache()
1929 end
1930
1931 def remove_reblog_mute(user, ap_id) do
1932 params = %{muted_reblogs: List.delete(user.muted_reblogs, ap_id)}
1933
1934 user
1935 |> cast(params, [:muted_reblogs])
1936 |> update_and_set_cache()
1937 end
1938
1939 def set_invisible(user, invisible) do
1940 params = %{invisible: invisible}
1941
1942 user
1943 |> cast(params, [:invisible])
1944 |> validate_required([:invisible])
1945 |> update_and_set_cache()
1946 end
1947 end