Merge branch 'block-handling' into 'develop'

Handle blocks with the pipeline

See merge request pleroma/pleroma!2685
This commit is contained in:
Haelwenn 2020-06-26 17:41:00 +00:00
commit bf1c5e255a
16 changed files with 285 additions and 163 deletions

View file

@ -1309,7 +1309,8 @@ def block(%User{} = blocker, %User{} = blocked) do
unsubscribe(blocked, blocker) unsubscribe(blocked, blocker)
if following?(blocked, blocker), do: unfollow(blocked, blocker) unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true)
if unfollowing_blocked && following?(blocked, blocker), do: unfollow(blocked, blocker)
{:ok, blocker} = update_follower_count(blocker) {:ok, blocker} = update_follower_count(blocker)
{:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked) {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
@ -1527,8 +1528,7 @@ def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
blocked_identifiers, blocked_identifiers,
fn blocked_identifier -> fn blocked_identifier ->
with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier), with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
{:ok, _user_block} <- block(blocker, blocked), {:ok, _block} <- CommonAPI.block(blocker, blocked) do
{:ok, _} <- ActivityPub.block(blocker, blocked) do
blocked blocked
else else
err -> err ->

View file

@ -366,33 +366,6 @@ defp do_unfollow(follower, followed, activity_id, local) do
end end
end end
@spec block(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | {:error, any()}
def block(blocker, blocked, activity_id \\ nil, local \\ true) do
with {:ok, result} <-
Repo.transaction(fn -> do_block(blocker, blocked, activity_id, local) end) do
result
end
end
defp do_block(blocker, blocked, activity_id, local) do
unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
if unfollow_blocked and fetch_latest_follow(blocker, blocked) do
unfollow(blocker, blocked, nil, local)
end
block_data = make_block_data(blocker, blocked, activity_id)
with {:ok, activity} <- insert(block_data, local),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
{:error, error} -> Repo.rollback(error)
end
end
@spec flag(map()) :: {:ok, Activity.t()} | {:error, any()} @spec flag(map()) :: {:ok, Activity.t()} | {:error, any()}
def flag( def flag(
%{ %{

View file

@ -138,6 +138,18 @@ def update(actor, object) do
}, []} }, []}
end end
@spec block(User.t(), User.t()) :: {:ok, map(), keyword()}
def block(blocker, blocked) do
{:ok,
%{
"id" => Utils.generate_activity_id(),
"type" => "Block",
"actor" => blocker.ap_id,
"object" => blocked.ap_id,
"to" => [blocked.ap_id]
}, []}
end
@spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()} @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
def announce(actor, object, options \\ []) do def announce(actor, object, options \\ []) do
public? = Keyword.get(options, :public, false) public? = Keyword.get(options, :public, false)

View file

@ -13,6 +13,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
@ -24,6 +25,25 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
@spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()} @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
def validate(object, meta) def validate(object, meta)
def validate(%{"type" => "Block"} = block_activity, meta) do
with {:ok, block_activity} <-
block_activity
|> BlockValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
block_activity = stringify_keys(block_activity)
outgoing_blocks = Pleroma.Config.get([:activitypub, :outgoing_blocks])
meta =
if !outgoing_blocks do
Keyword.put(meta, :do_not_federate, true)
else
meta
end
{:ok, block_activity, meta}
end
end
def validate(%{"type" => "Update"} = update_activity, meta) do def validate(%{"type" => "Update"} = update_activity, meta) do
with {:ok, update_activity} <- with {:ok, update_activity} <-
update_activity update_activity

View file

@ -0,0 +1,42 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator do
use Ecto.Schema
alias Pleroma.EctoType.ActivityPub.ObjectValidators
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@primary_key false
embedded_schema do
field(:id, ObjectValidators.ObjectID, primary_key: true)
field(:type, :string)
field(:actor, ObjectValidators.ObjectID)
field(:to, ObjectValidators.Recipients, default: [])
field(:cc, ObjectValidators.Recipients, default: [])
field(:object, ObjectValidators.ObjectID)
end
def cast_data(data) do
%__MODULE__{}
|> cast(data, __schema__(:fields))
end
def validate_data(cng) do
cng
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Block"])
|> validate_actor_presence()
|> validate_actor_presence(field_name: :object)
end
def cast_and_validate(data) do
data
|> cast_data
|> validate_data
end
end

