user.ex 36.4 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
15
  alias Comeonin.Pbkdf2
  alias Pleroma.Activity
  alias Pleroma.Formatter
  alias Pleroma.Notification
  alias Pleroma.Object
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: [])
Haelwenn's avatar
Haelwenn committed
55
    field(:bookmarks, {: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)
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
  def auth_active?(%User{local: false}), do: true
64

65
66
67
68
  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])
69

70
  def auth_active?(_), do: false
71

72
73
74
75
76
  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
77
    auth_active?(user) || superuser?(for_user)
78
79
  end

80
81
  def visible_for?(_, _), do: false

82
83
  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
84
  def superuser?(_), do: false
85

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

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

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

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

108
109
  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
110

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

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

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

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

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

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

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

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

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

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

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

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

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

193
194
195
    OAuth.Token.delete_user_tokens(struct)
    OAuth.Authorization.delete_user_authorizations(struct)

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

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

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

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

lain's avatar
lain committed
220
221
222
223
224
225
226
    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
227
      |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
href's avatar
href committed
228
      |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
229
230
231
      |> validate_format(:email, @email_regex)
      |> validate_length(:bio, max: 1000)
      |> validate_length(:name, min: 1, max: 100)
Ivan Tashkinov's avatar
Ivan Tashkinov committed
232
      |> put_change(:info, info_change)
lain's avatar
lain committed
233
234

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

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

250
251
252
253
254
255
256
257
258
259
  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
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),
lain's avatar
lain committed
267
         {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
lain's avatar
lain committed
268
         {:ok, _} <- try_send_confirmation_email(user) do
269
270
271
272
      {:ok, user}
    end
  end

273
  def try_send_confirmation_email(%User{} = user) do
274
275
    if user.info.confirmation_pending &&
         Pleroma.Config.get([:instance, :account_activation_required]) do
276
277
      user
      |> Pleroma.UserEmail.account_confirmation_email()
minibikini's avatar
Reports    
minibikini committed
278
      |> Pleroma.Mailer.deliver_async()
279
280
281
282
283
    else
      {:ok, :noop}
    end
  end

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

  def needs_update?(_), do: true

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

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

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

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

rinpatch's avatar
rinpatch committed
342
    {1, [follower]} = Repo.update_all(q, [])
lain's avatar
lain committed
343
344
345
346
347

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

lain's avatar
lain committed
348
    set_cache(follower)
lain's avatar
lain committed
349
350
  end

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

355
    ap_followers = followed.follower_address
356

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

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

364
365
366
367
368
      true ->
        if !followed.local && follower.local && !ap_enabled?(followed) do
          Websub.subscribe(follower, followed)
        end

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

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

378
379
        {:ok, _} = update_follower_count(followed)

380
        set_cache(follower)
381
    end
lain's avatar
lain committed
382
  end
lain's avatar
lain committed
383
384

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

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

rinpatch's avatar
rinpatch committed
395
      {1, [follower]} = Repo.update_all(q, [])
396
397
398

      {:ok, followed} = update_follower_count(followed)

399
400
      set_cache(follower)

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

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

412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
  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

430
  def locked?(%User{} = user) do
431
    user.info.locked || false
432
433
  end

434
435
436
437
  def get_by_id(id) do
    Repo.get_by(User, id: id)
  end

lain's avatar
lain committed
438
439
440
441
  def get_by_ap_id(ap_id) do
    Repo.get_by(User, ap_id: ap_id)
  end

442
443
  # 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
444
445
446
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}"

    get_by_nickname(nickname)
  end

452
453
454
455
456
457
458
  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
459
460
  def update_and_set_cache(changeset) do
    with {:ok, user} <- Repo.update(changeset) do
461
      set_cache(user)
lain's avatar
lain committed
462
463
464
465
466
    else
      e -> e
    end
  end

lain's avatar
lain committed
467
468
469
  def invalidate_cache(user) do
    Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
    Cachex.del(:user_cache, "nickname:#{user.nickname}")
470
    Cachex.del(:user_cache, "user_info:#{user.id}")
lain's avatar
lain committed
471
472
  end

lain's avatar
lain committed
473
  def get_cached_by_ap_id(ap_id) do
474
    key = "ap_id:#{ap_id}"
Thog's avatar
Thog committed
475
    Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
lain's avatar
lain committed
476
477
  end

478
479
  def get_cached_by_id(id) do
    key = "id:#{id}"
480
481
482
483

    ap_id =
      Cachex.fetch!(:user_cache, key, fn _ ->
        user = get_by_id(id)
484
485
486
487
488
489
490

        if user do
          Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
          {:commit, user.ap_id}
        else
          {:ignore, ""}
        end
491
492
493
      end)

    get_cached_by_ap_id(ap_id)
494
495
  end

lain's avatar
lain committed
496
  def get_cached_by_nickname(nickname) do
497
    key = "nickname:#{nickname}"
Thog's avatar
Thog committed
498
    Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) 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
