The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
+## Unreleased
+
+## Added
+- Nodeinfo keys for unauthenticated timeline visibility
+- Option to disable federated timeline
+- Option to make the bubble timeline publicly accessible
+
## 2023.03
## Fixed
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: [
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?}
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."
}
]
},
key: :federated,
type: :boolean,
description: "Disallow viewing the whole known network timeline."
+ },
+ %{
+ key: :bubble,
+ type: :boolean,
+ description: "Disallow viewing the bubble timeline."
}
]
},
%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
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
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
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
)
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"])
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"])
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)
restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]),
skipThreadContainment: Config.get([:instance, :skip_thread_containment], false),
privilegedStaff: Config.get([:instance, :privileged_staff]),
- localBubbleInstances: Config.get([:instance, :local_bubble], [])
+ localBubbleInstances: Config.get([:instance, :local_bubble], []),
+ publicTimelineVisibility: %{
+ 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
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 = %{
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], [])
- }
- }
- 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
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)
get("/timelines/public", TimelineController, :public)
get("/timelines/tag/:tag", TimelineController, :hashtag)
+ get("/timelines/bubble", TimelineController, :bubble)
get("/polls/:id", PollController, :show)
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
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"})
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 =
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
}
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})
assert response["metadata"]["federation"]["mrf_simple_info"] == expected_config
end
end
+
+ describe "public timeline visibility" do
+ test "shows public timeline visibility", %{conn: conn} do
+ clear_config([:restrict_unauthenticated, :timelines], %{local: false, federated: false})
+
+ response =
+ conn
+ |> get("/nodeinfo/2.1.json")
+ |> json_response(:ok)
+
+ assert response["metadata"]["publicTimelineVisibility"]["local"] == true
+ assert response["metadata"]["publicTimelineVisibility"]["federated"] == true
+
+ clear_config([:restrict_unauthenticated, :timelines], %{local: true, federated: false})
+
+ response =
+ conn
+ |> get("/nodeinfo/2.1.json")
+ |> json_response(:ok)
+
+ assert response["metadata"]["publicTimelineVisibility"]["local"] == false
+ assert response["metadata"]["publicTimelineVisibility"]["federated"] == true
+
+ clear_config([:restrict_unauthenticated, :timelines], %{local: false, federated: true})
+
+ response =
+ conn
+ |> get("/nodeinfo/2.1.json")
+ |> json_response(:ok)
+
+ assert response["metadata"]["publicTimelineVisibility"]["local"] == true
+ assert response["metadata"]["publicTimelineVisibility"]["federated"] == false
+ end
+ end
end