For now? I'm experimenting on an old concept.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Some optimisations!


Signed-off-by: MLC Bloeiman <mar@strawmelonjuice.com>

+187 -178
+1
server/gleam.toml
··· 33 33 sqlight = ">= 1.0.3 and < 2.0.0" 34 34 simplifile = ">= 2.4.0 and < 3.0.0" 35 35 booklet = ">= 1.1.0 and < 2.0.0" 36 + humanise = ">= 1.1.0 and < 2.0.0" 36 37 37 38 [dev_dependencies] 38 39 gleeunit = ">= 1.0.0 and < 2.0.0"
+2
server/manifest.toml
··· 28 28 { name = "gramps", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "8B7195978FBFD30B43DF791A8A272041B81E45D245314D7A41FC57237AA882A0" }, 29 29 { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" }, 30 30 { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, 31 + { name = "humanise", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "humanise", source = "hex", outer_checksum = "5B78C52863B673F7D903117BE9262628210946406E04B6AB09554D5C2A7F15F0" }, 31 32 { name = "logging", version = "1.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "BC5F18CE5DD9686100229FE5409BDC3DD5C46D5A7DF2F804AD2D8F0DD6C5060E" }, 32 33 { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, 33 34 { name = "mist", version = "6.0.2", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "6B03DEEA38A02F276333CB27B53B16D3D45BD741B89599085A601BAF635F2006" }, ··· 60 61 gleam_otp = { version = ">= 1.2.0 and < 2.0.0" } 61 62 gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 62 63 gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 64 + humanise = { version = ">= 1.1.0 and < 2.0.0" } 63 65 mist = { version = ">= 6.0.2 and < 7.0.0" } 64 66 parrot = { version = ">= 2.2.1 and < 3.0.0" } 65 67 pog = { version = ">= 4.1.0 and < 5.0.0" }
+184 -178
server/src/lumina_server.gleam
··· 16 16 // This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. [cite: 5] 17 17 // See the Licence for the specific language governing permissions and limitations. [cite: 6] 18 18 19 - 19 + import booklet.{type Booklet} 20 20 import envoy 21 21 import ewe.{type Request, type Response} 22 + import gleam/bit_array 22 23 import gleam/erlang/application 23 24 import gleam/erlang/process 24 - import gleam/function 25 25 import gleam/http/response 26 26 import gleam/int 27 - import gleam/list 28 27 import gleam/option.{None} 29 28 import gleam/result 30 - import gleam/string 31 29 import gleam/uri 32 - import booklet.{type Booklet} 30 + import humanise 33 31 import simplifile 34 32 import sqlight 35 33 import woof 36 34 37 35 type HandlerContext { 38 - HandlerContext(db: sqlight.Connection, client_hash: String, assets: String) 36 + HandlerContext( 37 + db: sqlight.Connection, 38 + client_hash: String, 39 + assets: String, 40 + static_responses: StaticResponses, 41 + ) 39 42 } 43 + 44 + type StaticRoute { 45 + RouteForIndex 46 + RouteForClientAsMinifiedJavascript 47 + RouteForClientAsJavascript 48 + RouteForClientStyles 49 + RouteForIconAsPNG 50 + RouteForIconAsSVG 51 + } 52 + 53 + type StaticResponses = 54 + fn(StaticRoute) -> response.Response(ewe.ResponseBody) 55 + 40 56 type ClientConnectionData { 41 - ClientConnectionData( 42 - client_type: option.Option(ClientType), 43 - user: option.Option(User) 44 - ) 57 + ClientConnectionData( 58 + client_type: option.Option(ClientType), 59 + user: option.Option(User), 60 + ) 45 61 } 62 + 46 63 type ClientType { 47 - WebClient 48 - NativeApp 64 + WebClient 65 + NativeApp 49 66 } 50 67 51 68 type User { 52 - User( 53 - // Todo 54 - Nil 55 - ) 69 + User( 70 + // Todo 71 + Nil, 72 + ) 56 73 } 57 74 58 75 pub fn main() { ··· 101 118 } 102 119 Ok(outcome) -> { 103 120 setuplog 104 - |> woof.log(woof.Info, "Found client revision!", [#("revision", outcome)]) 121 + |> woof.log(woof.Info, "Found client revision!", [ 122 + woof.field("revision", outcome), 123 + ]) 105 124 outcome 106 125 } 107 126 } 127 + 128 + let static_responses = static(client_hash, assets, setuplog) 108 129 // And start! 109 130 let assert Ok(_) = 110 - ewe.new(handler(_, HandlerContext(db:, assets:, client_hash:))) 131 + ewe.new(handler( 132 + _, 133 + HandlerContext(db:, assets:, client_hash:, static_responses:), 134 + )) 111 135 |> ewe.bind("0.0.0.0") 112 136 |> ewe.listening( 113 137 port: envoy.get("PORT") ··· 120 144 process.sleep_forever() 121 145 } 122 146 147 + fn static( 148 + client_hash: String, 149 + assets: String, 150 + setuplog: woof.Logger, 151 + ) -> StaticResponses { 152 + let client_servible = 153 + [ 154 + << 155 + "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, viewport-fit=cover\" /><title>Lumina</title><link rel=\"preconnect\" href=\"https://fontlay.com\" corossorigin /><link href=\"https://fontlay.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&family=Elms+Sans:ital,wght@0,100..900;1,100..900&family=Gantari:ital,wght@0,100..900;1,100..900&family=Josefin+Sans:ital,wght@0,100..700;1,100..700&family=Vend+Sans&display=swap\" rel=\"stylesheet\"><link rel=\"stylesheet\" href=\"/static/lumina.css\"/><meta name=\"robots\" content=\"noai, noimageai, nofollow\"><script>window.clientHash = \"":utf8, 156 + >>, 157 + client_hash |> bit_array.from_string, 158 + <<"\";</script><script type=\"module\">":utf8>>, 159 + { 160 + let assert Ok(client_js) = 161 + simplifile.read_bits(assets <> "/static/lumina_client.min.mjs") 162 + client_js 163 + }, 164 + <<"</script></head><body id=\"app\"></body></html>":utf8>>, 165 + ] 166 + |> bit_array.concat() 167 + setuplog 168 + |> woof.log( 169 + woof.Debug, 170 + "Total client size is: " 171 + <> bit_array.byte_size(client_servible) |> humanise.bytes_int(), 172 + [#("revision", client_hash)], 173 + ) 174 + let builtin_file = fn(file: String, mime: String) -> response.Response( 175 + ewe.ResponseBody, 176 + ) { 177 + case simplifile.read_bits(file) { 178 + Error(_) -> { 179 + setuplog 180 + |> woof.log(woof.Error, "Missing application assets.", [ 181 + woof.field("File", file), 182 + ]) 183 + panic as "Missing application assets." 184 + } 185 + Ok(outcome) -> { 186 + response.new(200) 187 + |> response.set_header("content-type", mime) 188 + |> response.set_body(ewe.BitsData(outcome)) 189 + } 190 + } 191 + } 192 + let index = 193 + response.set_body( 194 + response.set_header( 195 + response.new(200), 196 + "content-type", 197 + "text/html; charset=utf-8", 198 + ), 199 + ewe.BitsData(client_servible), 200 + ) 201 + let client_js_min = 202 + builtin_file( 203 + assets <> "/static/lumina_client.min.mjs", 204 + "application/javascript; charset=utf-8", 205 + ) 206 + let client_js = 207 + builtin_file( 208 + assets <> "/static/lumina_client.mjs", 209 + "application/javascript; charset=utf-8", 210 + ) 211 + let client_styles = 212 + builtin_file( 213 + assets <> "/static/lumina_client.css", 214 + "text/css; charset=utf-8", 215 + ) 216 + let icon_png = builtin_file(assets <> "/static/logo.png", "image/png") 217 + let icon_svg = builtin_file(assets <> "/static/logo.svg", "image/svg") 218 + fn(route: StaticRoute) { 219 + case route { 220 + RouteForIndex -> index 221 + RouteForIconAsSVG -> icon_svg 222 + RouteForIconAsPNG -> icon_png 223 + RouteForClientStyles -> client_styles 224 + RouteForClientAsJavascript -> client_js 225 + RouteForClientAsMinifiedJavascript -> client_js_min 226 + } 227 + } 228 + } 229 + 123 230 fn handler(req: Request, handler_ctx: HandlerContext) -> Response { 124 231 let httplogger = fn( 125 232 level: woof.Level, ··· 127 234 vars: List(#(String, String)), 128 235 ) { 129 236 woof.new("WEBSERVER") 130 - |> woof.log( 131 - level, 132 - msg, 133 - vars 134 - |> list.append([ 135 - woof.field("uri path", req.path), 136 - ]), 137 - ) 237 + |> woof.log(level, msg, [woof.field("uri path", req.path), ..vars]) 138 238 } 139 239 case req.path |> uri.path_segments() { 140 - ["/"] | [""] | [] -> { 141 - httplogger(woof.Info, "OK", []) 142 - response.new(200) 143 - |> response.set_header("content-type", "text/html; charset=utf-8") 144 - |> response.set_body(ewe.TextData( 145 - "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, viewport-fit=cover\" /><title>Lumina</title><link rel=\"preconnect\" href=\"https://fontlay.com\" corossorigin /><link href=\"https://fontlay.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&family=Elms+Sans:ital,wght@0,100..900;1,100..900&family=Gantari:ital,wght@0,100..900;1,100..900&family=Josefin+Sans:ital,wght@0,100..700;1,100..700&family=Vend+Sans&display=swap\" rel=\"stylesheet\"><link rel=\"stylesheet\" href=\"/static/lumina.css\"/><meta name=\"robots\" content=\"noai, noimageai, nofollow\"><script>window.clientHash = \"" 146 - <> { handler_ctx.client_hash } 147 - <> "\";</script><script type=\"module\" src=\"/static/lumina.min.mjs\"></script></head><body id=\"app\"></body></html>", 148 - )) 149 - } 150 - ["static", "lumina.min.mjs"] -> { 151 - let file = handler_ctx.assets <> "/static/lumina_client.min.mjs" 152 - case ewe.file(file, None, None) { 153 - Error(_) -> { 154 - httplogger(woof.Error, "Missing application assets.", []) 155 - response.new(500) 156 - |> response.set_header("content-type", "text/plain; charset=utf-8") 157 - |> response.set_body(ewe.TextData("500 Internal Server Error")) 158 - } 159 - Ok(outcome) -> { 160 - httplogger(woof.Info, "OK", []) 161 - response.new(200) 162 - |> response.set_header( 163 - "content-type", 164 - "application/javascript; charset=utf-8", 165 - ) 166 - |> response.set_body(outcome) 167 - } 168 - } 169 - } 170 - ["static", "lumina.mjs"] -> { 171 - let file = handler_ctx.assets <> "/static/lumina_client.mjs" 172 - case ewe.file(file, None, None) { 173 - Error(_) -> { 174 - httplogger(woof.Error, "Missing application assets.", []) 175 - response.new(500) 176 - |> response.set_header("content-type", "text/plain; charset=utf-8") 177 - |> response.set_body(ewe.TextData("500 Internal Server Error")) 178 - } 179 - Ok(outcome) -> { 180 - httplogger(woof.Info, "OK", []) 181 - response.new(200) 182 - |> response.set_header( 183 - "content-type", 184 - "application/javascript; charset=utf-8", 185 - ) 186 - |> response.set_body(outcome) 187 - } 188 - } 189 - } 190 - ["static", "lumina.css"] -> { 191 - let file = handler_ctx.assets <> "/static/lumina_client.css" 192 - case ewe.file(file, None, None) { 193 - Error(_) -> { 194 - httplogger(woof.Error, "Missing application assets.", []) 195 - response.new(500) 196 - |> response.set_header("content-type", "text/plain; charset=utf-8") 197 - |> response.set_body(ewe.TextData("500 Internal Server Error")) 198 - } 199 - Ok(outcome) -> { 200 - httplogger(woof.Info, "OK", []) 201 - response.new(200) 202 - |> response.set_header("content-type", "text/css; charset=utf-8") 203 - |> response.set_body(outcome) 204 - } 205 - } 206 - } 240 + ["/"] | [""] | [] -> { 241 + httplogger(woof.Info, "OK", []) 242 + handler_ctx.static_responses(RouteForIndex) 243 + } 244 + ["static", "lumina.min.mjs"] -> { 245 + httplogger(woof.Info, "OK", []) 246 + handler_ctx.static_responses(RouteForClientAsMinifiedJavascript) 247 + } 248 + ["static", "lumina.mjs"] -> { 249 + httplogger(woof.Info, "OK", []) 250 + handler_ctx.static_responses(RouteForClientAsJavascript) 251 + } 252 + ["static", "lumina.css"] -> { 253 + httplogger(woof.Info, "OK", []) 254 + handler_ctx.static_responses(RouteForClientStyles) 255 + } 207 256 208 - ["favicon.ico"] | ["static", "logo.png"] -> { 209 - let file = handler_ctx.assets <> "/static/logo.png" 210 - case ewe.file(file, None, None) { 211 - Error(_) -> { 212 - httplogger(woof.Error, "Missing application assets.", []) 213 - response.new(500) 214 - |> response.set_header("content-type", "text/plain; charset=utf-8") 215 - |> response.set_body(ewe.TextData("500 Internal Server Error")) 216 - } 217 - Ok(outcome) -> { 218 - httplogger(woof.Info, "OK", []) 219 - response.new(200) 220 - |> response.set_header("content-type", "image/png;") 221 - |> response.set_body(outcome) 222 - } 223 - } 224 - } 225 - ["static", staticfile] -> { 226 - let file = handler_ctx.assets <> "/static/" <> staticfile 227 - case ewe.file(file, None, None) { 228 - Error(_) -> { 229 - httplogger(woof.Warning, "Not found.", [woof.field("file", file)]) 230 - response.new(404) 231 - |> response.set_header("content-type", "text/plain; charset=utf-8") 232 - |> response.set_body(ewe.TextData("404! Not found!")) 233 - } 234 - Ok(outcome) -> { 235 - httplogger(woof.Info, "OK", [woof.field("file", file)]) 236 - response.new(200) 237 - |> case 238 - { 239 - staticfile 240 - |> string.split(".") 241 - |> list.last() 242 - |> result.unwrap("") 243 - } 244 - { 245 - "png" -> response.set_header(_, "content-type", "image/png;") 246 - "html" -> response.set_header(_, "content-type","text/html; charset=utf-8") 247 - "svg" -> response.set_header(_, "content-type", "image/svg+xml") 248 - "ttf" -> response.set_header(_, "content-type", "font/ttf") 249 - _ -> function.identity 250 - } 251 - |> response.set_body(outcome) 252 - } 253 - } 254 - } 255 - ["connection"] -> 256 - { 257 - ewe.upgrade_websocket( 258 - req, 259 - // If ever we need to send messages through processes to get to and from the client over here, we should 260 - // take a second look at the ewe example on 261 - // https://github.com/vshakitskiy/ewe/blob/mistress/examples/src/websocket.gleam 262 - on_init: fn(_conn, selector) { 263 - // Initial state for THIS specific client 264 - let state = WebsocketState( 265 - ctx: handler_ctx, 266 - conn_data: ClientConnectionData(None, None) 267 - ) 268 - #(state, selector) 269 - }, 270 - handler: client_communication_handler, 271 - on_close: fn(_conn, _state) { 272 - todo 273 - }, 274 - ) 275 - } 257 + ["favicon.ico"] | ["static", "logo.png"] -> { 258 + httplogger(woof.Info, "OK", []) 259 + handler_ctx.static_responses(RouteForIconAsPNG) 260 + } 261 + ["static", "logo.svg"] -> { 262 + httplogger(woof.Info, "OK", []) 263 + handler_ctx.static_responses(RouteForIconAsSVG) 264 + } 265 + ["connection"] -> { 266 + ewe.upgrade_websocket( 267 + req, 268 + // If ever we need to send messages through processes to get to and from the client over here, we should 269 + // take a second look at the ewe example on 270 + // https://github.com/vshakitskiy/ewe/blob/mistress/examples/src/websocket.gleam 271 + on_init: fn(_conn, selector) { 272 + // Initial state for THIS specific client 273 + let state = 274 + WebsocketState( 275 + ctx: handler_ctx, 276 + conn_data: ClientConnectionData(None, None), 277 + ) 278 + #(state, selector) 279 + }, 280 + handler: client_communication_handler, 281 + on_close: fn(_conn, _state) { todo as "On close not yet written." }, 282 + ) 283 + } 276 284 _ -> { 277 285 httplogger(woof.Warning, "Not found.", []) 278 286 response.new(404) ··· 283 291 } 284 292 285 293 type WebsocketState { 286 - WebsocketState( 287 - ctx: HandlerContext, 288 - conn_data: ClientConnectionData, 289 - ) 294 + WebsocketState(ctx: HandlerContext, conn_data: ClientConnectionData) 290 295 } 291 296 292 297 fn client_communication_handler( 293 - _conn: ewe.WebsocketConnection, 294 - state: WebsocketState, 295 - // That Nil is the internal message, again if we'd follow the example. But 296 - // Lumina mostly communicates with the database and stores more global variables in Booklets (which is ETS)... So no need. 297 - message: ewe.WebsocketMessage(Nil)) -> ewe.WebsocketNext(WebsocketState, Nil){ 298 - case message { 299 - ewe.Text(json_str) -> { 300 - // Todo 301 - ewe.websocket_continue(state) 302 - } 303 - ewe.Binary(_) -> ewe.websocket_continue(state) 304 - ewe.User(Nil) -> ewe.websocket_continue(state) 305 - } 298 + _conn: ewe.WebsocketConnection, 299 + state: WebsocketState, 300 + // That Nil is the internal message, again if we'd follow the example. But 301 + // Lumina mostly communicates with the database and stores more global variables in Booklets (which is ETS)... So no need. 302 + message: ewe.WebsocketMessage(Nil), 303 + ) -> ewe.WebsocketNext(WebsocketState, Nil) { 304 + case message { 305 + ewe.Text(json_str) -> { 306 + // Todo 307 + ewe.websocket_continue(state) 308 + } 309 + ewe.Binary(_) -> ewe.websocket_continue(state) 310 + ewe.User(Nil) -> ewe.websocket_continue(state) 311 + } 306 312 }