user.ex 31.7 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 11 12 13 14 15 16
  import Ecto.Changeset
  import Ecto.Query

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

26 27
  require Logger

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

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

href's avatar
href committed
32 33 34
  @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
35
  @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
href's avatar
href committed
36

lain's avatar
lain committed
37
  schema "users" do
lain's avatar
lain committed
38 39 40 41 42 43 44 45 46 47 48 49
    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)
50
    field(:search_rank, :float, virtual: true)
51
    field(:tags, {:array, :string}, default: [])
Haelwenn's avatar
Haelwenn committed
52
    field(:bookmarks, {:array, :string}, default: [])
53
    field(:last_refreshed_at, :naive_datetime)
lain's avatar
lain committed
54
    has_many(:notifications, Notification)
lain's avatar
lain committed
55
    embeds_one(:info, Pleroma.User.Info)
lain's avatar
lain committed
56 57 58

    timestamps()
  end
lain's avatar
lain committed
59

60
  def auth_active?(%User{local: false}), do: true
61

62 63 64 65
  def auth_active?(%User{info: %User.Info{confirmation_pending: false}}), do: true

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

67
  def auth_active?(_), do: false
68

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

191 192 193
    OAuth.Token.delete_user_tokens(struct)
    OAuth.Authorization.delete_user_authorizations(struct)

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

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

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

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

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

    if changeset.valid? do
233
      hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
lain's avatar
lain committed
234 235
      ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
      followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
lain's avatar
lain committed
236

lain's avatar
lain committed
237 238 239
      changeset
      |> put_change(:password_hash, hashed)
      |> put_change(:ap_id, ap_id)
rinpatch's avatar
rinpatch committed
240
      |> unique_constraint(:ap_id)
lain's avatar
lain committed
241
      |> put_change(:following, [followers])
242
      |> put_change(:follower_address, followers)
lain's avatar
lain committed
243 244 245 246 247
    else
      changeset
    end
  end

248 249 250 251 252 253 254 255 256 257
  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
258
    follow_all(user, autofollowed_users)
259 260
  end

261 262
  @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
263
    with {:ok, user} <- Repo.insert(changeset),
lain's avatar
lain committed
264 265
         {:ok, user} <- autofollow_users(user),
         {:ok, _} <- try_send_confirmation_email(user) do
266 267 268 269
      {:ok, user}
    end
  end

270
  def try_send_confirmation_email(%User{} = user) do
271 272
    if user.info.confirmation_pending &&
         Pleroma.Config.get([:instance, :account_activation_required]) do
273 274 275 276 277 278 279 280
      user
      |> Pleroma.UserEmail.account_confirmation_email()
      |> Pleroma.Mailer.deliver()
    else
      {:ok, :noop}
    end
  end

281 282 283 284 285 286 287 288 289 290
  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
    NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86400
  end

  def needs_update?(_), do: true

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

Maksim's avatar
Maksim committed
307
  def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
308 309
    if not following?(follower, followed) do
      follow(follower, followed)
310
    else
311
      {:ok, follower}
312 313 314
    end
  end

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

lain's avatar
lain committed
323 324 325
    q =
      from(u in User,
        where: u.id == ^follower.id,
326 327 328 329 330 331 332 333 334 335
        update: [
          set: [
            following:
              fragment(
                "array(select distinct unnest (array_cat(?, ?)))",
                u.following,
                ^followed_addresses
              )
          ]
        ]
lain's avatar
lain committed
336 337 338
      )

    {1, [follower]} = Repo.update_all(q, [], returning: true)
lain's avatar
lain committed
339 340 341 342 343

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

lain's avatar
lain committed
344
    set_cache(follower)
lain's avatar
lain committed
345 346
  end

lain's avatar
lain committed
347
  def follow(%User{} = follower, %User{info: info} = followed) do
348 349
    user_config = Application.get_env(:pleroma, :user)
    deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
350

351
    ap_followers = followed.follower_address
352

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

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

