Merge branch 'fix/credo-issues' into 'develop'
[akkoma] / lib / pleroma / user.ex
index 6e1d5559d359f6aab26e08731747a36a5d007ac9..0060d966bd17289cdfe7fba93dd88acbc7f192a9 100644 (file)
@@ -1,11 +1,38 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
 defmodule Pleroma.User do
   use Ecto.Schema
 
-  import Ecto.{Changeset, Query}
-  alias Pleroma.{Repo, User, Object, Web, Activity, Notification}
+  import Ecto.Changeset
+  import Ecto.Query
+
+  alias Pleroma.Repo
+  alias Pleroma.User
+  alias Pleroma.Object
+  alias Pleroma.Web
+  alias Pleroma.Activity
+  alias Pleroma.Notification
   alias Comeonin.Pbkdf2
-  alias Pleroma.Web.{OStatus, Websub, OAuth}
-  alias Pleroma.Web.ActivityPub.{Utils, ActivityPub}
+  alias Pleroma.Formatter
+  alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
+  alias Pleroma.Web.OStatus
+  alias Pleroma.Web.Websub
+  alias Pleroma.Web.OAuth
+  alias Pleroma.Web.ActivityPub.Utils
+  alias Pleroma.Web.ActivityPub.ActivityPub
+
+  require Logger
+
+  @type t :: %__MODULE__{}
+
+  @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
+
+  @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]+$/
+  @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
 
   schema "users" do
     field(:bio, :string)
@@ -19,15 +46,40 @@ defmodule Pleroma.User do
     field(:ap_id, :string)
     field(:avatar, :map)
     field(:local, :boolean, default: true)
-    field(:info, :map, default: %{})
     field(:follower_address, :string)
-    field(:search_distance, :float, virtual: true)
+    field(:search_rank, :float, virtual: true)
+    field(:tags, {:array, :string}, default: [])
+    field(:bookmarks, {:array, :string}, default: [])
     field(:last_refreshed_at, :naive_datetime)
     has_many(:notifications, Notification)
+    embeds_one(:info, Pleroma.User.Info)
 
     timestamps()
   end
 
+  def auth_active?(%User{local: false}), do: true
+
+  def auth_active?(%User{info: %User.Info{confirmation_pending: false}}), do: true
+
+  def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
+    do: !Pleroma.Config.get([:instance, :account_activation_required])
+
+  def auth_active?(_), do: false
+
+  def visible_for?(user, for_user \\ nil)
+
+  def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
+
+  def visible_for?(%User{} = user, for_user) do
+    auth_active?(user) || superuser?(for_user)
+  end
+
+  def visible_for?(_, _), do: false
+
+  def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
+  def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
+  def superuser?(_), do: false
+
   def avatar_url(user) do
     case user.avatar do
       %{"url" => [%{"href" => href} | _]} -> href
@@ -36,13 +88,13 @@ defmodule Pleroma.User do
   end
 
   def banner_url(user) do
-    case user.info["banner"] do
+    case user.info.banner do
       %{"url" => [%{"href" => href} | _]} -> href
       _ -> "#{Web.base_url()}/images/banner.png"
     end
   end
 
-  def profile_url(%User{info: %{"source_data" => %{"url" => url}}}), do: url
+  def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
   def profile_url(%User{ap_id: ap_id}), do: ap_id
   def profile_url(_), do: nil
 
@@ -54,44 +106,39 @@ defmodule Pleroma.User do
     "#{ap_id(user)}/followers"
   end
 
-  def follow_changeset(struct, params \\ %{}) do
-    struct
-    |> cast(params, [:following])
-    |> validate_required([:following])
-  end
-
-  def info_changeset(struct, params \\ %{}) do
-    struct
-    |> cast(params, [:info])
-    |> validate_required([:info])
-  end
-
   def user_info(%User{} = user) do
     oneself = if user.local, do: 1, else: 0
 
     %{
       following_count: length(user.following) - oneself,
-      note_count: user.info["note_count"] || 0,
-      follower_count: user.info["follower_count"] || 0,
-      locked: user.info["locked"] || false,
-      default_scope: user.info["default_scope"] || "public"
+      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
     }
   end
 
