diff --git a/lib/pleroma/ecto_enums.ex b/lib/pleroma/ecto_enums.ex index bad5ec523..b86229312 100644 --- a/lib/pleroma/ecto_enums.ex +++ b/lib/pleroma/ecto_enums.ex @@ -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 +) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 4b53dce13..d97c19d38 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -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} diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index afda9ffce..2f3bcfc3c 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -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 diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 88a5f434a..f77076906 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -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) diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index bc2f1017c..773cd9a97 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -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}) diff --git a/priv/repo/migrations/20191118084500_data_migration_populate_user_relationships.exs b/priv/repo/migrations/20191118084500_data_migration_populate_user_relationships.exs index f8dde7626..990e9f3b8 100644 --- a/priv/repo/migrations/20191118084500_data_migration_populate_user_relationships.exs +++ b/priv/repo/migrations/20191118084500_data_migration_populate_user_relationships.exs @@ -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 diff --git a/test/support/factory.ex b/test/support/factory.ex index e3f797f64..c2ceb5836 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -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 index 000000000..437450bc3 --- /dev/null +++ b/test/user_relationship_test.exs @@ -0,0 +1,130 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# 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 diff --git a/test/user_test.exs b/test/user_test.exs index b0838e498..ae90edbe9 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -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) diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 667ca89b9..b5d6d4055 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -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) diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index 4e5da6b06..53cd26a69 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -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)