Merge remote-tracking branch 'remotes/upstream/develop' into 1335-user-api-id-fields...
[akkoma] / lib / pleroma / user.ex
index 5d3f5572192966b7fff3d0c99a678e25b3058c2d..d6bc02d0420437334bd9396a43a956be23535396 100644 (file)
@@ -7,12 +7,14 @@ defmodule Pleroma.User do
 
   import Ecto.Changeset
   import Ecto.Query
+  import Ecto, only: [assoc: 2]
 
   alias Comeonin.Pbkdf2
   alias Ecto.Multi
   alias Pleroma.Activity
   alias Pleroma.Conversation.Participation
   alias Pleroma.Delivery
+  alias Pleroma.FollowingRelationship
   alias Pleroma.Keys
   alias Pleroma.Notification
   alias Pleroma.Object
@@ -20,6 +22,8 @@ defmodule Pleroma.User do
   alias Pleroma.Repo
   alias Pleroma.RepoStreamer
   alias Pleroma.User
+  alias Pleroma.UserBlock
+  alias Pleroma.UserMute
   alias Pleroma.Web
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.Utils
@@ -50,7 +54,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)
@@ -74,7 +77,6 @@ defmodule Pleroma.User do
     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: [])
@@ -119,11 +121,27 @@ defmodule Pleroma.User do
     has_many(:registrations, Registration)
     has_many(:deliveries, Delivery)
 
+    has_many(:blocker_blocks, UserBlock, foreign_key: :blocker_id)
+    has_many(:blockee_blocks, UserBlock, foreign_key: :blockee_id)
+    has_many(:blocked_users, through: [:blocker_blocks, :blockee])
+    has_many(:blocker_users, through: [:blockee_blocks, :blocker])
+
+    has_many(:muter_mutes, UserMute, foreign_key: :muter_id)
+    has_many(:mutee_mutes, UserMute, foreign_key: :mutee_id)
+    has_many(:muted_users, through: [:muter_mutes, :mutee])
+    has_many(:muter_users, through: [:mutee_mutes, :muter])
+
     field(:info, :map, default: %{})
 
+    # `:blocks` is deprecated (replaced with `blocked_users` relation)
+    field(:blocks, {:array, :string}, default: [])
+
     timestamps()
   end
 
+  @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])
 
@@ -216,13 +234,7 @@ defmodule Pleroma.User do
     from(u in query, where: u.deactivated != ^true)
   end
 
-  def following_count(%User{following: []}), do: 0
-
-  def following_count(%User{} = user) do
-    user
-    |> get_friends_query()
-    |> Repo.aggregate(:count, :id)
-  end
+  defdelegate following_count(user), to: FollowingRelationship
 
   defp truncate_fields_param(params) do
     if Map.has_key?(params, :fields) do
@@ -309,7 +321,6 @@ defmodule Pleroma.User do
         :bio,
         :name,
         :avatar,
-        :following,
         :locked,
         :no_rich_text,
         :default_scope,
@@ -454,7 +465,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
 
@@ -508,8 +518,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, 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
@@ -527,37 +537,22 @@ 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
-      )
+    followeds =
+      Enum.reject(followeds, fn followed ->
+        blocks?(follower, followed) || blocks?(followed, follower)
+      end)
 
-    {1, [follower]} = Repo.update_all(q, [])
+    Enum.each(followeds, &follow(follower, &1, "accept"))
 
     Enum.each(followeds, &update_follower_count/1)
 
     set_cache(follower)
   end
 
-  def follow(%User{} = follower, %User{} = 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
       followed.deactivated ->
@@ -567,14 +562,7 @@ defmodule Pleroma.User do
         {:error, "Could not follow user: #{followed.nickname} blocked you."}
 
       true ->
-        q =
-          from(u in User,
-            where: u.id == ^follower.id,
-            update: [push: [following: ^ap_followers]],
-            select: u
-          )
-
-        {1, [follower]} = Repo.update_all(q, [])
+        FollowingRelationship.follow(follower, followed, state)
 
         follower = maybe_update_following_count(follower)
 
@@ -585,17 +573,8 @@ defmodule Pleroma.User do
   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, [])
