Merge branch 'feature/confirm-user-acc-resend-confirmation' into 'develop'
[akkoma] / lib / pleroma / user.ex
index 1e055014ef48f92ecdbaec583b443ba8ed041ac9..3010fe87fd46976784a3cd1e24229fc344869c4a 100644 (file)
@@ -13,6 +13,7 @@ defmodule Pleroma.User do
   alias Pleroma.Activity
   alias Pleroma.Conversation.Participation
   alias Pleroma.Delivery
+  alias Pleroma.FollowingRelationship
   alias Pleroma.Keys
   alias Pleroma.Notification
   alias Pleroma.Object
@@ -26,9 +27,7 @@ defmodule Pleroma.User do
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
   alias Pleroma.Web.OAuth
-  alias Pleroma.Web.OStatus
   alias Pleroma.Web.RelMe
-  alias Pleroma.Web.Websub
   alias Pleroma.Workers.BackgroundWorker
 
   require Logger
@@ -52,7 +51,6 @@ defmodule Pleroma.User do
     field(:password, :string, virtual: true)
     field(:password_confirmation, :string, virtual: true)
     field(:keys, :string)
-    field(:following, {:array, :string}, default: [])
     field(:ap_id, :string)
     field(:avatar, :map)
     field(:local, :boolean, default: true)
@@ -63,21 +61,80 @@ defmodule Pleroma.User do
     field(:tags, {:array, :string}, default: [])
     field(:last_refreshed_at, :naive_datetime_usec)
     field(:last_digest_emailed_at, :naive_datetime)
+
+    field(:banner, :map, default: %{})
+    field(:background, :map, default: %{})
+    field(:source_data, :map, default: %{})
+    field(:note_count, :integer, default: 0)
+    field(:follower_count, :integer, default: 0)
+    field(:following_count, :integer, default: 0)
+    field(:locked, :boolean, default: false)
+    field(:confirmation_pending, :boolean, default: false)
+    field(:password_reset_pending, :boolean, default: false)
+    field(:confirmation_token, :string, default: nil)
+    field(:default_scope, :string, default: "public")
+    field(:blocks, {:array, :string}, default: [])
+    field(:domain_blocks, {:array, :string}, default: [])
+    field(:mutes, {:array, :string}, default: [])
+    field(:muted_reblogs, {:array, :string}, default: [])
+    field(:muted_notifications, {:array, :string}, default: [])
+    field(:subscribers, {:array, :string}, default: [])
+    field(:deactivated, :boolean, default: false)
+    field(:no_rich_text, :boolean, default: false)
+    field(:ap_enabled, :boolean, default: false)
+    field(:is_moderator, :boolean, default: false)
+    field(:is_admin, :boolean, default: false)
+    field(:show_role, :boolean, default: true)
+    field(:settings, :map, default: nil)
+    field(:magic_key, :string, default: nil)
+    field(:uri, :string, default: nil)
+    field(:hide_followers_count, :boolean, default: false)
+    field(:hide_follows_count, :boolean, default: false)
+    field(:hide_followers, :boolean, default: false)
+    field(:hide_follows, :boolean, default: false)
+    field(:hide_favorites, :boolean, default: true)
+    field(:unread_conversation_count, :integer, default: 0)
+    field(:pinned_activities, {:array, :string}, default: [])
+    field(:email_notifications, :map, default: %{"digest" => false})
+    field(:mascot, :map, default: nil)
+    field(:emoji, {:array, :map}, default: [])
+    field(:pleroma_settings_store, :map, default: %{})
+    field(:fields, {:array, :map}, default: [])
+    field(:raw_fields, {:array, :map}, default: [])
+    field(:discoverable, :boolean, default: false)
+    field(:invisible, :boolean, default: false)
+    field(:skip_thread_containment, :boolean, default: false)
+
+    field(:notification_settings, :map,
+      default: %{
+        "followers" => true,
+        "follows" => true,
+        "non_follows" => true,
+        "non_followers" => true
+      }
+    )
+
     has_many(:notifications, Notification)
     has_many(:registrations, Registration)
     has_many(:deliveries, Delivery)
-    embeds_one(:info, User.Info)
+
+    field(:info, :map, default: %{})
 
     timestamps()
   end
 
-  def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
+  @doc "Returns if the user should be allowed to authenticate"
+  def auth_active?(%User{deactivated: true}), do: false
+
+  def auth_active?(%User{confirmation_pending: true}),
     do: !Pleroma.Config.get([:instance, :account_activation_required])
 
   def auth_active?(%User{}), do: true
 
   def visible_for?(user, for_user \\ nil)
 
+  def visible_for?(%User{invisible: true}, _), do: false
+
   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
@@ -86,10 +143,13 @@ defmodule Pleroma.User do
 
   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?(%User{local: true, is_admin: true}), do: true
+  def superuser?(%User{local: true, is_moderator: true}), do: true
   def superuser?(_), do: false
 
+  def invisible?(%User{invisible: true}), do: true
+  def invisible?(_), do: false
+
   def avatar_url(user, options \\ []) do
     case user.avatar do
       %{"url" => [%{"href" => href} | _]} -> href
@@ -98,13 +158,13 @@ defmodule Pleroma.User do
   end
 
   def banner_url(user, options \\ []) do
-    case user.info.banner do
+    case user.banner do
       %{"url" => [%{"href" => href} | _]} -> href
       _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
     end
   end
 
