user.ex 37.3 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
    field(:bio, :string)
    field(:email, :string)
    field(:name, :string)
    field(:nickname, :string)
44
45
    field(:auth_provider, :string)
    field(:auth_provider_uid, :string)
lain's avatar
lain committed
46
47
48
49
50
51
52
53
    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)
54
    field(:search_rank, :float, virtual: true)
55
    field(:tags, {:array, :string}, default: [])
Haelwenn's avatar
Haelwenn committed
56
    field(:bookmarks, {:array, :string}, default: [])
57
    field(:last_refreshed_at, :naive_datetime)
lain's avatar
lain committed
58
    has_many(:notifications, Notification)
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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
  # TODO: FIXME (WIP):
  def oauth_register_changeset(struct, params \\ %{}) do
    info_change = User.Info.confirmation_changeset(%User.Info{}, :confirmed)

    changeset =
      struct
      |> cast(params, [:email, :nickname, :name, :bio, :auth_provider, :auth_provider_uid])
      |> validate_required([:auth_provider, :auth_provider_uid])
      |> 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

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

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

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

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

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

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

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

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

  def needs_update?(_), do: true

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

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

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

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

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

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

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

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

386
    ap_followers = followed.follower_address
387

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

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

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

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

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

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

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

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

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

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

      {:ok, followed} = update_follower_count(followed)

428
429
      set_cache(follower)

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

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

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

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

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

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

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

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

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

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

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

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

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

    get_cached_by_ap_id(ap_id)
523
524
  end

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

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

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

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

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

547
548
549
550
551
552
553
  def get_by_auth_provider_uid(auth_provider, auth_provider_uid),
    do:
      Repo.get_by(User,
        auth_provider: to_string(auth_provider),
        auth_provider_uid: to_string(auth_provider_uid)
      )

lain's avatar
lain committed
554
555
  def get_cached_user_info(user) do
    key = "user_info:#{user.id}"
Thog's avatar
Thog committed
556
    Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
lain's avatar
lain committed
557
  end
lain's avatar
lain committed
558

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

lain's avatar
lain committed
579
580
581
582
          user
        else
          _e -> nil
        end
lain's avatar
lain committed
583
    end
lain's avatar
lain committed
584
  end
lain's avatar
lain committed
585

586
587
588
589
590
591
592
593
594
595
596
  @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

597
  def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
598
599
600
601
602
603
604
    from(
      u in User,
      where: fragment("? <@ ?", ^[follower_address], u.following),
      where: u.id != ^id
    )
  end

605
  def get_followers_query(user, page) do
Maxim Filippov's avatar
Maxim Filippov committed
606
607
    from(u in get_followers_query(user, nil))
    |> paginate(page, 20)
608
609
610
611
612
613
  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
614
615
616
617

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

618
619
620
621
622
623
  def get_followers_ids(user, page \\ nil) do
    q = get_followers_query(user, page)

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

624
  def get_friends_query(%User{id: id, following: following}, nil) do
625
626
627
628
629
630
631
    from(
      u in User,
      where: u.follower_address in ^following,
      where: u.id != ^id
    )
  end

632
  def get_friends_query(user, page) do
Maxim Filippov's avatar
Maxim Filippov committed
633
634
    from(u in get_friends_query(user, nil))
    |> paginate(page, 20)
635
636
637
638
639
640
  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
641
642
643

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

645
646
647
648
649
650
  def get_friends_ids(user, page \\ nil) do
    q = get_friends_query(user, page)

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

651
652
653
  def get_follow_requests_query(%User{} = user) do
    from(
      a in Activity,
kaniini's avatar
kaniini committed
654
655
656
657
658
659
660
661
662
663
664
665
      where:
        fragment(
          "? ->> 'type' = 'Follow'",
          a.data
        ),
      where:
        fragment(
          "? ->> 'state' = 'pending'",
          a.data
        ),
      where:
        fragment(
666
          "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
kaniini's avatar
kaniini committed
667
          a.data,
668
669
          a.data,
          ^user.ap_id
kaniini's avatar
kaniini committed
670
        )
671
672
673
674
675
    )
  end

  def get_follow_requests(%User{} = user) do
    users =
676
677
      user
      |> User.get_follow_requests_query()
678
679
680
681
682
      |> 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()
683
684
685
686

    {:ok, users}
  end

687
  def increase_note_count(%User{} = user) do
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
    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
705
706
  end

707
  def decrease_note_count(%User{} = user) do
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
    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
725
726
  end

727
  def update_note_count(%User{} = user) do
lain's avatar
lain committed
728
729
730
731
732
733
    note_count_query =
      from(
        a in Object,
        where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
        select: count(a.id)
      )
734
735
736

    note_count = Repo.one(note_count_query)

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

lain's avatar
lain committed
739
740
741
    cng =
      change(user)
      |> put_embed(:info, info_cng)
742

lain's avatar
lain committed
743
    update_and_set_cache(cng)
744
745
746
  end

  def update_follower_count(%User{} = user) do
