user.ex 23.1 KB
Newer Older
lain's avatar
lain committed
1 2
defmodule Pleroma.User do
  use Ecto.Schema
3

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

Maksim's avatar
Maksim committed
12 13
  @type t :: %__MODULE__{}

href's avatar
href committed
14 15 16
  @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
17
  @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
href's avatar
href committed
18

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

    timestamps()
  end
lain's avatar
lain committed
40

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

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

lain's avatar
lain committed
48 49 50
  def avatar_url(user) do
    case user.avatar do
      %{"url" => [%{"href" => href} | _]} -> href
51
      _ -> "#{Web.base_url()}/images/avi.png"
lain's avatar
lain committed
52 53 54
    end
  end

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

lain's avatar
lain committed
62
  def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
63 64 65
  def profile_url(%User{ap_id: ap_id}), do: ap_id
  def profile_url(_), do: nil

lain's avatar
lain committed
66
  def ap_id(%User{nickname: nickname}) do
lain's avatar
lain committed
67
    "#{Web.base_url()}/users/#{nickname}"
lain's avatar
lain committed
68 69 70 71 72
  end

  def ap_followers(%User{} = user) do
    "#{ap_id(user)}/followers"
  end
lain's avatar
lain committed
73 74 75 76 77 78 79

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

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

lain's avatar
lain committed
83
    %{
84
      following_count: length(user.following) - oneself,
Ivan Tashkinov's avatar
Ivan Tashkinov committed
85 86 87 88 89
      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
90 91 92
    }
  end

lain's avatar
lain committed
93
  def remote_user_creation(params) do
lain's avatar
lain committed
94 95 96
    params =
      params
      |> Map.put(:info, params[:info] || %{})
lain's avatar
lain committed
97 98 99

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

lain's avatar
lain committed
100
    changes =
lain's avatar
lain committed
101
      %User{}
lain's avatar
lain committed
102
      |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
103
      |> validate_required([:name, :ap_id])
lain's avatar
lain committed
104 105 106 107 108
      |> 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
109
      |> put_embed(:info, info_cng)
lain's avatar
lain committed
110

111
    if changes.valid? do
lain's avatar
lain committed
112
      case info_cng.changes[:source_data] do
lain's avatar
lain committed
113 114 115
        %{"followers" => followers} ->
          changes
          |> put_change(:follower_address, followers)
lain's avatar
lain committed
116

lain's avatar
lain committed
117 118
        _ ->
          followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
lain's avatar
lain committed
119

lain's avatar
lain committed
120 121 122
          changes
          |> put_change(:follower_address, followers)
      end
123 124 125
    else
      changes
    end
lain's avatar
lain committed
126 127
  end

lain's avatar
lain committed
128
  def update_changeset(struct, params \\ %{}) do
Thog's avatar
Thog committed
129
    struct
lain's avatar
lain committed
130
    |> cast(params, [:bio, :name, :avatar])
lain's avatar
lain committed
131
    |> unique_constraint(:nickname)
href's avatar
href committed
132
    |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
133
    |> validate_length(:bio, max: 5000)
lain's avatar
lain committed
134 135 136
    |> validate_length(:name, min: 1, max: 100)
  end

lain's avatar
lain committed
137
  def upgrade_changeset(struct, params \\ %{}) do
138 139 140 141
    params =
      params
      |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())

lain's avatar
lain committed
142 143 144 145
    info_cng =
      struct.info
      |> User.Info.user_upgrade(params[:info])

lain's avatar
lain committed
146
    struct
lain's avatar
lain committed
147
    |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
lain's avatar
lain committed
148
    |> unique_constraint(:nickname)
href's avatar
href committed
149
    |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
150 151
    |> validate_length(:bio, max: 5000)
    |> validate_length(:name, max: 100)
lain's avatar
lain committed
152
    |> put_embed(:info, info_cng)
lain's avatar
lain committed
153 154
  end

Roger Braun's avatar
Roger Braun committed
155
  def password_update_changeset(struct, params) do
lain's avatar
lain committed
156 157 158 159 160
    changeset =
      struct
      |> cast(params, [:password, :password_confirmation])
      |> validate_required([:password, :password_confirmation])
      |> validate_confirmation(:password)
Roger Braun's avatar
Roger Braun committed
161

162 163 164
    OAuth.Token.delete_user_tokens(struct)
    OAuth.Authorization.delete_user_authorizations(struct)

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

