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