user.ex 38.5 KB
Newer Older
1
# Pleroma: A lightweight social networking server
kaniini's avatar
kaniini committed
2
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3
4
# SPDX-License-Identifier: AGPL-3.0-only

lain's avatar
lain committed
5
6
defmodule Pleroma.User do
  use Ecto.Schema
7

Haelwenn's avatar
Haelwenn committed
8
9
10
  import Ecto.Changeset
  import Ecto.Query

11
12
13
14
  alias Comeonin.Pbkdf2
  alias Pleroma.Activity
  alias Pleroma.Notification
  alias Pleroma.Object
15
  alias Pleroma.Registration
Haelwenn's avatar
Haelwenn committed
16
17
18
  alias Pleroma.Repo
  alias Pleroma.User
  alias Pleroma.Web
19
20
  alias Pleroma.Web.ActivityPub.ActivityPub
  alias Pleroma.Web.ActivityPub.Utils
Maxim Filippov's avatar
Maxim Filippov committed
21
  alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
Haelwenn's avatar
Haelwenn committed
22
  alias Pleroma.Web.OAuth
23
  alias Pleroma.Web.OStatus
24
  alias Pleroma.Web.RelMe
25
  alias Pleroma.Web.Websub
lain's avatar
lain committed
26

27
28
  require Logger

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

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

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

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

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

    timestamps()
  end
lain's avatar
lain committed
62

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

66
  def auth_active?(%User{}), do: true
67

68
69
70
71
72
  def visible_for?(user, for_user \\ nil)

  def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true

  def visible_for?(%User{} = user, for_user) do
73
    auth_active?(user) || superuser?(for_user)
74
75
  end

76
77
  def visible_for?(_, _), do: false

78
79
  def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
  def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
80
  def superuser?(_), do: false
81

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

89
  def banner_url(user, options \\ []) do
lain's avatar
lain committed
90
    case user.info.banner do
lain's avatar
lain committed
91
      %{"url" => [%{"href" => href} | _]} -> href
92
      _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
lain's avatar
lain committed
93
94
95
    end
  end

lain's avatar
lain committed
96
  def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
97
98
99
  def profile_url(%User{ap_id: ap_id}), do: ap_id
  def profile_url(_), do: nil

lain's avatar
lain committed
100
  def ap_id(%User{nickname: nickname}) do
lain's avatar
lain committed
101
    "#{Web.base_url()}/users/#{nickname}"
lain's avatar
lain committed
102
103
  end

104
105
  def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
  def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
lain's avatar
lain committed
106

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  def needs_update?(_), do: true

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

Maksim's avatar
Maksim committed
326
  def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
327
328
    if not following?(follower, followed) do
      follow(follower, followed)
329
    else
330
      {:ok, follower}
331
332
333
    end
  end

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

lain's avatar
lain committed
342
343
344
    q =
      from(u in User,
        where: u.id == ^follower.id,
345
346
347
348
349
350
351
352
353
        update: [
          set: [
            following:
              fragment(
                "array(select distinct unnest (array_cat(?, ?)))",
                u.following,
                ^followed_addresses
              )
          ]
rinpatch's avatar
rinpatch committed
354
355
        ],
        select: u
lain's avatar
lain committed
356
357
      )

rinpatch's avatar
rinpatch committed
358
    {1, [follower]} = Repo.update_all(q, [])
lain's avatar
lain committed
359
360
361
362
363

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

lain's avatar
lain committed
364
    set_cache(follower)
lain's avatar
lain committed
365
366
  end

lain's avatar
lain committed
367
  def follow(%User{} = follower, %User{info: info} = followed) do
368
369
    user_config = Application.get_env(:pleroma, :user)
    deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
370

371
    ap_followers = followed.follower_address
372

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

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

380
381
382
383
384
      true ->
        if !followed.local && follower.local && !ap_enabled?(followed) do
          Websub.subscribe(follower, followed)
        end