360 361 362 363 364
      true ->
        if !followed.local && follower.local && !ap_enabled?(followed) do
          Websub.subscribe(follower, followed)
        end

365 366 367 368 369
        q =
          from(u in User,
            where: u.id == ^follower.id,
            update: [push: [following: ^ap_followers]]
          )
370

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

373 374
        {:ok, _} = update_follower_count(followed)

375
        set_cache(follower)
376
    end
lain's avatar
lain committed
377
  end
lain's avatar
lain committed
378 379

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

382
    if following?(follower, followed) and follower.ap_id != followed.ap_id do
383 384 385 386 387
      q =
        from(u in User,
          where: u.id == ^follower.id,
          update: [pull: [following: ^ap_followers]]
        )
lain's avatar
lain committed
388

389
      {1, [follower]} = Repo.update_all(q, [], returning: true)
390 391 392

      {:ok, followed} = update_follower_count(followed)

393 394
      set_cache(follower)

395
      {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
396
    else
397
      {:error, "Not subscribed!"}
398
    end
lain's avatar
lain committed
399
  end
400

Maksim's avatar
Maksim committed
401
  @spec following?(User.t(), User.t()) :: boolean
402
  def following?(%User{} = follower, %User{} = followed) do
403
    Enum.member?(follower.following, followed.follower_address)
404
  end
lain's avatar
lain committed
405

406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
  def follow_import(%User{} = follower, followed_identifiers)
      when is_list(followed_identifiers) do
    Enum.map(
      followed_identifiers,
      fn followed_identifier ->
        with %User{} = followed <- get_or_fetch(followed_identifier),
             {:ok, follower} <- maybe_direct_follow(follower, followed),
             {:ok, _} <- ActivityPub.follow(follower, followed) do
          followed
        else
          err ->
            Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
            err
        end
      end
    )
  end

424
  def locked?(%User{} = user) do
425
    user.info.locked || false
426 427
  end

428 429 430 431
  def get_by_id(id) do
    Repo.get_by(User, id: id)
  end

lain's avatar
lain committed
432 433 434 435
  def get_by_ap_id(ap_id) do
    Repo.get_by(User, ap_id: ap_id)
  end

436 437 438 439 440 441 442 443 444
  # 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
  def get_by_guessed_nickname(ap_id) do
    domain = URI.parse(ap_id).host
    name = List.last(String.split(ap_id, "/"))
    nickname = "#{name}@#{domain}"

    get_by_nickname(nickname)
  end

445 446 447 448 449 450 451
  def set_cache(user) do
    Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
    Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
    Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
    {:ok, user}
  end

lain's avatar
lain committed
452 453
  def update_and_set_cache(changeset) do
    with {:ok, user} <- Repo.update(changeset) do
454
      set_cache(user)
lain's avatar
lain committed
455 456 457 458 459
    else
      e -> e
    end
  end

lain's avatar
lain committed
460 461 462
  def invalidate_cache(user) do
    Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
    Cachex.del(:user_cache, "nickname:#{user.nickname}")
463
    Cachex.del(:user_cache, "user_info:#{user.id}")
lain's avatar
lain committed
464 465
  end

lain's avatar
lain committed
466
  def get_cached_by_ap_id(ap_id) do
467
    key = "ap_id:#{ap_id}"
Thog's avatar
Thog committed
468
    Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
lain's avatar
lain committed
469 470
  end

471 472
  def get_cached_by_id(id) do
    key = "id:#{id}"
473 474 475 476

    ap_id =
      Cachex.fetch!(:user_cache, key, fn _ ->
        user = get_by_id(id)
477 478 479 480 481 482 483

        if user do
          Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
          {:commit, user.ap_id}
        else
          {:ignore, ""}
        end
484 485 486
      end)

    get_cached_by_ap_id(ap_id)
487 488
  end

lain's avatar
lain committed
489
  def get_cached_by_nickname(nickname) do
490
    key = "nickname:#{nickname}"
Thog's avatar
Thog committed
491
    Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
lain's avatar
lain committed
492
  end
lain's avatar
lain committed
493

494
  def get_cached_by_nickname_or_id(nickname_or_id) do
495
    get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
496 497
  end

lain's avatar
lain committed
498
  def get_by_nickname(nickname) do
499
    Repo.get_by(User, nickname: nickname) ||
500
      if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
501
        Repo.get_by(User, nickname: local_nickname(nickname))
502
      end
503 504
  end

505 506 507 508 509 510 511
  def get_by_nickname_or_email(nickname_or_email) do
    case user = Repo.get_by(User, nickname: nickname_or_email) do
      %User{} -> user
      nil -> Repo.get_by(User, email: nickname_or_email)
    end
  end

lain's avatar
lain committed
512 513
  def get_cached_user_info(user) do
    key = "user_info:#{user.id}"
Thog's avatar
Thog committed
514
    Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
lain's avatar
lain committed
515
  end
lain's avatar
lain committed
516

lain's avatar
lain committed
517 518 519 520 521 522 523 524 525
  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
526
  def get_or_fetch_by_nickname(nickname) do
lain's avatar
lain committed
527
    with %User{} = user <- get_by_nickname(nickname) do
lain's avatar
lain committed
528
      user
lain's avatar
lain committed
529 530 531 532 533 534 535 536
    else
      _e ->
        with [_nick, _domain] <- String.split(nickname, "@"),
             {:ok, user} <- fetch_by_nickname(nickname) do
          user
        else
          _e -> nil
        end
lain's avatar
lain committed
537
    end
lain's avatar
lain committed
538
  end
lain's avatar
lain committed
539

540
  def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
541 542 543 544 545 546 547
    from(
      u in User,
      where: fragment("? <@ ?", ^[follower_address], u.following),
      where: u.id != ^id
    )
  end

548 549 550 551 552 553 554 555 556 557 558 559
  def get_followers_query(user, page) do
    from(
      u in get_followers_query(user, nil),
      limit: 20,
      offset: ^((page - 1) * 20)
    )
  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
560 561 562 563

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

564 565 566 567 568 569
  def get_followers_ids(user, page \\ nil) do
    q = get_followers_query(user, page)

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

570
  def get_friends_query(%User{id: id, following: following}, nil) do
571 572 573 574 575 576 577
    from(
      u in User,
      where: u.follower_address in ^following,
      where: u.id != ^id
    )
  end

578 579 580 581 582 583 584 585 586 587 588 589
  def get_friends_query(user, page) do
    from(
      u in get_friends_query(user, nil),
      limit: 20,
      offset: ^((page - 1) * 20)
    )
  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
590 591 592

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

594 595 596 597 598 599
  def get_friends_ids(user, page \\ nil) do
    q = get_friends_query(user, page)

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

600 601 602
  def get_follow_requests_query(%User{} = user) do
    from(
      a in Activity,
kaniini's avatar
kaniini committed
603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
      where:
        fragment(
          "? ->> 'type' = 'Follow'",
          a.data
        ),
      where:
        fragment(
          "? ->> 'state' = 'pending'",
          a.data
        ),
      where:
        fragment(
          "? @> ?",
          a.data,
          ^%{"object" => user.ap_id}
        )
619 620 621 622 623 624 625 626
    )
  end

  def get_follow_requests(%User{} = user) do
    q = get_follow_requests_query(user)
    reqs = Repo.all(q)

    users =
kaniini's avatar
kaniini committed
627 628 629
      Enum.map(reqs, fn req -> req.actor end)
      |> Enum.uniq()
      |> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end)
