user.ex 46.2 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
  alias Comeonin.Pbkdf2
12
  alias Ecto.Multi
13
  alias Pleroma.Activity
14
  alias Pleroma.Conversation.Participation
15
  alias Pleroma.Delivery
16
  alias Pleroma.FollowingRelationship
17
  alias Pleroma.Keys
18
19
  alias Pleroma.Notification
  alias Pleroma.Object
20
  alias Pleroma.Registration
Haelwenn's avatar
Haelwenn committed
21
  alias Pleroma.Repo
Sergey Suprunenko's avatar
Sergey Suprunenko committed
22
  alias Pleroma.RepoStreamer
Haelwenn's avatar
Haelwenn committed
23
24
  alias Pleroma.User
  alias Pleroma.Web
25
26
  alias Pleroma.Web.ActivityPub.ActivityPub
  alias Pleroma.Web.ActivityPub.Utils
27
  alias Pleroma.Web.CommonAPI
Maxim Filippov's avatar
Maxim Filippov committed
28
  alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
Haelwenn's avatar
Haelwenn committed
29
  alias Pleroma.Web.OAuth
30
  alias Pleroma.Web.OStatus
31
  alias Pleroma.Web.RelMe
32
  alias Pleroma.Web.Websub
33
  alias Pleroma.Workers.BackgroundWorker
lain's avatar
lain committed
34

35
36
  require Logger

Maksim's avatar
Maksim committed
37
38
  @type t :: %__MODULE__{}

39
  @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
href's avatar
href committed
40

41
  # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
href's avatar
href committed
42
43
44
  @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
45
  @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
href's avatar
href committed
46

lain's avatar
lain committed
47
  schema "users" do
lain's avatar
lain committed
48
49
50
51
52
53
54
    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)
rinpatch's avatar
rinpatch committed
55
    field(:keys, :string)
lain's avatar
lain committed
56
57
58
59
    field(:ap_id, :string)
    field(:avatar, :map)
    field(:local, :boolean, default: true)
    field(:follower_address, :string)
60
    field(:following_address, :string)
61
    field(:search_rank, :float, virtual: true)
62
    field(:search_type, :integer, virtual: true)
63
    field(:tags, {:array, :string}, default: [])
rinpatch's avatar
rinpatch committed
64
    field(:last_refreshed_at, :naive_datetime_usec)
Roman Chvanikov's avatar
Roman Chvanikov committed
65
    field(:last_digest_emailed_at, :naive_datetime)
lain's avatar
lain committed
66
    has_many(:notifications, Notification)
67
    has_many(:registrations, Registration)
68
    has_many(:deliveries, Delivery)
69
    embeds_one(:info, User.Info)
lain's avatar
lain committed
70
71
72

    timestamps()
  end
lain's avatar
lain committed
73

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

77
  def auth_active?(%User{}), do: true
78

79
80
81
82
83
  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
84
    auth_active?(user) || superuser?(for_user)
85
86
  end

87
88
  def visible_for?(_, _), do: false

89
90
  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
91
  def superuser?(_), do: false
92

93
  def avatar_url(user, options \\ []) do
lain's avatar
lain committed
94
95
    case user.avatar do
      %{"url" => [%{"href" => href} | _]} -> href
96
      _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
lain's avatar
lain committed
97
98
99
    end
  end

100
  def banner_url(user, options \\ []) do
lain's avatar
lain committed
101
    case user.info.banner do
lain's avatar
lain committed
102
      %{"url" => [%{"href" => href} | _]} -> href
103
      _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
lain's avatar
lain committed
104
105
106
    end
  end

lain's avatar
lain committed
107
  def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
108
109
110
  def profile_url(%User{ap_id: ap_id}), do: ap_id
  def profile_url(_), do: nil

minibikini's avatar
minibikini committed
111
  def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
lain's avatar
lain committed
112

113
114
  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
115

116
117
118
119
  @spec ap_following(User.t()) :: Sring.t()
  def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
  def ap_following(%User{} = user), do: "#{ap_id(user)}/following"

120
121
  def user_info(%User{} = user, args \\ %{}) do
    following_count =
minibikini's avatar
minibikini committed
122
      Map.get(args, :following_count, user.info.following_count || following_count(user))
123