View file

@ -20,6 +20,21 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
def handle(object, meta \\ []) def handle(object, meta \\ [])
# Tasks this handles:
# - Unfollow and block
def handle(
%{data: %{"type" => "Block", "object" => blocked_user, "actor" => blocking_user}} =
object,
meta
) do
with %User{} = blocker <- User.get_cached_by_ap_id(blocking_user),
%User{} = blocked <- User.get_cached_by_ap_id(blocked_user) do
User.block(blocker, blocked)
end
{:ok, object, meta}
end
# Tasks this handles: # Tasks this handles:
# - Update the user # - Update the user
# #

View file

@ -673,7 +673,7 @@ def handle_incoming(
end end
def handle_incoming(%{"type" => type} = data, _options) def handle_incoming(%{"type" => type} = data, _options)
when type in ["Like", "EmojiReact", "Announce"] do when type in ~w{Like EmojiReact Announce} do
with :ok <- ObjectValidator.fetch_actor_and_object(data), with :ok <- ObjectValidator.fetch_actor_and_object(data),
{:ok, activity, _meta} <- {:ok, activity, _meta} <-
Pipeline.common_pipeline(data, local: false) do Pipeline.common_pipeline(data, local: false) do
@ -684,9 +684,10 @@ def handle_incoming(%{"type" => type} = data, _options)
end end
def handle_incoming( def handle_incoming(
%{"type" => "Update"} = data, %{"type" => type} = data,
_options _options
) do )
when type in ~w{Update Block} do
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
{:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
{:ok, activity} {:ok, activity}
@ -765,21 +766,6 @@ def handle_incoming(
end end
end end
def handle_incoming(
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
_options
) do
with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
{:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
User.unfollow(blocker, blocked)
User.block(blocker, blocked)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming( def handle_incoming(
%{ %{
"type" => "Move", "type" => "Move",

View file

@ -25,6 +25,13 @@ defmodule Pleroma.Web.CommonAPI do
require Pleroma.Constants require Pleroma.Constants
require Logger require Logger
def block(blocker, blocked) do
with {:ok, block_data, _} <- Builder.block(blocker, blocked),
{:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
{:ok, block}
end
end
def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]), with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
:ok <- validate_chat_content_length(content, !!maybe_attachment), :ok <- validate_chat_content_length(content, !!maybe_attachment),

View file

@ -385,8 +385,7 @@ def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
@doc "POST /api/v1/accounts/:id/block" @doc "POST /api/v1/accounts/:id/block"
def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
with {:ok, _user_block} <- User.block(blocker, blocked), with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
render(conn, "relationship.json", user: blocker, target: blocked) render(conn, "relationship.json", user: blocker, target: blocked)
else else
{:error, message} -> json_response(conn, :forbidden, %{error: message}) {:error, message} -> json_response(conn, :forbidden, %{error: message})

View file

@ -992,54 +992,6 @@ test "creates an undo activity for a pending follow request" do
end end
end end
describe "blocking" do
test "reverts block activity on error" do
[blocker, blocked] = insert_list(2, :user)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.block(blocker, blocked)
end
assert Repo.aggregate(Activity, :count, :id) == 0
assert Repo.aggregate(Object, :count, :id) == 0
end
test "creates a block activity" do
clear_config([:instance, :federating], true)
blocker = insert(:user)
blocked = insert(:user)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
{:ok, activity} = ActivityPub.block(blocker, blocked)
assert activity.data["type"] == "Block"
assert activity.data["actor"] == blocker.ap_id
assert activity.data["object"] == blocked.ap_id
assert called(Pleroma.Web.Federator.publish(activity))
end
end
test "works with outgoing blocks disabled, but doesn't federate" do
clear_config([:instance, :federating], true)
clear_config([:activitypub, :outgoing_blocks], false)
blocker = insert(:user)
blocked = insert(:user)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
{:ok, activity} = ActivityPub.block(blocker, blocked)
assert activity.data["type"] == "Block"
assert activity.data["actor"] == blocker.ap_id
assert activity.data["object"] == blocked.ap_id
refute called(Pleroma.Web.Federator.publish(:_))
end
end
end
describe "timeline post-processing" do describe "timeline post-processing" do
test "it filters broken threads" do test "it filters broken threads" do
user1 = insert(:user) user1 = insert(:user)

View file

@ -654,4 +654,31 @@ test "returns an error if the object can't be updated by the actor", %{
assert {:error, _cng} = ObjectValidator.validate(update, []) assert {:error, _cng} = ObjectValidator.validate(update, [])
end end
end end
describe "blocks" do
setup do
user = insert(:user, local: false)
blocked = insert(:user)
{:ok, valid_block, []} = Builder.block(user, blocked)
%{user: user, valid_block: valid_block}
end
test "validates a basic object", %{
valid_block: valid_block
} do
assert {:ok, _block, []} = ObjectValidator.validate(valid_block, [])
end
test "returns an error if we don't know the blocked user", %{
valid_block: valid_block
} do
block =
valid_block
|> Map.put("object", "https://gensokyo.2hu/users/raymoo")
assert {:error, _cng} = ObjectValidator.validate(block, [])
end
end
end end

View file

@ -64,6 +64,47 @@ test "it streams out notifications and streams" do
end end
end end
describe "blocking users" do
setup do
user = insert(:user)
blocked = insert(:user)
User.follow(blocked, user)
User.follow(user, blocked)
{:ok, block_data, []} = Builder.block(user, blocked)
{:ok, block, _meta} = ActivityPub.persist(block_data, local: true)
%{user: user, blocked: blocked, block: block}
end
test "it unfollows and blocks", %{user: user, blocked: blocked, block: block} do
assert User.following?(user, blocked)
assert User.following?(blocked, user)
{:ok, _, _} = SideEffects.handle(block)
refute User.following?(user, blocked)
refute User.following?(blocked, user)
assert User.blocks?(user, blocked)
end
test "it blocks but does not unfollow if the relevant setting is set", %{
user: user,
blocked: blocked,
block: block
} do
clear_config([:activitypub, :unfollow_blocked], false)
assert User.following?(user, blocked)
assert User.following?(blocked, user)
{:ok, _, _} = SideEffects.handle(block)
refute User.following?(user, blocked)
assert User.following?(blocked, user)
assert User.blocks?(user, blocked)
end
end
describe "update users" do describe "update users" do
setup do setup do
user = insert(:user) user = insert(:user)
@ -242,8 +283,7 @@ test "when activation is required", %{delete: delete, user: user} do
{:ok, like} = CommonAPI.favorite(user, post.id) {:ok, like} = CommonAPI.favorite(user, post.id)
{:ok, reaction} = CommonAPI.react_with_emoji(post.id, user, "👍") {:ok, reaction} = CommonAPI.react_with_emoji(post.id, user, "👍")
{:ok, announce} = CommonAPI.repeat(post.id, user) {:ok, announce} = CommonAPI.repeat(post.id, user)
{:ok, block} = ActivityPub.block(user, poster) {:ok, block} = CommonAPI.block(user, poster)
User.block(user, poster)
{:ok, undo_data, _meta} = Builder.undo(user, like) {:ok, undo_data, _meta} = Builder.undo(user, like)
{:ok, like_undo, _meta} = ActivityPub.persist(undo_data, local: true) {:ok, like_undo, _meta} = ActivityPub.persist(undo_data, local: true)

View file

@ -0,0 +1,63 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.BlockHandlingTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
import Pleroma.Factory
test "it works for incoming blocks" do
user = insert(:user)
data =
File.read!("test/fixtures/mastodon-block-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
blocker = insert(:user, ap_id: data["actor"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Block"
assert data["object"] == user.ap_id
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert User.blocks?(blocker, user)
end
test "incoming blocks successfully tear down any follow relationship" do
blocker = insert(:user)
blocked = insert(:user)
data =
File.read!("test/fixtures/mastodon-block-activity.json")
|> Poison.decode!()
|> Map.put("object", blocked.ap_id)
|> Map.put("actor", blocker.ap_id)
{:ok, blocker} = User.follow(blocker, blocked)
{:ok, blocked} = User.follow(blocked, blocker)
assert User.following?(blocker, blocked)
assert User.following?(blocked, blocker)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Block"
assert data["object"] == blocked.ap_id
assert data["actor"] == blocker.ap_id
blocker = User.get_cached_by_ap_id(data["actor"])
blocked = User.get_cached_by_ap_id(data["object"])
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
end
end

View file

@ -445,56 +445,6 @@ test "it works for incoming follows to locked account" do
assert [^pending_follower] = User.get_follow_requests(user) assert [^pending_follower] = User.get_follow_requests(user)
end end
test "it works for incoming blocks" do
user = insert(:user)
data =
File.read!("test/fixtures/mastodon-block-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Block"
assert data["object"] == user.ap_id
assert data["actor"] == "http://mastodon.example.org/users/admin"
blocker = User.get_cached_by_ap_id(data["actor"])
assert User.blocks?(blocker, user)
end
test "incoming blocks successfully tear down any follow relationship" do
blocker = insert(:user)
blocked = insert(:user)
data =
File.read!("test/fixtures/mastodon-block-activity.json")
|> Poison.decode!()
|> Map.put("object", blocked.ap_id)
|> Map.put("actor", blocker.ap_id)
{:ok, blocker} = User.follow(blocker, blocked)
{:ok, blocked} = User.follow(blocked, blocker)
assert User.following?(blocker, blocked)
assert User.following?(blocked, blocker)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Block"
assert data["object"] == blocked.ap_id
assert data["actor"] == blocker.ap_id
blocker = User.get_cached_by_ap_id(data["actor"])
blocked = User.get_cached_by_ap_id(data["object"])
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
end
test "it works for incoming accepts which were pre-accepted" do test "it works for incoming accepts which were pre-accepted" do
follower = insert(:user) follower = insert(:user)
followed = insert(:user) followed = insert(:user)

View file

@ -27,16 +27,6 @@ test "fetches the latest Follow activity" do
end end
end end
describe "fetch the latest Block" do
test "fetches the latest Block activity" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, activity} = ActivityPub.block(blocker, blocked)
assert activity == Utils.fetch_latest_block(blocker, blocked)
end
end
describe "determine_explicit_mentions()" do describe "determine_explicit_mentions()" do
test "works with an object that has mentions" do test "works with an object that has mentions" do
object = %{ object = %{
@ -344,9 +334,9 @@ test "fetches last block activities" do
user1 = insert(:user) user1 = insert(:user)
user2 = insert(:user) user2 = insert(:user)
assert {:ok, %Activity{} = _} = ActivityPub.block(user1, user2) assert {:ok, %Activity{} = _} = CommonAPI.block(user1, user2)
assert {:ok, %Activity{} = _} = ActivityPub.block(user1, user2) assert {:ok, %Activity{} = _} = CommonAPI.block(user1, user2)
assert {:ok, %Activity{} = activity} = ActivityPub.block(user1, user2) assert {:ok, %Activity{} = activity} = CommonAPI.block(user1, user2)
assert Utils.fetch_latest_block(user1, user2) == activity assert Utils.fetch_latest_block(user1, user2) == activity
end end

View file

@ -25,6 +25,52 @@ defmodule Pleroma.Web.CommonAPITest do
setup do: clear_config([:instance, :limit]) setup do: clear_config([:instance, :limit])
setup do: clear_config([:instance, :max_pinned_statuses]) setup do: clear_config([:instance, :max_pinned_statuses])
describe "blocking" do
setup do
blocker = insert(:user)
blocked = insert(:user)
User.follow(blocker, blocked)
User.follow(blocked, blocker)
%{blocker: blocker, blocked: blocked}
end
test "it blocks and federates", %{blocker: blocker, blocked: blocked} do
clear_config([:instance, :federating], true)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
assert {:ok, block} = CommonAPI.block(blocker, blocked)
assert block.local
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
assert called(Pleroma.Web.Federator.publish(block))
end
end
test "it blocks and does not federate if outgoing blocks are disabled", %{
blocker: blocker,
blocked: blocked
} do
clear_config([:instance, :federating], true)
clear_config([:activitypub, :outgoing_blocks], false)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
assert {:ok, block} = CommonAPI.block(blocker, blocked)
assert block.local
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
refute called(Pleroma.Web.Federator.publish(block))
end
end
end
describe "posting chat messages" do describe "posting chat messages" do
setup do: clear_config([:instance, :chat_limit]) setup do: clear_config([:instance, :chat_limit])