514
515
516
517
518
  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
519
520
  def get_cached_user_info(user) do
    key = "user_info:#{user.id}"
Thog's avatar
Thog committed
521
    Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
lain's avatar
lain committed
522
  end
lain's avatar
lain committed
523

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

lain's avatar
lain committed
544
545
546
547
          user
        else
          _e -> nil
        end
lain's avatar
lain committed
548
    end
lain's avatar
lain committed
549
  end
lain's avatar
lain committed
550

551
552
553
554
555
556
557
558
559
560
561
  @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

562
  def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
563
564
565
566
567
568
569
    from(
      u in User,
      where: fragment("? <@ ?", ^[follower_address], u.following),
      where: u.id != ^id
    )
  end

570
  def get_followers_query(user, page) do
Maxim Filippov's avatar
Maxim Filippov committed
571
572
    from(u in get_followers_query(user, nil))
    |> paginate(page, 20)
573
574
575
576
577
578
  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
579
580
581
582

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

583
584
585
586
587
588
  def get_followers_ids(user, page \\ nil) do
    q = get_followers_query(user, page)

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

589
  def get_friends_query(%User{id: id, following: following}, nil) do
590
591
592
593
594
595
596
    from(
      u in User,
      where: u.follower_address in ^following,
      where: u.id != ^id
    )
  end

597
  def get_friends_query(user, page) do
Maxim Filippov's avatar
Maxim Filippov committed
598
599
    from(u in get_friends_query(user, nil))
    |> paginate(page, 20)
600
601
602
603
604
605
  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
606
607
608

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

610
611
612
613
614
615
  def get_friends_ids(user, page \\ nil) do
    q = get_friends_query(user, page)

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

616
617
618
  def get_follow_requests_query(%User{} = user) do
    from(
      a in Activity,
kaniini's avatar
kaniini committed
619
620
621
622
623
624
625
626
627
628
629
630
      where:
        fragment(
          "? ->> 'type' = 'Follow'",
          a.data
        ),
      where:
        fragment(
          "? ->> 'state' = 'pending'",
          a.data
        ),
      where:
        fragment(
631
          "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
kaniini's avatar
kaniini committed
632
          a.data,
633
634
          a.data,
          ^user.ap_id
kaniini's avatar
kaniini committed
635
        )
636
637
638
639
640
    )
  end

  def get_follow_requests(%User{} = user) do
    users =
641
642
      user
      |> User.get_follow_requests_query()
rinpatch's avatar
rinpatch committed
643
      |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
644
645
646
647
      |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
      |> group_by([a, u], u.id)
      |> select([a, u], u)
      |> Repo.all()
648
649
650
651

    {:ok, users}
  end

652
  def increase_note_count(%User{} = user) do
653
654
655
656
657
658
659
660
661
662
663
664
    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
665
666
    |> select([u], u)
    |> Repo.update_all([])
667
668
669
670
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
671
672
  end

673
  def decrease_note_count(%User{} = user) do
674
675
676
677
678
679
680
681
682
683
684
685
    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
686
687
    |> select([u], u)
    |> Repo.update_all([])
688
689
690
691
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
692
693
  end

694
  def update_note_count(%User{} = user) do
lain's avatar
lain committed
695
696
697
698
699
700
    note_count_query =
      from(
        a in Object,
        where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
        select: count(a.id)
      )
701
702
703

    note_count = Repo.one(note_count_query)

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

lain's avatar
lain committed
706
707
708
    cng =
      change(user)
      |> put_embed(:info, info_cng)
709

lain's avatar
lain committed
710
    update_and_set_cache(cng)
711
712
713
  end

  def update_follower_count(%User{} = user) do
lain's avatar
lain committed
714
    follower_count_query =
715
716
717
718
      User
      |> where([u], ^user.follower_address in u.following)
      |> where([u], u.id != ^user.id)
      |> select([u], %{count: count(u.id)})
719

720
721
722
723
724
725
726
727
728
729
730
731
732
    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
733
734
    |> select([u], u)
    |> Repo.update_all([])
735
736
737
738
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
739
  end
740

741
  def get_users_from_set_query(ap_ids, false) do
742
743
    from(
      u in User,
744
      where: u.ap_id in ^ap_ids
745
746
747
    )
  end

748
749
  def get_users_from_set_query(ap_ids, true) do
    query = get_users_from_set_query(ap_ids, false)
750
751
752

    from(
      u in query,
753
754
755
756
      where: u.local == true
    )
  end

757
758
759
760
761
  def get_users_from_set(ap_ids, local_only \\ true) do
    get_users_from_set_query(ap_ids, local_only)
    |> Repo.all()
  end

762
  def get_recipients_from_activity(%Activity{recipients: to}) do
lain's avatar
lain committed
763
764
765
766
767
768
    query =
      from(
        u in User,
        where: u.ap_id in ^to,
        or_where: fragment("? && ?", u.following, ^to)
      )
769

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

772
773
774
    Repo.all(query)
  end

