user.ex 24.8 KB
Newer Older
1
2
3
4
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

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

8
  import Ecto.{Changeset, Query}
9
  alias Pleroma.{Repo, User, Object, Web, Activity, Notification}
10
  alias Comeonin.Pbkdf2
Maxim Filippov's avatar
Maxim Filippov committed
11
12
  alias Pleroma.Formatter
  alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
13
  alias Pleroma.Web.{OStatus, Websub, OAuth}
lain's avatar
lain committed
14
  alias Pleroma.Web.ActivityPub.{Utils, ActivityPub}
lain's avatar
lain committed
15

16
17
  require Logger

Maksim's avatar
Maksim committed
18
19
  @type t :: %__MODULE__{}

href's avatar
href committed
20
21
22
  @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
23
  @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
href's avatar
href committed
24

lain's avatar
lain committed
25
  schema "users" do
lain's avatar
lain committed
26
27
28
29
30
31
32
33
34
35
36
37
    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)
lain's avatar
lain committed
38
    field(:search_distance, :float, virtual: true)
39
    field(:tags, {:array, :string}, default: [])
40
    field(:last_refreshed_at, :naive_datetime)
lain's avatar
lain committed
41
    has_many(:notifications, Notification)
lain's avatar
lain committed
42
    embeds_one(:info, Pleroma.User.Info)
lain's avatar
lain committed
43
44
45

    timestamps()
  end
lain's avatar
lain committed
46

47
48
49
50
  def auth_active?(%User{} = user) do
    (user.info && !user.info.confirmation_pending) ||
      !Pleroma.Config.get([:instance, :account_activation_required])
  end
51
52

  def superuser?(%User{} = user), do: user.info && User.Info.superuser?(user.info)
53

lain's avatar
lain committed
54
55
56
  def avatar_url(user) do
    case user.avatar do
      %{"url" => [%{"href" => href} | _]} -> href
57
      _ -> "#{Web.base_url()}/images/avi.png"
lain's avatar
lain committed
58
59
60
    end
  end

lain's avatar
lain committed
61
  def banner_url(user) do
lain's avatar
lain committed
62
    case user.info.banner do
lain's avatar
lain committed
63
      %{"url" => [%{"href" => href} | _]} -> href
64
      _ -> "#{Web.base_url()}/images/banner.png"
lain's avatar
lain committed
65
66
67
    end
  end

lain's avatar
lain committed
68
  def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
69
70
71
  def profile_url(%User{ap_id: ap_id}), do: ap_id
  def profile_url(_), do: nil

lain's avatar
lain committed
72
  def ap_id(%User{nickname: nickname}) do
lain's avatar
lain committed
73
    "#{Web.base_url()}/users/#{nickname}"
lain's avatar
lain committed
74
75
76
77
78
  end

  def ap_followers(%User{} = user) do
    "#{ap_id(user)}/followers"
  end
lain's avatar
lain committed
79
80
81
82
83
84
85

  def follow_changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:following])
    |> validate_required([:following])
  end

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

lain's avatar
lain committed
89
    %{
90
      following_count: length(user.following) - oneself,
Ivan Tashkinov's avatar
Ivan Tashkinov committed
91
92
93
94
95
      note_count: user.info.note_count,
      follower_count: user.info.follower_count,
      locked: user.info.locked,
      confirmation_pending: user.info.confirmation_pending,
      default_scope: user.info.default_scope
lain's avatar
lain committed
96
97
98
    }
  end

lain's avatar
lain committed
99
  def remote_user_creation(params) do
lain's avatar
lain committed
100
101
102
    params =
      params
      |> Map.put(:info, params[:info] || %{})
lain's avatar
lain committed
103
104
105

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

lain's avatar
lain committed
106
    changes =
lain's avatar
lain committed
107
      %User{}
lain's avatar
lain committed
108
      |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
109
      |> validate_required([:name, :ap_id])
lain's avatar
lain committed
110
111
112
113
114
      |> 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
