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