Roger Braun's avatar
Roger Braun committed
168 169 170 171 172 173 174 175
      changeset
      |> put_change(:password_hash, hashed)
    else
      changeset
    end
  end

  def reset_password(user, data) do
lain's avatar
lain committed
176
    update_and_set_cache(password_update_changeset(user, data))
Roger Braun's avatar
Roger Braun committed
177 178
  end

179 180 181 182 183 184 185 186
  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
187 188
    info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)

lain's avatar
lain committed
189 190 191 192 193 194 195
    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
196
      |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
197 198 199
      |> validate_format(:email, @email_regex)
      |> validate_length(:bio, max: 1000)
      |> validate_length(:name, min: 1, max: 100)
Ivan Tashkinov's avatar
Ivan Tashkinov committed
200
      |> put_change(:info, info_change)
lain's avatar
lain committed
201 202

    if changeset.valid? do
203
      hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
lain's avatar
lain committed
204 205
      ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
      followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
lain's avatar
lain committed
206

lain's avatar
lain committed
207 208 209 210
      changeset
      |> put_change(:password_hash, hashed)
      |> put_change(:ap_id, ap_id)
      |> put_change(:following, [followers])
211
      |> put_change(:follower_address, followers)
lain's avatar
lain committed
212 213 214 215 216
    else
      changeset
    end
  end

217 218
  @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
219 220
    with {:ok, user} <- Repo.insert(changeset),
         {:ok, _} = try_send_confirmation_email(user) do
221 222 223 224
      {:ok, user}
    end
  end

225
  def try_send_confirmation_email(%User{} = user) do
226 227
    if user.info.confirmation_pending &&
         Pleroma.Config.get([:instance, :account_activation_required]) do
228 229 230 231 232 233 234 235
      user
      |> Pleroma.UserEmail.account_confirmation_email()
      |> Pleroma.Mailer.deliver()
    else
      {:ok, :noop}
    end
  end

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

Maksim's avatar
Maksim committed
262
  def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
263 264
    if not following?(follower, followed) do
      follow(follower, followed)
265
    else
266
      {:ok, follower}
267 268 269
    end
  end

lain's avatar
lain committed
270
  def follow(%User{} = follower, %User{info: info} = followed) do
271 272
    user_config = Application.get_env(:pleroma, :user)
    deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
273

274
    ap_followers = followed.follower_address
275

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

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

283 284 285 286 287 288 289 290
      true ->
        if !followed.local && follower.local && !ap_enabled?(followed) do
          Websub.subscribe(follower, followed)
        end

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

292 293 294 295
        follower =
          follower
          |> follow_changeset(%{following: following})
          |> update_and_set_cache
296

297 298 299
        {:ok, _} = update_follower_count(followed)

        follower
300
    end
lain's avatar
lain committed
301
  end
lain's avatar
lain committed
302 303

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

306
    if following?(follower, followed) and follower.ap_id != followed.ap_id do
lain's avatar
lain committed
307 308 309
      following =
        follower.following
        |> List.delete(ap_followers)
lain's avatar
lain committed
310

lain's avatar
lain committed
311 312 313 314
      {:ok, follower} =
        follower
        |> follow_changeset(%{following: following})
        |> update_and_set_cache
