Add timeline visibility options
authorFloatingGhost <hannah@coffee-and-dreams.uk>
Fri, 17 Mar 2023 15:33:28 +0000 (15:33 +0000)
committerFloatingGhost <hannah@coffee-and-dreams.uk>
Fri, 17 Mar 2023 15:33:28 +0000 (15:33 +0000)
CHANGELOG.md
config/config.exs
config/description.exs
lib/pleroma/instances/instance.ex
lib/pleroma/web/api_spec/operations/timeline_operation.ex
lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
lib/pleroma/web/nodeinfo/nodeinfo.ex
lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
lib/pleroma/web/router.ex
test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs
test/pleroma/web/mastodon_api/views/account_view_test.exs

index c3b6f775a48639f76c7690bb26b63532fa5602c2..1bf6253af55e7b5e21baa971c620120a9fd41401 100644 (file)
@@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 ## Added
 - Nodeinfo keys for unauthenticated timeline visibility
+- Option to disable federated timeline
+- Option to make the bubble timeline publicly accessible
 
 ## 2023.03
 
index 5eaa8ce760dc1ef696ea7fac0e3aa3f11e6c09ad..e216caf9d2c9913aa2c20be6785d594117971c01 100644 (file)
@@ -261,7 +261,8 @@ config :pleroma, :instance,
   privileged_staff: false,
   local_bubble: [],
   max_frontend_settings_json_chars: 100_000,
-  export_prometheus_metrics: true
+  export_prometheus_metrics: true,
+  federated_timeline_available: true
 
 config :pleroma, :welcome,
   direct_message: [
@@ -810,7 +811,7 @@ config :pleroma, :majic_pool, size: 2
 private_instance? = :if_instance_is_private
 
 config :pleroma, :restrict_unauthenticated,
-  timelines: %{local: private_instance?, federated: private_instance?},
+  timelines: %{local: private_instance?, federated: private_instance?, bubble: true},
   profiles: %{local: private_instance?, remote: private_instance?},
   activities: %{local: private_instance?, remote: private_instance?}
 
index 2a2d70a7b4a327a26fc92ac970db8d7cd9cf2c56..f8496760fccd0f6d4e0b405499fcfb1e0d4fd60e 100644 (file)
@@ -969,6 +969,12 @@ config :pleroma, :config_description, [
         key: :export_prometheus_metrics,
         type: :boolean,
         description: "Enable prometheus metrics (at /api/v1/akkoma/metrics)"
+      },
+      %{
+        key: :federated_timeline_available,
+        type: :boolean,
+        description:
+          "Let people view the 'firehose' feed of all public statuses from all instances."
       }
     ]
   },
index 6ddfa5042ae386fa642d47fdf67a99c2d8bfe466..5c70748b6da2d5060951bffc685a9dd3f5e3f007 100644 (file)
@@ -162,7 +162,7 @@ defmodule Pleroma.Instances.Instance do
     %Instance{
       host: Pleroma.Web.Endpoint.host(),
       favicon: Pleroma.Web.Endpoint.url() <> "/favicon.png",
-      nodeinfo: Pleroma.Web.Nodeinfo.NodeinfoController.raw_nodeinfo()
+      nodeinfo: Pleroma.Web.Nodeinfo.Nodeinfo.get_nodeinfo("2.1")
     }
   end
 
index 3eb6f700b7f959986d9f58c5a6a93b502d4cfd62..45c97cab6d39d56ca744cc09a2dfbd9eaa21d1c1 100644 (file)
@@ -70,7 +70,8 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
       operationId: "TimelineController.public",
       responses: %{
         200 => Operation.response("Array of Status", "application/json", array_of_statuses()),
-        401 => Operation.response("Error", "application/json", ApiError)
+        401 => Operation.response("Error", "application/json", ApiError),
+        404 => Operation.response("Error", "application/json", ApiError)
       }
     }
   end
index 2d0e36420f7a90c6007acd255e73fb467ddedda8..c9960187d5df352b1fe006b88c9c4c9cd08ae2e0 100644 (file)
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
   alias Pleroma.Web.Plugs.RateLimiter
 
   plug(Pleroma.Web.ApiSpec.CastAndValidate)
