user.ex 39.1 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
  alias Comeonin.Pbkdf2
  alias Pleroma.Activity
13
  alias Pleroma.Bookmark
14 15
  alias Pleroma.Notification
  alias Pleroma.Object
16
  alias Pleroma.Registration
Haelwenn's avatar
Haelwenn committed
17 18 19
  alias Pleroma.Repo
  alias Pleroma.User
  alias Pleroma.Web
20 21
  alias Pleroma.Web.ActivityPub.ActivityPub
  alias Pleroma.Web.ActivityPub.Utils
Maxim Filippov's avatar
Maxim Filippov committed
22
  alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
Haelwenn's avatar
Haelwenn committed
23
  alias Pleroma.Web.OAuth
24
  alias Pleroma.Web.OStatus
25
  alias Pleroma.Web.RelMe
26
  alias Pleroma.Web.Websub
lain's avatar
lain committed
27

28 29
  require Logger

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

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

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

lain's avatar
lain committed
40
  schema "users" do
lain's avatar
lain committed
41 42 43 44 45 46 47 48 49 50 51 52
    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)
    field(:following, {:array, :string}, default: [])
    field(:ap_id, :string)
    field(:avatar, :map)
    field(:local, :boolean, default: true)
    field(:follower_address, :string)
53
    field(:search_rank, :float, virtual: true)
54
    field(:search_type, :integer, virtual: true)
55
    field(:tags, {:array, :string}, default: [])
rinpatch's avatar
rinpatch committed
56
    field(:last_refreshed_at, :naive_datetime_usec)
57
    has_many(:bookmarks, Bookmark)
lain's avatar
lain committed
58
    has_many(:notifications, Notification)
59
    has_many(:registrations, Registration)
lain's avatar
lain committed
60
    embeds_one(:info, Pleroma.User.Info)
lain's avatar
lain committed
61 62 63

    timestamps()
  end
lain's avatar
lain committed
64

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

68
  def auth_active?(%User{}), do: true
69

70 71 72 73 74
  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
75
    auth_active?(user) || superuser?(for_user)
76 77
  end

78 79
  def visible_for?(_, _), do: false

80 81
  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
82
  def superuser?(_), do: false
83

84
  def avatar_url(user, options \\ []) do
lain's avatar
lain committed
85 86
    case user.avatar do
      %{"url" => [%{"href" => href} | _]} -> href
87
      _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
lain's avatar
lain committed
88 89 90
    end
  end

91
  def banner_url(user, options \\ []) do
lain's avatar
lain committed
92
    case user.info.banner do
lain's avatar
lain committed
93
      %{"url" => [%{"href" => href} | _]} -> href
94
      _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
lain's avatar
lain committed
95 96 97
    end
  end

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

lain's avatar
lain committed
102
  def ap_id(%User{nickname: nickname}) do
lain's avatar
lain committed
103
    "#{Web.base_url()}/users/#{nickname}"
lain's avatar
lain committed
104 105
  end

106 107
  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
108

lain's avatar
lain committed
109 110
  def user_info(%User{} = user) do
    %{
111
      following_count: following_count(user),
lain's avatar
lain committed
112 113 114
      note_count: user.info.note_count,
      follower_count: user.info.follower_count,
      locked: user.info.locked,
Ivan Tashkinov's avatar
Ivan Tashkinov committed
115
      confirmation_pending: user.info.confirmation_pending,
lain's avatar
lain committed
116
      default_scope: user.info.default_scope
lain's avatar
lain committed
117 118 119
    }
  end

120
  defp restrict_deactivated(query) do
121
    from(u in query,
122
      where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
123 124 125 126 127 128 129 130 131 132
    )
  end

  def following_count(%User{following: []}), do: 0

  def following_count(%User{following: following, id: id}) do
    from(u in User,
      where: u.follower_address in ^following,
      where: u.id != ^id
    )
133
    |> restrict_deactivated()
134 135 136
    |> Repo.aggregate(:count, :id)
  end

lain's avatar
lain committed
137
  def remote_user_creation(params) do
lain's avatar
lain committed
138 139 140
    params =
      params
      |> Map.put(:info, params[:info] || %{})
lain's avatar
lain committed
141 142 143

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

lain's avatar
lain committed
144
    changes =
lain's avatar
lain committed
145
      %User{}
lain's avatar
lain committed
146
      |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
147
      |> validate_required([:name, :ap_id])
