user.ex 25.7 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

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
  def auth_active?(%User{local: false}), do: false

49
50
51
52
53
54
  def auth_active?(%User{info: %User.Info{confirmation_pending: false}}), do: true

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

  def auth_active?(_), do: false
55

56
57
58
59
60
  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
61
    auth_active?(user) || superuser?(for_user)
62
63
  end

64
65
66
67
  def visible_for?(_, _), do: false

  def superuser?(%User{info: %User.Info{} = info}), do: User.Info.superuser?(info)
  def superuser?(_), do: false
68

lain's avatar
lain committed
69
70
71
  def avatar_url(user) do
    case user.avatar do
      %{"url" => [%{"href" => href} | _]} -> href
72
      _ -> "#{Web.base_url()}/images/avi.png"
lain's avatar
lain committed
73
74
75
    end
  end

lain's avatar
lain committed
76
  def banner_url(user) do
lain's avatar
lain committed
77
    case user.info.banner do
lain's avatar
lain committed
78
      %{"url" => [%{"href" => href} | _]} -> href
79
      _ -> "#{Web.base_url()}/images/banner.png"
lain's avatar
lain committed
80
81
82
    end
  end

lain's avatar
lain committed
83
  def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
84
85
86
  def profile_url(%User{ap_id: ap_id}), do: ap_id
  def profile_url(_), do: nil

lain's avatar
lain committed
87
  def ap_id(%User{nickname: nickname}) do
lain's avatar
lain committed
88
    "#{Web.base_url()}/users/#{nickname}"
lain's avatar
lain committed
89
90
91
92
93
  end

  def ap_followers(%User{} = user) do
    "#{ap_id(user)}/followers"
  end
lain's avatar
lain committed
94
95
96
97
98
99
100

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

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

lain's avatar
lain committed
104
    %{
105
      following_count: length(user.following) - oneself,
Ivan Tashkinov's avatar
Ivan Tashkinov committed
106
107
108
109
110
      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
111
112
113
    }
  end

lain's avatar
lain committed
114
  def remote_user_creation(params) do
lain's avatar
lain committed
115
116
117
    params =
      params
      |> Map.put(:info, params[:info] || %{})
lain's avatar
lain committed
118
119
120

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

lain's avatar
lain committed
121
    changes =
lain's avatar
lain committed
122
      %User{}
lain's avatar
lain committed
123
      |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
124
      |> validate_required([:name, :ap_id])
lain's avatar
lain committed
125
126
127
128
129
      |> 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
130
      |> put_embed(:info, info_cng)
lain's avatar
lain committed
131

132
    if changes.valid? do
lain's avatar
lain committed
133
      case info_cng.changes[:source_data] do
lain's avatar
lain committed
134
135
136
        %{"followers" => followers} ->
          changes
          |> put_change(:follower_address, followers)
lain's avatar
lain committed
137

lain's avatar
lain committed
138
139
        _ ->
          followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
lain's avatar
lain committed
140

lain's avatar
lain committed
141
142
143
          changes
          |> put_change(:follower_address, followers)
      end
144
145
146
    else
      changes
    end
lain's avatar
lain committed
147
148
  end

lain's avatar
lain committed
149
  def update_changeset(struct, params \\ %{}) do
Thog's avatar
Thog committed
150
    struct
lain's avatar
lain committed
151
    |> cast(params, [:bio, :name, :avatar])
lain's avatar
lain committed
152
    |> unique_constraint(:nickname)
href's avatar
href committed
153
    |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
154
    |> validate_length(:bio, max: 5000)
lain's avatar
lain committed
155
156
157
    |> validate_length(:name, min: 1, max: 100)
  end

lain's avatar
lain committed
158
  def upgrade_changeset(struct, params \\ %{}) do
159
160
161
162
    params =
      params
      |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())

lain's avatar
lain committed
163
164
165
166
    info_cng =
      struct.info
      |> User.Info.user_upgrade(params[:info])

lain's avatar
lain committed
167
    struct
lain's avatar
lain committed
168
    |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
lain's avatar
lain committed
169
    |> unique_constraint(:nickname)
