user.ex 37 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
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(:tags, {:array, :string}, default: [])
Haelwenn's avatar
Haelwenn committed
55
    field(:bookmarks, {:array, :string}, default: [])
56
    field(:last_refreshed_at, :naive_datetime)
lain's avatar
lain committed
57
    has_many(:notifications, Notification)
58
    has_many(:registrations, Registration)
lain's avatar
lain committed
59
    embeds_one(:info, Pleroma.User.Info)
lain's avatar
lain committed
60
61
62

    timestamps()
  end
lain's avatar
lain committed
63

64
  def auth_active?(%User{local: false}), do: true
65

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

195
196
197
    OAuth.Token.delete_user_tokens(struct)
    OAuth.Authorization.delete_user_authorizations(struct)

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

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

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

212
  # TODO: FIXME (WIP):
213
  def external_registration_changeset(struct, params \\ %{}) do
214
215
216
217
    info_change = User.Info.confirmation_changeset(%User.Info{}, :confirmed)

    changeset =
      struct
218
      |> cast(params, [:email, :nickname, :name, :bio])
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
      |> unique_constraint(:email)
      |> unique_constraint(:nickname)
      |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
      |> validate_format(:email, @email_regex)
      |> validate_length(:bio, max: 1000)
      |> put_change(:info, info_change)

    if changeset.valid? do
      nickname = changeset.changes[:nickname]
      ap_id = (nickname && User.ap_id(%User{nickname: nickname})) || nil
      followers = User.ap_followers(%User{nickname: ap_id})

      changeset
      |> put_change(:ap_id, ap_id)
      |> unique_constraint(:ap_id)
      |> put_change(:following, [followers])
      |> put_change(:follower_address, followers)
    else
      changeset
    end
  end

241
242
243
244
245
246
247
248
  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
249
250
    info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)

lain's avatar
lain committed
251
252
253
254
255
256
257
    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
258
      |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
href's avatar
href committed
259
      |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
260
261
262
      |> validate_format(:email, @email_regex)
      |> validate_length(:bio, max: 1000)
      |> validate_length(:name, min: 1, max: 100)
Ivan Tashkinov's avatar
Ivan Tashkinov committed
263
      |> put_change(:info, info_change)
lain's avatar
lain committed
264
265

    if changeset.valid? do
266
      hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
lain's avatar
lain committed
267
268
      ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
      followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
lain's avatar
lain committed
269

lain's avatar
lain committed
270
271
272
      changeset
      |> put_change(:password_hash, hashed)
      |> put_change(:ap_id, ap_id)
rinpatch's avatar
rinpatch committed
273
      |> unique_constraint(:ap_id)
lain's avatar
lain committed
274
      |> put_change(:following, [followers])
275
      |> put_change(:follower_address, followers)
lain's avatar
lain committed
276
277
278
279
280
    else
      changeset
    end
  end

281
282
283
284
285
286
287
288
289
290
  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
291
    follow_all(user, autofollowed_users)
292
293
  end

294
295
  @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
296
    with {:ok, user} <- Repo.insert(changeset),
lain's avatar
lain committed
297
         {:ok, user} <- autofollow_users(user),