115
      |> put_embed(:info, info_cng)
lain's avatar
lain committed
116

117
    if changes.valid? do
lain's avatar
lain committed
118
      case info_cng.changes[:source_data] do
lain's avatar
lain committed
119
120
121
        %{"followers" => followers} ->
          changes
          |> put_change(:follower_address, followers)
lain's avatar
lain committed
122

lain's avatar
lain committed
123
124
        _ ->
          followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
lain's avatar
lain committed
125

lain's avatar
lain committed
126
127
128
          changes
          |> put_change(:follower_address, followers)
      end
129
130
131
    else
      changes
    end
lain's avatar
lain committed
132
133
  end

lain's avatar
lain committed
134
  def update_changeset(struct, params \\ %{}) do
Thog's avatar
Thog committed
135
    struct
lain's avatar
lain committed
136
    |> cast(params, [:bio, :name, :avatar])
lain's avatar
lain committed
137
    |> unique_constraint(:nickname)
href's avatar
href committed
138
    |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
139
    |> validate_length(:bio, max: 5000)
lain's avatar
lain committed
140
141
142
    |> validate_length(:name, min: 1, max: 100)
  end

lain's avatar
lain committed
143
  def upgrade_changeset(struct, params \\ %{}) do
144
145
146
147
    params =
      params
      |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())

lain's avatar
lain committed
148
149
150
151
    info_cng =
      struct.info
      |> User.Info.user_upgrade(params[:info])

lain's avatar
lain committed
152
    struct
lain's avatar
lain committed
153
    |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
lain's avatar
lain committed
154
    |> unique_constraint(:nickname)
href's avatar
href committed
155
    |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
156
157
    |> validate_length(:bio, max: 5000)
    |> validate_length(:name, max: 100)
lain's avatar
lain committed
158
    |> put_embed(:info, info_cng)
lain's avatar
lain committed
159
160
  end

Roger Braun's avatar
Roger Braun committed
161
  def password_update_changeset(struct, params) do
lain's avatar
lain committed
162
163
164
165
166
    changeset =
      struct
      |> cast(params, [:password, :password_confirmation])
      |> validate_required([:password, :password_confirmation])
      |> validate_confirmation(:password)
Roger Braun's avatar
Roger Braun committed
167

168
169
170
    OAuth.Token.delete_user_tokens(struct)
    OAuth.Authorization.delete_user_authorizations(struct)

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

Roger Braun's avatar
Roger Braun committed
174
175
176
177
178
179
180
181
      changeset
      |> put_change(:password_hash, hashed)
    else
      changeset
    end
  end

  def reset_password(user, data) do
lain's avatar
lain committed
182
    update_and_set_cache(password_update_changeset(user, data))
Roger Braun's avatar
Roger Braun committed
183
184
  end

185
186
187
188
189
190
191
192
  def register_changeset(struct, params \\ %{}, opts \\ []) do
    confirmation_status =
      if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do
        :confirmed
      else
        :unconfirmed
      end

Ivan Tashkinov's avatar
Ivan Tashkinov committed
193
194
    info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)

lain's avatar
lain committed
195
196
197
198
199
200
201
    changeset =
      struct
      |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
      |> validate_required([:email, :name, :nickname, :password, :password_confirmation])
      |> validate_confirmation(:password)
      |> unique_constraint(:email)
      |> unique_constraint(:nickname)
lain's avatar
lain committed
202
      |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
href's avatar
href committed
203
      |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
204
205
206
      |> validate_format(:email, @email_regex)
      |> validate_length(:bio, max: 1000)
      |> validate_length(:name, min: 1, max: 100)
Ivan Tashkinov's avatar
Ivan Tashkinov committed
207
      |> put_change(:info, info_change)
lain's avatar
lain committed
208
209

    if changeset.valid? do
210
      hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
lain's avatar
lain committed
211
212
      ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
      followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
lain's avatar
lain committed
213

