user.ex 24.6 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
    Repo.get_by(User, nickname: nickname)
  end

393 394 395 396 397 398 399
  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
400 401
  def get_cached_user_info(user) do
    key = "user_info:#{user.id}"
Thog's avatar
Thog committed
402
    Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
lain's avatar
lain committed
403
  end
lain's avatar
lain committed
404

lain's avatar
lain committed
405 406 407 408 409 410 411 412 413
  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
414
  def get_or_fetch_by_nickname(nickname) do
lain's avatar
lain committed
415
    with %User{} = user <- get_by_nickname(nickname) do
lain's avatar
lain committed
416
      user
lain's avatar
lain committed
417 418 419 420 421 422 423 424
    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
425
    end
lain's avatar
lain committed
426
  end
lain's avatar
lain committed
427

428 429 430 431 432 433 434 435 436 437
  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
438 439 440 441

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

442 443 444 445 446 447 448 449 450 451
  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
452 453 454

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

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

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

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

    {:ok, users}
  end

491
  def increase_note_count(%User{} = user) do
lain's avatar
lain committed
492
    info_cng = User.Info.add_to_note_count(user.info, 1)
lain's avatar
lain committed
493 494 495 496

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

lain's avatar
lain committed
498
    update_and_set_cache(cng)
499 500
  end

501
  def decrease_note_count(%User{} = user) do
lain's avatar
lain committed
502
    info_cng = User.Info.add_to_note_count(user.info, -1)
lain's avatar
lain committed
503 504 505 506

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

lain's avatar
lain committed
508
    update_and_set_cache(cng)
509 510
  end

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

    note_count = Repo.one(note_count_query)

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

lain's avatar
lain committed
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 530
  end

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

    follower_count = Repo.one(follower_count_query)

lain's avatar
lain committed
541 542 543
    info_cng =
      user.info
      |> User.Info.set_follower_count(follower_count)
544

lain's avatar
lain committed
545 546 547
    cng =
      change(user)
      |> put_embed(:info, info_cng)
548

lain's avatar
lain committed
549
    update_and_set_cache(cng)
550
  end
551

552
  def get_users_from_set_query(ap_ids, false) do
553 554
    from(
      u in User,
555
      where: u.ap_id in ^ap_ids
556 557 558
    )
  end

559 560
  def get_users_from_set_query(ap_ids, true) do
    query = get_users_from_set_query(ap_ids, false)
561 562 563

    from(
      u in query,
564 565 566 567
      where: u.local == true
    )
  end

568 569 570 571 572
  def get_users_from_set(ap_ids, local_only \\ true) do
    get_users_from_set_query(ap_ids, local_only)
    |> Repo.all()
  end

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

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

583 584 585
    Repo.all(query)
  end

586
  def search(query, resolve \\ false) do
587 588 589
    # strip the beginning @ off if there is a query
    query = String.trim_leading(query, "@")

lain's avatar
lain committed
590 591 592
    if resolve do
      User.get_or_fetch_by_nickname(query)
    end
lain's avatar
lain committed
593

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

kaniini's avatar
kaniini committed
609 610 611 612 613 614
    q =
      from(
        s in subquery(inner),
        order_by: s.search_distance,
        limit: 20
      )
lain's avatar
lain committed
615

lain's avatar
lain committed
616 617
    Repo.all(q)
  end
lain's avatar
lain committed
618

619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635
  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

636 637 638 639 640 641 642 643 644 645 646 647 648 649
  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
650 651 652
    info_cng =
      blocker.info
      |> User.Info.add_to_block(ap_id)
lain's avatar
lain committed
653

lain's avatar
lain committed
654 655 656 657 658
    cng =
      change(blocker)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
lain's avatar
lain committed
659 660
  end

661 662 663 664 665
  # 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
666 667 668 669
  def unblock(blocker, %{ap_id: ap_id}) do
    info_cng =
      blocker.info
      |> User.Info.remove_from_block(ap_id)
lain's avatar
lain committed
670

lain's avatar
lain committed
671 672 673 674 675
    cng =
      change(blocker)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
lain's avatar
lain committed
676 677 678
  end

  def blocks?(user, %{ap_id: ap_id}) do
lain's avatar
lain committed
679 680
    blocks = user.info.blocks
    domain_blocks = user.info.domain_blocks
eal's avatar
eal committed
681
    %{host: host} = URI.parse(ap_id)
eal's avatar
eal committed
682 683 684 685 686

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

689 690 691
  def blocked_users(user),
    do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))

eal's avatar
eal committed
692
  def block_domain(user, domain) do
lain's avatar
lain committed
693 694 695
    info_cng =
      user.info
      |> User.Info.add_to_domain_block(domain)
eal's avatar
eal committed
696

lain's avatar
lain committed
697 698 699
    cng =
      change(user)
      |> put_embed(:info, info_cng)
lain's avatar
lain committed
700 701

    update_and_set_cache(cng)
