Ingestion Pipeline: Undo-Follow aka Unfollow

This commit is contained in:
Haelwenn (lanodan) Monnier 2021-06-17 18:43:46 +02:00
parent 173e977e28
commit bd977a3c3f
No known key found for this signature in database
GPG Key ID: D5B7A8E43C997DEE
7 changed files with 66 additions and 111 deletions

View File

@ -983,7 +983,7 @@ defmodule Pleroma.User do
end end
def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
{:error, "Not subscribed!"} {:error, "Can't unfollow yourself!"}
end end
@spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()} @spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
@ -1771,17 +1771,11 @@ defmodule Pleroma.User do
# Remove all relationships # Remove all relationships
user user
|> get_followers() |> get_followers()
|> Enum.each(fn follower -> |> Enum.each(fn follower -> ActivityPub.unfollow(follower, user) end)
ActivityPub.unfollow(follower, user)
unfollow(follower, user)
end)
user user
|> get_friends() |> get_friends()
|> Enum.each(fn followed -> |> Enum.each(fn followed -> ActivityPub.unfollow(user, followed) end)
ActivityPub.unfollow(user, followed)
unfollow(user, followed)
end)
delete_user_activities(user) delete_user_activities(user)
delete_notifications_from_user_activities(user) delete_notifications_from_user_activities(user)

View File

@ -20,8 +20,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Upload alias Pleroma.Upload
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Streamer alias Pleroma.Web.Streamer
alias Pleroma.Web.WebFinger alias Pleroma.Web.WebFinger
alias Pleroma.Workers.BackgroundWorker alias Pleroma.Workers.BackgroundWorker
@ -322,22 +325,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end end
end end
@spec unfollow(User.t(), User.t(), String.t() | nil, boolean()) :: @spec unfollow(User.t(), User.t()) :: {:ok, Activity.t()} | nil | {:error, any()}
{:ok, Activity.t()} | nil | {:error, any()} def unfollow(follower, followed) do
def unfollow(follower, followed, activity_id \\ nil, local \\ true) do with {:ok, result} <- Repo.transaction(fn -> do_unfollow(follower, followed) end) do
with {:ok, result} <-
Repo.transaction(fn -> do_unfollow(follower, followed, activity_id, local) end) do
result result
end end
end end
defp do_unfollow(follower, followed, activity_id, local) do defp do_unfollow(follower, followed) do
with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed), with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"), {:ok, unfollow_data, _meta} <- Builder.undo(follower, follow_activity),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id), {:ok, activity, _meta} <- Pipeline.common_pipeline(unfollow_data, local: true) do
{:ok, activity} <- insert(unfollow_data, local),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
else else
nil -> nil nil -> nil

View File

