user.ex 36 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
  def register_changeset(struct, params \\ %{}, opts \\ []) do
207 208 209
    need_confirmation? =
      if is_nil(opts[:need_confirmation]) do
        Pleroma.Config.get([:instance, :account_activation_required])
210
      else
211
        opts[:need_confirmation]
212 213
      end

214 215
    info_change =
      User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
Ivan Tashkinov's avatar
Ivan Tashkinov committed
216

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

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

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

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

254 255 256 257
  defp autofollow_users(user) do
    candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])

    autofollowed_users =
Alexander Strizhakov's avatar
Alexander Strizhakov committed
258
      User.Query.build(%{nickname: candidates, local: true})
259 260
      |> Repo.all()

lain's avatar
lain committed
261
    follow_all(user, autofollowed_users)
262 263
  end

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

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

      {:ok, :enqueued}
283 284 285 286 287
    else
      {:ok, :noop}
    end
  end

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

  def needs_update?(_), do: true

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

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

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

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

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

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

lain's avatar
lain committed
352
    set_cache(follower)
lain's avatar
lain committed
353 354
  end

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

359
    ap_followers = followed.follower_address
360

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

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

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

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

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

382 383
        {:ok, _} = update_follower_count(followed)

384
        set_cache(follower)
385
    end
lain's avatar
lain committed
386
  end
lain's avatar
lain committed
387 388

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

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

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

      {:ok, followed} = update_follower_count(followed)

403 404
      set_cache(follower)

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

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

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

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

438 439 440 441
  def get_by_id(id) do
    Repo.get_by(User, id: id)
  end

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

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

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

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

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

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

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

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

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

    get_cached_by_ap_id(ap_id)
501 502
  end

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

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

516
  def get_cached_by_nickname_or_id(nickname_or_id) do
517
    get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
518 519
  end

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

527 528
  def get_by_email(email), do: Repo.get_by(User, email: email)

529
  def get_by_nickname_or_email(nickname_or_email) do
530
    get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
531 532
  end

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

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

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

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

Alexander Strizhakov's avatar
Alexander Strizhakov committed
577 578 579
  @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
  def get_followers_query(%User{} = user, nil) do
    User.Query.build(%{followers: user})
580 581
  end

582
  def get_followers_query(user, page) do
Maxim Filippov's avatar
Maxim Filippov committed
583
    from(u in get_followers_query(user, nil))
Alexander Strizhakov's avatar
Alexander Strizhakov committed
584
    |> User.Query.paginate(page, 20)
585 586
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
587
  @spec get_followers_query(User.t()) :: Ecto.Query.t()
588 589 590 591
  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
592 593 594 595

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

596 597 598 599 600 601
  def get_followers_ids(user, page \\ nil) do
    q = get_followers_query(user, page)

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

Alexander Strizhakov's avatar
Alexander Strizhakov committed
602 603 604
  @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
  def get_friends_query(%User{} = user, nil) do
    User.Query.build(%{friends: user})
605 606
  end

607
  def get_friends_query(user, page) do
Maxim Filippov's avatar
Maxim Filippov committed
608
    from(u in get_friends_query(user, nil))
Alexander Strizhakov's avatar
Alexander Strizhakov committed
609
    |> User.Query.paginate(page, 20)
610 611
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
612
  @spec get_friends_query(User.t()) :: Ecto.Query.t()
613 614 615 616
  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
617 618 619

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

621 622 623 624 625 626
  def get_friends_ids(user, page \\ nil) do
    q = get_friends_query(user, page)

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

Alexander Strizhakov's avatar
Alexander Strizhakov committed
627
  @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
628 629
  def get_follow_requests(%User{} = user) do
    users =
Alexander Strizhakov's avatar
Alexander Strizhakov committed
630
      Activity.follow_requests_for_actor(user)
rinpatch's avatar
rinpatch committed
631
      |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
632 633 634 635
      |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
      |> group_by([a, u], u.id)
      |> select([a, u], u)
      |> Repo.all()