lain's avatar
lain committed
148 149 150 151 152
      |> 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
153
      |> put_embed(:info, info_cng)
lain's avatar
lain committed
154

155
    if changes.valid? do
lain's avatar
lain committed
156
      case info_cng.changes[:source_data] do
lain's avatar
lain committed
157 158 159
        %{"followers" => followers} ->
          changes
          |> put_change(:follower_address, followers)
lain's avatar
lain committed
160

lain's avatar
lain committed
161 162
        _ ->
          followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
lain's avatar
lain committed
163

lain's avatar
lain committed
164 165 166
          changes
          |> put_change(:follower_address, followers)
      end
167 168 169
    else
      changes
    end
lain's avatar
lain committed
170 171
  end

lain's avatar
lain committed
172
  def update_changeset(struct, params \\ %{}) do
Thog's avatar
Thog committed
173
    struct
lain's avatar
lain committed
174
    |> cast(params, [:bio, :name, :avatar])
lain's avatar
lain committed
175
    |> unique_constraint(:nickname)
href's avatar
href committed
176
    |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
177
    |> validate_length(:bio, max: 5000)
lain's avatar
lain committed
178 179 180
    |> validate_length(:name, min: 1, max: 100)
  end

lain's avatar
lain committed
181
  def upgrade_changeset(struct, params \\ %{}) do
182 183 184 185
    params =
      params
      |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())

lain's avatar
lain committed
186 187 188 189
    info_cng =
      struct.info
      |> User.Info.user_upgrade(params[:info])

lain's avatar
lain committed
190
    struct
lain's avatar
lain committed
191
    |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
lain's avatar
lain committed
192
    |> unique_constraint(:nickname)
href's avatar
href committed
193
    |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
194 195
    |> validate_length(:bio, max: 5000)
    |> validate_length(:name, max: 100)
lain's avatar
lain committed
196
    |> put_embed(:info, info_cng)
lain's avatar
lain committed
197 198
  end

Roger Braun's avatar
Roger Braun committed
199
  def password_update_changeset(struct, params) do
lain's avatar
lain committed
200 201 202 203 204
    changeset =
      struct
      |> cast(params, [:password, :password_confirmation])
      |> validate_required([:password, :password_confirmation])
      |> validate_confirmation(:password)
Roger Braun's avatar
Roger Braun committed
205

206 207 208
    OAuth.Token.delete_user_tokens(struct)
    OAuth.Authorization.delete_user_authorizations(struct)

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

Roger Braun's avatar
Roger Braun committed
212 213 214 215 216 217 218 219
      changeset
      |> put_change(:password_hash, hashed)
    else
      changeset
    end
  end

  def reset_password(user, data) do
lain's avatar
lain committed
220
    update_and_set_cache(password_update_changeset(user, data))
Roger Braun's avatar
Roger Braun committed
221 222
  end

223 224 225 226 227 228 229 230
  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
231 232
    info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)

lain's avatar
lain committed
233 234 235
    changeset =
      struct
      |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
236
      |> validate_required([:name, :nickname, :password, :password_confirmation])
lain's avatar
lain committed
237 238 239
      |> validate_confirmation(:password)
      |> unique_constraint(:email)
      |> unique_constraint(:nickname)
lain's avatar
lain committed
240
      |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
href's avatar
href committed
241
      |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
242 243 244
      |> validate_format(:email, @email_regex)
      |> validate_length(:bio, max: 1000)
      |> validate_length(:name, min: 1, max: 100)
Ivan Tashkinov's avatar
Ivan Tashkinov committed
245
      |> put_change(:info, info_change)
lain's avatar
lain committed
246

247 248 249 250 251 252 253
    changeset =
      if opts[:external] do
        changeset
      else
        validate_required(changeset, [:email])
      end

lain's avatar
lain committed
254
    if changeset.valid? do
255
      hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
lain's avatar
lain committed
256 257
      ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
      followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
lain's avatar
lain committed
258

lain's avatar
lain committed
259 260 261
      changeset
      |> put_change(:password_hash, hashed)
      |> put_change(:ap_id, ap_id)
rinpatch's avatar
rinpatch committed
262
      |> unique_constraint(:ap_id)
lain's avatar
lain committed
263
      |> put_change(:following, [followers])
264
      |> put_change(:follower_address, followers)
lain's avatar
lain committed
265 266 267 268 269
    else
      changeset
    end
  end

270 271 272 273 274 275 276 277 278 279
  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
