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

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

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

11
12
  alias Comeonin.Pbkdf2
  alias Pleroma.Activity
13
  alias Pleroma.Keys
14
15
  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(:search_type, :integer, virtual: true)
55
    field(:tags, {:array, :string}, default: [])
rinpatch's avatar
rinpatch committed
56
    field(:last_refreshed_at, :naive_datetime_usec)
lain's avatar
lain committed
57
    has_many(:notifications, Notification)
58
    has_many(:registrations, Registration)
59
    embeds_one(:info, User.Info)
lain's avatar
lain committed
60
61
62

    timestamps()
  end
lain's avatar
lain committed
63

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

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

69
70
71
72
73
  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
74
    auth_active?(user) || superuser?(for_user)
75
76
  end

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

79
80
  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
81
  def superuser?(_), do: false
82

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

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

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

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

105
106
  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
107

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  def needs_update?(_), do: true

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

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

lain's avatar
lain committed
335
336
337
    q =
      from(u in User,
        where: u.id == ^follower.id,
338
339
340
341
342
343
344
345
346
        update: [
          set: [
            following:
              fragment(
                "array(select distinct unnest (array_cat(?, ?)))",
                u.following,
                ^followed_addresses
              )
          ]
rinpatch's avatar
rinpatch committed
347
348
        ],
        select: u
lain's avatar
lain committed
349
350
      )

rinpatch's avatar
rinpatch committed
351
    {1, [follower]} = Repo.update_all(q, [])
lain's avatar
lain committed
352
353
354
355
356

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

lain's avatar
lain committed
357
    set_cache(follower)
lain's avatar
lain committed
358
359
  end

lain's avatar
lain committed
360
  def follow(%User{} = follower, %User{info: info} = followed) do
minibikini's avatar
minibikini committed
361
    deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
362
    ap_followers = followed.follower_address
363

364
    cond do
365
      info.deactivated ->
lain's avatar
lain committed
366
        {:error, "Could not follow user: You are deactivated."}
lain's avatar
lain committed
367

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

371
372
373
374
375
      true ->
        if !followed.local && follower.local && !ap_enabled?(followed) do
          Websub.subscribe(follower, followed)
        end

376
377
378
        q =
          from(u in User,
            where: u.id == ^follower.id,
rinpatch's avatar
rinpatch committed
379
380
            update: [push: [following: ^ap_followers]],
            select: u
381
          )
382

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

385
386
        {:ok, _} = update_follower_count(followed)

387
        set_cache(follower)
388
    end
lain's avatar
lain committed
389
  end
lain's avatar
lain committed
390
391

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

394
    if following?(follower, followed) and follower.ap_id != followed.ap_id do
395
396
397
      q =
        from(u in User,
          where: u.id == ^follower.id,
rinpatch's avatar
rinpatch committed
398
399
          update: [pull: [following: ^ap_followers]],
          select: u
400
        )
lain's avatar
lain committed
401

rinpatch's avatar
rinpatch committed
402
      {1, [follower]} = Repo.update_all(q, [])
403
404
405

      {:ok, followed} = update_follower_count(followed)

406
407
      set_cache(follower)

