Merge branch 'following-relationships-optimizations' 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, :follow_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, :follow_accept))
724
725 set_cache(follower)
726 end
727
728 defdelegate following(user), to: FollowingRelationship
729
730 def follow(%User{} = follower, %User{} = followed, state \\ :follow_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 [:follow_pending, :follow_accept] ->
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 @doc "Returns follow state as Pleroma.FollowingRelationship.State value"
776 def get_follow_state(%User{} = follower, %User{} = following) do
777 following_relationship = FollowingRelationship.get(follower, following)
778 get_follow_state(follower, following, following_relationship)
779 end
780
781 def get_follow_state(
782 %User{} = follower,
783 %User{} = following,
784 following_relationship
785 ) do
786 case {following_relationship, following.local} do
787 {nil, false} ->
788 case Utils.fetch_latest_follow(follower, following) do
789 %Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
790 FollowingRelationship.state_to_enum(state)
791
792 _ ->
793 nil
794 end
795
796 {%{state: state}, _} ->
797 state
798
799 {nil, _} ->
800 nil
801 end
802 end
803
804 def locked?(%User{} = user) do
805 user.locked || false
806 end
807
808 def get_by_id(id) do
809 Repo.get_by(User, id: id)
810 end
811
812 def get_by_ap_id(ap_id) do
813 Repo.get_by(User, ap_id: ap_id)
814 end
815
816 def get_all_by_ap_id(ap_ids) do
817 from(u in __MODULE__,
818 where: u.ap_id in ^ap_ids
819 )
820 |> Repo.all()
821 end
822
823 def get_all_by_ids(ids) do
824 from(u in __MODULE__, where: u.id in ^ids)
825 |> Repo.all()
826 end
827
828 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
829 # of the ap_id and the domain and tries to get that user
830 def get_by_guessed_nickname(ap_id) do
831 domain = URI.parse(ap_id).host
832 name = List.last(String.split(ap_id, "/"))
833 nickname = "#{name}@#{domain}"
834
835 get_cached_by_nickname(nickname)
836 end
837
838 def set_cache({:ok, user}), do: set_cache(user)
839 def set_cache({:error, err}), do: {:error, err}
840
841 def set_cache(%User{} = user) do
842 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
843 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
844 {:ok, user}
845 end
846
847 def update_and_set_cache(struct, params) do
848 struct
849 |> update_changeset(params)
850 |> update_and_set_cache()
851 end
852
853 def update_and_set_cache(changeset) do
854 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
855 set_cache(user)
856 end
857 end
858
859 def invalidate_cache(user) do
860 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
861 Cachex.del(:user_cache, "nickname:#{user.nickname}")
862 end
863
864 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
865 def get_cached_by_ap_id(ap_id) do
866 key = "ap_id:#{ap_id}"
867
868 with {:ok, nil} <- Cachex.get(:user_cache, key),
869 user when not is_nil(user) <- get_by_ap_id(ap_id),
870 {:ok, true} <- Cachex.put(:user_cache, key, user) do
871 user
872 else
873 {:ok, user} -> user
874 nil -> nil
875 end
876 end
877
878 def get_cached_by_id(id) do
879 key = "id:#{id}"
880
881 ap_id =
882 Cachex.fetch!(:user_cache, key, fn _ ->
883 user = get_by_id(id)
884
885 if user do
886 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
887 {:commit, user.ap_id}
888 else
889 {:ignore, ""}
890 end
891 end)
892
893 get_cached_by_ap_id(ap_id)
894 end
895
896 def get_cached_by_nickname(nickname) do
897 key = "nickname:#{nickname}"
898
899 Cachex.fetch!(:user_cache, key, fn ->
900 case get_or_fetch_by_nickname(nickname) do
901 {:ok, user} -> {:commit, user}
902 {:error, _error} -> {:ignore, nil}
903 end
904 end)
905 end
906
907 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
908 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
909
910 cond do
911 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
912 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
913
914 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
915 get_cached_by_nickname(nickname_or_id)
916
917 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
918 get_cached_by_nickname(nickname_or_id)
919
920 true ->
921 nil
922 end
923 end
924
925 def get_by_nickname(nickname) do
926 Repo.get_by(User, nickname: nickname) ||
927 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
928 Repo.get_by(User, nickname: local_nickname(nickname))
929 end
930 end
931
932 def get_by_email(email), do: Repo.get_by(User, email: email)
933
934 def get_by_nickname_or_email(nickname_or_email) do
935 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
936 end
937
938 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
939
940 def get_or_fetch_by_nickname(nickname) do
941 with %User{} = user <- get_by_nickname(nickname) do
942 {:ok, user}
943 else
944 _e ->
945 with [_nick, _domain] <- String.split(nickname, "@"),
946 {:ok, user} <- fetch_by_nickname(nickname) do
947 {:ok, user}
948 else
949 _e -> {:error, "not found " <> nickname}
950 end
951 end
952 end
953
954 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
955 def get_followers_query(%User{} = user, nil) do
956 User.Query.build(%{followers: user, deactivated: false})
957 end
958
959 def get_followers_query(user, page) do
960 user
961 |> get_followers_query(nil)
962 |> User.Query.paginate(page, 20)
963 end
964
965 @spec get_followers_query(User.t()) :: Ecto.Query.t()
966 def get_followers_query(user), do: get_followers_query(user, nil)
967
968 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
969 def get_followers(user, page \\ nil) do
970 user
971 |> get_followers_query(page)
972 |> Repo.all()
973 end
974
975 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
976 def get_external_followers(user, page \\ nil) do
977 user
978 |> get_followers_query(page)
979 |> User.Query.build(%{external: true})
980 |> Repo.all()
981 end
982
983 def get_followers_ids(user, page \\ nil) do
984 user
985 |> get_followers_query(page)
986 |> select([u], u.id)
987 |> Repo.all()
988 end
989
990 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
991 def get_friends_query(%User{} = user, nil) do
992 User.Query.build(%{friends: user, deactivated: false})
993 end
994
995 def get_friends_query(user, page) do
996 user
997 |> get_friends_query(nil)
998 |> User.Query.paginate(page, 20)
999 end
1000
1001 @spec get_friends_query(User.t()) :: Ecto.Query.t()
1002 def get_friends_query(user), do: get_friends_query(user, nil)
1003
1004 def get_friends(user, page \\ nil) do
1005 user
1006 |> get_friends_query(page)
1007 |> Repo.all()
1008 end
1009
1010 def get_friends_ap_ids(user) do
1011 user
1012 |> get_friends_query(nil)
1013 |> select([u], u.ap_id)
1014 |> Repo.all()
1015 end
1016
1017 def get_friends_ids(user, page \\ nil) do
1018 user
1019 |> get_friends_query(page)
1020 |> select([u], u.id)
1021 |> Repo.all()
1022 end
1023
1024 defdelegate get_follow_requests(user), to: FollowingRelationship
1025
1026 def increase_note_count(%User{} = user) do
1027 User
1028 |> where(id: ^user.id)
1029 |> update([u], inc: [note_count: 1])
1030 |> select([u], u)
1031 |> Repo.update_all([])
1032 |> case do
1033 {1, [user]} -> set_cache(user)
1034 _ -> {:error, user}
1035 end
1036 end
1037
1038 def decrease_note_count(%User{} = user) do
1039 User
1040 |> where(id: ^user.id)
1041 |> update([u],
1042 set: [
1043 note_count: fragment("greatest(0, note_count - 1)")
1044 ]
1045 )
1046 |> select([u], u)
1047 |> Repo.update_all([])
1048 |> case do
1049 {1, [user]} -> set_cache(user)
1050 _ -> {:error, user}
1051 end
1052 end
1053
1054 def update_note_count(%User{} = user, note_count \\ nil) do
1055 note_count =
1056 note_count ||
1057 from(
1058 a in Object,
1059 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1060 select: count(a.id)
1061 )
1062 |> Repo.one()
1063
1064 user
1065 |> cast(%{note_count: note_count}, [:note_count])
1066 |> update_and_set_cache()
1067 end
1068
1069 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1070 def maybe_fetch_follow_information(user) do
1071 with {:ok, user} <- fetch_follow_information(user) do
1072 user
1073 else
1074 e ->
1075 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1076
1077 user
1078 end
1079 end
1080
1081 def fetch_follow_information(user) do
1082 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1083 user
1084 |> follow_information_changeset(info)
1085 |> update_and_set_cache()
1086 end
1087 end
1088
1089 defp follow_information_changeset(user, params) do
1090 user
1091 |> cast(params, [
1092 :hide_followers,
1093 :hide_follows,
1094 :follower_count,
1095 :following_count,
1096 :hide_followers_count,
1097 :hide_follows_count
1098 ])
1099 end
1100
1101 def update_follower_count(%User{} = user) do
1102 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
1103 follower_count_query =
1104 User.Query.build(%{followers: user, deactivated: false})
1105 |> select([u], %{count: count(u.id)})
1106
1107 User
1108 |> where(id: ^user.id)
1109 |> join(:inner, [u], s in subquery(follower_count_query))
1110 |> update([u, s],
1111 set: [follower_count: s.count]
1112 )
1113 |> select([u], u)
1114 |> Repo.update_all([])
1115 |> case do
1116 {1, [user]} -> set_cache(user)
1117 _ -> {:error, user}
1118 end
1119 else
1120 {:ok, maybe_fetch_follow_information(user)}
1121 end
1122 end
1123
1124 @spec update_following_count(User.t()) :: User.t()
1125 def update_following_count(%User{local: false} = user) do
1126 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1127 maybe_fetch_follow_information(user)
1128 else
1129 user
1130 end
1131 end
1132
1133 def update_following_count(%User{local: true} = user) do
1134 following_count = FollowingRelationship.following_count(user)
1135
1136 user
1137 |> follow_information_changeset(%{following_count: following_count})
1138 |> Repo.update!()
1139 end
1140
1141 def set_unread_conversation_count(%User{local: true} = user) do
1142 unread_query = Participation.unread_conversation_count_for_user(user)
1143
1144 User
1145 |> join(:inner, [u], p in subquery(unread_query))
1146 |> update([u, p],
1147 set: [unread_conversation_count: p.count]
1148 )
1149 |> where([u], u.id == ^user.id)
1150 |> select([u], u)
1151 |> Repo.update_all([])
1152 |> case do
1153 {1, [user]} -> set_cache(user)
1154 _ -> {:error, user}
1155 end
1156 end
1157
1158 def set_unread_conversation_count(user), do: {:ok, user}
1159
1160 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
1161 unread_query =
1162 Participation.unread_conversation_count_for_user(user)
1163 |> where([p], p.conversation_id == ^conversation.id)
1164
1165 User
1166 |> join(:inner, [u], p in subquery(unread_query))
1167 |> update([u, p],
1168 inc: [unread_conversation_count: 1]
1169 )
1170 |> where([u], u.id == ^user.id)
1171 |> where([u, p], p.count == 0)
1172 |> select([u], u)
1173 |> Repo.update_all([])
1174 |> case do
1175 {1, [user]} -> set_cache(user)
1176 _ -> {:error, user}
1177 end
1178 end
1179
1180 def increment_unread_conversation_count(_, user), do: {:ok, user}
1181
1182 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
1183 def get_users_from_set(ap_ids, local_only \\ true) do
1184 criteria = %{ap_id: ap_ids, deactivated: false}
1185 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1186
1187 User.Query.build(criteria)
1188 |> Repo.all()
1189 end
1190
1191 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1192 def get_recipients_from_activity(%Activity{recipients: to}) do
1193 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1194 |> Repo.all()
1195 end
1196
1197 @spec mute(User.t(), User.t(), boolean()) ::
1198 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1199 def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
1200 add_to_mutes(muter, mutee, notifications?)
1201 end
1202
1203 def unmute(%User{} = muter, %User{} = mutee) do
1204 remove_from_mutes(muter, mutee)
1205 end
1206
1207 def subscribe(%User{} = subscriber, %User{} = target) do
1208 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1209
1210 if blocks?(target, subscriber) and deny_follow_blocked do
1211 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1212 else
1213 # Note: the relationship is inverse: subscriber acts as relationship target
1214 UserRelationship.create_inverse_subscription(target, subscriber)
1215 end
1216 end
1217
1218 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1219 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1220 subscribe(subscriber, subscribee)
1221 end
1222 end
1223
1224 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1225 # Note: the relationship is inverse: subscriber acts as relationship target
1226 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1227 end
1228
1229 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1230 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1231 unsubscribe(unsubscriber, user)
1232 end
1233 end
1234
1235 def block(%User{} = blocker, %User{} = blocked) do
1236 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1237 blocker =
1238 if following?(blocker, blocked) do
1239 {:ok, blocker, _} = unfollow(blocker, blocked)
1240 blocker
1241 else
1242 blocker
1243 end
1244
1245 # clear any requested follows as well
1246 blocked =
1247 case CommonAPI.reject_follow_request(blocked, blocker) do
1248 {:ok, %User{} = updated_blocked} -> updated_blocked
1249 nil -> blocked
1250 end
1251
1252 unsubscribe(blocked, blocker)
1253
1254 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1255
1256 {:ok, blocker} = update_follower_count(blocker)
1257 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1258 add_to_block(blocker, blocked)
1259 end
1260
1261 # helper to handle the block given only an actor's AP id
1262 def block(%User{} = blocker, %{ap_id: ap_id}) do
1263 block(blocker, get_cached_by_ap_id(ap_id))
1264 end
1265
1266 def unblock(%User{} = blocker, %User{} = blocked) do
1267 remove_from_block(blocker, blocked)
1268 end
1269
1270 # helper to handle the block given only an actor's AP id
1271 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1272 unblock(blocker, get_cached_by_ap_id(ap_id))
1273 end
1274
1275 def mutes?(nil, _), do: false
1276 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1277
1278 def mutes_user?(%User{} = user, %User{} = target) do
1279 UserRelationship.mute_exists?(user, target)
1280 end
1281
1282 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1283 def muted_notifications?(nil, _), do: false
1284
1285 def muted_notifications?(%User{} = user, %User{} = target),
1286 do: UserRelationship.notification_mute_exists?(user, target)
1287
1288 def blocks?(nil, _), do: false
1289
1290 def blocks?(%User{} = user, %User{} = target) do
1291 blocks_user?(user, target) ||
1292 (blocks_domain?(user, target) and not User.following?(user, target))
1293 end
1294
1295 def blocks_user?(%User{} = user, %User{} = target) do
1296 UserRelationship.block_exists?(user, target)
1297 end
1298
1299 def blocks_user?(_, _), do: false
1300
1301 def blocks_domain?(%User{} = user, %User{} = target) do
1302 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1303 %{host: host} = URI.parse(target.ap_id)
1304 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1305 end
1306
1307 def blocks_domain?(_, _), do: false
1308
1309 def subscribed_to?(%User{} = user, %User{} = target) do
1310 # Note: the relationship is inverse: subscriber acts as relationship target
1311 UserRelationship.inverse_subscription_exists?(target, user)
1312 end
1313
1314 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1315 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1316 subscribed_to?(user, target)
1317 end
1318 end
1319
1320 @doc """
1321 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1322 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1323 """
1324 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1325 def outgoing_relationships_ap_ids(_user, []), do: %{}
1326
1327 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1328
1329 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1330 when is_list(relationship_types) do
1331 db_result =
1332 user
1333 |> assoc(:outgoing_relationships)
1334 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1335 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1336 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1337 |> group_by([user_rel, u], user_rel.relationship_type)
1338 |> Repo.all()
1339 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1340
1341 Enum.into(
1342 relationship_types,
1343 %{},
1344 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1345 )
1346 end
1347
1348 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1349
1350 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1351
1352 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1353
1354 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1355 when is_list(relationship_types) do
1356 user
1357 |> assoc(:incoming_relationships)
1358 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1359 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1360 |> maybe_filter_on_ap_id(ap_ids)
1361 |> select([user_rel, u], u.ap_id)
1362 |> distinct(true)
1363 |> Repo.all()
1364 end
1365
1366 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1367 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1368 end
1369
1370 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1371
1372 def deactivate_async(user, status \\ true) do
1373 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1374 end
1375
1376 def deactivate(user, status \\ true)
1377
1378 def deactivate(users, status) when is_list(users) do
1379 Repo.transaction(fn ->
1380 for user <- users, do: deactivate(user, status)
1381 end)
1382 end
1383
1384 def deactivate(%User{} = user, status) do
1385 with {:ok, user} <- set_activation_status(user, status) do
1386 user
1387 |> get_followers()
1388 |> Enum.filter(& &1.local)
1389 |> Enum.each(fn follower ->
1390 follower |> update_following_count() |> set_cache()
1391 end)
1392
1393 # Only update local user counts, remote will be update during the next pull.
1394 user
1395 |> get_friends()
1396 |> Enum.filter(& &1.local)
1397 |> Enum.each(&update_follower_count/1)
1398
1399 {:ok, user}
1400 end
1401 end
1402
1403 def update_notification_settings(%User{} = user, settings) do
1404 user
1405 |> cast(%{notification_settings: settings}, [])
1406 |> cast_embed(:notification_settings)
1407 |> validate_required([:notification_settings])
1408 |> update_and_set_cache()
1409 end
1410
1411 def delete(users) when is_list(users) do
1412 for user <- users, do: delete(user)
1413 end
1414
1415 def delete(%User{} = user) do
1416 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1417 end
1418
1419 def perform(:force_password_reset, user), do: force_password_reset(user)
1420
1421 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1422 def perform(:delete, %User{} = user) do
1423 {:ok, _user} = ActivityPub.delete(user)
1424
1425 # Remove all relationships
1426 user
1427 |> get_followers()
1428 |> Enum.each(fn follower ->
1429 ActivityPub.unfollow(follower, user)
1430 unfollow(follower, user)
1431 end)
1432
1433 user
1434 |> get_friends()
1435 |> Enum.each(fn followed ->
1436 ActivityPub.unfollow(user, followed)
1437 unfollow(user, followed)
1438 end)
1439
1440 delete_user_activities(user)
1441 invalidate_cache(user)
1442 Repo.delete(user)
1443 end
1444
1445 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1446
1447 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1448 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1449 when is_list(blocked_identifiers) do
1450 Enum.map(
1451 blocked_identifiers,
1452 fn blocked_identifier ->
1453 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1454 {:ok, _user_block} <- block(blocker, blocked),
1455 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1456 blocked
1457 else
1458 err ->
1459 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1460 err
1461 end
1462 end
1463 )
1464 end
1465
1466 def perform(:follow_import, %User{} = follower, followed_identifiers)
1467 when is_list(followed_identifiers) do
1468 Enum.map(
1469 followed_identifiers,
1470 fn followed_identifier ->
1471 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1472 {:ok, follower} <- maybe_direct_follow(follower, followed),
1473 {:ok, _} <- ActivityPub.follow(follower, followed) do
1474 followed
1475 else
1476 err ->
1477 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1478 err
1479 end
1480 end
1481 )
1482 end
1483
1484 @spec external_users_query() :: Ecto.Query.t()
1485 def external_users_query do
1486 User.Query.build(%{
1487 external: true,
1488 active: true,
1489 order_by: :id
1490 })
1491 end
1492
1493 @spec external_users(keyword()) :: [User.t()]
1494 def external_users(opts \\ []) do
1495 query =
1496 external_users_query()
1497 |> select([u], struct(u, [:id, :ap_id]))
1498
1499 query =
1500 if opts[:max_id],
1501 do: where(query, [u], u.id > ^opts[:max_id]),
1502 else: query
1503
1504 query =
1505 if opts[:limit],
1506 do: limit(query, ^opts[:limit]),
1507 else: query
1508
1509 Repo.all(query)
1510 end
1511
1512 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1513 BackgroundWorker.enqueue("blocks_import", %{
1514 "blocker_id" => blocker.id,
1515 "blocked_identifiers" => blocked_identifiers
1516 })
1517 end
1518
1519 def follow_import(%User{} = follower, followed_identifiers)
1520 when is_list(followed_identifiers) do
1521 BackgroundWorker.enqueue("follow_import", %{
1522 "follower_id" => follower.id,
1523 "followed_identifiers" => followed_identifiers
1524 })
1525 end
1526
1527 def delete_user_activities(%User{ap_id: ap_id}) do
1528 ap_id
1529 |> Activity.Queries.by_actor()
1530 |> RepoStreamer.chunk_stream(50)
1531 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1532 |> Stream.run()
1533 end
1534
1535 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1536 activity
1537 |> Object.normalize()
1538 |> ActivityPub.delete()
1539 end
1540
1541 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1542 object = Object.normalize(activity)
1543
1544 activity.actor
1545 |> get_cached_by_ap_id()
1546 |> ActivityPub.unlike(object)
1547 end
1548
1549 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1550 object = Object.normalize(activity)
1551
1552 activity.actor
1553 |> get_cached_by_ap_id()
1554 |> ActivityPub.unannounce(object)
1555 end
1556
1557 defp delete_activity(_activity), do: "Doing nothing"
1558
1559 def html_filter_policy(%User{no_rich_text: true}) do
1560 Pleroma.HTML.Scrubber.TwitterText
1561 end
1562
1563 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1564
1565 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1566
1567 def get_or_fetch_by_ap_id(ap_id) do
1568 user = get_cached_by_ap_id(ap_id)
1569
1570 if !is_nil(user) and !needs_update?(user) do
1571 {:ok, user}
1572 else
1573 fetch_by_ap_id(ap_id)
1574 end
1575 end
1576
1577 @doc """
1578 Creates an internal service actor by URI if missing.
1579 Optionally takes nickname for addressing.
1580 """
1581 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1582 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1583 {_, user} =
1584 case get_cached_by_ap_id(uri) do
1585 nil ->
1586 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1587 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1588 {:error, nil}
1589 end
1590
1591 %User{invisible: false} = user ->
1592 set_invisible(user)
1593
1594 user ->
1595 {:ok, user}
1596 end
1597
1598 user
1599 end
1600
1601 @spec set_invisible(User.t()) :: {:ok, User.t()}
1602 defp set_invisible(user) do
1603 user
1604 |> change(%{invisible: true})
1605 |> update_and_set_cache()
1606 end
1607
1608 @spec create_service_actor(String.t(), String.t()) ::
1609 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1610 defp create_service_actor(uri, nickname) do
1611 %User{
1612 invisible: true,
1613 local: true,
1614 ap_id: uri,
1615 nickname: nickname,
1616 follower_address: uri <> "/followers"
1617 }
1618 |> change
1619 |> unique_constraint(:nickname)
1620 |> Repo.insert()
1621 |> set_cache()
1622 end
1623
1624 # AP style
1625 def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
1626 key =
1627 public_key_pem
1628 |> :public_key.pem_decode()
1629 |> hd()
1630 |> :public_key.pem_entry_decode()
1631
1632 {:ok, key}
1633 end
1634
1635 def public_key(_), do: {:error, "not found key"}
1636
1637 def get_public_key_for_ap_id(ap_id) do
1638 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1639 {:ok, public_key} <- public_key(user) do
1640 {:ok, public_key}
1641 else
1642 _ -> :error
1643 end
1644 end
1645
1646 defp blank?(""), do: nil
1647 defp blank?(n), do: n
1648
1649 def insert_or_update_user(data) do
1650 data
1651 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1652 |> remote_user_creation()
1653 |> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
1654 |> set_cache()
1655 end
1656
1657 def ap_enabled?(%User{local: true}), do: true
1658 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1659 def ap_enabled?(_), do: false
1660
1661 @doc "Gets or fetch a user by uri or nickname."
1662 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1663 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1664 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1665
1666 # wait a period of time and return newest version of the User structs
1667 # this is because we have synchronous follow APIs and need to simulate them
1668 # with an async handshake
1669 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1670 with %User{} = a <- get_cached_by_id(a.id),
1671 %User{} = b <- get_cached_by_id(b.id) do
1672 {:ok, a, b}
1673 else
1674 nil -> :error
1675 end
1676 end
1677
1678 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1679 with :ok <- :timer.sleep(timeout),
1680 %User{} = a <- get_cached_by_id(a.id),
1681 %User{} = b <- get_cached_by_id(b.id) do
1682 {:ok, a, b}
1683 else
1684 nil -> :error
1685 end
1686 end
1687
1688 def parse_bio(bio) when is_binary(bio) and bio != "" do
1689 bio
1690 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1691 |> elem(0)
1692 end
1693
1694 def parse_bio(_), do: ""
1695
1696 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1697 # TODO: get profile URLs other than user.ap_id
1698 profile_urls = [user.ap_id]
1699
1700 bio
1701 |> CommonUtils.format_input("text/plain",
1702 mentions_format: :full,
1703 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1704 )
1705 |> elem(0)
1706 end
1707
1708 def parse_bio(_, _), do: ""
1709
1710 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1711 Repo.transaction(fn ->
1712 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1713 end)
1714 end
1715
1716 def tag(nickname, tags) when is_binary(nickname),
1717 do: tag(get_by_nickname(nickname), tags)
1718
1719 def tag(%User{} = user, tags),
1720 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1721
1722 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1723 Repo.transaction(fn ->
1724 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1725 end)
1726 end
1727
1728 def untag(nickname, tags) when is_binary(nickname),
1729 do: untag(get_by_nickname(nickname), tags)
1730
1731 def untag(%User{} = user, tags),
1732 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1733
1734 defp update_tags(%User{} = user, new_tags) do
1735 {:ok, updated_user} =
1736 user
1737 |> change(%{tags: new_tags})
1738 |> update_and_set_cache()
1739
1740 updated_user
1741 end
1742
1743 defp normalize_tags(tags) do
1744 [tags]
1745 |> List.flatten()
1746 |> Enum.map(&String.downcase/1)
1747 end
1748
1749 defp local_nickname_regex do
1750 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1751 @extended_local_nickname_regex
1752 else
1753 @strict_local_nickname_regex
1754 end
1755 end
1756
1757 def local_nickname(nickname_or_mention) do
1758 nickname_or_mention
1759 |> full_nickname()
1760 |> String.split("@")
1761 |> hd()
1762 end
1763
1764 def full_nickname(nickname_or_mention),
1765 do: String.trim_leading(nickname_or_mention, "@")
1766
1767 def error_user(ap_id) do
1768 %User{
1769 name: ap_id,
1770 ap_id: ap_id,
1771 nickname: "erroruser@example.com",
1772 inserted_at: NaiveDateTime.utc_now()
1773 }
1774 end
1775
1776 @spec all_superusers() :: [User.t()]
1777 def all_superusers do
1778 User.Query.build(%{super_users: true, local: true, deactivated: false})
1779 |> Repo.all()
1780 end
1781
1782 def muting_reblogs?(%User{} = user, %User{} = target) do
1783 UserRelationship.reblog_mute_exists?(user, target)
1784 end
1785
1786 def showing_reblogs?(%User{} = user, %User{} = target) do
1787 not muting_reblogs?(user, target)
1788 end
1789
1790 @doc """
1791 The function returns a query to get users with no activity for given interval of days.
1792 Inactive users are those who didn't read any notification, or had any activity where
1793 the user is the activity's actor, during `inactivity_threshold` days.
1794 Deactivated users will not appear in this list.
1795
1796 ## Examples
1797
1798 iex> Pleroma.User.list_inactive_users()
1799 %Ecto.Query{}
1800 """
1801 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1802 def list_inactive_users_query(inactivity_threshold \\ 7) do
1803 negative_inactivity_threshold = -inactivity_threshold
1804 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1805 # Subqueries are not supported in `where` clauses, join gets too complicated.
1806 has_read_notifications =
1807 from(n in Pleroma.Notification,
1808 where: n.seen == true,
1809 group_by: n.id,
1810 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1811 select: n.user_id
1812 )
1813 |> Pleroma.Repo.all()
1814
1815 from(u in Pleroma.User,
1816 left_join: a in Pleroma.Activity,
1817 on: u.ap_id == a.actor,
1818 where: not is_nil(u.nickname),
1819 where: u.deactivated != ^true,
1820 where: u.id not in ^has_read_notifications,
1821 group_by: u.id,
1822 having:
1823 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1824 is_nil(max(a.inserted_at))
1825 )
1826 end
1827
1828 @doc """
1829 Enable or disable email notifications for user
1830
1831 ## Examples
1832
1833 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1834 Pleroma.User{email_notifications: %{"digest" => true}}
1835
1836 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1837 Pleroma.User{email_notifications: %{"digest" => false}}
1838 """
1839 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1840 {:ok, t()} | {:error, Ecto.Changeset.t()}
1841 def switch_email_notifications(user, type, status) do
1842 User.update_email_notifications(user, %{type => status})
1843 end
1844
1845 @doc """
1846 Set `last_digest_emailed_at` value for the user to current time
1847 """
1848 @spec touch_last_digest_emailed_at(t()) :: t()
1849 def touch_last_digest_emailed_at(user) do
1850 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1851
1852 {:ok, updated_user} =
1853 user
1854 |> change(%{last_digest_emailed_at: now})
1855 |> update_and_set_cache()
1856
1857 updated_user
1858 end
1859
1860 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1861 def toggle_confirmation(%User{} = user) do
1862 user
1863 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1864 |> update_and_set_cache()
1865 end
1866
1867 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1868 def toggle_confirmation(users) do
1869 Enum.map(users, &toggle_confirmation/1)
1870 end
1871
1872 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1873 mascot
1874 end
1875
1876 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1877 # use instance-default
1878 config = Pleroma.Config.get([:assets, :mascots])
1879 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1880 mascot = Keyword.get(config, default_mascot)
1881
1882 %{
1883 "id" => "default-mascot",
1884 "url" => mascot[:url],
1885 "preview_url" => mascot[:url],
1886 "pleroma" => %{
1887 "mime_type" => mascot[:mime_type]
1888 }
1889 }
1890 end
1891
1892 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1893
1894 def ensure_keys_present(%User{} = user) do
1895 with {:ok, pem} <- Keys.generate_rsa_pem() do
1896 user
1897 |> cast(%{keys: pem}, [:keys])
1898 |> validate_required([:keys])
1899 |> update_and_set_cache()
1900 end
1901 end
1902
1903 def get_ap_ids_by_nicknames(nicknames) do
1904 from(u in User,
1905 where: u.nickname in ^nicknames,
1906 select: u.ap_id
1907 )
1908 |> Repo.all()
1909 end
1910
1911 defdelegate search(query, opts \\ []), to: User.Search
1912
1913 defp put_password_hash(
1914 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1915 ) do
1916 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1917 end
1918
1919 defp put_password_hash(changeset), do: changeset
1920
1921 def is_internal_user?(%User{nickname: nil}), do: true
1922 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1923 def is_internal_user?(_), do: false
1924
1925 # A hack because user delete activities have a fake id for whatever reason
1926 # TODO: Get rid of this
1927 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1928
1929 def get_delivered_users_by_object_id(object_id) do
1930 from(u in User,
1931 inner_join: delivery in assoc(u, :deliveries),
1932 where: delivery.object_id == ^object_id
1933 )
1934 |> Repo.all()
1935 end
1936
1937 def change_email(user, email) do
1938 user
1939 |> cast(%{email: email}, [:email])
1940 |> validate_required([:email])
1941 |> unique_constraint(:email)
1942 |> validate_format(:email, @email_regex)
1943 |> update_and_set_cache()
1944 end
1945
1946 # Internal function; public one is `deactivate/2`
1947 defp set_activation_status(user, deactivated) do
1948 user
1949 |> cast(%{deactivated: deactivated}, [:deactivated])
1950 |> update_and_set_cache()
1951 end
1952
1953 def update_banner(user, banner) do
1954 user
1955 |> cast(%{banner: banner}, [:banner])
1956 |> update_and_set_cache()
1957 end
1958
1959 def update_background(user, background) do
1960 user
1961 |> cast(%{background: background}, [:background])
1962 |> update_and_set_cache()
1963 end
1964
1965 def update_source_data(user, source_data) do
1966 user
1967 |> cast(%{source_data: source_data}, [:source_data])
1968 |> update_and_set_cache()
1969 end
1970
1971 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1972 %{
1973 admin: is_admin,
1974 moderator: is_moderator
1975 }
1976 end
1977
1978 # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
1979 # For example: [{"name": "Pronoun", "value": "she/her"}, …]
1980 def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
1981 limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
1982
1983 attachment
1984 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
1985 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
1986 |> Enum.take(limit)
1987 end
1988
1989 def fields(%{fields: nil}), do: []
1990
1991 def fields(%{fields: fields}), do: fields
1992
1993 def validate_fields(changeset, remote? \\ false) do
1994 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
1995 limit = Pleroma.Config.get([:instance, limit_name], 0)
1996
1997 changeset
1998 |> validate_length(:fields, max: limit)
1999 |> validate_change(:fields, fn :fields, fields ->
2000 if Enum.all?(fields, &valid_field?/1) do
2001 []
2002 else
2003 [fields: "invalid"]
2004 end
2005 end)
2006 end
2007
2008 defp valid_field?(%{"name" => name, "value" => value}) do
2009 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
2010 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
2011
2012 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
2013 String.length(value) <= value_limit
2014 end
2015
2016 defp valid_field?(_), do: false
2017
2018 defp truncate_field(%{"name" => name, "value" => value}) do
2019 {name, _chopped} =
2020 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
2021
2022 {value, _chopped} =
2023 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
2024
2025 %{"name" => name, "value" => value}
2026 end
2027
2028 def admin_api_update(user, params) do
2029 user
2030 |> cast(params, [
2031 :is_moderator,
2032 :is_admin,
2033 :show_role
2034 ])
2035 |> update_and_set_cache()
2036 end
2037
2038 @doc "Signs user out of all applications"
2039 def global_sign_out(user) do
2040 OAuth.Authorization.delete_user_authorizations(user)
2041 OAuth.Token.delete_user_tokens(user)
2042 end
2043
2044 def mascot_update(user, url) do
2045 user
2046 |> cast(%{mascot: url}, [:mascot])
2047 |> validate_required([:mascot])
2048 |> update_and_set_cache()
2049 end
2050
2051 def mastodon_settings_update(user, settings) do
2052 user
2053 |> cast(%{settings: settings}, [:settings])
2054 |> validate_required([:settings])
2055 |> update_and_set_cache()
2056 end
2057
2058 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2059 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
2060 params =
2061 if need_confirmation? do
2062 %{
2063 confirmation_pending: true,
2064 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2065 }
2066 else
2067 %{
2068 confirmation_pending: false,
2069 confirmation_token: nil
2070 }
2071 end
2072
2073 cast(user, params, [:confirmation_pending, :confirmation_token])
2074 end
2075
2076 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2077 if id not in user.pinned_activities do
2078 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
2079 params = %{pinned_activities: user.pinned_activities ++ [id]}
2080
2081 user
2082 |> cast(params, [:pinned_activities])
2083 |> validate_length(:pinned_activities,
2084 max: max_pinned_statuses,
2085 message: "You have already pinned the maximum number of statuses"
2086 )
2087 else
2088 change(user)
2089 end
2090 |> update_and_set_cache()
2091 end
2092
2093 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2094 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2095
2096 user
2097 |> cast(params, [:pinned_activities])
2098 |> update_and_set_cache()
2099 end
2100
2101 def update_email_notifications(user, settings) do
2102 email_notifications =
2103 user.email_notifications
2104 |> Map.merge(settings)
2105 |> Map.take(["digest"])
2106
2107 params = %{email_notifications: email_notifications}
2108 fields = [:email_notifications]
2109
2110 user
2111 |> cast(params, fields)
2112 |> validate_required(fields)
2113 |> update_and_set_cache()
2114 end
2115
2116 defp set_domain_blocks(user, domain_blocks) do
2117 params = %{domain_blocks: domain_blocks}
2118
2119 user
2120 |> cast(params, [:domain_blocks])
2121 |> validate_required([:domain_blocks])
2122 |> update_and_set_cache()
2123 end
2124
2125 def block_domain(user, domain_blocked) do
2126 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2127 end
2128
2129 def unblock_domain(user, domain_blocked) do
2130 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2131 end
2132
2133 @spec add_to_block(User.t(), User.t()) ::
2134 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2135 defp add_to_block(%User{} = user, %User{} = blocked) do
2136 UserRelationship.create_block(user, blocked)
2137 end
2138
2139 @spec add_to_block(User.t(), User.t()) ::
2140 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2141 defp remove_from_block(%User{} = user, %User{} = blocked) do
2142 UserRelationship.delete_block(user, blocked)
2143 end
2144
2145 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2146 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2147 {:ok, user_notification_mute} <-
2148 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2149 {:ok, nil} do
2150 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2151 end
2152 end
2153
2154 defp remove_from_mutes(user, %User{} = muted_user) do
2155 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2156 {:ok, user_notification_mute} <-
2157 UserRelationship.delete_notification_mute(user, muted_user) do
2158 {:ok, [user_mute, user_notification_mute]}
2159 end
2160 end
2161
2162 def set_invisible(user, invisible) do
2163 params = %{invisible: invisible}
2164
2165 user
2166 |> cast(params, [:invisible])
2167 |> validate_required([:invisible])
2168 |> update_and_set_cache()
2169 end
2170
2171 def sanitize_html(%User{} = user) do
2172 sanitize_html(user, nil)
2173 end
2174
2175 # User data that mastodon isn't filtering (treated as plaintext):
2176 # - field name
2177 # - display name
2178 def sanitize_html(%User{} = user, filter) do
2179 fields =
2180 user
2181 |> User.fields()
2182 |> Enum.map(fn %{"name" => name, "value" => value} ->
2183 %{
2184 "name" => name,
2185 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2186 }
2187 end)
2188
2189 user
2190 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2191 |> Map.put(:fields, fields)
2192 end
2193 end