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