lain's avatar
lain committed
214
215
216
217
      changeset
      |> put_change(:password_hash, hashed)
      |> put_change(:ap_id, ap_id)
      |> put_change(:following, [followers])
218
      |> put_change(:follower_address, followers)
lain's avatar
lain committed
219
220
221
222
223
    else
      changeset
    end
  end

224
225
  @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
226
227
    with {:ok, user} <- Repo.insert(changeset),
         {:ok, _} = try_send_confirmation_email(user) do
228
229
230
231
      {:ok, user}
    end
  end

232
  def try_send_confirmation_email(%User{} = user) do
233
234
    if user.info.confirmation_pending &&
         Pleroma.Config.get([:instance, :account_activation_required]) do
235
236
237
238
239
240
241
242
      user
      |> Pleroma.UserEmail.account_confirmation_email()
      |> Pleroma.Mailer.deliver()
    else
      {:ok, :noop}
    end
  end

243
244
245
246
247
248
249
250
251
252
  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
    NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86400
  end

  def needs_update?(_), do: true

lain's avatar
lain committed
253
  def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
254
255
256
257
258
259
260
261
    {: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
262
    if not User.ap_enabled?(followed) do
263
      follow(follower, followed)
264
265
266
267
268
    else
      {:ok, follower}
    end
  end

Maksim's avatar
Maksim committed
269
  def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
270
271
    if not following?(follower, followed) do
      follow(follower, followed)
272
    else
273
      {:ok, follower}
274
275
276
    end
  end

lain's avatar
lain committed
277
  def follow(%User{} = follower, %User{info: info} = followed) do
278
279
    user_config = Application.get_env(:pleroma, :user)
    deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
280

281
    ap_followers = followed.follower_address
282

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

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

290
291
292
293
294
295
296
297
      true ->
        if !followed.local && follower.local && !ap_enabled?(followed) do
          Websub.subscribe(follower, followed)
        end

        following =
          [ap_followers | follower.following]
          |> Enum.uniq()
298

299
300
301
302
        follower =
          follower
          |> follow_changeset(%{following: following})
          |> update_and_set_cache
303

304
305
306
        {:ok, _} = update_follower_count(followed)

        follower
307
    end
lain's avatar
lain committed
308
  end
lain's avatar
lain committed
309
310

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

313
    if following?(follower, followed) and follower.ap_id != followed.ap_id do
lain's avatar
lain committed
314
315
316
      following =
        follower.following
        |> List.delete(ap_followers)
lain's avatar
lain committed
317

lain's avatar
lain committed
318
319
320
321
      {:ok, follower} =
        follower
        |> follow_changeset(%{following: following})
        |> update_and_set_cache
322
323
324
325

      {:ok, followed} = update_follower_count(followed)

      {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
326
    else
327
      {:error, "Not subscribed!"}
328
    end
lain's avatar
lain committed
329
  end
330

Maksim's avatar
Maksim committed
331
  @spec following?(User.t(), User.t()) :: boolean
332
  def following?(%User{} = follower, %User{} = followed) do
333
    Enum.member?(follower.following, followed.follower_address)
334
  end
lain's avatar
lain committed
335

336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
  def follow_import(%User{} = follower, followed_identifiers)
      when is_list(followed_identifiers) do
    Enum.map(
      followed_identifiers,
      fn followed_identifier ->
        with %User{} = followed <- get_or_fetch(followed_identifier),
             {:ok, follower} <- maybe_direct_follow(follower, followed),
             {:ok, _} <- ActivityPub.follow(follower, followed) do
          followed
        else
          err ->
            Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
            err
        end
      end
    )
  end

354
  def locked?(%User{} = user) do
355
    user.info.locked || false
356
357
  end

lain's avatar
lain committed
358
359
360
361
  def get_by_ap_id(ap_id) do
    Repo.get_by(User, ap_id: ap_id)
  end

lain's avatar
lain committed
362
363
  def update_and_set_cache(changeset) do
    with {:ok, user} <- Repo.update(changeset) do
Thog's avatar
Thog committed
364
365
366
      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))