280
    follow_all(user, autofollowed_users)
281 282
  end

283 284
  @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
285
    with {:ok, user} <- Repo.insert(changeset),
lain's avatar
lain committed
286
         {:ok, user} <- autofollow_users(user),
minibikini's avatar
minibikini committed
287
         {:ok, user} <- set_cache(user),
lain's avatar
lain committed
288
         {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
lain's avatar
lain committed
289
         {:ok, _} <- try_send_confirmation_email(user) do
290 291 292 293
      {:ok, user}
    end
  end

294
  def try_send_confirmation_email(%User{} = user) do
295 296
    if user.info.confirmation_pending &&
         Pleroma.Config.get([:instance, :account_activation_required]) do
297
      user
298 299
      |> Pleroma.Emails.UserEmail.account_confirmation_email()
      |> Pleroma.Emails.Mailer.deliver_async()
300 301

      {:ok, :enqueued}
302 303 304 305 306
    else
      {:ok, :noop}
    end
  end

307 308 309 310 311
  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
312
    NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
313 314 315 316
  end

  def needs_update?(_), do: true

lain's avatar
lain committed
317
  def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
318 319 320 321 322 323 324 325
    {: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
326
    if not User.ap_enabled?(followed) do
327
      follow(follower, followed)
328 329 330 331 332
    else
      {:ok, follower}
    end
  end

Maksim's avatar
Maksim committed
333
  def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
334 335
    if not following?(follower, followed) do
      follow(follower, followed)
336
    else
337
      {:ok, follower}
338 339 340
    end
  end

341
  @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
lain's avatar
lain committed
342 343
  @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
  def follow_all(follower, followeds) do
lain's avatar
lain committed
344 345
    followed_addresses =
      followeds
346
      |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
lain's avatar
lain committed
347
      |> Enum.map(fn %{follower_address: fa} -> fa end)
lain's avatar
lain committed
348

lain's avatar
lain committed
349 350 351
    q =
      from(u in User,
        where: u.id == ^follower.id,
352 353 354 355 356 357 358 359 360
        update: [
          set: [
            following:
              fragment(
                "array(select distinct unnest (array_cat(?, ?)))",
                u.following,
                ^followed_addresses
              )
          ]
rinpatch's avatar
rinpatch committed
361 362
        ],
        select: u
lain's avatar
lain committed
363 364
      )

rinpatch's avatar
rinpatch committed
365
    {1, [follower]} = Repo.update_all(q, [])
lain's avatar
lain committed
366 367 368 369 370

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

lain's avatar
lain committed
371
    set_cache(follower)
lain's avatar
lain committed
372 373
  end

lain's avatar
lain committed
374
  def follow(%User{} = follower, %User{info: info} = followed) do
375 376
    user_config = Application.get_env(:pleroma, :user)
    deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
377

378
    ap_followers = followed.follower_address
379

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

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

387 388 389 390 391
      true ->
        if !followed.local && follower.local && !ap_enabled?(followed) do
          Websub.subscribe(follower, followed)
        end

392 393 394
        q =
          from(u in User,
            where: u.id == ^follower.id,
rinpatch's avatar
rinpatch committed
395 396
            update: [push: [following: ^ap_followers]],
            select: u
397
          )
398

rinpatch's avatar
rinpatch committed
399
        {1, [follower]} = Repo.update_all(q, [])
400

401 402
        {:ok, _} = update_follower_count(followed)

403
        set_cache(follower)
404
    end
lain's avatar
lain committed
405
  end
lain's avatar
lain committed
406 407

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

410
    if following?(follower, followed) and follower.ap_id != followed.ap_id do
411 412 413
      q =
        from(u in User,
          where: u.id == ^follower.id,
rinpatch's avatar
rinpatch committed
414 415
          update: [pull: [following: ^ap_followers]],
          select: u
416
        )
lain's avatar
lain committed
417

rinpatch's avatar
rinpatch committed
418
      {1, [follower]} = Repo.update_all(q, [])
419 420 421

      {:ok, followed} = update_follower_count(followed)

422 423
      set_cache(follower)

424
      {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
425
    else
426
      {:error, "Not subscribed!"}
427
    end
lain's avatar
lain committed
428
  end
429

Maksim's avatar
Maksim committed
430
  @spec following?(User.t(), User.t()) :: boolean
431
  def following?(%User{} = follower, %User{} = followed) do
432
    Enum.member?(follower.following, followed.follower_address)
433
  end
lain's avatar
lain committed
434

435 436 437 438 439
  def follow_import(%User{} = follower, followed_identifiers)
      when is_list(followed_identifiers) do
    Enum.map(
      followed_identifiers,
      fn followed_identifier ->
440
        with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
441 442 443 444 445 446 447 448 449 450 451 452
             {: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

453
  def locked?(%User{} = user) do
454
    user.info.locked || false
455 456
  end

457 458 459 460
  def get_by_id(id) do
    Repo.get_by(User, id: id)
  end

lain's avatar
lain committed
461 462 463 464
  def get_by_ap_id(ap_id) do
    Repo.get_by(User, ap_id: ap_id)
  end

465 466
  # 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
467 468 469 470 471
  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
472
    get_cached_by_nickname(nickname)
473 474
  end

minibikini's avatar
minibikini committed
475 476 477 478
  def set_cache({:ok, user}), do: set_cache(user)
  def set_cache({:error, err}), do: {:error, err}

  def set_cache(%User{} = user) do
479 480 481 482 483 484
    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
485 486
  def update_and_set_cache(changeset) do
    with {:ok, user} <- Repo.update(changeset) do
487
      set_cache(user)
lain's avatar
lain committed
488 489 490 491 492
    else
      e -> e
    end
  end

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

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

504 505
  def get_cached_by_id(id) do
    key = "id:#{id}"
506 507 508 509

    ap_id =
      Cachex.fetch!(:user_cache, key, fn _ ->
        user = get_by_id(id)
510 511 512 513 514 515 516

        if user do
          Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
          {:commit, user.ap_id}
        else
          {:ignore, ""}
        end
517 518 519
      end)

    get_cached_by_ap_id(ap_id)
520 521
  end

lain's avatar
lain committed
522
  def get_cached_by_nickname(nickname) do
523
    key = "nickname:#{nickname}"
0x1C3B00DA's avatar
Run  
0x1C3B00DA committed
524

525 526 527 528 529
    Cachex.fetch!(:user_cache, key, fn ->
      user_result = get_or_fetch_by_nickname(nickname)

      case user_result do
        {:ok, user} -> {:commit, user}
Alexander Strizhakov's avatar
Alexander Strizhakov committed
530
        {:error, _error} -> {:ignore, nil}
531 532
      end
    end)
lain's avatar
lain committed
533
  end
lain's avatar
lain committed
534

535
  def get_cached_by_nickname_or_id(nickname_or_id) do
536
    get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
537 538
  end

lain's avatar
lain committed
539
  def get_by_nickname(nickname) do
540
    Repo.get_by(User, nickname: nickname) ||
541
      if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
542
        Repo.get_by(User, nickname: local_nickname(nickname))
543
      end
544 545
  end

546 547
  def get_by_email(email), do: Repo.get_by(User, email: email)

548
  def get_by_nickname_or_email(nickname_or_email) do
549
    get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
550 551
  end

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

lain's avatar
lain committed
557 558 559 560 561 562 563 564 565
  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
566
  def get_or_fetch_by_nickname(nickname) do
lain's avatar
lain committed
567
    with %User{} = user <- get_by_nickname(nickname) do
568
      {:ok, user}
lain's avatar
lain committed
569 570 571 572
    else
      _e ->
        with [_nick, _domain] <- String.split(nickname, "@"),
             {:ok, user} <- fetch_by_nickname(nickname) do
573
          if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
minibikini's avatar
minibikini committed
574
            # TODO turn into job
575 576 577
            {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
          end

578
          {:ok, user}
lain's avatar
lain committed
579
        else
Alexander Strizhakov's avatar
Alexander Strizhakov committed
580
          _e -> {:error, "not found " <> nickname}
lain's avatar
lain committed
581
        end
lain's avatar
lain committed
582
    end
lain's avatar
lain committed
583
  end
lain's avatar
lain committed
584

585 586 587 588 589 590 591 592 593 594 595
  @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

596
  def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
597 598 599 600 601
    from(
      u in User,
      where: fragment("? <@ ?", ^[follower_address], u.following),
      where: u.id != ^id
    )
602
    |> restrict_deactivated()
603 604
  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
    from(
      u in User,
      where: u.follower_address in ^following,
      where: u.id != ^id
    )
630
    |> restrict_deactivated()
631 632
  end

633
  def get_friends_query(user, page) do
Maxim Filippov's avatar
Maxim Filippov committed
634 635
    from(u in get_friends_query(user, nil))
    |> paginate(page, 20)
636 637 638 639 640 641
  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
642 643 644

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

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

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

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

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

    {:ok, users}
  end

688
  def increase_note_count(%User{} = user) do
689 690 691 692 693 694 695 696 697 698 699 700
    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
          )
      ]
    )
rinpatch's avatar
rinpatch committed
701 702
    |> select([u], u)
    |> Repo.update_all([])
703 704 705 706
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
707 708
  end

709
  def decrease_note_count(%User{} = user) do
710 711 712 713 714 715 716 717 718 719 720 721
    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
          )
      ]
    )
