Merge remote-tracking branch 'upstream/develop' into accept-deletes
[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}) do
1184 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1185 |> Repo.all()
1186 end
1187
1188 @spec mute(User.t(), User.t(), boolean()) ::
1189 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1190 def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
1191 add_to_mutes(muter, mutee, notifications?)
1192 end
1193
1194 def unmute(%User{} = muter, %User{} = mutee) do
1195 remove_from_mutes(muter, mutee)
1196 end
1197
1198 def subscribe(%User{} = subscriber, %User{} = target) do
1199 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1200
1201 if blocks?(target, subscriber) and deny_follow_blocked do
1202 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1203 else
1204 # Note: the relationship is inverse: subscriber acts as relationship target
1205 UserRelationship.create_inverse_subscription(target, subscriber)
1206 end
1207 end
1208
1209 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1210 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1211 subscribe(subscriber, subscribee)
1212 end
1213 end
1214
1215 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1216 # Note: the relationship is inverse: subscriber acts as relationship target
1217 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1218 end
1219
1220 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1221 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1222 unsubscribe(unsubscriber, user)
1223 end
1224 end
1225
1226 def block(%User{} = blocker, %User{} = blocked) do
1227 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1228 blocker =
1229 if following?(blocker, blocked) do
1230 {:ok, blocker, _} = unfollow(blocker, blocked)
1231 blocker
1232 else
1233 blocker
1234 end
1235
1236 # clear any requested follows as well
1237 blocked =
1238 case CommonAPI.reject_follow_request(blocked, blocker) do
1239 {:ok, %User{} = updated_blocked} -> updated_blocked
1240 nil -> blocked
1241 end
1242
1243 unsubscribe(blocked, blocker)
1244
1245 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1246
1247 {:ok, blocker} = update_follower_count(blocker)
1248 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1249 add_to_block(blocker, blocked)
1250 end
1251
1252 # helper to handle the block given only an actor's AP id
1253 def block(%User{} = blocker, %{ap_id: ap_id}) do
1254 block(blocker, get_cached_by_ap_id(ap_id))
1255 end
1256
1257 def unblock(%User{} = blocker, %User{} = blocked) do
1258 remove_from_block(blocker, blocked)
1259 end
1260
1261 # helper to handle the block given only an actor's AP id
1262 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1263 unblock(blocker, get_cached_by_ap_id(ap_id))
1264 end
1265
1266 def mutes?(nil, _), do: false
1267 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1268
1269 def mutes_user?(%User{} = user, %User{} = target) do
1270 UserRelationship.mute_exists?(user, target)
1271 end
1272
1273 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1274 def muted_notifications?(nil, _), do: false
1275
1276 def muted_notifications?(%User{} = user, %User{} = target),
1277 do: UserRelationship.notification_mute_exists?(user, target)
1278
1279 def blocks?(nil, _), do: false
1280
1281 def blocks?(%User{} = user, %User{} = target) do
1282 blocks_user?(user, target) ||
1283 (blocks_domain?(user, target) and not User.following?(user, target))
1284 end
1285
1286 def blocks_user?(%User{} = user, %User{} = target) do
1287 UserRelationship.block_exists?(user, target)
1288 end
1289
1290 def blocks_user?(_, _), do: false
1291
1292 def blocks_domain?(%User{} = user, %User{} = target) do
1293 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1294 %{host: host} = URI.parse(target.ap_id)
1295 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1296 end
1297
1298 def blocks_domain?(_, _), do: false
1299
1300 def subscribed_to?(%User{} = user, %User{} = target) do
1301 # Note: the relationship is inverse: subscriber acts as relationship target
1302 UserRelationship.inverse_subscription_exists?(target, user)
1303 end
1304
1305 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1306 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1307 subscribed_to?(user, target)
1308 end
1309 end
1310
1311 @doc """
1312 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1313 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1314 """
1315 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1316 def outgoing_relationships_ap_ids(_user, []), do: %{}
1317
1318 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1319
1320 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1321 when is_list(relationship_types) do
1322 db_result =
1323 user
1324 |> assoc(:outgoing_relationships)
1325 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1326 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1327 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1328 |> group_by([user_rel, u], user_rel.relationship_type)
1329 |> Repo.all()
1330 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1331
1332 Enum.into(
1333 relationship_types,
1334 %{},
1335 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1336 )
1337 end
1338
1339 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1340
1341 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1342
1343 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1344
1345 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1346 when is_list(relationship_types) do
1347 user
1348 |> assoc(:incoming_relationships)
1349 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1350 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1351 |> maybe_filter_on_ap_id(ap_ids)
1352 |> select([user_rel, u], u.ap_id)
1353 |> distinct(true)
1354 |> Repo.all()
1355 end
1356
1357 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1358 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1359 end
1360
1361 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1362
1363 def deactivate_async(user, status \\ true) do
1364 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1365 end
1366
1367 def deactivate(user, status \\ true)
1368
1369 def deactivate(users, status) when is_list(users) do
1370 Repo.transaction(fn ->
1371 for user <- users, do: deactivate(user, status)
1372 end)
1373 end
1374
1375 def deactivate(%User{} = user, status) do
1376 with {:ok, user} <- set_activation_status(user, status) do
1377 user
1378 |> get_followers()
1379 |> Enum.filter(& &1.local)
1380 |> Enum.each(fn follower ->
1381 follower |> update_following_count() |> set_cache()
1382 end)
1383
1384 # Only update local user counts, remote will be update during the next pull.
1385 user
1386 |> get_friends()
1387 |> Enum.filter(& &1.local)
1388 |> Enum.each(&update_follower_count/1)
1389
1390 {:ok, user}
1391 end
1392 end
1393
1394 def update_notification_settings(%User{} = user, settings) do
1395 user
1396 |> cast(%{notification_settings: settings}, [])
1397 |> cast_embed(:notification_settings)
1398 |> validate_required([:notification_settings])
1399 |> update_and_set_cache()
1400 end
1401
1402 def delete(users) when is_list(users) do
1403 for user <- users, do: delete(user)
1404 end
1405
1406 def delete(%User{} = user) do
1407 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1408 end
1409
1410 def perform(:force_password_reset, user), do: force_password_reset(user)
1411
1412 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1413 def perform(:delete, %User{} = user) do
1414 {:ok, _user} = ActivityPub.delete(user)
1415
1416 # Remove all relationships
1417 user
1418 |> get_followers()
1419 |> Enum.each(fn follower ->
1420 ActivityPub.unfollow(follower, user)
1421 unfollow(follower, user)
1422 end)
1423
1424 user
1425 |> get_friends()
1426 |> Enum.each(fn followed ->
1427 ActivityPub.unfollow(user, followed)
1428 unfollow(user, followed)
1429 end)
1430
1431 delete_user_activities(user)
1432 invalidate_cache(user)
1433 Repo.delete(user)
1434 end
1435
1436 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1437
1438 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1439 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1440 when is_list(blocked_identifiers) do
1441 Enum.map(
1442 blocked_identifiers,
1443 fn blocked_identifier ->
1444 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1445 {:ok, _user_block} <- block(blocker, blocked),
1446 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1447 blocked
1448 else
1449 err ->
1450 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1451 err
1452 end
1453 end
1454 )
1455 end
1456
1457 def perform(:follow_import, %User{} = follower, followed_identifiers)
1458 when is_list(followed_identifiers) do
1459 Enum.map(
1460 followed_identifiers,
1461 fn followed_identifier ->
1462 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1463 {:ok, follower} <- maybe_direct_follow(follower, followed),
1464 {:ok, _} <- ActivityPub.follow(follower, followed) do
1465 followed
1466 else
1467 err ->
1468 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1469 err
1470 end
1471 end
1472 )
1473 end
1474
1475 @spec external_users_query() :: Ecto.Query.t()
1476 def external_users_query do
1477 User.Query.build(%{
1478 external: true,
1479 active: true,
1480 order_by: :id
1481 })
1482 end
1483
1484 @spec external_users(keyword()) :: [User.t()]
1485 def external_users(opts \\ []) do
1486 query =
1487 external_users_query()
1488 |> select([u], struct(u, [:id, :ap_id]))
1489
1490 query =
1491 if opts[:max_id],
1492 do: where(query, [u], u.id > ^opts[:max_id]),
1493 else: query
1494
1495 query =
1496 if opts[:limit],
1497 do: limit(query, ^opts[:limit]),
1498 else: query
1499
1500 Repo.all(query)
1501 end
1502
1503 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1504 BackgroundWorker.enqueue("blocks_import", %{
1505 "blocker_id" => blocker.id,
1506 "blocked_identifiers" => blocked_identifiers
1507 })
1508 end
1509
1510 def follow_import(%User{} = follower, followed_identifiers)
1511 when is_list(followed_identifiers) do
1512 BackgroundWorker.enqueue("follow_import", %{
1513 "follower_id" => follower.id,
1514 "followed_identifiers" => followed_identifiers
1515 })
1516 end
1517
1518 def delete_user_activities(%User{ap_id: ap_id}) do
1519 ap_id
1520 |> Activity.Queries.by_actor()
1521 |> RepoStreamer.chunk_stream(50)
1522 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1523 |> Stream.run()
1524 end
1525
1526 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1527 activity
1528 |> Object.normalize()
1529 |> ActivityPub.delete()
1530 end
1531
1532 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1533 object = Object.normalize(activity)
1534
1535 activity.actor
1536 |> get_cached_by_ap_id()
1537 |> ActivityPub.unlike(object)
1538 end
1539
1540 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1541 object = Object.normalize(activity)
1542
1543 activity.actor
1544 |> get_cached_by_ap_id()
1545 |> ActivityPub.unannounce(object)
1546 end
1547
1548 defp delete_activity(_activity), do: "Doing nothing"
1549
1550 def html_filter_policy(%User{no_rich_text: true}) do
1551 Pleroma.HTML.Scrubber.TwitterText
1552 end
1553
1554 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1555
1556 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1557
1558 def get_or_fetch_by_ap_id(ap_id) do
1559 user = get_cached_by_ap_id(ap_id)
1560
1561 if !is_nil(user) and !needs_update?(user) do
1562 {:ok, user}
1563 else
1564 fetch_by_ap_id(ap_id)
1565 end
1566 end
1567
1568 @doc """
1569 Creates an internal service actor by URI if missing.
1570 Optionally takes nickname for addressing.
1571 """
1572 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1573 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1574 {_, user} =
1575 case get_cached_by_ap_id(uri) do
1576 nil ->
1577 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1578 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1579 {:error, nil}
1580 end
1581
1582 %User{invisible: false} = user ->
1583 set_invisible(user)
1584
1585 user ->
1586 {:ok, user}
1587 end
1588
1589 user
1590 end
1591
1592 @spec set_invisible(User.t()) :: {:ok, User.t()}
1593 defp set_invisible(user) do
1594 user
1595 |> change(%{invisible: true})
1596 |> update_and_set_cache()
1597 end
1598
1599 @spec create_service_actor(String.t(), String.t()) ::
1600 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1601 defp create_service_actor(uri, nickname) do
1602 %User{
1603 invisible: true,
1604 local: true,
1605 ap_id: uri,
1606 nickname: nickname,
1607 follower_address: uri <> "/followers"
1608 }
1609 |> change
1610 |> unique_constraint(:nickname)
1611 |> Repo.insert()
1612 |> set_cache()
1613 end
1614
1615 def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
1616 key =
1617 public_key_pem
1618 |> :public_key.pem_decode()
1619 |> hd()
1620 |> :public_key.pem_entry_decode()
1621
1622 {:ok, key}
1623 end
1624
1625 def public_key(_), do: {:error, "key not found"}
1626
1627 def get_public_key_for_ap_id(ap_id) do
1628 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1629 {:ok, public_key} <- public_key(user) do
1630 {:ok, public_key}
1631 else
1632 _ -> :error
1633 end
1634 end
1635
1636 def ap_enabled?(%User{local: true}), do: true
1637 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1638 def ap_enabled?(_), do: false
1639
1640 @doc "Gets or fetch a user by uri or nickname."
1641 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1642 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1643 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1644
1645 # wait a period of time and return newest version of the User structs
1646 # this is because we have synchronous follow APIs and need to simulate them
1647 # with an async handshake
1648 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1649 with %User{} = a <- get_cached_by_id(a.id),
1650 %User{} = b <- get_cached_by_id(b.id) do
1651 {:ok, a, b}
1652 else
1653 nil -> :error
1654 end
1655 end
1656
1657 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1658 with :ok <- :timer.sleep(timeout),
1659 %User{} = a <- get_cached_by_id(a.id),
1660 %User{} = b <- get_cached_by_id(b.id) do
1661 {:ok, a, b}
1662 else
1663 nil -> :error
1664 end
1665 end
1666
1667 def parse_bio(bio) when is_binary(bio) and bio != "" do
1668 bio
1669 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1670 |> elem(0)
1671 end
1672
1673 def parse_bio(_), do: ""
1674
1675 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1676 # TODO: get profile URLs other than user.ap_id
1677 profile_urls = [user.ap_id]
1678
1679 bio
1680 |> CommonUtils.format_input("text/plain",
1681 mentions_format: :full,
1682 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1683 )
1684 |> elem(0)
1685 end
1686
1687 def parse_bio(_, _), do: ""
1688
1689 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1690 Repo.transaction(fn ->
1691 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1692 end)
1693 end
1694
1695 def tag(nickname, tags) when is_binary(nickname),
1696 do: tag(get_by_nickname(nickname), tags)
1697
1698 def tag(%User{} = user, tags),
1699 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1700
1701 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1702 Repo.transaction(fn ->
1703 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1704 end)
1705 end
1706
1707 def untag(nickname, tags) when is_binary(nickname),
1708 do: untag(get_by_nickname(nickname), tags)
1709
1710 def untag(%User{} = user, tags),
1711 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1712
1713 defp update_tags(%User{} = user, new_tags) do
1714 {:ok, updated_user} =
1715 user
1716 |> change(%{tags: new_tags})
1717 |> update_and_set_cache()
1718
1719 updated_user
1720 end
1721
1722 defp normalize_tags(tags) do
1723 [tags]
1724 |> List.flatten()
1725 |> Enum.map(&String.downcase/1)
1726 end
1727
1728 defp local_nickname_regex do
1729 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1730 @extended_local_nickname_regex
1731 else
1732 @strict_local_nickname_regex
1733 end
1734 end
1735
1736 def local_nickname(nickname_or_mention) do
1737 nickname_or_mention
1738 |> full_nickname()
1739 |> String.split("@")
1740 |> hd()
1741 end
1742
1743 def full_nickname(nickname_or_mention),
1744 do: String.trim_leading(nickname_or_mention, "@")
1745
1746 def error_user(ap_id) do
1747 %User{
1748 name: ap_id,
1749 ap_id: ap_id,
1750 nickname: "erroruser@example.com",
1751 inserted_at: NaiveDateTime.utc_now()
1752 }
1753 end
1754
1755 @spec all_superusers() :: [User.t()]
1756 def all_superusers do
1757 User.Query.build(%{super_users: true, local: true, deactivated: false})
1758 |> Repo.all()
1759 end
1760
1761 def muting_reblogs?(%User{} = user, %User{} = target) do
1762 UserRelationship.reblog_mute_exists?(user, target)
1763 end
1764
1765 def showing_reblogs?(%User{} = user, %User{} = target) do
1766 not muting_reblogs?(user, target)
1767 end
1768
1769 @doc """
1770 The function returns a query to get users with no activity for given interval of days.
1771 Inactive users are those who didn't read any notification, or had any activity where
1772 the user is the activity's actor, during `inactivity_threshold` days.
1773 Deactivated users will not appear in this list.
1774
1775 ## Examples
1776
1777 iex> Pleroma.User.list_inactive_users()
1778 %Ecto.Query{}
1779 """
1780 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1781 def list_inactive_users_query(inactivity_threshold \\ 7) do
1782 negative_inactivity_threshold = -inactivity_threshold
1783 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1784 # Subqueries are not supported in `where` clauses, join gets too complicated.
1785 has_read_notifications =
1786 from(n in Pleroma.Notification,
1787 where: n.seen == true,
1788 group_by: n.id,
1789 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1790 select: n.user_id
1791 )
1792 |> Pleroma.Repo.all()
1793
1794 from(u in Pleroma.User,
1795 left_join: a in Pleroma.Activity,
1796 on: u.ap_id == a.actor,
1797 where: not is_nil(u.nickname),
1798 where: u.deactivated != ^true,
1799 where: u.id not in ^has_read_notifications,
1800 group_by: u.id,
1801 having:
1802 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1803 is_nil(max(a.inserted_at))
1804 )
1805 end
1806
1807 @doc """
1808 Enable or disable email notifications for user
1809
1810 ## Examples
1811
1812 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1813 Pleroma.User{email_notifications: %{"digest" => true}}
1814
1815 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1816 Pleroma.User{email_notifications: %{"digest" => false}}
1817 """
1818 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1819 {:ok, t()} | {:error, Ecto.Changeset.t()}
1820 def switch_email_notifications(user, type, status) do
1821 User.update_email_notifications(user, %{type => status})
1822 end
1823
1824 @doc """
1825 Set `last_digest_emailed_at` value for the user to current time
1826 """
1827 @spec touch_last_digest_emailed_at(t()) :: t()
1828 def touch_last_digest_emailed_at(user) do
1829 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1830
1831 {:ok, updated_user} =
1832 user
1833 |> change(%{last_digest_emailed_at: now})
1834 |> update_and_set_cache()
1835
1836 updated_user
1837 end
1838
1839 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1840 def toggle_confirmation(%User{} = user) do
1841 user
1842 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1843 |> update_and_set_cache()
1844 end
1845
1846 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1847 def toggle_confirmation(users) do
1848 Enum.map(users, &toggle_confirmation/1)
1849 end
1850
1851 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1852 mascot
1853 end
1854
1855 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1856 # use instance-default
1857 config = Pleroma.Config.get([:assets, :mascots])
1858 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1859 mascot = Keyword.get(config, default_mascot)
1860
1861 %{
1862 "id" => "default-mascot",
1863 "url" => mascot[:url],
1864 "preview_url" => mascot[:url],
1865 "pleroma" => %{
1866 "mime_type" => mascot[:mime_type]
1867 }
1868 }
1869 end
1870
1871 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1872
1873 def ensure_keys_present(%User{} = user) do
1874 with {:ok, pem} <- Keys.generate_rsa_pem() do
1875 user
1876 |> cast(%{keys: pem}, [:keys])
1877 |> validate_required([:keys])
1878 |> update_and_set_cache()
1879 end
1880 end
1881
1882 def get_ap_ids_by_nicknames(nicknames) do
1883 from(u in User,
1884 where: u.nickname in ^nicknames,
1885 select: u.ap_id
1886 )
1887 |> Repo.all()
1888 end
1889
1890 defdelegate search(query, opts \\ []), to: User.Search
1891
1892 defp put_password_hash(
1893 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1894 ) do
1895 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1896 end
1897
1898 defp put_password_hash(changeset), do: changeset
1899
1900 def is_internal_user?(%User{nickname: nil}), do: true
1901 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1902 def is_internal_user?(_), do: false
1903
1904 # A hack because user delete activities have a fake id for whatever reason
1905 # TODO: Get rid of this
1906 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1907
1908 def get_delivered_users_by_object_id(object_id) do
1909 from(u in User,
1910 inner_join: delivery in assoc(u, :deliveries),
1911 where: delivery.object_id == ^object_id
1912 )
1913 |> Repo.all()
1914 end
1915
1916 def change_email(user, email) do
1917 user
1918 |> cast(%{email: email}, [:email])
1919 |> validate_required([:email])
1920 |> unique_constraint(:email)
1921 |> validate_format(:email, @email_regex)
1922 |> update_and_set_cache()
1923 end
1924
1925 # Internal function; public one is `deactivate/2`
1926 defp set_activation_status(user, deactivated) do
1927 user
1928 |> cast(%{deactivated: deactivated}, [:deactivated])
1929 |> update_and_set_cache()
1930 end
1931
1932 def update_banner(user, banner) do
1933 user
1934 |> cast(%{banner: banner}, [:banner])
1935 |> update_and_set_cache()
1936 end
1937
1938 def update_background(user, background) do
1939 user
1940 |> cast(%{background: background}, [:background])
1941 |> update_and_set_cache()
1942 end
1943
1944 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1945 %{
1946 admin: is_admin,
1947 moderator: is_moderator
1948 }
1949 end
1950
1951 def validate_fields(changeset, remote? \\ false) do
1952 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
1953 limit = Pleroma.Config.get([:instance, limit_name], 0)
1954
1955 changeset
1956 |> validate_length(:fields, max: limit)
1957 |> validate_change(:fields, fn :fields, fields ->
1958 if Enum.all?(fields, &valid_field?/1) do
1959 []
1960 else
1961 [fields: "invalid"]
1962 end
1963 end)
1964 end
1965
1966 defp valid_field?(%{"name" => name, "value" => value}) do
1967 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
1968 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
1969
1970 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
1971 String.length(value) <= value_limit
1972 end
1973
1974 defp valid_field?(_), do: false
1975
1976 defp truncate_field(%{"name" => name, "value" => value}) do
1977 {name, _chopped} =
1978 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
1979
1980 {value, _chopped} =
1981 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
1982
1983 %{"name" => name, "value" => value}
1984 end
1985
1986 def admin_api_update(user, params) do
1987 user
1988 |> cast(params, [
1989 :is_moderator,
1990 :is_admin,
1991 :show_role
1992 ])
1993 |> update_and_set_cache()
1994 end
1995
1996 @doc "Signs user out of all applications"
1997 def global_sign_out(user) do
1998 OAuth.Authorization.delete_user_authorizations(user)
1999 OAuth.Token.delete_user_tokens(user)
2000 end
2001
2002 def mascot_update(user, url) do
2003 user
2004 |> cast(%{mascot: url}, [:mascot])
2005 |> validate_required([:mascot])
2006 |> update_and_set_cache()
2007 end
2008
2009 def mastodon_settings_update(user, settings) do
2010 user
2011 |> cast(%{settings: settings}, [:settings])
2012 |> validate_required([:settings])
2013 |> update_and_set_cache()
2014 end
2015
2016 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2017 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
2018 params =
2019 if need_confirmation? do
2020 %{
2021 confirmation_pending: true,
2022 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2023 }
2024 else
2025 %{
2026 confirmation_pending: false,
2027 confirmation_token: nil
2028 }
2029 end
2030
2031 cast(user, params, [:confirmation_pending, :confirmation_token])
2032 end
2033
2034 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2035 if id not in user.pinned_activities do
2036 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
2037 params = %{pinned_activities: user.pinned_activities ++ [id]}
2038
2039 user
2040 |> cast(params, [:pinned_activities])
2041 |> validate_length(:pinned_activities,
2042 max: max_pinned_statuses,
2043 message: "You have already pinned the maximum number of statuses"
2044 )
2045 else
2046 change(user)
2047 end
2048 |> update_and_set_cache()
2049 end
2050
2051 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2052 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2053
2054 user
2055 |> cast(params, [:pinned_activities])
2056 |> update_and_set_cache()
2057 end
2058
2059 def update_email_notifications(user, settings) do
2060 email_notifications =
2061 user.email_notifications
2062 |> Map.merge(settings)
2063 |> Map.take(["digest"])
2064
2065 params = %{email_notifications: email_notifications}
2066 fields = [:email_notifications]
2067
2068 user
2069 |> cast(params, fields)
2070 |> validate_required(fields)
2071 |> update_and_set_cache()
2072 end
2073
2074 defp set_domain_blocks(user, domain_blocks) do
2075 params = %{domain_blocks: domain_blocks}
2076
2077 user
2078 |> cast(params, [:domain_blocks])
2079 |> validate_required([:domain_blocks])
2080 |> update_and_set_cache()
2081 end
2082
2083 def block_domain(user, domain_blocked) do
2084 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2085 end
2086
2087 def unblock_domain(user, domain_blocked) do
2088 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2089 end
2090
2091 @spec add_to_block(User.t(), User.t()) ::
2092 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2093 defp add_to_block(%User{} = user, %User{} = blocked) do
2094 UserRelationship.create_block(user, blocked)
2095 end
2096
2097 @spec add_to_block(User.t(), User.t()) ::
2098 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2099 defp remove_from_block(%User{} = user, %User{} = blocked) do
2100 UserRelationship.delete_block(user, blocked)
2101 end
2102
2103 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2104 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2105 {:ok, user_notification_mute} <-
2106 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2107 {:ok, nil} do
2108 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2109 end
2110 end
2111
2112 defp remove_from_mutes(user, %User{} = muted_user) do
2113 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2114 {:ok, user_notification_mute} <-
2115 UserRelationship.delete_notification_mute(user, muted_user) do
2116 {:ok, [user_mute, user_notification_mute]}
2117 end
2118 end
2119
2120 def set_invisible(user, invisible) do
2121 params = %{invisible: invisible}
2122
2123 user
2124 |> cast(params, [:invisible])
2125 |> validate_required([:invisible])
2126 |> update_and_set_cache()
2127 end
2128
2129 def sanitize_html(%User{} = user) do
2130 sanitize_html(user, nil)
2131 end
2132
2133 # User data that mastodon isn't filtering (treated as plaintext):
2134 # - field name
2135 # - display name
2136 def sanitize_html(%User{} = user, filter) do
2137 fields =
2138 Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
2139 %{
2140 "name" => name,
2141 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2142 }
2143 end)
2144
2145 user
2146 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2147 |> Map.put(:fields, fields)
2148 end
2149 end