lain's avatar
lain committed
747
    follower_count_query =
748
749
750
751
      User
      |> where([u], ^user.follower_address in u.following)
      |> where([u], u.id != ^user.id)
      |> select([u], %{count: count(u.id)})
752

753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
    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
771
  end
772

773
  def get_users_from_set_query(ap_ids, false) do
774
775
    from(
      u in User,
776
      where: u.ap_id in ^ap_ids
777
778
779
    )
  end

780
781
  def get_users_from_set_query(ap_ids, true) do
    query = get_users_from_set_query(ap_ids, false)
782
783
784

    from(
      u in query,
785
786
787
788
      where: u.local == true
    )
  end

789
790
791
792
793
  def get_users_from_set(ap_ids, local_only \\ true) do
    get_users_from_set_query(ap_ids, local_only)
    |> Repo.all()
  end

794
  def get_recipients_from_activity(%Activity{recipients: to}) do
lain's avatar
lain committed
795
796
797
798
799
800
    query =
      from(
        u in User,
        where: u.ap_id in ^to,
        or_where: fragment("? && ?", u.following, ^to)
      )
801

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

804
805
806
    Repo.all(query)
  end

807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
  @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
830
831
832
833
834
          admin: Pleroma.User.t(),
          local: boolean(),
          page: number(),
          page_size: number()
        }) :: {:ok, [Pleroma.User.t()], number()}
835
836
837
838
839
840
841
  def search_for_admin(%{
        query: term,
        admin: admin,
        local: local,
        page: page,
        page_size: page_size
      }) do
842
    term = String.trim_leading(term, "@")
843

Maxim Filippov's avatar
Maxim Filippov committed
844
845
846
847
    local_paginated_query =
      User
      |> maybe_local_user_query(local)
      |> paginate(page, page_size)
lain's avatar
lain committed
848

Maxim Filippov's avatar
Maxim Filippov committed
849
    search_query = fts_search_subquery(term, local_paginated_query)
850

Maxim Filippov's avatar
Maxim Filippov committed
851
852
853
854
855
    count =
      term
      |> fts_search_subquery()
      |> maybe_local_user_query(local)
      |> Repo.aggregate(:count, :id)
lain's avatar
lain committed
856

Maxim Filippov's avatar
Maxim Filippov committed
857
    {:ok, do_search(search_query, admin), count}
858
  end
lain's avatar
lain committed
859

860
  def search(query, resolve \\ false, for_user \\ nil) do
861
    # Strip the beginning @ off if there is a query
862
863
    query = String.trim_leading(query, "@")

864
    if resolve, do: get_or_fetch(query)
lain's avatar
lain committed
865

866
    fts_results = do_search(fts_search_subquery(query), for_user)
867

lain's avatar
lain committed
868
869
870
871
872
    {: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
873

874
875
    Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
  end
lain's avatar
lain committed
876

877
  defp do_search(subquery, for_user, options \\ []) do
kaniini's avatar
kaniini committed
878
879
    q =
      from(
880
        s in subquery(subquery),
881
        order_by: [desc: s.search_rank],
882
        limit: ^(options[:limit] || 20)
kaniini's avatar
kaniini committed
883
      )
lain's avatar
lain committed
884

885
886
887
888
889
    results =
      q
      |> Repo.all()
      |> Enum.filter(&(&1.search_rank > 0))

890
891
    boost_search_results(results, for_user)
  end
892

Maxim Filippov's avatar
Maxim Filippov committed
893
  defp fts_search_subquery(term, query \\ User) do
894
    processed_query =
895
      term
896
897
898
899
900
      |> String.replace(~r/\W+/, " ")
      |> String.trim()
      |> String.split()
      |> Enum.map(&(&1 <> ":*"))
      |> Enum.join(" | ")
901

902
    from(
903
      u in query,
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
      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
920
921
922
923
924
925
926
927
928
929
      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
        )
930
931
    )
  end
932

Maxim Filippov's avatar
Maxim Filippov committed
933
  defp trigram_search_subquery(term) do
934
935
936
937
938
    from(
      u in User,
      select_merge: %{
        search_rank:
          fragment(
939
            "similarity(?, trim(? || ' ' || coalesce(?, '')))",
940
            ^term,
941
942
943
944
            u.nickname,
            u.name
          )
      },
945
      where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
946
947
948
949
950
951
952
953
    )
  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)
954

955
956
957
958
959
960
961
962
963
964
965
966
967
    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
968
969
          end

970
971
972
973
        Map.put(u, :search_rank, u.search_rank * search_rank_coef)
      end
    )
    |> Enum.sort_by(&(-&1.search_rank))
lain's avatar
lain committed
974
  end
lain's avatar
lain committed
975

976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
  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

993
  def mute(muter, %User{ap_id: ap_id}) do
994
995
996
997
998
999
1000
    info_cng =
      muter.info
      |> User.Info.add_to_mutes(ap_id)

    cng =
      change(muter)
      |> put_embed(:info, info_cng)
For faster browsing, not all history is shown. View entire blame