user.ex 38 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
  alias Comeonin.Pbkdf2
  alias Pleroma.Activity
  alias Pleroma.Notification
  alias Pleroma.Object
15
  alias Pleroma.Registration
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 44 45 46 47 48 49 50 51
    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)
52
    field(:search_rank, :float, virtual: true)
53
    field(:search_type, :integer, virtual: true)
54
    field(:tags, {:array, :string}, default: [])
rinpatch's avatar
rinpatch committed
55
    field(:last_refreshed_at, :naive_datetime_usec)
lain's avatar
lain committed
56
    has_many(:notifications, Notification)
57
    has_many(:registrations, Registration)
lain's avatar
lain committed
58
    embeds_one(:info, Pleroma.User.Info)
lain's avatar
lain committed
59 60 61

    timestamps()
  end
lain's avatar
lain committed
62

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

66
  def auth_active?(%User{}), do: true
67

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

76 77
  def visible_for?(_, _), do: false

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

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

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

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

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

104 105
  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
106

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

lain's avatar
lain committed
110
    %{
111
      following_count: length(user.following) - oneself,
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

lain's avatar
lain committed
120
  def remote_user_creation(params) do
lain's avatar
lain committed
121 122 123
    params =
      params
      |> Map.put(:info, params[:info] || %{})
lain's avatar
lain committed
124 125 126

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

lain's avatar
lain committed
127
    changes =
lain's avatar
lain committed
128
      %User{}
lain's avatar
lain committed
129
      |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
130
      |> validate_required([:name, :ap_id])
lain's avatar
lain committed
131 132 133 134 135
      |> 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
136
      |> put_embed(:info, info_cng)
lain's avatar
lain committed
137

138
    if changes.valid? do
lain's avatar
lain committed
139
      case info_cng.changes[:source_data] do
lain's avatar
lain committed
140 141 142
        %{"followers" => followers} ->
          changes
          |> put_change(:follower_address, followers)
lain's avatar
lain committed
143

lain's avatar
lain committed
144 145
        _ ->
          followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
lain's avatar
lain committed
146

lain's avatar
lain committed
147 148 149
          changes
          |> put_change(:follower_address, followers)
      end
150 151 152
    else
      changes
    end
lain's avatar
lain committed
153 154
  end

lain's avatar
lain committed
155
  def update_changeset(struct, params \\ %{}) do
Thog's avatar
Thog committed
156
    struct
lain's avatar
lain committed
157
    |> cast(params, [:bio, :name, :avatar])
lain's avatar
lain committed
158
    |> unique_constraint(:nickname)
href's avatar
href committed
159
    |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
160
    |> validate_length(:bio, max: 5000)
lain's avatar
lain committed
161 162 163
    |> validate_length(:name, min: 1, max: 100)
  end

lain's avatar
lain committed
164
  def upgrade_changeset(struct, params \\ %{}) do
165 166 167 168
    params =
      params
      |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())

lain's avatar
lain committed
169 170 171 172
    info_cng =
      struct.info
      |> User.Info.user_upgrade(params[:info])

lain's avatar
lain committed
173
    struct
lain's avatar
lain committed
174
    |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
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 178
    |> validate_length(:bio, max: 5000)
    |> validate_length(:name, max: 100)
lain's avatar
lain committed
179
    |> put_embed(:info, info_cng)
lain's avatar
lain committed
180 181
  end

Roger Braun's avatar
Roger Braun committed
182
  def password_update_changeset(struct, params) do
lain's avatar
lain committed
183 184 185 186 187
    changeset =
      struct
      |> cast(params, [:password, :password_confirmation])
      |> validate_required([:password, :password_confirmation])
      |> validate_confirmation(:password)
Roger Braun's avatar
Roger Braun committed
188

189 190 191
    OAuth.Token.delete_user_tokens(struct)
    OAuth.Authorization.delete_user_authorizations(struct)

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

Roger Braun's avatar
Roger Braun committed
195 196 197 198 199 200 201 202
      changeset
      |> put_change(:password_hash, hashed)
    else
      changeset
    end
  end

  def reset_password(user, data) do
lain's avatar
lain committed
203
    update_and_set_cache(password_update_changeset(user, data))
Roger Braun's avatar
Roger Braun committed
204 205
  end