-  @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])?)*$/
   def remote_user_creation(params) do
+    params =
+      params
+      |> Map.put(:info, params[:info] || %{})
+
+    info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
+
     changes =
       %User{}
-      |> cast(params, [:bio, :name, :ap_id, :nickname, :info, :avatar])
+      |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
       |> validate_required([:name, :ap_id])
       |> unique_constraint(:nickname)
       |> validate_format(:nickname, @email_regex)
       |> validate_length(:bio, max: 5000)
       |> validate_length(:name, max: 100)
       |> put_change(:local, false)
+      |> put_embed(:info, info_cng)
 
     if changes.valid? do
-      case changes.changes[:info]["source_data"] do
+      case info_cng.changes[:source_data] do
         %{"followers" => followers} ->
           changes
           |> put_change(:follower_address, followers)
@@ -109,9 +156,9 @@ defmodule Pleroma.User do
 
   def update_changeset(struct, params \\ %{}) do
     struct
-    |> cast(params, [:bio, :name])
+    |> cast(params, [:bio, :name, :avatar])
     |> unique_constraint(:nickname)
-    |> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
+    |> validate_format(:nickname, local_nickname_regex())
     |> validate_length(:bio, max: 5000)
     |> validate_length(:name, min: 1, max: 100)
   end
@@ -121,12 +168,17 @@ defmodule Pleroma.User do
       params
       |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
 
+    info_cng =
+      struct.info
+      |> User.Info.user_upgrade(params[:info])
+
     struct
-    |> cast(params, [:bio, :name, :info, :follower_address, :avatar, :last_refreshed_at])
+    |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
     |> unique_constraint(:nickname)
-    |> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
+    |> validate_format(:nickname, local_nickname_regex())
     |> validate_length(:bio, max: 5000)
     |> validate_length(:name, max: 100)
+    |> put_embed(:info, info_cng)
   end
 
   def password_update_changeset(struct, params) do
@@ -153,7 +205,16 @@ defmodule Pleroma.User do
     update_and_set_cache(password_update_changeset(user, data))
   end
 
-  def register_changeset(struct, params \\ %{}) do
+  def register_changeset(struct, params \\ %{}, opts \\ []) do
+    confirmation_status =
+      if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do
+        :confirmed
+      else
+        :unconfirmed
+      end
+
+    info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)
+
     changeset =
       struct
       |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
@@ -161,10 +222,12 @@ defmodule Pleroma.User do
       |> validate_confirmation(:password)
       |> unique_constraint(:email)
       |> unique_constraint(:nickname)
-      |> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
+      |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
+      |> validate_format(:nickname, local_nickname_regex())
       |> validate_format(:email, @email_regex)
       |> validate_length(:bio, max: 1000)
       |> validate_length(:name, min: 1, max: 100)
+      |> put_change(:info, info_change)
 
     if changeset.valid? do
       hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
@@ -181,6 +244,39 @@ defmodule Pleroma.User do
     end
   end
 
+  defp autofollow_users(user) do
+    candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
+
+    autofollowed_users =
+      from(u in User,
+        where: u.local == true,
+        where: u.nickname in ^candidates
+      )
+      |> Repo.all()
+
+    follow_all(user, autofollowed_users)
+  end
+
+  @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
+  def register(%Ecto.Changeset{} = changeset) do
+    with {:ok, user} <- Repo.insert(changeset),
+         {:ok, user} <- autofollow_users(user),
+         {:ok, _} <- try_send_confirmation_email(user) do
+      {:ok, user}
+    end
+  end
+
+  def try_send_confirmation_email(%User{} = user) do
+    if user.info.confirmation_pending &&
+         Pleroma.Config.get([:instance, :account_activation_required]) do
+      user
+      |> Pleroma.UserEmail.account_confirmation_email()
+      |> Pleroma.Mailer.deliver()
+    else
+      {:ok, :noop}
+    end
+  end
+
   def needs_update?(%User{local: true}), do: false
 
   def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