minibikini's avatar
minibikini committed
124
    follower_count = Map.get(args, :follower_count, user.info.follower_count)
125

lain's avatar
lain committed
126
    %{
lain's avatar
lain committed
127
128
      note_count: user.info.note_count,
      locked: user.info.locked,
Ivan Tashkinov's avatar
Ivan Tashkinov committed
129
      confirmation_pending: user.info.confirmation_pending,
lain's avatar
lain committed
130
      default_scope: user.info.default_scope
lain's avatar
lain committed
131
    }
132
133
134
135
    |> Map.put(:following_count, following_count)
    |> Map.put(:follower_count, follower_count)
  end

rinpatch's avatar
rinpatch committed
136
  def follow_state(%User{} = user, %User{} = target) do
minibikini's avatar
minibikini committed
137
138
    case Utils.fetch_latest_follow(user, target) do
      %{data: %{"state" => state}} -> state
rinpatch's avatar
rinpatch committed
139
      # Ideally this would be nil, but then Cachex does not commit the value
minibikini's avatar
minibikini committed
140
141
      _ -> false
    end
rinpatch's avatar
rinpatch committed
142
143
144
145
146
147
148
  end

  def get_cached_follow_state(user, target) do
    key = "follow_state:#{user.ap_id}|#{target.ap_id}"
    Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
  end

Maksim's avatar
Maksim committed
149
  @spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
rinpatch's avatar
rinpatch committed
150
  def set_follow_state_cache(user_ap_id, target_ap_id, state) do
minibikini's avatar
minibikini committed
151
    Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
rinpatch's avatar
rinpatch committed
152
153
  end

154
155
  def set_info_cache(user, args) do
    Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
lain's avatar
lain committed
156
157
  end

158
  @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
159
  def restrict_deactivated(query) do
160
    from(u in query,
161
      where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
162
163
164
    )
  end

165
  defdelegate following_count(user), to: FollowingRelationship
166

167
  defp truncate_if_exists(params, key, max_length) do
Sadposter's avatar
Sadposter committed
168
    if Map.has_key?(params, key) and is_binary(params[key]) do
169
170
171
172
173
174
175
      {value, _chopped} = String.split_at(params[key], max_length)
      Map.put(params, key, value)
    else
      params
    end
  end

lain's avatar
lain committed
176
  def remote_user_creation(params) do
177
178
    bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
    name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
lain's avatar
lain committed
179

180
181
182
183
184
185
    params =
      params
      |> Map.put(:info, params[:info] || %{})
      |> truncate_if_exists(:name, name_limit)
      |> truncate_if_exists(:bio, bio_limit)

minibikini's avatar
minibikini committed
186
187
    changeset =
      %User{local: false}
lain's avatar
lain committed
188
      |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
189
      |> validate_required([:name, :ap_id])
lain's avatar
lain committed
190
191
      |> unique_constraint(:nickname)
      |> validate_format(:nickname, @email_regex)
192
193
      |> validate_length(:bio, max: bio_limit)
      |> validate_length(:name, max: name_limit)
194
      |> change_info(&User.Info.remote_user_creation(&1, params[:info]))
lain's avatar
lain committed
195

minibikini's avatar
minibikini committed
196
197
198
199
200
    case params[:info][:source_data] do
      %{"followers" => followers, "following" => following} ->
        changeset
        |> put_change(:follower_address, followers)
        |> put_change(:following_address, following)
lain's avatar
lain committed
201

minibikini's avatar
minibikini committed
202
203
204
      _ ->
        followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
        put_change(changeset, :follower_address, followers)
205
    end
lain's avatar
lain committed
206
207
  end

lain's avatar
lain committed
208
  def update_changeset(struct, params \\ %{}) do
209
210
211
    bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
    name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)

Thog's avatar
Thog committed
212
    struct
213
    |> cast(params, [:bio, :name, :avatar])
lain's avatar
lain committed
214
    |> unique_constraint(:nickname)
href's avatar
href committed
215
    |> validate_format(:nickname, local_nickname_regex())
216
217
    |> validate_length(:bio, max: bio_limit)
    |> validate_length(:name, min: 1, max: name_limit)
lain's avatar
lain committed
218
219
  end

220
  def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
221
222
    bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
    name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
223

224
    params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
lain's avatar
lain committed
225

lain's avatar
lain committed
226
    struct