408
      {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
409
    else
410
      {:error, "Not subscribed!"}
411
    end
lain's avatar
lain committed
412
  end
413

Maksim's avatar
Maksim committed
414
  @spec following?(User.t(), User.t()) :: boolean
415
  def following?(%User{} = follower, %User{} = followed) do
416
    Enum.member?(follower.following, followed.follower_address)
417
  end
lain's avatar
lain committed
418

419
  def locked?(%User{} = user) do
420
    user.info.locked || false
421
422
  end

423
424
425
426
  def get_by_id(id) do
    Repo.get_by(User, id: id)
  end

lain's avatar
lain committed
427
428
429
430
  def get_by_ap_id(ap_id) do
    Repo.get_by(User, ap_id: ap_id)
  end

431
432
  # 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
433
434
435
436
437
  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
438
    get_cached_by_nickname(nickname)
439
440
  end

minibikini's avatar
minibikini committed
441
442
443
444
  def set_cache({:ok, user}), do: set_cache(user)
  def set_cache({:error, err}), do: {:error, err}

  def set_cache(%User{} = user) do
445
446
447
448
449
450
    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
451
452
  def update_and_set_cache(changeset) do
    with {:ok, user} <- Repo.update(changeset) do
453
      set_cache(user)
lain's avatar
lain committed
454
455
456
457
458
    else
      e -> e
    end
  end

lain's avatar
lain committed
459
460
461
  def invalidate_cache(user) do
    Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
    Cachex.del(:user_cache, "nickname:#{user.nickname}")
462
    Cachex.del(:user_cache, "user_info:#{user.id}")
lain's avatar
lain committed
463
464
  end

lain's avatar
lain committed
465
  def get_cached_by_ap_id(ap_id) do
466
    key = "ap_id:#{ap_id}"
Thog's avatar
Thog committed
467
    Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
lain's avatar
lain committed
468
469
  end

470
471
  def get_cached_by_id(id) do
    key = "id:#{id}"
472
473
474
475

    ap_id =
      Cachex.fetch!(:user_cache, key, fn _ ->
        user = get_by_id(id)
476
477
478
479
480
481
482

        if user do
          Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
          {:commit, user.ap_id}
        else
          {:ignore, ""}
        end
483
484
485
      end)

    get_cached_by_ap_id(ap_id)
486
487
  end

lain's avatar
lain committed
488
  def get_cached_by_nickname(nickname) do
489
    key = "nickname:#{nickname}"
0x1C3B00DA's avatar
Run    
0x1C3B00DA committed
490

491
492
493
494
495
    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
496
        {:error, _error} -> {:ignore, nil}
497
498
      end
    end)
lain's avatar
lain committed
499
  end
lain's avatar
lain committed
500

501
  def get_cached_by_nickname_or_id(nickname_or_id) do
502
    get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
503
504
  end

lain's avatar
lain committed
505
  def get_by_nickname(nickname) do
506
    Repo.get_by(User, nickname: nickname) ||
507
      if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
508
        Repo.get_by(User, nickname: local_nickname(nickname))
509
      end
510
511
  end

512
513
  def get_by_email(email), do: Repo.get_by(User, email: email)

514
  def get_by_nickname_or_email(nickname_or_email) do
515
    get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
516
517
  end

lain's avatar
lain committed
518
519
  def get_cached_user_info(user) do
    key = "user_info:#{user.id}"
Thog's avatar
Thog committed
520
    Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
lain's avatar
lain committed
521
  end
lain's avatar
lain committed
522

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

543
          {:ok, user}
lain's avatar
lain committed
544
        else
Alexander Strizhakov's avatar
Alexander Strizhakov committed
545
          _e -> {:error, "not found " <> nickname}
lain's avatar
lain committed
546
        end
lain's avatar
lain committed
547
    end
lain's avatar
lain committed
548
  end
lain's avatar
lain committed
549

550
  @doc "Fetch some posts when the user has just been federated with"
551
552
  def fetch_initial_posts(user),
    do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
553

Alexander Strizhakov's avatar
Alexander Strizhakov committed
554
555
  @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
  def get_followers_query(%User{} = user, nil) do
556
    User.Query.build(%{followers: user, deactivated: false})
557
558
  end

559
  def get_followers_query(user, page) do
Maxim Filippov's avatar
Maxim Filippov committed
560
    from(u in get_followers_query(user, nil))
Alexander Strizhakov's avatar
Alexander Strizhakov committed
561
    |> User.Query.paginate(page, 20)
562
563
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
564
  @spec get_followers_query(User.t()) :: Ecto.Query.t()
565
566
567
568
  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
569
570
571
572

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

573
574
575
576
577
578
  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
579
580
  @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
  def get_friends_query(%User{} = user, nil) do
581
    User.Query.build(%{friends: user, deactivated: false})
582
583
  end

584
  def get_friends_query(user, page) do
Maxim Filippov's avatar
Maxim Filippov committed
585
    from(u in get_friends_query(user, nil))
Alexander Strizhakov's avatar
Alexander Strizhakov committed
586
    |> User.Query.paginate(page, 20)
587
588
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
589
  @spec get_friends_query(User.t()) :: Ecto.Query.t()
590
591
592
593
  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
