user.ex 78.4 KB
Newer Older
1
# Pleroma: A lightweight social networking server
Sean King's avatar
Sean King committed
2
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3
4
# SPDX-License-Identifier: AGPL-3.0-only

lain's avatar
lain committed
5
6
defmodule Pleroma.User do
  use Ecto.Schema
7

Haelwenn's avatar
Haelwenn committed
8
9
  import Ecto.Changeset
  import Ecto.Query
10
  import Ecto, only: [assoc: 2]
11
  import Pleroma.Webhook.Notify, only: [trigger_webhooks: 2]
Haelwenn's avatar
Haelwenn committed
12

13
  alias Ecto.Multi
14
  alias Pleroma.Activity
15
  alias Pleroma.Config
16
  alias Pleroma.Conversation.Participation
17
  alias Pleroma.Delivery
18
  alias Pleroma.EctoType.ActivityPub.ObjectValidators
19
  alias Pleroma.Emoji
20
  alias Pleroma.FollowingRelationship
21
  alias Pleroma.Formatter
Haelwenn's avatar
Haelwenn committed
22
  alias Pleroma.HTML
23
  alias Pleroma.Keys
24
  alias Pleroma.MFA
25
26
  alias Pleroma.Notification
  alias Pleroma.Object
27
  alias Pleroma.Registration
Haelwenn's avatar
Haelwenn committed
28
29
  alias Pleroma.Repo
  alias Pleroma.User
30
  alias Pleroma.UserRelationship
31
  alias Pleroma.Web.ActivityPub.ActivityPub
32
33
  alias Pleroma.Web.ActivityPub.Builder
  alias Pleroma.Web.ActivityPub.Pipeline
34
  alias Pleroma.Web.ActivityPub.Utils
35
  alias Pleroma.Web.CommonAPI
Maxim Filippov's avatar
Maxim Filippov committed
36
  alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
37
  alias Pleroma.Web.Endpoint
Haelwenn's avatar
Haelwenn committed
38
  alias Pleroma.Web.OAuth
39
  alias Pleroma.Web.RelMe
40
  alias Pleroma.Webhook
41
  alias Pleroma.Workers.BackgroundWorker
lain's avatar
lain committed
42

43
44
  require Logger

Maksim's avatar
Maksim committed
45
  @type t :: %__MODULE__{}
46
47
48
49
50
51
  @type account_status ::
          :active
          | :deactivated
          | :password_reset_pending
          | :confirmation_pending
          | :approval_pending
52
  @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
href's avatar
href committed
53

54
  # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
href's avatar
href committed
55
56
57
  @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])?)*$/

  @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
href's avatar
href committed
58
  @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
href's avatar
href committed
59

60
  # AP ID user relationships (blocks, mutes etc.)
61
62
  # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
  @user_relationships_config [
63
64
65
66
67
68
69
70
    block: [
      blocker_blocks: :blocked_users,
      blockee_blocks: :blocker_users
    ],
    mute: [
      muter_mutes: :muted_users,
      mutee_mutes: :muter_users
    ],
71
72
73
74
75
76
77
    reblog_mute: [
      reblog_muter_mutes: :reblog_muted_users,
      reblog_mutee_mutes: :reblog_muter_users
    ],
    notification_mute: [
      notification_muter_mutes: :notification_muted_users,
      notification_mutee_mutes: :notification_muter_users
78
79
80
81
82
    ],
    # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
    inverse_subscription: [
      subscribee_subscriptions: :subscriber_users,
      subscriber_subscriptions: :subscribee_users
marcin mikołajczak's avatar
marcin mikołajczak committed
83
84
85
86
    ],
    endorsement: [
      endorser_endorsements: :endorsed_users,
      endorsee_endorsements: :endorser_users
marcin mikołajczak's avatar
marcin mikołajczak committed
87
    ]
88
89
  ]

90
91
  @cachex Pleroma.Config.get([:cachex, :provider], Cachex)

lain's avatar
lain committed
92
  schema "users" do
93
    field(:bio, :string, default: "")
94
    field(:raw_bio, :string)
lain's avatar
lain committed
95
96
97
98
99
100
    field(:email, :string)
    field(:name, :string)
    field(:nickname, :string)
    field(:password_hash, :string)
    field(:password, :string, virtual: true)
    field(:password_confirmation, :string, virtual: true)
rinpatch's avatar
rinpatch committed
101
    field(:keys, :string)
102
    field(:public_key, :string)
lain's avatar
lain committed
103
    field(:ap_id, :string)
104
    field(:avatar, :map, default: %{})
lain's avatar
lain committed
105
106
    field(:local, :boolean, default: true)
    field(:follower_address, :string)
107
    field(:following_address, :string)
