basic buzzer working with websocket comms
This commit is contained in:
@@ -23,6 +23,9 @@ mist = ">= 4.0.7 and < 5.0.0"
|
||||
argv = ">= 1.0.2 and < 2.0.0"
|
||||
gleam_erlang = ">= 0.34.0 and < 1.0.0"
|
||||
gleam_http = ">= 4.0.0 and < 5.0.0"
|
||||
gleam_otp = ">= 0.16.1 and < 1.0.0"
|
||||
gleam_crypto = ">= 1.5.0 and < 2.0.0"
|
||||
gleam_json = ">= 2.3.0 and < 3.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
gleeunit = ">= 1.0.0 and < 2.0.0"
|
||||
|
||||
@@ -34,8 +34,11 @@ packages = [
|
||||
|
||||
[requirements]
|
||||
argv = { version = ">= 1.0.2 and < 2.0.0" }
|
||||
gleam_crypto = { version = ">= 1.5.0 and < 2.0.0" }
|
||||
gleam_erlang = { version = ">= 0.34.0 and < 1.0.0" }
|
||||
gleam_http = { version = ">= 4.0.0 and < 5.0.0" }
|
||||
gleam_json = { version = ">= 2.3.0 and < 3.0.0" }
|
||||
gleam_otp = { version = ">= 0.16.1 and < 1.0.0" }
|
||||
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
|
||||
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
|
||||
glint = { version = ">= 1.2.1 and < 2.0.0" }
|
||||
|
||||
+130
-46
@@ -1,67 +1,47 @@
|
||||
import app/web
|
||||
import gleam/bit_array
|
||||
import gleam/bytes_tree
|
||||
import gleam/crypto
|
||||
import gleam/dynamic/decode
|
||||
import gleam/erlang/process
|
||||
import gleam/float
|
||||
import gleam/http/request.{type Request}
|
||||
import gleam/http/response.{type Response}
|
||||
import gleam/option.{None}
|
||||
import gleam/io
|
||||
import gleam/json
|
||||
import gleam/list
|
||||
import gleam/option.{None, Some}
|
||||
import gleam/otp/actor
|
||||
import gleam/result
|
||||
import gleam/string
|
||||
import mist.{type Connection, type ResponseData}
|
||||
|
||||
pub fn handle_request(
|
||||
request: Request(Connection),
|
||||
req: Request(Connection),
|
||||
ctx: web.Context,
|
||||
) -> Response(ResponseData) {
|
||||
case request.path_segments(request) {
|
||||
[] -> serve_frontend()
|
||||
["static", "frontend.wasm"] -> serve_wasm(ctx)
|
||||
io.println("Got request: " <> req.path)
|
||||
case request.path_segments(req) {
|
||||
[] -> serve_wasm(ctx, "index.html", "text/html")
|
||||
["static", "frontend.js"] ->
|
||||
serve_wasm(ctx, "frontend.js", "text/javascript")
|
||||
["static", "frontend_bg.wasm"] ->
|
||||
serve_wasm(ctx, "frontend_bg.wasm", "application/wasm")
|
||||
["ws"] -> serve_websocket(ctx, req)
|
||||
_ -> response.set_body(response.new(404), mist.Bytes(bytes_tree.new()))
|
||||
}
|
||||
}
|
||||
|
||||
const game_body = "
|
||||
<html lang=\"en\">
|
||||
|
||||
<head>
|
||||
<meta charset=\"utf-8\">
|
||||
<title>TITLE</title>
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
canvas {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
background: black;
|
||||
z-index: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<canvas id=\"glcanvas\" tabindex='1'></canvas>
|
||||
<!-- Minified and statically hosted version of https://github.com/not-fl3/macroquad/blob/master/js/mq_js_bundle.js -->
|
||||
<script src=\"https://not-fl3.github.io/miniquad-samples/mq_js_bundle.js\"></script>
|
||||
<script>load(\"static/frontend.wasm\");</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
"
|
||||
|
||||
fn serve_frontend() -> Response(ResponseData) {
|
||||
response.new(200)
|
||||
|> response.set_body(mist.Bytes(bytes_tree.from_string(game_body)))
|
||||
|> response.set_header("content-type", "text/html")
|
||||
}
|
||||
|
||||
fn serve_wasm(ctx: web.Context) -> Response(ResponseData) {
|
||||
let wasm_path = string.join([ctx.config.static_path, "frontend.wasm"], "/")
|
||||
fn serve_wasm(
|
||||
ctx: web.Context,
|
||||
file: String,
|
||||
mimetype: String,
|
||||
) -> Response(ResponseData) {
|
||||
let wasm_path = string.join([ctx.config.static_path, file], "/")
|
||||
mist.send_file(wasm_path, offset: 0, limit: None)
|
||||
|> result.map(fn(file) {
|
||||
response.new(200)
|
||||
|> response.prepend_header("content-type", "application/wasm")
|
||||
|> response.prepend_header("content-type", mimetype)
|
||||
|> response.set_body(file)
|
||||
})
|
||||
|> result.lazy_unwrap(fn() {
|
||||
@@ -69,3 +49,107 @@ fn serve_wasm(ctx: web.Context) -> Response(ResponseData) {
|
||||
|> response.set_body(mist.Bytes(bytes_tree.new()))
|
||||
})
|
||||
}
|
||||
|
||||
fn serve_websocket(
|
||||
ctx: web.Context,
|
||||
req: Request(Connection),
|
||||
) -> Response(ResponseData) {
|
||||
mist.websocket(
|
||||
request: req,
|
||||
on_init: fn(_conn) { #(ctx, Some(ctx.selector)) },
|
||||
on_close: fn(_state) { io.println("goodbye!") },
|
||||
handler: handle_ws_message,
|
||||
)
|
||||
}
|
||||
|
||||
fn decode_error_to_string(decode_err: decode.DecodeError) -> String {
|
||||
"Path: "
|
||||
<> string.join(decode_err.path, ".")
|
||||
<> " Expected: "
|
||||
<> decode_err.expected
|
||||
<> " Got: "
|
||||
<> decode_err.found
|
||||
}
|
||||
|
||||
fn decode_errors_to_string(decode_errs: List(decode.DecodeError)) -> String {
|
||||
string.join(list.map(decode_errs, decode_error_to_string), "\n")
|
||||
}
|
||||
|
||||
fn handle_ws_message(state, conn, message) {
|
||||
io.println("Got WS message")
|
||||
case message {
|
||||
mist.Text("ping") -> {
|
||||
io.println("Got Ping")
|
||||
let assert Ok(_) = mist.send_text_frame(conn, "pong")
|
||||
actor.continue(state)
|
||||
}
|
||||
mist.Text(msg) -> {
|
||||
echo msg
|
||||
case json.parse(msg, web.client_request_decoder()) {
|
||||
Error(err) -> {
|
||||
let err_msg = case err {
|
||||
json.UnableToDecode(decode_err) ->
|
||||
"Decode Error: " <> decode_errors_to_string(decode_err)
|
||||
json.UnexpectedByte(bad_byte) -> "Unexpeted byte: " <> bad_byte
|
||||
json.UnexpectedEndOfInput -> "Reached EoF"
|
||||
json.UnexpectedFormat(_) -> "Unexpected format"
|
||||
json.UnexpectedSequence(seq_msg) ->
|
||||
"Unexpected Sequence: " <> seq_msg
|
||||
}
|
||||
io.println("Could not parse client message: " <> err_msg)
|
||||
}
|
||||
Ok(req) -> {
|
||||
let resp = handle_client_msg(state, req)
|
||||
echo resp
|
||||
let resp_str =
|
||||
web.encode_game_response(resp)
|
||||
|> json.to_string
|
||||
|
||||
let assert Ok(_) = mist.send_text_frame(conn, resp_str)
|
||||
Nil
|
||||
}
|
||||
}
|
||||
|
||||
actor.continue(state)
|
||||
}
|
||||
mist.Binary(_) -> {
|
||||
actor.continue(state)
|
||||
}
|
||||
mist.Custom(_) -> {
|
||||
actor.continue(state)
|
||||
}
|
||||
mist.Closed | mist.Shutdown -> actor.Stop(process.Normal)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_client_msg(
|
||||
_ctx: web.Context,
|
||||
req: web.ClientRequest,
|
||||
) -> web.GameResponse {
|
||||
case req.msg {
|
||||
web.BuzzIn(time) -> {
|
||||
io.println("Got buzz in @ " <> float.to_string(time))
|
||||
web.AckBuzzer
|
||||
}
|
||||
web.Register(username, gamecode) -> {
|
||||
let secret = crypto.strong_random_bytes(16)
|
||||
let token =
|
||||
crypto.new_hasher(crypto.Sha512)
|
||||
|> crypto.hash_chunk(secret)
|
||||
|> crypto.hash_chunk(bit_array.from_string(username))
|
||||
|> crypto.hash_chunk(bit_array.from_string(gamecode))
|
||||
|> crypto.digest
|
||||
|
||||
let token_hash =
|
||||
crypto.new_hasher(crypto.Sha512)
|
||||
|> crypto.hash_chunk(token)
|
||||
|> crypto.digest
|
||||
|
||||
io.println("New token: " <> bit_array.base64_encode(token_hash, True))
|
||||
|
||||
let token = bit_array.base64_encode(token, True)
|
||||
|
||||
web.JoinResponse(token, username)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,70 @@
|
||||
import config
|
||||
import gleam/dynamic/decode
|
||||
import gleam/erlang/process
|
||||
import gleam/json
|
||||
import session
|
||||
import storail
|
||||
|
||||
pub type ClientMessage {
|
||||
BuzzIn(time: Float)
|
||||
Register(game_code: String, username: String)
|
||||
}
|
||||
|
||||
fn client_message_decoder() -> decode.Decoder(ClientMessage) {
|
||||
use variant <- decode.field("type", decode.string)
|
||||
case variant {
|
||||
"BuzzIn" -> {
|
||||
use time <- decode.field("time", decode.float)
|
||||
decode.success(BuzzIn(time:))
|
||||
}
|
||||
"Register" -> {
|
||||
use game_code <- decode.field("game_code", decode.string)
|
||||
use username <- decode.field("username", decode.string)
|
||||
decode.success(Register(game_code:, username:))
|
||||
}
|
||||
_ -> decode.failure(BuzzIn(time: 0.0), "ClientMessage")
|
||||
}
|
||||
}
|
||||
|
||||
pub type ClientRequest {
|
||||
ClientRequest(token: String, msg: ClientMessage)
|
||||
}
|
||||
|
||||
pub fn client_request_decoder() -> decode.Decoder(ClientRequest) {
|
||||
use token <- decode.field("token", decode.string)
|
||||
use msg <- decode.field("msg", client_message_decoder())
|
||||
decode.success(ClientRequest(token:, msg:))
|
||||
}
|
||||
|
||||
pub type GameResponse {
|
||||
JoinResponse(username: String, token: String)
|
||||
AckBuzzer
|
||||
ResetBuzzer
|
||||
UpdatePointTotal(score: Int)
|
||||
}
|
||||
|
||||
pub fn encode_game_response(game_response: GameResponse) -> json.Json {
|
||||
case game_response {
|
||||
JoinResponse(..) ->
|
||||
json.object([
|
||||
#("type", json.string("JoinResponse")),
|
||||
#("username", json.string(game_response.username)),
|
||||
#("token", json.string(game_response.token)),
|
||||
])
|
||||
AckBuzzer -> json.object([#("type", json.string("AckBuzzer"))])
|
||||
ResetBuzzer -> json.object([#("type", json.string("ResetBuzzer"))])
|
||||
UpdatePointTotal(..) ->
|
||||
json.object([
|
||||
#("type", json.string("UpdatePointTotal")),
|
||||
#("score", json.int(game_response.score)),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
pub type Context {
|
||||
Context(config: config.Config, sessions: storail.Collection(session.Session))
|
||||
Context(
|
||||
config: config.Config,
|
||||
sessions: storail.Collection(session.Session),
|
||||
selector: process.Selector(ClientRequest),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,7 +49,11 @@ pub fn run_server() -> glint.Command(Result(Nil, Error)) {
|
||||
let storail_cfg = storail.Config(cfg.storage_path)
|
||||
|
||||
let ctx =
|
||||
web.Context(config: cfg, sessions: setup_session_collection(storail_cfg))
|
||||
web.Context(
|
||||
config: cfg,
|
||||
sessions: setup_session_collection(storail_cfg),
|
||||
selector: process.new_selector(),
|
||||
)
|
||||
|
||||
let handler = router.handle_request(_, ctx)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user