href's avatar
href committed
170
    |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
171
172
    |> validate_length(:bio, max: 5000)
    |> validate_length(:name, max: 100)
lain's avatar
lain committed
173
    |> put_embed(:info, info_cng)
lain's avatar
lain committed
174
175
  end

Roger Braun's avatar
Roger Braun committed
176
  def password_update_changeset(struct, params) do
lain's avatar
lain committed
177
178
179
180
181
    changeset =
      struct
      |> cast(params, [:password, :password_confirmation])
      |> validate_required([:password, :password_confirmation])
      |> validate_confirmation(:password)
Roger Braun's avatar
Roger Braun committed
182

183
184
185
    OAuth.Token.delete_user_tokens(struct)
    OAuth.Authorization.delete_user_authorizations(struct)

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

Roger Braun's avatar
Roger Braun committed
189
190
191
192
193
194
195
196
      changeset
      |> put_change(:password_hash, hashed)
    else
      changeset
    end
  end

  def reset_password(user, data) do
lain's avatar
lain committed
197
    update_and_set_cache(password_update_changeset(user, data))
Roger Braun's avatar
Roger Braun committed
198
199
  end

200
201
202
203
204
205
206
207
  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
208
209
    info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)

lain's avatar
lain committed
210
211
212
213
214
215
216
    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
217
      |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
href's avatar
href committed
218
      |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
219
220
221
      |> validate_format(:email, @email_regex)
      |> validate_length(:bio, max: 1000)
      |> validate_length(:name, min: 1, max: 100)
Ivan Tashkinov's avatar
Ivan Tashkinov committed
222
      |> put_change(:info, info_change)
lain's avatar
lain committed
223
224

    if changeset.valid? do
225
      hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
lain's avatar
lain committed
226
227
      ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
      followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
lain's avatar
lain committed
228

lain's avatar
lain committed
229
230
231
232
      changeset
      |> put_change(:password_hash, hashed)
      |> put_change(:ap_id, ap_id)
      |> put_change(:following, [followers])
233
      |> put_change(:follower_address, followers)
lain's avatar
lain committed
234
235
236
237
238
    else
      changeset
    end
  end

239
240
  @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
241
242
    with {:ok, user} <- Repo.insert(changeset),
         {:ok, _} = try_send_confirmation_email(user) do
243
244
245
246
      {:ok, user}
    end
  end

247
  def try_send_confirmation_email(%User{} = user) do
248
249
    if user.info.confirmation_pending &&
         Pleroma.Config.get([:instance, :account_activation_required]) do
250
251
252
253
254
255
256
257
      user
      |> Pleroma.UserEmail.account_confirmation_email()
      |> Pleroma.Mailer.deliver()
    else
      {:ok, :noop}
    end
  end

258
259
260
261
262
263
264
265
266
267
  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
268
  def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