Alexander Strizhakov's avatar
Alexander Strizhakov committed
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: [])
rinpatch's avatar
rinpatch committed
112
    field(:last_refreshed_at, :naive_datetime_usec)
Roman Chvanikov's avatar
Roman Chvanikov committed
113
    field(:last_digest_emailed_at, :naive_datetime)
114
115
116
117
    field(:banner, :map, default: %{})
    field(:background, :map, default: %{})
    field(:note_count, :integer, default: 0)
    field(:follower_count, :integer, default: 0)
minibikini's avatar
minibikini committed
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
125
126
    field(:confirmation_token, :string, default: nil)
    field(:default_scope, :string, default: "public")
    field(:domain_blocks, {:array, :string}, default: [])
127
    field(:is_active, :boolean, default: true)
128
129
130
131
132
    field(:no_rich_text, :boolean, default: false)
    field(:ap_enabled, :boolean, default: false)
    field(:is_moderator, :boolean, default: false)
    field(:is_admin, :boolean, default: false)
    field(:show_role, :boolean, default: true)
133
    field(:uri, ObjectValidators.Uri, default: nil)
134
135
136
137
138
139
140
    field(:hide_followers_count, :boolean, default: false)
    field(:hide_follows_count, :boolean, default: false)
    field(:hide_followers, :boolean, default: false)
    field(:hide_follows, :boolean, default: false)
    field(:hide_favorites, :boolean, default: true)
    field(:email_notifications, :map, default: %{"digest" => false})
    field(:mascot, :map, default: nil)
141
    field(:emoji, :map, default: %{})
142
    field(:pleroma_settings_store, :map, default: %{})
143
    field(:fields, {:array, :map}, default: [])
144
    field(:raw_fields, {:array, :map}, default: [])
145
    field(:is_discoverable, :boolean, default: false)
146
    field(:invisible, :boolean, default: false)
147
    field(:allow_following_move, :boolean, default: true)
148
    field(:skip_thread_containment, :boolean, default: false)
149
    field(:actor_type, :string, default: "Person")
150
    field(:also_known_as, {:array, ObjectValidators.ObjectID}, default: [])
151
152
    field(:inbox, :string)
    field(:shared_inbox, :string)
153
    field(:accepts_chat_messages, :boolean, default: nil)
minibikini's avatar
minibikini committed
154
    field(:last_active_at, :naive_datetime)
155
    field(:disclose_client, :boolean, default: true)
Alexander Strizhakov's avatar
Alexander Strizhakov committed
156
    field(:pinned_objects, :map, default: %{})
157
    field(:is_suggested, :boolean, default: false)
158
    field(:last_status_at, :naive_datetime)
159
    field(:birthday, :date)
160
    field(:show_birthday, :boolean, default: false)
161
    field(:language, :string)
162

Maksim's avatar
Maksim committed
163
164
165
166
    embeds_one(
      :notification_settings,
      Pleroma.User.NotificationSetting,
      on_replace: :update
167
168
    )

lain's avatar
lain committed
169
    has_many(:notifications, Notification)
170
    has_many(:registrations, Registration)
171
    has_many(:deliveries, Delivery)
172

173
174
175
    has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
    has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)

176
177
178
179
180
    for {relationship_type,
         [
           {outgoing_relation, outgoing_relation_target},
           {incoming_relation, incoming_relation_source}
         ]} <- @user_relationships_config do
181
      # Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
marcin mikołajczak's avatar
marcin mikołajczak committed
182
      #   :notification_muter_mutes, :subscribee_subscriptions, :endorser_endorsements
183
184
185
186
      has_many(outgoing_relation, UserRelationship,
        foreign_key: :source_id,
        where: [relationship_type: relationship_type]
      )
187

188
      # Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
marcin mikołajczak's avatar
marcin mikołajczak committed
189
      #   :notification_mutee_mutes, :subscriber_subscriptions, :endorsee_endorsements
190
191
192
193
      has_many(incoming_relation, UserRelationship,
        foreign_key: :target_id,
        where: [relationship_type: relationship_type]
      )
194

195
      # Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
marcin mikołajczak's avatar
marcin mikołajczak committed
196
      #   :notification_muted_users, :subscriber_users, :endorsed_users
197
      has_many(outgoing_relation_target, through: [outgoing_relation, :target])
198

199
      # Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
marcin mikołajczak's avatar
marcin mikołajczak committed
200
      #   :notification_muter_users, :subscribee_users, :endorser_users
201
202
      has_many(incoming_relation_source, through: [incoming_relation, :source])
    end
203

204
205
    # `:blocks` is deprecated (replaced with `blocked_users` relation)
    field(:blocks, {:array, :string}, default: [])
