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.

/connection now upgrades to a websocket


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

+198 -115
+1
server/gleam.toml
··· 32 32 parrot = ">= 2.2.1 and < 3.0.0" 33 33 sqlight = ">= 1.0.3 and < 2.0.0" 34 34 simplifile = ">= 2.4.0 and < 3.0.0" 35 + booklet = ">= 1.1.0 and < 2.0.0" 35 36 36 37 [dev_dependencies] 37 38 gleeunit = ">= 1.0.0 and < 2.0.0"
+2
server/manifest.toml
··· 4 4 packages = [ 5 5 { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, 6 6 { name = "backoff", version = "1.1.6", build_tools = ["rebar3"], requirements = [], otp_app = "backoff", source = "hex", outer_checksum = "CF0CFFF8995FB20562F822E5CC47D8CCF664C5ECDC26A684CBE85C225F9D7C39" }, 7 + { name = "booklet", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "booklet", source = "hex", outer_checksum = "08E0FDB78DC4D8A5D3C80295B021505C7D2A2E7B6C6D5EAB7286C36F4A53C851" }, 7 8 { name = "collie", version = "1.0.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "logging", "websocks"], otp_app = "collie", source = "hex", outer_checksum = "5796F28859B0CF0AF7E7B409775C291995B432B48BA2E2CDCA0D4B018B8162B5" }, 8 9 { name = "compresso", version = "0.1.0", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_stdlib", "gleam_yielder", "logging"], otp_app = "compresso", source = "hex", outer_checksum = "8BE29A1EDA42F70826ED148EAE40C46BB3FC18E78FE472663DB01DD4A38172D4" }, 9 10 { name = "directories", version = "1.2.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "D13090CFCDF6759B87217E8DDD73A75903A700148A82C1D33799F333E249BF9E" }, ··· 48 49 ] 49 50 50 51 [requirements] 52 + booklet = { version = ">= 1.1.0 and < 2.0.0" } 51 53 collie = { version = ">= 1.0.0 and < 2.0.0" } 52 54 envoy = { version = ">= 1.1.0 and < 2.0.0" } 53 55 ewe = { version = ">= 3.0.7 and < 4.0.0" }
+195 -115
server/src/lumina_server.gleam
··· 1 + //// Lumina > Server 2 + //// Main entry point for Lumina. 3 + 4 + // Lumina/Peonies 5 + // Copyright (C) 2018-2026 MLC 'Strawmelonjuice' Bloeiman and contributors. [cite: 4] 6 + // 7 + // This software is licensed under the European Union Public Licence (EUPL) v1.2. 8 + // You may not use this work except in compliance with the Licence. 9 + // You may obtain a copy of the Licence at: https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 10 + // 11 + // AI TRAINING NOTICE: Rights for TDM and AI training are EXPRESSLY RESERVED 12 + // under Art 4(3) Dir 2019/790. AI training constitutes a Derivative Work. 13 + // See LICENSE file in the repository root for full details. 14 + // 15 + // 16 + // This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND. [cite: 5] 17 + // See the Licence for the specific language governing permissions and limitations. [cite: 6] 18 + 19 + 1 20 import envoy 2 21 import ewe.{type Request, type Response} 3 22 import gleam/erlang/application ··· 10 29 import gleam/result 11 30 import gleam/string 12 31 import gleam/uri 32 + import booklet.{type Booklet} 13 33 import simplifile 14 34 import sqlight 15 35 import woof 16 36 17 37 type HandlerContext { 18 38 HandlerContext(db: sqlight.Connection, client_hash: String, assets: String) 39 + } 40 + type ClientConnectionData { 41 + ClientConnectionData( 42 + client_type: option.Option(ClientType), 43 + user: option.Option(User) 44 + ) 45 + } 46 + type ClientType { 47 + WebClient 48 + NativeApp 49 + } 50 + 51 + type User { 52 + User( 53 + // Todo 54 + Nil 55 + ) 19 56 } 20 57 21 58 pub fn main() { ··· 100 137 ) 101 138 } 102 139 case req.path |> uri.path_segments() { 103 - ["/"] | [""] | [] -> { 104 - httplogger(woof.Info, "OK", []) 105 - response.new(200) 106 - |> response.set_header("content-type", "text/html; charset=utf-8") 107 - |> response.set_body(ewe.TextData( 108 - "<!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 = \"" 109 - <> { handler_ctx.client_hash } 110 - <> "\";</script><script type=\"module\" src=\"/static/lumina.min.mjs\"></script></head><body id=\"app\"></body></html>", 111 - )) 112 - } 113 - ["static", "lumina.min.mjs"] -> { 114 - let file = handler_ctx.assets <> "/static/lumina_client.min.mjs" 115 - case ewe.file(file, None, None) { 116 - Error(_) -> { 117 - httplogger(woof.Error, "Missing application assets.", []) 118 - response.new(500) 119 - |> response.set_header("content-type", "text/plain; charset=utf-8") 120 - |> response.set_body(ewe.TextData("500 Internal Server Error")) 121 - } 122 - Ok(outcome) -> { 123 - httplogger(woof.Info, "OK", []) 124 - response.new(200) 125 - |> response.set_header( 126 - "content-type", 127 - "application/javascript; charset=utf-8", 128 - ) 129 - |> response.set_body(outcome) 130 - } 131 - } 132 - } 133 - ["static", "lumina.mjs"] -> { 134 - let file = handler_ctx.assets <> "/static/lumina_client.mjs" 135 - case ewe.file(file, None, None) { 136 - Error(_) -> { 137 - httplogger(woof.Error, "Missing application assets.", []) 138 - response.new(500) 139 - |> response.set_header("content-type", "text/plain; charset=utf-8") 140 - |> response.set_body(ewe.TextData("500 Internal Server Error")) 141 - } 142 - Ok(outcome) -> { 143 - httplogger(woof.Info, "OK", []) 144 - response.new(200) 145 - |> response.set_header( 146 - "content-type", 147 - "application/javascript; charset=utf-8", 148 - ) 149 - |> response.set_body(outcome) 150 - } 151 - } 152 - } 153 - ["static", "lumina.css"] -> { 154 - let file = handler_ctx.assets <> "/static/lumina_client.css" 155 - case ewe.file(file, None, None) { 156 - Error(_) -> { 157 - httplogger(woof.Error, "Missing application assets.", []) 158 - response.new(500) 159 - |> response.set_header("content-type", "text/plain; charset=utf-8") 160 - |> response.set_body(ewe.TextData("500 Internal Server Error")) 161 - } 162 - Ok(outcome) -> { 163 - httplogger(woof.Info, "OK", []) 164 - response.new(200) 165 - |> response.set_header("content-type", "text/css; charset=utf-8") 166 - |> response.set_body(outcome) 167 - } 168 - } 169 - } 170 - 171 - ["favicon.ico"] | ["static", "logo.png"] -> { 172 - let file = handler_ctx.assets <> "/static/logo.png" 173 - case ewe.file(file, None, None) { 174 - Error(_) -> { 175 - httplogger(woof.Error, "Missing application assets.", []) 176 - response.new(500) 177 - |> response.set_header("content-type", "text/plain; charset=utf-8") 178 - |> response.set_body(ewe.TextData("500 Internal Server Error")) 179 - } 180 - Ok(outcome) -> { 181 - httplogger(woof.Info, "OK", []) 182 - response.new(200) 183 - |> response.set_header("content-type", "image/png;") 184 - |> response.set_body(outcome) 185 - } 186 - } 187 - } 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 + } 188 207 189 - ["static", staticfile] -> { 190 - let file = handler_ctx.assets <> "/static/" <> staticfile 191 - case ewe.file(file, None, None) { 192 - Error(_) -> { 193 - httplogger(woof.Warning, "Not found.", [woof.field("file", file)]) 194 - response.new(404) 195 - |> response.set_header("content-type", "text/plain; charset=utf-8") 196 - |> response.set_body(ewe.TextData("404! Not found!")) 197 - } 198 - Ok(outcome) -> { 199 - httplogger(woof.Info, "OK", [woof.field("file", file)]) 200 - response.new(200) 201 - |> case 202 - { 203 - staticfile 204 - |> string.split(".") 205 - |> list.last() 206 - |> result.unwrap("") 207 - } 208 - { 209 - "png" -> response.set_header(_, "content-type", "image/png;") 210 - "html" -> response.set_header(_, "content-type","text/html; charset=utf-8") 211 - "svg" -> response.set_header(_, "content-type", "image/svg+xml") 212 - "ttf" -> response.set_header(_, "content-type", "font/ttf") 213 - _ -> function.identity 214 - } 215 - |> response.set_body(outcome) 216 - } 217 - } 218 - } 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 + } 219 276 _ -> { 220 277 httplogger(woof.Warning, "Not found.", []) 221 278 response.new(404) ··· 224 281 } 225 282 } 226 283 } 284 + 285 + type WebsocketState { 286 + WebsocketState( 287 + ctx: HandlerContext, 288 + conn_data: ClientConnectionData, 289 + ) 290 + } 291 + 292 + 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 + } 306 + }