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