Bugfix: Reuse Controller.Helper pagination for APC2S
authorHaelwenn (lanodan) Monnier <contact@hacktivis.me>
Thu, 7 May 2020 19:52:45 +0000 (21:52 +0200)
committerHaelwenn (lanodan) Monnier <contact@hacktivis.me>
Fri, 29 May 2020 14:14:45 +0000 (16:14 +0200)
lib/pleroma/web/activity_pub/activity_pub_controller.ex
lib/pleroma/web/activity_pub/views/user_view.ex
lib/pleroma/web/controller_helper.ex
lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
test/web/activity_pub/activity_pub_controller_test.exs
test/web/activity_pub/views/user_view_test.exs

index 28727d619ea3d9180ecb811cf4ca6f0db2186103..b624d4255c0f631833e951bd8ddb1c15ad00080b 100644 (file)
@@ -21,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
   alias Pleroma.Web.ActivityPub.UserView
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.ActivityPub.Visibility
+  alias Pleroma.Web.ControllerHelper
   alias Pleroma.Web.Endpoint
   alias Pleroma.Web.FederatingPlug
   alias Pleroma.Web.Federator
@@ -251,6 +252,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
       |> put_view(UserView)
       |> render("activity_collection_page.json", %{
         activities: activities,
+        pagination: ControllerHelper.get_pagination_fields(conn, activities, %{"limit" => "10"}),
         iri: "#{user.ap_id}/outbox"
       })
     end
@@ -368,6 +370,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
     |> put_view(UserView)
     |> render("activity_collection_page.json", %{
       activities: activities,
+      pagination: ControllerHelper.get_pagination_fields(conn, activities, %{"limit" => "10"}),
       iri: "#{user.ap_id}/inbox"
     })
   end
index 34590b16d8f1950e5935dfa3b4efbcdc6ea80ca1..4a02b09a17dd3f731edf5d74daf62b8bcea839a6 100644 (file)
@@ -213,34 +213,24 @@ defmodule Pleroma.Web.ActivityPub.UserView do
     |> Map.merge(Utils.make_json_ld_header())
   end
 