269
270
271
272
273
274
275
276
    {: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
277
    if not User.ap_enabled?(followed) do
278
      follow(follower, followed)
279
280
281
282
283
    else
      {:ok, follower}
    end
  end

Maksim's avatar
Maksim committed
284
  def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
285
286
    if not following?(follower, followed) do
      follow(follower, followed)
287
    else
288
      {:ok, follower}
289
290
291
    end
  end

lain's avatar
lain committed
292
  def follow(%User{} = follower, %User{info: info} = followed) do
293
294
    user_config = Application.get_env(:pleroma, :user)
    deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
295

296
    ap_followers = followed.follower_address
297

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

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

305
306
307
308
309
310
311
312
      true ->
        if !followed.local && follower.local && !ap_enabled?(followed) do
          Websub.subscribe(follower, followed)
        end

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

314
315
316
317
        follower =
          follower
          |> follow_changeset(%{following: following})
          |> update_and_set_cache
318

319
320
321
        {:ok, _} = update_follower_count(followed)

        follower
322
    end
lain's avatar
lain committed
323
  end
lain's avatar
lain committed
324
325

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

328
    if following?(follower, followed) and follower.ap_id != followed.ap_id do
lain's avatar
lain committed
329
330
331
      following =
        follower.following
        |> List.delete(ap_followers)
lain's avatar
lain committed
332

lain's avatar
lain committed
333
334
335
336
      {:ok, follower} =
        follower
        |> follow_changeset(%{following: following})
        |> update_and_set_cache
337
338
339
340

      {:ok, followed} = update_follower_count(followed)

      {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
341
    else
342
      {:error, "Not subscribed!"}
343
    end
lain's avatar
lain committed
344
  end
345

Maksim's avatar
Maksim committed
346
  @spec following?(User.t(), User.t()) :: boolean
347
  def following?(%User{} = follower, %User{} = followed) do
348
    Enum.member?(follower.following, followed.follower_address)
349
  end
lain's avatar
lain committed
350

351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
  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

369
  def locked?(%User{} = user) do
370
    user.info.locked || false
371
372
  end

lain's avatar
lain committed
373
374
375
376
  def get_by_ap_id(ap_id) do
    Repo.get_by(User, ap_id: ap_id)
  end

377
378
379
380
381
382
383
384
385
  # 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
  def get_by_guessed_nickname(ap_id) do
    domain = URI.parse(ap_id).host
    name = List.last(String.split(ap_id, "/"))
    nickname = "#{name}@#{domain}"

    get_by_nickname(nickname)
  end

lain's avatar
lain committed
386
387
  def update_and_set_cache(changeset) do
    with {:ok, user} <- Repo.update(changeset) do
Thog's avatar
Thog committed
388
389
390
      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
391
392
393
394
395
396
      {:ok, user}
    else
      e -> e
    end
  end

lain's avatar
lain committed
397
398
399
  def invalidate_cache(user) do
    Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
    Cachex.del(:user_cache, "nickname:#{user.nickname}")
400
    Cachex.del(:user_cache, "user_info:#{user.id}")
lain's avatar
lain committed
401
402
  end

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

  def get_cached_by_nickname(nickname) do
409
    key = "nickname:#{nickname}"
Thog's avatar
Thog committed
410
    Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
lain's avatar
lain committed
411
  end
lain's avatar
lain committed
412

lain's avatar
lain committed
413
  def get_by_nickname(nickname) do
414
    Repo.get_by(User, nickname: nickname) ||
415
      if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
416
417
418
        [local_nickname, _] = String.split(nickname, "@")
        Repo.get_by(User, nickname: local_nickname)
      end
419
420
  end

421
422
423
424
425
426
427
  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
428
429
  def get_cached_user_info(user) do
    key = "user_info:#{user.id}"
Thog's avatar
Thog committed
430
    Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
lain's avatar
lain committed
431
  end
lain's avatar
lain committed
432

lain's avatar
lain committed
433
434
435
436
437
438
439
440
441
  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
442
  def get_or_fetch_by_nickname(nickname) do
lain's avatar
lain committed
443
    with %User{} = user <- get_by_nickname(nickname) do
lain's avatar
lain committed
444
      user
lain's avatar
lain committed
445
446
447
448
449
450
451
452
    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
453
    end
lain's avatar
lain committed
454
  end
lain's avatar
lain committed
455

456
457
458
459
460
461
462
463
464
465
  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
466
467
468
469

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

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

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

484
485
486
  def get_follow_requests_query(%User{} = user) do
    from(
      a in Activity,
kaniini's avatar
kaniini committed
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
      where:
        fragment(
          "? ->> 'type' = 'Follow'",
          a.data
        ),
      where:
        fragment(
          "? ->> 'state' = 'pending'",
          a.data
        ),
      where:
        fragment(
          "? @> ?",
          a.data,
          ^%{"object" => user.ap_id}
        )
503
504
505
506
507
508
509
510
    )
  end

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

    users =
kaniini's avatar
kaniini committed
511
512
513
      Enum.map(reqs, fn req -> req.actor end)
      |> Enum.uniq()
      |> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end)
514
      |> Enum.filter(fn u -> !is_nil(u) end)
515
      |> Enum.filter(fn u -> !following?(u, user) end)
516
517
518
519

    {:ok, users}
  end

520
  def increase_note_count(%User{} = user) do
lain's avatar
lain committed
521
    info_cng = User.Info.add_to_note_count(user.info, 1)
lain's avatar
lain committed
522
523
524
525

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

lain's avatar
lain committed
527
    update_and_set_cache(cng)
528
529
  end

530
  def decrease_note_count(%User{} = user) do
lain's avatar
lain committed
531
    info_cng = User.Info.add_to_note_count(user.info, -1)
lain's avatar
lain committed
532
533
534
535

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

lain's avatar
lain committed
537
    update_and_set_cache(cng)
538
539
  end

540
  def update_note_count(%User{} = user) do
lain's avatar
lain committed
541
542
543
544
545
546
    note_count_query =
      from(
        a in Object,
        where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
        select: count(a.id)
      )
547
548
549

    note_count = Repo.one(note_count_query)

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

lain's avatar
lain committed
552
553
554
    cng =
      change(user)
      |> put_embed(:info, info_cng)
555

lain's avatar
lain committed
556
    update_and_set_cache(cng)
557
558
559
  end

  def update_follower_count(%User{} = user) do
lain's avatar
lain committed
560
561
562
563
564
565
566
    follower_count_query =
      from(
        u in User,
        where: ^user.follower_address in u.following,
        where: u.id != ^user.id,
        select: count(u.id)
      )
567
568
569

    follower_count = Repo.one(follower_count_query)

lain's avatar
lain committed
570
571
572
    info_cng =
      user.info
      |> User.Info.set_follower_count(follower_count)
573

lain's avatar
lain committed
574
575
576
    cng =
      change(user)
      |> put_embed(:info, info_cng)
577

lain's avatar
lain committed
578
    update_and_set_cache(cng)
579
  end
580

581
  def get_users_from_set_query(ap_ids, false) do
582
583
    from(
      u in User,
584
      where: u.ap_id in ^ap_ids
585
586
587
    )
  end

588
589
  def get_users_from_set_query(ap_ids, true) do
    query = get_users_from_set_query(ap_ids, false)
590
591
592

    from(
      u in query,
593
594
595
596
      where: u.local == true
    )
  end

597
598
599
600
601
  def get_users_from_set(ap_ids, local_only \\ true) do
    get_users_from_set_query(ap_ids, local_only)
    |> Repo.all()
  end

602
  def get_recipients_from_activity(%Activity{recipients: to}) do
lain's avatar
lain committed
603
604
605
606
607
608
    query =
      from(
        u in User,
        where: u.ap_id in ^to,
        or_where: fragment("? && ?", u.following, ^to)
      )
609

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

612
613
614
    Repo.all(query)
  end

615
  def search(query, resolve \\ false) do
616
617
618
    # strip the beginning @ off if there is a query
    query = String.trim_leading(query, "@")

lain's avatar
lain committed
619
620
621
    if resolve do
      User.get_or_fetch_by_nickname(query)
    end
lain's avatar
lain committed
622

lain's avatar
lain committed
623
    inner =
lain's avatar
lain committed
624
625
      from(
        u in User,
lain's avatar
lain committed
626
        select_merge: %{
kaniini's avatar
kaniini committed
627
628
          search_distance:
            fragment(
cascode's avatar
cascode committed
629
              "? <-> (? || coalesce(?, ''))",
kaniini's avatar
kaniini committed
630
631
632
633
              ^query,
              u.nickname,
              u.name
            )
634
635
        },
        where: not is_nil(u.nickname)
lain's avatar
lain committed
636
637
      )

kaniini's avatar
kaniini committed
638
639
640
641
642
643
    q =
      from(
        s in subquery(inner),
        order_by: s.search_distance,
        limit: 20
      )
lain's avatar
lain committed
644

lain's avatar
lain committed
645
646
    Repo.all(q)
  end
lain's avatar
lain committed
647

648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
  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

665
666
667
668
669
670
671
672
673
674
675
676
677
678
  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
679
680
681
    info_cng =
      blocker.info
      |> User.Info.add_to_block(ap_id)
lain's avatar
lain committed
682

lain's avatar
lain committed
683
684
685
686
687
    cng =
      change(blocker)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
lain's avatar
lain committed
688
689
  end

690
691
692
693
694
  # 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
695
696
697
698
  def unblock(blocker, %{ap_id: ap_id}) do
    info_cng =
      blocker.info
      |> User.Info.remove_from_block(ap_id)
lain's avatar
lain committed
699

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

    update_and_set_cache(cng)
lain's avatar
lain committed
705
706
707
  end

  def blocks?(user, %{ap_id: ap_id}) do
lain's avatar
lain committed
708
709
    blocks = user.info.blocks
    domain_blocks = user.info.domain_blocks
eal's avatar
eal committed
710
    %{host: host} = URI.parse(ap_id)
eal's avatar
eal committed
711
712
713
714
715

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

718
719
720
  def blocked_users(user),
    do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))

