user.ex 37.3 KB
Newer Older
1
# Pleroma: A lightweight social networking server
kaniini's avatar
kaniini committed
2
# Copyright © 2017-2019 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 10
  import Ecto.Changeset
  import Ecto.Query

11 12 13 14 15
  alias Comeonin.Pbkdf2
  alias Pleroma.Activity
  alias Pleroma.Formatter
  alias Pleroma.Notification
  alias Pleroma.Object
Haelwenn's avatar
Haelwenn committed
16 17 18
  alias Pleroma.Repo
  alias Pleroma.User
  alias Pleroma.Web
19 20
  alias Pleroma.Web.ActivityPub.ActivityPub
  alias Pleroma.Web.ActivityPub.Utils
Maxim Filippov's avatar
Maxim Filippov committed
21
  alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
Haelwenn's avatar
Haelwenn committed
22
  alias Pleroma.Web.OAuth
23
  alias Pleroma.Web.OStatus
24
  alias Pleroma.Web.RelMe
25
  alias Pleroma.Web.Websub
lain's avatar
lain committed
26

27 28
  require Logger

Maksim's avatar
Maksim committed
29 30
  @type t :: %__MODULE__{}

href's avatar
href committed
31 32
  @primary_key {:id, Pleroma.FlakeId, autogenerate: true}

33
  # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
href's avatar
href committed
34 35 36
  @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
37
  @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
href's avatar
href committed
38

lain's avatar
lain committed
39
  schema "users" do
lain's avatar
lain committed
40 41 42 43
    field(:bio, :string)
    field(:email, :string)
    field(:name, :string)
    field(:nickname, :string)
44 45
    field(:auth_provider, :string)
    field(:auth_provider_uid, :string)
lain's avatar
lain committed
46 47 48 49 50 51 52 53
    field(:password_hash, :string)
    field(:password, :string, virtual: true)
    field(:password_confirmation, :string, virtual: true)
    field(:following, {:array, :string}, default: [])
    field(:ap_id, :string)
    field(:avatar, :map)
    field(:local, :boolean, default: true)
    field(:follower_address, :string)
54
    field(:search_rank, :float, virtual: true)
55
    field(:tags, {:array, :string}, default: [])
Haelwenn's avatar
Haelwenn committed
56
    field(:bookmarks, {:array, :string}, default: [])
57
    field(:last_refreshed_at, :naive_datetime)
lain's avatar
lain committed
58
    has_many(:notifications, Notification)
lain's avatar
lain committed
59
    embeds_one(:info, Pleroma.User.Info)
lain's avatar
lain committed
60 61 62

    timestamps()
  end
lain's avatar
lain committed
63

64
  def auth_active?(%User{local: false}), do: true
65

66 67 68 69
  def auth_active?(%User{info: %User.Info{confirmation_pending: false}}), do: true

  def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
    do: !Pleroma.Config.get([:instance, :account_activation_required])
70

71
  def auth_active?(_), do: false
72

73 74 75 76 77
  def visible_for?(user, for_user \\ nil)

  def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true

  def visible_for?(%User{} = user, for_user) do
78
    auth_active?(user) || superuser?(for_user)
79 80
  end

81 82
  def visible_for?(_, _), do: false

83 84
  def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
  def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
85
  def superuser?(_), do: false
86

lain's avatar
lain committed
87 88 89
  def avatar_url(user) do
    case user.avatar do
      %{"url" => [%{"href" => href} | _]} -> href
90
      _ -> "#{Web.base_url()}/images/avi.png"
lain's avatar
lain committed
91 92 93
    end
  end

lain's avatar
lain committed
94
  def banner_url(user) do
lain's avatar
lain committed
95
    case user.info.banner do
lain's avatar
lain committed
96
      %{"url" => [%{"href" => href} | _]} -> href
97
      _ -> "#{Web.base_url()}/images/banner.png"
lain's avatar
lain committed
98 99 100
    end
  end

lain's avatar
lain committed
101
  def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