rinpatch's avatar
rinpatch committed
722 723
    |> select([u], u)
    |> Repo.update_all([])
724 725 726 727
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
728 729
  end

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

    note_count = Repo.one(note_count_query)

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

742 743 744 745
    user
    |> change()
    |> put_embed(:info, info_cng)
    |> update_and_set_cache()
746 747 748
  end

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

756 757 758 759 760 761 762 763 764 765 766 767 768
    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
          )
      ]
    )
rinpatch's avatar
rinpatch committed
769 770
    |> select([u], u)
    |> Repo.update_all([])
771 772 773 774
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
775
  end
776

777
  def get_users_from_set_query(ap_ids, false) do
778 779
    from(
      u in User,
780
      where: u.ap_id in ^ap_ids
781 782 783
    )
  end

784 785
  def get_users_from_set_query(ap_ids, true) do
    query = get_users_from_set_query(ap_ids, false)
786 787 788

    from(
      u in query,
789 790 791 792
      where: u.local == true
    )
  end

793 794 795 796 797
  def get_users_from_set(ap_ids, local_only \\ true) do
    get_users_from_set_query(ap_ids, local_only)
    |> Repo.all()
  end

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

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

808 809 810
    Repo.all(query)
  end

811
  def search(query, resolve \\ false, for_user \\ nil) do