775
776
777
778
779
780
781
  @spec search_for_admin(%{
          local: boolean(),
          page: number(),
          page_size: number()
        }) :: {:ok, [Pleroma.User.t()], number()}
  def search_for_admin(%{query: nil, local: local, page: page, page_size: page_size}) do
    query =
Maxim Filippov's avatar
Maxim Filippov committed
782
      from(u in User, order_by: u.nickname)
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
      |> maybe_local_user_query(local)

    paginated_query =
      query
      |> paginate(page, page_size)

    count =
      query
      |> Repo.aggregate(:count, :id)

    {:ok, Repo.all(paginated_query), count}
  end

  @spec search_for_admin(%{
          query: binary(),
Maxim Filippov's avatar
Maxim Filippov committed
798
799
800
801
          local: boolean(),
          page: number(),
          page_size: number()
        }) :: {:ok, [Pleroma.User.t()], number()}
802
803
804
805
806
807
  def search_for_admin(%{
        query: term,
        local: local,
        page: page,
        page_size: page_size
      }) do
Maxim Filippov's avatar
Maxim Filippov committed
808
    maybe_local_query = User |> maybe_local_user_query(local)
809

Maxim Filippov's avatar
Maxim Filippov committed
810
811
    search_query = from(u in maybe_local_query, where: ilike(u.nickname, ^"%#{term}%"))
    count = search_query |> Repo.aggregate(:count, :id)
Maxim Filippov's avatar
Format    
Maxim Filippov committed
812

Maxim Filippov's avatar
Maxim Filippov committed
813
814
    results =
      search_query
Maxim Filippov's avatar
Maxim Filippov committed
815
      |> paginate(page, page_size)
Maxim Filippov's avatar
Maxim Filippov committed
816
      |> Repo.all()
lain's avatar
lain committed
817

Maxim Filippov's avatar
Maxim Filippov committed
818
    {:ok, results, count}
819
  end
lain's avatar
lain committed
820

821
  def search(query, resolve \\ false, for_user \\ nil) do
822
    # Strip the beginning @ off if there is a query
823
824
    query = String.trim_leading(query, "@")

825
    if resolve, do: get_or_fetch(query)
lain's avatar
lain committed
826

827
    {:ok, results} =
lain's avatar
lain committed
828
829
      Repo.transaction(fn ->
        Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
830
        Repo.all(search_query(query, for_user))
lain's avatar
lain committed
831
      end)
lain's avatar
lain committed
832

833
    results
834
  end
lain's avatar
lain committed
835

836
837
838
  def search_query(query, for_user) do
    fts_subquery = fts_search_subquery(query)
    trigram_subquery = trigram_search_subquery(query)
839
840
    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
841

842
843
844
845
846
    from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
      order_by: [desc: s.search_rank],
      limit: 20
    )
  end
847

848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
  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)

    from(u in subquery(query),
      select_merge: %{
        search_rank:
          fragment(
            """
             CASE WHEN (?) THEN (?) * 1.3 
             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
          )
      }
    )
874
  end
875

Maxim Filippov's avatar
Maxim Filippov committed
876
  defp fts_search_subquery(term, query \\ User) do
877
    processed_query =
878
      term
879
880
881
882
883
      |> String.replace(~r/\W+/, " ")
      |> String.trim()
      |> String.split()
      |> Enum.map(&(&1 <> ":*"))
      |> Enum.join(" | ")
884

885
    from(
886
      u in query,
887
      select_merge: %{
888
        search_type: ^0,
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
        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
904
905
906
907
908
909
910
911
912
913
      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
        )
914
915
    )
  end
916

Maxim Filippov's avatar
Maxim Filippov committed
917
  defp trigram_search_subquery(term) do
918
919
920
    from(
      u in User,
      select_merge: %{
921
922
        # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
        search_type: fragment("?", 1),
923
924
        search_rank:
          fragment(
925
            "similarity(?, trim(? || ' ' || coalesce(?, '')))",
926
            ^term,
927
928
929
930
            u.nickname,
            u.name
          )
      },
931
      where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
932
933
934
    )
  end

935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
  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

952
  def mute(muter, %User{ap_id: ap_id}) do
953
954
955
956
957
958
959
    info_cng =
      muter.info
      |> User.Info.add_to_mutes(ap_id)

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

961
    update_and_set_cache(cng)
962
963
  end

964
965
966
967
  def unmute(muter, %{ap_id: ap_id}) do
    info_cng =
      muter.info
      |> User.Info.remove_from_mutes(ap_id)
968

969
970
971
972
973
    cng =
      change(muter)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
974
975
  end

976
977
978
979
980
981
982
983
984
985
986
987
988
989
  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
990
991
992
    info_cng =
      blocker.info
      |> User.Info.add_to_block(ap_id)
lain's avatar
lain committed
993

lain's avatar
lain committed
994
995
996
997
998
    cng =
      change(blocker)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
lain's avatar
lain committed
999
1000
  end