206
207
    # `:mutes` is deprecated (replaced with `muted_users` relation)
    field(:mutes, {:array, :string}, default: [])
208
209
210
211
    # `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
    field(:muted_reblogs, {:array, :string}, default: [])
    # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
    field(:muted_notifications, {:array, :string}, default: [])
212
213
    # `:subscribers` is deprecated (replaced with `subscriber_users` relation)
    field(:subscribers, {:array, :string}, default: [])
lain's avatar
lain committed
214

215
216
217
218
219
220
    embeds_one(
      :multi_factor_authentication_settings,
      MFA.Settings,
      on_replace: :delete
    )

lain's avatar
lain committed
221
222
    timestamps()
  end
lain's avatar
lain committed
223

224
225
  for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
        @user_relationships_config do
226
227
    # `def blocked_users_relation/2`, `def muted_users_relation/2`,
    #   `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
marcin mikołajczak's avatar
marcin mikołajczak committed
228
    #   `def subscriber_users/2`, `def endorsed_users_relation/2`
229
230
231
232
    def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
      target_users_query = assoc(user, unquote(outgoing_relation_target))

      if restrict_deactivated? do
feld's avatar
feld committed
233
234
        target_users_query
        |> User.Query.build(%{deactivated: false})
235
236
237
238
239
      else
        target_users_query
      end
    end

240
    # `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
marcin mikołajczak's avatar
marcin mikołajczak committed
241
    #   `def notification_muted_users/2`, `def subscriber_users/2`, `def endorsed_users/2`
242
243
244
245
246
247
248
249
250
    def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
      __MODULE__
      |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
        user,
        restrict_deactivated?
      ])
      |> Repo.all()
    end

251
    # `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
marcin mikołajczak's avatar
marcin mikołajczak committed
252
253
    #   `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`,
    #   `def endorsed_users_ap_ids/2`
254
255
256
257
258
259
260
261
262
263
264
    def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
      __MODULE__
      |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
        user,
        restrict_deactivated?
      ])
      |> select([u], u.ap_id)
      |> Repo.all()
    end
  end

265
  def cached_blocked_users_ap_ids(user) do
266
    @cachex.fetch!(:user_cache, "blocked_users_ap_ids:#{user.ap_id}", fn _ ->
267
268
269
270
271
      blocked_users_ap_ids(user)
    end)
  end

  def cached_muted_users_ap_ids(user) do
272
    @cachex.fetch!(:user_cache, "muted_users_ap_ids:#{user.ap_id}", fn _ ->
273
274
275
276
      muted_users_ap_ids(user)
    end)
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
277
278
279
280
281
282
283
  defdelegate following_count(user), to: FollowingRelationship
  defdelegate following(user), to: FollowingRelationship
  defdelegate following?(follower, followed), to: FollowingRelationship
  defdelegate following_ap_ids(user), to: FollowingRelationship
  defdelegate get_follow_requests(user), to: FollowingRelationship
  defdelegate search(query, opts \\ []), to: User.Search

284
285
286
287
  @doc """
  Dumps Flake Id to SQL-compatible format (16-byte UUID).
  E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
  """
288
289
290
291
292
293
294
295
296
297
298
299
300
301
  def binary_id(source_id) when is_binary(source_id) do
    with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
      dumped_id
    else
      _ -> source_id
    end
  end

  def binary_id(source_ids) when is_list(source_ids) do
    Enum.map(source_ids, &binary_id/1)
  end

  def binary_id(%User{} = user), do: binary_id(user.id)

302
303
  @doc "Returns status account"
  @spec account_status(User.t()) :: account_status()
304
  def account_status(%User{is_active: false}), do: :deactivated
305
  def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
306
  def account_status(%User{local: true, is_approved: false}), do: :approval_pending
307
  def account_status(%User{local: true, is_confirmed: false}), do: :confirmation_pending
308
  def account_status(%User{}), do: :active
309

Alexander Strizhakov's avatar
Alexander Strizhakov committed
310
  @spec visible_for(User.t(), User.t() | nil) ::
311
          :visible
Alexander Strizhakov's avatar
Alexander Strizhakov committed
312
313
314
315
316
          | :invisible
          | :restricted_unauthenticated
          | :deactivated
          | :confirmation_pending
  def visible_for(user, for_user \\ nil)
317

Alexander Strizhakov's avatar
Alexander Strizhakov committed
318
  def visible_for(%User{invisible: true}, _), do: :invisible
319

320
  def visible_for(%User{id: user_id}, %User{id: user_id}), do: :visible
321

Alexander Strizhakov's avatar
Alexander Strizhakov committed
322
323
324
325
326
327
328
  def visible_for(%User{} = user, nil) do
    if restrict_unauthenticated?(user) do
      :restrict_unauthenticated
    else
      visible_account_status(user)
    end
  end
