Web-based stream overlay manager
1import clover/counter
2import clover/data
3import clover/pages/index
4import clover/pages/room
5import ewe
6import formal/form
7import gleam/erlang/application
8import gleam/erlang/process
9import gleam/http
10import gleam/http/cookie
11import gleam/http/request
12import gleam/http/response
13import gleam/int
14import gleam/list
15import gleam/option
16import gleam/string
17import gleam/uri
18import logging
19import lustre
20
21pub fn main() {
22 logging.configure()
23 logging.set_level(logging.Info)
24
25 let counter = counter.component()
26 let assert Ok(component) = lustre.start_server_component(counter, Nil)
27
28 let assert Ok(_) =
29 ewe.new(handler(_, component))
30 |> ewe.bind_all()
31 |> ewe.listening(1234)
32 |> ewe.start()
33
34 process.sleep_forever()
35}
36
37fn log_req(req: ewe.Request, f: fn(ewe.Request) -> ewe.Response) -> ewe.Response {
38 let resp = f(req)
39 logging.log(logging.Info, int.to_string(resp.status) <> " " <> req.path)
40 resp
41}
42
43fn handler(
44 req: ewe.Request,
45 counter: lustre.Runtime(counter.Msg),
46) -> ewe.Response {
47 use req <- log_req(req)
48
49 let pass = "1234"
50 let authed = authenticated(req, pass)
51
52 case request.path_segments(req) {
53 [] -> data.page(index.view())
54 ["join"] ->
55 case list.key_find(req.headers, "content-type") {
56 Ok("application/x-www-form-urlencoded")
57 | Ok("application/x-www-form-urlencoded; " <> _) -> {
58 use body <- data.require_string_body(req)
59 case uri.parse_query(body) {
60 Error(_) -> data.bad_request("Invalid form encoding")
61 Ok(pairs) -> {
62 let pairs = sort_keys(pairs)
63 let form = index.room_form() |> form.add_values(pairs)
64 case form.run(form) {
65 Ok(data) if data.password == pass ->
66 data.redirect("/room/")
67 |> response.set_cookie(
68 "password",
69 data.password,
70 cookie.defaults(http.Https),
71 )
72 _ -> data.redirect("/")
73 }
74 }
75 }
76 }
77 _ -> data.redirect("/")
78 }
79 ["room"] if authed -> data.page(room.view())
80 ["room", "html"] -> data.page(room.view_html())
81 ["lustre", "runtime.mjs"] -> serve_runtime()
82 ["ws"] if authed -> serve_counter(req, counter)
83 _ -> data.not_found()
84 }
85}
86
87fn authenticated(req: ewe.Request, password: String) -> Bool {
88 let cookies = request.get_cookies(req)
89 list.contains(cookies, #("password", password))
90}
91
92fn sort_keys(pairs: List(#(String, t))) -> List(#(String, t)) {
93 list.sort(pairs, fn(a, b) { string.compare(a.0, b.0) })
94}
95
96fn serve_runtime() -> response.Response(ewe.ResponseBody) {
97 let assert Ok(lustre_priv) = application.priv_directory("lustre")
98 let file_path = lustre_priv <> "/static/lustre-server-component.mjs"
99
100 case ewe.file(file_path, offset: option.None, limit: option.None) {
101 Ok(f) ->
102 response.new(200)
103 |> response.prepend_header(
104 "content-type",
105 "application/javascript; charset=utf-8",
106 )
107 |> response.set_body(f)
108 Error(_) -> data.not_found()
109 }
110}
111
112fn serve_counter(
113 req: request.Request(ewe.Connection),
114 counter: lustre.Runtime(counter.Msg),
115) -> response.Response(ewe.ResponseBody) {
116 let on_init = fn(a, b) { counter.init_socket(a, b, counter) }
117
118 req
119 |> ewe.upgrade_websocket(
120 on_init:,
121 handler: counter.loop_socket,
122 on_close: counter.close_socket,
123 )
124}