594
595
596

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

598
599
600
601
602
603
  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
604
  @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
605
606
  def get_follow_requests(%User{} = user) do
    users =
Alexander Strizhakov's avatar
Alexander Strizhakov committed
607
      Activity.follow_requests_for_actor(user)
rinpatch's avatar
rinpatch committed
608
      |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
609
610
611
612
      |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
      |> group_by([a, u], u.id)
      |> select([a, u], u)
      |> Repo.all()
613
614
615
616

    {:ok, users}
  end

617
  def increase_note_count(%User{} = user) do
618
619
620
621
622
623
624
625
626
627
628
629
    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
630
631
    |> select([u], u)
    |> Repo.update_all([])
632
633
634
635
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
636
637
  end

638
  def decrease_note_count(%User{} = user) do
639
640
641
642
643
644
645
646
647
648
649
650
    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
651
652
    |> select([u], u)
    |> Repo.update_all([])
653
654
655
656
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
657
658
  end

659
  def update_note_count(%User{} = user) do
lain's avatar
lain committed
660
661
662
663
664
665
    note_count_query =
      from(
        a in Object,
        where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
        select: count(a.id)
      )
666
667
668

    note_count = Repo.one(note_count_query)

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

671
672
673
674
    user
    |> change()
    |> put_embed(:info, info_cng)
    |> update_and_set_cache()
675
676
677
  end

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

682
683
684
685
686
687
688
689
690
691
692
693
694
    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
695
696
    |> select([u], u)
    |> Repo.update_all([])
697
698
699
700
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
701
  end
702

703
704
705
706
707
708
709
710
711
712
713
714
  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
715
  @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
716
  def get_users_from_set(ap_ids, local_only \\ true) do
717
    criteria = %{ap_id: ap_ids, deactivated: false}
Alexander Strizhakov's avatar
Alexander Strizhakov committed
718
719
720
    criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria

    User.Query.build(criteria)
721
722
723
    |> Repo.all()
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
724
  @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
725
  def get_recipients_from_activity(%Activity{recipients: to}) do
726
    User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
Alexander Strizhakov's avatar
Alexander Strizhakov committed
727
    |> Repo.all()
728
729
  end

730
  def mute(muter, %User{ap_id: ap_id}) do
731
732
733
734
735
736
737
    info_cng =
      muter.info
      |> User.Info.add_to_mutes(ap_id)

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

739
    update_and_set_cache(cng)
740
741
  end

742
743
744
745
  def unmute(muter, %{ap_id: ap_id}) do
    info_cng =
      muter.info
      |> User.Info.remove_from_mutes(ap_id)
746

747
748
749
750
751
    cng =
      change(muter)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
752
753
  end

754
  def subscribe(subscriber, %{ap_id: ap_id}) do
755
    deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
756

757
    with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
758
759
760
761
762
763
764
765
766
767
768
769
770
      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
771
    end
772
773
774
  end

  def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
775
    with %User{} = user <- get_cached_by_ap_id(ap_id) do
776
777
778
      info_cng =
        user.info
        |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
779

780
781
782
783
      change(user)
      |> put_embed(:info, info_cng)
      |> update_and_set_cache()
    end
784
785
  end

786
787
788
789
790
791
792
793
794
795
  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

796
797
798
799
800
801
802
803
    blocker =
      if subscribed_to?(blocked, blocker) do
        {:ok, blocker} = unsubscribe(blocked, blocker)
        blocker
      else
        blocker
      end

804
805
806
807
    if following?(blocked, blocker) do
      unfollow(blocked, blocker)
    end

808
809
    {:ok, blocker} = update_follower_count(blocker)

lain's avatar
lain committed
810
811
812
    info_cng =
      blocker.info
      |> User.Info.add_to_block(ap_id)
lain's avatar
lain committed
813

lain's avatar
lain committed
814
815
816
817
818
    cng =
      change(blocker)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
lain's avatar
lain committed
819
820
  end

821
822
  # helper to handle the block given only an actor's AP id
  def block(blocker, %{ap_id: ap_id}) do
minibikini's avatar
minibikini committed
823
    block(blocker, get_cached_by_ap_id(ap_id))
