commit: d41e2fbaafdd00cf72c1383c6d6aaefbec350b4b parent 8c9e130cc9089a041b9d710ff572390d8b6da8be Author: nicole mikołajczyk <me@mkljczk.pl> Date: Tue, 16 Dec 2025 20:54:00 +0100 Merge branch 'preferred-frontend' into 'develop' Port Akkoma frontend preference code See merge request pleroma/pleroma!4398Diffstat:
14 files changed, 264 insertions(+), 13 deletions(-)diff --git a/changelog.d/preferred-frontend.add b/changelog.d/preferred-frontend.add@@ -0,0 +1 @@ +Allow users to select preferred frontenddiff --git 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@@ -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@@ -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 +enddiff --git 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) dodiff --git a/lib/pleroma/web/frontend_switcher/frontend_switcher_controller.ex 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(<meta http-equiv="refresh" content="0; url=/">)) + end +enddiff --git a/lib/pleroma/web/frontend_switcher/frontend_switcher_view.ex 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 +enddiff --git a/lib/pleroma/web/pleroma_api/controllers/frontend_settings_controller.ex 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 +enddiff --git 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"])) enddiff --git 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@@ -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 dodiff --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@@ -0,0 +1,7 @@ +<h2>Switch frontend</h2> + +<%= form_for @conn, Routes.frontend_switcher_path(@conn, :do_switch), fn f -> %> + <%= select(f, :frontend, @choices) %> + + <%= submit do: "submit" %> +<% end %>diff --git a/test/pleroma/web/pleroma_api/controllers/frontend_settings_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/frontend_settings_controller_test.exs@@ -0,0 +1,17 @@ +defmodule Pleroma.Web.PleromaAPI.FrontendSettingsControllerTest do + use Pleroma.Web.ConnCase, async: false + + describe "PUT /api/v1/pleroma/preferred_frontend" do + test "sets a cookie with selected frontend" do + %{conn: conn} = oauth_access(["read"]) + + response = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/pleroma/preferred_frontend", %{"frontend_name" => "pleroma-fe/stable"}) + + json_response_and_validate_schema(response, 200) + assert %{"preferred_frontend" => %{value: "pleroma-fe/stable"}} = response.resp_cookies + end + end +enddiff --git a/test/pleroma/web/plugs/frontend_static_plug_test.exs b/test/pleroma/web/plugs/frontend_static_plug_test.exs@@ -97,6 +97,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do "users", "tags", "mailer", + "frontend_switcher", "inbox", "relay", "internal", @@ -113,4 +114,36 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do assert expected_routes == Pleroma.Web.Router.get_api_routes() end + + describe "preferred frontend cookie handling" do + test "returns preferred frontend file", %{conn: conn} do + name = "test-fe" + ref = "develop" + + clear_config([:frontends, :pickable], ["#{name}/#{ref}"]) + path = "#{@dir}/frontends/#{name}/#{ref}" + + Pleroma.Backports.mkdir_p!(path) + File.write!("#{path}/index.html", "from frontend plug") + + index = + conn + |> put_req_cookie("preferred_frontend", "#{name}/#{ref}") + |> get("/") + + assert html_response(index, 200) == "from frontend plug" + end + + test "only returns content from pickable frontends", %{conn: conn} do + clear_config([:instance, :static_dir], "instance/static") + clear_config([:frontends, :pickable], ["pleroma-fe/develop", "pl-fe/develop"]) + + config_file = + conn + |> put_req_cookie("preferred_frontend", "../../../config") + |> get("/config.exs") + + refute response(config_file, 200) =~ "import Config" + end + end end