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

28 29
  require Logger

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

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

34
  # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
href's avatar
href committed
35 36 37
  @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/

  @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
href's avatar
href committed
38
  @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
href's avatar
href committed
39

lain's avatar
lain committed
40
  schema "users" do
lain's avatar
lain committed
41 42 43 44 45 46 47 48 49 50 51 52
    field(:bio, :string)
    field(:email, :string)
    field(:name, :string)
    field(:nickname, :string)
    field(:password_hash, :string)
    field(:password, :string, virtual: true)
    field(:password_confirmation, :string, virtual: true)
    field(:following, {:array, :string}, default: [])
    field(:ap_id, :string)
    field(:avatar, :map)
    field(:local, :boolean, default: true)
    field(:follower_address, :string)
53
    field(:search_rank, :float, virtual: true)
54
    field(:search_type, :integer, virtual: true)
55
    field(:tags, {:array, :string}, default: [])
rinpatch's avatar
rinpatch committed
56
    field(:last_refreshed_at, :naive_datetime_usec)
lain's avatar
lain committed
57
    has_many(:notifications, Notification)
58
    has_many(:registrations, Registration)
59
    embeds_one(:info, User.Info)
lain's avatar
lain committed
60 61 62

    timestamps()
  end
lain's avatar
lain committed
63

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

67
  def auth_active?(%User{}), do: true
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

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

90
  def banner_url(user, options \\ []) do
lain's avatar
lain committed
91
    case user.info.banner do
lain's avatar
lain committed
92
      %{"url" => [%{"href" => href} | _]} -> href
93
      _ -> !options[:no_default] && "#{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
  end

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

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

119
  def restrict_deactivated(query) do
120
    from(u in query,
121
      where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
122 123 124 125 126
    )
  end

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

127 128 129
  def following_count(%User{} = user) do
    user
    |> get_friends_query()
130 131 132
    |> Repo.aggregate(:count, :id)
  end

lain's avatar
lain committed
133
  def remote_user_creation(params) do
lain's avatar
lain committed
134 135 136
    params =
      params
      |> Map.put(:info, params[:info] || %{})
lain's avatar
lain committed
137 138 139

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

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

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

lain's avatar
lain committed
157 158
        _ ->
          followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
lain's avatar
lain committed
159

lain's avatar
lain committed
160 161 162
          changes
          |> put_change(:follower_address, followers)
      end
163 164 165
    else
      changes
    end
lain's avatar
lain committed
166 167
  end

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

lain's avatar
lain committed
177
  def upgrade_changeset(struct, params \\ %{}) do
178 179 180 181
    params =
      params
      |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())

lain's avatar
lain committed
182 183 184 185
    info_cng =
      struct.info
      |> User.Info.user_upgrade(params[:info])

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

Roger Braun's avatar
Roger Braun committed
195
  def password_update_changeset(struct, params) do
lain's avatar
lain committed
196 197 198 199 200
    changeset =
      struct
      |> cast(params, [:password, :password_confirmation])
      |> validate_required([:password, :password_confirmation])
      |> validate_confirmation(:password)
Roger Braun's avatar
Roger Braun committed
201

202 203 204
    OAuth.Token.delete_user_tokens(struct)
    OAuth.Authorization.delete_user_authorizations(struct)

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

Roger Braun's avatar
Roger Braun committed
208 209 210 211 212 213 214 215
      changeset
      |> put_change(:password_hash, hashed)
    else
      changeset
    end
  end

  def reset_password(user, data) do
lain's avatar
lain committed
216
    update_and_set_cache(password_update_changeset(user, data))
Roger Braun's avatar
Roger Braun committed
217 218
  end

219
  def register_changeset(struct, params \\ %{}, opts \\ []) do
220 221 222
    need_confirmation? =
      if is_nil(opts[:need_confirmation]) do
        Pleroma.Config.get([:instance, :account_activation_required])
223
      else
224
        opts[:need_confirmation]
225 226
      end

227 228
    info_change =
      User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
Ivan Tashkinov's avatar
Ivan Tashkinov committed
229

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

