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

Maksim's avatar
Maksim committed
16 17
  @type t :: %__MODULE__{}

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

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

    timestamps()
  end
lain's avatar
lain committed
44

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

50 51
  def remote_or_auth_active?(%User{} = user), do: !user.local || auth_active?(user)

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)
href's avatar
href committed
202
      |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
203 204 205
      |> validate_format(:email, @email_regex)
      |> validate_length(:bio, max: 1000)
      |> validate_length(:name, min: 1, max: 100)
Ivan Tashkinov's avatar
Ivan Tashkinov committed
206
      |> put_change(:info, info_change)
lain's avatar
lain committed
207 208

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

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

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

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

242 243 244 245 246 247 248 249 250 251
  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
252
  def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
253 254 255 256 257 258 259 260
    {: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
261
    if not User.ap_enabled?(followed) do
262
      follow(follower, followed)
263 264 265 266 267
    else
      {:ok, follower}
    end
  end

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

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

280
    ap_followers = followed.follower_address
281

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

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

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

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

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

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

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

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

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

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

      {:ok, followed} = update_follower_count(followed)

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

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

335
  def locked?(%User{} = user) do
336
    user.info.locked || false
337 338
  end

lain's avatar
lain committed
339 340 341 342
  def get_by_ap_id(ap_id) do
    Repo.get_by(User, ap_id: ap_id)
  end

lain's avatar
lain committed
343 344
  def update_and_set_cache(changeset) do
    with {:ok, user} <- Repo.update(changeset) do
Thog's avatar
Thog committed
345 346 347
      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
348 349 350 351 352 353
      {:ok, user}
    else
      e -> e
    end
  end

lain's avatar
lain committed
354 355 356
  def invalidate_cache(user) do
    Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
    Cachex.del(:user_cache, "nickname:#{user.nickname}")
357
    Cachex.del(:user_cache, "user_info:#{user.id}")
lain's avatar
lain committed
358 359
  end

lain's avatar
lain committed
360
  def get_cached_by_ap_id(ap_id) do
361
    key = "ap_id:#{ap_id}"
Thog's avatar
Thog committed
362
    Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
lain's avatar
lain committed
363 364 365
  end

  def get_cached_by_nickname(nickname) do
366
    key = "nickname:#{nickname}"
Thog's avatar
Thog committed
367
    Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
lain's avatar
lain committed
368
  end
lain's avatar
lain committed
369

lain's avatar
lain committed
370
  def get_by_nickname(nickname) do
371 372 373
    Repo.get_by(User, nickname: nickname)
  end

374 375 376 377 378 379 380
  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
381 382
  def get_cached_user_info(user) do
    key = "user_info:#{user.id}"
Thog's avatar
Thog committed
383
    Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
lain's avatar
lain committed
384
  end
lain's avatar
lain committed
385

lain's avatar
lain committed
386 387 388 389 390 391 392 393 394
  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
395
  def get_or_fetch_by_nickname(nickname) do
lain's avatar
lain committed
396
    with %User{} = user <- get_by_nickname(nickname) do
lain's avatar
lain committed
397
      user
lain's avatar
lain committed
398 399 400 401 402 403 404 405
    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
406
    end
lain's avatar
lain committed
407
  end
lain's avatar
lain committed
408

409 410 411 412 413 414 415 416 417 418
  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
419 420 421 422

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

423 424 425 426 427 428 429 430 431 432
  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
433 434 435

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

437 438 439
  def get_follow_requests_query(%User{} = user) do
    from(
      a in Activity,
kaniini's avatar
kaniini committed
440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
      where:
        fragment(
          "? ->> 'type' = 'Follow'",
          a.data
        ),
      where:
        fragment(
          "? ->> 'state' = 'pending'",
          a.data
        ),
      where:
        fragment(
          "? @> ?",
          a.data,
          ^%{"object" => user.ap_id}
        )
456 457 458 459 460 461 462 463
    )
  end

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

    users =
kaniini's avatar
kaniini committed
464 465 466
      Enum.map(reqs, fn req -> req.actor end)
      |> Enum.uniq()
      |> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end)
