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