244 245 246 247 248 249 250
    changeset =
      if opts[:external] do
        changeset
      else
        validate_required(changeset, [:email])
      end

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

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

267 268 269 270
  defp autofollow_users(user) do
    candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])

    autofollowed_users =
271
      User.Query.build(%{nickname: candidates, local: true, deactivated: false})
272 273
      |> Repo.all()

lain's avatar
lain committed
274
    follow_all(user, autofollowed_users)
275 276
  end

277 278
  @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
279
    with {:ok, user} <- Repo.insert(changeset),
lain's avatar
lain committed
280
         {:ok, user} <- autofollow_users(user),
minibikini's avatar
minibikini committed
281
         {:ok, user} <- set_cache(user),
282
         {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
lain's avatar
lain committed
283
         {:ok, _} <- try_send_confirmation_email(user) do
284 285 286 287
      {:ok, user}
    end
  end

288
  def try_send_confirmation_email(%User{} = user) do
289 290
    if user.info.confirmation_pending &&
         Pleroma.Config.get([:instance, :account_activation_required]) do
291
      user
292 293
      |> Pleroma.Emails.UserEmail.account_confirmation_email()
      |> Pleroma.Emails.Mailer.deliver_async()
294 295

      {:ok, :enqueued}
296 297 298 299 300
    else
      {:ok, :noop}
    end
  end

301 302 303 304 305
  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
306
    NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
307 308 309 310
  end

  def needs_update?(_), do: true

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

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

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

rinpatch's avatar
rinpatch committed
351
    {1, [follower]} = Repo.update_all(q, [])
lain's avatar
lain committed
352 353 354 355 356

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

lain's avatar
lain committed
357
    set_cache(follower)
lain's avatar
lain committed
358 359
  end

lain's avatar
lain committed
360
  def follow(%User{} = follower, %User{info: info} = followed) do
minibikini's avatar
minibikini committed
361
    deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
362
    ap_followers = followed.follower_address
363

364
    cond do
365
      info.deactivated ->
lain's avatar
lain committed
366
        {:error, "Could not follow user: You are deactivated."}
lain's avatar
lain committed
367

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

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

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

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

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

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

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

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

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

      {:ok, followed} = update_follower_count(followed)

406 407
      set_cache(follower)

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

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

419
  def locked?(%User{} = user) do
420
    user.info.locked || false
421 422
  end

423 424 425 426
  def get_by_id(id) do
    Repo.get_by(User, id: id)
  end

lain's avatar
lain committed
427 428 429 430
  def get_by_ap_id(ap_id) do
    Repo.get_by(User, ap_id: ap_id)
  end

431 432
  # 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
433 434 435 436 437
  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
438
    get_cached_by_nickname(nickname)
439 440
  end

minibikini's avatar
minibikini committed
441 442 443 444
  def set_cache({:ok, user}), do: set_cache(user)
  def set_cache({:error, err}), do: {:error, err}

  def set_cache(%User{} = user) do
445 446 447 448 449 450
    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
451 452
  def update_and_set_cache(changeset) do
    with {:ok, user} <- Repo.update(changeset) do
453
      set_cache(user)
lain's avatar
lain committed
454 455 456 457 458
    else
      e -> e
    end
  end

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

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

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

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

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

    get_cached_by_ap_id(ap_id)
486 487
  end

lain's avatar
lain committed
488
  def get_cached_by_nickname(nickname) do
489
    key = "nickname:#{nickname}"
0x1C3B00DA's avatar
Run  
0x1C3B00DA committed
490

491 492 493 494 495
    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
496
        {:error, _error} -> {:ignore, nil}
497 498
      end
    end)
lain's avatar
lain committed
499
  end
lain's avatar
lain committed
500

501
  def get_cached_by_nickname_or_id(nickname_or_id) do
502
    get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
503 504
  end

lain's avatar
lain committed
505
  def get_by_nickname(nickname) do
506
    Repo.get_by(User, nickname: nickname) ||
507
      if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
508
        Repo.get_by(User, nickname: local_nickname(nickname))
509
      end
510 511
  end

512 513
  def get_by_email(email), do: Repo.get_by(User, email: email)

