[#1335] Reorganized users.subscribers as UserRelationship. Added tests for UserRelati...
authorIvan Tashkinov <ivantashkinov@gmail.com>
Wed, 20 Nov 2019 12:46:11 +0000 (15:46 +0300)
committerIvan Tashkinov <ivantashkinov@gmail.com>
Wed, 20 Nov 2019 12:46:11 +0000 (15:46 +0300)
lib/pleroma/ecto_enums.ex
lib/pleroma/user.ex
lib/pleroma/web/common_api/common_api.ex
lib/pleroma/web/common_api/utils.ex
lib/pleroma/web/pleroma_api/controllers/account_controller.ex
priv/repo/migrations/20191118084500_data_migration_populate_user_relationships.exs
test/support/factory.ex
test/user_relationship_test.exs [new file with mode: 0644]
test/user_test.exs
test/web/common_api/common_api_test.exs
test/web/mastodon_api/views/account_view_test.exs

index bad5ec523aabfbc24d997b084536ea8e693f59e4..b86229312672592c7b77fd28b31a4feaf060446f 100644 (file)
@@ -4,4 +4,10 @@
 
 import EctoEnum
 
-defenum(UserRelationshipTypeEnum, block: 1, mute: 2, reblog_mute: 3, notification_mute: 4)
+defenum(UserRelationshipTypeEnum,
+  block: 1,
+  mute: 2,
+  reblog_mute: 3,
+  notification_mute: 4,
+  inverse_subscription: 5
+)
index 4b53dce13e88e16de430f0587733abd783565fba..d97c19d38926383b53974ae279207d8a9fa97bec 100644 (file)
@@ -44,10 +44,17 @@ defmodule Pleroma.User do
   @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
   @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
 
+  # AP ID user relationships (blocks, mutes etc.)
   # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
   @user_relationships_config [
-    block: [blocker_blocks: :blocked_users, blockee_blocks: :blocker_users],
-    mute: [muter_mutes: :muted_users, mutee_mutes: :muter_users],
+    block: [
+      blocker_blocks: :blocked_users,
+      blockee_blocks: :blocker_users
+    ],
+    mute: [
+      muter_mutes: :muted_users,
+      mutee_mutes: :muter_users
+    ],
     reblog_mute: [
       reblog_muter_mutes: :reblog_muted_users,
       reblog_mutee_mutes: :reblog_muter_users
@@ -55,6 +62,11 @@ defmodule Pleroma.User do
     notification_mute: [
       notification_muter_mutes: :notification_muted_users,
       notification_mutee_mutes: :notification_muter_users
+    ],
+    # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
+    inverse_subscription: [
+      subscribee_subscriptions: :subscriber_users,
+      subscriber_subscriptions: :subscribee_users
     ]
   ]
 
@@ -90,7 +102,6 @@ defmodule Pleroma.User do
     field(:confirmation_token, :string, default: nil)
     field(:default_scope, :string, default: "public")
     field(:domain_blocks, {: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)
@@ -167,6 +178,8 @@ defmodule Pleroma.User do
     field(:muted_reblogs, {:array, :string}, default: [])
     # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
     field(:muted_notifications, {:array, :string}, default: [])
+    # `:subscribers` is deprecated (replaced with `subscriber_users` relation)
+    field(:subscribers, {:array, :string}, default: [])
 
     timestamps()
   end
@@ -1046,33 +1059,43 @@ defmodule Pleroma.User do
 
   @spec mute(User.t(), User.t(), boolean()) ::
           {:ok, list(UserRelationship.t())} | {:error, String.t()}
-  def mute(muter, %User{} = mutee, notifications? \\ true) do
+  def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
     add_to_mutes(muter, mutee, notifications?)
   end
 
-  def unmute(muter, %User{} = mutee) do
+  def unmute(%User{} = muter, %User{} = mutee) do
     remove_from_mutes(muter, mutee)
   end
 
-  def subscribe(subscriber, %{ap_id: ap_id}) do
-    with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
-      deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
+  def subscribe(%User{} = subscriber, %User{} = target) do
+    deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
 
-      if blocks?(subscribed, subscriber) and deny_follow_blocked do
-        {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
-      else
-        User.add_to_subscribers(subscribed, subscriber.ap_id)
-      end
+    if blocks?(target, subscriber) and deny_follow_blocked do
+      {:error, "Could not subscribe: #{target.nickname} is blocking you"}
+    else
+      # Note: the relationship is inverse: subscriber acts as relationship target
+      UserRelationship.create_inverse_subscription(target, subscriber)
     end
   end
 
-  def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
+  def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
+    with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
+      subscribe(subscriber, subscribee)
+    end
+  end
+
+  def unsubscribe(%User{} = unsubscriber, %User{} = target) do
+    # Note: the relationship is inverse: subscriber acts as relationship target
+    UserRelationship.delete_inverse_subscription(target, unsubscriber)
+  end
+
+  def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
     with %User{} = user <- get_cached_by_ap_id(ap_id) do
-      User.remove_from_subscribers(user, unsubscriber.ap_id)
+      unsubscribe(unsubscriber, user)
     end
   end
 
-  def block(blocker, %User{} = blocked) do
+  def block(%User{} = blocker, %User{} = blocked) do
     # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
     blocker =
       if following?(blocker, blocked) do
@@ -1089,13 +1112,7 @@ defmodule Pleroma.User do
         nil -> blocked
       end
 
-    blocker =
-      if subscribed_to?(blocked, blocker) do
-        {:ok, blocker} = unsubscribe(blocked, blocker)
-        blocker
-      else
-        blocker
-      end
+    unsubscribe(blocked, blocker)
 
     if following?(blocked, blocker), do: unfollow(blocked, blocker)
 
@@ -1105,16 +1122,16 @@ defmodule Pleroma.User do
   end
 
   # helper to handle the block given only an actor's AP id
-  def block(blocker, %{ap_id: ap_id}) do
+  def block(%User{} = blocker, %{ap_id: ap_id}) do
     block(blocker, get_cached_by_ap_id(ap_id))
   end
 
-  def unblock(blocker, %User{} = blocked) do
+  def unblock(%User{} = 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
+  def unblock(%User{} = blocker, %{ap_id: ap_id}) do
     unblock(blocker, get_cached_by_ap_id(ap_id))
   end
 
@@ -1128,7 +1145,7 @@ defmodule Pleroma.User do
   @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
   def muted_notifications?(nil, _), do: false
 
-  def muted_notifications?(user, %User{} = target),
+  def muted_notifications?(%User{} = user, %User{} = target),
     do: UserRelationship.notification_mute_exists?(user, target)
 
   def blocks?(nil, _), do: false
@@ -1151,9 +1168,14 @@ defmodule Pleroma.User do
 
   def blocks_domain?(_, _), do: false
 
-  def subscribed_to?(user, %{ap_id: ap_id}) do
+  def subscribed_to?(%User{} = user, %User{} = target) do
+    # Note: the relationship is inverse: subscriber acts as relationship target
+    UserRelationship.inverse_subscription_exists?(target, user)
+  end
+
+  def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
     with %User{} = target <- get_cached_by_ap_id(ap_id) do
-      Enum.member?(target.subscribers, user.ap_id)
+      subscribed_to?(user, target)
     end
   end
 
@@ -1183,12 +1205,6 @@ defmodule Pleroma.User do
     )
   end
 
-  @spec subscribers(User.t()) :: [User.t()]
-  def subscribers(user) do
-    User.Query.build(%{ap_id: user.subscribers, deactivated: false})
-    |> Repo.all()
-  end
-
   def deactivate_async(user, status \\ true) do
     BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
   end
@@ -1919,23 +1935,6 @@ defmodule Pleroma.User do
     |> 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}
 
index afda9ffce1ccbfbaf966b6d32c1ae7aa34473010..2f3bcfc3c979cd24af1070a7138230c009e0d3b5 100644 (file)
@@ -33,7 +33,7 @@ defmodule Pleroma.Web.CommonAPI do
   def unfollow(follower, unfollowed) do
     with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
          {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
-         {:ok, _unfollowed} <- User.unsubscribe(follower, unfollowed) do
+         {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
       {:ok, follower}
     end
   end
index 88a5f434a671277c0a949d22ad4173f0c07b5f37..f7707690654a0ced571f438c8b90e9bc021b5269 100644 (file)
@@ -492,7 +492,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
     with %User{} = user <- User.get_cached_by_ap_id(actor) do
       subscriber_ids =
         user
-        |> User.subscribers()
+        |> User.subscriber_users()
         |> Enum.filter(&Visibility.visible_for_user?(activity, &1))
         |> Enum.map(& &1.ap_id)
 
index bc2f1017c83fb52f9a3b5e1e025f835d9c2fcc99..773cd9a9743ac81585dfc5461ca9e2006a95a25a 100644 (file)
@@ -144,7 +144,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
 
   @doc "POST /api/v1/pleroma/accounts/:id/subscribe"
   def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do
-    with {:ok, subscription_target} <- User.subscribe(user, subscription_target) do
+    with {:ok, _subscription} <- User.subscribe(user, subscription_target) do
       render(conn, "relationship.json", user: user, target: subscription_target)
     else
       {:error, message} -> json_response(conn, :forbidden, %{error: message})
@@ -153,7 +153,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
 
   @doc "POST /api/v1/pleroma/accounts/:id/unsubscribe"
   def unsubscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do
-    with {:ok, subscription_target} <- User.unsubscribe(user, subscription_target) do
+    with {:ok, _subscription} <- User.unsubscribe(user, subscription_target) do
       render(conn, "relationship.json", user: user, target: subscription_target)
     else
       {:error, message} -> json_response(conn, :forbidden, %{error: message})
index f8dde7626b9fed8744cbd8777246778191a90c33..990e9f3b8f8d48d8b01022a50760c7e28c218bb9 100644 (file)
@@ -8,9 +8,13 @@ defmodule Pleroma.Repo.Migrations.DataMigrationPopulateUserRelationships do
 
   def up do
     Enum.each(
-      [blocks: 1, mutes: 2, muted_reblogs: 3, muted_notifications: 4],
+      [blocks: 1, mutes: 2, muted_reblogs: 3, muted_notifications: 4, subscribers: 5],
       fn {field, relationship_type_code} ->
         migrate(field, relationship_type_code)
+
+        if field == :subscribers do
+          drop_if_exists(index(:users, [:subscribers]))
+        end
       end
     )
   end
index e3f797f64c16b4abb0dcc8f05ada8cd8a811479f..c2ceb5836836f9743c911fafef917048e64a7c62 100644 (file)
@@ -43,6 +43,18 @@ defmodule Pleroma.Factory do
     }
   end
 
+  def user_relationship_factory(attrs \\ %{}) do
+    source = attrs[:source] || insert(:user)
+    target = attrs[:target] || insert(:user)
+    relationship_type = attrs[:relationship_type] || :block
+
+    %Pleroma.UserRelationship{
+      source_id: source.id,
+      target_id: target.id,
+      relationship_type: relationship_type
+    }
+  end
+
   def note_factory(attrs \\ %{}) do
     text = sequence(:text, &"This is :moominmamma: note #{&1}")
 
diff --git a/test/user_relationship_test.exs b/test/user_relationship_test.exs
new file mode 100644 (file)
index 0000000..437450b
--- /dev/null
@@ -0,0 +1,130 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.UserRelationshipTest do
+  alias Pleroma.UserRelationship
+
+  use Pleroma.DataCase
+
+  import Pleroma.Factory
+
+  describe "*_exists?/2" do
+    setup do
+      {:ok, users: insert_list(2, :user)}
+    end
+
+    test "returns false if record doesn't exist", %{users: [user1, user2]} do
+      refute UserRelationship.block_exists?(user1, user2)
+      refute UserRelationship.mute_exists?(user1, user2)
+      refute UserRelationship.notification_mute_exists?(user1, user2)
+      refute UserRelationship.reblog_mute_exists?(user1, user2)
+      refute UserRelationship.inverse_subscription_exists?(user1, user2)
+    end
+
+    test "returns true if record exists", %{users: [user1, user2]} do
+      for relationship_type <- [
+            :block,
+            :mute,
+            :notification_mute,
+            :reblog_mute,
+            :inverse_subscription
+          ] do
+        insert(:user_relationship,
+          source: user1,
+          target: user2,
+          relationship_type: relationship_type
+        )
+      end
+
+      assert UserRelationship.block_exists?(user1, user2)
+      assert UserRelationship.mute_exists?(user1, user2)
+      assert UserRelationship.notification_mute_exists?(user1, user2)
+      assert UserRelationship.reblog_mute_exists?(user1, user2)
+      assert UserRelationship.inverse_subscription_exists?(user1, user2)
+    end
+  end
+
+  describe "create_*/2" do
+    setup do
+      {:ok, users: insert_list(2, :user)}
+    end
+
+    test "creates user relationship record if it doesn't exist", %{users: [user1, user2]} do
+      for relationship_type <- [
+            :block,
+            :mute,
+            :notification_mute,
+            :reblog_mute,
+            :inverse_subscription
+          ] do
+        insert(:user_relationship,
+          source: user1,
+          target: user2,
+          relationship_type: relationship_type
+        )
+      end
+
+      UserRelationship.create_block(user1, user2)
+      UserRelationship.create_mute(user1, user2)
+      UserRelationship.create_notification_mute(user1, user2)
+      UserRelationship.create_reblog_mute(user1, user2)
+      UserRelationship.create_inverse_subscription(user1, user2)
+
+      assert UserRelationship.block_exists?(user1, user2)
+      assert UserRelationship.mute_exists?(user1, user2)
+      assert UserRelationship.notification_mute_exists?(user1, user2)
+      assert UserRelationship.reblog_mute_exists?(user1, user2)
+      assert UserRelationship.inverse_subscription_exists?(user1, user2)
+    end
+
+    test "if record already exists, returns it", %{users: [user1, user2]} do
+      user_block = UserRelationship.create_block(user1, user2)
+      assert user_block == UserRelationship.create_block(user1, user2)
+    end
+  end
+
+  describe "delete_*/2" do
+    setup do
+      {:ok, users: insert_list(2, :user)}
+    end
+
+    test "deletes user relationship record if it exists", %{users: [user1, user2]} do
+      for relationship_type <- [
+            :block,
+            :mute,
+            :notification_mute,
+            :reblog_mute,
+            :inverse_subscription
+          ] do
+        insert(:user_relationship,
+          source: user1,
+          target: user2,
+          relationship_type: relationship_type
+        )
+      end
+
+      assert {:ok, %UserRelationship{}} = UserRelationship.delete_block(user1, user2)
+      assert {:ok, %UserRelationship{}} = UserRelationship.delete_mute(user1, user2)
+      assert {:ok, %UserRelationship{}} = UserRelationship.delete_notification_mute(user1, user2)
+      assert {:ok, %UserRelationship{}} = UserRelationship.delete_reblog_mute(user1, user2)
+
+      assert {:ok, %UserRelationship{}} =
+               UserRelationship.delete_inverse_subscription(user1, user2)
+
+      refute UserRelationship.block_exists?(user1, user2)
+      refute UserRelationship.mute_exists?(user1, user2)
+      refute UserRelationship.notification_mute_exists?(user1, user2)
+      refute UserRelationship.reblog_mute_exists?(user1, user2)
+      refute UserRelationship.inverse_subscription_exists?(user1, user2)
+    end
+
+    test "if record does not exist, returns {:ok, nil}", %{users: [user1, user2]} do
+      assert {:ok, nil} = UserRelationship.delete_block(user1, user2)
+      assert {:ok, nil} = UserRelationship.delete_mute(user1, user2)
+      assert {:ok, nil} = UserRelationship.delete_notification_mute(user1, user2)
+      assert {:ok, nil} = UserRelationship.delete_reblog_mute(user1, user2)
+      assert {:ok, nil} = UserRelationship.delete_inverse_subscription(user1, user2)
+    end
+  end
+end
index b0838e498e254520f0223324acc5f603c9823615..ae90edbe956eadbf39929f2088f0ce4b74feeb4c 100644 (file)
@@ -25,6 +25,56 @@ defmodule Pleroma.UserTest do
 
   clear_config([:instance, :account_activation_required])
 
+  describe "AP ID user relationships" do
+    setup do
+      {:ok, user: insert(:user)}
+    end
+
+    test "outgoing_relations_ap_ids/1", %{user: user} do
+      rel_types = [:block, :mute, :notification_mute, :reblog_mute, :inverse_subscription]
+
+      ap_ids_by_rel =
+        Enum.into(
+          rel_types,
+          %{},
+          fn rel_type ->
+            rel_records =
+              insert_list(2, :user_relationship, %{source: user, relationship_type: rel_type})
+
+            ap_ids = Enum.map(rel_records, fn rr -> Repo.preload(rr, :target).target.ap_id end)
+            {rel_type, Enum.sort(ap_ids)}
+          end
+        )
+
+      assert ap_ids_by_rel[:block] == Enum.sort(User.blocked_users_ap_ids(user))
+      assert ap_ids_by_rel[:block] == Enum.sort(Enum.map(User.blocked_users(user), & &1.ap_id))
+
+      assert ap_ids_by_rel[:mute] == Enum.sort(User.muted_users_ap_ids(user))
+      assert ap_ids_by_rel[:mute] == Enum.sort(Enum.map(User.muted_users(user), & &1.ap_id))
+
+      assert ap_ids_by_rel[:notification_mute] ==
+               Enum.sort(User.notification_muted_users_ap_ids(user))
+
+      assert ap_ids_by_rel[:notification_mute] ==
+               Enum.sort(Enum.map(User.notification_muted_users(user), & &1.ap_id))
+
+      assert ap_ids_by_rel[:reblog_mute] == Enum.sort(User.reblog_muted_users_ap_ids(user))
+
+      assert ap_ids_by_rel[:reblog_mute] ==
+               Enum.sort(Enum.map(User.reblog_muted_users(user), & &1.ap_id))
+
+      assert ap_ids_by_rel[:inverse_subscription] == Enum.sort(User.subscriber_users_ap_ids(user))
+
+      assert ap_ids_by_rel[:inverse_subscription] ==
+               Enum.sort(Enum.map(User.subscriber_users(user), & &1.ap_id))
+
+      outgoing_relations_ap_ids = User.outgoing_relations_ap_ids(user, rel_types)
+
+      assert ap_ids_by_rel ==
+               Enum.into(outgoing_relations_ap_ids, %{}, fn {k, v} -> {k, Enum.sort(v)} end)
+    end
+  end
+
   describe "when tags are nil" do
     test "tagging a user" do
       user = insert(:user, %{tags: nil})
@@ -785,7 +835,7 @@ defmodule Pleroma.UserTest do
       blocker = insert(:user)
       blocked = insert(:user)
 
-      {:ok, blocker} = User.subscribe(blocked, blocker)
+      {:ok, _subscription} = User.subscribe(blocked, blocker)
 
       assert User.subscribed_to?(blocked, blocker)
       refute User.subscribed_to?(blocker, blocked)
index 667ca89b9574a2471fc4f7e853b781ba4d53fcfc..b5d6d40558441c250331f81b3bcfed0c131fb43d 100644 (file)
@@ -526,7 +526,7 @@ defmodule Pleroma.Web.CommonAPITest do
     test "also unsubscribes a user" do
       [follower, followed] = insert_pair(:user)
       {:ok, follower, followed, _} = CommonAPI.follow(follower, followed)
-      {:ok, followed} = User.subscribe(follower, followed)
+      {:ok, _subscription} = User.subscribe(follower, followed)
 
       assert User.subscribed_to?(follower, followed)
 
index 4e5da6b068c18f9e6d3a2c643ee52117276d7046..53cd26a697d7619c1797f93fd520376fbe5a083f 100644 (file)
@@ -190,7 +190,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
 
       {:ok, user} = User.follow(user, other_user)
       {:ok, other_user} = User.follow(other_user, user)
-      {:ok, other_user} = User.subscribe(user, other_user)
+      {:ok, _subscription} = User.subscribe(user, other_user)
       {:ok, _user_relationships} = User.mute(user, other_user, true)
       {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user)
 
@@ -218,7 +218,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
       other_user = insert(:user)
 
       {:ok, user} = User.follow(user, other_user)
-      {:ok, other_user} = User.subscribe(user, other_user)
+      {:ok, _subscription} = User.subscribe(user, other_user)
       {:ok, _user_relationship} = User.block(user, other_user)
       {:ok, _user_relationship} = User.block(other_user, user)