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