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