@ -254,10 +254,12 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
end end
@impl true @impl true
def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, meta) do def handle(%{data: %{"type" => "Undo", "object" => undone_activity}} = object, meta) do
with undone_object <- Activity.get_by_ap_id(undone_object), with undone_activity <- Activity.get_by_ap_id(undone_activity),
:ok <- handle_undoing(undone_object) do :ok <- handle_undoing(undone_activity) do
{:ok, object, meta} {:ok, object, meta}
else
e -> {:error, e}
end end
end end
@ -456,35 +458,48 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
end end
end end
def handle_undoing(%{data: %{"type" => "Like"}} = object) do def handle_undoing(%{data: %{"type" => "Like"}} = activity) do
object.data["object"] activity.data["object"]
|> Object.get_by_ap_id() |> Object.get_by_ap_id()
|> undo_like(object) |> undo_like(activity)
end end
def handle_undoing(%{data: %{"type" => "EmojiReact"}} = object) do def handle_undoing(%{data: %{"type" => "EmojiReact"}} = activity) do
with %Object{} = reacted_object <- Object.get_by_ap_id(object.data["object"]), with %Object{} = reacted_object <- Object.get_by_ap_id(activity.data["object"]),
{:ok, _} <- Utils.remove_emoji_reaction_from_object(object, reacted_object), {:ok, _} <- Utils.remove_emoji_reaction_from_object(activity, reacted_object),
{:ok, _} <- Repo.delete(object) do {:ok, _} <- Repo.delete(activity) do
:ok :ok
end end
end end
def handle_undoing(%{data: %{"type" => "Announce"}} = object) do def handle_undoing(%{data: %{"type" => "Announce"}} = activity) do
with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]), with %Object{} = liked_object <- Object.get_by_ap_id(activity.data["object"]),
{:ok, _} <- Utils.remove_announce_from_object(object, liked_object), {:ok, _} <- Utils.remove_announce_from_object(activity, liked_object),
{:ok, _} <- Repo.delete(object) do {:ok, _} <- Repo.delete(activity) do
:ok :ok
end end
end end
def handle_undoing( def handle_undoing(
%{data: %{"type" => "Block", "actor" => blocker, "object" => blocked}} = object %{data: %{"type" => "Block", "actor" => blocker, "object" => blocked}} = activity
) do ) do
with %User{} = blocker <- User.get_cached_by_ap_id(blocker), with %User{} = blocker <- User.get_cached_by_ap_id(blocker),
%User{} = blocked <- User.get_cached_by_ap_id(blocked), %User{} = blocked <- User.get_cached_by_ap_id(blocked),
{:ok, _} <- User.unblock(blocker, blocked), {:ok, _} <- User.unblock(blocker, blocked),
{:ok, _} <- Repo.delete(object) do {:ok, _} <- Repo.delete(activity) do
:ok
end
end
def handle_undoing(
%{data: %{"type" => "Follow", "object" => followed, "actor" => follower}} = activity
) do
with %User{} = follower <- User.get_cached_by_ap_id(follower),
%User{} = followed <- User.get_cached_by_ap_id(followed),
{:ok, _activity} <- Utils.update_follow_state_for_all(activity, "reject"),
{:ok, nil} <- FollowingRelationship.update(follower, followed, :follow_reject) do
Notification.dismiss(activity)
:ok :ok
end end
end end

View File

