Streamer: Stream boosts to the boosting user.
[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
692 # "Locked" (self-locked) users demand explicit authorization of follow requests
693 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
694 follow(follower, followed, :follow_pending)
695 end
696
697 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
698 follow(follower, followed)
699 end
700
701 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
702 if not ap_enabled?(followed) do
703 follow(follower, followed)
704 else
705 {:ok, follower}
706 end
707 end
708
709 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
710 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
711 def follow_all(follower, followeds) do
712 followeds
713 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
714 |> Enum.each(&follow(follower, &1, :follow_accept))
715
716 set_cache(follower)
717 end
718
719 defdelegate following(user), to: FollowingRelationship
720
721 def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
722 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
723
724 cond do
725 followed.deactivated ->
726 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
727
728 deny_follow_blocked and blocks?(followed, follower) ->
729 {:error, "Could not follow user: #{followed.nickname} blocked you."}
730
731 true ->
732 FollowingRelationship.follow(follower, followed, state)
733
734 {:ok, _} = update_follower_count(followed)
735
736 follower
737 |> update_following_count()
738 |> set_cache()
739 end
740 end
741
742 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
743 {:error, "Not subscribed!"}
744 end
745
746 def unfollow(%User{} = follower, %User{} = followed) do
747 case get_follow_state(follower, followed) do
748 state when state in [:follow_pending, :follow_accept] ->
749 FollowingRelationship.unfollow(follower, followed)
750 {:ok, followed} = update_follower_count(followed)
751
752 {:ok, follower} =
753 follower
754 |> update_following_count()
755 |> set_cache()
756
757 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
758
759 nil ->
760 {:error, "Not subscribed!"}
761 end
762 end
763
764 defdelegate following?(follower, followed), to: FollowingRelationship
765
766 @doc "Returns follow state as Pleroma.FollowingRelationship.State value"
767 def get_follow_state(%User{} = follower, %User{} = following) do
768 following_relationship = FollowingRelationship.get(follower, following)
769 get_follow_state(follower, following, following_relationship)
770 end
771
772 def get_follow_state(
773 %User{} = follower,
774 %User{} = following,
775 following_relationship
776 ) do
777 case {following_relationship, following.local} do
778 {nil, false} ->
779 case Utils.fetch_latest_follow(follower, following) do
780 %Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
781 FollowingRelationship.state_to_enum(state)
782
783 _ ->
784 nil
785 end
786
787 {%{state: state}, _} ->
788 state
789
790 {nil, _} ->
791 nil
792 end
793 end
794
795 def locked?(%User{} = user) do
796 user.locked || false
797 end
798
799 def get_by_id(id) do
800 Repo.get_by(User, id: id)
801 end
802
803 def get_by_ap_id(ap_id) do
804 Repo.get_by(User, ap_id: ap_id)
805 end
806
807 def get_all_by_ap_id(ap_ids) do
808 from(u in __MODULE__,
809 where: u.ap_id in ^ap_ids
810 )
811 |> Repo.all()
812 end
813
814 def get_all_by_ids(ids) do
815 from(u in __MODULE__, where: u.id in ^ids)
816 |> Repo.all()
817 end
818
819 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
820 # of the ap_id and the domain and tries to get that user
821 def get_by_guessed_nickname(ap_id) do
822 domain = URI.parse(ap_id).host
823 name = List.last(String.split(ap_id, "/"))
824 nickname = "#{name}@#{domain}"
825
826 get_cached_by_nickname(nickname)
827 end
828
829 def set_cache({:ok, user}), do: set_cache(user)
830 def set_cache({:error, err}), do: {:error, err}
831
832 def set_cache(%User{} = user) do
833 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
834 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
835 {:ok, user}
836 end
837
838 def update_and_set_cache(struct, params) do
839 struct
840 |> update_changeset(params)
841 |> update_and_set_cache()
842 end
843
844 def update_and_set_cache(changeset) do
845 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
846 set_cache(user)
847 end
848 end
849
850 def invalidate_cache(user) do
851 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
852 Cachex.del(:user_cache, "nickname:#{user.nickname}")
853 end
854
855 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
856 def get_cached_by_ap_id(ap_id) do
857 key = "ap_id:#{ap_id}"
858
859 with {:ok, nil} <- Cachex.get(:user_cache, key),
860 user when not is_nil(user) <- get_by_ap_id(ap_id),
861 {:ok, true} <- Cachex.put(:user_cache, key, user) do
862 user
863 else
864 {:ok, user} -> user
865 nil -> nil
866 end
867 end
868
869 def get_cached_by_id(id) do
870 key = "id:#{id}"
871
872 ap_id =
873 Cachex.fetch!(:user_cache, key, fn _ ->
874 user = get_by_id(id)
875
876 if user do
877 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
878 {:commit, user.ap_id}
879 else
880 {:ignore, ""}
881 end
882 end)
883
884 get_cached_by_ap_id(ap_id)
885 end
886
887 def get_cached_by_nickname(nickname) do
888 key = "nickname:#{nickname}"
889
890 Cachex.fetch!(:user_cache, key, fn ->
891 case get_or_fetch_by_nickname(nickname) do
892 {:ok, user} -> {:commit, user}
893 {:error, _error} -> {:ignore, nil}
894 end
895 end)
896 end
897
898 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
899 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
900
901 cond do
902 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
903 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
904
905 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
906 get_cached_by_nickname(nickname_or_id)
907
908 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
909 get_cached_by_nickname(nickname_or_id)
910
911 true ->
912 nil
913 end
914 end
915
916 def get_by_nickname(nickname) do
917 Repo.get_by(User, nickname: nickname) ||
918 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
919 Repo.get_by(User, nickname: local_nickname(nickname))
920 end
921 end
922
923 def get_by_email(email), do: Repo.get_by(User, email: email)
924
925 def get_by_nickname_or_email(nickname_or_email) do
926 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
927 end
928
929 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
930
931 def get_or_fetch_by_nickname(nickname) do
932 with %User{} = user <- get_by_nickname(nickname) do
933 {:ok, user}
934 else
935 _e ->
936 with [_nick, _domain] <- String.split(nickname, "@"),
937 {:ok, user} <- fetch_by_nickname(nickname) do
938 {:ok, user}
939 else
940 _e -> {:error, "not found " <> nickname}
941 end
942 end
943 end
944
945 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
946 def get_followers_query(%User{} = user, nil) do
947 User.Query.build(%{followers: user, deactivated: false})
948 end
949
950 def get_followers_query(user, page) do
951 user
952 |> get_followers_query(nil)
953 |> User.Query.paginate(page, 20)
954 end
955
956 @spec get_followers_query(User.t()) :: Ecto.Query.t()
957 def get_followers_query(user), do: get_followers_query(user, nil)
958
959 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
960 def get_followers(user, page \\ nil) do
961 user
962 |> get_followers_query(page)
963 |> Repo.all()
964 end
965
966 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
967 def get_external_followers(user, page \\ nil) do
968 user
969 |> get_followers_query(page)
970 |> User.Query.build(%{external: true})
971 |> Repo.all()
972 end
973
974 def get_followers_ids(user, page \\ nil) do
975 user
976 |> get_followers_query(page)
977 |> select([u], u.id)
978 |> Repo.all()
979 end
980
981 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
982 def get_friends_query(%User{} = user, nil) do
983 User.Query.build(%{friends: user, deactivated: false})
984 end
985
986 def get_friends_query(user, page) do
987 user
988 |> get_friends_query(nil)
989 |> User.Query.paginate(page, 20)
990 end
991
992 @spec get_friends_query(User.t()) :: Ecto.Query.t()
993 def get_friends_query(user), do: get_friends_query(user, nil)
994
995 def get_friends(user, page \\ nil) do
996 user
997 |> get_friends_query(page)
998 |> Repo.all()
999 end
1000
1001 def get_friends_ap_ids(user) do
1002 user
1003 |> get_friends_query(nil)
1004 |> select([u], u.ap_id)
1005 |> Repo.all()
1006 end
1007
1008 def get_friends_ids(user, page \\ nil) do
1009 user
1010 |> get_friends_query(page)
1011 |> select([u], u.id)
1012 |> Repo.all()
1013 end
1014
1015 defdelegate get_follow_requests(user), to: FollowingRelationship
1016
1017 def increase_note_count(%User{} = user) do
1018 User
1019 |> where(id: ^user.id)
1020 |> update([u], inc: [note_count: 1])
1021 |> select([u], u)
1022 |> Repo.update_all([])
1023 |> case do
1024 {1, [user]} -> set_cache(user)
1025 _ -> {:error, user}
1026 end
1027 end
1028
1029 def decrease_note_count(%User{} = user) do
1030 User
1031 |> where(id: ^user.id)
1032 |> update([u],
1033 set: [
1034 note_count: fragment("greatest(0, note_count - 1)")
1035 ]
1036 )
1037 |> select([u], u)
1038 |> Repo.update_all([])
1039 |> case do
1040 {1, [user]} -> set_cache(user)
1041 _ -> {:error, user}
1042 end
1043 end
1044
1045 def update_note_count(%User{} = user, note_count \\ nil) do
1046 note_count =
1047 note_count ||
1048 from(
1049 a in Object,
1050 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1051 select: count(a.id)
1052 )
1053 |> Repo.one()
1054
1055 user
1056 |> cast(%{note_count: note_count}, [:note_count])
1057 |> update_and_set_cache()
1058 end
1059
1060 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1061 def maybe_fetch_follow_information(user) do
1062 with {:ok, user} <- fetch_follow_information(user) do
1063 user
1064 else
1065 e ->
1066 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1067
1068 user
1069 end
1070 end
1071
1072 def fetch_follow_information(user) do
1073 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1074 user
1075 |> follow_information_changeset(info)
1076 |> update_and_set_cache()
1077 end
1078 end
1079
1080 defp follow_information_changeset(user, params) do
1081 user
1082 |> cast(params, [
1083 :hide_followers,
1084 :hide_follows,
1085 :follower_count,
1086 :following_count,
1087 :hide_followers_count,
1088 :hide_follows_count
1089 ])
1090 end
1091
1092 def update_follower_count(%User{} = user) do
1093 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
1094 follower_count_query =
1095 User.Query.build(%{followers: user, deactivated: false})
1096 |> select([u], %{count: count(u.id)})
1097
1098 User
1099 |> where(id: ^user.id)
1100 |> join(:inner, [u], s in subquery(follower_count_query))
1101 |> update([u, s],
1102 set: [follower_count: s.count]
1103 )
1104 |> select([u], u)
1105 |> Repo.update_all([])
1106 |> case do
1107 {1, [user]} -> set_cache(user)
1108 _ -> {:error, user}
1109 end
1110 else
1111 {:ok, maybe_fetch_follow_information(user)}
1112 end
1113 end
1114
1115 @spec update_following_count(User.t()) :: User.t()
1116 def update_following_count(%User{local: false} = user) do
1117 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1118 maybe_fetch_follow_information(user)
1119 else
1120 user
1121 end
1122 end
1123
1124 def update_following_count(%User{local: true} = user) do
1125 following_count = FollowingRelationship.following_count(user)
1126
1127 user
1128 |> follow_information_changeset(%{following_count: following_count})
1129 |> Repo.update!()
1130 end
1131
1132 def set_unread_conversation_count(%User{local: true} = user) do
1133 unread_query = Participation.unread_conversation_count_for_user(user)
1134
1135 User
1136 |> join(:inner, [u], p in subquery(unread_query))
1137 |> update([u, p],
1138 set: [unread_conversation_count: p.count]
1139 )
1140 |> where([u], u.id == ^user.id)
1141 |> select([u], u)
1142 |> Repo.update_all([])
1143 |> case do
1144 {1, [user]} -> set_cache(user)
1145 _ -> {:error, user}
1146 end
1147 end
1148
1149 def set_unread_conversation_count(user), do: {:ok, user}
1150
1151 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
1152 unread_query =
1153 Participation.unread_conversation_count_for_user(user)
1154 |> where([p], p.conversation_id == ^conversation.id)
1155
1156 User
1157 |> join(:inner, [u], p in subquery(unread_query))
1158 |> update([u, p],
1159 inc: [unread_conversation_count: 1]
1160 )
1161 |> where([u], u.id == ^user.id)
1162 |> where([u, p], p.count == 0)
1163 |> select([u], u)
1164 |> Repo.update_all([])
1165 |> case do
1166 {1, [user]} -> set_cache(user)
1167 _ -> {:error, user}
1168 end
1169 end
1170
1171 def increment_unread_conversation_count(_, user), do: {:ok, user}
1172
1173 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
1174 def get_users_from_set(ap_ids, local_only \\ true) do
1175 criteria = %{ap_id: ap_ids, deactivated: false}
1176 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1177
1178 User.Query.build(criteria)
1179 |> Repo.all()
1180 end
1181
1182 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1183 def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
1184 to = [actor | to]
1185
1186 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1187 |> Repo.all()
1188 end
1189
1190 @spec mute(User.t(), User.t(), boolean()) ::
1191 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1192 def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
1193 add_to_mutes(muter, mutee, notifications?)
1194 end
1195
1196 def unmute(%User{} = muter, %User{} = mutee) do
1197 remove_from_mutes(muter, mutee)
1198 end
1199
1200 def subscribe(%User{} = subscriber, %User{} = target) do
1201 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1202
1203 if blocks?(target, subscriber) and deny_follow_blocked do
1204 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1205 else
1206 # Note: the relationship is inverse: subscriber acts as relationship target
1207 UserRelationship.create_inverse_subscription(target, subscriber)
1208 end
1209 end
1210
1211 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1212 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1213 subscribe(subscriber, subscribee)
1214 end
1215 end
1216
1217 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1218 # Note: the relationship is inverse: subscriber acts as relationship target
1219 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1220 end
1221
1222 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1223 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1224 unsubscribe(unsubscriber, user)
1225 end
1226 end
1227
1228 def block(%User{} = blocker, %User{} = blocked) do
1229 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1230 blocker =
1231 if following?(blocker, blocked) do
1232 {:ok, blocker, _} = unfollow(blocker, blocked)
1233 blocker
1234 else
1235 blocker
1236 end
1237
1238 # clear any requested follows as well
1239 blocked =
1240 case CommonAPI.reject_follow_request(blocked, blocker) do
1241 {:ok, %User{} = updated_blocked} -> updated_blocked
1242 nil -> blocked
1243 end
1244
1245 unsubscribe(blocked, blocker)
1246
1247 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1248
1249 {:ok, blocker} = update_follower_count(blocker)
1250 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1251 add_to_block(blocker, blocked)
1252 end
1253
1254 # helper to handle the block given only an actor's AP id
1255 def block(%User{} = blocker, %{ap_id: ap_id}) do
1256 block(blocker, get_cached_by_ap_id(ap_id))
1257 end
1258
1259 def unblock(%User{} = blocker, %User{} = blocked) do
1260 remove_from_block(blocker, blocked)
1261 end
1262
1263 # helper to handle the block given only an actor's AP id
1264 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1265 unblock(blocker, get_cached_by_ap_id(ap_id))
1266 end
1267
1268 def mutes?(nil, _), do: false
1269 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1270
1271 def mutes_user?(%User{} = user, %User{} = target) do
1272 UserRelationship.mute_exists?(user, target)
1273 end
1274
1275 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1276 def muted_notifications?(nil, _), do: false
1277
1278 def muted_notifications?(%User{} = user, %User{} = target),
1279 do: UserRelationship.notification_mute_exists?(user, target)
1280
1281 def blocks?(nil, _), do: false
1282
1283 def blocks?(%User{} = user, %User{} = target) do
1284 blocks_user?(user, target) ||
1285 (blocks_domain?(user, target) and not User.following?(user, target))
1286 end
1287
1288 def blocks_user?(%User{} = user, %User{} = target) do
1289 UserRelationship.block_exists?(user, target)
1290 end
1291
1292 def blocks_user?(_, _), do: false
1293
1294 def blocks_domain?(%User{} = user, %User{} = target) do
1295 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1296 %{host: host} = URI.parse(target.ap_id)
1297 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1298 end
1299
1300 def blocks_domain?(_, _), do: false
1301
1302 def subscribed_to?(%User{} = user, %User{} = target) do
1303 # Note: the relationship is inverse: subscriber acts as relationship target
1304 UserRelationship.inverse_subscription_exists?(target, user)
1305 end
1306
1307 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1308 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1309 subscribed_to?(user, target)
1310 end
1311 end
1312
1313 @doc """
1314 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1315 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1316 """
1317 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1318 def outgoing_relationships_ap_ids(_user, []), do: %{}
1319
1320 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1321
1322 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1323 when is_list(relationship_types) do
1324 db_result =
1325 user
1326 |> assoc(:outgoing_relationships)
1327 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1328 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1329 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1330 |> group_by([user_rel, u], user_rel.relationship_type)
1331 |> Repo.all()
1332 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1333
1334 Enum.into(
1335 relationship_types,
1336 %{},
1337 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1338 )
1339 end
1340
1341 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1342
1343 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1344
1345 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1346
1347 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1348 when is_list(relationship_types) do
1349 user
1350 |> assoc(:incoming_relationships)
1351 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1352 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1353 |> maybe_filter_on_ap_id(ap_ids)
1354 |> select([user_rel, u], u.ap_id)
1355 |> distinct(true)
1356 |> Repo.all()
1357 end
1358
1359 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1360 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1361 end
1362
1363 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1364
1365 def deactivate_async(user, status \\ true) do
1366 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1367 end
1368
1369 def deactivate(user, status \\ true)
1370
1371 def deactivate(users, status) when is_list(users) do
1372 Repo.transaction(fn ->
1373 for user <- users, do: deactivate(user, status)
1374 end)
1375 end
1376
1377 def deactivate(%User{} = user, status) do
1378 with {:ok, user} <- set_activation_status(user, status) do
1379 user
1380 |> get_followers()
1381 |> Enum.filter(& &1.local)
1382 |> Enum.each(fn follower ->
1383 follower |> update_following_count() |> set_cache()
1384 end)
1385
1386 # Only update local user counts, remote will be update during the next pull.
1387 user
1388 |> get_friends()
1389 |> Enum.filter(& &1.local)
1390 |> Enum.each(&update_follower_count/1)
1391
1392 {:ok, user}
1393 end
1394 end
1395
1396 def update_notification_settings(%User{} = user, settings) do
1397 user
1398 |> cast(%{notification_settings: settings}, [])
1399 |> cast_embed(:notification_settings)
1400 |> validate_required([:notification_settings])
1401 |> update_and_set_cache()
1402 end
1403
1404 def delete(users) when is_list(users) do
1405 for user <- users, do: delete(user)
1406 end
1407
1408 def delete(%User{} = user) do
1409 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1410 end
1411
1412 def perform(:force_password_reset, user), do: force_password_reset(user)
1413
1414 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1415 def perform(:delete, %User{} = user) do
1416 {:ok, _user} = ActivityPub.delete(user)
1417
1418 # Remove all relationships
1419 user
1420 |> get_followers()
1421 |> Enum.each(fn follower ->
1422 ActivityPub.unfollow(follower, user)
1423 unfollow(follower, user)
1424 end)
1425
1426 user
1427 |> get_friends()
1428 |> Enum.each(fn followed ->
1429 ActivityPub.unfollow(user, followed)
1430 unfollow(user, followed)
1431 end)
1432
1433 delete_user_activities(user)
1434 invalidate_cache(user)
1435 Repo.delete(user)
1436 end
1437
1438 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1439
1440 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1441 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1442 when is_list(blocked_identifiers) do
1443 Enum.map(
1444 blocked_identifiers,
1445 fn blocked_identifier ->
1446 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1447 {:ok, _user_block} <- block(blocker, blocked),
1448 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1449 blocked
1450 else
1451 err ->
1452 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1453 err
1454 end
1455 end
1456 )
1457 end
1458
1459 def perform(:follow_import, %User{} = follower, followed_identifiers)
1460 when is_list(followed_identifiers) do
1461 Enum.map(
1462 followed_identifiers,
1463 fn followed_identifier ->
1464 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1465 {:ok, follower} <- maybe_direct_follow(follower, followed),
1466 {:ok, _} <- ActivityPub.follow(follower, followed) do
1467 followed
1468 else
1469 err ->
1470 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1471 err
1472 end
1473 end
1474 )
1475 end
1476
1477 @spec external_users_query() :: Ecto.Query.t()
1478 def external_users_query do
1479 User.Query.build(%{
1480 external: true,
1481 active: true,
1482 order_by: :id
1483 })
1484 end
1485
1486 @spec external_users(keyword()) :: [User.t()]
1487 def external_users(opts \\ []) do
1488 query =
1489 external_users_query()
1490 |> select([u], struct(u, [:id, :ap_id]))
1491
1492 query =
1493 if opts[:max_id],
1494 do: where(query, [u], u.id > ^opts[:max_id]),
1495 else: query
1496
1497 query =
1498 if opts[:limit],
1499 do: limit(query, ^opts[:limit]),
1500 else: query
1501
1502 Repo.all(query)
1503 end
1504
1505 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1506 BackgroundWorker.enqueue("blocks_import", %{
1507 "blocker_id" => blocker.id,
1508 "blocked_identifiers" => blocked_identifiers
1509 })
1510 end
1511
1512 def follow_import(%User{} = follower, followed_identifiers)
1513 when is_list(followed_identifiers) do
1514 BackgroundWorker.enqueue("follow_import", %{
1515 "follower_id" => follower.id,
1516 "followed_identifiers" => followed_identifiers
1517 })
1518 end
1519
1520 def delete_user_activities(%User{ap_id: ap_id}) do
1521 ap_id
1522 |> Activity.Queries.by_actor()
1523 |> RepoStreamer.chunk_stream(50)
1524 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1525 |> Stream.run()
1526 end
1527
1528 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1529 activity
1530 |> Object.normalize()
1531 |> ActivityPub.delete()
1532 end
1533
1534 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1535 object = Object.normalize(activity)
1536
1537 activity.actor
1538 |> get_cached_by_ap_id()
1539 |> ActivityPub.unlike(object)
1540 end
1541
1542 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1543 object = Object.normalize(activity)
1544
1545 activity.actor
1546 |> get_cached_by_ap_id()
1547 |> ActivityPub.unannounce(object)
1548 end
1549
1550 defp delete_activity(_activity), do: "Doing nothing"
1551
1552 def html_filter_policy(%User{no_rich_text: true}) do
1553 Pleroma.HTML.Scrubber.TwitterText
1554 end
1555
1556 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1557
1558 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1559
1560 def get_or_fetch_by_ap_id(ap_id) do
1561 user = get_cached_by_ap_id(ap_id)
1562
1563 if !is_nil(user) and !needs_update?(user) do
1564 {:ok, user}
1565 else
1566 fetch_by_ap_id(ap_id)
1567 end
1568 end
1569
1570 @doc """
1571 Creates an internal service actor by URI if missing.
1572 Optionally takes nickname for addressing.
1573 """
1574 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1575 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1576 {_, user} =
1577 case get_cached_by_ap_id(uri) do
1578 nil ->
1579 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1580 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1581 {:error, nil}
1582 end
1583
1584 %User{invisible: false} = user ->
1585 set_invisible(user)
1586
1587 user ->
1588 {:ok, user}
1589 end
1590
1591 user
1592 end
1593
1594 @spec set_invisible(User.t()) :: {:ok, User.t()}
1595 defp set_invisible(user) do
1596 user
1597 |> change(%{invisible: true})
1598 |> update_and_set_cache()
1599 end
1600
1601 @spec create_service_actor(String.t(), String.t()) ::
1602 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1603 defp create_service_actor(uri, nickname) do
1604 %User{
1605 invisible: true,
1606 local: true,
1607 ap_id: uri,
1608 nickname: nickname,
1609 follower_address: uri <> "/followers"
1610 }
1611 |> change
1612 |> unique_constraint(:nickname)
1613 |> Repo.insert()
1614 |> set_cache()
1615 end
1616
1617 def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
1618 key =
1619 public_key_pem
1620 |> :public_key.pem_decode()
1621 |> hd()
1622 |> :public_key.pem_entry_decode()
1623
1624 {:ok, key}
1625 end
1626
1627 def public_key(_), do: {:error, "key not found"}
1628
1629 def get_public_key_for_ap_id(ap_id) do
1630 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1631 {:ok, public_key} <- public_key(user) do
1632 {:ok, public_key}
1633 else
1634 _ -> :error
1635 end
1636 end
1637
1638 def ap_enabled?(%User{local: true}), do: true
1639 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1640 def ap_enabled?(_), do: false
1641
1642 @doc "Gets or fetch a user by uri or nickname."
1643 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1644 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1645 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1646
1647 # wait a period of time and return newest version of the User structs
1648 # this is because we have synchronous follow APIs and need to simulate them
1649 # with an async handshake
1650 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1651 with %User{} = a <- get_cached_by_id(a.id),
1652 %User{} = b <- get_cached_by_id(b.id) do
1653 {:ok, a, b}
1654 else
1655 nil -> :error
1656 end
1657 end
1658
1659 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1660 with :ok <- :timer.sleep(timeout),
1661 %User{} = a <- get_cached_by_id(a.id),
1662 %User{} = b <- get_cached_by_id(b.id) do
1663 {:ok, a, b}
1664 else
1665 nil -> :error
1666 end
1667 end
1668
1669 def parse_bio(bio) when is_binary(bio) and bio != "" do
1670 bio
1671 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1672 |> elem(0)
1673 end
1674
1675 def parse_bio(_), do: ""
1676
1677 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1678 # TODO: get profile URLs other than user.ap_id
1679 profile_urls = [user.ap_id]
1680
1681 bio
1682 |> CommonUtils.format_input("text/plain",
1683 mentions_format: :full,
1684 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1685 )
1686 |> elem(0)
1687 end
1688
1689 def parse_bio(_, _), do: ""
1690
1691 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1692 Repo.transaction(fn ->
1693 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1694 end)
1695 end
1696
1697 def tag(nickname, tags) when is_binary(nickname),
1698 do: tag(get_by_nickname(nickname), tags)
1699
1700 def tag(%User{} = user, tags),
1701 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1702
1703 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1704 Repo.transaction(fn ->
1705 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1706 end)
1707 end
1708
1709 def untag(nickname, tags) when is_binary(nickname),
1710 do: untag(get_by_nickname(nickname), tags)
1711
1712 def untag(%User{} = user, tags),
1713 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1714
1715 defp update_tags(%User{} = user, new_tags) do
1716 {:ok, updated_user} =
1717 user
1718 |> change(%{tags: new_tags})
1719 |> update_and_set_cache()
1720
1721 updated_user
1722 end
1723
1724 defp normalize_tags(tags) do
1725 [tags]
1726 |> List.flatten()
1727 |> Enum.map(&String.downcase/1)
1728 end
1729
1730 defp local_nickname_regex do
1731 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1732 @extended_local_nickname_regex
1733 else
1734 @strict_local_nickname_regex
1735 end
1736 end
1737
1738 def local_nickname(nickname_or_mention) do
1739 nickname_or_mention
1740 |> full_nickname()
1741 |> String.split("@")
1742 |> hd()
1743 end
1744
1745 def full_nickname(nickname_or_mention),
1746 do: String.trim_leading(nickname_or_mention, "@")
1747
1748 def error_user(ap_id) do
1749 %User{
1750 name: ap_id,
1751 ap_id: ap_id,
1752 nickname: "erroruser@example.com",
1753 inserted_at: NaiveDateTime.utc_now()
1754 }
1755 end
1756
1757 @spec all_superusers() :: [User.t()]
1758 def all_superusers do
1759 User.Query.build(%{super_users: true, local: true, deactivated: false})
1760 |> Repo.all()
1761 end
1762
1763 def muting_reblogs?(%User{} = user, %User{} = target) do
1764 UserRelationship.reblog_mute_exists?(user, target)
1765 end
1766
1767 def showing_reblogs?(%User{} = user, %User{} = target) do
1768 not muting_reblogs?(user, target)
1769 end
1770
1771 @doc """
1772 The function returns a query to get users with no activity for given interval of days.
1773 Inactive users are those who didn't read any notification, or had any activity where
1774 the user is the activity's actor, during `inactivity_threshold` days.
1775 Deactivated users will not appear in this list.
1776
1777 ## Examples
1778
1779 iex> Pleroma.User.list_inactive_users()
1780 %Ecto.Query{}
1781 """
1782 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1783 def list_inactive_users_query(inactivity_threshold \\ 7) do
1784 negative_inactivity_threshold = -inactivity_threshold
1785 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1786 # Subqueries are not supported in `where` clauses, join gets too complicated.
1787 has_read_notifications =
1788 from(n in Pleroma.Notification,
1789 where: n.seen == true,
1790 group_by: n.id,
1791 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1792 select: n.user_id
1793 )
1794 |> Pleroma.Repo.all()
1795
1796 from(u in Pleroma.User,
1797 left_join: a in Pleroma.Activity,
1798 on: u.ap_id == a.actor,
1799 where: not is_nil(u.nickname),
1800 where: u.deactivated != ^true,
1801 where: u.id not in ^has_read_notifications,
1802 group_by: u.id,
1803 having:
1804 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1805 is_nil(max(a.inserted_at))
1806 )
1807 end
1808
1809 @doc """
1810 Enable or disable email notifications for user
1811
1812 ## Examples
1813
1814 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1815 Pleroma.User{email_notifications: %{"digest" => true}}
1816
1817 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1818 Pleroma.User{email_notifications: %{"digest" => false}}
1819 """
1820 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1821 {:ok, t()} | {:error, Ecto.Changeset.t()}
1822 def switch_email_notifications(user, type, status) do
1823 User.update_email_notifications(user, %{type => status})
1824 end
1825
1826 @doc """
1827 Set `last_digest_emailed_at` value for the user to current time
1828 """
1829 @spec touch_last_digest_emailed_at(t()) :: t()
1830 def touch_last_digest_emailed_at(user) do
1831 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1832
1833 {:ok, updated_user} =
1834 user
1835 |> change(%{last_digest_emailed_at: now})
1836 |> update_and_set_cache()
1837
1838 updated_user
1839 end
1840
1841 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1842 def toggle_confirmation(%User{} = user) do
1843 user
1844 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1845 |> update_and_set_cache()
1846 end
1847
1848 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1849 def toggle_confirmation(users) do
1850 Enum.map(users, &toggle_confirmation/1)
1851 end
1852
1853 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1854 mascot
1855 end
1856
1857 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1858 # use instance-default
1859 config = Pleroma.Config.get([:assets, :mascots])
1860 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1861 mascot = Keyword.get(config, default_mascot)
1862
1863 %{
1864 "id" => "default-mascot",
1865 "url" => mascot[:url],
1866 "preview_url" => mascot[:url],
1867 "pleroma" => %{
1868 "mime_type" => mascot[:mime_type]
1869 }
1870 }
1871 end
1872
1873 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1874
1875 def ensure_keys_present(%User{} = user) do
1876 with {:ok, pem} <- Keys.generate_rsa_pem() do
1877 user
1878 |> cast(%{keys: pem}, [:keys])
1879 |> validate_required([:keys])
1880 |> update_and_set_cache()
1881 end
1882 end
1883
1884 def get_ap_ids_by_nicknames(nicknames) do
1885 from(u in User,
1886 where: u.nickname in ^nicknames,
1887 select: u.ap_id
1888 )
1889 |> Repo.all()
1890 end
1891
1892 defdelegate search(query, opts \\ []), to: User.Search
1893
1894 defp put_password_hash(
1895 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1896 ) do
1897 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1898 end
1899
1900 defp put_password_hash(changeset), do: changeset
1901
1902 def is_internal_user?(%User{nickname: nil}), do: true
1903 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1904 def is_internal_user?(_), do: false
1905
1906 # A hack because user delete activities have a fake id for whatever reason
1907 # TODO: Get rid of this
1908 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1909
1910 def get_delivered_users_by_object_id(object_id) do
1911 from(u in User,
1912 inner_join: delivery in assoc(u, :deliveries),
1913 where: delivery.object_id == ^object_id
1914 )
1915 |> Repo.all()
1916 end
1917
1918 def change_email(user, email) do
1919 user
1920 |> cast(%{email: email}, [:email])
1921 |> validate_required([:email])
1922 |> unique_constraint(:email)
1923 |> validate_format(:email, @email_regex)
1924 |> update_and_set_cache()
1925 end
1926
1927 # Internal function; public one is `deactivate/2`
1928 defp set_activation_status(user, deactivated) do
1929 user
1930 |> cast(%{deactivated: deactivated}, [:deactivated])
1931 |> update_and_set_cache()
1932 end
1933
1934 def update_banner(user, banner) do
1935 user
1936 |> cast(%{banner: banner}, [:banner])
1937 |> update_and_set_cache()
1938 end
1939
1940 def update_background(user, background) do
1941 user
1942 |> cast(%{background: background}, [:background])
1943 |> update_and_set_cache()
1944 end
1945
1946 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1947 %{
1948 admin: is_admin,
1949 moderator: is_moderator
1950 }
1951 end
1952
1953 def validate_fields(changeset, remote? \\ false) do
1954 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
1955 limit = Pleroma.Config.get([:instance, limit_name], 0)
1956
1957 changeset
1958 |> validate_length(:fields, max: limit)
1959 |> validate_change(:fields, fn :fields, fields ->
1960 if Enum.all?(fields, &valid_field?/1) do
1961 []
1962 else
1963 [fields: "invalid"]
1964 end
1965 end)
1966 end
1967
1968 defp valid_field?(%{"name" => name, "value" => value}) do
1969 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
1970 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
1971
1972 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
1973 String.length(value) <= value_limit
1974 end
1975
1976 defp valid_field?(_), do: false
1977
1978 defp truncate_field(%{"name" => name, "value" => value}) do
1979 {name, _chopped} =
1980 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
1981
1982 {value, _chopped} =
1983 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
1984
1985 %{"name" => name, "value" => value}
1986 end
1987
1988 def admin_api_update(user, params) do
1989 user
1990 |> cast(params, [
1991 :is_moderator,
1992 :is_admin,
1993 :show_role
1994 ])
1995 |> update_and_set_cache()
1996 end
1997
1998 @doc "Signs user out of all applications"
1999 def global_sign_out(user) do
2000 OAuth.Authorization.delete_user_authorizations(user)
2001 OAuth.Token.delete_user_tokens(user)
2002 end
2003
2004 def mascot_update(user, url) do
2005 user
2006 |> cast(%{mascot: url}, [:mascot])
2007 |> validate_required([:mascot])
2008 |> update_and_set_cache()
2009 end
2010
2011 def mastodon_settings_update(user, settings) do
2012 user
2013 |> cast(%{settings: settings}, [:settings])
2014 |> validate_required([:settings])
2015 |> update_and_set_cache()
2016 end
2017
2018 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2019 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
2020 params =
2021 if need_confirmation? do
2022 %{
2023 confirmation_pending: true,
2024 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2025 }
2026 else
2027 %{
2028 confirmation_pending: false,
2029 confirmation_token: nil
2030 }
2031 end
2032
2033 cast(user, params, [:confirmation_pending, :confirmation_token])
2034 end
2035
2036 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2037 if id not in user.pinned_activities do
2038 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
2039 params = %{pinned_activities: user.pinned_activities ++ [id]}
2040
2041 user
2042 |> cast(params, [:pinned_activities])
2043 |> validate_length(:pinned_activities,
2044 max: max_pinned_statuses,
2045 message: "You have already pinned the maximum number of statuses"
2046 )
2047 else
2048 change(user)
2049 end
2050 |> update_and_set_cache()
2051 end
2052
2053 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2054 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2055
2056 user
2057 |> cast(params, [:pinned_activities])
2058 |> update_and_set_cache()
2059 end
2060
2061 def update_email_notifications(user, settings) do
2062 email_notifications =
2063 user.email_notifications
2064 |> Map.merge(settings)
2065 |> Map.take(["digest"])
2066
2067 params = %{email_notifications: email_notifications}
2068 fields = [:email_notifications]
2069
2070 user
2071 |> cast(params, fields)
2072 |> validate_required(fields)
2073 |> update_and_set_cache()
2074 end
2075
2076 defp set_domain_blocks(user, domain_blocks) do
2077 params = %{domain_blocks: domain_blocks}
2078
2079 user
2080 |> cast(params, [:domain_blocks])
2081 |> validate_required([:domain_blocks])
2082 |> update_and_set_cache()
2083 end
2084
2085 def block_domain(user, domain_blocked) do
2086 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2087 end
2088
2089 def unblock_domain(user, domain_blocked) do
2090 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2091 end
2092
2093 @spec add_to_block(User.t(), User.t()) ::
2094 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2095 defp add_to_block(%User{} = user, %User{} = blocked) do
2096 UserRelationship.create_block(user, blocked)
2097 end
2098
2099 @spec add_to_block(User.t(), User.t()) ::
2100 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2101 defp remove_from_block(%User{} = user, %User{} = blocked) do
2102 UserRelationship.delete_block(user, blocked)
2103 end
2104
2105 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2106 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2107 {:ok, user_notification_mute} <-
2108 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2109 {:ok, nil} do
2110 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2111 end
2112 end
2113
2114 defp remove_from_mutes(user, %User{} = muted_user) do
2115 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2116 {:ok, user_notification_mute} <-
2117 UserRelationship.delete_notification_mute(user, muted_user) do
2118 {:ok, [user_mute, user_notification_mute]}
2119 end
2120 end
2121
2122 def set_invisible(user, invisible) do
2123 params = %{invisible: invisible}
2124
2125 user
2126 |> cast(params, [:invisible])
2127 |> validate_required([:invisible])
2128 |> update_and_set_cache()
2129 end
2130
2131 def sanitize_html(%User{} = user) do
2132 sanitize_html(user, nil)
2133 end
2134
2135 # User data that mastodon isn't filtering (treated as plaintext):
2136 # - field name
2137 # - display name
2138 def sanitize_html(%User{} = user, filter) do
2139 fields =
2140 Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
2141 %{
2142 "name" => name,
2143 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2144 }
2145 end)
2146
2147 user
2148 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2149 |> Map.put(:fields, fields)
2150 end
2151 end