diff --git a/CHANGELOG.md b/CHANGELOG.md index c3b6f775a..1bf6253af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/config/config.exs b/config/config.exs index 5eaa8ce76..e216caf9d 100644 --- a/config/config.exs +++ b/config/config.exs @@ -261,7 +261,8 @@ 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 @@ 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?} diff --git a/config/description.exs b/config/description.exs index 2a2d70a7b..f8496760f 100644 --- a/config/description.exs +++ b/config/description.exs @@ -969,6 +969,12 @@ 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." } ] }, diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index 6ddfa5042..5c70748b6 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -162,7 +162,7 @@ def local 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 diff --git a/lib/pleroma/web/api_spec/operations/timeline_operation.ex b/lib/pleroma/web/api_spec/operations/timeline_operation.ex index 3eb6f700b..45c97cab6 100644 --- a/lib/pleroma/web/api_spec/operations/timeline_operation.ex +++ b/lib/pleroma/web/api_spec/operations/timeline_operation.ex @@ -70,7 +70,8 @@ def public_operation 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 diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 2d0e36420..c9960187d 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -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 @@ def direct(%{assigns: %{user: user}} = conn, params) 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 @@ def public(%{assigns: %{user: user}} = conn, params) 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 @@ defp hashtag_fetching(params, user, local_only) 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) diff --git a/lib/pleroma/web/nodeinfo/nodeinfo.ex b/lib/pleroma/web/nodeinfo/nodeinfo.ex index 14e39e6b3..532ae53a7 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo.ex @@ -73,9 +73,13 @@ def get_nodeinfo("2.0") 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 diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index 9a76574d5..4c5a36895 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -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 @@ def schemas(conn, _params) 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 diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index faaf3d679..24ca5c37b 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -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) diff --git a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs index aa9006681..fcc7a204e 100644 --- a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs @@ -408,6 +408,26 @@ test "should not return local-only posts for anonymous users" 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 @@ test "with `%{local: true, federated: false}`, forbids unauthenticated access to 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 @@ test "filtering", %{conn: conn, user: user} 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 @@ test "filtering", %{conn: conn, user: user} 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 diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index c9036d67d..6ef89f799 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -269,8 +269,8 @@ test "Represent a Service(bot) account" 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})