This commit is contained in:
Sam
2026-06-10 11:51:56 -05:00
commit 66ba338b81
57 changed files with 5509 additions and 0 deletions

161
lib/forum/network.ex Normal file
View File

@@ -0,0 +1,161 @@
defmodule Forum.Network do
@moduledoc """
A per-network process for serving the app's forms view.
Requests to `/` use the root forms. Requests to `/<network>` use the
matching network forms from `priv/networks/<network>/index.html`.
"""
use GenServer
@default_hosts MapSet.new(["localhost", "127.0.0.1", "0.0.0.0", ""])
def child_spec(opts) do
slug = opts |> Keyword.fetch!(:slug) |> normalize_slug()
%{
id: {:network, slug},
start: {__MODULE__, :start_link, [[slug: slug]]},
restart: :permanent,
shutdown: 5_000,
type: :worker
}
end
def start_link(opts) do
slug = Keyword.fetch!(opts, :slug)
GenServer.start_link(__MODULE__, slug, name: via(slug))
end
def normalize_slug(slug) when is_binary(slug) do
slug
|> String.downcase()
|> String.split(":", parts: 2)
|> hd()
end
def serve(pid, request) do
GenServer.call(pid, {:serve, request})
end
@impl true
def init(slug) do
{:ok, %{slug: normalize_slug(slug), root: site_root(slug), started_at: DateTime.utc_now()}}
end
@impl true
def handle_call({:serve, request}, _from, state) do
response = do_serve(state, request)
{:reply, response, state}
end
defp do_serve(%{slug: slug} = state, request) do
path = Map.fetch!(request, :path)
cond do
default_site?(slug) and path in ["", "/"] ->
desktop_response()
network_site?(slug) and path in ["", "/"] ->
network_forms_response(slug)
File.dir?(state.root) ->
static_response(state.root, path)
true ->
%{
status: 404,
content_type: "text/plain",
body:
"No network configured for #{slug}. Create files under #{Path.relative_to_cwd(state.root)}."
}
end
end
defp desktop_response do
body =
Forum.Assets.path("ui/desktop.html")
|> File.read!()
|> String.replace("<!-- FORMS_HTML -->", Forum.Forms.html())
%{status: 200, content_type: "text/html", body: body}
end
defp network_forms_response(slug) do
body =
Forum.Assets.path("ui/desktop.html")
|> File.read!()
|> String.replace("<!-- FORMS_HTML -->", Forum.Forms.html(slug))
%{status: 200, content_type: "text/html", body: body}
end
defp static_response(root, path) do
file_path = resolve_path(root, path)
cond do
file_path == :invalid ->
%{status: 403, content_type: "text/plain", body: "Forbidden"}
File.dir?(file_path) and File.regular?(Path.join(file_path, "index.html")) ->
send_file(Path.join(file_path, "index.html"))
File.regular?(file_path) ->
send_file(file_path)
true ->
%{status: 404, content_type: "text/plain", body: "Not found"}
end
end
defp resolve_path(root, path) do
relative =
path
|> String.trim_leading("/")
|> URI.decode()
candidate = Path.expand(relative, root)
rel_to_root = Path.relative_to(candidate, Path.expand(root))
if rel_to_root == ".." or String.starts_with?(rel_to_root, "../") do
:invalid
else
candidate
end
end
defp send_file(path) do
%{status: 200, content_type: content_type(path), body: File.read!(path)}
end
defp content_type(path) do
case Path.extname(path) do
".html" -> "text/html"
".css" -> "text/css"
".js" -> "application/javascript"
".json" -> "application/json"
".svg" -> "image/svg+xml"
".png" -> "image/png"
".jpg" -> "image/jpeg"
".jpeg" -> "image/jpeg"
".webp" -> "image/webp"
".gif" -> "image/gif"
".ico" -> "image/x-icon"
".txt" -> "text/plain"
_ -> "application/octet-stream"
end
end
defp site_root(slug) do
source_path = Path.expand(Path.join(["priv", "networks", normalize_slug(slug)]), File.cwd!())
if File.exists?(source_path) do
source_path
else
Application.app_dir(:forum, Path.join(["priv", "networks", normalize_slug(slug)]))
end
end
defp default_site?(slug), do: MapSet.member?(@default_hosts, slug)
defp network_site?(slug), do: slug in Forum.Networks.network_slugs()
defp via(slug), do: {:via, Registry, {Forum.ProcessRegistry, {:network, normalize_slug(slug)}}}
end