630
      |> Enum.filter(fn u -> !is_nil(u) end)
631
      |> Enum.filter(fn u -> !following?(u, user) end)
632 633 634 635

    {:ok, users}
  end

636
  def increase_note_count(%User{} = user) do
lain's avatar
lain committed
637
    info_cng = User.Info.add_to_note_count(user.info, 1)
lain's avatar
lain committed
638 639 640 641

    cng =
      change(user)
      |> put_embed(:info, info_cng)
642

lain's avatar
lain committed
643
    update_and_set_cache(cng)
644 645
  end

646
  def decrease_note_count(%User{} = user) do
lain's avatar
lain committed
647
    info_cng = User.Info.add_to_note_count(user.info, -1)
lain's avatar
lain committed
648 649 650 651

    cng =
      change(user)
      |> put_embed(:info, info_cng)
652

lain's avatar
lain committed
653
    update_and_set_cache(cng)
654 655
  end

656
  def update_note_count(%User{} = user) do
lain's avatar
lain committed
657 658 659 660 661 662
    note_count_query =
      from(
        a in Object,
        where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
        select: count(a.id)
      )
663 664 665

    note_count = Repo.one(note_count_query)

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

lain's avatar
lain committed
668 669 670
    cng =
      change(user)
      |> put_embed(:info, info_cng)