lain's avatar
lain committed
298
         {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
lain's avatar
lain committed
299
         {:ok, _} <- try_send_confirmation_email(user) do
300
301
302
303
      {:ok, user}
    end
  end

304
  def try_send_confirmation_email(%User{} = user) do
305
306
    if user.info.confirmation_pending &&
         Pleroma.Config.get([:instance, :account_activation_required]) do
307
308
      user
      |> Pleroma.UserEmail.account_confirmation_email()
minibikini's avatar
Reports    
minibikini committed
309
      |> Pleroma.Mailer.deliver_async()
310
311
312
313
314
    else
      {:ok, :noop}
    end
  end

315
316
317
318
319
  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
320
    NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
321
322
323
324
  end

  def needs_update?(_), do: true

lain's avatar
lain committed
325
  def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
326
327
328
329
330
331
332
333
    {: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
334
    if not User.ap_enabled?(followed) do
335
      follow(follower, followed)
336
337
338
339
340
    else
      {:ok, follower}
    end
  end

Maksim's avatar
Maksim committed
341
  def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
342
343
    if not following?(follower, followed) do
      follow(follower, followed)
344
    else
345
      {:ok, follower}
346
347
348
    end
  end

349
  @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
lain's avatar
lain committed
350
351
  @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
  def follow_all(follower, followeds) do
lain's avatar
lain committed
352
353
    followed_addresses =
      followeds
354
      |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
lain's avatar
lain committed
355
      |> Enum.map(fn %{follower_address: fa} -> fa end)
lain's avatar
lain committed
356

lain's avatar
lain committed
357
358
359
    q =
      from(u in User,
        where: u.id == ^follower.id,
360
361
362
363
364
365
366
367
368
369
        update: [
          set: [
            following:
              fragment(
                "array(select distinct unnest (array_cat(?, ?)))",
                u.following,
                ^followed_addresses
              )
          ]
        ]
lain's avatar
lain committed
370
371
372
      )

    {1, [follower]} = Repo.update_all(q, [], returning: true)
lain's avatar
lain committed
373
374
375
376
377

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

lain's avatar
lain committed
378
    set_cache(follower)
lain's avatar
lain committed
379
380
  end

lain's avatar
lain committed
381
  def follow(%User{} = follower, %User{info: info} = followed) do
382
383
    user_config = Application.get_env(:pleroma, :user)
    deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
384

385
    ap_followers = followed.follower_address
386

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

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

394
395
396
397
398
      true ->
        if !followed.local && follower.local && !ap_enabled?(followed) do
          Websub.subscribe(follower, followed)
        end

399
400
401
402
403
        q =
          from(u in User,
            where: u.id == ^follower.id,
            update: [push: [following: ^ap_followers]]
          )
404

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

407
408
        {:ok, _} = update_follower_count(followed)

409
        set_cache(follower)
410
    end
lain's avatar
lain committed
411
  end
lain's avatar
lain committed
412
413

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

416
    if following?(follower, followed) and follower.ap_id != followed.ap_id do
417
418
419
420
421
      q =
        from(u in User,
          where: u.id == ^follower.id,
          update: [pull: [following: ^ap_followers]]
        )
lain's avatar
lain committed
422

423
      {1, [follower]} = Repo.update_all(q, [], returning: true)
424
425
426

      {:ok, followed} = update_follower_count(followed)

427
428
      set_cache(follower)

429
      {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
430
    else
431
      {:error, "Not subscribed!"}
432
    end
lain's avatar
lain committed
433
  end
434

Maksim's avatar
Maksim committed
435
  @spec following?(User.t(), User.t()) :: boolean
436
  def following?(%User{} = follower, %User{} = followed) do
437
    Enum.member?(follower.following, followed.follower_address)
438
  end
lain's avatar
lain committed
439

440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
  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

458
  def locked?(%User{} = user) do
459
    user.info.locked || false
460
461
  end

462
463
464
465
  def get_by_id(id) do
    Repo.get_by(User, id: id)
  end

lain's avatar
lain committed
466
467
468
469
  def get_by_ap_id(ap_id) do
    Repo.get_by(User, ap_id: ap_id)
  end

470
471
  # 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
472
473
474
475
476
477
478
479
  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

480
481
482
483
484
485
486
  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
487
488
  def update_and_set_cache(changeset) do
    with {:ok, user} <- Repo.update(changeset) do
489
      set_cache(user)
lain's avatar
lain committed
490
491
492
493
494
    else
      e -> e
    end
  end

lain's avatar
lain committed
495
496
497
  def invalidate_cache(user) do
    Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
    Cachex.del(:user_cache, "nickname:#{user.nickname}")
498
    Cachex.del(:user_cache, "user_info:#{user.id}")
lain's avatar
lain committed
499
500
  end

lain's avatar
lain committed
501
  def get_cached_by_ap_id(ap_id) do
502
    key = "ap_id:#{ap_id}"
Thog's avatar
Thog committed
503
    Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
lain's avatar
lain committed
504
505
  end

506
507
  def get_cached_by_id(id) do
    key = "id:#{id}"
508
509
510
511

    ap_id =
      Cachex.fetch!(:user_cache, key, fn _ ->
        user = get_by_id(id)
512
513
514
515
516
517
518

        if user do
          Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
          {:commit, user.ap_id}
        else
          {:ignore, ""}
        end
519
520
521
      end)

    get_cached_by_ap_id(ap_id)
522
523
  end

lain's avatar
lain committed
524
  def get_cached_by_nickname(nickname) do
525
    key = "nickname:#{nickname}"
Thog's avatar
Thog committed
526
    Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
lain's avatar
lain committed
527
  end
lain's avatar
lain committed
528

529
  def get_cached_by_nickname_or_id(nickname_or_id) do
530
    get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
531
532
  end

lain's avatar
lain committed
533
  def get_by_nickname(nickname) do
534
    Repo.get_by(User, nickname: nickname) ||
535
      if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
536
        Repo.get_by(User, nickname: local_nickname(nickname))
537
      end
538
539
  end

540
541
  def get_by_email(email), do: Repo.get_by(User, email: email)

542
  def get_by_nickname_or_email(nickname_or_email) do
543
    get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
544
545
  end

lain's avatar
lain committed
546
547
  def get_cached_user_info(user) do
    key = "user_info:#{user.id}"
Thog's avatar
Thog committed
548
    Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
lain's avatar
lain committed
549
  end
lain's avatar
lain committed
550

lain's avatar
lain committed
551
552
553
554
555
556
557
558
559
  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
560
  def get_or_fetch_by_nickname(nickname) do
lain's avatar
lain committed
561
    with %User{} = user <- get_by_nickname(nickname) do
lain's avatar
lain committed
562
      user
lain's avatar
lain committed
563
564
565
566
    else
      _e ->
        with [_nick, _domain] <- String.split(nickname, "@"),
             {:ok, user} <- fetch_by_nickname(nickname) do
567
568
569
570
          if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
            {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
          end

lain's avatar
lain committed
571
572
573
574
          user
        else
          _e -> nil
        end
lain's avatar
lain committed
575
    end
lain's avatar
lain committed
576
  end
lain's avatar
lain committed
577

578
579
580
581
582
583
584
585
586
587
588
  @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

589
  def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
590
591
592
593
594
595
596
    from(
      u in User,
      where: fragment("? <@ ?", ^[follower_address], u.following),
      where: u.id != ^id
    )
  end

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

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

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

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

616
  def get_friends_query(%User{id: id, following: following}, nil) do
617
618
619
620
621
622
623
    from(
      u in User,
      where: u.follower_address in ^following,
      where: u.id != ^id
    )
  end

624
  def get_friends_query(user, page) do
Maxim Filippov's avatar
Maxim Filippov committed
625
626
    from(u in get_friends_query(user, nil))
    |> paginate(page, 20)
627
628
629
630
631
632
  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
633
634
635

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

637
638
639
640
641
642
  def get_friends_ids(user, page \\ nil) do
    q = get_friends_query(user, page)

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

643
644
645
  def get_follow_requests_query(%User{} = user) do
    from(
      a in Activity,
kaniini's avatar
kaniini committed
646
647
648
649
650
651
652
653
654
655
656
657
      where:
        fragment(
          "? ->> 'type' = 'Follow'",
          a.data
        ),
      where:
        fragment(
          "? ->> 'state' = 'pending'",
          a.data
        ),
      where:
        fragment(
658
          "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
kaniini's avatar
kaniini committed
659
          a.data,
660
661
          a.data,
          ^user.ap_id
kaniini's avatar
kaniini committed
662
        )
663
664
665
666
667
    )
  end

  def get_follow_requests(%User{} = user) do
    users =
668
669
      user
      |> User.get_follow_requests_query()
670
671
672
673
674
      |> join(:inner, [a], u in User, a.actor == u.ap_id)
      |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
      |> group_by([a, u], u.id)
      |> select([a, u], u)
      |> Repo.all()
675
676
677
678

    {:ok, users}
  end

679
  def increase_note_count(%User{} = user) do
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
    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
          )
      ]
    )
    |> Repo.update_all([], returning: true)
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
697
698
  end

699
  def decrease_note_count(%User{} = user) do
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
    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
          )
      ]
    )
    |> Repo.update_all([], returning: true)
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
717
718
  end

