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