514
  def get_by_nickname_or_email(nickname_or_email) do
515
    get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
516 517
  end

lain's avatar
lain committed
518 519
  def get_cached_user_info(user) do
    key = "user_info:#{user.id}"
Thog's avatar
Thog committed
520
    Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
lain's avatar
lain committed
521
  end
lain's avatar
lain committed
522

lain's avatar
lain committed
523 524 525 526 527 528 529 530 531
  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
532
  def get_or_fetch_by_nickname(nickname) do
lain's avatar
lain committed
533
    with %User{} = user <- get_by_nickname(nickname) do
534
      {:ok, user}
lain's avatar
lain committed
535 536 537 538
    else
      _e ->
        with [_nick, _domain] <- String.split(nickname, "@"),
             {:ok, user} <- fetch_by_nickname(nickname) do
539
          if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
540
            fetch_initial_posts(user)
541 542
          end

543
          {:ok, user}
lain's avatar
lain committed
544
        else
Alexander Strizhakov's avatar
Alexander Strizhakov committed
545
          _e -> {:error, "not found " <> nickname}
lain's avatar
lain committed
546
        end
lain's avatar
lain committed
547
    end
lain's avatar
lain committed
548
  end
lain's avatar
lain committed
549

550
  @doc "Fetch some posts when the user has just been federated with"
551 552
  def fetch_initial_posts(user),
    do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
553

Alexander Strizhakov's avatar
Alexander Strizhakov committed
554 555
  @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
  def get_followers_query(%User{} = user, nil) do
556
    User.Query.build(%{followers: user, deactivated: false})
557 558
  end

559
  def get_followers_query(user, page) do
Maxim Filippov's avatar
Maxim Filippov committed
560
    from(u in get_followers_query(user, nil))
Alexander Strizhakov's avatar
Alexander Strizhakov committed
561
    |> User.Query.paginate(page, 20)
562 563
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
564
  @spec get_followers_query(User.t()) :: Ecto.Query.t()
565 566 567 568
  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
569 570 571 572

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

573 574 575 576 577 578
  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
579 580
  @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
  def get_friends_query(%User{} = user, nil) do
581
    User.Query.build(%{friends: user, deactivated: false})
582 583
  end

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

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

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

598 599 600 601 602 603
  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
604
  @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
605 606
  def get_follow_requests(%User{} = user) do
    users =
Alexander Strizhakov's avatar
Alexander Strizhakov committed
607
      Activity.follow_requests_for_actor(user)
rinpatch's avatar
rinpatch committed
608
      |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
609 610 611 612
      |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
      |> group_by([a, u], u.id)
      |> select([a, u], u)
      |> Repo.all()
613 614 615 616

    {:ok, users}
  end

617
  def increase_note_count(%User{} = user) do
618 619 620 621 622 623 624 625 626 627 628 629
    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
630 631
    |> select([u], u)
    |> Repo.update_all([])
632 633 634 635
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
636 637
  end

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

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

    note_count = Repo.one(note_count_query)

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

671 672 673 674
    user
    |> change()
    |> put_embed(:info, info_cng)
    |> update_and_set_cache()
675 676 677
  end

  def update_follower_count(%User{} = user) do
lain's avatar
lain committed
678
    follower_count_query =
679
      User.Query.build(%{followers: user, deactivated: false})
680
      |> select([u], %{count: count(u.id)})
681

682 683 684 685 686 687 688 689 690 691 692 693 694
    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
695 696
    |> select([u], u)
    |> Repo.update_all([])
697 698 699 700
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
701
  end
702

703 704 705 706 707 708 709 710 711 712 713 714
  def remove_duplicated_following(%User{following: following} = user) do
    uniq_following = Enum.uniq(following)

    if length(following) == length(uniq_following) do
      {:ok, user}
    else
      user
      |> update_changeset(%{following: uniq_following})
      |> update_and_set_cache()
    end
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
715
  @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
716
  def get_users_from_set(ap_ids, local_only \\ true) do
717
    criteria = %{ap_id: ap_ids, deactivated: false}
Alexander Strizhakov's avatar
Alexander Strizhakov committed
718 719 720
    criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria

    User.Query.build(criteria)