812
    # Strip the beginning @ off if there is a query
813 814
    query = String.trim_leading(query, "@")

815
    if resolve, do: get_or_fetch(query)
lain's avatar
lain committed
816

817
    {:ok, results} =
lain's avatar
lain committed
818 819
      Repo.transaction(fn ->
        Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
820
        Repo.all(search_query(query, for_user))
lain's avatar
lain committed
821
      end)
lain's avatar
lain committed
822

823
    results
824
  end
lain's avatar
lain committed
825

826 827 828
  def search_query(query, for_user) do
    fts_subquery = fts_search_subquery(query)
    trigram_subquery = trigram_search_subquery(query)
829 830
    union_query = from(s in trigram_subquery, union_all: ^fts_subquery)
    distinct_query = from(s in subquery(union_query), order_by: s.search_type, distinct: s.id)
lain's avatar
lain committed
831

832 833 834 835 836
    from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
      order_by: [desc: s.search_rank],
      limit: 20
    )
  end
837

838 839 840 841 842
  defp boost_search_rank_query(query, nil), do: query

  defp boost_search_rank_query(query, for_user) do
    friends_ids = get_friends_ids(for_user)
    followers_ids = get_followers_ids(for_user)
843

844 845 846 847 848
    from(u in subquery(query),
      select_merge: %{
        search_rank:
          fragment(
            """
849
             CASE WHEN (?) THEN (?) * 1.3
850 851 852 853 854 855 856 857 858 859 860 861 862 863
             WHEN (?) THEN (?) * 1.2
             WHEN (?) THEN (?) * 1.1
             ELSE (?) END
            """,
            u.id in ^friends_ids and u.id in ^followers_ids,
            u.search_rank,
            u.id in ^friends_ids,
            u.search_rank,
            u.id in ^followers_ids,
            u.search_rank,
            u.search_rank
          )
      }
    )
864
  end
865

Maxim Filippov's avatar
Maxim Filippov committed
866
  defp fts_search_subquery(term, query \\ User) do
867
    processed_query =
868
      term
869 870 871 872 873
      |> String.replace(~r/\W+/, " ")
      |> String.trim()
      |> String.split()
      |> Enum.map(&(&1 <> ":*"))
      |> Enum.join(" | ")
874

875
    from(
876
      u in query,
877
      select_merge: %{
878
        search_type: ^0,
879 880 881 882 883 884 885 886 887 888 889 890 891 892 893
        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
894 895 896 897 898 899 900 901 902 903
      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