-  plug(:skip_public_check when action in [:public, :hashtag])
+  plug(:skip_public_check when action in [:public, :hashtag, :bubble])
 
   # TODO: Replace with a macro when there is a Phoenix release with the following commit in it:
   # https://github.com/phoenixframework/phoenix/commit/2e8c63c01fec4dde5467dbbbf9705ff9e780735e
@@ -28,13 +28,13 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
   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, :bubble])
+  plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct])
   plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list)
 
   plug(
     OAuthScopesPlug,
     %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
-    when action in [:public, :hashtag]
+    when action in [:public, :hashtag, :bubble]
   )
 
   defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TimelineOperation
@@ -96,21 +96,19 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
     )
   end
 
-  defp restrict_unauthenticated?(true = _local_only) do
-    Config.restrict_unauthenticated_access?(:timelines, :local)
-  end
-
-  defp restrict_unauthenticated?(_) do
-    Config.restrict_unauthenticated_access?(:timelines, :federated)
+  defp restrict_unauthenticated?(type) do
+    Config.restrict_unauthenticated_access?(:timelines, type)
   end
 
   # GET /api/v1/timelines/public
   def public(%{assigns: %{user: user}} = conn, params) do
     local_only = params[:local]
+    timeline_type = if local_only, do: :local, else: :federated
 
-    if is_nil(user) and restrict_unauthenticated?(local_only) do
-      fail_on_bad_auth(conn)
-    else
+    with {:enabled, true} <-
+           {:enabled, local_only || Config.get([:instance, :federated_timeline_available], true)},
+         {:authenticated, true} <-
+           {:authenticated, !(is_nil(user) and restrict_unauthenticated?(timeline_type))} do
       activities =
         params
         |> Map.put(:type, ["Create"])
@@ -131,20 +129,28 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
         as: :activity,
         with_muted: Map.get(params, :with_muted, false)
       )
+    else
+      {:enabled, false} ->
+        conn
+        |> put_status(404)
+        |> json(%{error: "Federated timeline is disabled"})
+
+      {:authenticated, false} ->
+        fail_on_bad_auth(conn)
     end
   end
 
   # GET /api/v1/timelines/bubble
   def bubble(%{assigns: %{user: user}} = conn, params) do
-    bubble_instances =
-      Enum.uniq(
-        Config.get([:instance, :local_bubble], []) ++
-          [Pleroma.Web.Endpoint.host()]
-      )
-
-    if is_nil(user) do
+    if is_nil(user) and restrict_unauthenticated?(:bubble) do
       fail_on_bad_auth(conn)
     else
+      bubble_instances =
+        Enum.uniq(
+          Config.get([:instance, :local_bubble], []) ++
+            [Pleroma.Web.Endpoint.host()]
+        )
+
       activities =
         params
         |> Map.put(:type, ["Create"])
@@ -195,7 +201,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
   def hashtag(%{assigns: %{user: user}} = conn, params) do
     local_only = params[:local]
 
-    if is_nil(user) and restrict_unauthenticated?(local_only) do
+    if is_nil(user) and restrict_unauthenticated?(if local_only, do: :local, else: :federated) do
       fail_on_bad_auth(conn)
     else
       activities = hashtag_fetching(params, user, local_only)
index 14e39e6b3e583df8ae12f0824cfa77537fb0f858..532ae53a721d3a4eaaa19a9e70ba213360b67462 100644 (file)
@@ -73,9 +73,13 @@ defmodule Pleroma.Web.Nodeinfo.Nodeinfo do
         privilegedStaff: Config.get([:instance, :privileged_staff]),
         localBubbleInstances: Config.get([:instance, :local_bubble], []),
         publicTimelineVisibility: %{
-          federated: !Config.restrict_unauthenticated_access?(:timelines, :federated),
-          local: !Config.restrict_unauthenticated_access?(:timelines, :local)
-        }
+          federated:
+            !Config.restrict_unauthenticated_access?(:timelines, :federated) &&
+              Config.get([:instance, :federated_timeline_available], true),
+          local: !Config.restrict_unauthenticated_access?(:timelines, :local),
+          bubble: !Config.restrict_unauthenticated_access?(:timelines, :bubble)
+        },
+        federatedTimelineAvailable: Config.get([:instance, :federated_timeline_available], true)
       }
     }
   end