@@ -191,7 +287,7 @@ defmodule Pleroma.User do
 
   def needs_update?(_), do: true
 
-  def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{"locked" => true}}) do
+  def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
     {:ok, follower}
   end
 
@@ -200,14 +296,14 @@ defmodule Pleroma.User do
   end
 
   def maybe_direct_follow(%User{} = follower, %User{} = followed) do
-    if !User.ap_enabled?(followed) do
+    if not User.ap_enabled?(followed) do
       follow(follower, followed)
     else
       {:ok, follower}
     end
   end
 
-  def maybe_follow(%User{} = follower, %User{info: info} = followed) do
+  def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
     if not following?(follower, followed) do
       follow(follower, followed)
     else
@@ -215,6 +311,38 @@ defmodule Pleroma.User do
     end
   end
 
+  @doc "A mass follow for local users. Respects blocks but does not create activities."
+  @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
+  def follow_all(follower, followeds) do
+    followed_addresses =
+      followeds
+      |> Enum.reject(fn %{ap_id: ap_id} -> ap_id in follower.info.blocks end)
+      |> Enum.map(fn %{follower_address: fa} -> fa end)
+
+    q =
+      from(u in User,
+        where: u.id == ^follower.id,
+        update: [
+          set: [
+            following:
+              fragment(
+                "array(select distinct unnest (array_cat(?, ?)))",
+                u.following,
+                ^followed_addresses
+              )
+          ]
+        ]
+      )
+
+    {1, [follower]} = Repo.update_all(q, [], returning: true)
+
+    Enum.each(followeds, fn followed ->
+      update_follower_count(followed)
+    end)
+
+    set_cache(follower)
+  end
+
   def follow(%User{} = follower, %User{info: info} = followed) do
     user_config = Application.get_env(:pleroma, :user)
     deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
@@ -222,7 +350,7 @@ defmodule Pleroma.User do
     ap_followers = followed.follower_address
 
     cond do
-      following?(follower, followed) or info["deactivated"] ->
+      following?(follower, followed) or info.deactivated ->
         {:error, "Could not follow user: #{followed.nickname} is already on your list."}
 
       deny_follow_blocked and blocks?(followed, follower) ->
@@ -233,18 +361,17 @@ defmodule Pleroma.User do
           Websub.subscribe(follower, followed)
         end
 
-        following =
-          [ap_followers | follower.following]
-          |> Enum.uniq()
+        q =
+          from(u in User,
+            where: u.id == ^follower.id,
+            update: [push: [following: ^ap_followers]]
+          )
 
-        follower =
-          follower
-          |> follow_changeset(%{following: following})
-          |> update_and_set_cache
+        {1, [follower]} = Repo.update_all(q, [], returning: true)
 
         {:ok, _} = update_follower_count(followed)
 
-        follower
+        set_cache(follower)
     end
   end
 
@@ -252,41 +379,78 @@ defmodule Pleroma.User do
     ap_followers = followed.follower_address
 
     if following?(follower, followed) and follower.ap_id != followed.ap_id do
-      following =
-        follower.following
-        |> List.delete(ap_followers)
+      q =
+        from(u in User,
+          where: u.id == ^follower.id,
+          update: [pull: [following: ^ap_followers]]
+        )
 
-      {:ok, follower} =
-        follower
-        |> follow_changeset(%{following: following})
-        |> update_and_set_cache
+      {1, [follower]} = Repo.update_all(q, [], returning: true)
 
       {:ok, followed} = update_follower_count(followed)
 