-  def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
+  def profile_url(%User{source_data: %{"url" => url}}), do: url
   def profile_url(%User{ap_id: ap_id}), do: ap_id
   def profile_url(_), do: nil
 
@@ -118,19 +178,17 @@ defmodule Pleroma.User do
   def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
 
   def user_info(%User{} = user, args \\ %{}) do
-    following_count =
-      Map.get(args, :following_count, user.info.following_count || following_count(user))
-
-    follower_count = Map.get(args, :follower_count, user.info.follower_count)
+    following_count = Map.get(args, :following_count, user.following_count)
+    follower_count = Map.get(args, :follower_count, user.follower_count)
 
     %{
-      note_count: user.info.note_count,
-      locked: user.info.locked,
-      confirmation_pending: user.info.confirmation_pending,
-      default_scope: user.info.default_scope
+      note_count: user.note_count,
+      locked: user.locked,
+      confirmation_pending: user.confirmation_pending,
+      default_scope: user.default_scope,
+      follower_count: follower_count,
+      following_count: following_count
     }
-    |> Map.put(:following_count, following_count)
-    |> Map.put(:follower_count, follower_count)
   end
 
   def follow_state(%User{} = user, %User{} = target) do
@@ -157,17 +215,17 @@ defmodule Pleroma.User do
 
   @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
   def restrict_deactivated(query) do
-    from(u in query,
-      where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
-    )
+    from(u in query, where: u.deactivated != ^true)
   end
 
-  def following_count(%User{following: []}), do: 0
+  defdelegate following_count(user), to: FollowingRelationship
 
-  def following_count(%User{} = user) do
-    user
-    |> get_friends_query()
-    |> Repo.aggregate(:count, :id)
+  defp truncate_fields_param(params) do
+    if Map.has_key?(params, :fields) do
+      Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
+    else
+      params
+    end
   end
 
   defp truncate_if_exists(params, key, max_length) do
@@ -188,18 +246,43 @@ defmodule Pleroma.User do
       |> Map.put(:info, params[:info] || %{})
       |> truncate_if_exists(:name, name_limit)
       |> truncate_if_exists(:bio, bio_limit)
+      |> truncate_fields_param()
 
     changeset =
       %User{local: false}
-      |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
+      |> cast(
+        params,
+        [
+          :bio,
+          :name,
+          :ap_id,
+          :nickname,
+          :avatar,
+          :ap_enabled,
+          :source_data,
+          :banner,
+          :locked,
+          :magic_key,
+          :uri,
+          :hide_followers,
+          :hide_follows,
+          :hide_followers_count,
+          :hide_follows_count,
+          :follower_count,
+          :fields,
+          :following_count,
+          :discoverable,
+          :invisible
+        ]
+      )
       |> validate_required([:name, :ap_id])
       |> unique_constraint(:nickname)
       |> validate_format(:nickname, @email_regex)
       |> validate_length(:bio, max: bio_limit)
       |> validate_length(:name, max: name_limit)
-      |> change_info(&User.Info.remote_user_creation(&1, params[:info]))
+      |> validate_fields(true)
 
-    case params[:info][:source_data] do
+    case params[:source_data] do
       %{"followers" => followers, "following" => following} ->
         changeset
         |> put_change(:follower_address, followers)
@@ -216,11 +299,35 @@ defmodule Pleroma.User do
     name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
 
     struct
-    |> cast(params, [:bio, :name, :avatar, :following])
+    |> cast(
+      params,
+      [
+        :bio,
+        :name,
+        :avatar,
+        :locked,
+        :no_rich_text,
+        :default_scope,
+        :banner,
+        :hide_follows,
+        :hide_followers,
+        :hide_followers_count,
+        :hide_follows_count,
+        :hide_favorites,
+        :background,
+        :show_role,
+        :skip_thread_containment,
+        :fields,
+        :raw_fields,
+        :pleroma_settings_store,
+        :discoverable
+      ]
+    )
     |> unique_constraint(:nickname)
     |> validate_format(:nickname, local_nickname_regex())
     |> validate_length(:bio, max: bio_limit)
     |> validate_length(:name, min: 1, max: name_limit)
+    |> validate_fields(false)
   end
 
   def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
@@ -229,20 +336,38 @@ defmodule Pleroma.User do
 
     params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
 
+    params = if remote?, do: truncate_fields_param(params), else: params
+
     struct
-    |> cast(params, [
-      :bio,
-      :name,
-      :follower_address,
-      :following_address,
-      :avatar,
-      :last_refreshed_at
-    ])
+    |> cast(
+      params,
+      [
+        :bio,
+        :name,
+        :follower_address,
+        :following_address,
+        :avatar,
+        :last_refreshed_at,
+        :ap_enabled,
+        :source_data,
+        :banner,
+        :locked,
+        :magic_key,
+        :follower_count,
+        :following_count,
+        :hide_follows,
+        :fields,
+        :hide_followers,
+        :discoverable,
+        :hide_followers_count,
+        :hide_follows_count
+      ]
+    )
     |> unique_constraint(:nickname)
     |> validate_format(:nickname, local_nickname_regex())
     |> validate_length(:bio, max: bio_limit)
     |> validate_length(:name, max: name_limit)
-    |> change_info(&User.Info.user_upgrade(&1, params[:info], remote?))
+    |> validate_fields(remote?)
   end
 
   def password_update_changeset(struct, params) do
