add bubble timeline (#100)
authorfloatingghost <hannah@coffee-and-dreams.uk>
Fri, 22 Jul 2022 14:55:38 +0000 (14:55 +0000)
committerfloatingghost <hannah@coffee-and-dreams.uk>
Fri, 22 Jul 2022 14:55:38 +0000 (14:55 +0000)
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/100

config/config.exs
config/description.exs
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/api_spec/operations/timeline_operation.ex
lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
lib/pleroma/web/mastodon_api/websocket_handler.ex
lib/pleroma/web/router.ex
test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs

index 0f0daf7964aca73d868967035ceabf750363479a..cfe207dcc8f7b0feea00f6cfb37400ec31caca64 100644 (file)
@@ -259,7 +259,8 @@ config :pleroma, :instance,
   show_reactions: true,
   password_reset_token_validity: 60 * 60 * 24,
   profile_directory: true,
-  privileged_staff: false
+  privileged_staff: false,
+  local_bubble: []
 
 config :pleroma, :welcome,
   direct_message: [
index 1eb0a4161e1679e3361a12816e73fc522e8deb64..f7e3c714f2523b872d299571258537974656cafe 100644 (file)
@@ -947,6 +947,12 @@ config :pleroma, :config_description, [
         type: :boolean,
         description:
           "Let moderators access sensitive data (e.g. updating user credentials, get password reset token, delete users, index and read private statuses)"
+      },
+      %{
+        key: :local_bubble,
+        type: {:list, :string},
+        description:
+          "List of instances that make up your local bubble (closely-related instances). Used to populate the 'bubble' timeline (domain only)."
       }
     ]
   },
index 29055668bd327a04ef346fad85de1dd00fcfd8ec..3e58864c8fb008574d06d715180bf71415255540 100644 (file)
@@ -1154,6 +1154,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     )
   end
 
+  defp restrict_instance(query, %{instance: instance}) when is_list(instance) do
+    from(
+      activity in query,
+      where: fragment("split_part(actor::text, '/'::text, 3) = ANY(?)", ^instance)
+    )
+  end
+
   defp restrict_instance(query, _), do: query
 
   defp restrict_filtered(query, %{user: %User{} = user}) do
index d375c76b8a1fa953d633d25c387b2a402f6c54e1..3eb6f700b7f959986d9f58c5a6a93b502d4cfd62 100644 (file)
@@ -75,6 +75,26 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
     }
   end
 