lain's avatar
lain committed
367
368
369
370
371
372
      {:ok, user}
    else
      e -> e
    end
  end

lain's avatar
lain committed
373
374
375
  def invalidate_cache(user) do
    Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
    Cachex.del(:user_cache, "nickname:#{user.nickname}")
376
    Cachex.del(:user_cache, "user_info:#{user.id}")
lain's avatar
lain committed
377
378
  end

lain's avatar
lain committed
379
  def get_cached_by_ap_id(ap_id) do
380
    key = "ap_id:#{ap_id}"
Thog's avatar
Thog committed
381
    Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
lain's avatar
lain committed
382
383
384
  end

  def get_cached_by_nickname(nickname) do
385
    key = "nickname:#{nickname}"
Thog's avatar
Thog committed
386
    Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
lain's avatar
lain committed
387
  end
lain's avatar
lain committed
388

lain's avatar
lain committed
389
  def get_by_nickname(nickname) do
390
391
392
393
394
    Repo.get_by(User, nickname: nickname) ||
      if String.ends_with?(nickname, "@" <> Pleroma.Web.Endpoint.host()) do
        [local_nickname, _] = String.split(nickname, "@")
        Repo.get_by(User, nickname: local_nickname)
      end
395
396
  end

397
398
399
400
401
402
403
  def get_by_nickname_or_email(nickname_or_email) do
    case user = Repo.get_by(User, nickname: nickname_or_email) do
      %User{} -> user
      nil -> Repo.get_by(User, email: nickname_or_email)
    end
  end

lain's avatar
lain committed
404
405
  def get_cached_user_info(user) do
    key = "user_info:#{user.id}"
Thog's avatar
Thog committed
406
    Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
lain's avatar
lain committed
407
  end
lain's avatar
lain committed
408

lain's avatar
lain committed
409
410
411
412
413
414
415
416
417
  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
418
  def get_or_fetch_by_nickname(nickname) do
lain's avatar
lain committed
419
    with %User{} = user <- get_by_nickname(nickname) do
lain's avatar
lain committed
420
      user
lain's avatar
lain committed
421
422
423
424
425
426
427
428
    else
      _e ->
        with [_nick, _domain] <- String.split(nickname, "@"),
             {:ok, user} <- fetch_by_nickname(nickname) do
          user
        else
          _e -> nil
        end
lain's avatar
lain committed
429
    end
lain's avatar
lain committed
430
  end
lain's avatar
lain committed
431

432
433
434
435
436
437
438
439
440
441
  def get_followers_query(%User{id: id, follower_address: follower_address}) do
    from(
      u in User,
      where: fragment("? <@ ?", ^[follower_address], u.following),
      where: u.id != ^id
    )
  end

  def get_followers(user) do
    q = get_followers_query(user)
lain's avatar
lain committed
442
443
444
445

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

446
447
448
449
450
451
452
453
454
455
  def get_friends_query(%User{id: id, following: following}) do
    from(
      u in User,
      where: u.follower_address in ^following,
      where: u.id != ^id
    )
  end

  def get_friends(user) do
    q = get_friends_query(user)
lain's avatar
lain committed
456
457
458

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

460
461
462
  def get_follow_requests_query(%User{} = user) do
    from(
      a in Activity,
kaniini's avatar
kaniini committed
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
      where:
        fragment(
          "? ->> 'type' = 'Follow'",
          a.data
        ),
      where:
        fragment(
          "? ->> 'state' = 'pending'",
          a.data
        ),
      where:
        fragment(
          "? @> ?",
          a.data,
          ^%{"object" => user.ap_id}
        )
479
480
481
482
483
484
485
486
    )
  end

  def get_follow_requests(%User{} = user) do
    q = get_follow_requests_query(user)
    reqs = Repo.all(q)

    users =
