Compare commits

...

6 commits

Author SHA1 Message Date
Haelwenn b6d44ce578 Merge branch 'features/ingestion-unfollow' into 'develop'
Draft: Ingestion Pipeline: Undo-Follow aka Unfollow

See merge request pleroma/pleroma!3486
2024-04-25 15:47:17 +00:00
lain 50af909c01 Merge branch 'pleroma-card-image-description' into 'develop'
Include image description in status media cards

See merge request pleroma/pleroma!4101
2024-04-19 07:39:05 +00:00
marcin mikołajczak 6f6bede900 Include image description in status media cards
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2024-04-19 10:20:31 +04:00
lain 87b8ac3ce6 Merge branch 'receiverworker-error-handling' into 'develop'
ReceiverWorker: Make sure non-{:ok, _} is returned as {:error, …}

See merge request pleroma/pleroma!4100
2024-04-19 06:04:44 +00:00
Haelwenn (lanodan) Monnier a299ddb10e
ReceiverWorker: Make sure non-{:ok, _} is returned as {:error, …}
Otherwise an error like `{:signature, {:error, {:error, :not_found}}}` ends up considered a success.
2024-04-17 07:43:47 +02:00
Haelwenn (lanodan) Monnier bd977a3c3f
Ingestion Pipeline: Undo-Follow aka Unfollow 2021-07-21 09:19:28 +02:00
14 changed files with 78 additions and 112 deletions

View file

@ -0,0 +1 @@
Include image description in status media cards

View file

@ -0,0 +1 @@
ReceiverWorker: Make sure non-{:ok, _} is returned as {:error, …}

View file

@ -1105,7 +1105,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()}
@ -1975,17 +1975,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
@ -359,22 +362,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

@ -268,10 +268,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
@ -545,35 +547,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

@ -534,52 +534,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

@ -601,25 +601,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(
@ -653,18 +634,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

@ -58,6 +58,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
format: :uri, format: :uri,
description: "Preview thumbnail" description: "Preview thumbnail"
}, },
image_description: %Schema{
type: :string,
description: "Alternate text that describes what is in the thumbnail"
},
title: %Schema{type: :string, description: "Title of linked resource"}, title: %Schema{type: :string, description: "Title of linked resource"},
description: %Schema{type: :string, description: "Description of preview"} description: %Schema{type: :string, description: "Description of preview"}
} }

View file

@ -589,6 +589,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
provider_url: page_url_data.scheme <> "://" <> page_url_data.host, provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
url: page_url, url: page_url,
image: image_url, image: image_url,
image_description: rich_media["image:alt"] || "",
title: rich_media["title"] || "", title: rich_media["title"] || "",
description: rich_media["description"] || "", description: rich_media["description"] || "",
pleroma: %{ pleroma: %{

View file

@ -52,7 +52,8 @@ defmodule Pleroma.Workers.ReceiverWorker do
{:error, {:reject, reason}} -> {:cancel, reason} {:error, {:reject, reason}} -> {:cancel, reason}
{:signature, false} -> {:cancel, :invalid_signature} {:signature, false} -> {:cancel, :invalid_signature}
{:error, {:error, reason = "Object has been deleted"}} -> {:cancel, reason} {:error, {:error, reason = "Object has been deleted"}} -> {:cancel, reason}
e -> e {:error, _} = e -> e
e -> {:error, e}
end end
end end
end end

View file

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

@ -16,6 +16,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
@ -1433,15 +1434,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
@ -1455,15 +1456,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

View file

@ -1717,6 +1717,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
card_data = %{ card_data = %{
"image" => "http://ia.media-imdb.com/images/rock.jpg", "image" => "http://ia.media-imdb.com/images/rock.jpg",
"image_description" => "",
"provider_name" => "example.com", "provider_name" => "example.com",
"provider_url" => "https://example.com", "provider_url" => "https://example.com",
"title" => "The Rock", "title" => "The Rock",
@ -1770,6 +1771,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
"title" => "Pleroma", "title" => "Pleroma",
"description" => "", "description" => "",
"image" => nil, "image" => nil,
"image_description" => "",
"provider_name" => "example.com", "provider_name" => "example.com",
"provider_url" => "https://example.com", "provider_url" => "https://example.com",
"url" => "https://example.com/ogp-missing-data", "url" => "https://example.com/ogp-missing-data",

View file

@ -773,6 +773,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
page_url = "http://example.com" page_url = "http://example.com"
card = %{ card = %{
"image:alt" => "Example image description",
url: page_url, url: page_url,
site_name: "Example site name", site_name: "Example site name",
title: "Example website", title: "Example website",
@ -780,7 +781,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
description: "Example description" description: "Example description"
} }
%{provider_name: "example.com"} = %{provider_name: "example.com", image_description: "Example image description"} =
StatusView.render("card.json", %{page_url: page_url, rich_media: card}) StatusView.render("card.json", %{page_url: page_url, rich_media: card})
end end