@ -516,52 +516,30 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end end
def handle_incoming( def handle_incoming(
%{ %{"type" => "Undo", "object" => %{"type" => objtype, "id" => object_id}} = data,
"type" => "Undo",
"object" => %{"type" => "Follow", "object" => followed},
"actor" => follower,
"id" => id
} = _data,
_options
) do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
{:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
User.unfollow(follower, followed)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => type}
} = data,
_options _options
) )
when type in ["Like", "EmojiReact", "Announce", "Block"] do when objtype in ~w[Like EmojiReact Announce Block Follow] do
with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
{_, %Activity{}} <- {:exists, Activity.get_by_ap_id(object_id)},
{:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
{:ok, activity} {:ok, activity}
else
{:error, _} = e -> e
e -> {:error, e}
end end
end end
# For Undos that don't have the complete object attached, try to find it in our database. # For Undos that don't have the complete object attached, try to find it in our database.
def handle_incoming( def handle_incoming(%{"type" => "Undo", "object" => object} = activity, options)
%{
"type" => "Undo",
"object" => object
} = activity,
options
)
when is_binary(object) do when is_binary(object) do
with %Activity{data: data} <- Activity.get_by_ap_id(object) do with %Activity{data: data} <- Activity.get_by_ap_id(object) do
activity activity
|> Map.put("object", data) |> Map.put("object", data)
|> handle_incoming(options) |> handle_incoming(options)
else else
_e -> :error {:error, _} = e -> e
e -> {:error, e}
end end
end end

View File

@ -572,25 +572,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> Maps.put_if_present("id", activity_id) |> Maps.put_if_present("id", activity_id)
end end
def make_undo_data(
%User{ap_id: actor, follower_address: follower_address},
%Activity{
data: %{"id" => undone_activity_id, "context" => context},
actor: undone_activity_actor
},
activity_id \\ nil
) do
%{
"type" => "Undo",
"actor" => actor,
"object" => undone_activity_id,
"to" => [follower_address, undone_activity_actor],
"cc" => [Pleroma.Constants.as_public()],
"context" => context
}
|> Maps.put_if_present("id", activity_id)
end
@spec add_announce_to_object(Activity.t(), Object.t()) :: @spec add_announce_to_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()} {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def add_announce_to_object( def add_announce_to_object(
@ -624,18 +605,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do
defp take_announcements(_), do: [] defp take_announcements(_), do: []
#### Unfollow-related helpers
def make_unfollow_data(follower, followed, follow_activity, activity_id) do
%{
"type" => "Undo",
"actor" => follower.ap_id,
"to" => [followed.ap_id],
"object" => follow_activity.data
}
|> Maps.put_if_present("id", activity_id)
end
#### Block-related helpers #### Block-related helpers
@spec fetch_latest_block(User.t(), User.t()) :: Activity.t() | nil @spec fetch_latest_block(User.t(), User.t()) :: Activity.t() | nil
def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do

View File

@ -32,7 +32,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.UndoHandlingTest do
assert activity.data["type"] == "Undo" assert activity.data["type"] == "Undo"
end end
test "it returns an error for incoming unlikes wihout a like activity" do test "it returns an error for incoming unlikes without a like activity" do
user = insert(:user) user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "leave a like pls"}) {:ok, activity} = CommonAPI.post(user, %{status: "leave a like pls"})
@ -41,7 +41,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.UndoHandlingTest do
|> Jason.decode!() |> Jason.decode!()
|> Map.put("object", activity.data["object"]) |> Map.put("object", activity.data["object"])
assert Transmogrifier.handle_incoming(data) == :error assert Transmogrifier.handle_incoming(data) == {:error, nil}
end end
test "it works for incoming unlikes with an existing like activity" do test "it works for incoming unlikes with an existing like activity" do

View File

@ -15,6 +15,7 @@ defmodule Pleroma.Web.CommonAPITest do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
@ -1213,15 +1214,15 @@ defmodule Pleroma.Web.CommonAPITest do
assert {:ok, follower} = CommonAPI.unfollow(follower, followed) assert {:ok, follower} = CommonAPI.unfollow(follower, followed)
assert User.get_follow_state(follower, followed) == nil assert User.get_follow_state(follower, followed) == nil
assert %{id: ^activity_id, data: %{"state" => "cancelled"}} = assert %{id: ^activity_id, data: %{"state" => "reject"}} =
Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed) Utils.fetch_latest_follow(follower, followed)
assert %{ assert %{
data: %{ data: %{
"type" => "Undo", "type" => "Undo",
"object" => %{"type" => "Follow", "state" => "cancelled"} "object" => %{"type" => "Follow", "state" => "reject"}
} }
} = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower) } = Utils.fetch_latest_undo(follower)
end end
test "cancels a pending follow for a remote user" do test "cancels a pending follow for a remote user" do
@ -1235,15 +1236,15 @@ defmodule Pleroma.Web.CommonAPITest do
assert {:ok, follower} = CommonAPI.unfollow(follower, followed) assert {:ok, follower} = CommonAPI.unfollow(follower, followed)
assert User.get_follow_state(follower, followed) == nil assert User.get_follow_state(follower, followed) == nil
assert %{id: ^activity_id, data: %{"state" => "cancelled"}} = assert %{id: ^activity_id, data: %{"state" => "reject"}} =
Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed) Utils.fetch_latest_follow(follower, followed)
assert %{ assert %{
data: %{ data: %{
"type" => "Undo", "type" => "Undo",
"object" => %{"type" => "Follow", "state" => "cancelled"} "object" => %{"type" => "Follow", "state" => "reject"}
} }
} = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower) } = Utils.fetch_latest_undo(follower)
end end
end end