eal's avatar
eal committed
721
  def block_domain(user, domain) do
lain's avatar
lain committed
722
723
724
    info_cng =
      user.info
      |> User.Info.add_to_domain_block(domain)
eal's avatar
eal committed
725

lain's avatar
lain committed
726
727
728
    cng =
      change(user)
      |> put_embed(:info, info_cng)
lain's avatar
lain committed
729
730

    update_and_set_cache(cng)
eal's avatar
eal committed
731
732
733
  end

  def unblock_domain(user, domain) do
lain's avatar
lain committed
734
735
736
    info_cng =
      user.info
      |> User.Info.remove_from_domain_block(domain)
eal's avatar
eal committed
737

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

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

lain's avatar
lain committed
745
  def local_user_query() do
746
747
748
749
750
    from(
      u in User,
      where: u.local == true,
      where: not is_nil(u.nickname)
    )
lain's avatar
lain committed
751
752
  end

kaniini's avatar
kaniini committed
753
754
755
756
757
758
759
760
  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
761
  def deactivate(%User{} = user, status \\ true) do
lain's avatar
lain committed
762
    info_cng = User.Info.set_activation_status(user.info, status)
lain's avatar
lain committed
763
764
765
766

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

    update_and_set_cache(cng)
lain's avatar
lain committed
769
  end
