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