Add timeline visibility options
This commit is contained in:
parent
fe7045632b
commit
dd44387f1a
|
@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
- Nodeinfo keys for unauthenticated timeline visibility
|
- Nodeinfo keys for unauthenticated timeline visibility
|
||||||
|
- Option to disable federated timeline
|
||||||
|
- Option to make the bubble timeline publicly accessible
|
||||||
|
|
||||||
## 2023.03
|
## 2023.03
|
||||||
|
|
||||||
|
|
|
@ -261,7 +261,8 @@
|
||||||
privileged_staff: false,
|
privileged_staff: false,
|
||||||
local_bubble: [],
|
local_bubble: [],
|
||||||
max_frontend_settings_json_chars: 100_000,
|
max_frontend_settings_json_chars: 100_000,
|
||||||
export_prometheus_metrics: true
|
export_prometheus_metrics: true,
|
||||||
|
federated_timeline_available: true
|
||||||
|
|
||||||
config :pleroma, :welcome,
|
config :pleroma, :welcome,
|
||||||
direct_message: [
|
direct_message: [
|
||||||
|
@ -810,7 +811,7 @@
|
||||||
private_instance? = :if_instance_is_private
|
private_instance? = :if_instance_is_private
|
||||||
|
|
||||||
config :pleroma, :restrict_unauthenticated,
|
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?},
|
profiles: %{local: private_instance?, remote: private_instance?},
|
||||||
activities: %{local: private_instance?, remote: private_instance?}
|
activities: %{local: private_instance?, remote: private_instance?}
|
||||||
|
|
||||||
|
|
|
@ -969,6 +969,12 @@
|
||||||
key: :export_prometheus_metrics,
|
key: :export_prometheus_metrics,
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
description: "Enable prometheus metrics (at /api/v1/akkoma/metrics)"
|
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."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -162,7 +162,7 @@ def local do
|
||||||
%Instance{
|
%Instance{
|
||||||
host: Pleroma.Web.Endpoint.host(),
|
host: Pleroma.Web.Endpoint.host(),
|
||||||
favicon: Pleroma.Web.Endpoint.url() <> "/favicon.png",
|
favicon: Pleroma.Web.Endpoint.url() <> "/favicon.png",
|
||||||
nodeinfo: Pleroma.Web.Nodeinfo.NodeinfoController.raw_nodeinfo()
|
nodeinfo: Pleroma.Web.Nodeinfo.Nodeinfo.get_nodeinfo("2.1")
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,8 @@ def public_operation do
|
||||||
operationId: "TimelineController.public",
|
operationId: "TimelineController.public",
|
||||||
responses: %{
|
responses: %{
|
||||||
200 => Operation.response("Array of Status", "application/json", array_of_statuses()),
|
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
|
end
|
||||||
|
|
|
@ -16,7 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|
||||||
alias Pleroma.Web.Plugs.RateLimiter
|
alias Pleroma.Web.Plugs.RateLimiter
|
||||||
|
|
||||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
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:
|
# TODO: Replace with a macro when there is a Phoenix release with the following commit in it:
|
||||||
# https://github.com/phoenixframework/phoenix/commit/2e8c63c01fec4dde5467dbbbf9705ff9e780735e
|
# 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: :list_timeline] when action == :list)
|
||||||
plug(RateLimiter, [name: :timeline, bucket_name: :bubble_timeline] when action == :bubble)
|
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:lists"]} when action == :list)
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
OAuthScopesPlug,
|
OAuthScopesPlug,
|
||||||
%{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
|
%{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
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TimelineOperation
|
||||||
|
@ -96,21 +96,19 @@ def direct(%{assigns: %{user: user}} = conn, params) do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp restrict_unauthenticated?(true = _local_only) do
|
defp restrict_unauthenticated?(type) do
|
||||||
Config.restrict_unauthenticated_access?(:timelines, :local)
|
Config.restrict_unauthenticated_access?(:timelines, type)
|
||||||
end
|
|
||||||
|
|
||||||
defp restrict_unauthenticated?(_) do
|
|
||||||
Config.restrict_unauthenticated_access?(:timelines, :federated)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /api/v1/timelines/public
|
# GET /api/v1/timelines/public
|
||||||
def public(%{assigns: %{user: user}} = conn, params) do
|
def public(%{assigns: %{user: user}} = conn, params) do
|
||||||
local_only = params[:local]
|
local_only = params[:local]
|
||||||
|
timeline_type = if local_only, do: :local, else: :federated
|
||||||
|
|
||||||
if is_nil(user) and restrict_unauthenticated?(local_only) do
|
with {:enabled, true} <-
|
||||||
fail_on_bad_auth(conn)
|
{:enabled, local_only || Config.get([:instance, :federated_timeline_available], true)},
|
||||||
else
|
{:authenticated, true} <-
|
||||||
|
{:authenticated, !(is_nil(user) and restrict_unauthenticated?(timeline_type))} do
|
||||||
activities =
|
activities =
|
||||||
params
|
params
|
||||||
|> Map.put(:type, ["Create"])
|
|> Map.put(:type, ["Create"])
|
||||||
|
@ -131,20 +129,28 @@ def public(%{assigns: %{user: user}} = conn, params) do
|
||||||
as: :activity,
|
as: :activity,
|
||||||
with_muted: Map.get(params, :with_muted, false)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /api/v1/timelines/bubble
|
# GET /api/v1/timelines/bubble
|
||||||
def bubble(%{assigns: %{user: user}} = conn, params) do
|
def bubble(%{assigns: %{user: user}} = conn, params) do
|
||||||
|
if is_nil(user) and restrict_unauthenticated?(:bubble) do
|
||||||
|
fail_on_bad_auth(conn)
|
||||||
|
else
|
||||||
bubble_instances =
|
bubble_instances =
|
||||||
Enum.uniq(
|
Enum.uniq(
|
||||||
Config.get([:instance, :local_bubble], []) ++
|
Config.get([:instance, :local_bubble], []) ++
|
||||||
[Pleroma.Web.Endpoint.host()]
|
[Pleroma.Web.Endpoint.host()]
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_nil(user) do
|
|
||||||
fail_on_bad_auth(conn)
|
|
||||||
else
|
|
||||||
activities =
|
activities =
|
||||||
params
|
params
|
||||||
|> Map.put(:type, ["Create"])
|
|> Map.put(:type, ["Create"])
|
||||||
|
@ -195,7 +201,7 @@ defp hashtag_fetching(params, user, local_only) do
|
||||||
def hashtag(%{assigns: %{user: user}} = conn, params) do
|
def hashtag(%{assigns: %{user: user}} = conn, params) do
|
||||||
local_only = params[:local]
|
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)
|
fail_on_bad_auth(conn)
|
||||||
else
|
else
|
||||||
activities = hashtag_fetching(params, user, local_only)
|
activities = hashtag_fetching(params, user, local_only)
|
||||||
|
|
|
@ -73,9 +73,13 @@ def get_nodeinfo("2.0") do
|
||||||
privilegedStaff: Config.get([:instance, :privileged_staff]),
|
privilegedStaff: Config.get([:instance, :privileged_staff]),
|
||||||
localBubbleInstances: Config.get([:instance, :local_bubble], []),
|
localBubbleInstances: Config.get([:instance, :local_bubble], []),
|
||||||
publicTimelineVisibility: %{
|
publicTimelineVisibility: %{
|
||||||
federated: !Config.restrict_unauthenticated_access?(:timelines, :federated),
|
federated:
|
||||||
local: !Config.restrict_unauthenticated_access?(:timelines, :local)
|
!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
|
end
|
||||||
|
|
|
@ -11,6 +11,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
|
||||||
alias Pleroma.Web.Federator.Publisher
|
alias Pleroma.Web.Federator.Publisher
|
||||||
alias Pleroma.Web.MastodonAPI.InstanceView
|
alias Pleroma.Web.MastodonAPI.InstanceView
|
||||||
alias Pleroma.Web.Endpoint
|
alias Pleroma.Web.Endpoint
|
||||||
|
alias Pleroma.Web.Nodeinfo.Nodeinfo
|
||||||
|
|
||||||
def schemas(conn, _params) do
|
def schemas(conn, _params) do
|
||||||
response = %{
|
response = %{
|
||||||
|
@ -29,105 +30,15 @@ def schemas(conn, _params) do
|
||||||
json(conn, response)
|
json(conn, response)
|
||||||
end
|
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
|
# 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
|
# 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
|
conn
|
||||||
|> put_resp_header(
|
|> put_resp_header(
|
||||||
"content-type",
|
"content-type",
|
||||||
"application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8"
|
"application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8"
|
||||||
)
|
)
|
||||||
|> json(raw_nodeinfo())
|
|> json(Nodeinfo.get_nodeinfo(version))
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def nodeinfo(conn, _) do
|
def nodeinfo(conn, _) do
|
||||||
|
|
|
@ -598,7 +598,6 @@ defmodule Pleroma.Web.Router do
|
||||||
get("/timelines/home", TimelineController, :home)
|
get("/timelines/home", TimelineController, :home)
|
||||||
get("/timelines/direct", TimelineController, :direct)
|
get("/timelines/direct", TimelineController, :direct)
|
||||||
get("/timelines/list/:list_id", TimelineController, :list)
|
get("/timelines/list/:list_id", TimelineController, :list)
|
||||||
get("/timelines/bubble", TimelineController, :bubble)
|
|
||||||
|
|
||||||
get("/announcements", AnnouncementController, :index)
|
get("/announcements", AnnouncementController, :index)
|
||||||
post("/announcements/:id/dismiss", AnnouncementController, :mark_read)
|
post("/announcements/:id/dismiss", AnnouncementController, :mark_read)
|
||||||
|
@ -653,6 +652,7 @@ defmodule Pleroma.Web.Router do
|
||||||
|
|
||||||
get("/timelines/public", TimelineController, :public)
|
get("/timelines/public", TimelineController, :public)
|
||||||
get("/timelines/tag/:tag", TimelineController, :hashtag)
|
get("/timelines/tag/:tag", TimelineController, :hashtag)
|
||||||
|
get("/timelines/bubble", TimelineController, :bubble)
|
||||||
|
|
||||||
get("/polls/:id", PollController, :show)
|
get("/polls/:id", PollController, :show)
|
||||||
|
|
||||||
|
|
|
@ -408,6 +408,26 @@ test "should not return local-only posts for anonymous users" do
|
||||||
|
|
||||||
assert [] = result
|
assert [] = result
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
defp local_and_remote_activities do
|
defp local_and_remote_activities do
|
||||||
|
@ -1036,9 +1056,8 @@ test "with `%{local: true, federated: false}`, forbids unauthenticated access to
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "bubble" do
|
describe "bubble" do
|
||||||
setup do: oauth_access(["read:statuses"])
|
test "filtering" do
|
||||||
|
%{conn: conn, user: user} = oauth_access(["read:statuses"])
|
||||||
test "filtering", %{conn: conn, user: user} do
|
|
||||||
clear_config([:instance, :local_bubble], [])
|
clear_config([:instance, :local_bubble], [])
|
||||||
# our endpoint host has a port in it so let's set the AP ID
|
# 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"})
|
local_user = insert(:user, %{ap_id: "https://localhost/users/user"})
|
||||||
|
@ -1072,6 +1091,20 @@ test "filtering", %{conn: conn, user: user} do
|
||||||
assert local_activity.id in two_instances
|
assert local_activity.id in two_instances
|
||||||
assert remote_activity.id in two_instances
|
assert remote_activity.id in two_instances
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
defp create_remote_activity(user) do
|
defp create_remote_activity(user) do
|
||||||
|
|
|
@ -269,8 +269,8 @@ test "Represent a Service(bot) account" do
|
||||||
}
|
}
|
||||||
|
|
||||||
with_mock(
|
with_mock(
|
||||||
Pleroma.Web.Nodeinfo.NodeinfoController,
|
Pleroma.Web.Nodeinfo.Nodeinfo,
|
||||||
raw_nodeinfo: fn -> %{version: "2.0"} end
|
get_nodeinfo: fn _ -> %{version: "2.0"} end
|
||||||
) do
|
) do
|
||||||
assert expected ==
|
assert expected ==
|
||||||
AccountView.render("show.json", %{user: user, skip_visibility_check: true})
|
AccountView.render("show.json", %{user: user, skip_visibility_check: true})
|
||||||
|
|
Loading…
Reference in a new issue