102 103 104
  def profile_url(%User{ap_id: ap_id}), do: ap_id
  def profile_url(_), do: nil

lain's avatar
lain committed
105
  def ap_id(%User{nickname: nickname}) do
lain's avatar
lain committed
106
    "#{Web.base_url()}/users/#{nickname}"
lain's avatar
lain committed
107 108 109 110 111
  end

  def ap_followers(%User{} = user) do
    "#{ap_id(user)}/followers"
  end
lain's avatar
lain committed
112

lain's avatar
lain committed
113
  def user_info(%User{} = user) do
114
    oneself = if user.local, do: 1, else: 0
lain's avatar
lain committed
115

lain's avatar
lain committed
116
    %{
117
      following_count: length(user.following) - oneself,
lain's avatar
lain committed
118 119 120
      note_count: user.info.note_count,
      follower_count: user.info.follower_count,
      locked: user.info.locked,
Ivan Tashkinov's avatar
Ivan Tashkinov committed
121
      confirmation_pending: user.info.confirmation_pending,
lain's avatar
lain committed
122
      default_scope: user.info.default_scope
lain's avatar
lain committed
123 124 125
    }
  end

lain's avatar
lain committed
126
  def remote_user_creation(params) do
lain's avatar
lain committed
127 128 129
    params =
      params
      |> Map.put(:info, params[:info] || %{})
lain's avatar
lain committed
130 131 132

    info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])

lain's avatar
lain committed
133
    changes =
lain's avatar
lain committed
134
      %User{}
lain's avatar
lain committed
135
      |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
136
      |> validate_required([:name, :ap_id])
lain's avatar
lain committed
137 138 139 140 141
      |> unique_constraint(:nickname)
      |> validate_format(:nickname, @email_regex)
      |> validate_length(:bio, max: 5000)
      |> validate_length(:name, max: 100)
      |> put_change(:local, false)
lain's avatar
lain committed
142
      |> put_embed(:info, info_cng)
lain's avatar
lain committed
143

144
    if changes.valid? do
lain's avatar
lain committed
145
      case info_cng.changes[:source_data] do
lain's avatar
lain committed
146 147 148
        %{"followers" => followers} ->
          changes
          |> put_change(:follower_address, followers)
lain's avatar
lain committed
149

lain's avatar
lain committed
150 151
        _ ->
          followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
lain's avatar
lain committed
152

lain's avatar
lain committed
153 154 155
          changes
          |> put_change(:follower_address, followers)
      end
156 157 158
    else
      changes
    end
lain's avatar
lain committed
159 160
  end

lain's avatar
lain committed
161
  def update_changeset(struct, params \\ %{}) do
Thog's avatar
Thog committed
162
    struct
lain's avatar
lain committed
163
    |> cast(params, [:bio, :name, :avatar])
lain's avatar
lain committed
164
    |> unique_constraint(:nickname)
href's avatar
href committed
165
    |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
166
    |> validate_length(:bio, max: 5000)
lain's avatar
lain committed
167 168 169
    |> validate_length(:name, min: 1, max: 100)
  end

lain's avatar
lain committed
170
  def upgrade_changeset(struct, params \\ %{}) do
171 172 173 174
    params =
      params
      |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())

lain's avatar
lain committed
175 176 177 178
    info_cng =
      struct.info
      |> User.Info.user_upgrade(params[:info])

lain's avatar
lain committed
179
    struct
lain's avatar
lain committed
180
    |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
lain's avatar
lain committed
181
    |> unique_constraint(:nickname)
href's avatar
href committed
182
    |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
183 184
    |> validate_length(:bio, max: 5000)
    |> validate_length(:name, max: 100)
lain's avatar
lain committed
185
    |> put_embed(:info, info_cng)
lain's avatar
lain committed
186 187
  end

Roger Braun's avatar
Roger Braun committed
188
  def password_update_changeset(struct, params) do
