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