+      set_cache(follower)
+
       {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
     else
       {:error, "Not subscribed!"}
     end
   end
 
+  @spec following?(User.t(), User.t()) :: boolean
   def following?(%User{} = follower, %User{} = followed) do
     Enum.member?(follower.following, followed.follower_address)
   end
 
+  def follow_import(%User{} = follower, followed_identifiers)
+      when is_list(followed_identifiers) do
+    Enum.map(
+      followed_identifiers,
+      fn followed_identifier ->
+        with %User{} = followed <- get_or_fetch(followed_identifier),
+             {:ok, follower} <- maybe_direct_follow(follower, followed),
+             {:ok, _} <- ActivityPub.follow(follower, followed) do
+          followed
+        else
+          err ->
+            Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
+            err
+        end
+      end
+    )
+  end
+
   def locked?(%User{} = user) do
-    user.info["locked"] || false
+    user.info.locked || false
+  end
+
+  def get_by_id(id) do
+    Repo.get_by(User, id: id)
   end
 
   def get_by_ap_id(ap_id) do
     Repo.get_by(User, ap_id: ap_id)
   end
 
+  # This is mostly an SPC migration fix. This guesses the user nickname (by taking the last part of the ap_id and the domain) and tries to get that user
+  def get_by_guessed_nickname(ap_id) do
+    domain = URI.parse(ap_id).host
+    name = List.last(String.split(ap_id, "/"))
+    nickname = "#{name}@#{domain}"
+
+    get_by_nickname(nickname)
+  end
+
+  def set_cache(user) do
+    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))
+    {:ok, user}
+  end
+
   def update_and_set_cache(changeset) do
     with {:ok, user} <- Repo.update(changeset) do
-      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))
-      {:ok, user}
+      set_cache(user)
     else
       e -> e
     end
@@ -303,13 +467,38 @@ defmodule Pleroma.User do
     Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
   end
 
+  def get_cached_by_id(id) do
+    key = "id:#{id}"
+
+    ap_id =
+      Cachex.fetch!(:user_cache, key, fn _ ->
+        user = get_by_id(id)
+
+        if user do
+          Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
+          {:commit, user.ap_id}
+        else
+          {:ignore, ""}
+        end
+      end)
+
+    get_cached_by_ap_id(ap_id)
+  end
+
   def get_cached_by_nickname(nickname) do
     key = "nickname:#{nickname}"
     Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
   end
 
+  def get_cached_by_nickname_or_id(nickname_or_id) do
+    get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
+  end
+
   def get_by_nickname(nickname) do
-    Repo.get_by(User, nickname: nickname)
+    Repo.get_by(User, nickname: nickname) ||
+      if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
+        Repo.get_by(User, nickname: local_nickname(nickname))
+      end
   end
 
   def get_by_nickname_or_email(nickname_or_email) do
@@ -347,7 +536,7 @@ defmodule Pleroma.User do
     end
   end
 
-  def get_followers_query(%User{id: id, follower_address: follower_address}) do
+  def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
     from(
       u in User,
       where: fragment("? <@ ?", ^[follower_address], u.following),
@@ -355,13 +544,29 @@ defmodule Pleroma.User do
     )
   end
 
-  def get_followers(user) do
-    q = get_followers_query(user)
+  def get_followers_query(user, page) do
+    from(
+      u in get_followers_query(user, nil),
+      limit: 20,
+      offset: ^((page - 1) * 20)
+    )
+  end
+
+  def get_followers_query(user), do: get_followers_query(user, nil)
+
+  def get_followers(user, page \\ nil) do
+    q = get_followers_query(user, page)
 
     {:ok, Repo.all(q)}
   end
 
-  def get_friends_query(%User{id: id, following: following}) do
+  def get_followers_ids(user, page \\ nil) do
+    q = get_followers_query(user, page)
+
+    Repo.all(from(u in q, select: u.id))
+  end
+
+  def get_friends_query(%User{id: id, following: following}, nil) do
     from(
       u in User,
       where: u.follower_address in ^following,
@@ -369,12 +574,28 @@ defmodule Pleroma.User do
     )
   end
 
