Merge branch 'fix/follow-and-blocks-import' 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
1420 if user.local do
1421 user
1422 |> change(%{deactivated: true, email: nil})
1423 |> update_and_set_cache()
1424 else
1425 invalidate_cache(user)
1426 Repo.delete(user)
1427 end
1428 end
1429
1430 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1431
1432 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1433 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1434 when is_list(blocked_identifiers) do
1435 Enum.map(
1436 blocked_identifiers,
1437 fn blocked_identifier ->
1438 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1439 {:ok, _user_block} <- block(blocker, blocked),
1440 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1441 blocked
1442 else
1443 err ->
1444 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1445 err
1446 end
1447 end
1448 )
1449 end
1450
1451 def perform(:follow_import, %User{} = follower, followed_identifiers)
1452 when is_list(followed_identifiers) do
1453 Enum.map(
1454 followed_identifiers,
1455 fn followed_identifier ->
1456 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1457 {:ok, follower} <- maybe_direct_follow(follower, followed),
1458 {:ok, _} <- ActivityPub.follow(follower, followed) do
1459 followed
1460 else
1461 err ->
1462 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1463 err
1464 end
1465 end
1466 )
1467 end
1468
1469 @spec external_users_query() :: Ecto.Query.t()
1470 def external_users_query do
1471 User.Query.build(%{
1472 external: true,
1473 active: true,
1474 order_by: :id
1475 })
1476 end
1477
1478 @spec external_users(keyword()) :: [User.t()]
1479 def external_users(opts \\ []) do
1480 query =
1481 external_users_query()
1482 |> select([u], struct(u, [:id, :ap_id]))
1483
1484 query =
1485 if opts[:max_id],
1486 do: where(query, [u], u.id > ^opts[:max_id]),
1487 else: query
1488
1489 query =
1490 if opts[:limit],
1491 do: limit(query, ^opts[:limit]),
1492 else: query
1493
1494 Repo.all(query)
1495 end
1496
1497 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1498 BackgroundWorker.enqueue("blocks_import", %{
1499 "blocker_id" => blocker.id,
1500 "blocked_identifiers" => blocked_identifiers
1501 })
1502 end
1503
1504 def follow_import(%User{} = follower, followed_identifiers)
1505 when is_list(followed_identifiers) do
1506 BackgroundWorker.enqueue("follow_import", %{
1507 "follower_id" => follower.id,
1508 "followed_identifiers" => followed_identifiers
1509 })
1510 end
1511
1512 def delete_user_activities(%User{ap_id: ap_id}) do
1513 ap_id
1514 |> Activity.Queries.by_actor()
1515 |> RepoStreamer.chunk_stream(50)
1516 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1517 |> Stream.run()
1518 end
1519
1520 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1521 activity
1522 |> Object.normalize()
1523 |> ActivityPub.delete()
1524 end
1525
1526 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1527 object = Object.normalize(activity)
1528
1529 activity.actor
1530 |> get_cached_by_ap_id()
1531 |> ActivityPub.unlike(object)
1532 end
1533
1534 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1535 object = Object.normalize(activity)
1536
1537 activity.actor
1538 |> get_cached_by_ap_id()
1539 |> ActivityPub.unannounce(object)
1540 end
1541
1542 defp delete_activity(_activity), do: "Doing nothing"
1543
1544 def html_filter_policy(%User{no_rich_text: true}) do
1545 Pleroma.HTML.Scrubber.TwitterText
1546 end
1547
1548 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1549
1550 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1551
1552 def get_or_fetch_by_ap_id(ap_id) do
1553 user = get_cached_by_ap_id(ap_id)
1554
1555 if !is_nil(user) and !needs_update?(user) do
1556 {:ok, user}
1557 else
1558 fetch_by_ap_id(ap_id)
1559 end
1560 end
1561
1562 @doc """
1563 Creates an internal service actor by URI if missing.
1564 Optionally takes nickname for addressing.
1565 """
1566 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1567 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1568 {_, user} =
1569 case get_cached_by_ap_id(uri) do
1570 nil ->
1571 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1572 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1573 {:error, nil}
1574 end
1575
1576 %User{invisible: false} = user ->
1577 set_invisible(user)
1578
1579 user ->
1580 {:ok, user}
1581 end
1582
1583 user
1584 end
1585
1586 @spec set_invisible(User.t()) :: {:ok, User.t()}
1587 defp set_invisible(user) do
1588 user
1589 |> change(%{invisible: true})
1590 |> update_and_set_cache()
1591 end
1592
1593 @spec create_service_actor(String.t(), String.t()) ::
1594 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1595 defp create_service_actor(uri, nickname) do
1596 %User{
1597 invisible: true,
1598 local: true,
1599 ap_id: uri,
1600 nickname: nickname,
1601 follower_address: uri <> "/followers"
1602 }
1603 |> change
1604 |> unique_constraint(:nickname)
1605 |> Repo.insert()
1606 |> set_cache()
1607 end
1608
1609 # AP style
1610 def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
1611 key =
1612 public_key_pem
1613 |> :public_key.pem_decode()
1614 |> hd()
1615 |> :public_key.pem_entry_decode()
1616
1617 {:ok, key}
1618 end
1619
1620 def public_key(_), do: {:error, "not found key"}
1621
1622 def get_public_key_for_ap_id(ap_id) do
1623 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1624 {:ok, public_key} <- public_key(user) do
1625 {:ok, public_key}
1626 else
1627 _ -> :error
1628 end
1629 end
1630
1631 defp blank?(""), do: nil
1632 defp blank?(n), do: n
1633
1634 def insert_or_update_user(data) do
1635 data
1636 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1637 |> remote_user_creation()
1638 |> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
1639 |> set_cache()
1640 end
1641
1642 def ap_enabled?(%User{local: true}), do: true
1643 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1644 def ap_enabled?(_), do: false
1645
1646 @doc "Gets or fetch a user by uri or nickname."
1647 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1648 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1649 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1650
1651 # wait a period of time and return newest version of the User structs
1652 # this is because we have synchronous follow APIs and need to simulate them
1653 # with an async handshake
1654 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1655 with %User{} = a <- get_cached_by_id(a.id),
1656 %User{} = b <- get_cached_by_id(b.id) do
1657 {:ok, a, b}
1658 else
1659 nil -> :error
1660 end
1661 end
1662
1663 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1664 with :ok <- :timer.sleep(timeout),
1665 %User{} = a <- get_cached_by_id(a.id),
1666 %User{} = b <- get_cached_by_id(b.id) do
1667 {:ok, a, b}
1668 else
1669 nil -> :error
1670 end
1671 end
1672
1673 def parse_bio(bio) when is_binary(bio) and bio != "" do
1674 bio
1675 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1676 |> elem(0)
1677 end
1678
1679 def parse_bio(_), do: ""
1680
1681 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1682 # TODO: get profile URLs other than user.ap_id
1683 profile_urls = [user.ap_id]
1684
1685 bio
1686 |> CommonUtils.format_input("text/plain",
1687 mentions_format: :full,
1688 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1689 )
1690 |> elem(0)
1691 end
1692
1693 def parse_bio(_, _), do: ""
1694
1695 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1696 Repo.transaction(fn ->
1697 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1698 end)
1699 end
1700
1701 def tag(nickname, tags) when is_binary(nickname),
1702 do: tag(get_by_nickname(nickname), tags)
1703
1704 def tag(%User{} = user, tags),
1705 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1706
1707 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1708 Repo.transaction(fn ->
1709 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1710 end)
1711 end
1712
1713 def untag(nickname, tags) when is_binary(nickname),
1714 do: untag(get_by_nickname(nickname), tags)
1715
1716 def untag(%User{} = user, tags),
1717 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1718
1719 defp update_tags(%User{} = user, new_tags) do
1720 {:ok, updated_user} =
1721 user
1722 |> change(%{tags: new_tags})
1723 |> update_and_set_cache()
1724
1725 updated_user
1726 end
1727
1728 defp normalize_tags(tags) do
1729 [tags]
1730 |> List.flatten()
1731 |> Enum.map(&String.downcase/1)
1732 end
1733
1734 defp local_nickname_regex do
1735 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1736 @extended_local_nickname_regex
1737 else
1738 @strict_local_nickname_regex
1739 end
1740 end
1741
1742 def local_nickname(nickname_or_mention) do
1743 nickname_or_mention
1744 |> full_nickname()
1745 |> String.split("@")
1746 |> hd()
1747 end
1748
1749 def full_nickname(nickname_or_mention),
1750 do: String.trim_leading(nickname_or_mention, "@")
1751
1752 def error_user(ap_id) do
1753 %User{
1754 name: ap_id,
1755 ap_id: ap_id,
1756 nickname: "erroruser@example.com",
1757 inserted_at: NaiveDateTime.utc_now()
1758 }
1759 end
1760
1761 @spec all_superusers() :: [User.t()]
1762 def all_superusers do
1763 User.Query.build(%{super_users: true, local: true, deactivated: false})
1764 |> Repo.all()
1765 end
1766
1767 def showing_reblogs?(%User{} = user, %User{} = target) do
1768 not UserRelationship.reblog_mute_exists?(user, target)
1769 end
1770
1771 @doc """
1772 The function returns a query to get users with no activity for given interval of days.
1773 Inactive users are those who didn't read any notification, or had any activity where
1774 the user is the activity's actor, during `inactivity_threshold` days.
1775 Deactivated users will not appear in this list.
1776
1777 ## Examples
1778
1779 iex> Pleroma.User.list_inactive_users()
1780 %Ecto.Query{}
1781 """
1782 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1783 def list_inactive_users_query(inactivity_threshold \\ 7) do
1784 negative_inactivity_threshold = -inactivity_threshold
1785 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1786 # Subqueries are not supported in `where` clauses, join gets too complicated.
1787 has_read_notifications =
1788 from(n in Pleroma.Notification,
1789 where: n.seen == true,
1790 group_by: n.id,
1791 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1792 select: n.user_id
1793 )
1794 |> Pleroma.Repo.all()
1795
1796 from(u in Pleroma.User,
1797 left_join: a in Pleroma.Activity,
1798 on: u.ap_id == a.actor,
1799 where: not is_nil(u.nickname),
1800 where: u.deactivated != ^true,
1801 where: u.id not in ^has_read_notifications,
1802 group_by: u.id,
1803 having:
1804 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1805 is_nil(max(a.inserted_at))
1806 )
1807 end
1808
1809 @doc """
1810 Enable or disable email notifications for user
1811
1812 ## Examples
1813
1814 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1815 Pleroma.User{email_notifications: %{"digest" => true}}
1816
1817 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1818 Pleroma.User{email_notifications: %{"digest" => false}}
1819 """
1820 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1821 {:ok, t()} | {:error, Ecto.Changeset.t()}
1822 def switch_email_notifications(user, type, status) do
1823 User.update_email_notifications(user, %{type => status})
1824 end
1825
1826 @doc """
1827 Set `last_digest_emailed_at` value for the user to current time
1828 """
1829 @spec touch_last_digest_emailed_at(t()) :: t()
1830 def touch_last_digest_emailed_at(user) do
1831 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1832
1833 {:ok, updated_user} =
1834 user
1835 |> change(%{last_digest_emailed_at: now})
1836 |> update_and_set_cache()
1837
1838 updated_user
1839 end
1840
1841 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1842 def toggle_confirmation(%User{} = user) do
1843 user
1844 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1845 |> update_and_set_cache()
1846 end
1847
1848 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1849 def toggle_confirmation(users) do
1850 Enum.map(users, &toggle_confirmation/1)
1851 end
1852
1853 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1854 mascot
1855 end
1856
1857 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1858 # use instance-default
1859 config = Pleroma.Config.get([:assets, :mascots])
1860 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1861 mascot = Keyword.get(config, default_mascot)
1862
1863 %{
1864 "id" => "default-mascot",
1865 "url" => mascot[:url],
1866 "preview_url" => mascot[:url],
1867 "pleroma" => %{
1868 "mime_type" => mascot[:mime_type]
1869 }
1870 }
1871 end
1872
1873 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1874
1875 def ensure_keys_present(%User{} = user) do
1876 with {:ok, pem} <- Keys.generate_rsa_pem() do
1877 user
1878 |> cast(%{keys: pem}, [:keys])
1879 |> validate_required([:keys])
1880 |> update_and_set_cache()
1881 end
1882 end
1883
1884 def get_ap_ids_by_nicknames(nicknames) do
1885 from(u in User,
1886 where: u.nickname in ^nicknames,
1887 select: u.ap_id
1888 )
1889 |> Repo.all()
1890 end
1891
1892 defdelegate search(query, opts \\ []), to: User.Search
1893
1894 defp put_password_hash(
1895 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1896 ) do
1897 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1898 end
1899
1900 defp put_password_hash(changeset), do: changeset
1901
1902 def is_internal_user?(%User{nickname: nil}), do: true
1903 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1904 def is_internal_user?(_), do: false
1905
1906 # A hack because user delete activities have a fake id for whatever reason
1907 # TODO: Get rid of this
1908 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1909
1910 def get_delivered_users_by_object_id(object_id) do
1911 from(u in User,
1912 inner_join: delivery in assoc(u, :deliveries),
1913 where: delivery.object_id == ^object_id
1914 )
1915 |> Repo.all()
1916 end
1917
1918 def change_email(user, email) do
1919 user
1920 |> cast(%{email: email}, [:email])
1921 |> validate_required([:email])
1922 |> unique_constraint(:email)
1923 |> validate_format(:email, @email_regex)
1924 |> update_and_set_cache()
1925 end
1926
1927 # Internal function; public one is `deactivate/2`
1928 defp set_activation_status(user, deactivated) do
1929 user
1930 |> cast(%{deactivated: deactivated}, [:deactivated])
1931 |> update_and_set_cache()
1932 end
1933
1934 def update_banner(user, banner) do
1935 user
1936 |> cast(%{banner: banner}, [:banner])
1937 |> update_and_set_cache()
1938 end
1939
1940 def update_background(user, background) do
1941 user
1942 |> cast(%{background: background}, [:background])
1943 |> update_and_set_cache()
1944 end
1945
1946 def update_source_data(user, source_data) do
1947 user
1948 |> cast(%{source_data: source_data}, [:source_data])
1949 |> update_and_set_cache()
1950 end
1951
1952 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1953 %{
1954 admin: is_admin,
1955 moderator: is_moderator
1956 }
1957 end
1958
1959 # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
1960 # For example: [{"name": "Pronoun", "value": "she/her"}, …]
1961 def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
1962 limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
1963
1964 attachment
1965 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
1966 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
1967 |> Enum.take(limit)
1968 end
1969
1970 def fields(%{fields: nil}), do: []
1971
1972 def fields(%{fields: fields}), do: fields
1973
1974 def validate_fields(changeset, remote? \\ false) do
1975 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
1976 limit = Pleroma.Config.get([:instance, limit_name], 0)
1977
1978 changeset
1979 |> validate_length(:fields, max: limit)
1980 |> validate_change(:fields, fn :fields, fields ->
1981 if Enum.all?(fields, &valid_field?/1) do
1982 []
1983 else
1984 [fields: "invalid"]
1985 end
1986 end)
1987 end
1988
1989 defp valid_field?(%{"name" => name, "value" => value}) do
1990 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
1991 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
1992
1993 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
1994 String.length(value) <= value_limit
1995 end
1996
1997 defp valid_field?(_), do: false
1998
1999 defp truncate_field(%{"name" => name, "value" => value}) do
2000 {name, _chopped} =
2001 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
2002
2003 {value, _chopped} =
2004 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
2005
2006 %{"name" => name, "value" => value}
2007 end
2008
2009 def admin_api_update(user, params) do
2010 user
2011 |> cast(params, [
2012 :is_moderator,
2013 :is_admin,
2014 :show_role
2015 ])
2016 |> update_and_set_cache()
2017 end
2018
2019 @doc "Signs user out of all applications"
2020 def global_sign_out(user) do
2021 OAuth.Authorization.delete_user_authorizations(user)
2022 OAuth.Token.delete_user_tokens(user)
2023 end
2024
2025 def mascot_update(user, url) do
2026 user
2027 |> cast(%{mascot: url}, [:mascot])
2028 |> validate_required([:mascot])
2029 |> update_and_set_cache()
2030 end
2031
2032 def mastodon_settings_update(user, settings) do
2033 user
2034 |> cast(%{settings: settings}, [:settings])
2035 |> validate_required([:settings])
2036 |> update_and_set_cache()
2037 end
2038
2039 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2040 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
2041 params =
2042 if need_confirmation? do
2043 %{
2044 confirmation_pending: true,
2045 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2046 }
2047 else
2048 %{
2049 confirmation_pending: false,
2050 confirmation_token: nil
2051 }
2052 end
2053
2054 cast(user, params, [:confirmation_pending, :confirmation_token])
2055 end
2056
2057 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2058 if id not in user.pinned_activities do
2059 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
2060 params = %{pinned_activities: user.pinned_activities ++ [id]}
2061
2062 user
2063 |> cast(params, [:pinned_activities])
2064 |> validate_length(:pinned_activities,
2065 max: max_pinned_statuses,
2066 message: "You have already pinned the maximum number of statuses"
2067 )
2068 else
2069 change(user)
2070 end
2071 |> update_and_set_cache()
2072 end
2073
2074 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2075 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2076
2077 user
2078 |> cast(params, [:pinned_activities])
2079 |> update_and_set_cache()
2080 end
2081
2082 def update_email_notifications(user, settings) do
2083 email_notifications =
2084 user.email_notifications
2085 |> Map.merge(settings)
2086 |> Map.take(["digest"])
2087
2088 params = %{email_notifications: email_notifications}
2089 fields = [:email_notifications]
2090
2091 user
2092 |> cast(params, fields)
2093 |> validate_required(fields)
2094 |> update_and_set_cache()
2095 end
2096
2097 defp set_domain_blocks(user, domain_blocks) do
2098 params = %{domain_blocks: domain_blocks}
2099
2100 user
2101 |> cast(params, [:domain_blocks])
2102 |> validate_required([:domain_blocks])
2103 |> update_and_set_cache()
2104 end
2105
2106 def block_domain(user, domain_blocked) do
2107 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2108 end
2109
2110 def unblock_domain(user, domain_blocked) do
2111 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2112 end
2113
2114 @spec add_to_block(User.t(), User.t()) ::
2115 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2116 defp add_to_block(%User{} = user, %User{} = blocked) do
2117 UserRelationship.create_block(user, blocked)
2118 end
2119
2120 @spec add_to_block(User.t(), User.t()) ::
2121 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2122 defp remove_from_block(%User{} = user, %User{} = blocked) do
2123 UserRelationship.delete_block(user, blocked)
2124 end
2125
2126 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2127 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2128 {:ok, user_notification_mute} <-
2129 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2130 {:ok, nil} do
2131 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2132 end
2133 end
2134
2135 defp remove_from_mutes(user, %User{} = muted_user) do
2136 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2137 {:ok, user_notification_mute} <-
2138 UserRelationship.delete_notification_mute(user, muted_user) do
2139 {:ok, [user_mute, user_notification_mute]}
2140 end
2141 end
2142
2143 def set_invisible(user, invisible) do
2144 params = %{invisible: invisible}
2145
2146 user
2147 |> cast(params, [:invisible])
2148 |> validate_required([:invisible])
2149 |> update_and_set_cache()
2150 end
2151
2152 def sanitize_html(%User{} = user) do
2153 sanitize_html(user, nil)
2154 end
2155
2156 # User data that mastodon isn't filtering (treated as plaintext):
2157 # - field name
2158 # - display name
2159 def sanitize_html(%User{} = user, filter) do
2160 fields =
2161 user
2162 |> User.fields()
2163 |> Enum.map(fn %{"name" => name, "value" => value} ->
2164 %{
2165 "name" => name,
2166 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2167 }
2168 end)
2169
2170 user
2171 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2172 |> Map.put(:fields, fields)
2173 end
2174 end