329

Alexander Strizhakov's avatar
Alexander Strizhakov committed
330
  def visible_for(%User{} = user, for_user) do
331
332
333
334
335
    if superuser?(for_user) do
      :visible
    else
      visible_account_status(user)
    end
336
  end
337

338
  def visible_for(_, _), do: :invisible
Alexander Strizhakov's avatar
Alexander Strizhakov committed
339

340
341
342
  defp restrict_unauthenticated?(%User{local: true}) do
    Config.restrict_unauthenticated_access?(:profiles, :local)
  end
Alexander Strizhakov's avatar
Alexander Strizhakov committed
343

344
345
  defp restrict_unauthenticated?(%User{local: _}) do
    Config.restrict_unauthenticated_access?(:profiles, :remote)
346
347
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
348
349
  defp visible_account_status(user) do
    status = account_status(user)
350
351
352
353
354
355

    if status in [:active, :password_reset_pending] do
      :visible
    else
      status
    end
Alexander Strizhakov's avatar
Alexander Strizhakov committed
356
  end
357

358
  @spec superuser?(User.t()) :: boolean()
359
360
  def superuser?(%User{local: true, is_admin: true}), do: true
  def superuser?(%User{local: true, is_moderator: true}), do: true
361
  def superuser?(_), do: false
362

363
  @spec invisible?(User.t()) :: boolean()
364
  def invisible?(%User{invisible: true}), do: true
kaniini's avatar
kaniini committed
365
366
  def invisible?(_), do: false

367
  def avatar_url(user, options \\ []) do
lain's avatar
lain committed
368
    case user.avatar do
369
370
371
372
373
      %{"url" => [%{"href" => href} | _]} ->
        href

      _ ->
        unless options[:no_default] do
374
          Config.get([:assets, :default_user_avatar], "#{Endpoint.url()}/images/avi.png")
375
        end
lain's avatar
lain committed
376
377
378
    end
  end

379
  def banner_url(user, options \\ []) do
380
    case user.banner do
lain's avatar
lain committed
381
      %{"url" => [%{"href" => href} | _]} -> href
382
      _ -> !options[:no_default] && "#{Endpoint.url()}/images/banner.png"
lain's avatar
lain committed
383
384
385
    end
  end

386
  # Should probably be renamed or removed
Alexander Strizhakov's avatar
Alexander Strizhakov committed
387
  @spec ap_id(User.t()) :: String.t()
388
  def ap_id(%User{nickname: nickname}), do: "#{Endpoint.url()}/users/#{nickname}"
lain's avatar
lain committed
389

Alexander Strizhakov's avatar
Alexander Strizhakov committed
390
  @spec ap_followers(User.t()) :: String.t()
391
392
  def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
  def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
lain's avatar
lain committed
393

394
  @spec ap_following(User.t()) :: String.t()
395
396
397
  def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
  def ap_following(%User{} = user), do: "#{ap_id(user)}/following"

Alexander Strizhakov's avatar
Alexander Strizhakov committed
398
399
400
401
402
  @spec ap_featured_collection(User.t()) :: String.t()
  def ap_featured_collection(%User{featured_address: fa}) when is_binary(fa), do: fa

  def ap_featured_collection(%User{} = user), do: "#{ap_id(user)}/collections/featured"

403
404
405
406
407
408
409
410
  defp truncate_fields_param(params) do
    if Map.has_key?(params, :fields) do
      Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
    else
      params
    end
  end

411
  defp truncate_if_exists(params, key, max_length) do
Sadposter's avatar
Sadposter committed
412
    if Map.has_key?(params, key) and is_binary(params[key]) do
413
414
415
416
417
418
419
      {value, _chopped} = String.split_at(params[key], max_length)
      Map.put(params, key, value)
    else
      params
    end
  end

420
421
422
423
424
425
426
  defp fix_follower_address(%{follower_address: _, following_address: _} = params), do: params

  defp fix_follower_address(%{nickname: nickname} = params),
    do: Map.put(params, :follower_address, ap_followers(%User{nickname: nickname}))

  defp fix_follower_address(params), do: params

427
  def remote_user_changeset(struct \\ %User{local: false}, params) do
feld's avatar
feld committed
428
429
    bio_limit = Config.get([:instance, :user_bio_length], 5000)
    name_limit = Config.get([:instance, :user_name_length], 100)
lain's avatar
lain committed
430

rinpatch's avatar
rinpatch committed
431
432
433
434
435
436
    name =
      case params[:name] do
        name when is_binary(name) and byte_size(name) > 0 -> name
        _ -> params[:nickname]
      end

437
438
    params =
      params