-  def get_friends(user) do
-    q = get_friends_query(user)
+  def get_friends_query(user, page) do
+    from(
+      u in get_friends_query(user, nil),
+      limit: 20,
+      offset: ^((page - 1) * 20)
+    )
+  end
+
+  def get_friends_query(user), do: get_friends_query(user, nil)
+
+  def get_friends(user, page \\ nil) do
+    q = get_friends_query(user, page)
 
     {:ok, Repo.all(q)}
   end
 
+  def get_friends_ids(user, page \\ nil) do
+    q = get_friends_query(user, page)
+
+    Repo.all(from(u in q, select: u.id))
+  end
+
   def get_follow_requests_query(%User{} = user) do
     from(
       a in Activity,
@@ -405,28 +626,30 @@ defmodule Pleroma.User do
       Enum.map(reqs, fn req -> req.actor end)
       |> Enum.uniq()
       |> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end)
+      |> Enum.filter(fn u -> !is_nil(u) end)
       |> Enum.filter(fn u -> !following?(u, user) end)
 
     {:ok, users}
   end
 
   def increase_note_count(%User{} = user) do
-    note_count = (user.info["note_count"] || 0) + 1
-    new_info = Map.put(user.info, "note_count", note_count)
+    info_cng = User.Info.add_to_note_count(user.info, 1)
 
-    cs = info_changeset(user, %{info: new_info})
+    cng =
+      change(user)
+      |> put_embed(:info, info_cng)
 
-    update_and_set_cache(cs)
+    update_and_set_cache(cng)
   end
 
   def decrease_note_count(%User{} = user) do
-    note_count = user.info["note_count"] || 0
-    note_count = if note_count <= 0, do: 0, else: note_count - 1
-    new_info = Map.put(user.info, "note_count", note_count)
+    info_cng = User.Info.add_to_note_count(user.info, -1)
 
-    cs = info_changeset(user, %{info: new_info})
+    cng =
+      change(user)
+      |> put_embed(:info, info_cng)
 
-    update_and_set_cache(cs)
+    update_and_set_cache(cng)
   end
 
   def update_note_count(%User{} = user) do
@@ -439,11 +662,13 @@ defmodule Pleroma.User do
 
     note_count = Repo.one(note_count_query)
 
-    new_info = Map.put(user.info, "note_count", note_count)
+    info_cng = User.Info.set_note_count(user.info, note_count)
 
-    cs = info_changeset(user, %{info: new_info})
+    cng =
+      change(user)
+      |> put_embed(:info, info_cng)
 
-    update_and_set_cache(cs)
+    update_and_set_cache(cng)
   end
 
   def update_follower_count(%User{} = user) do
@@ -457,11 +682,15 @@ defmodule Pleroma.User do
 
     follower_count = Repo.one(follower_count_query)
 
-    new_info = Map.put(user.info, "follower_count", follower_count)
+    info_cng =
+      user.info
+      |> User.Info.set_follower_count(follower_count)
 
-    cs = info_changeset(user, %{info: new_info})
+    cng =
+      change(user)
+      |> put_embed(:info, info_cng)
 
-    update_and_set_cache(cs)
+    update_and_set_cache(cng)
   end
 
   def get_users_from_set_query(ap_ids, false) do
@@ -498,37 +727,137 @@ defmodule Pleroma.User do
     Repo.all(query)
   end
 
-  def search(query, resolve \\ false) do
-    # strip the beginning @ off if there is a query
+  def search(query, resolve \\ false, for_user \\ nil) do
+    # Strip the beginning @ off if there is a query
     query = String.trim_leading(query, "@")
 
-    if resolve do
-      User.get_or_fetch_by_nickname(query)
-    end
+    if resolve, do: User.get_or_fetch_by_nickname(query)
 