kaniini's avatar
kaniini committed
487
488
489
      Enum.map(reqs, fn req -> req.actor end)
      |> Enum.uniq()
      |> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end)
490
      |> Enum.filter(fn u -> !following?(u, user) end)
491
492
493
494

    {:ok, users}
  end

495
  def increase_note_count(%User{} = user) do
lain's avatar
lain committed
496
    info_cng = User.Info.add_to_note_count(user.info, 1)
lain's avatar
lain committed
497
498
499
500

    cng =
      change(user)
      |> put_embed(:info, info_cng)
501

lain's avatar
lain committed
502
    update_and_set_cache(cng)
503
504
  end

505
  def decrease_note_count(%User{} = user) do
lain's avatar
lain committed
506
    info_cng = User.Info.add_to_note_count(user.info, -1)
lain's avatar
lain committed
507
508
509
510

    cng =
      change(user)
      |> put_embed(:info, info_cng)
511

lain's avatar
lain committed
512
    update_and_set_cache(cng)
513
514
  end

515
  def update_note_count(%User{} = user) do
lain's avatar
lain committed
516
517
518
519
520
521
    note_count_query =
      from(
        a in Object,
        where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
        select: count(a.id)
      )
522
523
524

    note_count = Repo.one(note_count_query)

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

lain's avatar
lain committed
527
528
529
    cng =
      change(user)
      |> put_embed(:info, info_cng)
530

lain's avatar
lain committed
531
    update_and_set_cache(cng)
532
533
534
  end

  def update_follower_count(%User{} = user) do
lain's avatar
lain committed
535
536
537
538
539
540
541
    follower_count_query =
      from(
        u in User,
        where: ^user.follower_address in u.following,
        where: u.id != ^user.id,
        select: count(u.id)
      )
542
543
544

    follower_count = Repo.one(follower_count_query)

lain's avatar
lain committed
545
546
547
    info_cng =
      user.info
      |> User.Info.set_follower_count(follower_count)
548

lain's avatar
lain committed
549
550
551
    cng =
      change(user)
      |> put_embed(:info, info_cng)
552

lain's avatar
lain committed
553
    update_and_set_cache(cng)
554
  end
555

556
  def get_users_from_set_query(ap_ids, false) do
557
558
    from(
      u in User,
559
      where: u.ap_id in ^ap_ids
560
561
562
    )
  end

563
564
  def get_users_from_set_query(ap_ids, true) do
    query = get_users_from_set_query(ap_ids, false)
565
566
567

    from(
      u in query,
568
569
570
571
      where: u.local == true
    )
  end

572
573
574
575
576
  def get_users_from_set(ap_ids, local_only \\ true) do
    get_users_from_set_query(ap_ids, local_only)
    |> Repo.all()
  end

577
  def get_recipients_from_activity(%Activity{recipients: to}) do
lain's avatar
lain committed
578
579
580
581
582
583
    query =
      from(
        u in User,
        where: u.ap_id in ^to,
        or_where: fragment("? && ?", u.following, ^to)
      )
584

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

587
588
589
    Repo.all(query)
  end

590
  def search(query, resolve \\ false) do
591
592
593
    # strip the beginning @ off if there is a query
    query = String.trim_leading(query, "@")

lain's avatar
lain committed
594
595
596
    if resolve do
      User.get_or_fetch_by_nickname(query)
    end
lain's avatar
lain committed
597

lain's avatar
lain committed
598
    inner =
lain's avatar
lain committed
599
600
      from(
        u in User,
lain's avatar
lain committed
601
        select_merge: %{
kaniini's avatar
kaniini committed
602
603
604
605
606
607
608
          search_distance:
            fragment(
              "? <-> (? || ?)",
              ^query,
              u.nickname,
              u.name
            )
609
610
        },
        where: not is_nil(u.nickname)
lain's avatar
lain committed
611
612
      )

