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