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