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