user.ex 66.1 KB
Newer Older
1
# Pleroma: A lightweight social networking server
2
# Copyright © 2017-2020 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]
Haelwenn's avatar
Haelwenn committed
11

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

42 43
  require Logger

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

48
  # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
href's avatar
href committed
49 50 51
  @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
52
  @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
href's avatar
href committed
53

54
  # AP ID user relationships (blocks, mutes etc.)
55 56
  # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
  @user_relationships_config [
57 58 59 60 61 62 63 64
    block: [
      blocker_blocks: :blocked_users,
      blockee_blocks: :blocker_users
    ],
    mute: [
      muter_mutes: :muted_users,
      mutee_mutes: :muter_users
    ],
65 66 67 68 69 70 71
    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
72 73 74 75 76
    ],
    # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
    inverse_subscription: [
      subscribee_subscriptions: :subscriber_users,
      subscriber_subscriptions: :subscribee_users
77 78 79
    ]
  ]

lain's avatar
lain committed
80
  schema "users" do
lain's avatar
lain committed
81 82 83 84 85 86 87
    field(:bio, :string)
    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
88
    field(:keys, :string)
89
    field(:public_key, :string)
lain's avatar
lain committed
90 91 92 93
    field(:ap_id, :string)
    field(:avatar, :map)
    field(:local, :boolean, default: true)
    field(:follower_address, :string)
94
    field(:following_address, :string)
95
    field(:search_rank, :float, virtual: true)
96
    field(:search_type, :integer, virtual: true)
97
    field(:tags, {:array, :string}, default: [])
rinpatch's avatar
rinpatch committed
98
    field(:last_refreshed_at, :naive_datetime_usec)
Roman Chvanikov's avatar
Roman Chvanikov committed
99
    field(:last_digest_emailed_at, :naive_datetime)
100 101 102 103
    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
104
    field(:following_count, :integer, default: 0)
105 106 107 108 109 110 111 112 113 114 115 116 117
    field(:locked, :boolean, default: false)
    field(:confirmation_pending, :boolean, default: false)
    field(:password_reset_pending, :boolean, default: false)
    field(:confirmation_token, :string, default: nil)
    field(:default_scope, :string, default: "public")
    field(:domain_blocks, {:array, :string}, default: [])
    field(:deactivated, :boolean, default: false)
    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)
    field(:settings, :map, default: nil)
Haelwenn's avatar
Haelwenn committed
118
    field(:uri, Types.Uri, default: nil)
119 120 121 122 123 124 125 126 127
    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(:unread_conversation_count, :integer, default: 0)
    field(:pinned_activities, {:array, :string}, default: [])
    field(:email_notifications, :map, default: %{"digest" => false})
    field(:mascot, :map, default: nil)
128
    field(:emoji, :map, default: %{})
129
    field(:pleroma_settings_store, :map, default: %{})
130
    field(:fields, {:array, :map}, default: [])
131 132
    field(:raw_fields, {:array, :map}, default: [])
    field(:discoverable, :boolean, default: false)
133
    field(:invisible, :boolean, default: false)
134
    field(:allow_following_move, :boolean, default: true)
135
    field(:skip_thread_containment, :boolean, default: false)
136
    field(:actor_type, :string, default: "Person")
137
    field(:also_known_as, {:array, :string}, default: [])
138 139
    field(:inbox, :string)
    field(:shared_inbox, :string)
140

Maksim's avatar
Maksim committed
141 142 143 144
    embeds_one(
      :notification_settings,
      Pleroma.User.NotificationSetting,
      on_replace: :update
145 146
    )

lain's avatar
lain committed
147
    has_many(:notifications, Notification)
148
    has_many(:registrations, Registration)
149
    has_many(:deliveries, Delivery)
150

151 152 153
    has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
    has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)

154 155 156 157 158
    for {relationship_type,
         [
           {outgoing_relation, outgoing_relation_target},
           {incoming_relation, incoming_relation_source}
         ]} <- @user_relationships_config do