rinpatch's avatar
rinpatch committed
439
      |> Map.put(:name, name)
440
      |> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())
441
442
      |> truncate_if_exists(:name, name_limit)
      |> truncate_if_exists(:bio, bio_limit)
443
      |> truncate_fields_param()
444
      |> fix_follower_address()
445

446
    struct
447
448
449
450
    |> cast(
      params,
      [
        :bio,
451
        :emoji,
452
        :ap_id,
453
454
        :inbox,
        :shared_inbox,
455
        :nickname,
456
        :public_key,
457
458
459
        :avatar,
        :ap_enabled,
        :banner,
460
        :is_locked,
461
        :last_refreshed_at,
462
463
464
        :uri,
        :follower_address,
        :following_address,
Alexander Strizhakov's avatar
Alexander Strizhakov committed
465
        :featured_address,
466
467
468
469
470
471
472
        :hide_followers,
        :hide_follows,
        :hide_followers_count,
        :hide_follows_count,
        :follower_count,
        :fields,
        :following_count,
473
        :is_discoverable,
474
475
        :invisible,
        :actor_type,
476
        :also_known_as,
Alexander Strizhakov's avatar
Alexander Strizhakov committed
477
        :accepts_chat_messages,
478
        :pinned_objects,
479
480
        :birthday,
        :show_birthday
481
482
      ]
    )
483
484
485
    |> cast(params, [:name], empty_values: [])
    |> validate_required([:ap_id])
    |> validate_required([:name], trim: false)
486
487
488
489
490
    |> unique_constraint(:nickname)
    |> validate_format(:nickname, @email_regex)
    |> validate_length(:bio, max: bio_limit)
    |> validate_length(:name, max: name_limit)
    |> validate_fields(true)
491
492
493
494
495
496
497
498
499
500
501
502
    |> validate_non_local()
  end

  defp validate_non_local(cng) do
    local? = get_field(cng, :local)

    if local? do
      cng
      |> add_error(:local, "User is local, can't update with this changeset.")
    else
      cng
    end
lain's avatar
lain committed
503
504
  end

lain's avatar
lain committed
505
  def update_changeset(struct, params \\ %{}) do
feld's avatar
feld committed
506
507
    bio_limit = Config.get([:instance, :user_bio_length], 5000)
    name_limit = Config.get([:instance, :user_name_length], 100)
508

Thog's avatar
Thog committed
509
    struct
510
511
512
513
    |> cast(
      params,
      [
        :bio,
514
        :raw_bio,
515
        :name,
516
        :emoji,
517
        :avatar,
518
        :public_key,
519
520
        :inbox,
        :shared_inbox,
521
        :is_locked,
522
523
524
525
526
527
528
529
        :no_rich_text,
        :default_scope,
        :banner,
        :hide_follows,
        :hide_followers,
        :hide_followers_count,
        :hide_follows_count,
        :hide_favorites,
530
        :allow_following_move,
Alex Gleason's avatar
Alex Gleason committed
531
        :also_known_as,
532
533
534
535
536
537
        :background,
        :show_role,
        :skip_thread_containment,
        :fields,
        :raw_fields,
        :pleroma_settings_store,
538
        :is_discoverable,
539
        :actor_type,
feld's avatar
feld committed
540
        :accepts_chat_messages,
541
        :disclose_client,
542
        :birthday,
543
        :show_birthday
544
545
      ]
    )
546
    |> validate_min_age()
lain's avatar
lain committed
547
    |> unique_constraint(:nickname)
href's avatar
href committed
548
    |> validate_format(:nickname, local_nickname_regex())
549
550
    |> validate_length(:bio, max: bio_limit)
    |> validate_length(:name, min: 1, max: name_limit)
551
    |> validate_inclusion(:actor_type, ["Person", "Service"])
552
    |> put_fields()
553
    |> put_emoji()
554
555
556
557
558
559
560
561
    |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
    |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
    |> put_change_if_present(:banner, &put_upload(&1, :banner))
    |> put_change_if_present(:background, &put_upload(&1, :background))
    |> put_change_if_present(
      :pleroma_settings_store,
      &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
    )
562
    |> validate_fields(false)
lain's avatar
lain committed
563
564
  end

565
566
567
568
569
570
571
572
  defp put_fields(changeset) do
    if raw_fields = get_change(changeset, :raw_fields) do
      raw_fields =
        raw_fields
        |> Enum.filter(fn %{"name" => n} -> n != "" end)

      fields =
        raw_fields
573
        |> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
574
575
576
577
578
579
580
581
582

      changeset
      |> put_change(:raw_fields, raw_fields)
      |> put_change(:fields, fields)
    else
      changeset
    end
  end