315 316 317 318

      {:ok, followed} = update_follower_count(followed)

      {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
319
    else
320
      {:error, "Not subscribed!"}
321
    end
lain's avatar
lain committed
322
  end
323

Maksim's avatar
Maksim committed
324
  @spec following?(User.t(), User.t()) :: boolean
325
  def following?(%User{} = follower, %User{} = followed) do
326
    Enum.member?(follower.following, followed.follower_address)
327
  end
lain's avatar
lain committed
328

329
  def locked?(%User{} = user) do
330
    user.info.locked || false
331 332
  end

lain's avatar
lain committed
333 334 335 336
  def get_by_ap_id(ap_id) do
    Repo.get_by(User, ap_id: ap_id)
  end

lain's avatar
lain committed
337 338
  def update_and_set_cache(changeset) do
    with {:ok, user} <- Repo.update(changeset) do
Thog's avatar
Thog committed
339 340 341
      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
342 343 344 345 346 347
      {:ok, user}
    else
      e -> e
    end
  end

lain's avatar
lain committed
348 349 350
  def invalidate_cache(user) do
    Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
    Cachex.del(:user_cache, "nickname:#{user.nickname}")
351
    Cachex.del(:user_cache, "user_info:#{user.id}")
lain's avatar
lain committed
352 353
  end

lain's avatar
lain committed
354
  def get_cached_by_ap_id(ap_id) do
355
    key = "ap_id:#{ap_id}"
Thog's avatar
Thog committed
356
    Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
lain's avatar
lain committed
357 358 359
  end

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

lain's avatar
lain committed
364
  def get_by_nickname(nickname) do
365 366 367
    Repo.get_by(User, nickname: nickname)
  end

368 369 370 371 372 373 374
  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
375 376
  def get_cached_user_info(user) do
    key = "user_info:#{user.id}"
Thog's avatar
Thog committed
377
    Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
lain's avatar
lain committed
378
  end
lain's avatar
lain committed
379

lain's avatar
lain committed
380 381 382 383 384 385 386 387 388
  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
389
  def get_or_fetch_by_nickname(nickname) do
lain's avatar
lain committed
390
    with %User{} = user <- get_by_nickname(nickname) do
lain's avatar
lain committed
391
      user
lain's avatar
lain committed
392 393 394 395 396 397 398 399
    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
400
    end
lain's avatar
lain committed
401
  end
lain's avatar
lain committed
402

403 404 405 406 407 408 409 410 411 412
  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
413 414 415 416

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

417 418 419 420 421 422 423 424 425 426
  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
427 428 429

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

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

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

    users =
kaniini's avatar
kaniini committed
458 459 460
      Enum.map(reqs, fn req -> req.actor end)
      |> Enum.uniq()
      |> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end)
461
      |> Enum.filter(fn u -> !following?(u, user) end)
462 463 464 465

    {:ok, users}
  end

466
  def increase_note_count(%User{} = user) do
lain's avatar
lain committed
467
    info_cng = User.Info.add_to_note_count(user.info, 1)
lain's avatar
lain committed
468 469 470 471

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

lain's avatar
lain committed
473
    update_and_set_cache(cng)
474 475
  end

476
  def decrease_note_count(%User{} = user) do
lain's avatar
lain committed
477
    info_cng = User.Info.add_to_note_count(user.info, -1)
lain's avatar
lain committed
478 479 480 481

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

lain's avatar
lain committed
483
    update_and_set_cache(cng)
484 485
  end

486
  def update_note_count(%User{} = user) do
lain's avatar
lain committed
487 488 489 490 491 492
    note_count_query =
      from(
        a in Object,
        where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
        select: count(a.id)
      )
493 494 495

    note_count = Repo.one(note_count_query)

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

lain's avatar
lain committed
498 499 500
    cng =
      change(user)
      |> put_embed(:info, info_cng)
501

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

  def update_follower_count(%User{} = user) do
lain's avatar
lain committed
506 507 508 509 510 511 512
    follower_count_query =
      from(
        u in User,
        where: ^user.follower_address in u.following,
        where: u.id != ^user.id,
        select: count(u.id)
      )
513 514 515

    follower_count = Repo.one(follower_count_query)

lain's avatar
lain committed
516 517 518
    info_cng =
      user.info
      |> User.Info.set_follower_count(follower_count)
519

lain's avatar
lain committed
520 521 522
    cng =
      change(user)
      |> put_embed(:info, info_cng)
523

lain's avatar
lain committed
524
    update_and_set_cache(cng)
525
  end
526

527
  def get_users_from_set_query(ap_ids, false) do
528 529
    from(
      u in User,
530
      where: u.ap_id in ^ap_ids
531 532 533
    )
  end

534 535
  def get_users_from_set_query(ap_ids, true) do
    query = get_users_from_set_query(ap_ids, false)
536 537 538

    from(
      u in query,
539 540 541 542
      where: u.local == true
    )
  end

543 544 545 546 547
  def get_users_from_set(ap_ids, local_only \\ true) do
    get_users_from_set_query(ap_ids, local_only)
    |> Repo.all()
  end

548
  def get_recipients_from_activity(%Activity{recipients: to}) do
lain's avatar
lain committed
549 550 551 552 553 554
    query =
      from(
        u in User,
        where: u.ap_id in ^to,
        or_where: fragment("? && ?", u.following, ^to)
      )
555

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

558 559 560
    Repo.all(query)
  end

561
  def search(query, resolve \\ false) do
562 563 564
    # strip the beginning @ off if there is a query
    query = String.trim_leading(query, "@")

lain's avatar
lain committed
565 566 567
    if resolve do
      User.get_or_fetch_by_nickname(query)
    end
lain's avatar
lain committed
568

lain's avatar
lain committed
569
    inner =
