[#1335] Reorganized users.subscribers as UserRelationship. Added tests for UserRelationship-related functionality.

This commit is contained in:
Ivan Tashkinov 2019-11-20 15:46:11 +03:00
parent 555edd01ab
commit de892d2fe1
11 changed files with 263 additions and 62 deletions

View file

@ -4,4 +4,10 @@
import EctoEnum 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
)

View file

@ -44,10 +44,17 @@ defmodule Pleroma.User do
@strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/ @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
@extended_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]] # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
@user_relationships_config [ @user_relationships_config [
block: [blocker_blocks: :blocked_users, blockee_blocks: :blocker_users], block: [
mute: [muter_mutes: :muted_users, mutee_mutes: :muter_users], blocker_blocks: :blocked_users,
blockee_blocks: :blocker_users
],
mute: [
muter_mutes: :muted_users,
mutee_mutes: :muter_users
],
reblog_mute: [ reblog_mute: [
reblog_muter_mutes: :reblog_muted_users, reblog_muter_mutes: :reblog_muted_users,
reblog_mutee_mutes: :reblog_muter_users reblog_mutee_mutes: :reblog_muter_users
@ -55,6 +62,11 @@ defmodule Pleroma.User do
notification_mute: [ notification_mute: [
notification_muter_mutes: :notification_muted_users, notification_muter_mutes: :notification_muted_users,
notification_mutee_mutes: :notification_muter_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(:confirmation_token, :string, default: nil)
field(:default_scope, :string, default: "public") field(:default_scope, :string, default: "public")
field(:domain_blocks, {:array, :string}, default: []) field(:domain_blocks, {:array, :string}, default: [])
field(:subscribers, {:array, :string}, default: [])
field(:deactivated, :boolean, default: false) field(:deactivated, :boolean, default: false)
field(:no_rich_text, :boolean, default: false) field(:no_rich_text, :boolean, default: false)
field(:ap_enabled, :boolean, default: false) field(:ap_enabled, :boolean, default: false)
@ -167,6 +178,8 @@ defmodule Pleroma.User do
field(:muted_reblogs, {:array, :string}, default: []) field(:muted_reblogs, {:array, :string}, default: [])
# `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation) # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
field(:muted_notifications, {:array, :string}, default: []) field(:muted_notifications, {:array, :string}, default: [])
# `:subscribers` is deprecated (replaced with `subscriber_users` relation)
field(:subscribers, {:array, :string}, default: [])
timestamps() timestamps()
end end
@ -1046,33 +1059,43 @@ def get_recipients_from_activity(%Activity{recipients: to}) do
@spec mute(User.t(), User.t(), boolean()) :: @spec mute(User.t(), User.t(), boolean()) ::
{:ok, list(UserRelationship.t())} | {:error, String.t()} {: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?) add_to_mutes(muter, mutee, notifications?)
end end
def unmute(muter, %User{} = mutee) do def unmute(%User{} = muter, %User{} = mutee) do
remove_from_mutes(muter, mutee) remove_from_mutes(muter, mutee)
end end
def subscribe(subscriber, %{ap_id: ap_id}) do def subscribe(%User{} = subscriber, %User{} = target) do
with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
if blocks?(subscribed, subscriber) and deny_follow_blocked do if blocks?(target, subscriber) and deny_follow_blocked do
{:error, "Could not subscribe: #{subscribed.nickname} is blocking you"} {:error, "Could not subscribe: #{target.nickname} is blocking you"}
else else
User.add_to_subscribers(subscribed, subscriber.ap_id) # Note: the relationship is inverse: subscriber acts as relationship target
end UserRelationship.create_inverse_subscription(target, subscriber)
end end
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 with %User{} = user <- get_cached_by_ap_id(ap_id) do
User.remove_from_subscribers(user, unsubscriber.ap_id) unsubscribe(unsubscriber, user)
end end
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) # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
blocker = blocker =
if following?(blocker, blocked) do if following?(blocker, blocked) do
@ -1089,13 +1112,7 @@ def block(blocker, %User{} = blocked) do
nil -> blocked nil -> blocked
end end
blocker = unsubscribe(blocked, blocker)
if subscribed_to?(blocked, blocker) do
{:ok, blocker} = unsubscribe(blocked, blocker)
blocker
else
blocker
end
if following?(blocked, blocker), do: unfollow(blocked, blocker) if following?(blocked, blocker), do: unfollow(blocked, blocker)
@ -1105,16 +1122,16 @@ def block(blocker, %User{} = blocked) do
end end
# helper to handle the block given only an actor's AP id # 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)) block(blocker, get_cached_by_ap_id(ap_id))
end end
def unblock(blocker, %User{} = blocked) do def unblock(%User{} = blocker, %User{} = blocked) do
remove_from_block(blocker, blocked) remove_from_block(blocker, blocked)
end end
# helper to handle the block given only an actor's AP id # 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)) unblock(blocker, get_cached_by_ap_id(ap_id))
end end
@ -1128,7 +1145,7 @@ def mutes_user?(%User{} = user, %User{} = target) do
@spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean() @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
def muted_notifications?(nil, _), do: false 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) do: UserRelationship.notification_mute_exists?(user, target)
def blocks?(nil, _), do: false def blocks?(nil, _), do: false
@ -1151,9 +1168,14 @@ def blocks_domain?(%User{} = user, %User{} = target) do
def blocks_domain?(_, _), do: false 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 with %User{} = target <- get_cached_by_ap_id(ap_id) do
Enum.member?(target.subscribers, user.ap_id) subscribed_to?(user, target)
end end
end end
@ -1183,12 +1205,6 @@ def outgoing_relations_ap_ids(%User{} = user, relationship_types)
) )
end 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 def deactivate_async(user, status \\ true) do
BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status}) BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
end end
@ -1919,23 +1935,6 @@ def update_email_notifications(user, settings) do
|> update_and_set_cache() |> update_and_set_cache()
end 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 defp set_domain_blocks(user, domain_blocks) do
params = %{domain_blocks: domain_blocks} params = %{domain_blocks: domain_blocks}