lain's avatar
lain committed
189 190 191 192 193
    changeset =
      struct
      |> cast(params, [:password, :password_confirmation])
      |> validate_required([:password, :password_confirmation])
      |> validate_confirmation(:password)
Roger Braun's avatar
Roger Braun committed
194

195 196 197
    OAuth.Token.delete_user_tokens(struct)
    OAuth.Authorization.delete_user_authorizations(struct)

Roger Braun's avatar
Roger Braun committed
198 199
    if changeset.valid? do
      hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
lain's avatar
lain committed
200

Roger Braun's avatar
Roger Braun committed
201 202 203 204 205 206 207 208
      changeset
      |> put_change(:password_hash, hashed)
    else
      changeset
    end
  end

  def reset_password(user, data) do
lain's avatar
lain committed
209
    update_and_set_cache(password_update_changeset(user, data))
Roger Braun's avatar
Roger Braun committed
210 211
  end

212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
  # TODO: FIXME (WIP):
  def oauth_register_changeset(struct, params \\ %{}) do
    info_change = User.Info.confirmation_changeset(%User.Info{}, :confirmed)

    changeset =
      struct
      |> cast(params, [:email, :nickname, :name, :bio, :auth_provider, :auth_provider_uid])
      |> validate_required([:auth_provider, :auth_provider_uid])
      |> unique_constraint(:email)
      |> unique_constraint(:nickname)
      |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
      |> validate_format(:email, @email_regex)
      |> validate_length(:bio, max: 1000)
      |> put_change(:info, info_change)

    if changeset.valid? do
      nickname = changeset.changes[:nickname]
      ap_id = (nickname && User.ap_id(%User{nickname: nickname})) || nil
      followers = User.ap_followers(%User{nickname: ap_id})

      changeset
      |> put_change(:ap_id, ap_id)
      |> unique_constraint(:ap_id)
      |> put_change(:following, [followers])
      |> put_change(:follower_address, followers)
    else
      changeset
    end
  end

242 243 244 245 246 247 248 249
  def register_changeset(struct, params \\ %{}, opts \\ []) do
    confirmation_status =
      if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do
        :confirmed
      else
        :unconfirmed
      end

Ivan Tashkinov's avatar
Ivan Tashkinov committed
250 251
    info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)

lain's avatar
lain committed
252 253 254 255 256 257 258
    changeset =
      struct
      |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
      |> validate_required([:email, :name, :nickname, :password, :password_confirmation])
      |> validate_confirmation(:password)
      |> unique_constraint(:email)
      |> unique_constraint(:nickname)
lain's avatar
lain committed
259
      |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
href's avatar
href committed
260
      |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
261 262 263
      |> validate_format(:email, @email_regex)
      |> validate_length(:bio, max: 1000)
      |> validate_length(:name, min: 1, max: 100)
Ivan Tashkinov's avatar
Ivan Tashkinov committed
264
      |> put_change(:info, info_change)
lain's avatar
lain committed
265 266

    if changeset.valid? do
267
      hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
lain's avatar
lain committed
268 269
      ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
      followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
lain's avatar
lain committed
270

lain's avatar
lain committed
271 272 273
      changeset
      |> put_change(:password_hash, hashed)
      |> put_change(:ap_id, ap_id)
rinpatch's avatar
rinpatch committed
274
      |> unique_constraint(:ap_id)
lain's avatar
lain committed
275
      |> put_change(:following, [followers])
276
      |> put_change(:follower_address, followers)
lain's avatar
lain committed
277 278 279 280 281
    else
      changeset
    end
  end

282 283 284 285 286 287 288 289 290 291
  defp autofollow_users(user) do
    candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])

    autofollowed_users =
      from(u in User,
        where: u.local == true,
        where: u.nickname in ^candidates
      )
      |> Repo.all()

lain's avatar
lain committed
292
    follow_all(user, autofollowed_users)
293 294
  end

295 296
  @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
  def register(%Ecto.Changeset{} = changeset) do
Ivan Tashkinov's avatar
Ivan Tashkinov committed
297
    with {:ok, user} <- Repo.insert(changeset),