lain's avatar
lain committed
570 571
      from(
        u in User,
lain's avatar
lain committed
572
        select_merge: %{
kaniini's avatar
kaniini committed
573 574 575 576 577 578 579
          search_distance:
            fragment(
              "? <-> (? || ?)",
              ^query,
              u.nickname,
              u.name
            )
580 581
        },
        where: not is_nil(u.nickname)
lain's avatar
lain committed
582 583
      )

kaniini's avatar
kaniini committed
584 585 586 587 588 589
    q =
      from(
        s in subquery(inner),
        order_by: s.search_distance,
        limit: 20
      )
lain's avatar
lain committed
590

lain's avatar
lain committed
591 592
    Repo.all(q)
  end
lain's avatar
lain committed
593

594 595 596 597 598 599 600 601 602 603 604 605 606 607
  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
608 609 610
    info_cng =
      blocker.info
      |> User.Info.add_to_block(ap_id)
lain's avatar
lain committed
611

lain's avatar
lain committed
612 613 614 615 616
    cng =
      change(blocker)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
lain's avatar
lain committed
617 618
  end

619 620 621 622 623
  # 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
624 625 626 627
  def unblock(blocker, %{ap_id: ap_id}) do
    info_cng =
      blocker.info
      |> User.Info.remove_from_block(ap_id)
lain's avatar
lain committed
628

lain's avatar
lain committed
629 630 631 632 633
    cng =
      change(blocker)
      |> put_embed(:info, info_cng)

    update_and_set_cache(cng)
lain's avatar
lain committed
634 635 636
  end

  def blocks?(user, %{ap_id: ap_id}) do
lain's avatar
lain committed
637 638
    blocks = user.info.blocks
    domain_blocks = user.info.domain_blocks
eal's avatar
eal committed
639
    %{host: host} = URI.parse(ap_id)
eal's avatar
eal committed
640 641 642 643 644

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

  def block_domain(user, domain) do
lain's avatar
lain committed
648 649 650
    info_cng =
      user.info
      |> User.Info.add_to_domain_block(domain)
eal's avatar
eal committed
651

lain's avatar
lain committed
652 653 654
    cng =
      change(user)
      |> put_embed(:info, info_cng)
lain's avatar
lain committed
655 656

    update_and_set_cache(cng)
eal's avatar
eal committed
657 658 659
  end

  def unblock_domain(user, domain) do
lain's avatar
lain committed
660 661 662
    info_cng =
      user.info
      |> User.Info.remove_from_domain_block(domain)
eal's avatar
eal committed
663

lain's avatar
lain committed
664 665 666
    cng =
      change(user)
      |> put_embed(:info, info_cng)
lain's avatar
lain committed
667 668

    update_and_set_cache(cng)
lain's avatar
lain committed
669 670
  end

lain's avatar
lain committed
671
  def local_user_query() do
672 673 674 675 676
    from(
      u in User,
      where: u.local == true,
      where: not is_nil(u.nickname)
    )
lain's avatar
lain committed
677 678
  end

kaniini's avatar
kaniini committed
679 680 681 682 683 684 685 686
  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
687
  def deactivate(%User{} = user, status \\ true) do
lain's avatar
lain committed
688
    info_cng = User.Info.set_activation_status(user.info, status)
lain's avatar
lain committed
689 690 691 692

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

    update_and_set_cache(cng)
lain's avatar
lain committed
695
  end
lain's avatar
lain committed
696

lain's avatar
lain committed
697
  def delete(%User{} = user) do
lain's avatar
lain committed
698 699 700
    {:ok, user} = User.deactivate(user)

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

lain's avatar
lain committed
703
    followers
lain's avatar
lain committed
704
    |> Enum.each(fn follower -> User.unfollow(follower, user) end)
lain's avatar
lain committed
705 706

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

lain's avatar
lain committed
708
    friends
lain's avatar
lain committed
709
    |> Enum.each(fn followed -> User.unfollow(user, followed) end)
lain's avatar
lain committed
710

lain's avatar
lain committed
711
    query = from(a in Activity, where: a.actor == ^user.ap_id)
lain's avatar
lain committed
712 713

    Repo.all(query)