View file

@ -33,7 +33,7 @@ def follow(follower, followed) do
def unfollow(follower, unfollowed) do def unfollow(follower, unfollowed) do
with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed), with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
{:ok, _activity} <- ActivityPub.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} {:ok, follower}
end end
end end

View file

@ -492,7 +492,7 @@ def maybe_notify_subscribers(
with %User{} = user <- User.get_cached_by_ap_id(actor) do with %User{} = user <- User.get_cached_by_ap_id(actor) do
subscriber_ids = subscriber_ids =
user user
|> User.subscribers() |> User.subscriber_users()
|> Enum.filter(&Visibility.visible_for_user?(activity, &1)) |> Enum.filter(&Visibility.visible_for_user?(activity, &1))
|> Enum.map(& &1.ap_id) |> Enum.map(& &1.ap_id)

View file

@ -144,7 +144,7 @@ def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do
@doc "POST /api/v1/pleroma/accounts/:id/subscribe" @doc "POST /api/v1/pleroma/accounts/:id/subscribe"
def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do 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) render(conn, "relationship.json", user: user, target: subscription_target)
else else
{:error, message} -> json_response(conn, :forbidden, %{error: message}) {:error, message} -> json_response(conn, :forbidden, %{error: message})
@ -153,7 +153,7 @@ def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _p
@doc "POST /api/v1/pleroma/accounts/:id/unsubscribe" @doc "POST /api/v1/pleroma/accounts/:id/unsubscribe"
def unsubscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do 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) render(conn, "relationship.json", user: user, target: subscription_target)
else else
{:error, message} -> json_response(conn, :forbidden, %{error: message}) {:error, message} -> json_response(conn, :forbidden, %{error: message})

View file

@ -8,9 +8,13 @@ defmodule Pleroma.Repo.Migrations.DataMigrationPopulateUserRelationships do
def up do def up do
Enum.each( 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} -> fn {field, relationship_type_code} ->
migrate(field, relationship_type_code) migrate(field, relationship_type_code)
if field == :subscribers do
drop_if_exists(index(:users, [:subscribers]))
end
end end
) )
end end

View file

@ -43,6 +43,18 @@ def user_factory do
} }
end 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 def note_factory(attrs \\ %{}) do
text = sequence(:text, &"This is :moominmamma: note #{&1}") text = sequence(:text, &"This is :moominmamma: note #{&1}")

View file

@ -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

View file

@ -25,6 +25,56 @@ defmodule Pleroma.UserTest do
clear_config([:instance, :account_activation_required]) 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 describe "when tags are nil" do
test "tagging a user" do test "tagging a user" do
user = insert(:user, %{tags: nil}) user = insert(:user, %{tags: nil})
@ -785,7 +835,7 @@ test "blocks tear down blocked->blocker subscription relationships" do
blocker = insert(:user) blocker = insert(:user)
blocked = insert(:user) blocked = insert(:user)
{:ok, blocker} = User.subscribe(blocked, blocker) {:ok, _subscription} = User.subscribe(blocked, blocker)
assert User.subscribed_to?(blocked, blocker) assert User.subscribed_to?(blocked, blocker)
refute User.subscribed_to?(blocker, blocked) refute User.subscribed_to?(blocker, blocked)

View file

@ -526,7 +526,7 @@ test "remove a reblog mute", %{muter: muter, muted: muted} do
test "also unsubscribes a user" do test "also unsubscribes a user" do
[follower, followed] = insert_pair(:user) [follower, followed] = insert_pair(:user)
{:ok, follower, followed, _} = CommonAPI.follow(follower, followed) {: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) assert User.subscribed_to?(follower, followed)

View file

@ -190,7 +190,7 @@ test "represent a relationship for the following and followed user" do
{:ok, user} = User.follow(user, other_user) {:ok, user} = User.follow(user, other_user)
{:ok, other_user} = User.follow(other_user, 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, _user_relationships} = User.mute(user, other_user, true)
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user) {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user)
@ -218,7 +218,7 @@ test "represent a relationship for the blocking and blocked user" do
other_user = insert(:user) other_user = insert(:user)
{:ok, user} = User.follow(user, other_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(user, other_user)
{:ok, _user_relationship} = User.block(other_user, user) {:ok, _user_relationship} = User.block(other_user, user)