kaniini's avatar
kaniini committed
613
614
615
616
617
618
    q =
      from(
        s in subquery(inner),
        order_by: s.search_distance,
        limit: 20
      )
lain's avatar
lain committed
619

lain's avatar
lain committed
620
621
    Repo.all(q)
  end
lain's avatar
lain committed
622

623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
  def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
    Enum.map(
      blocked_identifiers,
      fn blocked_identifier ->
        with %User{} = blocked <- get_or_fetch(blocked_identifier),
             {:ok, blocker} <- block(blocker, blocked),
             {:ok, _} <- ActivityPub.block(blocker, blocked) do
          blocked
        else
          err ->
            Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
            err
        end
      end
    )
  end

640
641
642
643
644
645
646
647
648
649
650
651
652
653
  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

    if following?(blocked, blocker) do
      unfollow(blocked, blocker)
    end

lain's avatar
lain committed
654
655
656
    info_cng =
      blocker.info
      |> User.Info.add_to_block(ap_id)
lain's avatar
lain committed
657

lain's avatar
lain committed
658
659
660
661
662
    cng =
      change(blocker)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
lain's avatar
lain committed
663
664
  end

665
666
667
668
669
  # helper to handle the block given only an actor's AP id
  def block(blocker, %{ap_id: ap_id}) do
    block(blocker, User.get_by_ap_id(ap_id))
  end

lain's avatar
lain committed
670
671
672
673
  def unblock(blocker, %{ap_id: ap_id}) do
    info_cng =
      blocker.info
      |> User.Info.remove_from_block(ap_id)
lain's avatar
lain committed
674

lain's avatar
lain committed
675
676
677
678
679
    cng =
      change(blocker)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
lain's avatar
lain committed
680
681
682
  end

  def blocks?(user, %{ap_id: ap_id}) do
lain's avatar
lain committed
683
684
    blocks = user.info.blocks
    domain_blocks = user.info.domain_blocks
eal's avatar
eal committed
685
    %{host: host} = URI.parse(ap_id)
eal's avatar
eal committed
686
687
688
689
690

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

693
694
695
  def blocked_users(user),
    do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))

eal's avatar
eal committed
696
  def block_domain(user, domain) do
lain's avatar
lain committed
697
698
699
    info_cng =
      user.info
      |> User.Info.add_to_domain_block(domain)
eal's avatar
eal committed
700

lain's avatar
lain committed
701
702
703
    cng =
      change(user)
      |> put_embed(:info, info_cng)
lain's avatar
lain committed
704
705

    update_and_set_cache(cng)
eal's avatar
eal committed
706
707
708
  end

  def unblock_domain(user, domain) do
lain's avatar
lain committed
709
710
711
    info_cng =
      user.info
      |> User.Info.remove_from_domain_block(domain)
eal's avatar
eal committed
712

lain's avatar
lain committed
713
714
715
    cng =
      change(user)
      |> put_embed(:info, info_cng)
lain's avatar
lain committed
716
717

    update_and_set_cache(cng)
lain's avatar
lain committed
718
719
  end

lain's avatar
lain committed
720
  def local_user_query() do
721
722
723
724
725
    from(
      u in User,
      where: u.local == true,
      where: not is_nil(u.nickname)
    )
lain's avatar
lain committed
726
727
  end

kaniini's avatar
kaniini committed
728
729
730
731
732
733
734
735
  def moderator_user_query() do
    from(
      u in User,
      where: u.local == true,
      where: fragment("?->'is_moderator' @> 'true'", u.info)
    )
  end

scarlett's avatar
scarlett committed
736
  def deactivate(%User{} = user, status \\ true) do
lain's avatar
lain committed
737
    info_cng = User.Info.set_activation_status(user.info, status)
lain's avatar
lain committed
738
739
740
741

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

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

lain's avatar
lain committed
746
  def delete(%User{} = user) do
lain's avatar
lain committed
747
748
749
    {:ok, user} = User.deactivate(user)

    # Remove all relationships