583
584
585
586
587
588
  defp parse_fields(value) do
    value
    |> Formatter.linkify(mentions_format: :full)
    |> elem(0)
  end

589
  defp put_emoji(changeset) do
590
591
592
593
594
595
596
597
598
599
600
601
602
603
    emojified_fields = [:bio, :name, :raw_fields]

    if Enum.any?(changeset.changes, fn {k, _} -> k in emojified_fields end) do
      bio = Emoji.Formatter.get_emoji_map(get_field(changeset, :bio))
      name = Emoji.Formatter.get_emoji_map(get_field(changeset, :name))

      emoji = Map.merge(bio, name)

      emoji =
        changeset
        |> get_field(:raw_fields)
        |> Enum.reduce(emoji, fn x, acc ->
          Map.merge(acc, Emoji.Formatter.get_emoji_map(x["name"] <> x["value"]))
        end)
604
605
606
607
608
609
610

      put_change(changeset, :emoji, emoji)
    else
      changeset
    end
  end

611
  defp put_change_if_present(changeset, map_field, value_function) do
612
613
614
    with {:ok, value} <- fetch_change(changeset, map_field),
         {:ok, new_value} <- value_function.(value) do
      put_change(changeset, map_field, new_value)
615
    else
616
      _ -> changeset
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
    end
  end

  defp put_upload(value, type) do
    with %Plug.Upload{} <- value,
         {:ok, object} <- ActivityPub.upload(value, type: type) do
      {:ok, object.data}
    end
  end

  def update_as_admin_changeset(struct, params) do
    struct
    |> update_changeset(params)
    |> cast(params, [:email])
    |> delete_change(:also_known_as)
    |> unique_constraint(:email)
    |> validate_format(:email, @email_regex)
Alexander Strizhakov's avatar
Alexander Strizhakov committed
634
    |> validate_inclusion(:actor_type, ["Person", "Service"])
635
636
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
637
  @spec update_as_admin(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
638
639
640
641
642
643
644
645
646
647
648
  def update_as_admin(user, params) do
    params = Map.put(params, "password_confirmation", params["password"])
    changeset = update_as_admin_changeset(user, params)

    if params["password"] do
      reset_password(user, changeset, params)
    else
      User.update_and_set_cache(changeset)
    end
  end

Roger Braun's avatar
Roger Braun committed
649
  def password_update_changeset(struct, params) do
650
651
652
653
    struct
    |> cast(params, [:password, :password_confirmation])
    |> validate_required([:password, :password_confirmation])
    |> validate_confirmation(:password)
654
655
    |> put_password_hash()
    |> put_change(:password_reset_pending, false)
656
657
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
658
  @spec reset_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
659
660
661
662
663
  def reset_password(%User{} = user, params) do
    reset_password(user, user, params)
  end

  def reset_password(%User{id: user_id} = user, struct, params) do
664
665
    multi =
      Multi.new()
666
      |> Multi.update(:user, password_update_changeset(struct, params))
667
668
669
670
671
672
      |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
      |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))

    case Repo.transaction(multi) do
      {:ok, %{user: user} = _} -> set_cache(user)
      {:error, _, changeset, _} -> {:error, changeset}
Roger Braun's avatar
Roger Braun committed
673
674
675
    end
  end

676
677
678
679
680
681
682
  def update_password_reset_pending(user, value) do
    user
    |> change()
    |> put_change(:password_reset_pending, value)
    |> update_and_set_cache()
  end

683
684
685
686
687
  def force_password_reset_async(user) do
    BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
  end

  @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
688
  def force_password_reset(user), do: update_password_reset_pending(user, true)
689

690
  # Used to auto-register LDAP accounts which won't have a password hash stored locally
691
  def register_changeset_ldap(struct, params = %{password: password})
692
693
694
      when is_nil(password) do
    params = Map.put_new(params, :accepts_chat_messages, true)

695
696
697
698
699
700
701
    params =
      if Map.has_key?(params, :email) do
        Map.put_new(params, :email, params[:email])
      else
        params
      end

702
703
704
705
    struct
    |> cast(params, [
      :name,
      :nickname,
706
      :email,
707
708
      :accepts_chat_messages
    ])
709
    |> validate_required([:name, :nickname])
710
711
712
713
714
    |> unique_constraint(:nickname)
    |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
    |> validate_format(:nickname, local_nickname_regex())
    |> put_ap_id()
    |> unique_constraint(:ap_id)
Alexander Strizhakov's avatar
Alexander Strizhakov committed
715
    |> put_following_and_follower_and_featured_address()
716
717
  end

718
  def register_changeset(struct, params \\ %{}, opts \\ []) do
feld's avatar
feld committed
719
720
    bio_limit = Config.get([:instance, :user_bio_length], 5000)
    name_limit = Config.get([:instance, :user_name_length], 100)