lain's avatar
lain committed
770

lain's avatar
lain committed
771
  def delete(%User{} = user) do
lain's avatar
lain committed
772
773
774
    {:ok, user} = User.deactivate(user)

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

lain's avatar
lain committed
777
    followers
lain's avatar
lain committed
778
    |> Enum.each(fn follower -> User.unfollow(follower, user) end)
lain's avatar
lain committed
779
780

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

lain's avatar
lain committed
782
    friends
lain's avatar
lain committed
783
    |> Enum.each(fn followed -> User.unfollow(user, followed) end)
lain's avatar
lain committed
784

lain's avatar
lain committed
785
    query = from(a in Activity, where: a.actor == ^user.ap_id)
lain's avatar
lain committed
786
787

    Repo.all(query)
lain's avatar
lain committed
788
    |> Enum.each(fn activity ->
lain's avatar
lain committed
789
      case activity.data["type"] do
lain's avatar
lain committed
790
        "Create" ->
791
          ActivityPub.delete(Object.normalize(activity.data["object"]))
lain's avatar
lain committed
792
793
794
795

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

799
    {:ok, user}
lain's avatar
lain committed
800
  end
801

lain's avatar
lain committed
802
  def html_filter_policy(%User{info: %{no_rich_text: true}}) do
kaniini's avatar
kaniini committed
803
804
805
    Pleroma.HTML.Scrubber.TwitterText
  end

806
807
808
  @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])

  def html_filter_policy(_), do: @default_scrubbers
kaniini's avatar
kaniini committed
809

810
  def get_or_fetch_by_ap_id(ap_id) do
811
812
813
    user = get_by_ap_id(ap_id)

    if !is_nil(user) and !User.needs_update?(user) do
814
815
      user
    else
lain's avatar
lain committed
816
817
818
      ap_try = ActivityPub.make_user_from_ap_id(ap_id)

      case ap_try do
lain's avatar
lain committed
819
820
821
        {:ok, user} ->
          user

lain's avatar
lain committed
822
823
824
        _ ->
          case OStatus.make_user(ap_id) do
            {:ok, user} -> user