159 160
      # Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
      #   :notification_muter_mutes, :subscribee_subscriptions
161 162 163 164
      has_many(outgoing_relation, UserRelationship,
        foreign_key: :source_id,
        where: [relationship_type: relationship_type]
      )
165

166 167
      # Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
      #   :notification_mutee_mutes, :subscriber_subscriptions
168 169 170 171
      has_many(incoming_relation, UserRelationship,
        foreign_key: :target_id,
        where: [relationship_type: relationship_type]
      )
172

173 174
      # Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
      #   :notification_muted_users, :subscriber_users
175
      has_many(outgoing_relation_target, through: [outgoing_relation, :target])
176

177 178
      # Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
      #   :notification_muter_users, :subscribee_users
179 180
      has_many(incoming_relation_source, through: [incoming_relation, :source])
    end
181

182 183
    # `:blocks` is deprecated (replaced with `blocked_users` relation)
    field(:blocks, {:array, :string}, default: [])
184 185
    # `:mutes` is deprecated (replaced with `muted_users` relation)
    field(:mutes, {:array, :string}, default: [])
186 187 188 189
    # `: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: [])
190 191
    # `:subscribers` is deprecated (replaced with `subscriber_users` relation)
    field(:subscribers, {:array, :string}, default: [])
lain's avatar
lain committed
192

193 194 195 196 197 198
    embeds_one(
      :multi_factor_authentication_settings,
      MFA.Settings,
      on_replace: :delete
    )

lain's avatar
lain committed
199 200
    timestamps()
  end
lain's avatar
lain committed
201

202 203
  for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
        @user_relationships_config do
204 205 206
    # `def blocked_users_relation/2`, `def muted_users_relation/2`,
    #   `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
    #   `def subscriber_users/2`
207 208 209 210 211 212 213 214 215 216
    def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
      target_users_query = assoc(user, unquote(outgoing_relation_target))

      if restrict_deactivated? do
        restrict_deactivated(target_users_query)
      else
        target_users_query
      end
    end

217 218
    # `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
    #   `def notification_muted_users/2`, `def subscriber_users/2`
219 220 221 222 223 224 225 226 227
    def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
      __MODULE__
      |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
        user,
        restrict_deactivated?
      ])
      |> Repo.all()
    end

228 229
    # `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
    #   `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`
230 231 232 233 234 235 236 237 238 239 240
    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