227
228
229
230
231
232
233
234
    |> cast(params, [
      :bio,
      :name,
      :follower_address,
      :following_address,
      :avatar,
      :last_refreshed_at
    ])
lain's avatar
lain committed
235
    |> unique_constraint(:nickname)
href's avatar
href committed
236
    |> validate_format(:nickname, local_nickname_regex())
237
238
    |> validate_length(:bio, max: bio_limit)
    |> validate_length(:name, max: name_limit)
239
    |> change_info(&User.Info.user_upgrade(&1, params[:info], remote?))
lain's avatar
lain committed
240
241
  end

Roger Braun's avatar
Roger Braun committed
242
  def password_update_changeset(struct, params) do
243
244
245
246
247
    struct
    |> cast(params, [:password, :password_confirmation])
    |> validate_required([:password, :password_confirmation])
    |> validate_confirmation(:password)
    |> put_password_hash
248
    |> put_embed(:info, User.Info.set_password_reset_pending(struct.info, false))
249
250
  end

Maksim's avatar
Maksim committed
251
  @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
252
253
254
255
256
257
258
259
260
261
  def reset_password(%User{id: user_id} = user, data) do
    multi =
      Multi.new()
      |> Multi.update(:user, password_update_changeset(user, data))
      |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
      |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))

    case Repo.transaction(multi) do
      {:ok, %{user: user} = _} -> set_cache(user)
      {:error, _, changeset, _} -> {:error, changeset}
Roger Braun's avatar
Roger Braun committed
262
263
264
    end
  end

265
266
267
268
269
270
271
272
273
274
275
276
277
278
  def force_password_reset_async(user) do
    BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
  end

  @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
  def force_password_reset(user) do
    info_cng = User.Info.set_password_reset_pending(user.info, true)

    user
    |> change()
    |> put_embed(:info, info_cng)
    |> update_and_set_cache()
  end

279
  def register_changeset(struct, params \\ %{}, opts \\ []) do
280
281
282
    bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
    name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)

283
284
285
    need_confirmation? =
      if is_nil(opts[:need_confirmation]) do
        Pleroma.Config.get([:instance, :account_activation_required])
286
      else
287
        opts[:need_confirmation]
288
289
      end

minibikini's avatar
minibikini committed
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
    struct
    |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
    |> validate_required([:name, :nickname, :password, :password_confirmation])
    |> validate_confirmation(:password)
    |> unique_constraint(:email)
    |> unique_constraint(:nickname)
    |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
    |> validate_format(:nickname, local_nickname_regex())
    |> validate_format(:email, @email_regex)
    |> validate_length(:bio, max: bio_limit)
    |> validate_length(:name, min: 1, max: name_limit)
    |> change_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
    |> maybe_validate_required_email(opts[:external])
    |> put_password_hash
    |> put_ap_id()
    |> unique_constraint(:ap_id)
    |> put_following_and_follower_address()
  end
Ivan Tashkinov's avatar
Ivan Tashkinov committed
308

minibikini's avatar
minibikini committed
309
310
  def maybe_validate_required_email(changeset, true), do: changeset
  def maybe_validate_required_email(changeset, _), do: validate_required(changeset, [:email])
lain's avatar
lain committed
311

minibikini's avatar
minibikini committed
312
313
314
315
  defp put_ap_id(changeset) do
    ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
    put_change(changeset, :ap_id, ap_id)
  end
316

minibikini's avatar
minibikini committed
317
318
  defp put_following_and_follower_address(changeset) do
    followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
lain's avatar
lain committed
319

minibikini's avatar
minibikini committed
320
321
    changeset
    |> put_change(:follower_address, followers)
lain's avatar
lain committed
322
323
  end

324
325
326
327
  defp autofollow_users(user) do
    candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])

    autofollowed_users =
328
      User.Query.build(%{nickname: candidates, local: true, deactivated: false})
329
330
      |> Repo.all()

lain's avatar
lain committed
331
    follow_all(user, autofollowed_users)
332
333
  end

334
335
  @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
  def register(%Ecto.Changeset{} = changeset) do
minibikini's avatar
minibikini committed
336
337
    with {:ok, user} <- Repo.insert(changeset) do
      post_register_action(user)
338
339
340
341
342
    end
  end

  def post_register_action(%User{} = user) do
    with {:ok, user} <- autofollow_users(user),