467
      |> Enum.filter(fn u -> !following?(u, user) end)
468 469 470 471

    {:ok, users}
  end

472
  def increase_note_count(%User{} = user) do
lain's avatar
lain committed
473
    info_cng = User.Info.add_to_note_count(user.info, 1)
lain's avatar
lain committed
474 475 476 477

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

lain's avatar
lain committed
479
    update_and_set_cache(cng)
480 481
  end

482
  def decrease_note_count(%User{} = user) do
lain's avatar
lain committed
483
    info_cng = User.Info.add_to_note_count(user.info, -1)
lain's avatar
lain committed
484 485 486 487

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

lain's avatar
lain committed
489
    update_and_set_cache(cng)
490 491
  end

492
  def update_note_count(%User{} = user) do
lain's avatar
lain committed
493 494 495 496 497 498
    note_count_query =
      from(
        a in Object,
        where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
        select: count(a.id)
      )
499 500 501

    note_count = Repo.one(note_count_query)

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

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

  def update_follower_count(%User{} = user) do
lain's avatar
lain committed
512 513 514 515 516 517 518
    follower_count_query =
      from(
        u in User,
        where: ^user.follower_address in u.following,
        where: u.id != ^user.id,
        select: count(u.id)
      )
519 520 521

    follower_count = Repo.one(follower_count_query)

lain's avatar
lain committed
522 523 524
    info_cng =
      user.info
      |> User.Info.set_follower_count(follower_count)
525

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

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

533
  def get_users_from_set_query(ap_ids, false) do
534 535
    from(
      u in User,
536
      where: u.ap_id in ^ap_ids
537 538 539
    )
  end

540 541
  def get_users_from_set_query(ap_ids, true) do
    query = get_users_from_set_query(ap_ids, false)
542 543 544

    from(
      u in query,
545 546 547 548
      where: u.local == true
    )
  end

549 550 551 552 553
  def get_users_from_set(ap_ids, local_only \\ true) do
    get_users_from_set_query(ap_ids, local_only)
    |> Repo.all()
  end

554
  def get_recipients_from_activity(%Activity{recipients: to}) do
lain's avatar
lain committed
555 556 557 558 559 560
    query =
      from(
        u in User,
        where: u.ap_id in ^to,
        or_where: fragment("? && ?", u.following, ^to)
      )
561

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

564 565 566
    Repo.all(query)
  end

567
  def search(query, resolve \\ false) do
568 569 570
    # strip the beginning @ off if there is a query
    query = String.trim_leading(query, "@")

lain's avatar
lain committed
571 572 573
    if resolve do
      User.get_or_fetch_by_nickname(query)
    end
lain's avatar
lain committed
574

lain's avatar
lain committed
575
    inner =
lain's avatar
lain committed
576 577
      from(
        u in User,
lain's avatar
lain committed
578
        select_merge: %{
kaniini's avatar
kaniini committed
579 580 581 582 583 584 585
          search_distance:
            fragment(
              "? <-> (? || ?)",
              ^query,
              u.nickname,
              u.name
            )
586 587
        },
        where: not is_nil(u.nickname)
lain's avatar
lain committed
588 589
      )

kaniini's avatar
kaniini committed
590 591 592 593 594 595
    q =
      from(
        s in subquery(inner),
        order_by: s.search_distance,
        limit: 20
      )
lain's avatar
lain committed
596

lain's avatar
lain committed
597 598
    Repo.all(q)
  end
lain's avatar
lain committed
599

600 601 602 603 604 605 606 607 608 609 610 611 612 613
  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
614 615 616
    info_cng =
      blocker.info
      |> User.Info.add_to_block(ap_id)
lain's avatar
lain committed
617

lain's avatar
lain committed
618 619 620 621 622
    cng =
      change(blocker)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
lain's avatar
lain committed
623 624
  end

625 626 627 628 629
  # 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