206 207 208 209 210 211 212 213
  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
214 215
    info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)

lain's avatar
lain committed
216 217 218
    changeset =
      struct
      |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
219
      |> validate_required([:name, :nickname, :password, :password_confirmation])
lain's avatar
lain committed
220 221 222
      |> validate_confirmation(:password)
      |> unique_constraint(:email)
      |> unique_constraint(:nickname)
lain's avatar
lain committed
223
      |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
href's avatar
href committed
224
      |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
225 226 227
      |> validate_format(:email, @email_regex)
      |> validate_length(:bio, max: 1000)
      |> validate_length(:name, min: 1, max: 100)
Ivan Tashkinov's avatar
Ivan Tashkinov committed
228
      |> put_change(:info, info_change)
lain's avatar
lain committed
229

230 231 232 233 234 235 236
    changeset =
      if opts[:external] do
        changeset
      else
        validate_required(changeset, [:email])
      end

lain's avatar
lain committed
237
    if changeset.valid? do
238
      hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
lain's avatar
lain committed
239 240
      ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
      followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
lain's avatar
lain committed
241

lain's avatar
lain committed
242 243 244
      changeset
      |> put_change(:password_hash, hashed)
      |> put_change(:ap_id, ap_id)
rinpatch's avatar
rinpatch committed
245
      |> unique_constraint(:ap_id)
lain's avatar
lain committed
246
      |> put_change(:following, [followers])
247
      |> put_change(:follower_address, followers)
lain's avatar
lain committed
248 249 250 251 252
    else
      changeset
    end
  end

253 254 255 256 257 258 259 260 261 262
  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
263
    follow_all(user, autofollowed_users)
264 265
  end

266 267
  @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
268
    with {:ok, user} <- Repo.insert(changeset),
lain's avatar
lain committed
269
         {:ok, user} <- autofollow_users(user),
minibikini's avatar
minibikini committed
270
         {:ok, user} <- set_cache(user),
