Resolve follow activity from accept/reject without ID (#328)
authorfloatingghost <hannah@coffee-and-dreams.uk>
Fri, 2 Dec 2022 11:12:37 +0000 (11:12 +0000)
committerfloatingghost <hannah@coffee-and-dreams.uk>
Fri, 2 Dec 2022 11:12:37 +0000 (11:12 +0000)
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/328

lib/pleroma/activity.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/object_validators/accept_reject_validator.ex
test/pleroma/web/activity_pub/transmogrifier/reject_handling_test.exs
test/support/factory.ex

index b01a838d865363aaad1fc63ecba09180d1146f5d..c5b51474269fb7afd76494cdf003c512db472752 100644 (file)
@@ -367,6 +367,14 @@ defmodule Pleroma.Activity do
     |> Repo.all()
   end
 
+  def follow_activity(%User{ap_id: ap_id}, %User{ap_id: followed_ap_id}) do
+    Queries.by_type("Follow")
+    |> where([a], a.actor == ^ap_id)
+    |> where([a], fragment("?->>'object' = ?", a.data, ^followed_ap_id))
+    |> where([a], fragment("?->>'state'", a.data) in ["pending", "accept"])
+    |> Repo.one()
+  end
+
   def restrict_deactivated_users(query) do
     query
     |> join(
index db5dbc815f248e45f1c426f135afbb2e314336be..a4f1c7041969510c30a2a2cecefb9ac6f98132ff 100644 (file)
@@ -105,7 +105,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     end
   end
 
-  @unpersisted_activity_types ~w[Undo Delete Remove]
+  @unpersisted_activity_types ~w[Undo Delete Remove Accept Reject]
   @impl true
   def persist(%{"type" => type} = object, [local: false] = meta)
       when type in @unpersisted_activity_types do
index 7c3c8d0fad9bc21ea58ec76055ae129da30d7ada..847d0be6222ea9050fe0f596ad01087b9f03312f 100644 (file)
@@ -6,6 +6,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator do
   use Ecto.Schema
 
   alias Pleroma.Activity
+  alias Pleroma.Object
+  alias Pleroma.User
 
   import Ecto.Changeset
   import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@@ -29,7 +31,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator do
 
   defp validate_data(cng) do
     cng
-    |> validate_required([:id, :type, :actor, :to, :cc, :object])
+    |> validate_required([:type, :actor, :to, :cc, :object])
     |> validate_inclusion(:type, ["Accept", "Reject"])
     |> validate_actor_presence()
     |> validate_object_presence(allowed_types: ["Follow"])
@@ -38,6 +40,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator do
 
   def cast_and_validate(data) do
     data
+    |> maybe_fetch_object()
     |> cast_data
     |> validate_data
   end
@@ -53,4 +56,31 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AcceptRejectValidator do
         |> add_error(:actor, "can't accept or reject the given activity")
     end
   end
+
+  defp maybe_fetch_object(%{"object" => %{} = object} = activity) do
+    # If we don't have an ID, we may have to fetch the object
+    if Map.has_key?(object, "id") do
+      # Do nothing
+      activity
+    else
+      Map.put(activity, "object", fetch_transient_object(object))
+    end
+  end
+
+  defp maybe_fetch_object(activity), do: activity
+
+  defp fetch_transient_object(
+         %{"actor" => actor, "object" => target, "type" => "Follow"} = object
+       ) do
+    with %User{} = actor <- User.get_cached_by_ap_id(actor),
+         %User{local: true} = target <- User.get_cached_by_ap_id(target),
+         %Activity{} = activity <- Activity.follow_activity(actor, target) do
+      activity.data
+    else
+      _e ->
+        object
+    end
+  end
+
+  defp fetch_transient_object(_), do: {:error, "not a supported transient object"}
 end
index 355e664d434d41cc173b2a0b2fa43984a1243df5..52a61f3103ca3ceb0586ee17b2a49b768e5ba2b8 100644 (file)
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.RejectHandlingTest do
   alias Pleroma.Activity
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.Transmogrifier
+  alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.CommonAPI
 
   import Pleroma.Factory
@@ -53,6 +54,81 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.RejectHandlingTest do
     assert User.following?(follower, followed) == false
   end
 
+  describe "when accept/reject references a transient activity" do
+    test "it handles accept activities that do not contain an ID key" do
+      follower = insert(:user)
+      followed = insert(:user, is_locked: true)
+
+      pending_follow =
+        insert(:follow_activity, follower: follower, followed: followed, state: "pending")
+
+      refute User.following?(follower, followed)
+
+      without_id = Map.delete(pending_follow.data, "id")
+
+      reject_data =
+        File.read!("test/fixtures/mastodon-reject-activity.json")
+        |> Jason.decode!()
+        |> Map.put("actor", followed.ap_id)
+        |> Map.delete("id")
+        |> Map.put("object", without_id)
+
+      {:ok, %Activity{data: _}} = Transmogrifier.handle_incoming(reject_data)
+
+      follower = User.get_cached_by_id(follower.id)
+
+      refute User.following?(follower, followed)
+      assert Utils.fetch_latest_follow(follower, followed).data["state"] == "reject"
+    end
+
+    test "it handles reject activities that do not contain an ID key" do
+      follower = insert(:user)
+      followed = insert(:user)
+      {:ok, follower, followed} = User.follow(follower, followed)
+      {:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed)
+      assert Utils.fetch_latest_follow(follower, followed).data["state"] == "accept"
+      assert User.following?(follower, followed)
+
+      without_id = Map.delete(follow_activity.data, "id")
+
+      reject_data =
+        File.read!("test/fixtures/mastodon-reject-activity.json")
+        |> Jason.decode!()
+        |> Map.put("actor", followed.ap_id)
+        |> Map.delete("id")
+        |> Map.put("object", without_id)
+
+      {:ok, %Activity{data: _}} = Transmogrifier.handle_incoming(reject_data)
+
+      follower = User.get_cached_by_id(follower.id)
+
+      refute User.following?(follower, followed)
+      assert Utils.fetch_latest_follow(follower, followed).data["state"] == "reject"
+    end
+
+    test "it does not accept follows that are not in pending or accepted" do
+      follower = insert(:user)
+      followed = insert(:user, is_locked: true)
+
+      rejected_follow =
+        insert(:follow_activity, follower: follower, followed: followed, state: "reject")
+
+      refute User.following?(follower, followed)
+
+      without_id = Map.delete(rejected_follow.data, "id")
+
+      accept_data =
+        File.read!("test/fixtures/mastodon-accept-activity.json")
+        |> Jason.decode!()
+        |> Map.put("actor", followed.ap_id)
+        |> Map.put("object", without_id)
+
+      {:error, _} = Transmogrifier.handle_incoming(accept_data)
+
+      refute User.following?(follower, followed)
+    end
+  end
+
   test "it rejects activities without a valid ID" do
     user = insert(:user)
 
index 904987aaf464d5ca64dd215ba41e183b535b2670..3e426c565594ac8299d8acfa447e7058202f1e1a 100644 (file)
@@ -452,15 +452,16 @@ defmodule Pleroma.Factory do
     }
   end
 
-  def follow_activity_factory do
-    follower = insert(:user)
-    followed = insert(:user)
+  def follow_activity_factory(attrs \\ %{}) do
+    follower = attrs[:follower] || insert(:user)
+    followed = attrs[:followed] || insert(:user)
 
     data = %{
       "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
       "actor" => follower.ap_id,
       "type" => "Follow",
       "object" => followed.ap_id,
+      "state" => attrs[:state] || "pending",
       "published_at" => DateTime.utc_now() |> DateTime.to_iso8601()
     }