721
    reason_limit = Config.get([:instance, :registration_reason_length], 500)
722
    params = Map.put_new(params, :accepts_chat_messages, true)
723

724
725
726
    confirmed? =
      if is_nil(opts[:confirmed]) do
        !Config.get([:instance, :account_activation_required])
727
      else
728
        opts[:confirmed]
729
730
      end

731
732
733
    approved? =
      if is_nil(opts[:approved]) do
        !Config.get([:instance, :account_approval_required])
734
      else
735
        opts[:approved]
736
737
      end

minibikini's avatar
minibikini committed
738
    struct
739
    |> confirmation_changeset(set_confirmation: confirmed?)
740
    |> approval_changeset(set_approval: approved?)
741
742
743
744
745
746
747
748
    |> cast(params, [
      :bio,
      :raw_bio,
      :email,
      :name,
      :nickname,
      :password,
      :password_confirmation,
749
      :emoji,
750
      :accepts_chat_messages,
751
      :registration_reason,
752
753
      :birthday,
      :language
754
    ])
minibikini's avatar
minibikini committed
755
756
757
    |> validate_required([:name, :nickname, :password, :password_confirmation])
    |> validate_confirmation(:password)
    |> unique_constraint(:email)
758
759
760
761
762
763
764
765
    |> validate_format(:email, @email_regex)
    |> validate_change(:email, fn :email, email ->
      valid? =
        Config.get([User, :email_blacklist])
        |> Enum.all?(fn blacklisted_domain ->
          !String.ends_with?(email, ["@" <> blacklisted_domain, "." <> blacklisted_domain])
        end)

766
      if valid?, do: [], else: [email: "Invalid email"]
767
    end)
minibikini's avatar
minibikini committed
768
    |> unique_constraint(:nickname)
feld's avatar
feld committed
769
    |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
minibikini's avatar
minibikini committed
770
771
772
    |> validate_format(:nickname, local_nickname_regex())
    |> validate_length(:bio, max: bio_limit)
    |> validate_length(:name, min: 1, max: name_limit)
773
    |> validate_length(:registration_reason, max: reason_limit)
minibikini's avatar
minibikini committed
774
    |> maybe_validate_required_email(opts[:external])
775
    |> maybe_validate_required_birthday
776
    |> validate_min_age()
minibikini's avatar
minibikini committed
777
778
779
    |> put_password_hash
    |> put_ap_id()
    |> unique_constraint(:ap_id)
Alexander Strizhakov's avatar
Alexander Strizhakov committed
780
    |> put_following_and_follower_and_featured_address()
minibikini's avatar
minibikini committed
781
  end
Ivan Tashkinov's avatar
Ivan Tashkinov committed
782

minibikini's avatar
minibikini committed
783
  def maybe_validate_required_email(changeset, true), do: changeset
784
785

  def maybe_validate_required_email(changeset, _) do
feld's avatar
feld committed
786
    if Config.get([:instance, :account_activation_required]) do
787
788
789
790
791
      validate_required(changeset, [:email])
    else
      changeset
    end
  end
lain's avatar
lain committed
792

793
794
795
  defp maybe_validate_required_birthday(changeset) do
    if Config.get([:instance, :birthday_required]) do
      validate_required(changeset, [:birthday])
796
797
798
799
800
801
802
    else
      changeset
    end
  end

  defp validate_min_age(changeset) do
    changeset
803
    |> validate_change(:birthday, fn :birthday, birthday ->
804
805
      valid? =
        Date.utc_today()
806
807
        |> Date.diff(birthday) >=
          Config.get([:instance, :birthday_min_age])
808

809
      if valid?, do: [], else: [birthday: "Invalid age"]
810
811
812
    end)
  end

minibikini's avatar
minibikini committed
813
814
815
816
  defp put_ap_id(changeset) do
    ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
    put_change(changeset, :ap_id, ap_id)
  end
817

Alexander Strizhakov's avatar
Alexander Strizhakov committed
818
819
820
821
822
  defp put_following_and_follower_and_featured_address(changeset) do
    user = %User{nickname: get_field(changeset, :nickname)}
    followers = ap_followers(user)
    following = ap_following(user)
    featured = ap_featured_collection(user)
lain's avatar
lain committed
823

minibikini's avatar
minibikini committed
824
825
    changeset
    |> put_change(:follower_address, followers)
Alexander Strizhakov's avatar
Alexander Strizhakov committed
826
827
    |> put_change(:following_address, following)
    |> put_change(:featured_address, featured)
lain's avatar
lain committed
828
829
  end

830
  defp autofollow_users(user) do