719
  def update_note_count(%User{} = user) do
lain's avatar
lain committed
720
721
722
723
724
725
    note_count_query =
      from(
        a in Object,
        where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
        select: count(a.id)
      )
726
727
728

    note_count = Repo.one(note_count_query)

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

lain's avatar
lain committed
731
732
733
    cng =
      change(user)
      |> put_embed(:info, info_cng)
734

lain's avatar
lain committed
735
    update_and_set_cache(cng)
736
737
738
  end

  def update_follower_count(%User{} = user) do
lain's avatar
lain committed
739
    follower_count_query =
740
741
742
743
      User
      |> where([u], ^user.follower_address in u.following)
      |> where([u], u.id != ^user.id)
      |> select([u], %{count: count(u.id)})
744

745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
    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
          )
      ]
    )
    |> Repo.update_all([], returning: true)
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
763
  end
764

765
  def get_users_from_set_query(ap_ids, false) do
766
767
    from(
      u in User,
768
      where: u.ap_id in ^ap_ids
769
770
771
    )
  end

772
773
  def get_users_from_set_query(ap_ids, true) do
    query = get_users_from_set_query(ap_ids, false)
774
775
776

    from(
      u in query,
777
778
779
780
      where: u.local == true
    )
  end

781
782
783
784
785
  def get_users_from_set(ap_ids, local_only \\ true) do
    get_users_from_set_query(ap_ids, local_only)
    |> Repo.all()
  end

786
  def get_recipients_from_activity(%Activity{recipients: to}) do
lain's avatar
lain committed
787
788
789
790
791
792
    query =
      from(
        u in User,
        where: u.ap_id in ^to,
        or_where: fragment("? && ?", u.following, ^to)
      )
793

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

796
797
798
    Repo.all(query)
  end

