diff --git a/changelog.d/preferred-frontend.add b/changelog.d/preferred-frontend.add new file mode 100644 index 000000000..145e9451b --- /dev/null +++ b/changelog.d/preferred-frontend.add @@ -0,0 +1 @@ +Allow users to select preferred frontend diff --git a/config/description.exs b/config/description.exs index 2566cfba6..c388d17c3 100644 --- a/config/description.exs +++ b/config/description.exs @@ -3333,6 +3333,12 @@ config :pleroma, :config_description, [ description: "A map containing available frontends and parameters for their installation.", children: frontend_options + }, + %{ + key: :pickable, + type: {:list, :string}, + description: + "A list containing all frontends users can pick as their preference, format is :name/:ref, e.g pleroma-fe/stable." } ] }, diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex index 3e0ac3704..5006167ea 100644 --- a/lib/pleroma/web/api_spec.ex +++ b/lib/pleroma/web/api_spec.ex @@ -151,7 +151,8 @@ defmodule Pleroma.Web.ApiSpec do "Suggestions", "Announcements", "Remote interaction", - "Others" + "Others", + "Preferred frontends" ] } ] diff --git a/lib/pleroma/web/api_spec/operations/pleroma_frontend_settings_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_frontend_settings_operation.ex new file mode 100644 index 000000000..923e4fcc9 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_frontend_settings_operation.ex @@ -0,0 +1,65 @@ +defmodule Pleroma.Web.ApiSpec.PleromaFrontendSettingsOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + import Pleroma.Web.ApiSpec.Helpers + + @spec open_api_operation(atom) :: Operation.t() + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def available_frontends_operation do + %Operation{ + tags: ["Preferred frontends"], + summary: "Frontend settings profiles", + description: "List frontend setting profiles", + operationId: "PleromaAPI.FrontendSettingsController.available_frontends", + responses: %{ + 200 => + Operation.response("Frontends", "application/json", %Schema{ + type: :array, + items: %Schema{ + type: :string + } + }) + } + } + end + + def update_preferred_frontend_operation do + %Operation{ + tags: ["Preferred frontends"], + summary: "Update preferred frontend setting", + description: "Store preferred frontend in cookies", + operationId: "PleromaAPI.FrontendSettingsController.update_preferred_frontend", + requestBody: + request_body( + "Frontend", + %Schema{ + type: :object, + required: [:frontend_name], + properties: %{ + frontend_name: %Schema{ + type: :string, + description: "Frontend name" + } + } + }, + required: true + ), + responses: %{ + 200 => + Operation.response("Preferred frontend", "application/json", %Schema{ + type: :object, + properties: %{ + frontend_name: %Schema{ + type: :string, + description: "Frontend name" + } + } + }) + } + } + end +end diff --git a/lib/pleroma/web/fallback/redirect_controller.ex b/lib/pleroma/web/fallback/redirect_controller.ex index 6637848a9..60fc15b9e 100644 --- a/lib/pleroma/web/fallback/redirect_controller.ex +++ b/lib/pleroma/web/fallback/redirect_controller.ex @@ -30,7 +30,7 @@ defmodule Pleroma.Web.Fallback.RedirectController do end def redirector(conn, _params, code \\ 200) do - {:ok, index_content} = File.read(index_file_path()) + {:ok, index_content} = File.read(index_file_path(conn)) response = index_content @@ -51,7 +51,7 @@ defmodule Pleroma.Web.Fallback.RedirectController do end def redirector_with_meta(conn, params) do - {:ok, index_content} = File.read(index_file_path()) + {:ok, index_content} = File.read(index_file_path(conn)) tags = build_tags(conn, params) preloads = preload_data(conn, params) @@ -69,7 +69,7 @@ defmodule Pleroma.Web.Fallback.RedirectController do end def redirector_with_preload(conn, params) do - {:ok, index_content} = File.read(index_file_path()) + {:ok, index_content} = File.read(index_file_path(conn)) preloads = preload_data(conn, params) response = @@ -91,8 +91,10 @@ defmodule Pleroma.Web.Fallback.RedirectController do |> text("") end - defp index_file_path do - Pleroma.Web.Plugs.InstanceStatic.file_path("index.html") + defp index_file_path(conn) do + frontend_type = Pleroma.Web.Plugs.FrontendStatic.preferred_or_fallback(conn, :primary) + + Pleroma.Web.Plugs.InstanceStatic.file_path("index.html", frontend_type) end defp build_tags(conn, params) do diff --git a/lib/pleroma/web/frontend_switcher/frontend_switcher_controller.ex b/lib/pleroma/web/frontend_switcher/frontend_switcher_controller.ex new file mode 100644 index 000000000..18752c63c --- /dev/null +++ b/lib/pleroma/web/frontend_switcher/frontend_switcher_controller.ex @@ -0,0 +1,20 @@ +defmodule Pleroma.Web.FrontendSwitcher.FrontendSwitcherController do + use Pleroma.Web, :controller + alias Pleroma.Config + + @doc "GET /frontend_switcher" + def switch(conn, _params) do + pickable = Config.get([:frontends, :pickable], []) + + conn + |> put_view(Pleroma.Web.FrontendSwitcher.FrontendSwitcherView) + |> render("switch.html", choices: pickable) + end + + @doc "POST /frontend_switcher" + def do_switch(conn, params) do + conn + |> put_resp_cookie("preferred_frontend", params["frontend"]) + |> html(~s()) + end +end diff --git a/lib/pleroma/web/frontend_switcher/frontend_switcher_view.ex b/lib/pleroma/web/frontend_switcher/frontend_switcher_view.ex new file mode 100644 index 000000000..284477431 --- /dev/null +++ b/lib/pleroma/web/frontend_switcher/frontend_switcher_view.ex @@ -0,0 +1,5 @@ +defmodule Pleroma.Web.FrontendSwitcher.FrontendSwitcherView do + use Pleroma.Web, :view + + import Phoenix.HTML.Form +end diff --git a/lib/pleroma/web/pleroma_api/controllers/frontend_settings_controller.ex b/lib/pleroma/web/pleroma_api/controllers/frontend_settings_controller.ex new file mode 100644 index 000000000..41531c97e --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/frontend_settings_controller.ex @@ -0,0 +1,37 @@ +defmodule Pleroma.Web.PleromaAPI.FrontendSettingsController do + use Pleroma.Web, :controller + + alias Pleroma.Web.Plugs.OAuthScopesPlug + + plug( + OAuthScopesPlug, + %{fallback: :proceed_unauthenticated, scopes: []} + when action in [ + :available_frontends, + :update_preferred_frontend + ] + ) + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaFrontendSettingsOperation + + action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + + @doc "GET /api/v1/pleroma/preferred_frontend/available" + def available_frontends(conn, _params) do + available = Pleroma.Config.get([:frontends, :pickable]) + + conn + |> json(available) + end + + @doc "PUT /api/v1/pleroma/preferred_frontend" + def update_preferred_frontend( + %{body_params: %{frontend_name: preferred_frontend}} = conn, + _params + ) do + conn + |> put_resp_cookie("preferred_frontend", preferred_frontend) + |> json(%{frontend_name: preferred_frontend}) + end +end diff --git a/lib/pleroma/web/plugs/frontend_static.ex b/lib/pleroma/web/plugs/frontend_static.ex index 6ab8e4667..f1df185e3 100644 --- a/lib/pleroma/web/plugs/frontend_static.ex +++ b/lib/pleroma/web/plugs/frontend_static.ex @@ -5,17 +5,23 @@ defmodule Pleroma.Web.Plugs.FrontendStatic do require Pleroma.Constants + @frontend_cookie_name "preferred_frontend" + @moduledoc """ This is a shim to call `Plug.Static` but with runtime `from` configuration`. It dispatches to the different frontends. """ @behaviour Plug - def file_path(path, frontend_type \\ :primary) do - if configuration = Pleroma.Config.get([:frontends, frontend_type]) do - instance_static_path = Pleroma.Config.get([:instance, :static_dir], "instance/static") + defp instance_static_path do + Pleroma.Config.get([:instance, :static_dir], "instance/static") + end + def file_path(path, frontend_type \\ :primary) + + def file_path(path, frontend_type) when is_atom(frontend_type) do + if configuration = Pleroma.Config.get([:frontends, frontend_type]) do Path.join([ - instance_static_path, + instance_static_path(), "frontends", configuration["name"], configuration["ref"], @@ -26,6 +32,15 @@ defmodule Pleroma.Web.Plugs.FrontendStatic do end end + def file_path(path, frontend_type) when is_binary(frontend_type) do + Path.join([ + instance_static_path(), + "frontends", + frontend_type, + path + ]) + end + def init(opts) do opts |> Keyword.put(:from, "__unconfigured_frontend_static_plug") @@ -36,7 +51,8 @@ defmodule Pleroma.Web.Plugs.FrontendStatic do def call(conn, opts) do with false <- api_route?(conn.path_info), false <- invalid_path?(conn.path_info), - frontend_type <- Map.get(opts, :frontend_type, :primary), + fallback_frontend_type <- Map.get(opts, :frontend_type, :primary), + frontend_type <- preferred_or_fallback(conn, fallback_frontend_type), path when not is_nil(path) <- file_path("", frontend_type) do call_static(conn, opts, path) else @@ -45,6 +61,31 @@ defmodule Pleroma.Web.Plugs.FrontendStatic do end end + def preferred_frontend(conn) do + %{req_cookies: cookies} = + conn + |> Plug.Conn.fetch_cookies() + + Map.get(cookies, @frontend_cookie_name) + end + + # Only override primary frontend + def preferred_or_fallback(conn, :primary) do + case preferred_frontend(conn) do + nil -> + :primary + + frontend -> + if Enum.member?(Pleroma.Config.get([:frontends, :pickable], []), frontend) do + frontend + else + :primary + end + end + end + + def preferred_or_fallback(_conn, fallback), do: fallback + defp invalid_path?(list) do invalid_path?(list, :binary.compile_pattern(["/", "\\", ":", "\0"])) end diff --git a/lib/pleroma/web/plugs/instance_static.ex b/lib/pleroma/web/plugs/instance_static.ex index f82b9a098..d2a674d39 100644 --- a/lib/pleroma/web/plugs/instance_static.ex +++ b/lib/pleroma/web/plugs/instance_static.ex @@ -13,11 +13,11 @@ defmodule Pleroma.Web.Plugs.InstanceStatic do """ @behaviour Plug - def file_path(path) do + def file_path(path, frontend_type \\ :primary) do instance_path = Path.join(Pleroma.Config.get([:instance, :static_dir], "instance/static/"), path) - frontend_path = Pleroma.Web.Plugs.FrontendStatic.file_path(path, :primary) + frontend_path = Pleroma.Web.Plugs.FrontendStatic.file_path(path, frontend_type) (File.exists?(instance_path) && instance_path) || (frontend_path && File.exists?(frontend_path) && frontend_path) || diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 9a49db2f1..7c1d97f63 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -561,6 +561,18 @@ defmodule Pleroma.Web.Router do get("/apps", AppController, :index) get("/statuses/:id/reactions/:emoji", EmojiReactionController, :index) get("/statuses/:id/reactions", EmojiReactionController, :index) + + get( + "/preferred_frontend/available", + FrontendSettingsController, + :available_frontends + ) + + put( + "/preferred_frontend", + FrontendSettingsController, + :update_preferred_frontend + ) end scope "/api/v0/pleroma", Pleroma.Web.PleromaAPI do @@ -906,7 +918,11 @@ defmodule Pleroma.Web.Router do scope "/", Pleroma.Web do pipe_through(:browser) + get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe) + + get("/frontend_switcher", FrontendSwitcher.FrontendSwitcherController, :switch) + post("/frontend_switcher", FrontendSwitcher.FrontendSwitcherController, :do_switch) end pipeline :ap_service_actor do diff --git a/lib/pleroma/web/templates/frontend_switcher/frontend_switcher/switch.html.eex b/lib/pleroma/web/templates/frontend_switcher/frontend_switcher/switch.html.eex new file mode 100644 index 000000000..c801c8ee8 --- /dev/null +++ b/lib/pleroma/web/templates/frontend_switcher/frontend_switcher/switch.html.eex @@ -0,0 +1,7 @@ +