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