-    inner =
-      from(
-        u in User,
-        select_merge: %{
-          search_distance:
-            fragment(
-              "? <-> (? || ?)",
-              ^query,
-              u.nickname,
-              u.name
-            )
-        },
-        where: not is_nil(u.nickname)
-      )
+    fts_results = do_search(fts_search_subquery(query), for_user)
+
+    {:ok, trigram_results} =
+      Repo.transaction(fn ->
+        Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
+        do_search(trigram_search_subquery(query), for_user)
+      end)
 
+    Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
+  end
+
+  defp do_search(subquery, for_user, options \\ []) do
     q =
       from(
-        s in subquery(inner),
-        order_by: s.search_distance,
-        limit: 20
+        s in subquery(subquery),
+        order_by: [desc: s.search_rank],
+        limit: ^(options[:limit] || 20)
       )
 
-    Repo.all(q)
+    results =
+      q
+      |> Repo.all()
+      |> Enum.filter(&(&1.search_rank > 0))
+
+    boost_search_results(results, for_user)
+  end
+
+  defp fts_search_subquery(query) do
+    processed_query =
+      query
+      |> String.replace(~r/\W+/, " ")
+      |> String.trim()
+      |> String.split()
+      |> Enum.map(&(&1 <> ":*"))
+      |> Enum.join(" | ")
+
+    from(
+      u in User,
+      select_merge: %{
+        search_rank:
+          fragment(
+            """
+            ts_rank_cd(
+              setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
+              setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
+              to_tsquery('simple', ?),
+              32
+            )
+            """,
+            u.nickname,
+            u.name,
+            ^processed_query
+          )
+      },
+      where:
+        fragment(
+          """
+            (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
+            setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
+          """,
+          u.nickname,
+          u.name,
+          ^processed_query
+        )
+    )
+  end
+
+  defp trigram_search_subquery(query) do
+    from(
+      u in User,
+      select_merge: %{
+        search_rank:
+          fragment(
+            "similarity(?, trim(? || ' ' || coalesce(?, '')))",
+            ^query,
+            u.nickname,
+            u.name
+          )
+      },
+      where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^query)
+    )
+  end
+
+  defp boost_search_results(results, nil), do: results
+
+  defp boost_search_results(results, for_user) do
+    friends_ids = get_friends_ids(for_user)
+    followers_ids = get_followers_ids(for_user)
+
+    Enum.map(
+      results,
+      fn u ->
+        search_rank_coef =
+          cond do
+            u.id in friends_ids ->
+              1.2
+
+            u.id in followers_ids ->
+              1.1
+
+            true ->
+              1
+          end
+
+        Map.put(u, :search_rank, u.search_rank * search_rank_coef)
+      end
+    )
+    |> Enum.sort_by(&(-&1.search_rank))
+  end
+
+  def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
+    Enum.map(
+      blocked_identifiers,
+      fn blocked_identifier ->
+        with %User{} = blocked <- get_or_fetch(blocked_identifier),
+             {:ok, blocker} <- block(blocker, blocked),
+             {:ok, _} <- ActivityPub.block(blocker, blocked) do
+          blocked
+        else
+          err ->
+            Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
+            err
+        end
+      end
+    )
   end
 
   def block(blocker, %User{ap_id: ap_id} = blocked) do
@@ -545,12 +874,15 @@ defmodule Pleroma.User do
       unfollow(blocked, blocker)
     end
 
-    blocks = blocker.info["blocks"] || []
-    new_blocks = Enum.uniq([ap_id | blocks])
-    new_info = Map.put(blocker.info, "blocks", new_blocks)
+    info_cng =
+      blocker.info
+      |> User.Info.add_to_block(ap_id)
+
+    cng =
+      change(blocker)
+      |> put_embed(:info, info_cng)
 
-    cs = User.info_changeset(blocker, %{info: new_info})
-    update_and_set_cache(cs)
+    update_and_set_cache(cng)
   end
 
   # helper to handle the block given only an actor's AP id
@@ -558,18 +890,21 @@ defmodule Pleroma.User do
     block(blocker, User.get_by_ap_id(ap_id))
   end
 
