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, info_cng)
879

880
    update_and_set_cache(cng)
881
882
  end

883
884
885
886
  def unmute(muter, %{ap_id: ap_id}) do
    info_cng =
      muter.info
      |> User.Info.remove_from_mutes(ap_id)
887

888
889
890
891
892
    cng =
      change(muter)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
893
894
  end

895
  def subscribe(subscriber, %{ap_id: ap_id}) do
896
    deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
897

898
    with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
899
900
901
902
903
904
905
906
907
908
909
910
911
      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
912
    end
913
914
915
  end

  def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
916
    with %User{} = user <- get_cached_by_ap_id(ap_id) do
917
918
919
      info_cng =
        user.info
        |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
920

921
922
923
924
      change(user)
      |> put_embed(:info, info_cng)
      |> update_and_set_cache()
    end
925
926
  end

927
928
929
930
931
932
933
934
935
936
  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

937
938
939
940
941
942
943
944
    blocker =
      if subscribed_to?(blocked, blocker) do
        {:ok, blocker} = unsubscribe(blocked, blocker)
        blocker
      else
        blocker
      end

945
946
947
948
    if following?(blocked, blocker) do
      unfollow(blocked, blocker)
    end

949
950
    {:ok, blocker} = update_follower_count(blocker)

lain's avatar
lain committed
951
952
953
    info_cng =
      blocker.info
      |> User.Info.add_to_block(ap_id)
lain's avatar
lain committed
954

lain's avatar
lain committed
955
956
957
958
959
    cng =
      change(blocker)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
lain's avatar
lain committed
960
961
  end

962
963
  # 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
964
    block(blocker, get_cached_by_ap_id(ap_id))
965
966
  end

lain's avatar
lain committed
967
968