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