Merge branch 'block-behavior' into 'develop'

Configurable block visibility, fixes #2123

Closes #2123

See merge request pleroma/pleroma!3242
This commit is contained in:
lain 2021-11-15 14:27:59 +00:00
commit e2772d6bf1
9 changed files with 126 additions and 2 deletions

View File

@ -135,6 +135,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Support pagination of blocks and mutes. - Support pagination of blocks and mutes.
- Account backup. - Account backup.
- Configuration: Add `:instance, autofollowing_nicknames` setting to provide a way to make accounts automatically follow new users that register on the local Pleroma instance. - Configuration: Add `:instance, autofollowing_nicknames` setting to provide a way to make accounts automatically follow new users that register on the local Pleroma instance.
- `[:activitypub, :blockers_visible]` config to control visibility of blockers.
- Ability to view remote timelines, with ex. `/api/v1/timelines/public?instance=lain.com` and streams `public:remote` and `public:remote:media`. - Ability to view remote timelines, with ex. `/api/v1/timelines/public?instance=lain.com` and streams `public:remote` and `public:remote:media`.
- The site title is now injected as a `title` tag like preloads or metadata. - The site title is now injected as a `title` tag like preloads or metadata.
- Password reset tokens now are not accepted after a certain age. - Password reset tokens now are not accepted after a certain age.

View File

@ -349,6 +349,7 @@
config :pleroma, :activitypub, config :pleroma, :activitypub,
unfollow_blocked: true, unfollow_blocked: true,
outgoing_blocks: true, outgoing_blocks: true,
blockers_visible: true,
follow_handshake_timeout: 500, follow_handshake_timeout: 500,
note_replies_output_limit: 5, note_replies_output_limit: 5,
sign_object_fetches: true, sign_object_fetches: true,

View File

