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