minibikini's avatar
minibikini committed
343
         {:ok, user} <- set_cache(user),
344
         {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
345
         {:ok, _} <- try_send_confirmation_email(user) do
346
347
348
349
      {:ok, user}
    end
  end

350
  def try_send_confirmation_email(%User{} = user) do
351
352
    if user.info.confirmation_pending &&
         Pleroma.Config.get([:instance, :account_activation_required]) do
353
354
355
      user
      |> Pleroma.Emails.UserEmail.account_confirmation_email()
      |> Pleroma.Emails.Mailer.deliver_async()
356
357

      {:ok, :enqueued}
358
359
360
361
362
    else
      {:ok, :noop}
    end
  end

363
364
365
366
367
  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
368
    NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
369
370
371
372
  end

  def needs_update?(_), do: true

Maksim's avatar
Maksim committed
373
  @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
374
375
376
377
378
  def maybe_direct_follow(
        %User{} = follower,
        %User{local: true, info: %{locked: true}} = followed
      ) do
    follow(follower, followed, "pending")
379
380
381
382
383
384
385
  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
minibikini's avatar
minibikini committed
386
    if not ap_enabled?(followed) do
387
      follow(follower, followed)
388
389
390
391
392
    else
      {:ok, follower}
    end
  end

393
  @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
lain's avatar
lain committed
394
395
  @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
  def follow_all(follower, followeds) do
396
397
398
399
    followeds =
      Enum.reject(followeds, fn followed ->
        blocks?(follower, followed) || blocks?(followed, follower)
      end)
lain's avatar
lain committed
400

401
    Enum.each(followeds, &follow(follower, &1, "accept"))
lain's avatar
lain committed
402

minibikini's avatar
minibikini committed
403
    Enum.each(followeds, &update_follower_count/1)
lain's avatar
lain committed
404

lain's avatar
lain committed
405
    set_cache(follower)
lain's avatar
lain committed
406
407
  end

408
409
410
  defdelegate following(user), to: FollowingRelationship

  def follow(%User{} = follower, %User{info: info} = followed, state \\ "accept") do
minibikini's avatar
minibikini committed
411
    deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
412

413
    cond do
414
      info.deactivated ->
lain's avatar
lain committed
415
        {:error, "Could not follow user: You are deactivated."}
lain's avatar
lain committed
416

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

420
421
422
423
424
      true ->
        if !followed.local && follower.local && !ap_enabled?(followed) do
          Websub.subscribe(follower, followed)
        end

425
        FollowingRelationship.follow(follower, followed, state)
426

427
428
        follower = maybe_update_following_count(follower)

429
430
        {:ok, _} = update_follower_count(followed)

431
        set_cache(follower)
432
    end
lain's avatar
lain committed
433
  end
lain's avatar
lain committed
434
435

  def unfollow(%User{} = follower, %User{} = followed) do
436
    if following?(follower, followed) and follower.ap_id != followed.ap_id do
437
      FollowingRelationship.unfollow(follower, followed)
438

439
440
      follower = maybe_update_following_count(follower)

441
442
      {:ok, followed} = update_follower_count(followed)

443
444
      set_cache(follower)

445
      {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
446
    else
447
      {:error, "Not subscribed!"}
448
    end
lain's avatar
lain committed
449
  end
450

451
  defdelegate following?(follower, followed), to: FollowingRelationship
lain's avatar
lain committed
452

453
  def locked?(%User{} = user) do
454
    user.info.locked || false
455
456
  end

457
458
459
460
  def get_by_id(id) do
    Repo.get_by(User, id: id)
  end

lain's avatar
lain committed
461
462
463
464
  def get_by_ap_id(ap_id) do
    Repo.get_by(User, ap_id: ap_id)
  end

465
466
467
468
469
470
471
  def get_all_by_ap_id(ap_ids) do
    from(u in __MODULE__,
      where: u.ap_id in ^ap_ids
    )
    |> Repo.all()
  end

Maksim's avatar
Maksim committed
472
473
474
475
476
  def get_all_by_ids(ids) do
    from(u in __MODULE__, where: u.id in ^ids)
    |> Repo.all()
  end

477
478
  # 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
479
480
481
482
483
  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
484
    get_cached_by_nickname(nickname)
485
486
  end

minibikini's avatar
minibikini committed
487
488
489
490
  def set_cache({:ok, user}), do: set_cache(user)
  def set_cache({:error, err}), do: {:error, err}

  def set_cache(%User{} = user) do
491
492
493
494
495
496
    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
497
  def update_and_set_cache(changeset) do
498
    with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
499
      set_cache(user)
lain's avatar
lain committed
500
501
502
    end
  end

lain's avatar
lain committed
503
504
505
  def invalidate_cache(user) do
    Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
    Cachex.del(:user_cache, "nickname:#{user.nickname}")
506
    Cachex.del(:user_cache, "user_info:#{user.id}")
lain's avatar
lain committed
507
508
  end

lain's avatar
lain committed
509
  def get_cached_by_ap_id(ap_id) do
510
    key = "ap_id:#{ap_id}"
Thog's avatar
Thog committed
511
    Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
lain's avatar
lain committed
512
513
  end

514
515
  def get_cached_by_id(id) do
    key = "id:#{id}"
516
517
518
519

    ap_id =
      Cachex.fetch!(:user_cache, key, fn _ ->
        user = get_by_id(id)
520
521
522
523
524
525
526

        if user do
          Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
          {:commit, user.ap_id}
        else
          {:ignore, ""}
        end
527
528
529
      end)

    get_cached_by_ap_id(ap_id)
530
531
  end

lain's avatar
lain committed
532
  def get_cached_by_nickname(nickname) do
533
    key = "nickname:#{nickname}"
0x1C3B00DA's avatar
Run    
0x1C3B00DA committed
534

535
    Cachex.fetch!(:user_cache, key, fn ->
minibikini's avatar
minibikini committed
536
      case get_or_fetch_by_nickname(nickname) do
537
        {:ok, user} -> {:commit, user}
Alexander Strizhakov's avatar
Alexander Strizhakov committed
538
        {:error, _error} -> {:ignore, nil}
539
540
      end
    end)
lain's avatar
lain committed
541
  end
lain's avatar
lain committed
542

543
  def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
544
545
546
    restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])

    cond do