feld's avatar
feld committed
831
    candidates = Config.get([:instance, :autofollowed_nicknames])
832
833

    autofollowed_users =
834
      User.Query.build(%{nickname: candidates, local: true, is_active: true})
835
836
      |> Repo.all()

lain's avatar
lain committed
837
    follow_all(user, autofollowed_users)
838
839
  end

840
841
842
843
844
845
846
847
848
849
  defp autofollowing_users(user) do
    candidates = Config.get([:instance, :autofollowing_nicknames])

    User.Query.build(%{nickname: candidates, local: true, deactivated: false})
    |> Repo.all()
    |> Enum.each(&follow(&1, user, :follow_accept))

    {:ok, :success}
  end

850
851
  @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
  def register(%Ecto.Changeset{} = changeset) do
minibikini's avatar
minibikini committed
852
853
    with {:ok, user} <- Repo.insert(changeset) do
      post_register_action(user)
854
      trigger_webhooks(user, :"account.created")
855
856
857
    end
  end

858
  def post_register_action(%User{is_confirmed: false} = user) do
feld's avatar
feld committed
859
    with {:ok, _} <- maybe_send_confirmation_email(user) do
860
861
862
863
      {:ok, user}
    end
  end

864
  def post_register_action(%User{is_approved: false} = user) do
865
866
867
868
869
870
    with {:ok, _} <- send_user_approval_email(user),
         {:ok, _} <- send_admin_approval_emails(user) do
      {:ok, user}
    end
  end

871
  def post_register_action(%User{is_approved: true, is_confirmed: true} = user) do
872
    with {:ok, user} <- autofollow_users(user),
873
         {:ok, _} <- autofollowing_users(user),
minibikini's avatar
minibikini committed
874
         {:ok, user} <- set_cache(user),
875
         {:ok, _} <- maybe_send_registration_email(user),
876
877
878
         {:ok, _} <- maybe_send_welcome_email(user),
         {:ok, _} <- maybe_send_welcome_message(user),
         {:ok, _} <- maybe_send_welcome_chat_message(user) do
879
880
881
882
      {:ok, user}
    end
  end

883
  defp send_user_approval_email(user) do
884
885
886
887
    user
    |> Pleroma.Emails.UserEmail.approval_pending_email()
    |> Pleroma.Emails.Mailer.deliver_async()

888
889
890
891
    {:ok, :enqueued}
  end

  defp send_admin_approval_emails(user) do
892
893
894
895
896
897
898
899
    all_superusers()
    |> Enum.filter(fn user -> not is_nil(user.email) end)
    |> Enum.each(fn superuser ->
      superuser
      |> Pleroma.Emails.AdminEmail.new_unapproved_registration(user)
      |> Pleroma.Emails.Mailer.deliver_async()
    end)

900
    {:ok, :enqueued}
901
902
  end

903
  defp maybe_send_welcome_message(user) do
Maksim's avatar
Maksim committed
904
905
906
907
908
909
910
911
    if User.WelcomeMessage.enabled?() do
      User.WelcomeMessage.post_message(user)
      {:ok, :enqueued}
    else
      {:ok, :noop}
    end
  end

912
  defp maybe_send_welcome_chat_message(user) do
Ilja's avatar
Ilja committed
913
914
915
916
917
918
919
920
    if User.WelcomeChatMessage.enabled?() do
      User.WelcomeChatMessage.post_message(user)
      {:ok, :enqueued}
    else
      {:ok, :noop}
    end
  end

921
  defp maybe_send_welcome_email(%User{email: email} = user) when is_binary(email) do
Maksim's avatar
Maksim committed
922
923
924
925
926
927
928
    if User.WelcomeEmail.enabled?() do
      User.WelcomeEmail.send_email(user)
      {:ok, :enqueued}
    else
      {:ok, :noop}
    end
  end
Maksim's avatar
Maksim committed
929

930
  defp maybe_send_welcome_email(_), do: {:ok, :noop}
931

feld's avatar
feld committed
932
933
  @spec maybe_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
  def maybe_send_confirmation_email(%User{is_confirmed: false, email: email} = user)
934
      when is_binary(email) do
935
936
    if Config.get([:instance, :account_activation_required]) do
      send_confirmation_email(user)
937
      {:ok, :enqueued}
938
939
940
941
942
    else
      {:ok, :noop}
    end
  end

feld's avatar
feld committed
943
  def maybe_send_confirmation_email(_), do: {:ok, :noop}
944
945
946
947
948
949
950
951

  @spec send_confirmation_email(Uset.t()) :: User.t()
  def send_confirmation_email(%User{} = user) do
    user
    |> Pleroma.Emails.UserEmail.account_confirmation_email()
    |> Pleroma.Emails.Mailer.deliver_async()

    user