824
825
  end

lain's avatar
lain committed
826
827
828
829
  def unblock(blocker, %{ap_id: ap_id}) do
    info_cng =
      blocker.info
      |> User.Info.remove_from_block(ap_id)
lain's avatar
lain committed
830

lain's avatar
lain committed
831
832
833
834
835
    cng =
      change(blocker)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
lain's avatar
lain committed
836
837
  end

838
  def mutes?(nil, _), do: false
839
  def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
840

lain's avatar
lain committed
841
  def blocks?(user, %{ap_id: ap_id}) do
lain's avatar
lain committed
842
843
    blocks = user.info.blocks
    domain_blocks = user.info.domain_blocks
eal's avatar
eal committed
844
    %{host: host} = URI.parse(ap_id)
eal's avatar
eal committed
845
846
847
848
849

    Enum.member?(blocks, ap_id) ||
      Enum.any?(domain_blocks, fn domain ->
        host == domain
      end)
eal's avatar
eal committed
850
851
  end

Sadposter's avatar
Sadposter committed
852
  def subscribed_to?(user, %{ap_id: ap_id}) do
minibikini's avatar
minibikini committed
853
    with %User{} = target <- get_cached_by_ap_id(ap_id) do
854
855
      Enum.member?(target.info.subscribers, user.ap_id)
    end
Sadposter's avatar
Sadposter committed
856
857
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
858
859
  @spec muted_users(User.t()) :: [User.t()]
  def muted_users(user) do
860
    User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
Alexander Strizhakov's avatar
Alexander Strizhakov committed
861
862
    |> Repo.all()
  end
863

Alexander Strizhakov's avatar
Alexander Strizhakov committed
864
865
  @spec blocked_users(User.t()) :: [User.t()]
  def blocked_users(user) do
866
    User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
Alexander Strizhakov's avatar
Alexander Strizhakov committed
867
868
    |> Repo.all()
  end
869

Alexander Strizhakov's avatar
Alexander Strizhakov committed
870
871
  @spec subscribers(User.t()) :: [User.t()]
  def subscribers(user) do
872
    User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
Alexander Strizhakov's avatar
Alexander Strizhakov committed
873
874
    |> Repo.all()
  end
Sadposter's avatar
Sadposter committed
875

eal's avatar
eal committed
876
  def block_domain(user, domain) do
lain's avatar
lain committed
877
878
879
    info_cng =
      user.info
      |> User.Info.add_to_domain_block(domain)
eal's avatar
eal committed
880

lain's avatar
lain committed
881
882
883
    cng =
      change(user)
      |> put_embed(:info, info_cng)
lain's avatar
lain committed
884
885

    update_and_set_cache(cng)
eal's avatar
eal committed
886
887
888
  end

  def unblock_domain(user, domain) do
lain's avatar
lain committed
889
890
891
    info_cng =
      user.info
      |> User.Info.remove_from_domain_block(domain)
eal's avatar
eal committed
892

lain's avatar
lain committed
893
894
895
    cng =
      change(user)
      |> put_embed(:info, info_cng)
lain's avatar
lain committed
896
897

    update_and_set_cache(cng)
lain's avatar
lain committed
898
899
  end

900
  def deactivate_async(user, status \\ true) do
901
    PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
902
903
  end

scarlett's avatar
scarlett committed
904
  def deactivate(%User{} = user, status \\ true) do
lain's avatar
lain committed
905
    info_cng = User.Info.set_activation_status(user.info, status)
lain's avatar
lain committed
906

minibikini's avatar
minibikini committed
907
908
909
    with {:ok, friends} <- User.get_friends(user),
         {:ok, followers} <- User.get_followers(user),
         {:ok, user} <-
minibikini's avatar
minibikini committed
910
911
912
           user
           |> change()
           |> put_embed(:info, info_cng)
minibikini's avatar
minibikini committed
913
914
           |> update_and_set_cache() do
      Enum.each(followers, &invalidate_cache(&1))
915
      Enum.each(friends, &update_follower_count(&1))
minibikini's avatar
minibikini committed
916

917
918
      {:ok, user}
    end
lain's avatar
lain committed
919
  end
lain's avatar
lain committed
920