Merge branch 'preferred-frontend' into 'develop'

Port Akkoma frontend preference code

See merge request pleroma/pleroma!4398
This commit is contained in:
nicole mikołajczyk 2025-12-16 20:54:00 +01:00
commit d41e2fbaaf
14 changed files with 264 additions and 13 deletions

View file

@ -151,7 +151,8 @@ defmodule Pleroma.Web.ApiSpec do
"Suggestions",
"Announcements",
"Remote interaction",
"Others"
"Others",
"Preferred frontends"
]
}
]

View file

@ -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

View file

@ -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

View file

@ -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
end

View file

@ -0,0 +1,5 @@
defmodule Pleroma.Web.FrontendSwitcher.FrontendSwitcherView do
use Pleroma.Web, :view
import Phoenix.HTML.Form
end

View file

@ -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

View file

@ -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

View file

@ -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) ||

View file

@ -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

View file

@ -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 %>