+  def bubble_operation do
+    %Operation{
+      tags: ["Timelines"],
+      summary: "Bubble timeline",
+      security: [%{"oAuth" => ["read:statuses"]}],
+      parameters: [
+        only_media_param(),
+        remote_param(),
+        with_muted_param(),
+        exclude_visibilities_param(),
+        reply_visibility_param() | pagination_params()
+      ],
+      operationId: "TimelineController.bubble",
+      responses: %{
+        200 => Operation.response("Array of Status", "application/json", array_of_statuses()),
+        401 => Operation.response("Error", "application/json", ApiError)
+      }
+    }
+  end
+
   def hashtag_operation do
     %Operation{
       tags: ["Timelines"],
index 10c27989338af0dd3a6e2d5e3e5ac8d8fe373407..6200263744bc468d010872c960e8d571a15b8414 100644 (file)
@@ -26,8 +26,9 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
   plug(RateLimiter, [name: :timeline, bucket_name: :home_timeline] when action == :home)
   plug(RateLimiter, [name: :timeline, bucket_name: :hashtag_timeline] when action == :hashtag)
   plug(RateLimiter, [name: :timeline, bucket_name: :list_timeline] when action == :list)
+  plug(RateLimiter, [name: :timeline, bucket_name: :bubble_timeline] when action == :bubble)
 
-  plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct])
+  plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct, :bubble])
   plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list)
 
   plug(
@@ -125,6 +126,33 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
     end
   end
 
+  # GET /api/v1/timelines/bubble
+  def bubble(%{assigns: %{user: user}} = conn, params) do
+    bubble_instances = Config.get([:instance, :local_bubble], [])
+
+    if is_nil(user) do
+      fail_on_bad_auth(conn)
+    else
+      activities =
+        params
+        |> Map.put(:type, ["Create"])
+        |> Map.put(:blocking_user, user)
+        |> Map.put(:muting_user, user)
+        |> Map.put(:reply_filtering_user, user)
+        |> Map.put(:instance, bubble_instances)
+        |> ActivityPub.fetch_public_activities()
+
+      conn
+      |> add_link_headers(activities)
+      |> render("index.json",
+        activities: activities,
+        for: user,
+        as: :activity,
+        with_muted: Map.get(params, :with_muted, false)
+      )
+    end
+  end
+
   defp fail_on_bad_auth(conn) do
     render_error(conn, :unauthorized, "authorization required for timeline view")
   end
index b978167b6288f95b3e8b042fb0b9625c2429e37f..861a7ce3eb5c7d893cd6b55b850a8e08beb7ef52 100644 (file)
@@ -65,6 +65,11 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
   # We only receive pings for now
   def websocket_handle(:ping, state), do: {:ok, state}
 
+  def websocket_handle({:text, "ping"}, state) do
+    if state.timer, do: Process.cancel_timer(state.timer)
+    {:reply, {:text, "pong"}, %{state | timer: timer()}}
+  end
+
   def websocket_handle(frame, state) do
     Logger.error("#{__MODULE__} received frame: #{inspect(frame)}")
     {:ok, state}
index d413835bb0d9b883cc1b2e377339b61b2fe8af6b..a0310bbb50b535341181729c6a2717533138f00f 100644 (file)
@@ -560,6 +560,7 @@ defmodule Pleroma.Web.Router do
     get("/timelines/home", TimelineController, :home)
     get("/timelines/direct", TimelineController, :direct)
     get("/timelines/list/:list_id", TimelineController, :list)
+    get("/timelines/bubble", TimelineController, :bubble)
 
     get("/announcements", AnnouncementController, :index)
     post("/announcements/:id/dismiss", AnnouncementController, :mark_read)
index 187982d92572b42b0cb21552bbc353de94aa6132..7ef08f258aaf68e12e17331bf1a21c2d05855fdd 100644 (file)
@@ -994,6 +994,59 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
     end
   end
 
+  describe "bubble" do
+    setup do: oauth_access(["read:statuses"])
+
+    test "it returns nothing if no bubble is configured", %{user: user, conn: conn} do
+      clear_config([:instance, :local_bubble], [])
+      {:ok, _} = CommonAPI.post(user, %{status: ".", visibility: "public"})
+
+      conn = get(conn, "/api/v1/timelines/bubble")
+
+      assert [] = json_response_and_validate_schema(conn, :ok)
+    end
+
+    test "filtering", %{conn: conn, user: user} do
+      clear_config([:instance, :local_bubble], [])
+      local_user = insert(:user)
+      remote_user = insert(:user, %{ap_id: "https://example.com/users/remote_user"})
+      {:ok, user, local_user} = User.follow(user, local_user)
+      {:ok, _user, remote_user} = User.follow(user, remote_user)
+
+      {:ok, local_activity} = CommonAPI.post(local_user, %{status: "Status"})
+      remote_activity = create_remote_activity(remote_user)
+
+      resp =
+        conn
+        |> get("/api/v1/timelines/bubble")
+        |> json_response_and_validate_schema(200)
+        |> Enum.map(& &1["id"])
+
+      assert Enum.empty?(resp)
+
+      clear_config([:instance, :local_bubble], ["localhost:4001"])
+
+      one_instance =
+        conn
+        |> get("/api/v1/timelines/bubble")
+        |> json_response_and_validate_schema(200)
+        |> Enum.map(& &1["id"])
+
+      assert local_activity.id in one_instance
+
+      clear_config([:instance, :local_bubble], ["localhost:4001", "example.com"])
+
+      two_instances =
+        conn
+        |> get("/api/v1/timelines/bubble")
+        |> json_response_and_validate_schema(200)
+        |> Enum.map(& &1["id"])
+
+      assert local_activity.id in two_instances
+      assert remote_activity.id in two_instances
+    end
+  end
+
   defp create_remote_activity(user) do
     obj =
       insert(:note, %{