@ -1670,6 +1670,11 @@
type: :boolean, type: :boolean,
description: "Whether to federate blocks to other instances" description: "Whether to federate blocks to other instances"
}, },
%{
key: :blockers_visible,
type: :boolean,
description: "Whether a user can see someone who has blocked them"
},
%{ %{
key: :sign_object_fetches, key: :sign_object_fetches,
type: :boolean, type: :boolean,

View File

@ -230,6 +230,7 @@ Notes:
### :activitypub ### :activitypub
* `unfollow_blocked`: Whether blocks result in people getting unfollowed * `unfollow_blocked`: Whether blocks result in people getting unfollowed
* `outgoing_blocks`: Whether to federate blocks to other instances * `outgoing_blocks`: Whether to federate blocks to other instances
* `blockers_visible`: Whether a user can see the posts of users who blocked them
* `deny_follow_blocked`: Whether to disallow following an account that has blocked the user in question * `deny_follow_blocked`: Whether to disallow following an account that has blocked the user in question
* `sign_object_fetches`: Sign object fetches with HTTP signatures * `sign_object_fetches`: Sign object fetches with HTTP signatures
* `authorized_fetch_mode`: Require HTTP signatures for AP fetches * `authorized_fetch_mode`: Require HTTP signatures for AP fetches

View File

@ -128,6 +128,7 @@ def for_user_query(user, opts \\ %{}) do
|> where([user_actor: user_actor], user_actor.is_active) |> where([user_actor: user_actor], user_actor.is_active)
|> exclude_notification_muted(user, exclude_notification_muted_opts) |> exclude_notification_muted(user, exclude_notification_muted_opts)
|> exclude_blocked(user, exclude_blocked_opts) |> exclude_blocked(user, exclude_blocked_opts)
|> exclude_blockers(user)
|> exclude_filtered(user) |> exclude_filtered(user)
|> exclude_visibility(opts) |> exclude_visibility(opts)
end end
@ -141,6 +142,17 @@ defp exclude_blocked(query, user, opts) do
|> FollowingRelationship.keep_following_or_not_domain_blocked(user) |> FollowingRelationship.keep_following_or_not_domain_blocked(user)
end end
defp exclude_blockers(query, user) do
if Pleroma.Config.get([:activitypub, :blockers_visible]) == true do
query
else
blocker_ap_ids = User.incoming_relationships_ungrouped_ap_ids(user, [:block])
query
|> where([n, a], a.actor not in ^blocker_ap_ids)
end
end
defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do
query query
end end

View File

@ -441,6 +441,7 @@ def fetch_activities_for_context_query(context, opts) do
|> maybe_preload_bookmarks(opts) |> maybe_preload_bookmarks(opts)
|> maybe_set_thread_muted_field(opts) |> maybe_set_thread_muted_field(opts)
|> restrict_blocked(opts) |> restrict_blocked(opts)
|> restrict_blockers_visibility(opts)
|> restrict_recipients(recipients, opts[:user]) |> restrict_recipients(recipients, opts[:user])
|> restrict_filtered(opts) |> restrict_filtered(opts)
|> where( |> where(
@ -1028,7 +1029,10 @@ defp restrict_blocked(query, %{blocking_user: %User{} = user} = opts) do
from( from(
[activity, object: o] in query, [activity, object: o] in query,
# You don't block the author
where: fragment("not (? = ANY(?))", activity.actor, ^blocked_ap_ids), where: fragment("not (? = ANY(?))", activity.actor, ^blocked_ap_ids),
# You don't block any recipients, and didn't author the post
where: where:
fragment( fragment(
"((not (? && ?)) or ? = ?)", "((not (? && ?)) or ? = ?)",
@ -1037,12 +1041,18 @@ defp restrict_blocked(query, %{blocking_user: %User{} = user} = opts) do
activity.actor, activity.actor,
^user.ap_id ^user.ap_id
), ),
# You don't block the domain of any recipients, and didn't author the post
where: where:
fragment( fragment(
"recipients_contain_blocked_domains(?, ?) = false", "(recipients_contain_blocked_domains(?, ?) = false) or ? = ?",
activity.recipients, activity.recipients,
^domain_blocks ^domain_blocks,
activity.actor,
^user.ap_id
), ),
# It's not a boost of a user you block
where: where:
fragment( fragment(
"not (?->>'type' = 'Announce' and ?->'to' \\?| ?)", "not (?->>'type' = 'Announce' and ?->'to' \\?| ?)",
@ -1050,6 +1060,8 @@ defp restrict_blocked(query, %{blocking_user: %User{} = user} = opts) do
activity.data, activity.data,
^blocked_ap_ids ^blocked_ap_ids
), ),
# You don't block the author's domain, and also don't follow the author
where: where:
fragment( fragment(
"(not (split_part(?, '/', 3) = ANY(?))) or ? = ANY(?)", "(not (split_part(?, '/', 3) = ANY(?))) or ? = ANY(?)",
@ -1058,6 +1070,8 @@ defp restrict_blocked(query, %{blocking_user: %User{} = user} = opts) do
activity.actor, activity.actor,
^following_ap_ids ^following_ap_ids
), ),
# Same as above, but checks the Object
where: where:
fragment( fragment(
"(not (split_part(?->>'actor', '/', 3) = ANY(?))) or (?->>'actor') = ANY(?)", "(not (split_part(?->>'actor', '/', 3) = ANY(?))) or (?->>'actor') = ANY(?)",
@ -1071,6 +1085,31 @@ defp restrict_blocked(query, %{blocking_user: %User{} = user} = opts) do
defp restrict_blocked(query, _), do: query defp restrict_blocked(query, _), do: query
defp restrict_blockers_visibility(query, %{blocking_user: %User{} = user}) do
if Config.get([:activitypub, :blockers_visible]) == true do
query
else
blocker_ap_ids = User.incoming_relationships_ungrouped_ap_ids(user, [:block])
from(
activity in query,
# The author doesn't block you
where: fragment("not (? = ANY(?))", activity.actor, ^blocker_ap_ids),
# It's not a boost of a user that blocks you
where:
fragment(
"not (?->>'type' = 'Announce' and ?->'to' \\?| ?)",
activity.data,
activity.data,
^blocker_ap_ids
)
)
end
end
defp restrict_blockers_visibility(query, _), do: query
defp restrict_unlisted(query, %{restrict_unlisted: true}) do defp restrict_unlisted(query, %{restrict_unlisted: true}) do
from( from(
activity in query, activity in query,
@ -1297,6 +1336,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_state(opts) |> restrict_state(opts)
|> restrict_favorited_by(opts) |> restrict_favorited_by(opts)
|> restrict_blocked(restrict_blocked_opts) |> restrict_blocked(restrict_blocked_opts)
|> restrict_blockers_visibility(opts)
|> restrict_muted(restrict_muted_opts) |> restrict_muted(restrict_muted_opts)
|> restrict_filtered(opts) |> restrict_filtered(opts)
|> restrict_media(opts) |> restrict_media(opts)

View File

@ -776,6 +776,18 @@ test "doesn't return blocked activities" do
assert Enum.member?(activities, activity_one) assert Enum.member?(activities, activity_one)
end end
test "always see your own posts even when they address people you block" do
user = insert(:user)
blockee = insert(:user)
{:ok, _} = User.block(user, blockee)
{:ok, activity} = CommonAPI.post(user, %{status: "hey! @#{blockee.nickname}"})
activities = ActivityPub.fetch_activities([], %{blocking_user: user})
assert Enum.member?(activities, activity)
end
test "doesn't return transitive interactions concerning blocked users" do test "doesn't return transitive interactions concerning blocked users" do
blocker = insert(:user) blocker = insert(:user)
blockee = insert(:user) blockee = insert(:user)
@ -875,6 +887,21 @@ test "doesn't return activities from blocked domains" do
refute repeat_activity in activities refute repeat_activity in activities
end end
test "see your own posts even when they adress actors from blocked domains" do
user = insert(:user)
domain = "dogwhistle.zone"
domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"})
{:ok, user} = User.block_domain(user, domain)
{:ok, activity} = CommonAPI.post(user, %{status: "hey! @#{domain_user.nickname}"})
activities = ActivityPub.fetch_activities([], %{blocking_user: user})
assert Enum.member?(activities, activity)
end
test "does return activities from followed users on blocked domains" do test "does return activities from followed users on blocked domains" do
domain = "meanies.social" domain = "meanies.social"
domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"}) domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"})

View File

@ -101,6 +101,25 @@ test "by default, does not contain pleroma:report" do
assert [_] = result assert [_] = result
end end
test "excludes mentions from blockers when blockers_visible is false" do
clear_config([:activitypub, :blockers_visible], false)
%{user: user, conn: conn} = oauth_access(["read:notifications"])
blocker = insert(:user)
{:ok, _} = CommonAPI.block(blocker, user)
{:ok, activity} = CommonAPI.post(blocker, %{status: "hi @#{user.nickname}"})
{:ok, [_notification]} = Notification.create_notifications(activity)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/notifications")
assert [] == json_response_and_validate_schema(conn, 200)
end
test "getting a single notification" do test "getting a single notification" do
%{user: user, conn: conn} = oauth_access(["read:notifications"]) %{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user = insert(:user) other_user = insert(:user)

View File

@ -273,6 +273,24 @@ test "doesn't return replies if follower is posting with blocked user" do
[%{"id" => ^reply_from_me}, %{"id" => ^activity_id}] = response [%{"id" => ^reply_from_me}, %{"id" => ^activity_id}] = response
end end
test "doesn't return posts from users who blocked you when :blockers_visible is disabled" do
clear_config([:activitypub, :blockers_visible], false)
%{conn: conn, user: blockee} = oauth_access(["read:statuses"])
blocker = insert(:user)
{:ok, _} = User.block(blocker, blockee)
conn = assign(conn, :user, blockee)
{:ok, _} = CommonAPI.post(blocker, %{status: "hey!"})
response =
get(conn, "/api/v1/timelines/public")
|> json_response_and_validate_schema(200)
assert length(response) == 0
end
test "doesn't return replies if follow is posting with users from blocked domain" do test "doesn't return replies if follow is posting with users from blocked domain" do
%{conn: conn, user: blocker} = oauth_access(["read:statuses"]) %{conn: conn, user: blocker} = oauth_access(["read:statuses"])
friend = insert(:user) friend = insert(:user)