activitypub: publisher: align sharedinbox usage with AP specification rules
authorAriadne Conill <ariadne@dereferenced.org>
Mon, 22 Jul 2019 02:18:45 +0000 (02:18 +0000)
committerAriadne Conill <ariadne@dereferenced.org>
Mon, 22 Jul 2019 02:38:31 +0000 (02:38 +0000)
While debugging the follow breakage, I observed that our sharedInbox usage
did not match the rules in the specification.  Accordingly, I have better
aligned our usage of sharedInbox with the rules outlined in the ActivityPub
specification.

CHANGELOG.md
lib/pleroma/web/activity_pub/publisher.ex
test/web/activity_pub/publisher_test.exs [new file with mode: 0644]

index 5c7214f98d07950101c2ef45909810a2366b643c..6d1c9ed31d3fe2175dec3e57c3e4ecbf7fed5379 100644 (file)
@@ -22,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - ActivityPub C2S: follower/following collection pages being inaccessible even when authentifucated if `hide_followers`/ `hide_follows` was set
 - Existing user id not being preserved on insert conflict
 - Rich Media: Parser failing when no TTL can be found by image TTL setters
+- ActivityPub S2S: sharedInbox usage has been mostly aligned with the rules in the AP specification.
 
 ### Added
 - MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`)
index c505223f751259d8adc8a5bb63d7ac007c3d5d48..f8a4a4420b6897dda9af9d99daad52f11ef1fcbd 100644 (file)
@@ -112,6 +112,45 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
     |> Enum.map(& &1.ap_id)
   end
 
+  @as_public "https://www.w3.org/ns/activitystreams#Public"
+
+  defp maybe_use_sharedinbox(%User{info: %{source_data: data}}),
+    do: (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
+
+  @doc """
+  Determine a user inbox to use based on heuristics.  These heuristics
+  are based on an approximation of the ``sharedInbox`` rules in the
+  [ActivityPub specification][ap-sharedinbox].
+
+  Please do not edit this function (or its children) without reading
+  the spec, as editing the code is likely to introduce some breakage
+  without some familiarity.
+
+     [ap-sharedinbox]: https://www.w3.org/TR/activitypub/#shared-inbox-delivery
+  """
+  def determine_inbox(
+        %Activity{data: activity_data},
+        %User{info: %{source_data: data}} = user
+      ) do
+    to = activity_data["to"] || []
+    cc = activity_data["cc"] || []
+    type = activity_data["type"]
+
+    cond do
+      type == "Delete" ->
+        maybe_use_sharedinbox(user)
+
+      @as_public in to || @as_public in cc ->
+        maybe_use_sharedinbox(user)
+
+      length(to) + length(cc) > 1 ->
+        maybe_use_sharedinbox(user)
+
+      true ->
+        data["inbox"]
+    end
+  end
+
   @doc """
   Publishes an activity with BCC to all relevant peers.
   """
@@ -166,8 +205,8 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
 
     recipients(actor, activity)
     |> Enum.filter(fn user -> User.ap_enabled?(user) end)
-    |> Enum.map(fn %{info: %{source_data: data}} ->
-      (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
+    |> Enum.map(fn %User{} = user ->
+      determine_inbox(activity, user)
     end)
     |> Enum.uniq()
     |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
diff --git a/test/web/activity_pub/publisher_test.exs b/test/web/activity_pub/publisher_test.exs
new file mode 100644 (file)
index 0000000..2b15b3e
--- /dev/null
@@ -0,0 +1,112 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.PublisherTest do
+  use Pleroma.DataCase
+
+  import Pleroma.Factory
+
+  alias Pleroma.Activity
+  alias Pleroma.Web.ActivityPub.Publisher
+
+  @as_public "https://www.w3.org/ns/activitystreams#Public"
+
+  describe "determine_inbox/2" do
+    test "it returns sharedInbox for messages involving as:Public in to" do
+      user =
+        insert(:user, %{
+          info: %{source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}}}
+        })
+
+      activity = %Activity{
+        data: %{"to" => [@as_public], "cc" => [user.follower_address]}
+      }
+
+      assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
+    end
+
+    test "it returns sharedInbox for messages involving as:Public in cc" do
+      user =
+        insert(:user, %{
+          info: %{source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}}}
+        })
+
+      activity = %Activity{
+        data: %{"cc" => [@as_public], "to" => [user.follower_address]}
+      }
+
+      assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
+    end
+
+    test "it returns sharedInbox for messages involving multiple recipients in to" do
+      user =
+        insert(:user, %{
+          info: %{source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}}}
+        })
+
+      user_two = insert(:user)
+      user_three = insert(:user)
+
+      activity = %Activity{
+        data: %{"cc" => [], "to" => [user.ap_id, user_two.ap_id, user_three.ap_id]}
+      }
+
+      assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
+    end
+
+    test "it returns sharedInbox for messages involving multiple recipients in cc" do
+      user =
+        insert(:user, %{
+          info: %{source_data: %{"endpoints" => %{"sharedInbox" => "http://example.com/inbox"}}}
+        })
+
+      user_two = insert(:user)
+      user_three = insert(:user)
+
+      activity = %Activity{
+        data: %{"to" => [], "cc" => [user.ap_id, user_two.ap_id, user_three.ap_id]}
+      }
+
+      assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
+    end
+
+    test "it returns sharedInbox for messages involving multiple recipients in total" do
+      user =
+        insert(:user, %{
+          info: %{
+            source_data: %{
+              "inbox" => "http://example.com/personal-inbox",
+              "endpoints" => %{"sharedInbox" => "http://example.com/inbox"}
+            }
+          }
+        })
+
+      user_two = insert(:user)
+
+      activity = %Activity{
+        data: %{"to" => [user_two.ap_id], "cc" => [user.ap_id]}
+      }
+
+      assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
+    end
+
+    test "it returns inbox for messages involving single recipients in total" do
+      user =
+        insert(:user, %{
+          info: %{
+            source_data: %{
+              "inbox" => "http://example.com/personal-inbox",
+              "endpoints" => %{"sharedInbox" => "http://example.com/inbox"}
+            }
+          }
+        })
+
+      activity = %Activity{
+        data: %{"to" => [user.ap_id], "cc" => []}
+      }
+
+      assert Publisher.determine_inbox(activity, user) == "http://example.com/personal-inbox"
+    end
+  end
+end