+      FollowingRelationship.unfollow(follower, followed)
 
       follower = maybe_update_following_count(follower)
 
@@ -609,10 +588,7 @@ 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.locked || false
@@ -834,16 +810,7 @@ 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
@@ -995,18 +962,6 @@ defmodule Pleroma.User do
 
   def increment_unread_conversation_count(_, user), do: {:ok, user}
 
-  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
-
   @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
   def get_users_from_set(ap_ids, local_only \\ true) do
     criteria = %{ap_id: ap_ids, deactivated: false}
@@ -1023,12 +978,12 @@ defmodule Pleroma.User do
   end
 
   @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
-  def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
-    add_to_mutes(muter, ap_id, notifications?)
+  def mute(muter, %User{} = mutee, notifications? \\ true) do
+    add_to_mutes(muter, mutee, notifications?)
   end
 
-  def unmute(muter, %{ap_id: ap_id}) do
-    remove_from_mutes(muter, ap_id)
+  def unmute(muter, %User{} = mutee) do
+    remove_from_mutes(muter, mutee)
   end
 
   def subscribe(subscriber, %{ap_id: ap_id}) do
@@ -1049,7 +1004,7 @@ defmodule Pleroma.User do
     end
   end
 
-  def block(blocker, %User{ap_id: ap_id} = blocked) do
+  def block(blocker, %User{} = blocked) do
     # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
     blocker =
       if following?(blocker, blocked) do
@@ -1078,7 +1033,7 @@ defmodule Pleroma.User do
 
     {:ok, blocker} = update_follower_count(blocker)
     {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
-    add_to_block(blocker, ap_id)
+    add_to_block(blocker, blocked)
   end
 
   # helper to handle the block given only an actor's AP id
@@ -1086,12 +1041,21 @@ defmodule Pleroma.User do
     block(blocker, get_cached_by_ap_id(ap_id))
   end
 
+  def unblock(blocker, %User{} = blocked) do
+    remove_from_block(blocker, blocked)
+  end
+
+  # helper to handle the block given only an actor's AP id
   def unblock(blocker, %{ap_id: ap_id}) do
-    remove_from_block(blocker, ap_id)
+    unblock(blocker, get_cached_by_ap_id(ap_id))
   end
 
   def mutes?(nil, _), do: false
-  def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.mutes, ap_id)
+  def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
+
+  def mutes_user?(%User{} = user, %User{} = target) do
+    UserMute.exists?(user, target)
+  end
 
   @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
   def muted_notifications?(nil, _), do: false
@@ -1099,17 +1063,17 @@ defmodule Pleroma.User do
   def muted_notifications?(user, %{ap_id: ap_id}),
     do: Enum.member?(user.muted_notifications, ap_id)
 
+  def blocks?(nil, _), do: false
+
   def blocks?(%User{} = user, %User{} = target) do
-    blocks_ap_id?(user, target) || blocks_domain?(user, target)
+    blocks_user?(user, target) || blocks_domain?(user, target)
   end
 
-  def blocks?(nil, _), do: false
-
-  def blocks_ap_id?(%User{} = user, %User{} = target) do
-    Enum.member?(user.blocks, target.ap_id)
+  def blocks_user?(%User{} = user, %User{} = target) do
+    UserBlock.exists?(user, target)
   end
 
-  def blocks_ap_id?(_, _), do: false
+  def blocks_user?(_, _), do: false
 
   def blocks_domain?(%User{} = user, %User{} = target) do
     domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
@@ -1127,16 +1091,63 @@ defmodule Pleroma.User do
 
   @spec muted_users(User.t()) :: [User.t()]
   def muted_users(user) do
-    User.Query.build(%{ap_id: user.mutes, deactivated: false})
+    user
+    |> assoc(:muted_users)
+    |> restrict_deactivated()
+    |> Repo.all()
+  end
+
+  def muted_ap_ids(user) do
+    user
+    |> assoc(:muted_users)
+    |> select([u], u.ap_id)
     |> Repo.all()
   end
 
   @spec blocked_users(User.t()) :: [User.t()]
   def blocked_users(user) do