eal's avatar
eal committed
702 703 704
  end

  def unblock_domain(user, domain) do
lain's avatar
lain committed
705 706 707
    info_cng =
      user.info
      |> User.Info.remove_from_domain_block(domain)
eal's avatar
eal committed
708

lain's avatar
lain committed
709 710 711
    cng =
      change(user)
      |> put_embed(:info, info_cng)
lain's avatar
lain committed
712 713

    update_and_set_cache(cng)
lain's avatar
lain committed
714 715
  end

lain's avatar
lain committed
716
  def local_user_query() do
717 718 719 720 721
    from(
      u in User,
      where: u.local == true,
      where: not is_nil(u.nickname)
    )
lain's avatar
lain committed
722 723
  end

kaniini's avatar
kaniini committed
724 725 726 727 728 729 730 731
  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
732
  def deactivate(%User{} = user, status \\ true) do
lain's avatar
lain committed
733
    info_cng = User.Info.set_activation_status(user.info, status)
lain's avatar
lain committed
734 735 736 737

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

    update_and_set_cache(cng)
lain's avatar
lain committed
740
  end
lain's avatar
lain committed
741

lain's avatar
lain committed
742
  def delete(%User{} = user) do
lain's avatar
lain committed
743 744 745
    {:ok, user} = User.deactivate(user)

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

lain's avatar
lain committed
748
    followers
lain's avatar
lain committed
749
    |> Enum.each(fn follower -> User.unfollow(follower, user) end)
lain's avatar
lain committed
750 751

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

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

lain's avatar
lain committed
756
    query = from(a in Activity, where: a.actor == ^user.ap_id)
lain's avatar
lain committed
757 758

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

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

770
    {:ok, user}
lain's avatar
lain committed
771
  end
772

lain's avatar
lain committed
773
  def html_filter_policy(%User{info: %{no_rich_text: true}}) do
kaniini's avatar
kaniini committed
774 775 776 777 778
    Pleroma.HTML.Scrubber.TwitterText
  end

  def html_filter_policy(_), do: nil

779
  def get_or_fetch_by_ap_id(ap_id) do
780 781 782
    user = get_by_ap_id(ap_id)

    if !is_nil(user) and !User.needs_update?(user) do
783 784
      user
    else
lain's avatar
lain committed
785 786 787
      ap_try = ActivityPub.make_user_from_ap_id(ap_id)

      case ap_try do
lain's avatar
lain committed
788 789 790
        {:ok, user} ->
          user

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

800
  def get_or_create_instance_user do
801 802 803
    relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"

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

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

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

lain's avatar
lain committed
829
    {:ok, key}
830 831 832
  end

  # OStatus Magic Key
lain's avatar
lain committed
833
  def public_key_from_info(%{magic_key: magic_key}) do
834 835 836
    {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
  end

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

846 847 848
  defp blank?(""), do: nil
  defp blank?(n), do: n

lain's avatar
lain committed
849
  def insert_or_update_user(data) do
lain's avatar
lain committed
850 851 852 853
    data =
      data
      |> Map.put(:name, blank?(data[:name]) || data[:nickname])

lain's avatar
lain committed
854
    cs = User.remote_user_creation(data)
lain's avatar
lain committed
855

lain's avatar
lain committed
856 857
    Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
  end
858

859
  def ap_enabled?(%User{local: true}), do: true
lain's avatar
lain committed
860
  def ap_enabled?(%User{info: info}), do: info.ap_enabled
lain's avatar
lain committed
861
  def ap_enabled?(_), do: false
lain's avatar
lain committed
862

Maksim's avatar
Maksim committed
863 864 865 866
  @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)
867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890

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

892
  def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
lain's avatar
lain committed
893 894
  def parse_bio(nil, _user), do: ""
  def parse_bio(bio, _user) when bio == "", do: bio
895 896

  def parse_bio(bio, user) do
Maxim Filippov's avatar
Maxim Filippov committed
897 898 899 900 901 902 903 904 905 906
    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
907 908 909
    bio
    |> CommonUtils.format_input(mentions, tags, "text/plain")
    |> Formatter.emojify(emoji)
Maxim Filippov's avatar
Maxim Filippov committed
910
  end
911

912 913 914 915 916
  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
917

Maksim's avatar
Maksim committed
918 919 920 921
  def tag(nickname, tags) when is_binary(nickname),
    do: tag(User.get_by_nickname(nickname), tags)

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

924 925 926 927 928
  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
929

930 931
  def untag(nickname, tags) when is_binary(nickname),
    do: untag(User.get_by_nickname(nickname), tags)
932

933 934
  def untag(%User{} = user, tags),
    do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
935

936 937 938 939 940
  defp update_tags(%User{} = user, new_tags) do
    {:ok, updated_user} =
      user
      |> change(%{tags: new_tags})
      |> Repo.update()
941

942
    updated_user
943
  end
Ivan Tashkinov's avatar
Ivan Tashkinov committed
944

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

  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