547
      is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
548
549
        get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)

550
      restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
551
552
553
554
555
556
557
        get_cached_by_nickname(nickname_or_id)

      restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
        get_cached_by_nickname(nickname_or_id)

      true ->
        nil
558
    end
559
560
  end

lain's avatar
lain committed
561
  def get_by_nickname(nickname) do
562
    Repo.get_by(User, nickname: nickname) ||
563
      if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
564
        Repo.get_by(User, nickname: local_nickname(nickname))
565
      end
566
567
  end

568
569
  def get_by_email(email), do: Repo.get_by(User, email: email)

570
  def get_by_nickname_or_email(nickname_or_email) do
571
    get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
572
573
  end

lain's avatar
lain committed
574
575
  def get_cached_user_info(user) do
    key = "user_info:#{user.id}"
minibikini's avatar
minibikini committed
576
    Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
lain's avatar
lain committed
577
  end
lain's avatar
lain committed
578

lain's avatar
lain committed
579
  def fetch_by_nickname(nickname) do
minibikini's avatar
minibikini committed
580
    case ActivityPub.make_user_from_nickname(nickname) do
lain's avatar
lain committed
581
582
583
584
585
      {:ok, user} -> {:ok, user}
      _ -> OStatus.make_user(nickname)
    end
  end

lain's avatar
lain committed
586
  def get_or_fetch_by_nickname(nickname) do
lain's avatar
lain committed
587
    with %User{} = user <- get_by_nickname(nickname) do
588
      {:ok, user}
lain's avatar
lain committed
589
590
591
592
    else
      _e ->
        with [_nick, _domain] <- String.split(nickname, "@"),
             {:ok, user} <- fetch_by_nickname(nickname) do
593
          if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
594
            fetch_initial_posts(user)
595
596
          end

597
          {:ok, user}
lain's avatar
lain committed
598
        else
Alexander Strizhakov's avatar
Alexander Strizhakov committed
599
          _e -> {:error, "not found " <> nickname}
lain's avatar
lain committed
600
        end
lain's avatar
lain committed
601
    end
lain's avatar
lain committed
602
  end
lain's avatar
lain committed
603

604
  @doc "Fetch some posts when the user has just been federated with"