lain's avatar
lain committed
750
751
    {:ok, followers} = User.get_followers(user)

lain's avatar
lain committed
752
    followers
lain's avatar
lain committed
753
    |> Enum.each(fn follower -> User.unfollow(follower, user) end)
lain's avatar
lain committed
754
755

    {:ok, friends} = User.get_friends(user)
lain's avatar
lain committed
756

lain's avatar
lain committed
757
    friends
lain's avatar
lain committed
758
    |> Enum.each(fn followed -> User.unfollow(user, followed) end)
lain's avatar
lain committed
759

lain's avatar
lain committed
760
    query = from(a in Activity, where: a.actor == ^user.ap_id)
lain's avatar
lain committed
761
762

    Repo.all(query)
lain's avatar
lain committed
763
    |> Enum.each(fn activity ->
lain's avatar
lain committed
764
      case activity.data["type"] do
lain's avatar
lain committed
765
        "Create" ->
766
          ActivityPub.delete(Object.normalize(activity.data["object"]))
lain's avatar
lain committed
767
768
769
770

        # TODO: Do something with likes, follows, repeats.
        _ ->
          "Doing nothing"
lain's avatar
lain committed
771
772
773
      end
    end)

774
    {:ok, user}
lain's avatar
lain committed
775
  end
776

lain's avatar
lain committed
777
  def html_filter_policy(%User{info: %{no_rich_text: true}}) do
kaniini's avatar
kaniini committed
778
779
780
781
782
    Pleroma.HTML.Scrubber.TwitterText
  end

  def html_filter_policy(_), do: nil

783
  def get_or_fetch_by_ap_id(ap_id) do
784
785
786
    user = get_by_ap_id(ap_id)

    if !is_nil(user) and !User.needs_update?(user) do
787
788
      user
    else
lain's avatar
lain committed
789
790
791
      ap_try = ActivityPub.make_user_from_ap_id(ap_id)

      case ap_try do
lain's avatar
lain committed
792
793
794
        {:ok, user} ->
          user

lain's avatar
lain committed
795
796
797
        _ ->
          case OStatus.make_user(ap_id) do
            {:ok, user} -> user
feld's avatar
feld committed
798
            _ -> {:error, "Could not fetch by AP id"}
lain's avatar
lain committed
799
          end
800
801
802
803
      end
    end
  end

804
  def get_or_create_instance_user do
805
806
807
    relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"

    if user = get_by_ap_id(relay_uri) do
808
809
810
      user
    else
      changes =
lain's avatar
lain committed
811
        %User{info: %User.Info{}}
812
        |> cast(%{}, [:ap_id, :nickname, :local])
813
        |> put_change(:ap_id, relay_uri)
814
815
        |> put_change(:nickname, nil)
        |> put_change(:local, true)
816
        |> put_change(:follower_address, relay_uri <> "/followers")
817
818
819
820
821
822

      {:ok, user} = Repo.insert(changes)
      user
    end
  end

823
  # AP style
lain's avatar
lain committed
824
  def public_key_from_info(%{
lain's avatar
lain committed
825
        source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
lain's avatar
lain committed
826
827
      }) do
    key =
Maksim's avatar
Maksim committed
828
829
      public_key_pem
      |> :public_key.pem_decode()
lain's avatar
lain committed
830
831
      |> hd()
      |> :public_key.pem_entry_decode()
832

lain's avatar
lain committed
833
    {:ok, key}
834
835
836
  end

  # OStatus Magic Key
lain's avatar
lain committed
837
  def public_key_from_info(%{magic_key: magic_key}) do
