Commit d79cc5f5 authored by Roman Chvanikov's avatar Roman Chvanikov

Add/update mix tasks for bundles and basic routing functionality

parent dd4d10b2
Pipeline #24281 failed with stages
in 6 minutes
......@@ -52,6 +52,11 @@
hostname: "localhost",
pool_size: 10
config :pleroma, :frontends,
primary: %{"name" => "pleroma", "ref" => "develop"},
mastodon: "",
admin: "develop"
if File.exists?("./config/dev.secret.exs") do
import_config "dev.secret.exs"
else
......
# Managing frontends
`mix pleroma.frontend install kenoma --ref=stable`
`develop` and `stable` refs are special: they are not necessary `develop` or
`stable` branches of the chosen frontend repo, but are smart aliases for either
default branch of a frontend repo (develop), or latest release in a repo (stable).
Only refs that have been built with Gitlab CI can be installed
\ No newline at end of file
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Frontend do
use Mix.Task
import Mix.Pleroma
# alias Pleroma.Config
@shortdoc "Manages bundled Pleroma frontends"
@moduledoc File.read!("docs/administration/CLI_tasks/frontend.md")
@known_frontends ~w(pleroma kenoma mastodon admin)
@pleroma_gitlab_host "git.pleroma.social"
@projects %{
"pleroma" => "pleroma/pleroma-fe",
"kenoma" => "lambadalambda/kenoma",
"admin" => "pleroma/admin-fe",
"mastodon" => "pleroma/mastofe"
}
def run(["install", "none" | _args]) do
shell_info("Skipping frontend installation because none was requested")
end
def run(["install", unknown_fe | _args]) when unknown_fe not in @known_frontends do
shell_error(
"Frontend #{unknown_fe} is not known. Known frontends are: #{
Enum.join(@known_frontends, ", ")
}"
)
end
def run(["install", frontend | args]) do
{:ok, _} = Application.ensure_all_started(:pleroma)
{options, [], []} =
OptionParser.parse(
args,
strict: [
ref: :string
]
)
ref = suggest_ref(options, frontend)
%{"name" => bundle_name, "url" => bundle_url} =
get_bundle_meta(ref, @pleroma_gitlab_host, @projects[frontend])
shell_info("Installing #{frontend} frontend (version: #{bundle_name}, url: #{bundle_url})")
dest = Path.join([Pleroma.Config.get!([:instance, :static_dir]), "frontends", frontend, ref])
case install_bundle(bundle_url, dest) do
:ok ->
shell_info("Installed!")
{:error, error} ->
shell_error("Error: #{inspect(error)}")
end
end
defp suggest_ref(options, frontend) do
case Pleroma.Config.get([:frontends, String.to_atom(frontend)]) do
nil ->
primary_fe_config = Pleroma.Config.get([:frontends, :primary])
case primary_fe_config["name"] == frontend do
true ->
primary_fe_config["ref"]
false ->
nil
end
val ->
val
end
|> case do
nil ->
stable_pleroma? = Pleroma.Application.stable?()
current_stable_out =
case stable_pleroma? do
true -> "stable"
false -> "develop"
end
get_option(
options,
:ref,
"You are currently running #{current_stable_out} version of Pleroma backend. What version of \"#{
frontend
}\" frontend you want to install? (\"stable\", \"develop\" or specific ref)",
current_stable_out
)
config_value ->
current_ref =
case config_value do
%{"ref" => ref} -> ref
ref -> ref
end
get_option(
options,
:ref,
"You are currently running #{current_ref} version of \"#{frontend}\" frontend. What version do you want to install? (\"stable\", \"develop\" or specific ref)",
current_ref
)
end
end
defp get_bundle_meta("develop", gitlab_base_url, project) do
url = "#{gitlab_api_url(gitlab_base_url, project)}/repository/branches"
http_client = http_client()
%{status: 200, body: json} = Tesla.get!(http_client, url)
%{"name" => name, "commit" => %{"short_id" => last_commit_ref}} =
Enum.find(json, &(&1["default"] == true))
%{
"name" => name,
"url" => build_url(gitlab_base_url, project, last_commit_ref)
}
end
defp get_bundle_meta("stable", gitlab_base_url, project) do
url = "#{gitlab_api_url(gitlab_base_url, project)}/releases"
http_client = http_client()
%{status: 200, body: json} = Tesla.get!(http_client, url)
[%{"commit" => %{"short_id" => commit_id}, "name" => name} | _] =
Enum.sort(json, fn r1, r2 -> r1 > r2 end)
%{
"name" => name,
"url" => build_url(gitlab_base_url, project, commit_id)
}
end
defp get_bundle_meta(ref, gitlab_base_url, project) do
%{
"name" => ref,
"url" => build_url(gitlab_base_url, project, ref)
}
end
defp install_bundle(bundle_url, dir) do
http_client = http_client()
with {:ok, %{status: 200, body: zip_body}} <- Tesla.get(http_client, bundle_url),
{:ok, unzipped} <- :zip.unzip(zip_body, [:memory]) do
unzipped
|> Enum.each(fn {path, data} ->
path =
path
|> to_string()
|> String.replace(~r/^dist\//, "")
file_path = Path.join(dir, path)
file_path
|> Path.dirname()
|> File.mkdir_p!()
File.write!(file_path, data)
end)
else
{:ok, %{status: 404}} ->
{:error, "Bundle not found"}
error ->
{:error, error}
end
end
defp gitlab_api_url(gitlab_base_url, project),
do: "https://#{gitlab_base_url}/api/v4/projects/#{URI.encode_www_form(project)}"
defp build_url(gitlab_base_url, project, ref),
do: "https://#{gitlab_base_url}/#{project}/-/jobs/artifacts/#{ref}/download?job=build"
defp http_client do
middleware = [
Tesla.Middleware.FollowRedirects,
Tesla.Middleware.JSON
]
Tesla.client(middleware)
end
end
......@@ -33,7 +33,14 @@ def run(["gen" | rest]) do
uploads_dir: :string,
static_dir: :string,
listen_ip: :string,
listen_port: :string
listen_port: :string,
fe_primary: :string,
fe_primary_ref: :string,
fe_mastodon: :string,
fe_mastodon_ref: :string,
fe_admin: :string,
fe_admin_ref: :string,
fe_static: :string
],
aliases: [
o: :output,
......@@ -158,6 +165,58 @@ def run(["gen" | rest]) do
Config.put([:instance, :static_dir], static_dir)
install_fe = &Mix.Tasks.Pleroma.Frontend.run(["install", &1, "--ref", &2])
fe_primary =
get_option(
options,
:fe_primary,
"Choose primary frontend for your instance (available: pleroma/kenoma/none)",
"pleroma"
)
fe_primary_ref =
get_frontend_ref(fe_primary !== "none", fe_primary, :fe_primary_ref, options)
install_fe.(fe_primary, fe_primary_ref)
enable_static_fe? =
get_option(
options,
:fe_static,
"Would you like to enable Static frontend (render profiles and posts using server-generated HTML that is viewable without using JavaScript)?",
"y"
) === "y"
install_mastodon_fe? =
get_option(
options,
:fe_mastodon,
"Would you like to install Mastodon frontend?",
"y"
) === "y"
fe_mastodon_ref =
get_frontend_ref(install_mastodon_fe?, "mastodon", :fe_mastodon_ref, options)
with true <- install_mastodon_fe? do
install_fe.("mastodon", fe_mastodon_ref)
end
install_admin_fe? =
get_option(
options,
:fe_admin,
"Would you like to install Admin frontend?",
"y"
) === "y"
fe_admin_ref = get_frontend_ref(install_admin_fe?, "admin", :fe_admin_ref, options)
with true <- install_admin_fe? do
install_fe.("admin", fe_admin_ref)
end
secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
jwt_secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64)
signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8)
......@@ -186,7 +245,11 @@ def run(["gen" | rest]) do
uploads_dir: uploads_dir,
rum_enabled: rum_enabled,
listen_ip: listen_ip,
listen_port: listen_port
listen_port: listen_port,
fe_primary: %{"name" => fe_primary, "ref" => fe_primary_ref},
fe_mastodon: %{"name" => "mastodon", "ref" => fe_mastodon_ref},
fe_admin: %{"name" => "admin", "ref" => fe_admin_ref},
enable_static_fe?: enable_static_fe?
)
result_psql =
......@@ -247,4 +310,25 @@ defp write_robots_txt(indexable, template_dir) do
File.write(robots_txt_path, robots_txt)
shell_info("Writing #{robots_txt_path}.")
end
defp get_frontend_ref(false, _frontend, _option_key, _options), do: ""
defp get_frontend_ref(true, frontend, option_key, options) do
stable_pleroma? = Pleroma.Application.stable?()
current_stable_out =
case stable_pleroma? do
true -> "stable"
false -> "develop"
end
get_option(
options,
option_key,
"You are currently running #{current_stable_out} version of Pleroma. What version of #{
frontend
} you want to install? (\"stable\", \"develop\" or specific ref)",
current_stable_out
)
end
end
......@@ -272,7 +272,7 @@ def run(["invite" | rest]) do
shell_info("Generated user invite token " <> String.replace(invite.invite_type, "_", " "))
url =
Pleroma.Web.Router.Helpers.redirect_url(
Pleroma.Web.Router.Helpers.frontend_url(
Pleroma.Web.Endpoint,
:registration_page,
invite.token
......
......@@ -13,11 +13,13 @@ defmodule Pleroma.Application do
@name Mix.Project.config()[:name]
@version Mix.Project.config()[:version]
@stable? Mix.Project.config()[:stable?]
@repository Mix.Project.config()[:source_url]
@env Mix.env()
def name, do: @name
def version, do: @version
def stable?, do: @stable?
def named_version, do: @name <> " " <> @version
def repository, do: @repository
......
......@@ -47,7 +47,7 @@ def user_invitation_email(
to_name \\ nil
) do
registration_url =
Router.Helpers.redirect_url(
Router.Helpers.frontend_url(
Endpoint,
:registration_page,
user_invite_token.token
......
defmodule Pleroma.Frontend do
def get_primary_fe_config,
do: [:frontends] |> Pleroma.Config.get(%{}) |> get_primary_fe_config()
def get_primary_fe_config(%{primary: %{"name" => "none"}, static: static}) do
%{
config: %{},
module: Pleroma.Frontend.Headless,
static: static
}
end
def get_primary_fe_config(fe_config) do
%{
config: fe_config[:primary],
module: Module.concat(Pleroma.Frontend, String.capitalize(fe_config[:primary][:name])),
static: fe_config[:static]
}
end
end
defmodule Pleroma.Plugs.FrontendPlug do
@moduledoc """
Sets private key `:frontend` for the given connection.
It is set to one of admin|mastodon|primary frontends config values based
on `conn.path_info`
"""
import Plug.Conn
@behaviour Plug
def init(_opts) do
Pleroma.Config.get([:frontends], %{})
end
def call(%{path_info: ["pleroma", "admin" | _rest]} = conn, fe_config) do
put_private(conn, :frontend, %{
config: fe_config[:admin],
module: Pleroma.Frontend.Admin
})
end
def call(%{path_info: ["web" | _rest]} = conn, fe_config) do
put_private(conn, :frontend, %{
config: fe_config[:mastodon],
module: Pleroma.Frontend.Mastodon
})
end
def call(conn, fe_config) do
put_private(conn, :frontend, Pleroma.Frontend.get_primary_fe_config(fe_config))
end
end
......@@ -10,17 +10,6 @@ defmodule Pleroma.Plugs.InstanceStatic do
"""
@behaviour Plug
def file_path(path) do
instance_path =
Path.join(Pleroma.Config.get([:instance, :static_dir], "instance/static/"), path)
if File.exists?(instance_path) do
instance_path
else
Path.join(Application.app_dir(:pleroma, "priv/static/"), path)
end
end
@only ~w(index.html robots.txt static emoji packs sounds images instance favicon.png sw.js
sw-pleroma.js)
......@@ -38,22 +27,28 @@ def call(%{request_path: "/" <> unquote(only) <> _} = conn, opts) do
call_static(
conn,
opts,
unquote(at),
Pleroma.Config.get([:instance, :static_dir], "instance/static")
unquote(at)
)
end
end
def call(conn, _) do
conn
end
def call(conn, _opts), do: conn
defp call_static(conn, opts, at, from) do
opts =
opts
|> Map.put(:from, from)
|> Map.put(:at, at)
defp call_static(conn, opts, at) do
instance_static_path = Pleroma.Config.get([:instance, :static_dir], "instance/static")
opts = %{opts | at: at, from: instance_static_path}
Plug.Static.call(conn, opts)
# try to serve static file from frontend-specific directory
# if it fails, conn returned from Plug.Static.call/2 remains the same as before
# and we fallback to try to serve file from instance static directory
with %{"name" => name, "ref" => ref} <- Pleroma.Config.get([:frontends, :primary]),
opts2 = %{opts | from: Path.join([instance_static_path, "frontends", name, ref])},
conn2 = Plug.Static.call(conn, opts2),
true <- conn2 !== conn do
conn2
else
_ ->
Plug.Static.call(conn, opts)
end
end
end
......@@ -18,7 +18,7 @@ def call(conn, _) do
end
end
defp enabled?, do: Pleroma.Config.get([:static_fe, :enabled], false)
defp enabled?, do: Pleroma.Config.get([:frontends, :static], false)
defp accepts_html?(conn) do
case get_req_header(conn, "accept") do
......
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Fallback.RedirectController do
use Pleroma.Web, :controller
require Logger
alias Pleroma.User
alias Pleroma.Web.Metadata
def api_not_implemented(conn, _params) do
conn
|> put_status(404)
|> json(%{error: "Not implemented"})
end
def redirector(conn, _params, code \\ 200)
# redirect to admin section
# /pleroma/admin -> /pleroma/admin/
#
def redirector(conn, %{"path" => ["pleroma", "admin"]} = _, _code) do
redirect(conn, to: "/pleroma/admin/")
end
def redirector(conn, _params, code) do
conn
|> put_resp_content_type("text/html")
|> send_file(code, index_file_path())
end
def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do
with %User{} = user <- User.get_cached_by_nickname_or_id(maybe_nickname_or_id) do
redirector_with_meta(conn, %{user: user})
else
nil ->
redirector(conn, params)
end
end
def redirector_with_meta(conn, params) do
{:ok, index_content} = File.read(index_file_path())
tags =
try do
Metadata.build_tags(params)
rescue
e ->
Logger.error(
"Metadata rendering for #{conn.request_path} failed.\n" <>
Exception.format(:error, e, __STACKTRACE__)
)
""
end
response = String.replace(index_content, "<!--server-generated-meta-->", tags)
conn
|> put_resp_content_type("text/html")
|> send_resp(200, response)
end
def index_file_path do
Pleroma.Plugs.InstanceStatic.file_path("index.html")
end
def registration_page(conn, params) do
redirector(conn, params)
end
def empty(conn, _params) do
conn
|> put_status(204)
|> text("")
end
end
......@@ -5,7 +5,6 @@
defmodule Pleroma.Web.Feed.UserController do
use Pleroma.Web, :controller
alias Fallback.RedirectController
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.ActivityPubController
......@@ -19,7 +18,9 @@ defmodule Pleroma.Web.Feed.UserController do
def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do
RedirectController.redirector_with_meta(conn, %{user: user})
conn
|> Map.put(:params, %{user: user})
|> Pleroma.Web.FrontendController.call(:index_with_meta)
end
end
......
defmodule Pleroma.Web.Frontend.AdminController do
use Pleroma.Web, :controller
end
defmodule Pleroma.Web.Frontend.DefaultController do
defmacro __using__(_opts) do
quote do
import Pleroma.Web.FrontendController, only: [index_file_path: 1]
def index(conn, _params, fe_config) do
status = conn.status || 200
conn
|> put_resp_content_type("text/html")
|> send_file(status, index_file_path(fe_config))
end
def api_not_implemented(conn, _params, _fe_config) do
conn
|> put_status(404)
|> json(%{error: "Not implemented"})
end
def empty(conn, _params, _fe_config) do
conn
|> put_status(204)
|> text("")
end
defoverridable index: 3, api_not_implemented: 3, empty: 3
end
end
end
defmodule Pleroma.Web.Frontend.HeadlessController do
use Pleroma.Web, :controller
def index(conn, _params) do
conn
|> put_status(404)
|> text("")
end
end
defmodule Pleroma.Web.Frontend.KenomaController do
use Pleroma.Web, :controller
use Pleroma.Web.Frontend.DefaultController
end
defmodule Pleroma.Web.Frontend.MastodonController do
use Pleroma.Web, :controller
alias Pleroma.Plugs.OAuthScopesPlug
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :put_settings)
# Note: :index action handles attempt of unauthenticated access to private instance with redirect
plug(
OAuthScopesPlug,
%{scopes: ["read"], fallback: :proceed_unauthenticated, skip_instance_privacy_check: true}
when action == :index
)
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :index)
def index(%{assigns: %{user: user, token: token}} = conn, _params)
when not is_nil(user) and not is_nil(token) do
conn
|> put_layout(false)
|> render("index.html",
token: token.token,
user: user,
custom_emojis: Pleroma.Emoji.get_all()
)
end
def index(conn, _params) do
conn
|> put_session(:return_to, conn.request_path)
|> redirect(to: "/web/login")
end
@doc "GET /web/manifest.json"
def manifest(conn, _params) do
# TODO move view?
render(conn, "manifest.json")
end
end
defmodule Pleroma.Web.Frontend.PleromaController do
use Pleroma.Web, :controller
use Pleroma.Web.Frontend.DefaultController
require Logger
alias Pleroma.User
alias Pleroma.Web.Metadata
def index_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params, fe_config) do
case User.get_cached_by_nickname_or_id(maybe_nickname_or_id) do
%User{} = user ->
index_with_meta(conn, %{user: user}, fe_config)
_ ->
index(conn, params, fe_config)