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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  def html_filter_policy(_), do: nil

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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