241 242 243 244
  @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>>
  """
245 246 247 248 249 250 251 252 253 254 255 256 257 258
  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)

259 260 261 262
  @doc "Returns status account"
  @spec account_status(User.t()) :: account_status()
  def account_status(%User{deactivated: true}), do: :deactivated
  def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
263

264 265 266 267 268 269
  def account_status(%User{confirmation_pending: true}) do
    case Config.get([:instance, :account_activation_required]) do
      true -> :confirmation_pending
      _ -> :active
    end
  end
270

271
  def account_status(%User{}), do: :active
272

273
  @spec visible_for?(User.t(), User.t() | nil) :: boolean()
274 275
  def visible_for?(user, for_user \\ nil)

276 277
  def visible_for?(%User{invisible: true}, _), do: false

278 279 280 281 282 283 284 285 286 287 288 289
  def visible_for?(%User{id: user_id}, %User{id: user_id}), do: true

  def visible_for?(%User{local: local} = user, nil) do
    cfg_key =
      if local,
        do: :local,
        else: :remote

    if Config.get([:restrict_unauthenticated, :profiles, cfg_key]),
      do: false,
      else: account_status(user) == :active
  end
290 291

  def visible_for?(%User{} = user, for_user) do
292
    account_status(user) == :active || superuser?(for_user)
293 294
  end

295 296
  def visible_for?(_, _), do: false

297
  @spec superuser?(User.t()) :: boolean()
298 299
  def superuser?(%User{local: true, is_admin: true}), do: true
  def superuser?(%User{local: true, is_moderator: true}), do: true
300
  def superuser?(_), do: false
301

302
  @spec invisible?(User.t()) :: boolean()
303
  def invisible?(%User{invisible: true}), do: true
kaniini's avatar
kaniini committed
304 305
  def invisible?(_), do: false

306
  def avatar_url(user, options \\ []) do
lain's avatar
lain committed
307
    case user.avatar do
308 309 310 311 312 313 314
      %{"url" => [%{"href" => href} | _]} ->
        href

      _ ->
        unless options[:no_default] do
          Config.get([:assets, :default_user_avatar], "#{Web.base_url()}/images/avi.png")
        end
lain's avatar
lain committed
315 316 317
    end
  end

318
  def banner_url(user, options \\ []) do
319
    case user.banner do
lain's avatar
lain committed
320
      %{"url" => [%{"href" => href} | _]} -> href
321
      _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
lain's avatar
lain committed
322 323 324
    end
  end

325
  # Should probably be renamed or removed
minibikini's avatar
minibikini committed
326
  def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
lain's avatar
lain committed
327

328 329
  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
330

331
  @spec ap_following(User.t()) :: String.t()
332 333 334 335
  def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
  def ap_following(%User{} = user), do: "#{ap_id(user)}/following"

  @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
336
  def restrict_deactivated(query) do
337
    from(u in query, where: u.deactivated != ^true)
338 339
  end

340
  defdelegate following_count(user), to: FollowingRelationship
341

342 343 344 345 346 347 348 349
  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

350
  defp truncate_if_exists(params, key, max_length) do
Sadposter's avatar
Sadposter committed
351
    if Map.has_key?(params, key) and is_binary(params[key]) do
352 353 354 355 356 357 358
      {value, _chopped} = String.split_at(params[key], max_length)
      Map.put(params, key, value)
    else
      params
    end
  end

359 360 361 362 363 364 365
  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

366
  def remote_user_changeset(struct \\ %User{local: false}, params) do
367 368
    bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
    name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
lain's avatar
lain committed
369

rinpatch's avatar
rinpatch committed
370 371 372 373 374 375
    name =
      case params[:name] do
        name when is_binary(name) and byte_size(name) > 0 -> name
        _ -> params[:nickname]
      end

376 377
    params =
      params
rinpatch's avatar
rinpatch committed
378
      |> Map.put(:name, name)
379
      |> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())
380 381
      |> truncate_if_exists(:name, name_limit)
      |> truncate_if_exists(:bio, bio_limit)
382
      |> truncate_fields_param()
383
      |> fix_follower_address()
384

385
    struct
386 387 388 389 390
    |> cast(
      params,
      [
        :bio,
        :name,
391
        :emoji,
392
        :ap_id,
393 394
        :inbox,
        :shared_inbox,
395
        :nickname,
396
        :public_key,
397 398 399 400
        :avatar,
        :ap_enabled,
        :banner,
        :locked,
401
        :last_refreshed_at,
402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
        :uri,
        :follower_address,
        :following_address,
        :hide_followers,
        :hide_follows,
        :hide_followers_count,
        :hide_follows_count,
        :follower_count,
        :fields,
        :following_count,
        :discoverable,
        :invisible,
        :actor_type,
        :also_known_as
      ]
    )
    |> validate_required([:name, :ap_id])
    |> unique_constraint(:nickname)
    |> validate_format(:nickname, @email_regex)
    |> validate_length(:bio, max: bio_limit)
    |> validate_length(:name, max: name_limit)
    |> validate_fields(true)
lain's avatar
lain committed
424 425
  end

lain's avatar
lain committed
426
  def update_changeset(struct, params \\ %{}) do
427 428 429
    bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
    name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)

Thog's avatar
Thog committed
430
    struct
431 432 433 434 435
    |> cast(
      params,
      [
        :bio,
        :name,
436
        :emoji,
437
        :avatar,
438
        :public_key,
439 440
        :inbox,
        :shared_inbox,
441 442 443 444 445 446 447 448 449
        :locked,
        :no_rich_text,
        :default_scope,
        :banner,
        :hide_follows,
        :hide_followers,
        :hide_followers_count,
        :hide_follows_count,
        :hide_favorites,
450
        :allow_following_move,
451 452 453 454 455 456
        :background,
        :show_role,
        :skip_thread_containment,
        :fields,
        :raw_fields,
        :pleroma_settings_store,
457
        :discoverable,
458
        :actor_type,
459
        :also_known_as
460 461
      ]
    )
lain's avatar
lain committed
462
    |> unique_constraint(:nickname)
href's avatar
href committed
463
    |> validate_format(:nickname, local_nickname_regex())
464 465
    |> validate_length(:bio, max: bio_limit)
    |> validate_length(:name, min: 1, max: name_limit)
466
    |> put_fields()
467
    |> put_emoji()
468 469 470 471 472 473 474 475
    |> 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)}
    )
476
    |> validate_fields(false)
lain's avatar
lain committed
477 478
  end

479 480 481 482 483 484 485 486
  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
487
        |> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
488 489 490 491 492 493 494 495 496

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

497 498 499 500 501 502
  defp parse_fields(value) do
    value
    |> Formatter.linkify(mentions_format: :full)
    |> elem(0)
  end

503 504 505 506 507 508 509 510 511 512 513 514
  defp put_emoji(changeset) do
    bio = get_change(changeset, :bio)
    name = get_change(changeset, :name)

    if bio || name do
      emoji = Map.merge(Emoji.Formatter.get_emoji_map(bio), Emoji.Formatter.get_emoji_map(name))
      put_change(changeset, :emoji, emoji)
    else
      changeset
    end
  end

515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554
  defp put_change_if_present(changeset, map_field, value_function) do
    if value = get_change(changeset, map_field) do
      with {:ok, new_value} <- value_function.(value) do
        put_change(changeset, map_field, new_value)
      else
        _ -> changeset
      end
    else
      changeset
    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)
  end

  @spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
  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
555
  def password_update_changeset(struct, params) do
556 557 558 559
    struct
    |> cast(params, [:password, :password_confirmation])
    |> validate_required([:password, :password_confirmation])
    |> validate_confirmation(:password)
560 561
    |> put_password_hash()
    |> put_change(:password_reset_pending, false)
562 563
  end

Maksim's avatar
Maksim committed
564
  @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
565 566 567 568 569
  def reset_password(%User{} = user, params) do
    reset_password(user, user, params)
  end

  def reset_password(%User{id: user_id} = user, struct, params) do
570 571
    multi =
      Multi.new()
572
      |> Multi.update(:user, password_update_changeset(struct, params))
573 574 575 576 577 578
      |> 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
579 580 581
    end
  end

582 583 584 585 586 587 588
  def update_password_reset_pending(user, value) do
    user
    |> change()
    |> put_change(:password_reset_pending, value)
    |> update_and_set_cache()
  end

589 590 591 592 593
  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()}
594
  def force_password_reset(user), do: update_password_reset_pending(user, true)
595

596
  def register_changeset(struct, params \\ %{}, opts \\ []) do
597 598 599
    bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
    name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)

600 601 602
    need_confirmation? =
      if is_nil(opts[:need_confirmation]) do
        Pleroma.Config.get([:instance, :account_activation_required])
603
      else
604
        opts[:need_confirmation]
605 606
      end

minibikini's avatar
minibikini committed
607
    struct
608
    |> confirmation_changeset(need_confirmation: need_confirmation?)
609
    |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation, :emoji])
minibikini's avatar
minibikini committed
610 611 612 613 614 615 616 617 618 619 620 621 622 623 624
    |> validate_required([:name, :nickname, :password, :password_confirmation])
    |> validate_confirmation(:password)
    |> unique_constraint(:email)
    |> unique_constraint(:nickname)
    |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
    |> validate_format(:nickname, local_nickname_regex())
    |> validate_format(:email, @email_regex)
    |> validate_length(:bio, max: bio_limit)
    |> validate_length(:name, min: 1, max: name_limit)
    |> maybe_validate_required_email(opts[:external])
    |> put_password_hash
    |> put_ap_id()
    |> unique_constraint(:ap_id)
    |> put_following_and_follower_address()
  end
Ivan Tashkinov's avatar
Ivan Tashkinov committed
625

minibikini's avatar
minibikini committed
626
  def maybe_validate_required_email(changeset, true), do: changeset
627 628 629 630 631 632 633 634

  def maybe_validate_required_email(changeset, _) do
    if Pleroma.Config.get([:instance, :account_activation_required]) do
      validate_required(changeset, [:email])
    else
      changeset
    end
  end
lain's avatar
lain committed
635

minibikini's avatar
minibikini committed
636 637 638 639
  defp put_ap_id(changeset) do
    ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
    put_change(changeset, :ap_id, ap_id)
  end
640

minibikini's avatar
minibikini committed
641 642
  defp put_following_and_follower_address(changeset) do
    followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
lain's avatar
lain committed
643

minibikini's avatar
minibikini committed
644 645
    changeset
    |> put_change(:follower_address, followers)
lain's avatar
lain committed
646 647
  end

648 649 650 651
  defp autofollow_users(user) do
    candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])

    autofollowed_users =
652
      User.Query.build(%{nickname: candidates, local: true, deactivated: false})
653 654
      |> Repo.all()

lain's avatar
lain committed
655
    follow_all(user, autofollowed_users)
656 657
  end

658 659
  @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
  def register(%Ecto.Changeset{} = changeset) do
minibikini's avatar
minibikini committed
660 661
    with {:ok, user} <- Repo.insert(changeset) do
      post_register_action(user)
662 663 664 665 666
    end
  end

  def post_register_action(%User{} = user) do
    with {:ok, user} <- autofollow_users(user),
minibikini's avatar
minibikini committed
667
         {:ok, user} <- set_cache(user),
668
         {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
669
         {:ok, _} <- try_send_confirmation_email(user) do
670 671 672 673
      {:ok, user}
    end
  end

674
  def try_send_confirmation_email(%User{} = user) do
675
    if user.confirmation_pending &&
676
         Pleroma.Config.get([:instance, :account_activation_required]) do
677 678 679
      user
      |> Pleroma.Emails.UserEmail.account_confirmation_email()
      |> Pleroma.Emails.Mailer.deliver_async()
680 681

      {:ok, :enqueued}
682 683 684 685 686
    else
      {:ok, :noop}
    end
  end

687
  def try_send_confirmation_email(users) do
Maxim Filippov's avatar
Maxim Filippov committed
688
    Enum.each(users, &try_send_confirmation_email/1)
689 690
  end

691 692 693 694 695
  def needs_update?(%User{local: true}), do: false

  def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true

  def needs_update?(%User{local: false} = user) do
696
    NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
697 698 699 700
  end

  def needs_update?(_), do: true

Maksim's avatar
Maksim committed
701
  @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
702 703

  # "Locked" (self-locked) users demand explicit authorization of follow requests
704
  def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
705
    follow(follower, followed, :follow_pending)
706 707 708 709 710 711 712
  end

  def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
    follow(follower, followed)
  end

  def maybe_direct_follow(%User{} = follower, %User{} = followed) do
minibikini's avatar
minibikini committed
713
    if not ap_enabled?(followed) do
714
      follow(follower, followed)
715 716 717 718 719
    else
      {:ok, follower}
    end
  end

720
  @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
lain's avatar
lain committed
721 722
  @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
  def follow_all(follower, followeds) do
minibikini's avatar
minibikini committed
723 724
    followeds
    |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
725
    |> Enum.each(&follow(follower, &1, :follow_accept))
lain's avatar
lain committed
726

lain's avatar
lain committed
727
    set_cache(follower)
lain's avatar
lain committed
728 729
  end

730 731
  defdelegate following(user), to: FollowingRelationship

732
  def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
minibikini's avatar
minibikini committed
733
    deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
734

735
    cond do
736 737
      followed.deactivated ->
        {:error, "Could not follow user: #{followed.nickname} is deactivated."}
lain's avatar
lain committed
738

739
      deny_follow_blocked and blocks?(followed, follower) ->
740
        {:error, "Could not follow user: #{followed.nickname} blocked you."}
lain's avatar
lain committed
741

742
      true ->
743
        FollowingRelationship.follow(follower, followed, state)
744

745 746
        {:ok, _} = update_follower_count(followed)

minibikini's avatar
minibikini committed
747 748 749
        follower
        |> update_following_count()
        |> set_cache()
750
    end
lain's avatar
lain committed
751
  end
lain's avatar
lain committed
752

753 754 755 756
  def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
    {:error, "Not subscribed!"}
  end

757
  @spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
lain's avatar
lain committed
758
  def unfollow(%User{} = follower, %User{} = followed) do
759 760 761 762 763 764 765 766 767 768 769
    case do_unfollow(follower, followed) do
      {:ok, follower, followed} ->
        {:ok, follower, Utils.fetch_latest_follow(follower, followed)}

      error ->
        error
    end
  end

  @spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()}
  defp do_unfollow(%User{} = follower, %User{} = followed) do
minibikini's avatar
minibikini committed
770
    case get_follow_state(follower, followed) do
771
      state when state in [:follow_pending, :follow_accept] ->
772 773
        FollowingRelationship.unfollow(follower, followed)
        {:ok, followed} = update_follower_count(followed)
774

775 776 777 778
        {:ok, follower} =
          follower
          |> update_following_count()
          |> set_cache()
779

780
        {:ok, follower, followed}
781

782 783
      nil ->
        {:error, "Not subscribed!"}
784
    end
lain's avatar
lain committed
785
  end
786

787
  defdelegate following?(follower, followed), to: FollowingRelationship
lain's avatar
lain committed
788

789
  @doc "Returns follow state as Pleroma.FollowingRelationship.State value"
minibikini's avatar
minibikini committed
790 791
  def get_follow_state(%User{} = follower, %User{} = following) do
    following_relationship = FollowingRelationship.get(follower, following)
792 793
    get_follow_state(follower, following, following_relationship)
  end
minibikini's avatar
minibikini committed
794

795 796 797 798 799
  def get_follow_state(
        %User{} = follower,
        %User{} = following,
        following_relationship
      ) do
minibikini's avatar
minibikini committed
800 801 802
    case {following_relationship, following.local} do
      {nil, false} ->
        case Utils.fetch_latest_follow(follower, following) do
803 804 805 806 807
          %Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
            FollowingRelationship.state_to_enum(state)

          _ ->
            nil
minibikini's avatar
minibikini committed
808 809 810 811 812 813 814 815 816 817
        end

      {%{state: state}, _} ->
        state

      {nil, _} ->
        nil
    end
  end

818
  def locked?(%User{} = user) do
819
    user.locked || false
820 821
  end

822 823 824 825
  def get_by_id(id) do
    Repo.get_by(User, id: id)
  end

lain's avatar
lain committed
826 827 828 829
  def get_by_ap_id(ap_id) do
    Repo.get_by(User, ap_id: ap_id)
  end

830 831 832 833 834 835 836
  def get_all_by_ap_id(ap_ids) do
    from(u in __MODULE__,
      where: u.ap_id in ^ap_ids
    )
    |> Repo.all()
  end

Maksim's avatar
Maksim committed
837 838 839 840 841
  def get_all_by_ids(ids) do
    from(u in __MODULE__, where: u.id in ^ids)
    |> Repo.all()
  end

842 843
  # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
  # of the ap_id and the domain and tries to get that user
844 845 846 847 848
  def get_by_guessed_nickname(ap_id) do
    domain = URI.parse(ap_id).host
    name = List.last(String.split(ap_id, "/"))
    nickname = "#{name}@#{domain}"

minibikini's avatar
minibikini committed
849
    get_cached_by_nickname(nickname)
850 851
  end

minibikini's avatar
minibikini committed
852 853 854 855
  def set_cache({:ok, user}), do: set_cache(user)
  def set_cache({:error, err}), do: {:error, err}

  def set_cache(%User{} = user) do
856 857
    Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
    Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
Alexander Strizhakov's avatar
Alexander Strizhakov committed
858
    Cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
859 860 861
    {:ok, user}
  end

862 863 864 865 866 867
  def update_and_set_cache(struct, params) do
    struct
    |> update_changeset(params)
    |> update_and_set_cache()
  end

lain's avatar
lain committed
868
  def update_and_set_cache(changeset) do
869
    with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
870
      set_cache(user)
lain's avatar
lain committed
871 872 873
    end
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
874 875 876 877 878 879 880 881 882 883 884 885
  def get_user_friends_ap_ids(user) do
    from(u in User.get_friends_query(user), select: u.ap_id)
    |> Repo.all()
  end

  @spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
  def get_cached_user_friends_ap_ids(user) do
    Cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
      get_user_friends_ap_ids(user)
    end)
  end

lain's avatar
lain committed
886 887 888
  def invalidate_cache(user) do
    Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
    Cachex.del(:user_cache, "nickname:#{user.nickname}")
Alexander Strizhakov's avatar
Alexander Strizhakov committed
889
    Cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
lain's avatar
lain committed
890 891
  end

892
  @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
lain's avatar
lain committed
893
  def get_cached_by_ap_id(ap_id) do
894
    key = "ap_id:#{ap_id}"
895 896 897 898 899 900 901 902 903

    with {:ok, nil} <- Cachex.get(:user_cache, key),
         user when not is_nil(user) <- get_by_ap_id(ap_id),
         {:ok, true} <- Cachex.put(:user_cache, key, user) do
      user
    else
      {:ok, user} -> user
      nil -> nil
    end
lain's avatar
lain committed
904 905
  end

906 907
  def get_cached_by_id(id) do
    key = "id:#{id}"
908 909 910 911

    ap_id =
      Cachex.fetch!(:user_cache, key, fn _ ->
        user = get_by_id(id)
912 913 914 915 916 917 918

        if user do
          Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
          {:commit, user.ap_id}
        else
          {:ignore, ""}
        end
919 920 921
      end)

    get_cached_by_ap_id(ap_id)
922 923
  end

lain's avatar
lain committed
924
  def get_cached_by_nickname(nickname) do
925
    key = "nickname:#{nickname}"
0x1C3B00DA's avatar
Run  
0x1C3B00DA committed
926

927
    Cachex.fetch!(:user_cache, key, fn ->
minibikini's avatar
minibikini committed
928
      case get_or_fetch_by_nickname(nickname) do
929
        {:ok, user} -> {:commit, user}
Alexander Strizhakov's avatar
Alexander Strizhakov committed
930
        {:error, _error} -> {:ignore, nil}
931 932
      end
    end)
lain's avatar
lain committed
933
  end
lain's avatar
lain committed
934

935
  def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
936 937 938
    restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])

    cond do
939
      is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
940 941
        get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)

942
      restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
943 944 945 946 947 948 949
        get_cached_by_nickname(nickname_or_id)

      restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
        get_cached_by_nickname(nickname_or_id)

      true ->
        nil
950
    end
951 952
  end

953
  @spec get_by_nickname(String.t()) :: User.t() | nil
lain's avatar
lain committed
954
  def get_by_nickname(nickname) do
955
    Repo.get_by(User, nickname: nickname) ||
956
      if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
957
        Repo.get_by(User, nickname: local_nickname(nickname))
958
      end
959 960
  end

961 962
  def get_by_email(email), do: Repo.get_by(User, email: email)

963
  def get_by_nickname_or_email(nickname_or_email) do
964
    get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
965 966
  end

967