385
386
387
        q =
          from(u in User,
            where: u.id == ^follower.id,
rinpatch's avatar
rinpatch committed
388
389
            update: [push: [following: ^ap_followers]],
            select: u
390
          )
391

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

394
395
        {:ok, _} = update_follower_count(followed)

396
        set_cache(follower)
397
    end
lain's avatar
lain committed
398
  end
lain's avatar
lain committed
399
400

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

403
    if following?(follower, followed) and follower.ap_id != followed.ap_id do
404
405
406
      q =
        from(u in User,
          where: u.id == ^follower.id,
rinpatch's avatar
rinpatch committed
407
408
          update: [pull: [following: ^ap_followers]],
          select: u
409
        )
lain's avatar
lain committed
410

rinpatch's avatar
rinpatch committed
411
      {1, [follower]} = Repo.update_all(q, [])
412
413
414

      {:ok, followed} = update_follower_count(followed)

415
416
      set_cache(follower)

417
      {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
418
    else
419
      {:error, "Not subscribed!"}
420
    end
lain's avatar
lain committed
421
  end
422

Maksim's avatar
Maksim committed
423
  @spec following?(User.t(), User.t()) :: boolean
424
  def following?(%User{} = follower, %User{} = followed) do
425
    Enum.member?(follower.following, followed.follower_address)
426
  end
lain's avatar
lain committed
427

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

432
433
434
435
  def get_by_id(id) do
    Repo.get_by(User, id: id)
  end

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

440
441
  # 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
442
443
444
445
446
  def get_by_guessed_nickname(ap_id) do
    domain = URI.parse(ap_id).host
    name = List.last(String.split(ap_id, "/"))
    nickname = "#{name}@#{domain}"

minibikini's avatar
minibikini committed
447
    get_cached_by_nickname(nickname)
448
449
  end

minibikini's avatar
minibikini committed
450
451
452
453
  def set_cache({:ok, user}), do: set_cache(user)
  def set_cache({:error, err}), do: {:error, err}

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

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

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

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

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

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

    get_cached_by_ap_id(ap_id)
495
496
  end

lain's avatar
lain committed
497
  def get_cached_by_nickname(nickname) do
498
    key = "nickname:#{nickname}"
0x1C3B00DA's avatar
Run    
0x1C3B00DA committed
499

500
501
502
503
504
    Cachex.fetch!(:user_cache, key, fn ->
      user_result = get_or_fetch_by_nickname(nickname)

      case user_result do
        {:ok, user} -> {:commit, user}
Alexander Strizhakov's avatar
Alexander Strizhakov committed
505
        {:error, _error} -> {:ignore, nil}
506
507
      end
    end)
lain's avatar
lain committed
508
  end
lain's avatar
lain committed
509

510
  def get_cached_by_nickname_or_id(nickname_or_id) do
511
    get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
512
513
  end

lain's avatar
lain committed
514
  def get_by_nickname(nickname) do
515
    Repo.get_by(User, nickname: nickname) ||
516
      if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
517
        Repo.get_by(User, nickname: local_nickname(nickname))
518
      end
519
520
  end

521
522
  def get_by_email(email), do: Repo.get_by(User, email: email)

523
  def get_by_nickname_or_email(nickname_or_email) do
524
    get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
525
526
  end

lain's avatar
lain committed
527
528
  def get_cached_user_info(user) do
    key = "user_info:#{user.id}"
Thog's avatar
Thog committed
529
    Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
lain's avatar
lain committed
530
  end
lain's avatar
lain committed
531

lain's avatar
lain committed
532
533
534
535
536
537
538
539
540
  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
541
  def get_or_fetch_by_nickname(nickname) do
lain's avatar
lain committed
542
    with %User{} = user <- get_by_nickname(nickname) do
543
      {:ok, user}
lain's avatar
lain committed
544
545
546
547
    else
      _e ->
        with [_nick, _domain] <- String.split(nickname, "@"),
             {:ok, user} <- fetch_by_nickname(nickname) do
548
          if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
549
            fetch_initial_posts(user)
550
551
          end

552
          {:ok, user}
lain's avatar
lain committed
553
        else
Alexander Strizhakov's avatar
Alexander Strizhakov committed
554
          _e -> {:error, "not found " <> nickname}
lain's avatar
lain committed
555
        end
lain's avatar
lain committed
556
    end
lain's avatar
lain committed
557
  end
lain's avatar
lain committed
558

559
  @doc "Fetch some posts when the user has just been federated with"
560
561
  def fetch_initial_posts(user),
    do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
562

Alexander Strizhakov's avatar
Alexander Strizhakov committed
563
564
  @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
  def get_followers_query(%User{} = user, nil) do
565
    User.Query.build(%{followers: user, deactivated: false})
566
567
  end

568
  def get_followers_query(user, page) do
Maxim Filippov's avatar
Maxim Filippov committed
569
    from(u in get_followers_query(user, nil))
Alexander Strizhakov's avatar
Alexander Strizhakov committed
570
    |> User.Query.paginate(page, 20)
571
572
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
573
  @spec get_followers_query(User.t()) :: Ecto.Query.t()
574
575
576
577
  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
578
579
580
581

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

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

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

Alexander Strizhakov's avatar
Alexander Strizhakov committed
588
589
  @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
  def get_friends_query(%User{} = user, nil) do
590
    User.Query.build(%{friends: user, deactivated: false})
591
592
  end

593
  def get_friends_query(user, page) do
Maxim Filippov's avatar
Maxim Filippov committed
594
    from(u in get_friends_query(user, nil))
Alexander Strizhakov's avatar
Alexander Strizhakov committed
595
    |> User.Query.paginate(page, 20)
596
597
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
598
  @spec get_friends_query(User.t()) :: Ecto.Query.t()
599
600
601
602
  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
603
604
605

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

607
608
609
610
611
612
  def get_friends_ids(user, page \\ nil) do
    q = get_friends_query(user, page)

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

Alexander Strizhakov's avatar
Alexander Strizhakov committed
613
  @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
614
615
  def get_follow_requests(%User{} = user) do
    users =
Alexander Strizhakov's avatar
Alexander Strizhakov committed
616
      Activity.follow_requests_for_actor(user)
rinpatch's avatar
rinpatch committed
617
      |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
618
619
620
621
      |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
      |> group_by([a, u], u.id)
      |> select([a, u], u)
      |> Repo.all()
622
623
624
625

    {:ok, users}
  end

626
  def increase_note_count(%User{} = user) do
627
628
629
630
631
632
633
634
635
636
637
638
    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
639
640
    |> select([u], u)
    |> Repo.update_all([])
641
642
643
644
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
645
646
  end

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

668
  def update_note_count(%User{} = user) do
lain's avatar
lain committed
669
670
671
672
673
674
    note_count_query =
      from(
        a in Object,
        where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
        select: count(a.id)
      )
675
676
677

    note_count = Repo.one(note_count_query)

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

680
681
682
683
    user
    |> change()
    |> put_embed(:info, info_cng)
    |> update_and_set_cache()
684
685
686
  end

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

691
692
693
694
695
696
697
698
699
700
701
702
703
    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
704
705
    |> select([u], u)
    |> Repo.update_all([])
706
707
708
709
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
710
  end
711

712
713
714
715
716
717
718
719
720
721
722
723
  def remove_duplicated_following(%User{following: following} = user) do
    uniq_following = Enum.uniq(following)

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

Alexander Strizhakov's avatar
Alexander Strizhakov committed
724
  @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
725
  def get_users_from_set(ap_ids, local_only \\ true) do
726
    criteria = %{ap_id: ap_ids, deactivated: false}
Alexander Strizhakov's avatar
Alexander Strizhakov committed
727
728
729
    criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria

    User.Query.build(criteria)
730
731
732
    |> Repo.all()
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
733
  @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
734
  def get_recipients_from_activity(%Activity{recipients: to}) do
735
    User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
Alexander Strizhakov's avatar
Alexander Strizhakov committed
736
    |> Repo.all()
737
738
  end

739
  def search(query, resolve \\ false, for_user \\ nil) do
740
    # Strip the beginning @ off if there is a query
741
742
    query = String.trim_leading(query, "@")

743
    if resolve, do: get_or_fetch(query)
lain's avatar
lain committed
744

745
    {:ok, results} =
lain's avatar
lain committed
746
747
      Repo.transaction(fn ->
        Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
748
        Repo.all(search_query(query, for_user))
lain's avatar
lain committed
749
      end)
lain's avatar
lain committed
750

751
    results
752
  end
lain's avatar
lain committed
753

754
755
756
  def search_query(query, for_user) do
    fts_subquery = fts_search_subquery(query)
    trigram_subquery = trigram_search_subquery(query)
757
758
    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
759

760
761
762
763
764
    from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
      order_by: [desc: s.search_rank],
      limit: 20
    )
  end