721 722 723
    |> Repo.all()
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
724
  @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
725
  def get_recipients_from_activity(%Activity{recipients: to}) do
726
    User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
Alexander Strizhakov's avatar
Alexander Strizhakov committed
727
    |> Repo.all()
728 729
  end

730
  def mute(muter, %User{ap_id: ap_id}) do
731 732 733 734 735 736 737
    info_cng =
      muter.info
      |> User.Info.add_to_mutes(ap_id)

    cng =
      change(muter)
      |> put_embed(:info, info_cng)
738

739
    update_and_set_cache(cng)
740 741
  end

742 743 744 745
  def unmute(muter, %{ap_id: ap_id}) do
    info_cng =
      muter.info
      |> User.Info.remove_from_mutes(ap_id)
746

747 748 749 750 751
    cng =
      change(muter)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
752 753
  end

754
  def subscribe(subscriber, %{ap_id: ap_id}) do
755
    deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
756

757
    with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
758 759 760 761 762 763 764 765 766 767 768 769 770
      blocked = blocks?(subscribed, subscriber) and deny_follow_blocked

      if blocked do
        {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
      else
        info_cng =
          subscribed.info
          |> User.Info.add_to_subscribers(subscriber.ap_id)

        change(subscribed)
        |> put_embed(:info, info_cng)
        |> update_and_set_cache()
      end
771
    end
772 773 774
  end

  def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
775
    with %User{} = user <- get_cached_by_ap_id(ap_id) do
776 777 778
      info_cng =
        user.info
        |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
779

780 781 782 783
      change(user)
      |> put_embed(:info, info_cng)
      |> update_and_set_cache()
    end
784 785
  end

786 787 788 789 790 791 792 793 794 795
  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

796 797 798 799 800 801 802 803
    blocker =
      if subscribed_to?(blocked, blocker) do
        {:ok, blocker} = unsubscribe(blocked, blocker)
        blocker
      else
        blocker
      end

804 805 806 807
    if following?(blocked, blocker) do
      unfollow(blocked, blocker)
    end

808 809
    {:ok, blocker} = update_follower_count(blocker)

lain's avatar
lain committed
810 811 812
    info_cng =
      blocker.info
      |> User.Info.add_to_block(ap_id)
lain's avatar
lain committed
813

lain's avatar
lain committed
814 815 816 817 818
    cng =
      change(blocker)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
lain's avatar
lain committed
819 820
  end

821 822
  # helper to handle the block given only an actor's AP id
  def block(blocker, %{ap_id: ap_id}) do
minibikini's avatar
minibikini committed
823
    block(blocker, get_cached_by_ap_id(ap_id))
824 825
  end

lain's avatar
lain committed
826 827 828 829
  def unblock(blocker, %{ap_id: ap_id}) do
    info_cng =
      blocker.info
      |> User.Info.remove_from_block(ap_id)
lain's avatar
lain committed
830

lain's avatar
lain committed
831 832 833 834 835
    cng =
      change(blocker)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
lain's avatar
lain committed
836 837
  end

838
  def mutes?(nil, _), do: false
839
  def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
840

lain's avatar
lain committed
841
  def blocks?(user, %{ap_id: ap_id}) do
lain's avatar
lain committed
842 843
    blocks = user.info.blocks
    domain_blocks = user.info.domain_blocks
eal's avatar
eal committed
844
    %{host: host} = URI.parse(ap_id)
eal's avatar
eal committed
845 846 847 848 849

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

Sadposter's avatar
Sadposter committed
852
  def subscribed_to?(user, %{ap_id: ap_id}) do
minibikini's avatar
minibikini committed
853
    with %User{} = target <- get_cached_by_ap_id(ap_id) do
854 855
      Enum.member?(target.info.subscribers, user.ap_id)
    end
Sadposter's avatar
Sadposter committed
856 857
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
858 859
  @spec muted_users(User.t()) :: [User.t()]
  def muted_users(user) do
860
    User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
Alexander Strizhakov's avatar
Alexander Strizhakov committed
861 862
    |> Repo.all()
  end