@@ -250,8 +375,8 @@ defmodule Pleroma.User do
     |> cast(params, [:password, :password_confirmation])
     |> validate_required([:password, :password_confirmation])
     |> validate_confirmation(:password)
-    |> put_password_hash
-    |> put_embed(:info, User.Info.set_password_reset_pending(struct.info, false))
+    |> put_password_hash()
+    |> put_change(:password_reset_pending, false)
   end
 
   @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
@@ -268,19 +393,19 @@ defmodule Pleroma.User do
     end
   end
 
+  def update_password_reset_pending(user, value) do
+    user
+    |> change()
+    |> put_change(:password_reset_pending, value)
+    |> update_and_set_cache()
+  end
+
   def force_password_reset_async(user) do
     BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
   end
 
   @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
-  def force_password_reset(user) do
-    info_cng = User.Info.set_password_reset_pending(user.info, true)
-
-    user
-    |> change()
-    |> put_embed(:info, info_cng)
-    |> update_and_set_cache()
-  end
+  def force_password_reset(user), do: update_password_reset_pending(user, true)
 
   def register_changeset(struct, params \\ %{}, opts \\ []) do
     bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
@@ -294,6 +419,7 @@ defmodule Pleroma.User do
       end
 
     struct
+    |> confirmation_changeset(need_confirmation: need_confirmation?)
     |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
     |> validate_required([:name, :nickname, :password, :password_confirmation])
     |> validate_confirmation(:password)
@@ -304,7 +430,6 @@ defmodule Pleroma.User do
     |> validate_format(:email, @email_regex)
     |> validate_length(:bio, max: bio_limit)
     |> validate_length(:name, min: 1, max: name_limit)
-    |> change_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
     |> maybe_validate_required_email(opts[:external])
     |> put_password_hash
     |> put_ap_id()
@@ -324,7 +449,6 @@ defmodule Pleroma.User do
     followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
 
     changeset
-    |> put_change(:following, [followers])
     |> put_change(:follower_address, followers)
   end
 
@@ -355,7 +479,7 @@ defmodule Pleroma.User do
   end
 
   def try_send_confirmation_email(%User{} = user) do
-    if user.info.confirmation_pending &&
+    if user.confirmation_pending &&
          Pleroma.Config.get([:instance, :account_activation_required]) do
       user
       |> Pleroma.Emails.UserEmail.account_confirmation_email()
@@ -367,6 +491,10 @@ defmodule Pleroma.User do
     end
   end
 
+  def try_send_confirmation_email(users) do
+    Enum.each(users, &try_send_confirmation_email/1)
+  end
+
   def needs_update?(%User{local: true}), do: false
 
   def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
@@ -378,8 +506,8 @@ defmodule Pleroma.User do
   def needs_update?(_), do: true
 
   @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
-  def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
-    {:ok, follower}
+  def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
+    follow(follower, followed, "pending")
   end
 
   def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
@@ -397,87 +525,46 @@ defmodule Pleroma.User do
   @doc "A mass follow for local users. Respects blocks in both directions 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 followed -> blocks?(follower, followed) || blocks?(followed, follower) 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
-              )
-          ]
-        ],
-        select: u
-      )
-
-    {1, [follower]} = Repo.update_all(q, [])
-
-    Enum.each(followeds, &update_follower_count/1)
+    followeds
+    |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
+    |> Enum.each(&follow(follower, &1, "accept"))
 
     set_cache(follower)
   end
 
-  def follow(%User{} = follower, %User{info: info} = followed) do
+  defdelegate following(user), to: FollowingRelationship
+
+  def follow(%User{} = follower, %User{} = followed, state \\ "accept") do
     deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
-    ap_followers = followed.follower_address
 
     cond do
-      info.deactivated ->
-        {:error, "Could not follow user: You are deactivated."}
+      followed.deactivated ->
+        {:error, "Could not follow user: #{followed.nickname} is deactivated."}
 
       deny_follow_blocked and blocks?(followed, follower) ->
         {:error, "Could not follow user: #{followed.nickname} blocked you."}
 
       true ->
-        benchmark? = Pleroma.Config.get([:env]) == :benchmark
-
-        if !followed.local && follower.local && !ap_enabled?(followed) && !benchmark? do
-          Websub.subscribe(follower, followed)
-        end
-
-        q =
-          from(u in User,
-            where: u.id == ^follower.id,
-            update: [push: [following: ^ap_followers]],
-            select: u
-          )
-
-        {1, [follower]} = Repo.update_all(q, [])
-
-        follower = maybe_update_following_count(follower)
+        FollowingRelationship.follow(follower, followed, state)
 
         {:ok, _} = update_follower_count(followed)
 
-        set_cache(follower)
+        follower
+        |> update_following_count()
+        |> set_cache()
     end
   end
 
   def unfollow(%User{} = follower, %User{} = followed) do
-    ap_followers = followed.follower_address
-
     if following?(follower, followed) and follower.ap_id != followed.ap_id do
-      q =
-        from(u in User,
-          where: u.id == ^follower.id,
-          update: [pull: [following: ^ap_followers]],
-          select: u
-        )
-
-      {1, [follower]} = Repo.update_all(q, [])
-
-      follower = maybe_update_following_count(follower)
+      FollowingRelationship.unfollow(follower, followed)
 
       {:ok, followed} = update_follower_count(followed)
 