765

766
767
768
769
770
  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)
771

772
773
774
775
776
    from(u in subquery(query),
      select_merge: %{
        search_rank:
          fragment(
            """
777
             CASE WHEN (?) THEN (?) * 1.3
778
779
780
781
782
783
784
785
786
787
788
789
790
791
             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
          )
      }
    )
792
  end
793

Maxim Filippov's avatar
Maxim Filippov committed
794
  defp fts_search_subquery(term, query \\ User) do
795
    processed_query =
796
      term
797
798
799
800
801
      |> String.replace(~r/\W+/, " ")
      |> String.trim()
      |> String.split()
      |> Enum.map(&(&1 <> ":*"))
      |> Enum.join(" | ")
802

803
    from(
804
      u in query,
805
      select_merge: %{
806
        search_type: ^0,
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
        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
822
823
824
825
826
827
828
829
830
831
      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
        )
832
    )
833
    |> restrict_deactivated()
834
  end
835

Maxim Filippov's avatar
Maxim Filippov committed
836
  defp trigram_search_subquery(term) do
837
838
839
    from(
      u in User,
      select_merge: %{
840
841
        # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
        search_type: fragment("?", 1),
842
843
        search_rank:
          fragment(
844
            "similarity(?, trim(? || ' ' || coalesce(?, '')))",
845
            ^term,
846
847
848
849
            u.nickname,
            u.name
          )
      },
850
      where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
851
    )
852
    |> restrict_deactivated()
853
854
  end

855
  def mute(muter, %User{ap_id: ap_id}) do
856
857
858
859
860
861
862
    info_cng =
      muter.info
      |> User.Info.add_to_mutes(ap_id)

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

864
    update_and_set_cache(cng)
865
866
  end

867
868
869
870
  def unmute(muter, %{ap_id: ap_id}) do
    info_cng =
      muter.info
      |> User.Info.remove_from_mutes(ap_id)
871

872
873
874
875
876
    cng =
      change(muter)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
877
878
  end

879
  def subscribe(subscriber, %{ap_id: ap_id}) do
880
    deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
881

882
    with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
883
884
885
886
887
888
889
890
891
892
893
894
895
      blocked = blocks?(subscribed, subscriber) and deny_follow_blocked

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

        change(subscribed)
        |> put_embed(:info, info_cng)
        |> update_and_set_cache()
      end
896
    end
897
898
899
  end

  def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
900
    with %User{} = user <- get_cached_by_ap_id(ap_id) do
901
902
903
      info_cng =
        user.info
        |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
904

905
906
907
908
      change(user)
      |> put_embed(:info, info_cng)
      |> update_and_set_cache()
    end
909
910
  end

911
912
913
914
915
916
917
918
919
920
  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

921
922
923
924
925
926
927
928
    blocker =
      if subscribed_to?(blocked, blocker) do
        {:ok, blocker} = unsubscribe(blocked, blocker)
        blocker
      else
        blocker
      end

929
930
931
932
    if following?(blocked, blocker) do
      unfollow(blocked, blocker)
    end

933
934
    {:ok, blocker} = update_follower_count(blocker)