index 9a76574d54e0f682e08e3b9a1a570e9f29ffca4e..4c5a36895ceac1a09f025b3e9daae04450f9ba48 100644 (file)
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
   alias Pleroma.Web.Federator.Publisher
   alias Pleroma.Web.MastodonAPI.InstanceView
   alias Pleroma.Web.Endpoint
+  alias Pleroma.Web.Nodeinfo.Nodeinfo
 
   def schemas(conn, _params) do
     response = %{
@@ -29,105 +30,15 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
     json(conn, response)
   end
 
-  # returns a nodeinfo 2.0 map, since 2.1 just adds a repository field
-  # under software.
-  def raw_nodeinfo do
-    stats = Stats.get_stats()
-
-    staff_accounts =
-      User.all_superusers()
-      |> Enum.map(fn u -> u.ap_id end)
-      |> Enum.filter(fn u -> not Enum.member?(Config.get([:instance, :staff_transparency]), u) end)
-
-    features = InstanceView.features()
-    federation = InstanceView.federation()
-
-    %{
-      version: "2.0",
-      software: %{
-        name: Pleroma.Application.name() |> String.downcase(),
-        version: Pleroma.Application.version()
-      },
-      protocols: Publisher.gather_nodeinfo_protocol_names(),
-      services: %{
-        inbound: [],
-        outbound: []
-      },
-      openRegistrations: Config.get([:instance, :registrations_open]),
-      usage: %{
-        users: %{
-          total: Map.get(stats, :user_count, 0)
-        },
-        localPosts: Map.get(stats, :status_count, 0)
-      },
-      metadata: %{
-        nodeName: Config.get([:instance, :name]),
-        nodeDescription: Config.get([:instance, :description]),
-        private: !Config.get([:instance, :public], true),
-        suggestions: %{
-          enabled: false
-        },
-        staffAccounts: staff_accounts,
-        federation: federation,
-        pollLimits: Config.get([:instance, :poll_limits]),
-        postFormats: Config.get([:instance, :allowed_post_formats]),
-        uploadLimits: %{
-          general: Config.get([:instance, :upload_limit]),
-          avatar: Config.get([:instance, :avatar_upload_limit]),
-          banner: Config.get([:instance, :banner_upload_limit]),
-          background: Config.get([:instance, :background_upload_limit])
-        },
-        fieldsLimits: %{
-          maxFields: Config.get([:instance, :max_account_fields]),
-          maxRemoteFields: Config.get([:instance, :max_remote_account_fields]),
-          nameLength: Config.get([:instance, :account_field_name_length]),
-          valueLength: Config.get([:instance, :account_field_value_length])
-        },
-        accountActivationRequired: Config.get([:instance, :account_activation_required], false),
-        invitesEnabled: Config.get([:instance, :invites_enabled], false),
-        mailerEnabled: Config.get([Pleroma.Emails.Mailer, :enabled], false),
-        features: features,
-        restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]),
-        skipThreadContainment: Config.get([:instance, :skip_thread_containment], false),
-        localBubbleInstances: Config.get([:instance, :local_bubble], []),
-        publicTimelineVisibility: %{
-          federated: !Config.restrict_unauthenticated_access?(:timelines, :federated),
-          local: !Config.restrict_unauthenticated_access?(:timelines, :local)
-        }
-      }
-    }
-  end
-
   # Schema definition: https://github.com/jhass/nodeinfo/blob/master/schemas/2.0/schema.json
   # and https://github.com/jhass/nodeinfo/blob/master/schemas/2.1/schema.json
-  def nodeinfo(conn, %{"version" => "2.0"}) do
+  def nodeinfo(conn, %{"version" => version}) when version in ["2.0", "2.1"] do
     conn
     |> put_resp_header(
       "content-type",
       "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8"
     )