lain's avatar
lain committed
298
         {:ok, user} <- autofollow_users(user),
lain's avatar
lain committed
299
         {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
lain's avatar
lain committed
300
         {:ok, _} <- try_send_confirmation_email(user) do
301 302 303 304
      {:ok, user}
    end
  end

305
  def try_send_confirmation_email(%User{} = user) do
306 307
    if user.info.confirmation_pending &&
         Pleroma.Config.get([:instance, :account_activation_required]) do
308 309
      user
      |> Pleroma.UserEmail.account_confirmation_email()
minibikini's avatar
Reports  
minibikini committed
310
      |> Pleroma.Mailer.deliver_async()
311 312 313 314 315
    else
      {:ok, :noop}
    end
  end

316 317 318 319 320
  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
321
    NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
322 323 324 325
  end

  def needs_update?(_), do: true

lain's avatar
lain committed
326
  def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
327 328 329 330 331 332 333 334
    {:ok, follower}
  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
Maksim's avatar
Maksim committed
335
    if not User.ap_enabled?(followed) do
336
      follow(follower, followed)
337 338 339 340 341
    else
      {:ok, follower}
    end
  end

Maksim's avatar
Maksim committed
342
  def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
343 344
    if not following?(follower, followed) do
      follow(follower, followed)
345
    else
346
      {:ok, follower}
347 348 349
    end
  end

350
  @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
lain's avatar
lain committed
351 352
  @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
  def follow_all(follower, followeds) do
lain's avatar
lain committed
353 354
    followed_addresses =
      followeds
355
      |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
lain's avatar
lain committed
356
      |> Enum.map(fn %{follower_address: fa} -> fa end)
lain's avatar
lain committed
357

lain's avatar
lain committed
358 359 360
    q =
      from(u in User,
        where: u.id == ^follower.id,
361 362 363 364 365 366 367 368 369 370
        update: [
          set: [
            following:
              fragment(
                "array(select distinct unnest (array_cat(?, ?)))",
                u.following,
                ^followed_addresses
              )
          ]
        ]
lain's avatar
lain committed
371 372 373
      )

    {1, [follower]} = Repo.update_all(q, [], returning: true)
lain's avatar
lain committed
374 375 376 377 378

    Enum.each(followeds, fn followed ->
      update_follower_count(followed)
    end)

lain's avatar
lain committed
379
    set_cache(follower)
lain's avatar
lain committed
380 381
  end

lain's avatar
lain committed
382
  def follow(%User{} = follower, %User{info: info} = followed) do
383 384
    user_config = Application.get_env(:pleroma, :user)
    deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
385

386
    ap_followers = followed.follower_address
387

388
    cond do
lain's avatar
lain committed
389
      following?(follower, followed) or info.deactivated ->
390
        {:error, "Could not follow user: #{followed.nickname} is already on your list."}
lain's avatar
lain committed
391

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

395 396 397 398 399
      true ->
        if !followed.local && follower.local && !ap_enabled?(followed) do
          Websub.subscribe(follower, followed)
        end

400 401 402 403 404
        q =
          from(u in User,
            where: u.id == ^follower.id,
            update: [push: [following: ^ap_followers]]
          )
405

406
        {1, [follower]} = Repo.update_all(q, [], returning: true)
407

408 409
        {:ok, _} = update_follower_count(followed)

410
        set_cache(follower)
411
    end
lain's avatar
lain committed
412
  end
lain's avatar
lain committed
413 414

  def unfollow(%User{} = follower, %User{} = followed) do
415
    ap_followers = followed.follower_address
lain's avatar
lain committed
416

417
    if following?(follower, followed) and follower.ap_id != followed.ap_id do
418 419 420 421 422
      q =
        from(u in User,
          where: u.id == ^follower.id,
          update: [pull: [following: ^ap_followers]]
        )
lain's avatar
lain committed
423

424
      {1, [follower]} = Repo.update_all(q, [], returning: true)
425 426 427

      {:ok, followed} = update_follower_count(followed)

428 429
      set_cache(follower)

430
      {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
431
    else
432
      {:error, "Not subscribed!"}
433
    end
lain's avatar
lain committed
434
  end
435

Maksim's avatar
Maksim committed
436
  @spec following?(User.t(), User.t()) :: boolean
437
  def following?(%User{} = follower, %User{} = followed) do
438
    Enum.member?(follower.following, followed.follower_address)
439
  end
lain's avatar
lain committed
440

441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458
  def follow_import(%User{} = follower, followed_identifiers)
      when is_list(followed_identifiers) do
    Enum.map(
      followed_identifiers,
      fn followed_identifier ->
        with %User{} = followed <- get_or_fetch(followed_identifier),
             {:ok, follower} <- maybe_direct_follow(follower, followed),
             {:ok, _} <- ActivityPub.follow(follower, followed) do
          followed
        else
          err ->
            Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
            err
        end
      end
    )
  end

459
  def locked?(%User{} = user) do
460
    user.info.locked || false
461 462
  end

463 464 465 466
  def get_by_id(id) do
    Repo.get_by(User, id: id)
  end

lain's avatar
lain committed
467 468 469 470
  def get_by_ap_id(ap_id) do
    Repo.get_by(User, ap_id: ap_id)
  end

471 472
  # 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
473 474 475 476 477 478 479 480
  def get_by_guessed_nickname(ap_id) do
    domain = URI.parse(ap_id).host
    name = List.last(String.split(ap_id, "/"))
    nickname = "#{name}@#{domain}"

    get_by_nickname(nickname)
  end

481 482 483 484 485 486 487
  def set_cache(user) do
    Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
    Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
    Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
    {:ok, user}
  end

lain's avatar
lain committed
488 489
  def update_and_set_cache(changeset) do
    with {:ok, user} <- Repo.update(changeset) do
490
      set_cache(user)
lain's avatar
lain committed
491 492 493 494 495
    else
      e -> e
    end
  end

lain's avatar
lain committed
496 497 498
  def invalidate_cache(user) do
    Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
    Cachex.del(:user_cache, "nickname:#{user.nickname}")
499
    Cachex.del(:user_cache, "user_info:#{user.id}")
lain's avatar
lain committed
500 501
  end

lain's avatar
lain committed
502
  def get_cached_by_ap_id(ap_id) do
503
    key = "ap_id:#{ap_id}"
Thog's avatar
Thog committed
504
    Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
lain's avatar
lain committed
505 506
  end

507 508
  def get_cached_by_id(id) do
    key = "id:#{id}"
509 510 511 512

    ap_id =
      Cachex.fetch!(:user_cache, key, fn _ ->
        user = get_by_id(id)
513 514 515 516 517 518 519

        if user do
          Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
          {:commit, user.ap_id}
        else
          {:ignore, ""}
        end
520 521 522
      end)

    get_cached_by_ap_id(ap_id)
523 524
  end

lain's avatar
lain committed
525
  def get_cached_by_nickname(nickname) do
526
    key = "nickname:#{nickname}"
Thog's avatar
Thog committed
527
    Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
lain's avatar
lain committed
528
  end
lain's avatar
lain committed
529

530
  def get_cached_by_nickname_or_id(nickname_or_id) do
531
    get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
532 533
  end

lain's avatar
lain committed
534
  def get_by_nickname(nickname) do
535
    Repo.get_by(User, nickname: nickname) ||
536
      if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
537
        Repo.get_by(User, nickname: local_nickname(nickname))
538
      end
539 540
  end

541 542
  def get_by_email(email), do: Repo.get_by(User, email: email)

543
  def get_by_nickname_or_email(nickname_or_email) do
544
    get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
545 546
  end

547 548 549 550 551 552 553
  def get_by_auth_provider_uid(auth_provider, auth_provider_uid),
    do:
      Repo.get_by(User,
        auth_provider: to_string(auth_provider),
        auth_provider_uid: to_string(auth_provider_uid)
      )

lain's avatar
lain committed
554 555
  def get_cached_user_info(user) do
    key = "user_info:#{user.id}"
Thog's avatar
Thog committed
556
    Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
lain's avatar
lain committed
557
  end
lain's avatar
lain committed
558

lain's avatar
lain committed
559 560 561 562 563 564 565 566 567
  def fetch_by_nickname(nickname) do
    ap_try = ActivityPub.make_user_from_nickname(nickname)

    case ap_try do
      {:ok, user} -> {:ok, user}
      _ -> OStatus.make_user(nickname)
    end
  end

lain's avatar
lain committed
568
  def get_or_fetch_by_nickname(nickname) do
lain's avatar
lain committed
569
    with %User{} = user <- get_by_nickname(nickname) do
lain's avatar
lain committed
570
      user
lain's avatar
lain committed
571 572 573 574
    else
      _e ->
        with [_nick, _domain] <- String.split(nickname, "@"),
             {:ok, user} <- fetch_by_nickname(nickname) do
575 576 577 578
          if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
            {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
          end

lain's avatar
lain committed
579 580 581 582
          user
        else
          _e -> nil
        end
lain's avatar
lain committed
583
    end
lain's avatar
lain committed
584
  end
lain's avatar
lain committed
585

586 587 588 589 590 591 592 593 594 595 596
  @doc "Fetch some posts when the user has just been federated with"
  def fetch_initial_posts(user) do
    pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])

    Enum.each(
      # Insert all the posts in reverse order, so they're in the right order on the timeline
      Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
      &Pleroma.Web.Federator.incoming_ap_doc/1
    )
  end

597
  def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
598 599 600 601 602 603 604
    from(
      u in User,
      where: fragment("? <@ ?", ^[follower_address], u.following),
      where: u.id != ^id
    )
  end

605
  def get_followers_query(user, page) do
Maxim Filippov's avatar
Maxim Filippov committed
606 607
    from(u in get_followers_query(user, nil))
    |> paginate(page, 20)
608 609 610 611 612 613
  end

  def get_followers_query(user), do: get_followers_query(user, nil)

  def get_followers(user, page \\ nil) do
    q = get_followers_query(user, page)
lain's avatar
lain committed
614 615 616 617

    {:ok, Repo.all(q)}
  end

618 619 620 621 622 623
  def get_followers_ids(user, page \\ nil) do
    q = get_followers_query(user, page)

    Repo.all(from(u in q, select: u.id))
  end

624
  def get_friends_query(%User{id: id, following: following}, nil) do
625 626 627 628 629 630 631
    from(
      u in User,
      where: u.follower_address in ^following,
      where: u.id != ^id
    )
  end

632
  def get_friends_query(user, page) do
Maxim Filippov's avatar
Maxim Filippov committed
633 634
    from(u in get_friends_query(user, nil))
    |> paginate(page, 20)
635 636 637 638 639 640
  end

  def get_friends_query(user), do: get_friends_query(user, nil)

  def get_friends(user, page \\ nil) do
    q = get_friends_query(user, page)
lain's avatar
lain committed
641 642 643

    {:ok, Repo.all(q)}
  end
644

645 646 647 648 649 650
  def get_friends_ids(user, page \\ nil) do
    q = get_friends_query(user, page)

    Repo.all(from(u in q, select: u.id))
  end

651 652 653
  def get_follow_requests_query(%User{} = user) do
    from(
      a in Activity,
kaniini's avatar
kaniini committed
654 655 656 657 658 659 660 661 662 663 664 665
      where:
        fragment(
          "? ->> 'type' = 'Follow'",
          a.data
        ),
      where:
        fragment(
          "? ->> 'state' = 'pending'",
          a.data
        ),
      where:
        fragment(
666
          "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
kaniini's avatar
kaniini committed
667
          a.data,
668 669
          a.data,
          ^user.ap_id
kaniini's avatar
kaniini committed
670
        )
671 672 673 674 675
    )
  end

  def get_follow_requests(%User{} = user) do
    users =
676 677
      user
      |> User.get_follow_requests_query()
678 679 680 681 682
      |> join(:inner, [a], u in User, a.actor == u.ap_id)
      |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
      |> group_by([a, u], u.id)
      |> select([a, u], u)
      |> Repo.all()
683 684 685 686

    {:ok, users}
  end

687
  def increase_note_count(%User{} = user) do
688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704
    User
    |> where(id: ^user.id)
    |> update([u],
      set: [
        info:
          fragment(
            "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
            u.info,
            u.info
          )
      ]
    )
    |> Repo.update_all([], returning: true)
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
705 706
  end

707
  def decrease_note_count(%User{} = user) do
708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724
    User
    |> where(id: ^user.id)
    |> update([u],
      set: [
        info:
          fragment(
            "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
            u.info,
            u.info
          )
      ]
    )
    |> Repo.update_all([], returning: true)
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
725 726
  end

727
  def update_note_count(%User{} = user) do
lain's avatar
lain committed
728 729 730 731 732 733
    note_count_query =
      from(
        a in Object,
        where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
        select: count(a.id)
      )
734 735 736

    note_count = Repo.one(note_count_query)

lain's avatar
lain committed
737
    info_cng = User.Info.set_note_count(user.info, note_count)
738

lain's avatar
lain committed
739 740 741
    cng =
      change(user)
      |> put_embed(:info, info_cng)
742

lain's avatar
lain committed
743
    update_and_set_cache(cng)
744 745 746
  end

  def update_follower_count(%User{} = user) do
lain's avatar
lain committed
747
    follower_count_query =
748 749 750 751
      User
      |> where([u], ^user.follower_address in u.following)
      |> where([u], u.id != ^user.id)
      |> select([u], %{count: count(u.id)})
752

753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770
    User
    |> where(id: ^user.id)
    |> join(:inner, [u], s in subquery(follower_count_query))
    |> update([u, s],
      set: [
        info:
          fragment(
            "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
            u.info,
            s.count
          )
      ]
    )
    |> Repo.update_all([], returning: true)
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
771
  end
772

773
  def get_users_from_set_query(ap_ids, false) do
774 775
    from(
      u in User,
776
      where: u.ap_id in ^ap_ids
777 778 779
    )
  end

780 781
  def get_users_from_set_query(ap_ids, true) do
    query = get_users_from_set_query(ap_ids, false)
782 783 784

    from(
      u in query,
785 786 787 788
      where: u.local == true
    )
  end

789 790 791 792 793
  def get_users_from_set(ap_ids, local_only \\ true) do
    get_users_from_set_query(ap_ids, local_only)
    |> Repo.all()
  end

794
  def get_recipients_from_activity(%Activity{recipients: to}) do
lain's avatar
lain committed
795 796 797 798 799 800
    query =
      from(
        u in User,
        where: u.ap_id in ^to,
        or_where: fragment("? && ?", u.following, ^to)
      )
801

lain's avatar
lain committed
802
    query = from(u in query, where: u.local == true)
803

804 805 806
    Repo.all(query)
  end

807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829
  @spec search_for_admin(%{
          local: boolean(),
          page: number(),
          page_size: number()
        }) :: {:ok, [Pleroma.User.t()], number()}
  def search_for_admin(%{query: nil, local: local, page: page, page_size: page_size}) do
    query =
      from(u in User, order_by: u.id)
      |> maybe_local_user_query(local)

    paginated_query =
      query
      |> paginate(page, page_size)

    count =
      query
      |> Repo.aggregate(:count, :id)

    {:ok, Repo.all(paginated_query), count}
  end

  @spec search_for_admin(%{
          query: binary(),
Maxim Filippov's avatar
Maxim Filippov committed
830 831 832 833 834
          admin: Pleroma.User.t(),
          local: boolean(),
          page: number(),
          page_size: number()
        }) :: {:ok, [Pleroma.User.t()], number()}
835 836 837 838 839 840 841
  def search_for_admin(%{
        query: term,
        admin: admin,
        local: local,
        page: page,
        page_size: page_size
      }) do
842
    term = String.trim_leading(term, "@")
843

Maxim Filippov's avatar
Maxim Filippov committed
844 845 846 847
    local_paginated_query =
      User
      |> maybe_local_user_query(local)
      |> paginate(page, page_size)
lain's avatar
lain committed
848

Maxim Filippov's avatar
Maxim Filippov committed
849
    search_query = fts_search_subquery(term, local_paginated_query)
850

Maxim Filippov's avatar
Maxim Filippov committed
851 852 853 854 855
    count =
      term
      |> fts_search_subquery()
      |> maybe_local_user_query(local)
      |> Repo.aggregate(:count, :id)
lain's avatar
lain committed
856

Maxim Filippov's avatar
Maxim Filippov committed
857
    {:ok, do_search(search_query, admin), count}
858
  end
lain's avatar
lain committed
859

860
  def search(query, resolve \\ false, for_user \\ nil) do
861
    # Strip the beginning @ off if there is a query
862 863
    query = String.trim_leading(query, "@")

864
    if resolve, do: get_or_fetch(query)
lain's avatar
lain committed
865

866
    fts_results = do_search(fts_search_subquery(query), for_user)
867

lain's avatar
lain committed
868 869 870 871 872
    {:ok, trigram_results} =
      Repo.transaction(fn ->
        Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
        do_search(trigram_search_subquery(query), for_user)
      end)
lain's avatar
lain committed
873

874 875
    Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
  end
lain's avatar
lain committed
876

877
  defp do_search(subquery, for_user, options \\ []) do
kaniini's avatar
kaniini committed
878 879
    q =
      from(
880
        s in subquery(subquery),
881
        order_by: [desc: s.search_rank],
882
        limit: ^(options[:limit] || 20)
kaniini's avatar
kaniini committed
883
      )
lain's avatar
lain committed
884

885 886 887 888 889
    results =
      q
      |> Repo.all()
      |> Enum.filter(&(&1.search_rank > 0))

890 891
    boost_search_results(results, for_user)
  end
892

Maxim Filippov's avatar
Maxim Filippov committed
893
  defp fts_search_subquery(term, query \\ User) do
894
    processed_query =
895
      term
896 897 898 899 900
      |> String.replace(~r/\W+/, " ")
      |> String.trim()
      |> String.split()
      |> Enum.map(&(&1 <> ":*"))
      |> Enum.join(" | ")
901

902
    from(
903
      u in query,
904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919
      select_merge: %{
        search_rank:
          fragment(
            """
            ts_rank_cd(
              setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
              setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
              to_tsquery('simple', ?),
              32
            )
            """,
            u.nickname,
            u.name,
            ^processed_query
          )
      },
lain's avatar
lain committed
920 921 922 923 924 925 926 927 928 929
      where:
        fragment(
          """
            (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
            setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
          """,
          u.nickname,
          u.name,
          ^processed_query
        )
930 931
    )
  end
932

Maxim Filippov's avatar
Maxim Filippov committed
933
  defp trigram_search_subquery(term) do
934 935 936 937 938
    from(
      u in User,
      select_merge: %{
        search_rank:
          fragment(
939
            "similarity(?, trim(? || ' ' || coalesce(?, '')))",
940
            ^term,
941 942 943 944
            u.nickname,
            u.name
          )
      },
945
      where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
946 947 948 949 950 951 952 953
    )
  end

  defp boost_search_results(results, nil), do: results

  defp boost_search_results(results, for_user) do
    friends_ids = get_friends_ids(for_user)
    followers_ids = get_followers_ids(for_user)
954

955 956 957 958 959 960 961 962 963 964 965 966 967
    Enum.map(
      results,
      fn u ->
        search_rank_coef =
          cond do
            u.id in friends_ids ->
              1.2

            u.id in followers_ids ->
              1.1

            true ->
              1
968 969
          end