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