-    |> json(raw_nodeinfo())
-  end
-
-  def nodeinfo(conn, %{"version" => "2.1"}) do
-    raw_response = raw_nodeinfo()
-
-    updated_software =
-      raw_response
-      |> Map.get(:software)
-      |> Map.put(:repository, Pleroma.Application.repository())
-
-    response =
-      raw_response
-      |> Map.put(:software, updated_software)
-      |> Map.put(:version, "2.1")
-
-    conn
-    |> put_resp_header(
-      "content-type",
-      "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.1#; charset=utf-8"
-    )
-    |> json(response)
+    |> json(Nodeinfo.get_nodeinfo(version))
   end
 
   def nodeinfo(conn, _) do
index faaf3d67979b409c2224d85a99c6b62588bb6cbc..24ca5c37bffde89f9f81f9fd893ebb12ceb5fae1 100644 (file)
@@ -598,7 +598,6 @@ 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)
@@ -653,6 +652,7 @@ defmodule Pleroma.Web.Router do
 
     get("/timelines/public", TimelineController, :public)
     get("/timelines/tag/:tag", TimelineController, :hashtag)
+    get("/timelines/bubble", TimelineController, :bubble)
 
     get("/polls/:id", PollController, :show)
 
index aa9006681823441f93c963028078910c9a8701cd..fcc7a204eb58b429d7afd4246ded0a7d30cc59dc 100644 (file)
@@ -408,6 +408,26 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
 
       assert [] = result
     end
+
+    test "should return 404 if disabled" do
+      clear_config([:instance, :federated_timeline_available], false)
+
+      result =
+        build_conn()
+        |> get("/api/v1/timelines/public")
+        |> json_response_and_validate_schema(404)
+
+      assert %{"error" => "Federated timeline is disabled"} = result
+    end
+
+    test "should not return 404 if local is specified" do
+      clear_config([:instance, :federated_timeline_available], false)
+
+      result =
+        build_conn()
+        |> get("/api/v1/timelines/public?local=true")
+        |> json_response_and_validate_schema(200)
+    end
   end
 
   defp local_and_remote_activities do
@@ -1036,9 +1056,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
   end
 
   describe "bubble" do
-    setup do: oauth_access(["read:statuses"])
-
-    test "filtering", %{conn: conn, user: user} do
+    test "filtering" do
+      %{conn: conn, user: user} = oauth_access(["read:statuses"])
       clear_config([:instance, :local_bubble], [])
       # our endpoint host has a port in it so let's set the AP ID
       local_user = insert(:user, %{ap_id: "https://localhost/users/user"})
@@ -1060,7 +1079,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
 
       assert local_activity.id in one_instance
 
-      # If we have others, also include theirs 
+      # If we have others, also include theirs
       clear_config([:instance, :local_bubble], ["example.com"])
 
       two_instances =
@@ -1072,6 +1091,20 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
       assert local_activity.id in two_instances
       assert remote_activity.id in two_instances
     end
+
+    test "restrict_unauthenticated with bubble timeline", %{conn: conn} do
+      clear_config([:restrict_unauthenticated, :timelines, :bubble], true)
+
+      conn
+      |> get("/api/v1/timelines/bubble")
+      |> json_response_and_validate_schema(:unauthorized)
+
+      clear_config([:restrict_unauthenticated, :timelines, :bubble], false)
+
+      conn
+      |> get("/api/v1/timelines/bubble")
+      |> json_response_and_validate_schema(200)
+    end
   end
 
   defp create_remote_activity(user) do
index c9036d67d225165ddde7bc08572412c3be162b0d..6ef89f7998136e137b436f1a9e76ed6ef360b3d6 100644 (file)
@@ -269,8 +269,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
     }
 
     with_mock(
-      Pleroma.Web.Nodeinfo.NodeinfoController,
-      raw_nodeinfo: fn -> %{version: "2.0"} end
+      Pleroma.Web.Nodeinfo.Nodeinfo,
+      get_nodeinfo: fn _ -> %{version: "2.0"} end
     ) do
       assert expected ==
                AccountView.render("show.json", %{user: user, skip_visibility_check: true})