630 631 632 633
  def unblock(blocker, %{ap_id: ap_id}) do
    info_cng =
      blocker.info
      |> User.Info.remove_from_block(ap_id)
lain's avatar
lain committed
634

lain's avatar
lain committed
635 636 637 638 639
    cng =
      change(blocker)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
lain's avatar
lain committed
640 641 642
  end

  def blocks?(user, %{ap_id: ap_id}) do
lain's avatar
lain committed
643 644
    blocks = user.info.blocks
    domain_blocks = user.info.domain_blocks
eal's avatar
eal committed
645
    %{host: host} = URI.parse(ap_id)
eal's avatar
eal committed
646 647 648 649 650

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

  def block_domain(user, domain) do
lain's avatar
lain committed
654 655 656
    info_cng =
      user.info
      |> User.Info.add_to_domain_block(domain)
eal's avatar
eal committed
657

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

    update_and_set_cache(cng)
eal's avatar
eal committed
663 664 665
  end

  def unblock_domain(user, domain) do
lain's avatar
lain committed
666 667 668
    info_cng =
      user.info
      |> User.Info.remove_from_domain_block(domain)
eal's avatar
eal committed
669

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

    update_and_set_cache(cng)
lain's avatar
lain committed
675 676
  end

lain's avatar
lain committed
677
  def local_user_query() do
678 679 680 681 682
    from(
      u in User,
      where: u.local == true,
      where: not is_nil(u.nickname)
    )
lain's avatar
lain committed
683 684
  end

kaniini's avatar
kaniini committed
685 686 687 688 689 690 691 692
  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
693
  def deactivate(%User{} = user, status \\ true) do
lain's avatar
lain committed
694
    info_cng = User.Info.set_activation_status(user.info, status)
lain's avatar
lain committed
695 696 697 698

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

    update_and_set_cache(cng)
lain's avatar
lain committed
701
  end
lain's avatar
lain committed
702

lain's avatar
lain committed
703
  def delete(%User{} = user) do
lain's avatar
lain committed
704 705 706
    {:ok, user} = User.deactivate(user)

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

lain's avatar
lain committed
709
    followers
lain's avatar
lain committed
710
    |> Enum.each(fn follower -> User.unfollow(follower, user) end)
lain's avatar
lain committed
711 712

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

lain's avatar
lain committed
714
    friends
lain's avatar
lain committed
715
    |> Enum.each(fn followed -> User.unfollow(user, followed) end)
lain's avatar
lain committed
716

lain's avatar
lain committed
717
    query = from(a in Activity, where: a.actor == ^user.ap_id)
lain's avatar
lain committed
718 719

    Repo.all(query)
