handle cross socket message passing

This commit is contained in:
Joey Hines 2025-05-18 16:32:01 -06:00
parent b6a4936247
commit af0fd811b3
Signed by: joeyahines
GPG Key ID: 38BA6F25C94C9382
10 changed files with 240 additions and 63 deletions

View File

@ -24,7 +24,7 @@ packages = [
{ name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" },
{ name = "mist", version = "4.0.7", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "F7D15A1E3232E124C7CE31900253633434E59B34ED0E99F273DEE61CDB573CDD" }, { name = "mist", version = "4.0.7", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "F7D15A1E3232E124C7CE31900253633434E59B34ED0E99F273DEE61CDB573CDD" },
{ name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" },
{ name = "shared", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], source = "local", path = "../shared" }, { name = "shared", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_json", "gleam_stdlib"], source = "local", path = "../shared" },
{ name = "simplifile", version = "2.2.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C88E0EE2D509F6D86EB55161D631657675AA7684DAB83822F7E59EB93D9A60E3" }, { name = "simplifile", version = "2.2.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C88E0EE2D509F6D86EB55161D631657675AA7684DAB83822F7E59EB93D9A60E3" },
{ name = "snag", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "7E9F06390040EB5FAB392CE642771484136F2EC103A92AE11BA898C8167E6E17" }, { name = "snag", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "7E9F06390040EB5FAB392CE642771484136F2EC103A92AE11BA898C8167E6E17" },
{ name = "storail", version = "3.0.0", build_tools = ["gleam"], requirements = ["directories", "filepath", "gleam_crypto", "gleam_json", "gleam_stdlib", "simplifile"], otp_app = "storail", source = "hex", outer_checksum = "D032EE5C89AA4B6306FF81929BF9B5DD2583E9F743F0047788AE5F1F52AFE3B4" }, { name = "storail", version = "3.0.0", build_tools = ["gleam"], requirements = ["directories", "filepath", "gleam_crypto", "gleam_json", "gleam_stdlib", "simplifile"], otp_app = "storail", source = "hex", outer_checksum = "D032EE5C89AA4B6306FF81929BF9B5DD2583E9F743F0047788AE5F1F52AFE3B4" },

View File

@ -3,7 +3,7 @@ import gleam/bit_array
import gleam/bytes_tree import gleam/bytes_tree
import gleam/crypto import gleam/crypto
import gleam/dynamic/decode import gleam/dynamic/decode
import gleam/erlang/process import gleam/erlang/process.{type Subject}
import gleam/float import gleam/float
import gleam/http/request.{type Request} import gleam/http/request.{type Request}
import gleam/http/response.{type Response} import gleam/http/response.{type Response}
@ -11,7 +11,7 @@ import gleam/int
import gleam/io import gleam/io
import gleam/json import gleam/json
import gleam/list import gleam/list
import gleam/option.{None, Some} import gleam/option.{type Option, None, Some}
import gleam/otp/actor import gleam/otp/actor
import gleam/result import gleam/result
import gleam/string import gleam/string
@ -19,6 +19,7 @@ import mist.{type Connection, type ResponseData}
import player import player
import player_session import player_session
import session import session
import socket_manager
import storail import storail
pub fn handle_request( pub fn handle_request(
@ -55,14 +56,39 @@ fn serve_wasm(
}) })
} }
pub type SocketState {
SocketState(
ctx: web.Context,
id: Int,
subject: Subject(web.GameResponse),
user_id: Option(Int),
game_id: Option(String),
)
}
fn serve_websocket( fn serve_websocket(
ctx: web.Context, ctx: web.Context,
req: Request(Connection), req: Request(Connection),
) -> Response(ResponseData) { ) -> Response(ResponseData) {
mist.websocket( mist.websocket(
request: req, request: req,
on_init: fn(_conn) { #(ctx, Some(ctx.selector)) }, on_init: fn(_conn) {
on_close: fn(_state) { io.println("goodbye!") }, let self = process.new_subject()
let selector =
process.new_selector()
|> process.selecting(self, fn(x) { x })
let id = socket_manager.add_socket_subj(ctx.socket_manager, self)
let state =
SocketState(ctx:, id:, subject: self, user_id: None, game_id: None)
#(state, Some(selector))
},
on_close: fn(state) {
io.println("Shutting down socket" <> int.to_string(state.id))
socket_manager.remove_socket_subj(state.ctx.socket_manager, state.subject)
},
handler: handle_ws_message, handler: handle_ws_message,
) )
} }
@ -80,7 +106,11 @@ fn decode_errors_to_string(decode_errs: List(decode.DecodeError)) -> String {
string.join(list.map(decode_errs, decode_error_to_string), "\n") string.join(list.map(decode_errs, decode_error_to_string), "\n")
} }
fn handle_ws_message(state, conn, message) { fn handle_ws_message(
state: SocketState,
conn,
message: mist.WebsocketMessage(web.GameResponse),
) {
io.println("Got WS message") io.println("Got WS message")
case message { case message {
mist.Text("ping") -> { mist.Text("ping") -> {
@ -90,7 +120,7 @@ fn handle_ws_message(state, conn, message) {
} }
mist.Text(msg) -> { mist.Text(msg) -> {
echo msg echo msg
case json.parse(msg, web.client_request_decoder()) { let state = case json.parse(msg, web.client_request_decoder()) {
Error(err) -> { Error(err) -> {
let err_msg = case err { let err_msg = case err {
json.UnableToDecode(decode_err) -> json.UnableToDecode(decode_err) ->
@ -102,16 +132,17 @@ fn handle_ws_message(state, conn, message) {
"Unexpected Sequence: " <> seq_msg "Unexpected Sequence: " <> seq_msg
} }
io.println("Could not parse client message: " <> err_msg) io.println("Could not parse client message: " <> err_msg)
state
} }
Ok(req) -> { Ok(req) -> {
let resp = handle_client_msg(state, req) let #(state, resp) = handle_client_msg(state, req)
echo resp echo resp
let resp_str = let resp_str =
web.encode_game_response(resp) web.encode_game_response(resp)
|> json.to_string |> json.to_string
let assert Ok(_) = mist.send_text_frame(conn, resp_str) let assert Ok(_) = mist.send_text_frame(conn, resp_str)
Nil state
} }
} }
@ -120,7 +151,12 @@ fn handle_ws_message(state, conn, message) {
mist.Binary(_) -> { mist.Binary(_) -> {
actor.continue(state) actor.continue(state)
} }
mist.Custom(_) -> { mist.Custom(resp) -> {
echo resp
let resp_str =
web.encode_game_response(resp)
|> json.to_string
let assert Ok(_) = mist.send_text_frame(conn, resp_str)
actor.continue(state) actor.continue(state)
} }
mist.Closed | mist.Shutdown -> actor.Stop(process.Normal) mist.Closed | mist.Shutdown -> actor.Stop(process.Normal)
@ -128,7 +164,7 @@ fn handle_ws_message(state, conn, message) {
} }
fn register_user( fn register_user(
ctx: web.Context, state: SocketState,
username: String, username: String,
gamecode: String, gamecode: String,
) -> #(String, Int) { ) -> #(String, Int) {
@ -139,40 +175,40 @@ fn register_user(
|> crypto.hash_chunk(bit_array.from_string(username)) |> crypto.hash_chunk(bit_array.from_string(username))
|> crypto.hash_chunk(bit_array.from_string(gamecode)) |> crypto.hash_chunk(bit_array.from_string(gamecode))
|> crypto.digest |> crypto.digest
let token_hash =
crypto.new_hasher(crypto.Sha512)
|> crypto.hash_chunk(token)
|> crypto.digest
|> bit_array.base64_encode(True) |> bit_array.base64_encode(True)
let token_hash = player_session.hash_token(token)
let assert <<user_id:int-size(64)>> = crypto.strong_random_bytes(8) let assert <<user_id:int-size(64)>> = crypto.strong_random_bytes(8)
let key = storail.key(ctx.player_sessions, int.to_base16(user_id)) let key = storail.key(state.ctx.player_sessions, int.to_base16(user_id))
let assert Ok(Nil) = let assert Ok(Nil) =
storail.write( storail.write(
key, key,
player_session.PlayerSession(id: user_id, token_hash:, username:), player_session.PlayerSession(
id: user_id,
token_hash:,
username:,
socket_id: state.id,
),
) )
let token = bit_array.base64_encode(token, True)
#(token, user_id) #(token, user_id)
} }
fn handle_client_msg( fn handle_client_msg(
ctx: web.Context, state: SocketState,
req: web.ClientRequest, req: web.ClientRequest,
) -> web.GameResponse { ) -> #(SocketState, web.GameResponse) {
case req.msg { case req.msg {
web.BuzzIn(time) -> { web.BuzzIn(time) -> {
io.println("Got buzz in @ " <> float.to_string(time)) io.println("Got buzz in @ " <> float.to_string(time))
web.AckBuzzer #(state, web.AckBuzzer)
} }
web.Register(gamecode, username) -> { web.Register(gamecode, username) -> {
let #(token, user_id) = register_user(ctx, username, gamecode) let #(token, user_id) = register_user(state, username, gamecode)
let key = storail.key(ctx.sessions, gamecode) let key = storail.key(state.ctx.sessions, gamecode)
let assert Ok(session) = storail.read(key) let assert Ok(session) = storail.read(key)
let player = player.Player(name: username, id: user_id, score: 0) let player = player.Player(name: username, id: user_id, score: 0)
@ -184,16 +220,54 @@ fn handle_client_msg(
let assert Ok(Nil) = storail.write(key, session) let assert Ok(Nil) = storail.write(key, session)
web.JoinResponse(username, token) #(
SocketState(..state, user_id: Some(user_id), game_id: Some(gamecode)),
web.JoinResponse(username, token),
)
} }
web.CreateRoom(gamecode, username) -> { web.CreateRoom(gamecode, username) -> {
let #(token, user_id) = register_user(ctx, username, gamecode) let #(token, user_id) = register_user(state, username, gamecode)
let key = storail.key(ctx.sessions, gamecode) let key = storail.key(state.ctx.sessions, gamecode)
let session = session.Session(gamecode, user_id, []) let session = session.Session(gamecode, user_id, [])
let assert Ok(Nil) = storail.write(key, session) let assert Ok(Nil) = storail.write(key, session)
web.HostResponse(username, token) #(
SocketState(..state, user_id: Some(user_id), game_id: Some(gamecode)),
web.HostResponse(username, token),
)
}
web.ResetBuzzers -> {
let _token_hash = player_session.hash_token(req.token)
let assert Some(user_id) = state.user_id
let assert Some(session_id) = state.game_id
let player_key =
storail.key(state.ctx.player_sessions, int.to_base16(user_id))
let assert Ok(_user) = storail.read(player_key)
let session_key = storail.key(state.ctx.sessions, session_id)
let assert Ok(session) = storail.read(session_key)
list.map(session.players, fn(player) { player.id })
|> list.map(fn(player_id) {
let player_key =
storail.key(state.ctx.player_sessions, int.to_base16(player_id))
let assert Ok(player) = storail.read(player_key)
player
})
|> list.map(fn(player) {
let assert Ok(subject) =
socket_manager.get_socket_subj_by_id(
state.ctx.socket_manager,
player.socket_id,
)
subject
})
|> list.each(fn(subject) { actor.send(subject, web.ResetBuzzer) })
#(state, web.AckBuzzer)
} }
} }
} }

View File

@ -1,39 +1,18 @@
import config import config
import gleam/dynamic/decode import gleam/dynamic/decode
import gleam/erlang/process
import gleam/json import gleam/json
import player_session import player_session
import session import session
import socket_manager
import storail import storail
pub type ClientMessage { pub type ClientMessage {
BuzzIn(time: Float) BuzzIn(time: Float)
ResetBuzzers
Register(game_code: String, username: String) Register(game_code: String, username: String)
CreateRoom(game_code: String, username: String) CreateRoom(game_code: String, username: String)
} }
pub fn encode_client_message(client_message: ClientMessage) -> json.Json {
case client_message {
BuzzIn(..) ->
json.object([
#("type", json.string("buzz_in")),
#("time", json.float(client_message.time)),
])
Register(..) ->
json.object([
#("type", json.string("register")),
#("game_code", json.string(client_message.game_code)),
#("username", json.string(client_message.username)),
])
CreateRoom(..) ->
json.object([
#("type", json.string("create_room")),
#("game_code", json.string(client_message.game_code)),
#("username", json.string(client_message.username)),
])
}
}
pub fn client_message_decoder() -> decode.Decoder(ClientMessage) { pub fn client_message_decoder() -> decode.Decoder(ClientMessage) {
use variant <- decode.field("type", decode.string) use variant <- decode.field("type", decode.string)
case variant { case variant {
@ -51,6 +30,9 @@ pub fn client_message_decoder() -> decode.Decoder(ClientMessage) {
use username <- decode.field("username", decode.string) use username <- decode.field("username", decode.string)
decode.success(CreateRoom(game_code:, username:)) decode.success(CreateRoom(game_code:, username:))
} }
"ResetBuzzers" -> {
decode.success(ResetBuzzers)
}
_ -> decode.failure(BuzzIn(0.0), "ClientMessage") _ -> decode.failure(BuzzIn(0.0), "ClientMessage")
} }
} }
@ -102,6 +84,6 @@ pub type Context {
config: config.Config, config: config.Config,
sessions: storail.Collection(session.Session), sessions: storail.Collection(session.Session),
player_sessions: storail.Collection(player_session.PlayerSession), player_sessions: storail.Collection(player_session.PlayerSession),
selector: process.Selector(ClientRequest), socket_manager: socket_manager.SocketManager(GameResponse),
) )
} }

View File

@ -8,6 +8,7 @@ import glint
import mist import mist
import player_session import player_session
import session import session
import socket_manager
import storail import storail
pub type Error { pub type Error {
@ -65,7 +66,7 @@ pub fn run_server() -> glint.Command(Result(Nil, Error)) {
config: cfg, config: cfg,
sessions: setup_session_collection(storail_cfg), sessions: setup_session_collection(storail_cfg),
player_sessions: setup_player_session_collection(storail_cfg), player_sessions: setup_player_session_collection(storail_cfg),
selector: process.new_selector(), socket_manager: socket_manager.new(),
) )
let handler = router.handle_request(_, ctx) let handler = router.handle_request(_, ctx)

View File

@ -0,0 +1,89 @@
import gleam/dict.{type Dict}
import gleam/erlang/process.{type Subject}
import gleam/list
import gleam/otp/actor
pub type SocketManager(a) {
SocketManager(subject: Subject(SocketManagerMsg(a)))
}
type State(a) {
State(subjects: Dict(Int, Subject(a)), next_id: Int)
}
pub type SocketManagerMsg(a) {
AddSocketSubject(subject: Subject(a), client: Subject(Int))
RemoveSocketSubject(subject: Subject(a))
GetSubjectById(id: Int, client: Subject(Result(Subject(a), Nil)))
Shutdown
}
fn handle(
msg: SocketManagerMsg(a),
state: State(a),
) -> actor.Next(SocketManagerMsg(a), State(a)) {
case msg {
AddSocketSubject(subject, client) -> {
let subject_id = state.next_id
actor.send(client, subject_id)
actor.continue(State(
dict.insert(state.subjects, state.next_id, subject),
state.next_id + 1,
))
}
RemoveSocketSubject(subject) -> {
let remove_id =
list.first(
dict.keys(
dict.filter(state.subjects, fn(_key, value) { value == subject }),
),
)
case remove_id {
Ok(id) -> {
actor.continue(
State(..state, subjects: dict.delete(state.subjects, id)),
)
}
Error(_) -> {
actor.continue(state)
}
}
}
GetSubjectById(id, client) -> {
actor.send(client, dict.get(state.subjects, id))
actor.continue(state)
}
Shutdown -> {
actor.Stop(process.Normal)
}
}
}
pub fn new() -> SocketManager(a) {
let assert Ok(subject) = actor.start(State(dict.new(), 0), handle)
SocketManager(subject)
}
pub fn add_socket_subj(
socket_manager: SocketManager(a),
subject: Subject(a),
) -> Int {
actor.call(socket_manager.subject, AddSocketSubject(subject, _), 1000)
}
pub fn get_socket_subj_by_id(
socket_manager: SocketManager(a),
id: Int,
) -> Result(Subject(a), Nil) {
actor.call(socket_manager.subject, GetSubjectById(id, _), 1000)
}
pub fn remove_socket_subj(socket_manager: SocketManager(a), subject: Subject(a)) {
actor.send(socket_manager.subject, RemoveSocketSubject(subject))
}
pub fn shutdown(socket_manager: SocketManager(a)) {
actor.send(socket_manager.subject, Shutdown)
}

View File

@ -1,16 +1,12 @@
use macroquad::{ use macroquad::prelude::*;
color::{BLACK, WHITE},
text::draw_text,
window::{clear_background, screen_height, screen_width},
};
use crate::screen::Screen; use crate::{model::ClientMessage, screen::Screen};
#[derive(Default)] #[derive(Default)]
pub struct HostScreen {} pub struct HostScreen {}
impl Screen for HostScreen { impl Screen for HostScreen {
fn handle_frame(&mut self, _ctx: &mut crate::context::Context) { fn handle_frame(&mut self, ctx: &mut crate::context::Context) {
clear_background(WHITE); clear_background(WHITE);
draw_text( draw_text(
@ -20,6 +16,25 @@ impl Screen for HostScreen {
100.0, 100.0,
BLACK, BLACK,
); );
let button_size = screen_width() * 0.01;
let button_location = Vec2::new(screen_width() * 0.50, screen_height() * 0.70);
draw_circle(
button_location.x,
button_location.y,
button_size * 1.1,
DARKBROWN,
);
draw_circle(button_location.x, button_location.y, button_size, RED);
if is_mouse_button_pressed(MouseButton::Left) {
let loc = Vec2::from(mouse_position());
let normalize = loc - button_location;
if normalize.length() < button_size {
ctx.send_msg(&ClientMessage::ResetBuzzers);
}
}
} }
fn handle_messages( fn handle_messages(

View File

@ -11,6 +11,7 @@ pub enum ClientMessage {
BuzzIn(BuzzIn), BuzzIn(BuzzIn),
Register(Register), Register(Register),
CreateRoom(Register), CreateRoom(Register),
ResetBuzzers,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]

View File

@ -15,6 +15,7 @@ version = "1.0.0"
[dependencies] [dependencies]
gleam_stdlib = ">= 0.44.0 and < 2.0.0" gleam_stdlib = ">= 0.44.0 and < 2.0.0"
gleam_json = ">= 2.3.0 and < 3.0.0" gleam_json = ">= 2.3.0 and < 3.0.0"
gleam_crypto = ">= 1.5.0 and < 2.0.0"
[dev-dependencies] [dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0" gleeunit = ">= 1.0.0 and < 2.0.0"

View File

@ -2,12 +2,14 @@
# You typically do not need to edit this file # You typically do not need to edit this file
packages = [ packages = [
{ name = "gleam_crypto", version = "1.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "917BC8B87DBD584830E3B389CBCAB140FFE7CB27866D27C6D0FB87A9ECF35602" },
{ name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" }, { name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" },
{ name = "gleam_stdlib", version = "0.59.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F8FEE9B35797301994B81AF75508CF87C328FE1585558B0FFD188DC2B32EAA95" }, { name = "gleam_stdlib", version = "0.59.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F8FEE9B35797301994B81AF75508CF87C328FE1585558B0FFD188DC2B32EAA95" },
{ name = "gleeunit", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "A7DD6C07B7DA49A6E28796058AA89E651D233B357D5607006D70619CD89DAAAB" }, { name = "gleeunit", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "A7DD6C07B7DA49A6E28796058AA89E651D233B357D5607006D70619CD89DAAAB" },
] ]
[requirements] [requirements]
gleam_crypto = { version = ">= 1.5.0 and < 2.0.0" }
gleam_json = { version = ">= 2.3.0 and < 3.0.0" } gleam_json = { version = ">= 2.3.0 and < 3.0.0" }
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" }

View File

@ -1,8 +1,10 @@
import gleam/bit_array
import gleam/crypto
import gleam/dynamic/decode import gleam/dynamic/decode
import gleam/json import gleam/json
pub type PlayerSession { pub type PlayerSession {
PlayerSession(id: Int, token_hash: String, username: String) PlayerSession(id: Int, token_hash: String, username: String, socket_id: Int)
} }
pub fn encode_player_session(player_session: PlayerSession) -> json.Json { pub fn encode_player_session(player_session: PlayerSession) -> json.Json {
@ -10,6 +12,7 @@ pub fn encode_player_session(player_session: PlayerSession) -> json.Json {
#("id", json.int(player_session.id)), #("id", json.int(player_session.id)),
#("token_hash", json.string(player_session.token_hash)), #("token_hash", json.string(player_session.token_hash)),
#("username", json.string(player_session.username)), #("username", json.string(player_session.username)),
#("socket_id", json.int(player_session.socket_id)),
]) ])
} }
@ -17,5 +20,14 @@ pub fn player_session_decoder() -> decode.Decoder(PlayerSession) {
use id <- decode.field("id", decode.int) use id <- decode.field("id", decode.int)
use token_hash <- decode.field("token_hash", decode.string) use token_hash <- decode.field("token_hash", decode.string)
use username <- decode.field("username", decode.string) use username <- decode.field("username", decode.string)
decode.success(PlayerSession(id:, token_hash:, username:)) use socket_id <- decode.field("socket_id", decode.int)
decode.success(PlayerSession(id:, token_hash:, username:, socket_id:))
}
pub fn hash_token(token: String) -> String {
let token =
crypto.new_hasher(crypto.Sha512)
|> crypto.hash_chunk(bit_array.from_string(token))
|> crypto.digest
|> bit_array.base64_encode(True)
} }