lain's avatar
lain committed
271
         {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
lain's avatar
lain committed
272
         {:ok, _} <- try_send_confirmation_email(user) do
273 274 275 276
      {:ok, user}
    end
  end

277
  def try_send_confirmation_email(%User{} = user) do
278 279
    if user.info.confirmation_pending &&
         Pleroma.Config.get([:instance, :account_activation_required]) do
280
      user
281 282
      |> Pleroma.Emails.UserEmail.account_confirmation_email()
      |> Pleroma.Emails.Mailer.deliver_async()
283 284

      {:ok, :enqueued}
285 286 287 288 289
    else
      {:ok, :noop}
    end
  end

290 291 292 293 294
  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
295
    NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
296 297 298 299
  end

  def needs_update?(_), do: true

lain's avatar
lain committed
300
  def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
301 302 303 304 305 306 307 308
    {: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
309
    if not User.ap_enabled?(followed) do
310
      follow(follower, followed)
311 312 313 314 315
    else
      {:ok, follower}
    end
  end

Maksim's avatar
Maksim committed
316
  def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
317 318
    if not following?(follower, followed) do
      follow(follower, followed)
319
    else
320
      {:ok, follower}
321 322 323
    end
  end

324
  @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
lain's avatar
lain committed
325 326
  @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
  def follow_all(follower, followeds) do
lain's avatar
lain committed
327 328
    followed_addresses =
      followeds
329
      |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
lain's avatar
lain committed
330
      |> Enum.map(fn %{follower_address: fa} -> fa end)
lain's avatar
lain committed
331

lain's avatar
lain committed
332 333 334
    q =
      from(u in User,
        where: u.id == ^follower.id,
335 336 337 338 339 340 341 342 343
        update: [
          set: [
            following:
              fragment(
                "array(select distinct unnest (array_cat(?, ?)))",
                u.following,
                ^followed_addresses
              )
          ]
rinpatch's avatar
rinpatch committed
344 345
        ],
        select: u
lain's avatar
lain committed
346 347
      )

rinpatch's avatar
rinpatch committed
348
    {1, [follower]} = Repo.update_all(q, [])
lain's avatar
lain committed
349 350 351 352 353

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

lain's avatar
lain committed
354
    set_cache(follower)
lain's avatar
lain committed
355 356
  end

lain's avatar
lain committed
357
  def follow(%User{} = follower, %User{info: info} = followed) do
358 359
    user_config = Application.get_env(:pleroma, :user)
    deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
360

361
    ap_followers = followed.follower_address
362

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

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

370 371 372 373 374
      true ->
        if !followed.local && follower.local && !ap_enabled?(followed) do
          Websub.subscribe(follower, followed)
        end

375 376 377
        q =
          from(u in User,
            where: u.id == ^follower.id,
rinpatch's avatar
rinpatch committed
378 379
            update: [push: [following: ^ap_followers]],
            select: u
380
          )
381

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

384 385
        {:ok, _} = update_follower_count(followed)

386
        set_cache(follower)
387
    end
lain's avatar
lain committed
388
  end
lain's avatar
lain committed
389 390

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

393
    if following?(follower, followed) and follower.ap_id != followed.ap_id do
394 395 396
      q =
        from(u in User,
          where: u.id == ^follower.id,
rinpatch's avatar
rinpatch committed
397 398
          update: [pull: [following: ^ap_followers]],
          select: u
399
        )
lain's avatar
lain committed
400

rinpatch's avatar
rinpatch committed
401
      {1, [follower]} = Repo.update_all(q, [])
402 403 404

      {:ok, followed} = update_follower_count(followed)

405 406
      set_cache(follower)

407
      {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
408
    else
409
      {:error, "Not subscribed!"}
410
    end
lain's avatar
lain committed
411
  end
412

Maksim's avatar
Maksim committed
413
  @spec following?(User.t(), User.t()) :: boolean
414
  def following?(%User{} = follower, %User{} = followed) do
415
    Enum.member?(follower.following, followed.follower_address)
416
  end
lain's avatar
lain committed
417

418 419 420 421 422
  def follow_import(%User{} = follower, followed_identifiers)
      when is_list(followed_identifiers) do
    Enum.map(
      followed_identifiers,
      fn followed_identifier ->
423
        with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
424 425 426 427 428 429 430 431 432 433 434 435
             {: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

436
  def locked?(%User{} = user) do
437
    user.info.locked || false
438 439
  end

440 441 442 443
  def get_by_id(id) do
    Repo.get_by(User, id: id)
  end

lain's avatar
lain committed
444 445 446 447
  def get_by_ap_id(ap_id) do
    Repo.get_by(User, ap_id: ap_id)
  end

448 449
  # 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
450 451 452 453 454
  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
455
    get_cached_by_nickname(nickname)
456 457
  end

minibikini's avatar
minibikini committed
458 459 460 461
  def set_cache({:ok, user}), do: set_cache(user)
  def set_cache({:error, err}), do: {:error, err}

  def set_cache(%User{} = user) do
462 463 464 465 466 467
    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
468 469
  def update_and_set_cache(changeset) do
    with {:ok, user} <- Repo.update(changeset) do
470
      set_cache(user)
lain's avatar
lain committed
471 472 473 474 475
    else
      e -> e
    end
  end

lain's avatar
lain committed
476 477 478
  def invalidate_cache(user) do
    Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
    Cachex.del(:user_cache, "nickname:#{user.nickname}")
479
    Cachex.del(:user_cache, "user_info:#{user.id}")
lain's avatar
lain committed
480 481
  end

lain's avatar
lain committed
482
  def get_cached_by_ap_id(ap_id) do
483
    key = "ap_id:#{ap_id}"
Thog's avatar
Thog committed
484
    Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
lain's avatar
lain committed
485 486
  end

487 488
  def get_cached_by_id(id) do
    key = "id:#{id}"
489 490 491 492

    ap_id =
      Cachex.fetch!(:user_cache, key, fn _ ->
        user = get_by_id(id)
493 494 495 496 497 498 499

        if user do
          Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
          {:commit, user.ap_id}
        else
          {:ignore, ""}
        end
500 501 502
      end)

    get_cached_by_ap_id(ap_id)
503 504
  end

lain's avatar
lain committed
505
  def get_cached_by_nickname(nickname) do
506
    key = "nickname:#{nickname}"
0x1C3B00DA's avatar
Run  
0x1C3B00DA committed
507

508 509 510 511 512
    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
513
        {:error, _error} -> {:ignore, nil}
514 515
      end
    end)
lain's avatar
lain committed
516
  end
lain's avatar
lain committed
517

518
  def get_cached_by_nickname_or_id(nickname_or_id) do
519
    get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
520 521
  end

lain's avatar
lain committed
522
  def get_by_nickname(nickname) do
523
    Repo.get_by(User, nickname: nickname) ||
524
      if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
525
        Repo.get_by(User, nickname: local_nickname(nickname))
526
      end
527 528
  end

529 530
  def get_by_email(email), do: Repo.get_by(User, email: email)

531
  def get_by_nickname_or_email(nickname_or_email) do
532
    get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
533 534
  end

lain's avatar
lain committed
535 536
  def get_cached_user_info(user) do
    key = "user_info:#{user.id}"
Thog's avatar
Thog committed
537
    Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
lain's avatar
lain committed
538
  end
lain's avatar
lain committed
539

lain's avatar
lain committed
540 541 542 543 544 545 546 547 548
  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
549
  def get_or_fetch_by_nickname(nickname) do
lain's avatar
lain committed
550
    with %User{} = user <- get_by_nickname(nickname) do
551
      {:ok, user}
lain's avatar
lain committed
552 553 554 555
    else
      _e ->
        with [_nick, _domain] <- String.split(nickname, "@"),
             {:ok, user} <- fetch_by_nickname(nickname) do
556
          if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
minibikini's avatar
minibikini committed
557
            # TODO turn into job
558 559 560
            {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
          end

561
          {:ok, user}
lain's avatar
lain committed
562
        else
Alexander Strizhakov's avatar
Alexander Strizhakov committed
563
          _e -> {:error, "not found " <> nickname}
lain's avatar
lain committed
564
        end
lain's avatar
lain committed
565
    end
lain's avatar
lain committed
566
  end
lain's avatar
lain committed
567

568 569 570 571 572 573 574 575 576 577 578
  @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

579
  def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
580 581 582 583 584 585 586
    from(
      u in User,
      where: fragment("? <@ ?", ^[follower_address], u.following),
      where: u.id != ^id
    )
  end

587
  def get_followers_query(user, page) do
Maxim Filippov's avatar
Maxim Filippov committed
588 589
    from(u in get_followers_query(user, nil))
    |> paginate(page, 20)
590 591 592 593 594 595
  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
596 597 598 599

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

600 601 602 603 604 605
  def get_followers_ids(user, page \\ nil) do
    q = get_followers_query(user, page)

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

606
  def get_friends_query(%User{id: id, following: following}, nil) do
607 608 609 610 611 612 613
    from(
      u in User,
      where: u.follower_address in ^following,
      where: u.id != ^id
    )
  end

614
  def get_friends_query(user, page) do
Maxim Filippov's avatar
Maxim Filippov committed
615 616
    from(u in get_friends_query(user, nil))
    |> paginate(page, 20)
617 618 619 620 621 622
  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
623 624 625

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

627 628 629 630 631 632
  def get_friends_ids(user, page \\ nil) do
    q = get_friends_query(user, page)

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

633 634 635
  def get_follow_requests_query(%User{} = user) do
    from(
      a in Activity,
kaniini's avatar
kaniini committed
636 637 638 639 640 641 642 643 644 645 646 647
      where:
        fragment(
          "? ->> 'type' = 'Follow'",
          a.data
        ),
      where:
        fragment(
          "? ->> 'state' = 'pending'",
          a.data
        ),
      where:
        fragment(
648
          "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
kaniini's avatar
kaniini committed
649
          a.data,
650 651
          a.data,
          ^user.ap_id
kaniini's avatar
kaniini committed
652
        )
653 654 655 656 657
    )
  end

  def get_follow_requests(%User{} = user) do
    users =
658 659
      user
      |> User.get_follow_requests_query()
rinpatch's avatar
rinpatch committed
660
      |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
661 662 663 664
      |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
      |> group_by([a, u], u.id)
      |> select([a, u], u)
      |> Repo.all()
665 666 667 668

    {:ok, users}
  end

669
  def increase_note_count(%User{} = user) do
670 671 672 673 674 675 676 677 678 679 680 681
    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
682 683
    |> select([u], u)
    |> Repo.update_all([])
684 685 686 687
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
688 689
  end

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

711
  def update_note_count(%User{} = user) do
lain's avatar
lain committed
712 713 714 715 716 717
    note_count_query =
      from(
        a in Object,
        where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
        select: count(a.id)
      )
718 719 720

    note_count = Repo.one(note_count_query)

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

lain's avatar
lain committed
723 724 725
    cng =
      change(user)
      |> put_embed(:info, info_cng)
726

lain's avatar
lain committed
727
    update_and_set_cache(cng)
728 729 730
  end

  def update_follower_count(%User{} = user) do
lain's avatar
lain committed
731
    follower_count_query =
732 733 734 735
      User
      |> where([u], ^user.follower_address in u.following)
      |> where([u], u.id != ^user.id)
      |> select([u], %{count: count(u.id)})
736

737 738 739 740 741 742 743 744 745 746 747 748 749
    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
750 751
    |> select([u], u)
    |> Repo.update_all([])
752 753 754 755
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
756
  end
757

758
  def get_users_from_set_query(ap_ids, false) do
759 760
    from(
      u in User,
761
      where: u.ap_id in ^ap_ids
762 763 764
    )
  end

765 766
  def get_users_from_set_query(ap_ids, true) do
    query = get_users_from_set_query(ap_ids, false)
767 768 769

    from(
      u in query,
770 771 772 773
      where: u.local == true
    )
  end

774 775 776 777 778
  def get_users_from_set(ap_ids, local_only \\ true) do
    get_users_from_set_query(ap_ids, local_only)
    |> Repo.all()
  end

779
  def get_recipients_from_activity(%Activity{recipients: to}) do
lain's avatar
lain committed
780 781 782 783 784 785
    query =
      from(
        u in User,
        where: u.ap_id in ^to,
        or_where: fragment("? && ?", u.following, ^to)
      )
786

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

789 790 791
    Repo.all(query)
  end

792
  def search(query, resolve \\ false, for_user \\ nil) do
793
    # Strip the beginning @ off if there is a query
794 795
    query = String.trim_leading(query, "@")

796
    if resolve, do: get_or_fetch(query)
lain's avatar
lain committed
797

798
    {:ok, results} =
lain's avatar
lain committed
799 800
      Repo.transaction(fn ->
        Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
801
        Repo.all(search_query(query, for_user))
lain's avatar
lain committed
802
      end)
lain's avatar
lain committed
803

804
    results
805
  end
lain's avatar
lain committed
806

807 808 809
  def search_query(query, for_user) do
    fts_subquery = fts_search_subquery(query)
    trigram_subquery = trigram_search_subquery(query)
810 811
    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
812

813 814 815 816 817
    from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
      order_by: [desc: s.search_rank],
      limit: 20
    )
  end
818

819 820 821 822 823
  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)
824

825 826 827 828 829
    from(u in subquery(query),
      select_merge: %{
        search_rank:
          fragment(
            """
830
             CASE WHEN (?) THEN (?) * 1.3
831 832 833 834 835 836 837 838 839 840 841 842 843 844
             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
          )
      }
    )
845
  end
846

Maxim Filippov's avatar
Maxim Filippov committed
847
  defp fts_search_subquery(term, query \\ User) do
848
    processed_query =
849
      term
850 851 852 853 854
      |> String.replace(~r/\W+/, " ")
      |> String.trim()
      |> String.split()
      |> Enum.map(&(&1 <> ":*"))
      |> Enum.join(" | ")
855

856
    from(
857
      u in query,
858
      select_merge: %{
859
        search_type: ^0,
860 861 862 863 864 865 866 867 868 869 870 871 872 873 874
        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
875 876 877 878 879 880 881 882 883 884
      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
        )
885 886
    )
  end
887

Maxim Filippov's avatar
Maxim Filippov committed
888
  defp trigram_search_subquery(term) do
889 890 891
    from(
      u in User,
      select_merge: %{
892 893
        # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
        search_type: fragment("?", 1),
894 895
        search_rank:
          fragment(
896
            "similarity(?, trim(? || ' ' || coalesce(?, '')))",
897
            ^term,
898 899 900 901
            u.nickname,
            u.name
          )
      },