-  def unblock(user, %{ap_id: ap_id}) do
-    blocks = user.info["blocks"] || []
-    new_blocks = List.delete(blocks, ap_id)
-    new_info = Map.put(user.info, "blocks", new_blocks)
+  def unblock(blocker, %{ap_id: ap_id}) do
+    info_cng =
+      blocker.info
+      |> User.Info.remove_from_block(ap_id)
+
+    cng =
+      change(blocker)
+      |> put_embed(:info, info_cng)
 
-    cs = User.info_changeset(user, %{info: new_info})
-    update_and_set_cache(cs)
+    update_and_set_cache(cng)
   end
 
   def blocks?(user, %{ap_id: ap_id}) do
-    blocks = user.info["blocks"] || []
-    domain_blocks = user.info["domain_blocks"] || []
+    blocks = user.info.blocks
+    domain_blocks = user.info.domain_blocks
     %{host: host} = URI.parse(ap_id)
 
     Enum.member?(blocks, ap_id) ||
@@ -578,25 +913,34 @@ defmodule Pleroma.User do
       end)
   end
 
+  def blocked_users(user),
+    do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
+
   def block_domain(user, domain) do
-    domain_blocks = user.info["domain_blocks"] || []
-    new_blocks = Enum.uniq([domain | domain_blocks])
-    new_info = Map.put(user.info, "domain_blocks", new_blocks)
+    info_cng =
+      user.info
+      |> User.Info.add_to_domain_block(domain)
+
+    cng =
+      change(user)
+      |> put_embed(:info, info_cng)
 
-    cs = User.info_changeset(user, %{info: new_info})
-    update_and_set_cache(cs)
+    update_and_set_cache(cng)
   end
 
   def unblock_domain(user, domain) do
-    blocks = user.info["domain_blocks"] || []
-    new_blocks = List.delete(blocks, domain)
-    new_info = Map.put(user.info, "domain_blocks", new_blocks)
+    info_cng =
+      user.info
+      |> User.Info.remove_from_domain_block(domain)
+
+    cng =
+      change(user)
+      |> put_embed(:info, info_cng)
 
-    cs = User.info_changeset(user, %{info: new_info})
-    update_and_set_cache(cs)
+    update_and_set_cache(cng)
   end
 
-  def local_user_query() do
+  def local_user_query do
     from(
       u in User,
       where: u.local == true,
@@ -604,7 +948,14 @@ defmodule Pleroma.User do
     )
   end
 