671

lain's avatar
lain committed
672
    update_and_set_cache(cng)
673 674 675
  end

  def update_follower_count(%User{} = user) do
lain's avatar
lain committed
676 677 678 679 680 681 682
    follower_count_query =
      from(
        u in User,
        where: ^user.follower_address in u.following,
        where: u.id != ^user.id,
        select: count(u.id)
      )
683 684 685

    follower_count = Repo.one(follower_count_query)

lain's avatar
lain committed
686 687 688
    info_cng =
      user.info
      |> User.Info.set_follower_count(follower_count)
689

lain's avatar
lain committed
690 691 692
    cng =
      change(user)
      |> put_embed(:info, info_cng)
693

lain's avatar
lain committed
694
    update_and_set_cache(cng)
695
  end
696

697
  def get_users_from_set_query(ap_ids, false) do
698 699
    from(
      u in User,
700
      where: u.ap_id in ^ap_ids
701 702 703
    )
  end

704 705
  def get_users_from_set_query(ap_ids, true) do
    query = get_users_from_set_query(ap_ids, false)
706 707 708

    from(
      u in query,
709 710 711 712
      where: u.local == true
    )
  end

713 714 715 716 717
  def get_users_from_set(ap_ids, local_only \\ true) do
    get_users_from_set_query(ap_ids, local_only)
    |> Repo.all()
  end

718
  def get_recipients_from_activity(%Activity{recipients: to}) do
lain's avatar
lain committed
719 720 721 722 723 724
    query =
      from(
        u in User,
        where: u.ap_id in ^to,
        or_where: fragment("? && ?", u.following, ^to)
      )
725

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

728 729 730
    Repo.all(query)
  end

731
  def search(query, resolve \\ false, for_user \\ nil) do
732
    # Strip the beginning @ off if there is a query
733 734
    query = String.trim_leading(query, "@")

735
    if resolve, do: get_or_fetch(query)
lain's avatar
lain committed
736

737
    fts_results = do_search(fts_search_subquery(query), for_user)
738

lain's avatar
lain committed
739 740 741 742 743
    {:ok, trigram_results} =
      Repo.transaction(fn ->
        Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
        do_search(trigram_search_subquery(query), for_user)
      end)
lain's avatar
lain committed
744

745 746
    Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
  end
lain's avatar
lain committed
747

748
  defp do_search(subquery, for_user, options \\ []) do
kaniini's avatar
kaniini committed
749 750
    q =
      from(
751
        s in subquery(subquery),
752
        order_by: [desc: s.search_rank],
753
        limit: ^(options[:limit] || 20)
kaniini's avatar
kaniini committed
754
      )
lain's avatar
lain committed
755

756 757 758 759 760
    results =
      q
      |> Repo.all()
      |> Enum.filter(&(&1.search_rank > 0))

761 762
    boost_search_results(results, for_user)
  end
763

764 765 766 767 768 769 770 771
  defp fts_search_subquery(query) do
    processed_query =
      query
      |> String.replace(~r/\W+/, " ")
      |> String.trim()
      |> String.split()
      |> Enum.map(&(&1 <> ":*"))
      |> Enum.join(" | ")
772