feld's avatar
feld committed
825
            _ -> {:error, "Could not fetch by AP id"}
lain's avatar
lain committed
826
          end
827
828
829
830
      end
    end
  end

831
  def get_or_create_instance_user do
832
833
834
    relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"

    if user = get_by_ap_id(relay_uri) do
835
836
837
      user
    else
      changes =
lain's avatar
lain committed
838
        %User{info: %User.Info{}}
839
        |> cast(%{}, [:ap_id, :nickname, :local])
840
        |> put_change(:ap_id, relay_uri)
841
842
        |> put_change(:nickname, nil)
        |> put_change(:local, true)
843
        |> put_change(:follower_address, relay_uri <> "/followers")
844
845
846
847
848
849

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

850
  # AP style
lain's avatar
lain committed
851
  def public_key_from_info(%{
lain's avatar
lain committed
852
        source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
lain's avatar
lain committed
853
854
      }) do
    key =
Maksim's avatar
Maksim committed
855
856
      public_key_pem
      |> :public_key.pem_decode()
lain's avatar
lain committed
857
858
      |> hd()
      |> :public_key.pem_entry_decode()
859

lain's avatar
lain committed
860
    {:ok, key}
861
862
863
  end

  # OStatus Magic Key
lain's avatar
lain committed
864
  def public_key_from_info(%{magic_key: magic_key}) do
865
866
867
    {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
  end

868
  def get_public_key_for_ap_id(ap_id) do
869
870
    with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
         {:ok, public_key} <- public_key_from_info(user.info) do
871
872
873
874
875
      {:ok, public_key}
    else
      _ -> :error
    end
  end
lain's avatar
lain committed
876

877
878
879
  defp blank?(""), do: nil
  defp blank?(n), do: n

lain's avatar
lain committed
880
  def insert_or_update_user(data) do
lain's avatar
lain committed
881
882
883
884
    data =
      data
      |> Map.put(:name, blank?(data[:name]) || data[:nickname])

lain's avatar
lain committed
885
    cs = User.remote_user_creation(data)
lain's avatar
lain committed
886

lain's avatar
lain committed
887
888
    Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
  end
889

890
  def ap_enabled?(%User{local: true}), do: true
lain's avatar
lain committed
891
  def ap_enabled?(%User{info: info}), do: info.ap_enabled
lain's avatar
lain committed
892
  def ap_enabled?(_), do: false
lain's avatar
lain committed
893

Maksim's avatar
Maksim committed
894
895
896
897
  @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)
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921

  # 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
922

923
  def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
lain's avatar
lain committed
924
925
  def parse_bio(nil, _user), do: ""
  def parse_bio(bio, _user) when bio == "", do: bio
926
927

  def parse_bio(bio, user) do
Maxim Filippov's avatar
Maxim Filippov committed
928
929
930
931
932
933
934
935
936
937
    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
938
939
940
    bio
    |> CommonUtils.format_input(mentions, tags, "text/plain")
    |> Formatter.emojify(emoji)
Maxim Filippov's avatar
Maxim Filippov committed
941
  end
942

943
944
945
946
947
  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
948

Maksim's avatar
Maksim committed
949
950
951
952
  def tag(nickname, tags) when is_binary(nickname),
    do: tag(User.get_by_nickname(nickname), tags)

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

955
956
957
958
959
  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
960

961
962
  def untag(nickname, tags) when is_binary(nickname),
    do: untag(User.get_by_nickname(nickname), tags)
963

964
965
  def untag(%User{} = user, tags),
    do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
966

967
968
969
970
971
  defp update_tags(%User{} = user, new_tags) do
    {:ok, updated_user} =
      user
      |> change(%{tags: new_tags})
      |> Repo.update()
972

973
    updated_user
974
  end
Ivan Tashkinov's avatar
Ivan Tashkinov committed
975

976
977
978
979
980
  defp normalize_tags(tags) do
    [tags]
    |> List.flatten()
    |> Enum.map(&String.downcase(&1))
  end
href's avatar
href committed
981
982
983
984
985
986
987
988

  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
989
end