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 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
  defp autofollow_users(user) do
    candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])

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

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

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

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

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

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

  def needs_update?(_), do: true

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

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

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

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

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

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

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

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

358
    ap_followers = followed.follower_address
359

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

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

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

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

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

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

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

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

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

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

      {:ok, followed} = update_follower_count(followed)

402 403
      set_cache(follower)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    get_cached_by_ap_id(ap_id)
500 501
  end

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

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

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

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

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

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

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

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

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

565 566 567 568 569 570 571 572 573 574 575
  @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
576 577 578
  @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})
579 580
  end

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

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

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

595 596 597 598 599 600
  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
601 602 603
  @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})
604 605
  end

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

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

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

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

    {:ok, users}
  end

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

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

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

    note_count = Repo.one(note_count_query)

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

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

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

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

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

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

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

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

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

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

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

752
    results
753
  end
lain's avatar
lain committed
754

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

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

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

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

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

804
    from(
805
      u in query,
806
      select_merge: %{
807
        search_type: ^0,
808 809 810 811 812 813 814 815 816 817 818 819 820 821 822
        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
823 824 825 826 827 828 829 830 831 832
      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
        )
833 834
    )
  end
835

Maxim Filippov's avatar
Maxim Filippov committed
836
  defp trigram_search_subquery(term) do
837 838 839
    from(
      u in User,
      select_merge: %{
840 841
        # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
        search_type: fragment("?", 1),
842 843
        search_rank:
          fragment(
844
            "similarity(?, trim(? || ' ' || coalesce(?, '')))",
845
            ^term,
846 847 848 849
            u.nickname,
            u.name
          )
      },
850
      where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
851 852 853
    )
  end

854 855 856 857
  def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
    Enum.map(
      blocked_identifiers,
      fn blocked_identifier ->
858
        with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
859 860 861 862 863 864 865 866 867 868 869 870
             {:ok, blocker} <- block(blocker, blocked),
             {:ok, _} <- ActivityPub.block(blocker, blocked) do
          blocked
        else
          err ->
            Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
            err
        end
      end
    )
  end

871
  def mute(muter, %User{ap_id: ap_id}) do
872 873 874 875 876 877 878
    info_cng =
      muter.info
      |> User.Info.add_to_mutes(ap_id)

    cng =
      change(muter)
      |> put_embed(:info