-  def moderator_user_query() do
+  def active_local_user_query do
+    from(
+      u in local_user_query(),
+      where: fragment("not (?->'deactivated' @> 'true')", u.info)
+    )
+  end
+
+  def moderator_user_query do
     from(
       u in User,
       where: u.local == true,
@@ -613,9 +964,13 @@ defmodule Pleroma.User do
   end
 
   def deactivate(%User{} = user, status \\ true) do
-    new_info = Map.put(user.info, "deactivated", status)
-    cs = User.info_changeset(user, %{info: new_info})
-    update_and_set_cache(cs)
+    info_cng = User.Info.set_activation_status(user.info, status)
+
+    cng =
+      change(user)
+      |> put_embed(:info, info_cng)
+
+    update_and_set_cache(cng)
   end
 
   def delete(%User{} = user) do
@@ -649,11 +1004,13 @@ defmodule Pleroma.User do
     {:ok, user}
   end
 
-  def html_filter_policy(%User{info: %{"no_rich_text" => true}}) do
+  def html_filter_policy(%User{info: %{no_rich_text: true}}) do
     Pleroma.HTML.Scrubber.TwitterText
   end
 
-  def html_filter_policy(_), do: nil
+  @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
+
+  def html_filter_policy(_), do: @default_scrubbers
 
   def get_or_fetch_by_ap_id(ap_id) do
     user = get_by_ap_id(ap_id)
@@ -683,7 +1040,7 @@ defmodule Pleroma.User do
       user
     else
       changes =
-        %User{}
+        %User{info: %User.Info{}}
         |> cast(%{}, [:ap_id, :nickname, :local])
         |> put_change(:ap_id, relay_uri)
         |> put_change(:nickname, nil)
@@ -697,10 +1054,11 @@ defmodule Pleroma.User do
 
   # AP style
   def public_key_from_info(%{
-        "source_data" => %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
+        source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
       }) do
     key =
-      :public_key.pem_decode(public_key_pem)
+      public_key_pem
+      |> :public_key.pem_decode()
       |> hd()
       |> :public_key.pem_entry_decode()
 
@@ -708,7 +1066,7 @@ defmodule Pleroma.User do
   end
 
   # OStatus Magic Key
-  def public_key_from_info(%{"magic_key" => magic_key}) do
+  def public_key_from_info(%{magic_key: magic_key}) do
     {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
   end
 
@@ -730,20 +1088,18 @@ defmodule Pleroma.User do
       |> Map.put(:name, blank?(data[:name]) || data[:nickname])
 
     cs = User.remote_user_creation(data)
+
     Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
   end
 
   def ap_enabled?(%User{local: true}), do: true
-  def ap_enabled?(%User{info: info}), do: info["ap_enabled"]
+  def ap_enabled?(%User{info: info}), do: info.ap_enabled
   def ap_enabled?(_), do: false
 
-  def get_or_fetch(uri_or_nickname) do
-    if String.starts_with?(uri_or_nickname, "http") do
-      get_or_fetch_by_ap_id(uri_or_nickname)
-    else
-      get_or_fetch_by_nickname(uri_or_nickname)
-    end
-  end
+  @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)
 
   # 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
@@ -768,4 +1124,107 @@ defmodule Pleroma.User do
         :error
     end
   end
+
+  def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
+  def parse_bio(nil, _user), do: ""
+  def parse_bio(bio, _user) when bio == "", do: bio
+
+  def parse_bio(bio, user) do
+    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)
+
+    bio
+    |> CommonUtils.format_input(mentions, tags, "text/plain", user_links: [format: :full])
+    |> Formatter.emojify(emoji)
+  end
+
+  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
+
+  def tag(nickname, tags) when is_binary(nickname),
+    do: tag(User.get_by_nickname(nickname), tags)
+
+  def tag(%User{} = user, tags),
+    do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
+
+  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
+
+  def untag(nickname, tags) when is_binary(nickname),
+    do: untag(User.get_by_nickname(nickname), tags)
+
+  def untag(%User{} = user, tags),
+    do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
+
+  defp update_tags(%User{} = user, new_tags) do
+    {:ok, updated_user} =
+      user
+      |> change(%{tags: new_tags})
+      |> Repo.update()
+
+    updated_user
+  end
+
+  def bookmark(%User{} = user, status_id) do
+    bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
+    update_bookmarks(user, bookmarks)
+  end
+
+  def unbookmark(%User{} = user, status_id) do
+    bookmarks = Enum.uniq(user.bookmarks -- [status_id])
+    update_bookmarks(user, bookmarks)
+  end
+
+  def update_bookmarks(%User{} = user, bookmarks) do
+    user
+    |> change(%{bookmarks: bookmarks})
+    |> update_and_set_cache
+  end
+
+  defp normalize_tags(tags) do
+    [tags]
+    |> List.flatten()
+    |> Enum.map(&String.downcase(&1))
+  end
+
+  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
+
+  def local_nickname(nickname_or_mention) do
+    nickname_or_mention
+    |> full_nickname()
+    |> String.split("@")
+    |> hd()
+  end
+
+  def full_nickname(nickname_or_mention),
+    do: String.trim_leading(nickname_or_mention, "@")
+
+  def error_user(ap_id) do
+    %User{
+      name: ap_id,
+      ap_id: ap_id,
+      info: %User.Info{},
+      nickname: "erroruser@example.com",
+      inserted_at: NaiveDateTime.utc_now()
+    }
+  end
 end