lain's avatar
lain committed
720
    |> Enum.each(fn activity ->
lain's avatar
lain committed
721
      case activity.data["type"] do
lain's avatar
lain committed
722
        "Create" ->
723
          ActivityPub.delete(Object.normalize(activity.data["object"]))
lain's avatar
lain committed
724 725 726 727

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

731
    {:ok, user}
lain's avatar
lain committed
732
  end
733

lain's avatar
lain committed
734
  def html_filter_policy(%User{info: %{no_rich_text: true}}) do
kaniini's avatar
kaniini committed
735 736 737 738 739
    Pleroma.HTML.Scrubber.TwitterText
  end

  def html_filter_policy(_), do: nil

740
  def get_or_fetch_by_ap_id(ap_id) do
741 742 743
    user = get_by_ap_id(ap_id)

    if !is_nil(user) and !User.needs_update?(user) do
744 745
      user
    else
lain's avatar
lain committed
746 747 748
      ap_try = ActivityPub.make_user_from_ap_id(ap_id)

      case ap_try do
lain's avatar
lain committed
749 750 751
        {:ok, user} ->
          user

lain's avatar
lain committed
752 753 754
        _ ->
          case OStatus.make_user(ap_id) do
            {:ok, user} -> user
feld's avatar
feld committed
755
            _ -> {:error, "Could not fetch by AP id"}
lain's avatar
lain committed
756
          end
757 758 759 760
      end
    end
  end

761
  def get_or_create_instance_user do
762 763 764
    relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"

    if user = get_by_ap_id(relay_uri) do
765 766 767
      user
    else
      changes =
lain's avatar
lain committed
768
        %User{info: %User.Info{}}
769
        |> cast(%{}, [:ap_id, :nickname, :local])
770
        |> put_change(:ap_id, relay_uri)
771 772
        |> put_change(:nickname, nil)
        |> put_change(:local, true)
773
        |> put_change(:follower_address, relay_uri <> "/followers")
774 775 776 777 778 779

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

780
  # AP style
lain's avatar
lain committed
781
  def public_key_from_info(%{
lain's avatar
lain committed
782
        source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
lain's avatar
lain committed
783 784
      }) do
    key =
Maksim's avatar
Maksim committed
785 786
      public_key_pem
      |> :public_key.pem_decode()
lain's avatar
lain committed
787 788
      |> hd()
      |> :public_key.pem_entry_decode()
789

lain's avatar
lain committed
790
    {:ok, key}
791 792 793
  end

  # OStatus Magic Key
lain's avatar
lain committed
794
  def public_key_from_info(%{magic_key: magic_key}) do
795 796 797
    {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
  end

798
  def get_public_key_for_ap_id(ap_id) do
799 800
    with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
         {:ok, public_key} <- public_key_from_info(user.info) do
801 802 803 804 805
      {:ok, public_key}
    else
      _ -> :error
    end
  end
lain's avatar
lain committed
806

807 808 809
  defp blank?(""), do: nil
  defp blank?(n), do: n

lain's avatar
lain committed
810
  def insert_or_update_user(data) do
lain's avatar
lain committed
811 812 813 814
    data =
      data
      |> Map.put(:name, blank?(data[:name]) || data[:nickname])

lain's avatar
lain committed
815
    cs = User.remote_user_creation(data)
lain's avatar
lain committed
816

lain's avatar
lain committed
817 818
    Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
  end
819

820
  def ap_enabled?(%User{local: true}), do: true
lain's avatar
lain committed
821
  def ap_enabled?(%User{info: info}), do: info.ap_enabled
lain's avatar
lain committed
822
  def ap_enabled?(_), do: false
lain's avatar
lain committed
823

Maksim's avatar
Maksim committed
824 825 826 827
  @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)
828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851

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

853
  def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
lain's avatar
lain committed
854 855
  def parse_bio(nil, _user), do: ""
  def parse_bio(bio, _user) when bio == "", do: bio
856 857

  def parse_bio(bio, user) do
Maxim Filippov's avatar
Maxim Filippov committed
858 859 860 861 862 863 864 865 866 867
    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
868 869 870
    bio
    |> CommonUtils.format_input(mentions, tags, "text/plain")
    |> Formatter.emojify(emoji)
Maxim Filippov's avatar
Maxim Filippov committed
871
  end
872

873 874 875 876 877
  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
878

Maksim's avatar
Maksim committed
879 880 881 882
  def tag(nickname, tags) when is_binary(nickname),
    do: tag(User.get_by_nickname(nickname), tags)

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

885 886 887 888 889
  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
890

891 892
  def untag(nickname, tags) when is_binary(nickname),
    do: untag(User.get_by_nickname(nickname), tags)
893

894 895
  def untag(%User{} = user, tags),
    do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
896

897 898 899 900 901
  defp update_tags(%User{} = user, new_tags) do
    {:ok, updated_user} =
      user
      |> change(%{tags: new_tags})
      |> Repo.update()
902

903
    updated_user
904
  end
Ivan Tashkinov's avatar
Ivan Tashkinov committed
905

906 907 908 909 910
  defp normalize_tags(tags) do
    [tags]
    |> List.flatten()
    |> Enum.map(&String.downcase(&1))
  end
href's avatar
href committed
911 912 913 914 915 916 917 918

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