838
839
840
    {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
  end

841
  def get_public_key_for_ap_id(ap_id) do
842
843
    with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
         {:ok, public_key} <- public_key_from_info(user.info) do
844
845
846
847
848
      {:ok, public_key}
    else
      _ -> :error
    end
  end
lain's avatar
lain committed
849

850
851
852
  defp blank?(""), do: nil
  defp blank?(n), do: n

lain's avatar
lain committed
853
  def insert_or_update_user(data) do
lain's avatar
lain committed
854
855
856
857
    data =
      data
      |> Map.put(:name, blank?(data[:name]) || data[:nickname])

lain's avatar
lain committed
858
    cs = User.remote_user_creation(data)
lain's avatar
lain committed
859

lain's avatar
lain committed
860
861
    Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
  end
862

863
  def ap_enabled?(%User{local: true}), do: true
lain's avatar
lain committed
864
  def ap_enabled?(%User{info: info}), do: info.ap_enabled
lain's avatar
lain committed
865
  def ap_enabled?(_), do: false
lain's avatar
lain committed
866

Maksim's avatar
Maksim committed
867
868
869
870
  @doc "Gets or fetch a user by uri or nickname."
  @spec get_or_fetch(String.t()) :: User.t()
  def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
  def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894

  # wait a period of time and return newest version of the User structs
  # this is because we have synchronous follow APIs and need to simulate them
  # with an async handshake
  def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
    with %User{} = a <- Repo.get(User, a.id),
         %User{} = b <- Repo.get(User, b.id) do
      {:ok, a, b}
    else
      _e ->
        :error
    end
  end

  def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
    with :ok <- :timer.sleep(timeout),
         %User{} = a <- Repo.get(User, a.id),
         %User{} = b <- Repo.get(User, b.id) do
      {:ok, a, b}
    else
      _e ->
        :error
    end
  end
Maxim Filippov's avatar
Maxim Filippov committed
895

896
  def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
lain's avatar
lain committed
897
898
  def parse_bio(nil, _user), do: ""
  def parse_bio(bio, _user) when bio == "", do: bio
899
900

  def parse_bio(bio, user) do
Maxim Filippov's avatar
Maxim Filippov committed
901
902
903
904
905
906
907
908
909
910
    mentions = Formatter.parse_mentions(bio)
    tags = Formatter.parse_tags(bio)

    emoji =
      (user.info.source_data["tag"] || [])
      |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
      |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
        {String.trim(name, ":"), url}
      end)

Maksim's avatar
Maksim committed
911
912
913
    bio
    |> CommonUtils.format_input(mentions, tags, "text/plain")
    |> Formatter.emojify(emoji)
Maxim Filippov's avatar
Maxim Filippov committed
914
  end
915

916
917
918
919
920
  def tag(user_identifiers, tags) when is_list(user_identifiers) do
    Repo.transaction(fn ->
      for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
    end)
  end
921

Maksim's avatar
Maksim committed
922
923
924
925
  def tag(nickname, tags) when is_binary(nickname),
    do: tag(User.get_by_nickname(nickname), tags)

  def tag(%User{} = user, tags),
926
    do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
Maksim's avatar
Maksim committed
927

928
929
930
931
932
  def untag(user_identifiers, tags) when is_list(user_identifiers) do
    Repo.transaction(fn ->
      for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
    end)
  end
933

934
935
  def untag(nickname, tags) when is_binary(nickname),
    do: untag(User.get_by_nickname(nickname), tags)
936

937
938
  def untag(%User{} = user, tags),
    do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
939

940
941
942
943
944
  defp update_tags(%User{} = user, new_tags) do
    {:ok, updated_user} =
      user
      |> change(%{tags: new_tags})
      |> Repo.update()
945

946
    updated_user
947
  end
Ivan Tashkinov's avatar
Ivan Tashkinov committed
948

949
950
951
952
953
  defp normalize_tags(tags) do
    [tags]
    |> List.flatten()
    |> Enum.map(&String.downcase(&1))
  end
href's avatar
href committed
954
955
956
957
958
959
960
961

  defp local_nickname_regex() do
    if Pleroma.Config.get([:instance, :extended_nickname_format]) do
      @extended_local_nickname_regex
    else
      @strict_local_nickname_regex
    end
  end
lain's avatar
lain committed
962
end