773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790
    from(
      u in User,
      select_merge: %{
        search_rank:
          fragment(
            """
            ts_rank_cd(
              setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
              setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
              to_tsquery('simple', ?),
              32
            )
            """,
            u.nickname,
            u.name,
            ^processed_query
          )
      },
lain's avatar
lain committed
791 792 793 794 795 796 797 798 799 800
      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
        )
801 802
    )
  end
803

804 805 806 807 808 809
  defp trigram_search_subquery(query) do
    from(
      u in User,
      select_merge: %{
        search_rank:
          fragment(
810
            "similarity(?, trim(? || ' ' || coalesce(?, '')))",
811 812 813 814 815
            ^query,
            u.nickname,
            u.name
          )
      },
lain's avatar
lain committed
816
      where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^query)
817 818 819 820 821 822 823 824
    )
  end

  defp boost_search_results(results, nil), do: results

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

826 827 828 829 830 831 832 833 834 835 836 837 838
    Enum.map(
      results,
      fn u ->
        search_rank_coef =
          cond do
            u.id in friends_ids ->
              1.2

            u.id in followers_ids ->
              1.1

            true ->
              1
839 840
          end

841 842 843 844
        Map.put(u, :search_rank, u.search_rank * search_rank_coef)
      end
    )
    |> Enum.sort_by(&(-&1.search_rank))
lain's avatar
lain committed
845
  end
lain's avatar
lain committed
846

847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863
  def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
    Enum.map(
      blocked_identifiers,
      fn blocked_identifier ->
        with %User{} = blocked <- get_or_fetch(blocked_identifier),
             {: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

864 865 866 867 868 869 870 871 872 873 874 875 876 877
  def block(blocker, %User{ap_id: ap_id} = blocked) do
    # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
    blocker =
      if following?(blocker, blocked) do
        {:ok, blocker, _} = unfollow(blocker, blocked)
        blocker
      else
        blocker
      end

    if following?(blocked, blocker) do
      unfollow(blocked, blocker)
    end

lain's avatar
lain committed
878 879 880
    info_cng =
      blocker.info
      |> User.Info.add_to_block(ap_id)
lain's avatar
lain committed
881

lain's avatar
lain committed
882 883 884 885 886
    cng =
      change(blocker)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
lain's avatar
lain committed
887 888
  end

889 890 891 892 893
  # helper to handle the block given only an actor's AP id
  def block(blocker, %{ap_id: ap_id}) do
    block(blocker, User.get_by_ap_id(ap_id))
  end

lain's avatar
lain committed
894 895 896 897
  def unblock(blocker, %{ap_id: ap_id}) do
    info_cng =
      blocker.info
      |> User.Info.remove_from_block(ap_id)
lain's avatar
lain committed
898

lain's avatar
lain committed
899 900 901 902 903
    cng =
      change(blocker)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
lain's avatar
lain committed
904 905 906
  end

  def blocks?(user, %{ap_id: ap_id}) do
lain's avatar
lain committed
907 908
    blocks = user.info.blocks
    domain_blocks = user.info.domain_blocks
eal's avatar
eal committed
909
    %{host: host} = URI.parse(ap_id)
eal's avatar
eal committed
910 911 912 913 914

    Enum.member?(blocks, ap_id) ||
      Enum.any?(domain_blocks, fn domain ->
        host == domain
      end)
eal's avatar
eal committed
915 916
  end

917 918 919
  def blocked_users(user),
    do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))

eal's avatar
eal committed
920
  def block_domain(user, domain) do
lain's avatar
lain committed
921 922 923
    info_cng =
      user.info
      |> User.Info.add_to_domain_block(domain)
eal's avatar
eal committed
924

lain's avatar
lain committed
925 926 927
    cng =
      change(user)
      |> put_embed(:info, info_cng)
lain's avatar
lain committed
928 929

    update_and_set_cache(cng)
eal's avatar
eal committed
930 931 932
  end

  def unblock_domain(user, domain) do
lain's avatar
lain committed
933 934 935
    info_cng =
      user.info
      |> User.Info.remove_from_domain_block(domain)
eal's avatar
eal committed
936