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