636 637 638 639

    {:ok, users}
  end

640
  def increase_note_count(%User{} = user) do
641 642 643 644 645 646 647 648 649 650 651 652
    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
653 654
    |> select([u], u)
    |> Repo.update_all([])
655 656 657 658
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
659 660
  end

661
  def decrease_note_count(%User{} = user) do
662 663 664 665 666 667 668 669 670 671 672 673
    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
674 675
    |> select([u], u)
    |> Repo.update_all([])
676 677 678 679
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
680 681
  end

682
  def update_note_count(%User{} = user) do
lain's avatar
lain committed
683 684 685 686 687 688
    note_count_query =
      from(
        a in Object,
        where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
        select: count(a.id)
      )
689 690 691

    note_count = Repo.one(note_count_query)

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

lain's avatar
lain committed
694 695 696
    cng =
      change(user)
      |> put_embed(:info, info_cng)
697

lain's avatar
lain committed
698
    update_and_set_cache(cng)
699 700 701
  end

  def update_follower_count(%User{} = user) do
lain's avatar
lain committed
702
    follower_count_query =
Alexander Strizhakov's avatar
Alexander Strizhakov committed
703
      User.Query.build(%{followers: user}) |> select([u], %{count: count(u.id)})
704

705 706 707 708 709 710 711 712 713 714 715 716 717
    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
718 719
    |> select([u], u)
    |> Repo.update_all([])
720 721 722 723
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
724
  end
725

Alexander Strizhakov's avatar
Alexander Strizhakov committed
726
  @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
727
  def get_users_from_set(ap_ids, local_only \\ true) do
Alexander Strizhakov's avatar
Alexander Strizhakov committed
728 729 730 731
    criteria = %{ap_id: ap_ids}
    criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria

    User.Query.build(criteria)
732 733 734
    |> Repo.all()
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
735
  @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
736
  def get_recipients_from_activity(%Activity{recipients: to}) do
Alexander Strizhakov's avatar
Alexander Strizhakov committed
737 738
    User.Query.build(%{recipients_from_activity: to, local: true})
    |> Repo.all()
739 740
  end

741
  def search(query, resolve \\ false, for_user \\ nil) do
742
    # Strip the beginning @ off if there is a query
743 744
    query = String.trim_leading(query, "@")

745
    if resolve, do: get_or_fetch(query)
lain's avatar
lain committed
746

747
    {:ok, results} =
lain's avatar
lain committed
748 749
      Repo.transaction(fn ->
        Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
750
        Repo.all(search_query(query, for_user))
lain's avatar
lain committed
751
      end)
lain's avatar
lain committed
752

753
    results
754
  end
lain's avatar
lain committed
755

756 757 758
  def search_query(query, for_user) do
    fts_subquery = fts_search_subquery(query)
    trigram_subquery = trigram_search_subquery(query)
759 760
    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
761

762 763 764 765 766
    from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
      order_by: [desc: s.search_rank],
      limit: 20
    )
  end
767

768 769 770 771 772
  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)
773

774 775 776 777 778
    from(u in subquery(query),
      select_merge: %{
        search_rank:
          fragment(
            """
779
             CASE WHEN (?) THEN (?) * 1.3
780 781 782 783 784 785 786 787 788 789 790 791 792 793
             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
          )
      }
    )
794
  end
795

Maxim Filippov's avatar
Maxim Filippov committed
796
  defp fts_search_subquery(term, query \\ User) do
797
    processed_query =
798
      term
799 800 801 802 803
      |> String.replace(~r/\W+/, " ")
      |> String.trim()
      |> String.split()
      |> Enum.map(&(&1 <> ":*"))
      |> Enum.join(" | ")
804

805
    from(
806
      u in query,
807
      select_merge: %{
808
        search_type: ^0,
809 810 811 812 813 814 815 816 817 818 819 820 821 822 823
        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
824 825 826 827 828 829 830 831 832 833
      where:
        fragment(
          """
            (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||