605
  def fetch_initial_posts(user) do
606
    BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
607
  end
608

Alexander Strizhakov's avatar
Alexander Strizhakov committed
609
610
  @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
  def get_followers_query(%User{} = user, nil) do
611
    User.Query.build(%{followers: user, deactivated: false})
612
613
  end

614
  def get_followers_query(user, page) do
minibikini's avatar
minibikini committed
615
616
    user
    |> get_followers_query(nil)
Alexander Strizhakov's avatar
Alexander Strizhakov committed
617
    |> User.Query.paginate(page, 20)
618
619
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
620
  @spec get_followers_query(User.t()) :: Ecto.Query.t()
621
622
  def get_followers_query(user), do: get_followers_query(user, nil)

623
  @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
624
  def get_followers(user, page \\ nil) do
minibikini's avatar
minibikini committed
625
626
627
    user
    |> get_followers_query(page)
    |> Repo.all()
lain's avatar
lain committed
628
629
  end

630
631
  @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
  def get_external_followers(user, page \\ nil) do
minibikini's avatar
minibikini committed
632
633
634
635
    user
    |> get_followers_query(page)
    |> User.Query.build(%{external: true})
    |> Repo.all()
636
637
  end

638
  def get_followers_ids(user, page \\ nil) do
minibikini's avatar
minibikini committed
639
640
641
642
    user
    |> get_followers_query(page)
    |> select([u], u.id)
    |> Repo.all()
643
644
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
645
646
  @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
  def get_friends_query(%User{} = user, nil) do
647
    User.Query.build(%{friends: user, deactivated: false})
648
649
  end

650
  def get_friends_query(user, page) do
minibikini's avatar
minibikini committed
651
652
    user
    |> get_friends_query(nil)
Alexander Strizhakov's avatar
Alexander Strizhakov committed
653
    |> User.Query.paginate(page, 20)
654
655
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
656
  @spec get_friends_query(User.t()) :: Ecto.Query.t()
657
658
659
  def get_friends_query(user), do: get_friends_query(user, nil)

  def get_friends(user, page \\ nil) do
minibikini's avatar
minibikini committed
660
661
662
    user
    |> get_friends_query(page)
    |> Repo.all()
lain's avatar
lain committed
663
  end
664

665
  def get_friends_ids(user, page \\ nil) do
minibikini's avatar
minibikini committed
666
667
668
669
    user
    |> get_friends_query(page)
    |> select([u], u.id)
    |> Repo.all()
670
671
  end

672
  defdelegate get_follow_requests(user), to: FollowingRelationship
673

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

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

716
  def update_note_count(%User{} = user) do
717
    note_count =
lain's avatar
lain committed
718
719
720
721
722
      from(
        a in Object,
        where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
        select: count(a.id)
      )
723
      |> Repo.one()
724

725
    update_info(user, &User.Info.set_note_count(&1, note_count))
726
727
  end

Maksim's avatar
Maksim committed
728
729
730
731
732
733
734
735
736
737
738
739
740
  def update_mascot(user, url) do
    info_changeset =
      User.Info.mascot_update(
        user.info,
        url
      )

    user
    |> change()
    |> put_embed(:info, info_changeset)
    |> update_and_set_cache()
  end

741
  @spec maybe_fetch_follow_information(User.t()) :: User.t()
742
743
744
745
746
  def maybe_fetch_follow_information(user) do
    with {:ok, user} <- fetch_follow_information(user) do
      user
    else
      e ->
rinpatch's avatar
rinpatch committed
747
        Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
748
749
750
751
752
753
754

        user
    end
  end

  def fetch_follow_information(user) do
    with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
755
      update_info(user, &User.Info.follow_information_update(&1, info))
756
757
758
    end
  end

759
  def update_follower_count(%User{} = user) do
760
    if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
      follower_count_query =
        User.Query.build(%{followers: user, deactivated: false})
        |> select([u], %{count: count(u.id)})

      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
            )
        ]
      )
      |> select([u], u)
      |> Repo.update_all([])
      |> case do
        {1, [user]} -> set_cache(user)
        _ -> {:error, user}
      end
    else
      {:ok, maybe_fetch_follow_information(user)}
    end
  end
788

789
  @spec maybe_update_following_count(User.t()) :: User.t()
