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