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