790
791
  def maybe_update_following_count(%User{local: false} = user) do
    if Pleroma.Config.get([:instance, :external_user_synchronization]) do
792
      maybe_fetch_follow_information(user)
793
794
    else
      user
795
    end
796
  end
797

798
799
  def maybe_update_following_count(user), do: user

800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
  def set_unread_conversation_count(%User{local: true} = user) do
    unread_query = Participation.unread_conversation_count_for_user(user)

    User
    |> join(:inner, [u], p in subquery(unread_query))
    |> update([u, p],
      set: [
        info:
          fragment(
            "jsonb_set(?, '{unread_conversation_count}', ?::varchar::jsonb, true)",
            u.info,
            p.count
          )
      ]
    )
    |> where([u], u.id == ^user.id)
    |> select([u], u)
    |> Repo.update_all([])
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
  end

  def set_unread_conversation_count(_), do: :noop

  def increment_unread_conversation_count(conversation, %User{local: true} = user) do
    unread_query =
      Participation.unread_conversation_count_for_user(user)
      |> where([p], p.conversation_id == ^conversation.id)

    User
    |> join(:inner, [u], p in subquery(unread_query))
    |> update([u, p],
      set: [
        info:
          fragment(
            "jsonb_set(?, '{unread_conversation_count}', (coalesce((?->>'unread_conversation_count')::int, 0) + 1)::varchar::jsonb, true)",
            u.info,
            u.info
          )
      ]
    )
    |> where([u], u.id == ^user.id)
    |> where([u, p], p.count == 0)
    |> select([u], u)
    |> Repo.update_all([])
    |> case do
      {1, [user]} -> set_cache(user)
      _ -> {:error, user}
    end
  end

  def increment_unread_conversation_count(_, _), do: :noop

Alexander Strizhakov's avatar
Alexander Strizhakov committed
855
  @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
856
  def get_users_from_set(ap_ids, local_only \\ true) do
857
    criteria = %{ap_id: ap_ids, deactivated: false}
Alexander Strizhakov's avatar
Alexander Strizhakov committed
858
859
860
    criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria

    User.Query.build(criteria)
861
862
863
    |> Repo.all()
  end

Alexander Strizhakov's avatar
Alexander Strizhakov committed
864
  @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
865
  def get_recipients_from_activity(%Activity{recipients: to}) do
866
    User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
Alexander Strizhakov's avatar
Alexander Strizhakov committed
867
    |> Repo.all()
868
869
  end

870
871
  @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
  def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
872
    update_info(muter, &User.Info.add_to_mutes(&1, ap_id, notifications?))
873
874
  end

875
  def unmute(muter, %{ap_id: ap_id}) do
876
    update_info(muter, &User.Info.remove_from_mutes(&1, ap_id))
877
878
  end

879
  def subscribe(subscriber, %{ap_id: ap_id}) do
880
    with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
minibikini's avatar
minibikini committed
881
      deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
882

minibikini's avatar
minibikini committed
883
      if blocks?(subscribed, subscriber) and deny_follow_blocked do
884
885
        {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
      else
886
        update_info(subscribed, &User.Info.add_to_subscribers(&1, subscriber.ap_id))
887
      end
888
    end
889
890
891
  end

  def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
892
    with %User{} = user <- get_cached_by_ap_id(ap_id) do
893
      update_info(user, &User.Info.remove_from_subscribers(&1, unsubscriber.ap_id))
894
    end
895
896
  end

897
898
899
900
901
902
903
904
905
906
  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

907
908
909
910
911
912
913
    # clear any requested follows as well
    blocked =
      case CommonAPI.reject_follow_request(blocked, blocker) do
        {:ok, %User{} = updated_blocked} -> updated_blocked
        nil -> blocked
      end

914
915
916
917
918
919
920
921
    blocker =
      if subscribed_to?(blocked, blocker) do
        {:ok, blocker} = unsubscribe(blocked, blocker)
        blocker
      else
        blocker
      end

minibikini's avatar
minibikini committed
922
    if following?(blocked, blocker), do: unfollow(blocked, blocker)
923

924
925
    {:ok, blocker} = update_follower_count(blocker)

926
    update_info(blocker, &User.Info.add_to_block(&1, ap_id))
lain's avatar
lain committed
927
928
  end

929
930
  # 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