-      set_cache(follower)
+      {:ok, follower} =
+        follower
+        |> update_following_count()
+        |> set_cache()
 
       {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
     else
@@ -485,13 +572,10 @@ defmodule Pleroma.User do
     end
   end
 
-  @spec following?(User.t(), User.t()) :: boolean
-  def following?(%User{} = follower, %User{} = followed) do
-    Enum.member?(follower.following, followed.follower_address)
-  end
+  defdelegate following?(follower, followed), to: FollowingRelationship
 
   def locked?(%User{} = user) do
-    user.info.locked || false
+    user.locked || false
   end
 
   def get_by_id(id) do
@@ -534,6 +618,12 @@ defmodule Pleroma.User do
     {:ok, user}
   end
 
+  def update_and_set_cache(struct, params) do
+    struct
+    |> update_changeset(params)
+    |> update_and_set_cache()
+  end
+
   def update_and_set_cache(changeset) do
     with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
       set_cache(user)
@@ -616,12 +706,7 @@ defmodule Pleroma.User do
     Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
   end
 
-  def fetch_by_nickname(nickname) do
-    case ActivityPub.make_user_from_nickname(nickname) do
-      {:ok, user} -> {:ok, user}
-      _ -> OStatus.make_user(nickname)
-    end
-  end
+  def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
 
   def get_or_fetch_by_nickname(nickname) do
     with %User{} = user <- get_by_nickname(nickname) do
@@ -709,30 +794,12 @@ defmodule Pleroma.User do
     |> Repo.all()
   end
 
-  @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
-  def get_follow_requests(%User{} = user) do
-    user
-    |> Activity.follow_requests_for_actor()
-    |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
-    |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
-    |> group_by([a, u], u.id)
-    |> select([a, u], u)
-    |> Repo.all()
-  end
+  defdelegate get_follow_requests(user), to: FollowingRelationship
 
   def increase_note_count(%User{} = user) do
     User
     |> where(id: ^user.id)
-    |> update([u],
-      set: [
-        info:
-          fragment(
-            "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
-            u.info,
-            u.info
-          )
-      ]
-    )
+    |> update([u], inc: [note_count: 1])
     |> select([u], u)
     |> Repo.update_all([])
     |> case do
@@ -746,12 +813,7 @@ defmodule Pleroma.User do
     |> where(id: ^user.id)
     |> update([u],
       set: [
-        info:
-          fragment(
-            "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
-            u.info,
-            u.info
-          )
+        note_count: fragment("greatest(0, note_count - 1)")
       ]
     )
     |> select([u], u)
@@ -762,28 +824,18 @@ defmodule Pleroma.User do
     end
   end
 
-  def update_note_count(%User{} = user) do
+  def update_note_count(%User{} = user, note_count \\ nil) do
     note_count =
-      from(
-        a in Object,
-        where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
-        select: count(a.id)
-      )
-      |> Repo.one()
-
-    update_info(user, &User.Info.set_note_count(&1, note_count))
-  end
-
-  def update_mascot(user, url) do
-    info_changeset =
-      User.Info.mascot_update(
-        user.info,
-        url
-      )
+      note_count ||
+        from(
+          a in Object,
+          where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
+          select: count(a.id)
+        )
+        |> Repo.one()
 
     user
-    |> change()
-    |> put_embed(:info, info_changeset)
+    |> cast(%{note_count: note_count}, [:note_count])
     |> update_and_set_cache()
   end
 
@@ -801,10 +853,24 @@ defmodule Pleroma.User do
 
   def fetch_follow_information(user) do
     with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
-      update_info(user, &User.Info.follow_information_update(&1, info))
+      user
+      |> follow_information_changeset(info)
+      |> update_and_set_cache()
     end
   end
 
+  defp follow_information_changeset(user, params) do
+    user
+    |> cast(params, [
+      :hide_followers,
+      :hide_follows,
+      :follower_count,
+      :following_count,
+      :hide_followers_count,
+      :hide_follows_count
+    ])
+  end
+
   def update_follower_count(%User{} = user) do
     if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
       follower_count_query =
@@ -815,14 +881,7 @@ defmodule Pleroma.User do
       |> where(id: ^user.id)
       |> join(:inner, [u], s in subquery(follower_count_query))
       |> update([u, s],
-        set: [
-          info:
-            fragment(
-              "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
-              u.info,
-              s.count
-            )
-        ]
+        set: [follower_count: s.count]
       )
       |> select([u], u)
       |> Repo.update_all([])
@@ -835,8 +894,8 @@ defmodule Pleroma.User do
     end
   end
 
-  @spec maybe_update_following_count(User.t()) :: User.t()
-  def maybe_update_following_count(%User{local: false} = user) do
+  @spec update_following_count(User.t()) :: User.t()
+  def update_following_count(%User{local: false} = user) do
     if Pleroma.Config.get([:instance, :external_user_synchronization]) do
       maybe_fetch_follow_information(user)
     else
@@ -844,7 +903,13 @@ defmodule Pleroma.User do
     end
   end
 
-  def maybe_update_following_count(user), do: user
+  def update_following_count(%User{local: true} = user) do
+    following_count = FollowingRelationship.following_count(user)
+
+    user
+    |> follow_information_changeset(%{following_count: following_count})
+    |> Repo.update!()
+  end
 
   def set_unread_conversation_count(%User{local: true} = user) do
     unread_query = Participation.unread_conversation_count_for_user(user)
@@ -852,14 +917,7 @@ defmodule Pleroma.User do
     User
     |> join(:inner, [u], p in subquery(unread_query))
     |> update([u, p],
-      set: [
-        info:
-          fragment(
-            "jsonb_set(?, '{unread_conversation_count}', ?::varchar::jsonb, true)",
-            u.info,
-            p.count
-          )
-      ]
+      set: [unread_conversation_count: p.count]
     )
     |> where([u], u.id == ^user.id)
     |> select([u], u)
@@ -870,7 +928,7 @@ defmodule Pleroma.User do
     end
   end
 
-  def set_unread_conversation_count(_), do: :noop
+  def set_unread_conversation_count(user), do: {:ok, user}
 
   def increment_unread_conversation_count(conversation, %User{local: true} = user) do
     unread_query =
@@ -880,14 +938,7 @@ defmodule Pleroma.User do
     User
     |> join(:inner, [u], p in subquery(unread_query))
     |> update([u, p],
-      set: [
-        info:
-          fragment(
-            "jsonb_set(?, '{unread_conversation_count}', (coalesce((?->>'unread_conversation_count')::int, 0) + 1)::varchar::jsonb, true)",
-            u.info,
-            u.info
-          )
-      ]
+      inc: [unread_conversation_count: 1]
     )
     |> where([u], u.id == ^user.id)
     |> where([u, p], p.count == 0)
@@ -899,19 +950,7 @@ defmodule Pleroma.User do
     end
   end
 
-  def increment_unread_conversation_count(_, _), do: :noop
-
-  def remove_duplicated_following(%User{following: following} = user) do
-    uniq_following = Enum.uniq(following)
-
-    if length(following) == length(uniq_following) do
-      {:ok, user}
-    else
-      user
-      |> update_changeset(%{following: uniq_following})
-      |> update_and_set_cache()
-    end
-  end
+  def increment_unread_conversation_count(_, user), do: {:ok, user}
 
   @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
   def get_users_from_set(ap_ids, local_only \\ true) do
@@ -930,11 +969,11 @@ defmodule Pleroma.User do
 
   @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
   def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
-    update_info(muter, &User.Info.add_to_mutes(&1, ap_id, notifications?))
+    add_to_mutes(muter, ap_id, notifications?)
   end
 
   def unmute(muter, %{ap_id: ap_id}) do
-    update_info(muter, &User.Info.remove_from_mutes(&1, ap_id))
+    remove_from_mutes(muter, ap_id)
   end
 
   def subscribe(subscriber, %{ap_id: ap_id}) do
@@ -944,14 +983,14 @@ defmodule Pleroma.User do
       if blocks?(subscribed, subscriber) and deny_follow_blocked do
         {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
       else
-        update_info(subscribed, &User.Info.add_to_subscribers(&1, subscriber.ap_id))
+        User.add_to_subscribers(subscribed, subscriber.ap_id)
       end
     end
   end
 
   def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
     with %User{} = user <- get_cached_by_ap_id(ap_id) do
-      update_info(user, &User.Info.remove_from_subscribers(&1, unsubscriber.ap_id))
+      User.remove_from_subscribers(user, unsubscriber.ap_id)
     end
   end
 
@@ -983,8 +1022,8 @@ defmodule Pleroma.User do
     if following?(blocked, blocker), do: unfollow(blocked, blocker)
 
     {:ok, blocker} = update_follower_count(blocker)
-
-    update_info(blocker, &User.Info.add_to_block(&1, ap_id))
+    {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
+    add_to_block(blocker, ap_id)
   end
 
   # helper to handle the block given only an actor's AP id
@@ -993,17 +1032,17 @@ defmodule Pleroma.User do
   end
 
   def unblock(blocker, %{ap_id: ap_id}) do
-    update_info(blocker, &User.Info.remove_from_block(&1, ap_id))
+    remove_from_block(blocker, ap_id)
   end
 
   def mutes?(nil, _), do: false
-  def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
+  def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.mutes, ap_id)
 
   @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
   def muted_notifications?(nil, _), do: false
 
   def muted_notifications?(user, %{ap_id: ap_id}),
-    do: Enum.member?(user.info.muted_notifications, ap_id)
+    do: Enum.member?(user.muted_notifications, ap_id)
 
   def blocks?(%User{} = user, %User{} = target) do
     blocks_ap_id?(user, target) || blocks_domain?(user, target)
@@ -1012,13 +1051,13 @@ defmodule Pleroma.User do
   def blocks?(nil, _), do: false
 
   def blocks_ap_id?(%User{} = user, %User{} = target) do
-    Enum.member?(user.info.blocks, target.ap_id)
+    Enum.member?(user.blocks, target.ap_id)
   end
 
   def blocks_ap_id?(_, _), do: false
 
   def blocks_domain?(%User{} = user, %User{} = target) do
-    domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
+    domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
     %{host: host} = URI.parse(target.ap_id)
     Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
   end
@@ -1027,51 +1066,80 @@ defmodule Pleroma.User do
 
   def subscribed_to?(user, %{ap_id: ap_id}) do
     with %User{} = target <- get_cached_by_ap_id(ap_id) do
-      Enum.member?(target.info.subscribers, user.ap_id)
+      Enum.member?(target.subscribers, user.ap_id)
     end
   end
 
   @spec muted_users(User.t()) :: [User.t()]
   def muted_users(user) do
-    User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
+    User.Query.build(%{ap_id: user.mutes, deactivated: false})
     |> Repo.all()
   end
 
   @spec blocked_users(User.t()) :: [User.t()]
   def blocked_users(user) do
-    User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
+    User.Query.build(%{ap_id: user.blocks, deactivated: false})
     |> Repo.all()
   end
 
   @spec subscribers(User.t()) :: [User.t()]
   def subscribers(user) do
-    User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
+    User.Query.build(%{ap_id: user.subscribers, deactivated: false})
     |> Repo.all()
   end
 
-  def block_domain(user, domain) do
-    update_info(user, &User.Info.add_to_domain_block(&1, domain))
+  def deactivate_async(user, status \\ true) do
+    BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
   end
 
-  def unblock_domain(user, domain) do
-    update_info(user, &User.Info.remove_from_domain_block(&1, domain))
-  end
+  def deactivate(user, status \\ true)
 
-  def deactivate_async(user, status \\ true) do
-    BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
+  def deactivate(users, status) when is_list(users) do
+    Repo.transaction(fn ->
+      for user <- users, do: deactivate(user, status)
+    end)
   end
 
-  def deactivate(%User{} = user, status \\ true) do
-    with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
-      Enum.each(get_followers(user), &invalidate_cache/1)
-      Enum.each(get_friends(user), &update_follower_count/1)
+  def deactivate(%User{} = user, status) do
+    with {:ok, user} <- set_activation_status(user, status) do
+      user
+      |> get_followers()
+      |> Enum.filter(& &1.local)
+      |> Enum.each(fn follower ->
+        follower |> update_following_count() |> set_cache()
+      end)
+
+      # Only update local user counts, remote will be update during the next pull.
+      user
+      |> get_friends()
+      |> Enum.filter(& &1.local)
+      |> Enum.each(&update_follower_count/1)
 
       {:ok, user}
     end
   end
 
-  def update_notification_settings(%User{} = user, settings \\ %{}) do
-    update_info(user, &User.Info.update_notification_settings(&1, settings))
+  def update_notification_settings(%User{} = user, settings) do
+    settings =
+      settings
+      |> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end)
+      |> Map.new()
+
+    notification_settings =
+      user.notification_settings
+      |> Map.merge(settings)
+      |> Map.take(["followers", "follows", "non_follows", "non_followers"])
+
+    params = %{notification_settings: notification_settings}
+
+    user
+    |> cast(params, [:notification_settings])
+    |> validate_required([:notification_settings])
+    |> update_and_set_cache()
+  end
+
+  def delete(users) when is_list(users) do
+    for user <- users, do: delete(user)
   end
 
   def delete(%User{} = user) do
@@ -1109,7 +1177,7 @@ defmodule Pleroma.User do
     pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
 
     # Insert all the posts in reverse order, so they're in the right order on the timeline
-    user.info.source_data["outbox"]
+    user.source_data["outbox"]
     |> Utils.fetch_ordered_collection(pages)
     |> Enum.reverse()
     |> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
@@ -1230,24 +1298,13 @@ defmodule Pleroma.User do
 
   defp delete_activity(_activity), do: "Doing nothing"
 
-  def html_filter_policy(%User{info: %{no_rich_text: true}}) do
+  def html_filter_policy(%User{no_rich_text: true}) do
     Pleroma.HTML.Scrubber.TwitterText
   end
 
   def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
 
-  def fetch_by_ap_id(ap_id) do
-    case ActivityPub.make_user_from_ap_id(ap_id) do
-      {:ok, user} ->
-        {:ok, user}
-
-      _ ->
-        case OStatus.make_user(ap_id) do
-          {:ok, user} -> {:ok, user}
-          _ -> {:error, "Could not fetch by AP id"}
-        end
-    end
-  end
+  def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
 
   def get_or_fetch_by_ap_id(ap_id) do
     user = get_cached_by_ap_id(ap_id)
@@ -1270,29 +1327,28 @@ defmodule Pleroma.User do
     end
   end
 
-  @doc "Creates an internal service actor by URI if missing.  Optionally takes nickname for addressing."
+  @doc """
+  Creates an internal service actor by URI if missing.
+  Optionally takes nickname for addressing.
+  """
   def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
-    with %User{} = user <- get_cached_by_ap_id(uri) do
-      user
-    else
-      _ ->
-        {:ok, user} =
-          %User{info: %User.Info{}}
-          |> cast(%{}, [:ap_id, :nickname, :local])
-          |> put_change(:ap_id, uri)
-          |> put_change(:nickname, nickname)
-          |> put_change(:local, true)
-          |> put_change(:follower_address, uri <> "/followers")
-          |> Repo.insert()
+    with user when is_nil(user) <- get_cached_by_ap_id(uri) do
+      {:ok, user} =
+        %User{
+          invisible: true,
+          local: true,
+          ap_id: uri,
+          nickname: nickname,
+          follower_address: uri <> "/followers"
+        }
+        |> Repo.insert()
 
-        user
+      user
     end
   end
 
   # AP style
-  def public_key_from_info(%{
-        source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
-      }) do
+  def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
     key =
       public_key_pem
       |> :public_key.pem_decode()
@@ -1302,16 +1358,11 @@ defmodule Pleroma.User do
     {:ok, key}
   end
 
-  # OStatus Magic Key
-  def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
-    {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
-  end
-
-  def public_key_from_info(_), do: {:error, "not found key"}
+  def public_key(_), do: {:error, "not found key"}
 
   def get_public_key_for_ap_id(ap_id) do
     with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
-         {:ok, public_key} <- public_key_from_info(user.info) do
+         {:ok, public_key} <- public_key(user) do
       {:ok, public_key}
     else
       _ -> :error
@@ -1330,7 +1381,7 @@ defmodule Pleroma.User do
   end
 
   def ap_enabled?(%User{local: true}), do: true
-  def ap_enabled?(%User{info: info}), do: info.ap_enabled
+  def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
   def ap_enabled?(_), do: false
 
   @doc "Gets or fetch a user by uri or nickname."
@@ -1443,7 +1494,6 @@ defmodule Pleroma.User do
     %User{
       name: ap_id,
       ap_id: ap_id,
-      info: %User.Info{},
       nickname: "erroruser@example.com",
       inserted_at: NaiveDateTime.utc_now()
     }
@@ -1456,7 +1506,7 @@ defmodule Pleroma.User do
   end
 
   def showing_reblogs?(%User{} = user, %User{} = target) do
-    target.ap_id not in user.info.muted_reblogs
+    target.ap_id not in user.muted_reblogs
   end
 
   @doc """
@@ -1488,7 +1538,7 @@ defmodule Pleroma.User do
       left_join: a in Pleroma.Activity,
       on: u.ap_id == a.actor,
       where: not is_nil(u.nickname),
-      where: fragment("not (?->'deactivated' @> 'true')", u.info),
+      where: u.deactivated != ^true,
       where: u.id not in ^has_read_notifications,
       group_by: u.id,
       having:
@@ -1502,16 +1552,16 @@ defmodule Pleroma.User do
 
   ## Examples
 
-      iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
-      Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
+      iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
+      Pleroma.User{email_notifications: %{"digest" => true}}
 
-      iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
-      Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
+      iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
+      Pleroma.User{email_notifications: %{"digest" => false}}
   """
   @spec switch_email_notifications(t(), String.t(), boolean()) ::
           {:ok, t()} | {:error, Ecto.Changeset.t()}
   def switch_email_notifications(user, type, status) do
-    update_info(user, &User.Info.update_email_notifications(&1, %{type => status}))
+    User.update_email_notifications(user, %{type => status})
   end
 
   @doc """
@@ -1531,17 +1581,21 @@ defmodule Pleroma.User do
 
   @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
   def toggle_confirmation(%User{} = user) do
-    need_confirmation? = !user.info.confirmation_pending
-
     user
-    |> update_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
+    |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
+    |> update_and_set_cache()
+  end
+
+  @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
+  def toggle_confirmation(users) do
+    Enum.map(users, &toggle_confirmation/1)
   end
 
-  def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
+  def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
     mascot
   end
 
-  def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
+  def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
     # use instance-default
     config = Pleroma.Config.get([:assets, :mascots])
     default_mascot = Pleroma.Config.get([:assets, :default_mascot])
@@ -1611,25 +1665,285 @@ defmodule Pleroma.User do
     |> update_and_set_cache()
   end
 
-  @doc """
-  Changes `user.info` and returns the user changeset.
+  # Internal function; public one is `deactivate/2`
+  defp set_activation_status(user, deactivated) do
+    user
+    |> cast(%{deactivated: deactivated}, [:deactivated])
+    |> update_and_set_cache()
+  end
 
-  `fun` is called with the `user.info`.
-  """
-  def change_info(user, fun) do
-    changeset = change(user)
-    info = get_field(changeset, :info) || %User.Info{}
-    put_embed(changeset, :info, fun.(info))
+  def update_banner(user, banner) do
+    user
+    |> cast(%{banner: banner}, [:banner])
+    |> update_and_set_cache()
   end
 
-  @doc """
-  Updates `user.info` and sets cache.
+  def update_background(user, background) do
+    user
+    |> cast(%{background: background}, [:background])
+    |> update_and_set_cache()
+  end
+
+  def update_source_data(user, source_data) do
+    user
+    |> cast(%{source_data: source_data}, [:source_data])
+    |> update_and_set_cache()
+  end
+
+  def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
+    %{
+      admin: is_admin,
+      moderator: is_moderator
+    }
+  end
+
+  # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
+  # For example: [{"name": "Pronoun", "value": "she/her"}, …]
+  def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
+    limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
+
+    attachment
+    |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
+    |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
+    |> Enum.take(limit)
+  end
+
+  def fields(%{fields: nil}), do: []
+
+  def fields(%{fields: fields}), do: fields
+
+  def validate_fields(changeset, remote? \\ false) do
+    limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
+    limit = Pleroma.Config.get([:instance, limit_name], 0)
+
+    changeset
+    |> validate_length(:fields, max: limit)
+    |> validate_change(:fields, fn :fields, fields ->
+      if Enum.all?(fields, &valid_field?/1) do
+        []
+      else
+        [fields: "invalid"]
+      end
+    end)
+  end
+
+  defp valid_field?(%{"name" => name, "value" => value}) do
+    name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
+    value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
+
+    is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
+      String.length(value) <= value_limit
+  end
+
+  defp valid_field?(_), do: false
+
+  defp truncate_field(%{"name" => name, "value" => value}) do
+    {name, _chopped} =
+      String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
+
+    {value, _chopped} =
+      String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
+
+    %{"name" => name, "value" => value}
+  end
+
+  def admin_api_update(user, params) do
+    user
+    |> cast(params, [
+      :is_moderator,
+      :is_admin,
+      :show_role
+    ])
+    |> update_and_set_cache()
+  end
+
+  def mascot_update(user, url) do
+    user
+    |> cast(%{mascot: url}, [:mascot])
+    |> validate_required([:mascot])
+    |> update_and_set_cache()
+  end
+
+  def mastodon_settings_update(user, settings) do
+    user
+    |> cast(%{settings: settings}, [:settings])
+    |> validate_required([:settings])
+    |> update_and_set_cache()
+  end
+
+  @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
+  def confirmation_changeset(user, need_confirmation: need_confirmation?) do
+    params =
+      if need_confirmation? do
+        %{
+          confirmation_pending: true,
+          confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
+        }
+      else
+        %{
+          confirmation_pending: false,
+          confirmation_token: nil
+        }
+      end
+
+    cast(user, params, [:confirmation_pending, :confirmation_token])
+  end
+
+  def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
+    if id not in user.pinned_activities do
+      max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
+      params = %{pinned_activities: user.pinned_activities ++ [id]}
+
+      user
+      |> cast(params, [:pinned_activities])
+      |> validate_length(:pinned_activities,
+        max: max_pinned_statuses,
+        message: "You have already pinned the maximum number of statuses"
+      )
+    else
+      change(user)
+    end
+    |> update_and_set_cache()
+  end
+
+  def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
+    params = %{pinned_activities: List.delete(user.pinned_activities, id)}
+
+    user
+    |> cast(params, [:pinned_activities])
+    |> update_and_set_cache()
+  end
+
+  def update_email_notifications(user, settings) do
+    email_notifications =
+      user.email_notifications
+      |> Map.merge(settings)
+      |> Map.take(["digest"])
+
+    params = %{email_notifications: email_notifications}
+    fields = [:email_notifications]
+
+    user
+    |> cast(params, fields)
+    |> validate_required(fields)
+    |> update_and_set_cache()
+  end
+
+  defp set_subscribers(user, subscribers) do
+    params = %{subscribers: subscribers}
+
+    user
+    |> cast(params, [:subscribers])
+    |> validate_required([:subscribers])
+    |> update_and_set_cache()
+  end
+
+  def add_to_subscribers(user, subscribed) do
+    set_subscribers(user, Enum.uniq([subscribed | user.subscribers]))
+  end
+
+  def remove_from_subscribers(user, subscribed) do
+    set_subscribers(user, List.delete(user.subscribers, subscribed))
+  end
+
+  defp set_domain_blocks(user, domain_blocks) do
+    params = %{domain_blocks: domain_blocks}
+
+    user
+    |> cast(params, [:domain_blocks])
+    |> validate_required([:domain_blocks])
+    |> update_and_set_cache()
+  end
+
+  def block_domain(user, domain_blocked) do
+    set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
+  end
+
+  def unblock_domain(user, domain_blocked) do
+    set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
+  end
+
+  defp set_blocks(user, blocks) do
+    params = %{blocks: blocks}
+
+    user
+    |> cast(params, [:blocks])
+    |> validate_required([:blocks])
+    |> update_and_set_cache()
+  end
+
+  def add_to_block(user, blocked) do
+    set_blocks(user, Enum.uniq([blocked | user.blocks]))
+  end
+
+  def remove_from_block(user, blocked) do
+    set_blocks(user, List.delete(user.blocks, blocked))
+  end
+
+  defp set_mutes(user, mutes) do
+    params = %{mutes: mutes}
+
+    user
+    |> cast(params, [:mutes])
+    |> validate_required([:mutes])
+    |> update_and_set_cache()
+  end
+
+  def add_to_mutes(user, muted, notifications?) do
+    with {:ok, user} <- set_mutes(user, Enum.uniq([muted | user.mutes])) do
+      set_notification_mutes(
+        user,
+        Enum.uniq([muted | user.muted_notifications]),
+        notifications?
+      )
+    end
+  end
+
+  def remove_from_mutes(user, muted) do
+    with {:ok, user} <- set_mutes(user, List.delete(user.mutes, muted)) do
+      set_notification_mutes(
+        user,
+        List.delete(user.muted_notifications, muted),
+        true
+      )
+    end
+  end
+
+  defp set_notification_mutes(user, _muted_notifications, false = _notifications?) do
+    {:ok, user}
+  end
+
+  defp set_notification_mutes(user, muted_notifications, true = _notifications?) do
+    params = %{muted_notifications: muted_notifications}
+
+    user
+    |> cast(params, [:muted_notifications])
+    |> validate_required([:muted_notifications])
+    |> update_and_set_cache()
+  end
+
+  def add_reblog_mute(user, ap_id) do
+    params = %{muted_reblogs: user.muted_reblogs ++ [ap_id]}
+
+    user
+    |> cast(params, [:muted_reblogs])
+    |> update_and_set_cache()
+  end
+
+  def remove_reblog_mute(user, ap_id) do
+    params = %{muted_reblogs: List.delete(user.muted_reblogs, ap_id)}
+
+    user
+    |> cast(params, [:muted_reblogs])
+    |> update_and_set_cache()
+  end
+
+  def set_invisible(user, invisible) do
+    params = %{invisible: invisible}
 
-  `fun` is called with the `user.info`.
-  """
-  def update_info(user, fun) do
     user
-    |> change_info(fun)
+    |> cast(params, [:invisible])
+    |> validate_required([:invisible])
     |> update_and_set_cache()
   end
 end