-    User.Query.build(%{ap_id: user.blocks, deactivated: false})
+    user
+    |> assoc(:blocked_users)
+    |> restrict_deactivated()
+    |> Repo.all()
+  end
+
+  def blocked_ap_ids(user) do
+    user
+    |> assoc(:blocked_users)
+    |> select([u], u.ap_id)
     |> Repo.all()
   end
 
+  defp related_ap_ids_sql(join_table, source_column, target_column) do
+    "(SELECT array_agg(u.ap_id) FROM users as u " <>
+      "INNER JOIN #{join_table} AS join_table " <>
+      "ON join_table.#{source_column} = $1 " <>
+      "WHERE u.id = join_table.#{target_column})"
+  end
+
+  @related_ap_ids_sql_params %{
+    blocked_users: ["user_blocks", "blocker_id", "blockee_id"],
+    muted_users: ["user_mutes", "muter_id", "mutee_id"]
+  }
+
+  def related_ap_ids(user, relations) when is_list(relations) do
+    query =
+      relations
+      |> Enum.map(fn r -> @related_ap_ids_sql_params[r] end)
+      |> Enum.filter(& &1)
+      |> Enum.map(fn [join_table, source_column, target_column] ->
+        related_ap_ids_sql(join_table, source_column, target_column)
+      end)
+      |> Enum.join(", ")
+
+    with {:ok, %{rows: [ap_ids_arrays]}} <-
+           Repo.query("SELECT #{query}", [FlakeId.from_string(user.id)]) do
+      ap_ids_arrays = Enum.map(ap_ids_arrays, &(&1 || []))
+      {:ok, ap_ids_arrays}
+    end
+  end
+
   @spec subscribers(User.t()) :: [User.t()]
   def subscribers(user) do
     User.Query.build(%{ap_id: user.subscribers, deactivated: false})
@@ -1158,7 +1169,12 @@ defmodule Pleroma.User do
   def deactivate(%User{} = user, status) do
     with {:ok, user} <- set_activation_status(user, status) do
       Enum.each(get_followers(user), &invalidate_cache/1)
-      Enum.each(get_friends(user), &update_follower_count/1)
+
+      # 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
@@ -1237,7 +1253,7 @@ defmodule Pleroma.User do
       blocked_identifiers,
       fn blocked_identifier ->
         with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
-             {:ok, blocker} <- block(blocker, blocked),
+             {:ok, _user_block} <- block(blocker, blocked),
              {:ok, _} <- ActivityPub.block(blocker, blocked) do
           blocked
         else
@@ -1902,49 +1918,38 @@ defmodule Pleroma.User 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()
+  @spec add_to_block(User.t(), User.t()) :: {:ok, UserBlock.t()} | {:error, Ecto.Changeset.t()}
+  defp add_to_block(%User{} = user, %User{} = blocked) do
+    UserBlock.create(user, blocked)
   end
 
-  def add_to_block(user, blocked) do
-    set_blocks(user, Enum.uniq([blocked | user.blocks]))
+  @spec add_to_block(User.t(), User.t()) ::
+          {:ok, UserBlock.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
+  defp remove_from_block(%User{} = user, %User{} = blocked) do
+    UserBlock.delete(user, blocked)
   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?
-      )
+  defp add_to_mutes(%User{} = user, %User{ap_id: ap_id} = muted_user, notifications?) do
+    with {:ok, user_mute} <- UserMute.create(user, muted_user),
+         {:ok, _user} <-
+           set_notification_mutes(
+             user,
+             Enum.uniq([ap_id | user.muted_notifications]),
+             notifications?
+           ) do
+      {:ok, user_mute}
     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
-      )
+  defp remove_from_mutes(user, %User{ap_id: ap_id} = muted_user) do
+    with {:ok, user_mute} <- UserMute.delete(user, muted_user),
+         {:ok, _user} <-
+           set_notification_mutes(
+             user,
+             List.delete(user.muted_notifications, ap_id),
+             true
+           ) do
+      {:ok, user_mute}
     end
   end