-  def render("activity_collection_page.json", %{activities: activities, iri: iri}) do
-    # this is sorted chronologically, so first activity is the newest (max)
-    {max_id, min_id, collection} =
-      if length(activities) > 0 do
-        {
-          Enum.at(activities, 0).id,
-          Enum.at(Enum.reverse(activities), 0).id,
-          Enum.map(activities, fn act ->
-            {:ok, data} = Transmogrifier.prepare_outgoing(act.data)
-            data
-          end)
-        }
-      else
-        {
-          0,
-          0,
-          []
-        }
-      end
+  def render("activity_collection_page.json", %{
+        activities: activities,
+        iri: iri,
+        pagination: pagination
+      }) do
+    collection =
+      Enum.map(activities, fn activity ->
+        {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
+        data
+      end)
 
     %{
-      "id" => "#{iri}?max_id=#{max_id}&page=true",
       "type" => "OrderedCollectionPage",
       "partOf" => iri,
-      "orderedItems" => collection,
-      "next" => "#{iri}?max_id=#{min_id}&page=true"
+      "orderedItems" => collection
     }
     |> Map.merge(Utils.make_json_ld_header())
+    |> Map.merge(pagination)
   end
 
   defp maybe_put_total_items(map, false, _total), do: map
index 5a1316a5f166f5871be0cb75e059de9299f96735..2d35bb56cddd3e5b568b16a587e9f3eab2b91ab6 100644 (file)
@@ -5,6 +5,8 @@
 defmodule Pleroma.Web.ControllerHelper do
   use Pleroma.Web, :controller
 
+  alias Pleroma.Pagination
+
   # As in Mastodon API, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html
   @falsy_param_values [false, 0, "0", "f", "F", "false", "False", "FALSE", "off", "OFF"]
 
@@ -46,6 +48,16 @@ defmodule Pleroma.Web.ControllerHelper do
     do: conn
 
   def add_link_headers(conn, activities, extra_params) do
+    case get_pagination_fields(conn, activities, extra_params) do
+      %{"next" => next_url, "prev" => prev_url} ->
+        put_resp_header(conn, "link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
+
+      _ ->
+        conn
+    end
+  end
+
+  def get_pagination_fields(conn, activities, extra_params \\ %{}) do
     case List.last(activities) do
       %{id: max_id} ->
         params =
@@ -54,29 +66,29 @@ defmodule Pleroma.Web.ControllerHelper do
           |> Map.drop(["since_id", "max_id", "min_id"])
           |> Map.merge(extra_params)
 
-        limit =
-          params
-          |> Map.get("limit", "20")
-          |> String.to_integer()
-
         min_id =
-          if length(activities) <= limit do
-            activities
-            |> List.first()
-            |> Map.get(:id)
-          else
-            activities
-            |> Enum.at(limit * -1)
-            |> Map.get(:id)
-          end
-
-        next_url = current_url(conn, Map.merge(params, %{max_id: max_id}))
-        prev_url = current_url(conn, Map.merge(params, %{min_id: min_id}))
-
-        put_resp_header(conn, "link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
+          activities
+          |> List.first()
+          |> Map.get(:id)
+
+        fields = %{
+          "next" => current_url(conn, Map.put(params, :max_id, max_id)),
+          "prev" => current_url(conn, Map.put(params, :min_id, min_id))
+        }
+
+        #  Generating an `id` without already present pagination keys would
+        # need a query-restriction with an `q.id >= ^id` or `q.id <= ^id`
+        # instead of the `q.id > ^min_id` and `q.id < ^max_id`.
+        #  This is because we only have ids present inside of the page, while
+        # `min_id`, `since_id` and `max_id` requires to know one outside of it.
+        if Map.take(conn.params, Pagination.page_keys() -- ["limit", "order"]) != [] do
+          Map.put(fields, "id", current_url(conn, conn.params))
+        else
+          fields
+        end
 
       _ ->
-        conn
+        %{}
     end
   end
 
index 958567510bcc22d26e24c9c8bc456d6e28463476..c852082a5a1485d12addb07231a8eef91576639d 100644 (file)
@@ -51,10 +51,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
       |> Map.put("reply_filtering_user", user)
       |> Map.put("user", user)
 
-    recipients = [user.ap_id | User.following(user)]
-
     activities =
-      recipients
+      [user.ap_id | User.following(user)]
       |> ActivityPub.fetch_activities(params)
       |> Enum.reverse()
 
index 24edab41ae4f5595d6aa4baf8c1780dc8b392aad..3f48553c98a1fbbbe69598a09bb329047c7092f4 100644 (file)
@@ -804,17 +804,63 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
   end
 
   describe "GET /users/:nickname/outbox" do
+    test "it paginates correctly", %{conn: conn} do
+      user = insert(:user)
+      conn = assign(conn, :user, user)
+      outbox_endpoint = user.ap_id <> "/outbox"
+
+      _posts =
+        for i <- 0..15 do
+          {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"})
+          activity
+        end
+
+      result =
+        conn
+        |> put_req_header("accept", "application/activity+json")
+        |> get(outbox_endpoint <> "?page=true")
+        |> json_response(200)
+
+      result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end)
+      assert length(result["orderedItems"]) == 10
+      assert length(result_ids) == 10
+      assert result["next"]
+      assert String.starts_with?(result["next"], outbox_endpoint)
+
+      result_next =
+        conn
+        |> put_req_header("accept", "application/activity+json")
+        |> get(result["next"])
+        |> json_response(200)
+
+      result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end)
+      assert length(result_next["orderedItems"]) == 6
+      assert length(result_next_ids) == 6
+      refute Enum.find(result_next_ids, fn x -> x in result_ids end)
+      refute Enum.find(result_ids, fn x -> x in result_next_ids end)
+      assert String.starts_with?(result["id"], outbox_endpoint)
+
+      result_next_again =
+        conn
+        |> put_req_header("accept", "application/activity+json")
+        |> get(result_next["id"])
+        |> json_response(200)
+
+      assert result_next == result_next_again
+    end
+
     test "it returns 200 even if there're no activities", %{conn: conn} do
       user = insert(:user)
+      outbox_endpoint = user.ap_id <> "/outbox"
 
       conn =
         conn
         |> assign(:user, user)
         |> put_req_header("accept", "application/activity+json")
-        |> get("/users/#{user.nickname}/outbox")
+        |> get(outbox_endpoint)
 
       result = json_response(conn, 200)
-      assert user.ap_id <> "/outbox" == result["id"]
+      assert outbox_endpoint == result["id"]
     end
 
     test "it returns a note activity in a collection", %{conn: conn} do
index 20b0f223c8b6506d4a14154a0a00eff3e933b3f6..bec15a996fd256ba56300402679a1cef60820b41 100644 (file)
@@ -158,35 +158,4 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
       assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user})
     end
   end
-
-  test "activity collection page aginates correctly" do
-    user = insert(:user)
-
-    posts =
-      for i <- 0..25 do
-        {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"})
-        activity
-      end
-
-    # outbox sorts chronologically, newest first, with ten per page
-    posts = Enum.reverse(posts)
-
-    %{"next" => next_url} =
-      UserView.render("activity_collection_page.json", %{
-        iri: "#{user.ap_id}/outbox",
-        activities: Enum.take(posts, 10)
-      })
-
-    next_id = Enum.at(posts, 9).id
-    assert next_url =~ next_id
-
-    %{"next" => next_url} =
-      UserView.render("activity_collection_page.json", %{
-        iri: "#{user.ap_id}/outbox",
-        activities: Enum.take(Enum.drop(posts, 10), 10)
-      })
-
-    next_id = Enum.at(posts, 19).id
-    assert next_url =~ next_id
-  end
 end