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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

166 167 168
    OAuth.Token.delete_user_tokens(struct)
    OAuth.Authorization.delete_user_authorizations(struct)

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

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

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

183 184 185 186 187 188 189 190
  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
191 192
    info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)

lain's avatar
lain committed
193 194 195 196 197 198 199
    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
200
      |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
href's avatar
href committed
201
      |> validate_format(:nickname, local_nickname_regex())
lain's avatar
lain committed
202 203 204
      |> validate_format(:email, @email_regex)
      |> validate_length(:bio, max: 1000)
      |> validate_length(:name, min: 1, max: 100)
Ivan Tashkinov's avatar
Ivan Tashkinov committed
205
      |> put_change(:info, info_change)
lain's avatar
lain committed
206 207

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

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

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

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

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

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

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

279
    ap_followers = followed.follower_address
280

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

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

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

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

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

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

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

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

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

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

      {:ok, followed} = update_follower_count(followed)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    {:ok, users}
  end

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

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

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

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

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

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

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

    note_count = Repo.one(note_count_query)

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

lain's avatar
lain committed
503 504 505
    cng =
      change(user)
      |> put_embed(:info, info_cng)
506

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

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

    follower_count = Repo.one(follower_count_query)

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

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

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

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

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

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

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

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

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

563 564 565
    Repo.all(query)
  end

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

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

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

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

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

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

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

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

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

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

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

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

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

652 653 654
  def blocked_users(user),
    do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))

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

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

    update_and_set_cache(cng)
eal's avatar
eal committed
665 666 667
  end

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

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

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

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

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

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

    update_and_set_cache(cng)
lain's avatar
lain committed
703
  end
lain's avatar
lain committed
704

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

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

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

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

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

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

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

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

733
    {:ok, user}
lain's avatar
lain committed
734
  end
735

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

  def html_filter_policy(_), do: nil

742
  def get_or_fetch_by_ap_id(ap_id) do
743 744 745
    user = get_by_ap_id(ap_id)

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

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

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

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

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

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

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

lain's avatar
lain committed
792
    {:ok, key}
793 794 795
  end

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

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

809 810 811
  defp blank?(""), do: nil
  defp blank?(n), do: n

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

lain's avatar
lain committed
817
    cs = User.remote_user_creation(data)
lain's avatar
lain committed
818

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

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

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

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

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

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

875 876 877 878 879
  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
880

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

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

887 888 889 890 891
  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
892

893 894
  def untag(nickname, tags) when is_binary(nickname),
    do: untag(User.get_by_nickname(nickname), tags)
895

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

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

905
    updated_user
906
  end
Ivan Tashkinov's avatar
Ivan Tashkinov committed
907

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

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