799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
  @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 =
      from(u in User, order_by: u.id)
      |> 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
822
823
824
825
826
          admin: Pleroma.User.t(),
          local: boolean(),
          page: number(),
          page_size: number()
        }) :: {:ok, [Pleroma.User.t()], number()}
827
828
829
830
831
832
833
  def search_for_admin(%{
        query: term,
        admin: admin,
        local: local,
        page: page,
        page_size: page_size
      }) do
834
    term = String.trim_leading(term, "@")
835

Maxim Filippov's avatar
Maxim Filippov committed
836
837
838
839
    local_paginated_query =
      User
      |> maybe_local_user_query(local)
      |> paginate(page, page_size)
lain's avatar
lain committed
840

Maxim Filippov's avatar
Maxim Filippov committed
841
    search_query = fts_search_subquery(term, local_paginated_query)
842

Maxim Filippov's avatar
Maxim Filippov committed
843
844
845
846
847
    count =
      term
      |> fts_search_subquery()
      |> maybe_local_user_query(local)
      |> Repo.aggregate(:count, :id)
lain's avatar
lain committed
848

Maxim Filippov's avatar
Maxim Filippov committed
849
    {:ok, do_search(search_query, admin), count}
850
  end
lain's avatar
lain committed
851

852
  def search(query, resolve \\ false, for_user \\ nil) do
853
    # Strip the beginning @ off if there is a query
854
855
    query = String.trim_leading(query, "@")

856
    if resolve, do: get_or_fetch(query)
lain's avatar
lain committed
857

858
    fts_results = do_search(fts_search_subquery(query), for_user)
859

lain's avatar
lain committed
860
861
862
863
864
    {:ok, trigram_results} =
      Repo.transaction(fn ->
        Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
        do_search(trigram_search_subquery(query), for_user)
      end)
lain's avatar
lain committed
865

866
867
    Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
  end
lain's avatar
lain committed
868

869
  defp do_search(subquery, for_user, options \\ []) do
kaniini's avatar
kaniini committed
870
871
    q =
      from(
872
        s in subquery(subquery),
873
        order_by: [desc: s.search_rank],
874
        limit: ^(options[:limit] || 20)
kaniini's avatar
kaniini committed
875
      )
lain's avatar
lain committed
876

877
878
879
880
881
    results =
      q
      |> Repo.all()
      |> Enum.filter(&(&1.search_rank > 0))

882
883
    boost_search_results(results, for_user)
  end
884

Maxim Filippov's avatar
Maxim Filippov committed
885
  defp fts_search_subquery(term, query \\ User) do
886
    processed_query =
887
      term
888
889
890
891
892
      |> String.replace(~r/\W+/, " ")
      |> String.trim()
      |> String.split()
      |> Enum.map(&(&1 <> ":*"))
      |> Enum.join(" | ")
893

894
    from(
895
      u in query,
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
      select_merge: %{
        search_rank:
          fragment(
            """
            ts_rank_cd(
              setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
              setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
              to_tsquery('simple', ?),
              32
            )
            """,
            u.nickname,
            u.name,
            ^processed_query
          )
      },
lain's avatar
lain committed
912
913
914
915
916
917
918
919
920
921
      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
        )
922
923
    )
  end
924

Maxim Filippov's avatar
Maxim Filippov committed
925
  defp trigram_search_subquery(term) do
926
927
928
929
930
    from(
      u in User,
      select_merge: %{
        search_rank:
          fragment(
931
            "similarity(?, trim(? || ' ' || coalesce(?, '')))",
932
            ^term,
933
934
935
936
            u.nickname,
            u.name
          )
      },
937
      where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
938
939
940
941
942
943
944
945
    )
  end

  defp boost_search_results(results, nil), do: results

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

947
948
949
950
951
952
953
954
955
956
957
958
959
    Enum.map(
      results,
      fn u ->
        search_rank_coef =
          cond do
            u.id in friends_ids ->
              1.2

            u.id in followers_ids ->
              1.1

            true ->
              1
960
961
          end

962
963
964
965
        Map.put(u, :search_rank, u.search_rank * search_rank_coef)
      end
    )
    |> Enum.sort_by(&(-&1.search_rank))
lain's avatar
lain committed
966
  end
lain's avatar
lain committed
967

968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
  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

985
  def mute(muter, %User{ap_id: ap_id}) do
986
987
988
989
990
991
992
    info_cng =
      muter.info
      |> User.Info.add_to_mutes(ap_id)

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

994
    update_and_set_cache(cng)
995
996
  end

997
998
999
1000
  def unmute(muter, %{ap_id: ap_id}) do
    info_cng =
      muter.info
      |> User.Info.remove_from_mutes(ap_id)
For faster browsing, not all history is shown. View entire blame