lain's avatar
lain committed
714
    |> Enum.each(fn activity ->
lain's avatar
lain committed
715
      case activity.data["type"] do
lain's avatar
lain committed
716
        "Create" ->
717
          ActivityPub.delete(Object.normalize(activity.data["object"]))
lain's avatar
lain committed
718 719 720 721

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

725
    {:ok, user}
lain's avatar
lain committed
726
  end
727

lain's avatar
lain committed
728
  def html_filter_policy(%User{info: %{no_rich_text: true}}) do
kaniini's avatar
kaniini committed
729 730 731 732 733
    Pleroma.HTML.Scrubber.TwitterText
  end

  def html_filter_policy(_), do: nil

734
  def get_or_fetch_by_ap_id(ap_id) do
735 736 737
    user = get_by_ap_id(ap_id)

    if !is_nil(user) and !User.needs_update?(user) do
738 739
      user
    else
lain's avatar
lain committed
740 741 742
      ap_try = ActivityPub.make_user_from_ap_id(ap_id)

      case ap_try do
lain's avatar
lain committed
743 744 745
        {:ok, user} ->
          user

lain's avatar
lain committed
746 747 748
        _ ->
          case OStatus.make_user(ap_id) do
            {:ok, user} -> user
feld's avatar
feld committed
749
            _ -> {:error, "Could not fetch by AP id"}
lain's avatar
lain committed
750
          end
751 752 753 754
      end
    end
  end

755
  def get_or_create_instance_user do
756 757 758
    relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"

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

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

774
  # AP style
lain's avatar
lain committed
775
  def public_key_from_info(%{
lain's avatar
lain committed
776
        source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
lain's avatar
lain committed
777 778
      }) do
    key =
Maksim's avatar
Maksim committed
779 780
      public_key_pem
      |> :public_key.pem_decode()
lain's avatar
lain committed
781 782
      |> hd()
      |> :public_key.pem_entry_decode()
783

lain's avatar
lain committed
784
    {:ok, key}
785 786 787
  end

  # OStatus Magic Key
lain's avatar
lain committed
788
  def public_key_from_info(%{magic_key: magic_key}) do
789 790 791
    {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
  end

792
  def get_public_key_for_ap_id(ap_id) do
793 794
    with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
         {:ok, public_key} <- public_key_from_info(user.info) do
795 796 797 798 799
      {:ok, public_key}
    else
      _ -> :error
    end
  end
lain's avatar
lain committed
800

801 802 803
  defp blank?(""), do: nil
  defp blank?(n), do: n

lain's avatar
lain committed
804
  def insert_or_update_user(data) do
lain's avatar
lain committed
805 806 807 808
    data =
      data
      |> Map.put(:name, blank?(data[:name]) || data[:nickname])

lain's avatar
lain committed
809
    cs = User.remote_user_creation(data)
lain's avatar
lain committed
810

lain's avatar
lain committed
811 812
    Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
  end
813

814
  def ap_enabled?(%User{local: true}), do: true
lain's avatar
lain committed
815
  def ap_enabled?(%User{info: info}), do: info.ap_enabled
lain's avatar
lain committed
816
  def ap_enabled?(_), do: false
lain's avatar
lain committed
817

Maksim's avatar
Maksim committed
818 819 820 821
  @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)
822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845

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

847
  def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
lain's avatar
lain committed
848 849
  def parse_bio(nil, _user), do: ""
  def parse_bio(bio, _user) when bio == "", do: bio
850 851

  def parse_bio(bio, user) do
Maxim Filippov's avatar
Maxim Filippov committed
852 853 854 855 856 857 858 859 860 861
    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
862 863 864
    bio
    |> CommonUtils.format_input(mentions, tags, "text/plain")
    |> Formatter.emojify(emoji)
Maxim Filippov's avatar
Maxim Filippov committed
865
  end
866

867 868 869 870 871
  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
872

Maksim's avatar
Maksim committed
873 874 875 876
  def tag(nickname, tags) when is_binary(nickname),
    do: tag(User.get_by_nickname(nickname), tags)

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

879 880 881 882 883
  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
884

885 886
  def untag(nickname, tags) when is_binary(nickname),
    do: untag(User.get_by_nickname(nickname), tags)
887

888 889
  def untag(%User{} = user, tags),
    do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
890

891 892 893 894 895
  defp update_tags(%User{} = user, new_tags) do
    {:ok, updated_user} =
      user
      |> change(%{tags: new_tags})
      |> Repo.update()
896

897
    updated_user
898
  end
Ivan Tashkinov's avatar
Ivan Tashkinov committed
899

900 901 902 903 904
  defp normalize_tags(tags) do
    [tags]
    |> List.flatten()
    |> Enum.map(&String.downcase(&1))
  end
href's avatar
href committed
905 906 907 908 909 910 911 912

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