mirror of
https://github.com/uqbar-dao/nectar.git
synced 2025-01-03 06:11:01 +03:00
chess work
This commit is contained in:
parent
5a4813b8c4
commit
84a5bbbe6c
@ -62,7 +62,7 @@ snow = { version = "0.9.3", features = ["ring-resolver"] }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.28", features = ["fs", "macros", "rt-multi-thread", "sync"] }
|
||||
tokio-tungstenite = "*"
|
||||
url = "*"
|
||||
url = "2.4.1"
|
||||
uqbar_process_lib = { git = "ssh://git@github.com/uqbar-dao/process_lib.git", rev = "e53c124" }
|
||||
uuid = { version = "1.1.2", features = ["serde", "v4"] }
|
||||
warp = "0.3.5"
|
||||
|
92
modules/chess/Cargo.lock
generated
92
modules/chess/Cargo.lock
generated
@ -50,6 +50,12 @@ version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@ -124,6 +130,21 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-cprng"
|
||||
version = "0.1.1"
|
||||
@ -162,12 +183,33 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.1.0"
|
||||
@ -234,6 +276,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
|
||||
|
||||
[[package]]
|
||||
name = "pleco"
|
||||
version = "0.5.0"
|
||||
@ -532,12 +580,42 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.10.1"
|
||||
@ -553,16 +631,30 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
||||
[[package]]
|
||||
name = "uqbar_process_lib"
|
||||
version = "0.3.0"
|
||||
source = "git+ssh://git@github.com/uqbar-dao/process_lib.git?rev=db26c5b#db26c5b1607ba6532bcc7687bf8902a21ebd3393"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
"http",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"url",
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
|
@ -17,7 +17,7 @@ bincode = "1.3.3"
|
||||
pleco = "0.5"
|
||||
serde = {version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
uqbar_process_lib = { path = "../../../process_lib" }
|
||||
uqbar_process_lib = { git = "ssh://git@github.com/uqbar-dao/process_lib.git", rev = "db26c5b" }
|
||||
wit-bindgen = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "5390bab780733f1660d14c254ec985df2816bf1d" }
|
||||
|
||||
[lib]
|
||||
|
@ -1,79 +0,0 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import json
|
||||
import glob
|
||||
import shutil
|
||||
|
||||
def compile_process(process_dir, pkg_dir, root_dir):
|
||||
# Get the path to the source code and the compiled WASM file
|
||||
src_path = os.path.join(process_dir, "src")
|
||||
wasm_path = os.path.join(pkg_dir, os.path.basename(process_dir) + ".wasm")
|
||||
|
||||
# Check if the source code or the Cargo.toml file has been modified since the last compile
|
||||
src_mtime = max(os.path.getmtime(f) for f in glob.glob(os.path.join(src_path, '**'), recursive=True))
|
||||
wasm_mtime = os.path.getmtime(wasm_path) if os.path.exists(wasm_path) else 0
|
||||
|
||||
# Change to the process directory
|
||||
os.chdir(process_dir)
|
||||
|
||||
# Create the target/bindings/$name/ directory
|
||||
bindings_dir = os.path.join(process_dir, "target", "bindings", os.path.basename(process_dir))
|
||||
os.makedirs(bindings_dir, exist_ok=True)
|
||||
|
||||
# Create target.wasm (compiled .wit) & world
|
||||
subprocess.check_call([
|
||||
"wasm-tools", "component", "wit",
|
||||
os.path.join(root_dir, "wit"),
|
||||
"-o", os.path.join(bindings_dir, "target.wasm"),
|
||||
"--wasm"
|
||||
])
|
||||
|
||||
# Copy /wit (world is empty file currently)
|
||||
shutil.copytree(os.path.join(root_dir, "wit"), os.path.join(bindings_dir, "wit"), dirs_exist_ok=True)
|
||||
# shutil.copy(os.path.join(root_dir, "world"), os.path.join(bindings_dir, "world"))
|
||||
|
||||
# Create an empty world file
|
||||
with open(os.path.join(bindings_dir, "world"), 'w') as f:
|
||||
pass
|
||||
|
||||
# Build the module using Cargo
|
||||
subprocess.check_call([
|
||||
"cargo", "+nightly", "build",
|
||||
"--release",
|
||||
"--no-default-features",
|
||||
"--target", "wasm32-wasi"
|
||||
])
|
||||
|
||||
# Adapt the module using wasm-tools
|
||||
wasm_file = os.path.join(process_dir, "target", "wasm32-wasi", "release", os.path.basename(process_dir) + ".wasm")
|
||||
adapted_wasm_file = wasm_file.replace(".wasm", "_adapted.wasm")
|
||||
subprocess.check_call([
|
||||
"wasm-tools", "component", "new",
|
||||
wasm_file,
|
||||
"-o", adapted_wasm_file,
|
||||
"--adapt", os.path.join(root_dir, "wasi_snapshot_preview1.wasm")
|
||||
])
|
||||
|
||||
# Embed "wit" into the component and place it in the expected location
|
||||
subprocess.check_call([
|
||||
"wasm-tools", "component", "embed", os.path.join(root_dir, "wit"),
|
||||
"--world", "process",
|
||||
adapted_wasm_file,
|
||||
"-o", wasm_path
|
||||
])
|
||||
|
||||
if __name__ == "__main__":
|
||||
root_dir = os.getcwd()
|
||||
pkg_dir = os.path.join(root_dir, "pkg")
|
||||
|
||||
# If a specific process is provided, compile it
|
||||
if len(sys.argv) > 1:
|
||||
process_dir = os.path.abspath(os.path.join(root_dir, sys.argv[1]))
|
||||
compile_process(process_dir, pkg_dir, root_dir)
|
||||
else:
|
||||
# Compile each base dir folder that has a Cargo.toml
|
||||
for root, dirs, files in os.walk(root_dir):
|
||||
if 'Cargo.toml' in files and "process_lib" not in root:
|
||||
process_dir = os.path.abspath(root)
|
||||
compile_process(process_dir, pkg_dir, root_dir)
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"package": "chess2",
|
||||
"package": "chess",
|
||||
"publisher": "uqbar",
|
||||
"version": [0, 1, 0]
|
||||
"version": [0, 2, 0]
|
||||
}
|
||||
|
@ -1,15 +1,17 @@
|
||||
#![feature(let_chains)]
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
extern crate base64;
|
||||
extern crate pleco;
|
||||
use pleco::Board;
|
||||
use uqbar_process_lib::uqbar::process::standard as wit;
|
||||
use uqbar_process_lib::{
|
||||
get_payload, get_typed_state, grant_messaging, println, receive, set_state, Address, Message,
|
||||
Payload, ProcessId, Request, Response,
|
||||
get_payload, get_typed_state, grant_messaging, http, println, receive, set_state, Address,
|
||||
Message, Payload, ProcessId, Request, Response,
|
||||
};
|
||||
|
||||
mod utils;
|
||||
|
||||
wit_bindgen::generate!({
|
||||
path: "../../wit",
|
||||
world: "process",
|
||||
@ -21,7 +23,7 @@ wit_bindgen::generate!({
|
||||
struct Component;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Game {
|
||||
pub struct Game {
|
||||
pub id: String, // the node with whom we are playing
|
||||
pub turns: u64,
|
||||
pub board: Board,
|
||||
@ -31,7 +33,7 @@ struct Game {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct StoredGame {
|
||||
pub struct StoredGame {
|
||||
pub id: String, // the node with whom we are playing
|
||||
pub turns: u64,
|
||||
pub board: String,
|
||||
@ -41,132 +43,17 @@ struct StoredGame {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ChessState {
|
||||
pub struct ChessState {
|
||||
pub games: HashMap<String, Game>, // game is by opposing player id
|
||||
pub records: HashMap<String, (u64, u64, u64)>, // wins, losses, draws
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct StoredChessState {
|
||||
pub struct StoredChessState {
|
||||
pub games: HashMap<String, StoredGame>, // game is by opposing player id
|
||||
pub records: HashMap<String, (u64, u64, u64)>, // wins, losses, draws
|
||||
}
|
||||
|
||||
fn convert_game(game: Game) -> StoredGame {
|
||||
StoredGame {
|
||||
id: game.id,
|
||||
turns: game.turns,
|
||||
board: game.board.fen(),
|
||||
white: game.white,
|
||||
black: game.black,
|
||||
ended: game.ended,
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_state(state: ChessState) -> StoredChessState {
|
||||
StoredChessState {
|
||||
games: state
|
||||
.games
|
||||
.iter()
|
||||
.map(|(id, game)| (id.to_string(), convert_game(game.clone())))
|
||||
.collect(),
|
||||
records: state.records.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn json_game(game: &Game) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"id": game.id,
|
||||
"turns": game.turns,
|
||||
"board": game.board.fen(),
|
||||
"white": game.white,
|
||||
"black": game.black,
|
||||
"ended": game.ended,
|
||||
})
|
||||
}
|
||||
|
||||
fn send_http_response(
|
||||
status: u16,
|
||||
headers: HashMap<String, String>,
|
||||
payload_bytes: Vec<u8>,
|
||||
) -> anyhow::Result<()> {
|
||||
Response::new()
|
||||
.ipc(
|
||||
serde_json::json!({
|
||||
"status": status,
|
||||
"headers": headers,
|
||||
})
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
)
|
||||
.payload(Payload {
|
||||
mime: Some("application/octet-stream".to_string()),
|
||||
bytes: payload_bytes,
|
||||
})
|
||||
.send()
|
||||
}
|
||||
|
||||
fn send_ws_update(our: Address, game: Game) -> anyhow::Result<()> {
|
||||
Request::new()
|
||||
.target((&our.node, "encryptor", "sys", "uqbar"))
|
||||
.ipc(
|
||||
serde_json::json!({
|
||||
"EncryptAndForward": {
|
||||
"channel_id": our.process.to_string(),
|
||||
"forward_to": {
|
||||
"node": our.node.clone(),
|
||||
"process": {
|
||||
"process_name": "http_server",
|
||||
"package_name": "sys",
|
||||
"publisher_node": "uqbar"
|
||||
}
|
||||
}, // node, process
|
||||
"json": Some(serde_json::json!({ // this is the JSON to forward
|
||||
"WebSocketPush": {
|
||||
"target": {
|
||||
"node": our.node.clone(),
|
||||
"id": "chess", // If the message passed in an ID then we could send to just that ID
|
||||
}
|
||||
}
|
||||
})),
|
||||
}
|
||||
|
||||
})
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
)
|
||||
.payload(Payload {
|
||||
mime: Some("application/json".to_string()),
|
||||
bytes: serde_json::json!({
|
||||
"kind": "game_update",
|
||||
"data": json_game(&game),
|
||||
})
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
})
|
||||
.send()
|
||||
}
|
||||
|
||||
fn response_success() -> bool {
|
||||
let Some(payload) = get_payload() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Ok(status) = String::from_utf8(payload.bytes) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
status == "success"
|
||||
}
|
||||
|
||||
fn save_chess_state(state: ChessState) {
|
||||
let stored_state = convert_state(state);
|
||||
set_state(&bincode::serialize(&stored_state).unwrap());
|
||||
}
|
||||
|
||||
const CHESS_PAGE: &str = include_str!("../pkg/chess.html");
|
||||
const CHESS_JS: &str = include_str!("../pkg/index.js");
|
||||
const CHESS_CSS: &str = include_str!("../pkg/index.css");
|
||||
@ -174,67 +61,56 @@ const CHESS_CSS: &str = include_str!("../pkg/index.css");
|
||||
impl Guest for Component {
|
||||
fn init(our: String) {
|
||||
let our = Address::from_str(&our).unwrap();
|
||||
println!("chess: start");
|
||||
println!("{our}: start");
|
||||
|
||||
grant_messaging(
|
||||
&our,
|
||||
vec![ProcessId::new(Some("http_server"), "sys", "uqbar")],
|
||||
);
|
||||
|
||||
for path in ["/", "/games"] {
|
||||
let _ = Request::new()
|
||||
.target((our.node.as_str(), "http_server", "sys", "uqbar"))
|
||||
.ipc(
|
||||
serde_json::json!({
|
||||
"BindPath": {
|
||||
"path": path,
|
||||
"authenticated": true,
|
||||
"local_only": false
|
||||
}
|
||||
})
|
||||
.to_string()
|
||||
// serve static page at /
|
||||
// dynamically handle requests to /games
|
||||
http::bind_http_static_path(
|
||||
"/",
|
||||
true,
|
||||
false,
|
||||
Some("text/html".to_string()),
|
||||
CHESS_PAGE
|
||||
.replace("${node}", &our.node)
|
||||
.replace("${process}", &our.process.to_string())
|
||||
// TODO serve these independently on paths..
|
||||
// also build utils for just serving a vfs dir
|
||||
.replace("${js}", CHESS_JS)
|
||||
.replace("${css}", CHESS_CSS)
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
)
|
||||
.send();
|
||||
}
|
||||
.unwrap();
|
||||
http::bind_http_path("/games", true, false).unwrap();
|
||||
|
||||
let mut state: ChessState =
|
||||
match get_typed_state(|bytes| Ok(bincode::deserialize::<StoredChessState>(bytes)?)) {
|
||||
Some(state) => {
|
||||
let mut games = HashMap::new();
|
||||
for (id, game) in state.games {
|
||||
if let Ok(board) = Board::from_fen(&game.board) {
|
||||
games.insert(
|
||||
id,
|
||||
let mut state: ChessState = match get_typed_state(|bytes| {
|
||||
Ok(bincode::deserialize::<StoredChessState>(bytes)?)
|
||||
}) {
|
||||
Some(mut state) => ChessState {
|
||||
games: state
|
||||
.games
|
||||
.iter_mut()
|
||||
.map(|(id, game)| {
|
||||
(
|
||||
id.clone(),
|
||||
Game {
|
||||
id: game.id.clone(),
|
||||
id: id.to_owned(),
|
||||
turns: game.turns,
|
||||
board,
|
||||
white: game.white.clone(),
|
||||
black: game.black.clone(),
|
||||
board: Board::from_fen(&game.board).unwrap_or(Board::start_pos()),
|
||||
white: game.white.to_owned(),
|
||||
black: game.black.to_owned(),
|
||||
ended: game.ended,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
games.insert(
|
||||
id,
|
||||
Game {
|
||||
id: game.id.clone(),
|
||||
turns: 0,
|
||||
board: Board::start_pos(),
|
||||
white: game.white.clone(),
|
||||
black: game.black.clone(),
|
||||
ended: game.ended,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
ChessState {
|
||||
games,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
records: state.records,
|
||||
}
|
||||
}
|
||||
},
|
||||
None => ChessState {
|
||||
games: HashMap::new(),
|
||||
records: HashMap::new(),
|
||||
@ -250,12 +126,9 @@ impl Guest for Component {
|
||||
println!("chess: got unexpected Response");
|
||||
continue;
|
||||
};
|
||||
|
||||
match handle_request(&our, &source, &request, &mut state) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
println!("chess: error handling request: {:?}", e);
|
||||
}
|
||||
Ok(()) => continue,
|
||||
Err(e) => println!("chess: error handling request: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -267,21 +140,29 @@ fn handle_request(
|
||||
request: &wit::Request,
|
||||
state: &mut ChessState,
|
||||
) -> anyhow::Result<()> {
|
||||
let message_json: serde_json::Value = match serde_json::from_slice(&request.ipc) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Err(anyhow::anyhow!("chess: failed to parse ipc JSON, skipping")),
|
||||
};
|
||||
|
||||
// print_to_terminal(1, &format!("chess: parsed ipc JSON: {:?}", message_json));
|
||||
|
||||
if source.process == "chess:chess:uqbar" {
|
||||
let action = message_json["action"].as_str().unwrap_or("");
|
||||
let game_id = source.node.clone();
|
||||
let message_json = serde_json::from_slice::<serde_json::Value>(&request.ipc)?;
|
||||
handle_chess_request(our, source, message_json, state)
|
||||
} else if source.process.to_string() == "http_server:sys:uqbar" {
|
||||
let http_request = serde_json::from_slice::<http::IncomingHttpRequest>(&request.ipc)?;
|
||||
handle_http_request(our, http_request, state)
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("chess: got request from unexpected source"));
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_chess_request(
|
||||
our: &Address,
|
||||
source: &Address,
|
||||
message_json: serde_json::Value,
|
||||
state: &mut ChessState,
|
||||
) -> anyhow::Result<()> {
|
||||
let action = message_json["action"].as_str().unwrap_or("");
|
||||
let game_id = &source.node;
|
||||
match action {
|
||||
"new_game" => {
|
||||
// make a new game with source.node if the current game has ended
|
||||
if let Some(game) = state.games.get(&game_id) {
|
||||
if let Some(game) = state.games.get(game_id) {
|
||||
if !game.ended {
|
||||
return Response::new()
|
||||
.ipc(vec![])
|
||||
@ -293,24 +174,23 @@ fn handle_request(
|
||||
}
|
||||
}
|
||||
let game = Game {
|
||||
id: game_id.clone(),
|
||||
id: game_id.to_string(),
|
||||
turns: 0,
|
||||
board: Board::start_pos(),
|
||||
white: message_json["white"]
|
||||
.as_str()
|
||||
.unwrap_or(game_id.as_str())
|
||||
.unwrap_or(game_id)
|
||||
.to_string(),
|
||||
black: message_json["black"]
|
||||
.as_str()
|
||||
.unwrap_or(our.node.as_str())
|
||||
.unwrap_or(&our.node)
|
||||
.to_string(),
|
||||
ended: false,
|
||||
};
|
||||
state.games.insert(game_id.clone(), game.clone());
|
||||
state.games.insert(game_id.to_string(), game.clone());
|
||||
|
||||
let _ = send_ws_update(our.clone(), game.clone());
|
||||
|
||||
save_chess_state(state.clone());
|
||||
utils::send_ws_update(&our, &game)?;
|
||||
utils::save_chess_state(&state);
|
||||
|
||||
Response::new()
|
||||
.ipc(vec![])
|
||||
@ -322,7 +202,7 @@ fn handle_request(
|
||||
}
|
||||
"make_move" => {
|
||||
// check the move and then update if correct and send WS update
|
||||
let Some(game) = state.games.get_mut(&game_id) else {
|
||||
let Some(game) = state.games.get_mut(game_id) else {
|
||||
return Response::new()
|
||||
.ipc(vec![])
|
||||
.payload(Payload {
|
||||
@ -375,8 +255,8 @@ fn handle_request(
|
||||
}
|
||||
}
|
||||
|
||||
let _ = send_ws_update(our.clone(), game.clone());
|
||||
save_chess_state(state.clone());
|
||||
utils::send_ws_update(&our, &game)?;
|
||||
utils::save_chess_state(&state);
|
||||
|
||||
Response::new()
|
||||
.ipc(vec![])
|
||||
@ -397,7 +277,7 @@ fn handle_request(
|
||||
}
|
||||
"end_game" => {
|
||||
// end the game and send WS update, update the standings
|
||||
let Some(game) = state.games.get_mut(&game_id) else {
|
||||
let Some(game) = state.games.get_mut(game_id) else {
|
||||
return Response::new()
|
||||
.ipc(vec![])
|
||||
.payload(Payload {
|
||||
@ -415,8 +295,8 @@ fn handle_request(
|
||||
state.records.insert(game.id.clone(), (1, 0, 0));
|
||||
}
|
||||
|
||||
let _ = send_ws_update(our.clone(), game.clone());
|
||||
save_chess_state(state.clone());
|
||||
utils::send_ws_update(&our, &game)?;
|
||||
utils::save_chess_state(&state);
|
||||
|
||||
Response::new()
|
||||
.ipc(vec![])
|
||||
@ -428,218 +308,140 @@ fn handle_request(
|
||||
}
|
||||
_ => return Err(anyhow::anyhow!("chess: got unexpected action")),
|
||||
}
|
||||
} else if source.process.to_string() == "http_server:sys:uqbar" {
|
||||
let path = message_json["path"].as_str().unwrap_or("");
|
||||
let method = message_json["method"].as_str().unwrap_or("");
|
||||
}
|
||||
|
||||
let mut default_headers = HashMap::new();
|
||||
default_headers.insert("Content-Type".to_string(), "text/html".to_string());
|
||||
// Handle incoming http
|
||||
match path {
|
||||
"/" => {
|
||||
return send_http_response(
|
||||
200,
|
||||
default_headers.clone(),
|
||||
CHESS_PAGE
|
||||
.replace("${node}", &our.node)
|
||||
.replace("${process}", &our.process.to_string())
|
||||
.replace("${js}", CHESS_JS)
|
||||
.replace("${css}", CHESS_CSS)
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
);
|
||||
}
|
||||
"/games" => {
|
||||
match method {
|
||||
"GET" => {
|
||||
return send_http_response(
|
||||
200,
|
||||
{
|
||||
let mut headers = default_headers.clone();
|
||||
headers.insert(
|
||||
"Content-Type".to_string(),
|
||||
"application/json".to_string(),
|
||||
);
|
||||
headers
|
||||
},
|
||||
{
|
||||
let mut json_games: HashMap<String, serde_json::Value> =
|
||||
HashMap::new();
|
||||
for (id, game) in &state.games {
|
||||
json_games.insert(id.to_string(), json_game(&game));
|
||||
}
|
||||
json!(json_games).to_string().as_bytes().to_vec()
|
||||
},
|
||||
fn handle_http_request(
|
||||
our: &Address,
|
||||
http_request: http::IncomingHttpRequest,
|
||||
state: &mut ChessState,
|
||||
) -> anyhow::Result<()> {
|
||||
if http_request.path()? != "/games" {
|
||||
return http::send_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
None,
|
||||
"Not Found".to_string().as_bytes().to_vec(),
|
||||
);
|
||||
}
|
||||
match http_request.method.as_str() {
|
||||
"GET" => http::send_response(
|
||||
http::StatusCode::OK,
|
||||
Some(HashMap::from([(
|
||||
String::from("Content-Type"),
|
||||
String::from("application/json"),
|
||||
)])),
|
||||
serde_json::to_vec(&serde_json::json!(state
|
||||
.games
|
||||
.iter()
|
||||
.map(|(id, game)| (id.to_string(), utils::json_game(game)))
|
||||
.collect::<HashMap<String, serde_json::Value>>()))?,
|
||||
),
|
||||
"POST" => {
|
||||
// create a new game
|
||||
if let Some(payload) = get_payload() {
|
||||
if let Ok(payload_json) =
|
||||
serde_json::from_slice::<serde_json::Value>(&payload.bytes)
|
||||
let Some(payload) = get_payload() else {
|
||||
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
|
||||
};
|
||||
let payload_json = serde_json::from_slice::<serde_json::Value>(&payload.bytes)?;
|
||||
let Some(game_id) = payload_json["id"].as_str() else {
|
||||
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
|
||||
};
|
||||
if let Some(game) = state.games.get(game_id)
|
||||
&& !game.ended
|
||||
{
|
||||
let game_id =
|
||||
String::from(payload_json["id"].as_str().unwrap_or(""));
|
||||
if game_id == "" {
|
||||
return send_http_response(
|
||||
400,
|
||||
default_headers.clone(),
|
||||
"Bad Request".to_string().as_bytes().to_vec(),
|
||||
);
|
||||
}
|
||||
return http::send_response(http::StatusCode::CONFLICT, None, vec![]);
|
||||
};
|
||||
|
||||
if let Some(game) = state.games.get(&game_id) {
|
||||
if !game.ended {
|
||||
return send_http_response(
|
||||
409,
|
||||
default_headers.clone(),
|
||||
"Conflict".to_string().as_bytes().to_vec(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let white = payload_json["white"]
|
||||
let player_white = payload_json["white"]
|
||||
.as_str()
|
||||
.unwrap_or(our.node.as_str())
|
||||
.to_string();
|
||||
let black = payload_json["black"]
|
||||
let player_black = payload_json["black"]
|
||||
.as_str()
|
||||
.unwrap_or(game_id.as_str())
|
||||
.unwrap_or(game_id)
|
||||
.to_string();
|
||||
|
||||
// send the other player a new game request
|
||||
let response = Request::new()
|
||||
.target((game_id.as_str(), "chess", "chess", "uqbar"))
|
||||
.ipc(
|
||||
serde_json::json!({
|
||||
.target((game_id, "chess", "chess", "uqbar"))
|
||||
.ipc(serde_json::to_vec(&serde_json::json!({
|
||||
"action": "new_game",
|
||||
"white": white.clone(),
|
||||
"black": black.clone(),
|
||||
})
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
)
|
||||
"white": player_white.clone(),
|
||||
"black": player_black.clone(),
|
||||
}))?)
|
||||
.send_and_await_response(30)?;
|
||||
|
||||
match response {
|
||||
Ok(_response) => {
|
||||
if !response_success() {
|
||||
return send_http_response(
|
||||
503,
|
||||
default_headers.clone(),
|
||||
"Service Unavailable"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
// if they accept, create a new game
|
||||
// otherwise, should surface error to FE...
|
||||
let Ok((_source, Message::Response((resp, _context)))) = response else {
|
||||
return http::send_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
None,
|
||||
"Service Unavailable".to_string().as_bytes().to_vec(),
|
||||
);
|
||||
};
|
||||
if resp.ipc != "success".as_bytes() {
|
||||
return http::send_response(http::StatusCode::SERVICE_UNAVAILABLE, None, vec![]);
|
||||
}
|
||||
// create a new game
|
||||
let game = Game {
|
||||
id: game_id.clone(),
|
||||
id: game_id.to_string(),
|
||||
turns: 0,
|
||||
board: Board::start_pos(),
|
||||
white: white.clone(),
|
||||
black: black.clone(),
|
||||
white: player_white,
|
||||
black: player_black,
|
||||
ended: false,
|
||||
};
|
||||
state.games.insert(game_id.clone(), game.clone());
|
||||
|
||||
save_chess_state(state.clone());
|
||||
|
||||
return send_http_response(
|
||||
200,
|
||||
{
|
||||
let mut headers = default_headers.clone();
|
||||
headers.insert(
|
||||
"Content-Type".to_string(),
|
||||
"application/json".to_string(),
|
||||
);
|
||||
headers
|
||||
},
|
||||
json_game(&game).to_string().as_bytes().to_vec(),
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
return send_http_response(
|
||||
503,
|
||||
default_headers.clone(),
|
||||
"Service Unavailable".to_string().as_bytes().to_vec(),
|
||||
let body = serde_json::to_vec(&utils::json_game(&game))?;
|
||||
state.games.insert(game_id.to_string(), game);
|
||||
utils::save_chess_state(&state);
|
||||
http::send_response(
|
||||
http::StatusCode::OK,
|
||||
Some(HashMap::from([(
|
||||
String::from("Content-Type"),
|
||||
String::from("application/json"),
|
||||
)])),
|
||||
body,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return send_http_response(
|
||||
400,
|
||||
default_headers.clone(),
|
||||
"Bad Request".to_string().as_bytes().to_vec(),
|
||||
);
|
||||
}
|
||||
"PUT" => {
|
||||
// make a move
|
||||
if let Some(payload) = get_payload() {
|
||||
if let Ok(payload_json) =
|
||||
serde_json::from_slice::<serde_json::Value>(&payload.bytes)
|
||||
{
|
||||
let game_id =
|
||||
String::from(payload_json["id"].as_str().unwrap_or(""));
|
||||
|
||||
if game_id == "" {
|
||||
return send_http_response(
|
||||
400,
|
||||
default_headers.clone(),
|
||||
"No game ID".to_string().as_bytes().to_vec(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(game) = state.games.get_mut(&game_id) {
|
||||
let Some(payload) = get_payload() else {
|
||||
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
|
||||
};
|
||||
let payload_json = serde_json::from_slice::<serde_json::Value>(&payload.bytes)?;
|
||||
let Some(game_id) = payload_json["id"].as_str() else {
|
||||
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
|
||||
};
|
||||
let Some(game) = state.games.get_mut(game_id) else {
|
||||
return http::send_response(http::StatusCode::NOT_FOUND, None, vec![]);
|
||||
};
|
||||
if (game.turns % 2 == 0 && game.white != our.node)
|
||||
|| (game.turns % 2 == 1 && game.black != our.node)
|
||||
{
|
||||
return send_http_response(
|
||||
403,
|
||||
default_headers.clone(),
|
||||
"Forbidden".to_string().as_bytes().to_vec(),
|
||||
);
|
||||
return http::send_response(http::StatusCode::FORBIDDEN, None, vec![]);
|
||||
} else if game.ended {
|
||||
return send_http_response(
|
||||
409,
|
||||
default_headers.clone(),
|
||||
"Conflict".to_string().as_bytes().to_vec(),
|
||||
);
|
||||
return http::send_response(http::StatusCode::CONFLICT, None, vec![]);
|
||||
}
|
||||
|
||||
let move_str = payload_json["move"].as_str().unwrap_or("");
|
||||
let valid_move = game.board.apply_uci_move(move_str);
|
||||
if valid_move {
|
||||
if !game.board.apply_uci_move(move_str) {
|
||||
// TODO surface illegal move to player or something here
|
||||
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
|
||||
}
|
||||
// send the move to the other player
|
||||
// check if the game is over
|
||||
// if so, update the records
|
||||
let response = Request::new()
|
||||
.target((game_id.as_str(), "chess", "chess", "uqbar"))
|
||||
.ipc(
|
||||
serde_json::json!({
|
||||
.target((game_id, "chess", "chess", "uqbar"))
|
||||
.ipc(serde_json::to_vec(&serde_json::json!({
|
||||
"action": "make_move",
|
||||
"move": move_str,
|
||||
})
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
)
|
||||
}))?)
|
||||
.send_and_await_response(30)?;
|
||||
|
||||
match response {
|
||||
Ok(_response) => {
|
||||
if !response_success() {
|
||||
return send_http_response(
|
||||
503,
|
||||
default_headers.clone(),
|
||||
"Service Unavailable"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
);
|
||||
let Ok((_source, Message::Response((resp, _context)))) = response else {
|
||||
// TODO surface error to player, let them know other player is
|
||||
// offline or whatever they respond here was invalid
|
||||
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
|
||||
};
|
||||
if resp.ipc != "success".as_bytes() {
|
||||
return http::send_response(http::StatusCode::SERVICE_UNAVAILABLE, None, vec![]);
|
||||
}
|
||||
// update the game
|
||||
game.turns += 1;
|
||||
@ -650,29 +452,22 @@ fn handle_request(
|
||||
game.ended = true;
|
||||
let winner = if checkmate {
|
||||
if game.turns % 2 == 1 {
|
||||
game.white.clone()
|
||||
&game.white
|
||||
} else {
|
||||
game.black.clone()
|
||||
&game.black
|
||||
}
|
||||
} else {
|
||||
"".to_string()
|
||||
""
|
||||
};
|
||||
|
||||
// update the records
|
||||
if draw {
|
||||
if let Some(record) =
|
||||
state.records.get_mut(&game.id)
|
||||
{
|
||||
if let Some(record) = state.records.get_mut(&game.id) {
|
||||
record.2 += 1;
|
||||
} else {
|
||||
state
|
||||
.records
|
||||
.insert(game.id.clone(), (0, 0, 1));
|
||||
state.records.insert(game.id.clone(), (0, 0, 1));
|
||||
}
|
||||
} else {
|
||||
if let Some(record) =
|
||||
state.records.get_mut(&game.id)
|
||||
{
|
||||
if let Some(record) = state.records.get_mut(&game.id) {
|
||||
if winner == our.node {
|
||||
record.0 += 1;
|
||||
} else {
|
||||
@ -680,155 +475,75 @@ fn handle_request(
|
||||
}
|
||||
} else {
|
||||
if winner == our.node {
|
||||
state.records.insert(
|
||||
game.id.clone(),
|
||||
(1, 0, 0),
|
||||
);
|
||||
state.records.insert(game.id.clone(), (1, 0, 0));
|
||||
} else {
|
||||
state.records.insert(
|
||||
game.id.clone(),
|
||||
(0, 1, 0),
|
||||
);
|
||||
state.records.insert(game.id.clone(), (0, 1, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let game = game.clone();
|
||||
save_chess_state(state.clone());
|
||||
// game is not over, update state and return to FE
|
||||
let body = serde_json::to_vec(&utils::json_game(&game))?;
|
||||
utils::save_chess_state(&state);
|
||||
// return the game
|
||||
return send_http_response(
|
||||
200,
|
||||
{
|
||||
let mut headers = default_headers.clone();
|
||||
headers.insert(
|
||||
"Content-Type".to_string(),
|
||||
"application/json".to_string(),
|
||||
);
|
||||
headers
|
||||
},
|
||||
json_game(&game)
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
return send_http_response(
|
||||
503,
|
||||
default_headers.clone(),
|
||||
"Service Unavailable"
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
http::send_response(
|
||||
http::StatusCode::OK,
|
||||
Some(HashMap::from([(
|
||||
String::from("Content-Type"),
|
||||
String::from("application/json"),
|
||||
)])),
|
||||
body,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("chess: never got a response");
|
||||
return send_http_response(
|
||||
400,
|
||||
default_headers.clone(),
|
||||
"Bad Request".to_string().as_bytes().to_vec(),
|
||||
);
|
||||
}
|
||||
"DELETE" => {
|
||||
let game_id = message_json["query_params"]["id"]
|
||||
.as_str()
|
||||
.unwrap_or("");
|
||||
if game_id == "" {
|
||||
return send_http_response(
|
||||
400,
|
||||
default_headers.clone(),
|
||||
"Bad Request".to_string().as_bytes().to_vec(),
|
||||
);
|
||||
} else {
|
||||
let Some(game) = state.games.get_mut(game_id) else {
|
||||
return send_http_response(
|
||||
400,
|
||||
default_headers.clone(),
|
||||
"Bad Request".to_string().as_bytes().to_vec(),
|
||||
);
|
||||
// "end the game"?
|
||||
let query_params = http_request.query_params()?;
|
||||
let Some(game_id) = query_params.get("id") else {
|
||||
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
|
||||
};
|
||||
let Some(game) = state.games.get_mut(game_id) else {
|
||||
return http::send_response(http::StatusCode::BAD_REQUEST, None, vec![]);
|
||||
};
|
||||
// send the other player an end game request
|
||||
let response = Request::new()
|
||||
.target((game_id, "chess", "chess", "uqbar"))
|
||||
.ipc(
|
||||
serde_json::json!({
|
||||
.ipc(serde_json::to_vec(&serde_json::json!({
|
||||
"action": "end_game",
|
||||
})
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
)
|
||||
}))?)
|
||||
.send_and_await_response(30)?;
|
||||
|
||||
match response {
|
||||
Ok(_response) => {
|
||||
if !response_success() {
|
||||
return send_http_response(
|
||||
503,
|
||||
default_headers.clone(),
|
||||
"Service Unavailable".to_string().as_bytes().to_vec(),
|
||||
);
|
||||
let Ok((_source, Message::Response((resp, _context)))) = response else {
|
||||
// TODO surface error to player, let them know other player is
|
||||
// offline or whatever they respond here was invalid
|
||||
return http::send_response(http::StatusCode::SERVICE_UNAVAILABLE, None, vec![]);
|
||||
};
|
||||
if resp.ipc != "success".as_bytes() {
|
||||
return http::send_response(http::StatusCode::SERVICE_UNAVAILABLE, None, vec![]);
|
||||
}
|
||||
|
||||
game.ended = true;
|
||||
|
||||
if let Some(record) = state.records.get_mut(&game.id) {
|
||||
record.1 += 1;
|
||||
} else {
|
||||
state.records.insert(game.id.clone(), (0, 1, 0));
|
||||
}
|
||||
|
||||
let game = game.clone();
|
||||
save_chess_state(state.clone());
|
||||
|
||||
// return the game
|
||||
return send_http_response(
|
||||
200,
|
||||
{
|
||||
let mut headers = default_headers.clone();
|
||||
headers.insert(
|
||||
"Content-Type".to_string(),
|
||||
"application/json".to_string(),
|
||||
);
|
||||
headers
|
||||
},
|
||||
json_game(&game).to_string().as_bytes().to_vec(),
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
return send_http_response(
|
||||
503,
|
||||
default_headers.clone(),
|
||||
"Service Unavailable".to_string().as_bytes().to_vec(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return send_http_response(
|
||||
404,
|
||||
default_headers.clone(),
|
||||
"Not Found".to_string().as_bytes().to_vec(),
|
||||
let body = serde_json::to_vec(&utils::json_game(&game))?;
|
||||
utils::save_chess_state(&state);
|
||||
|
||||
http::send_response(
|
||||
http::StatusCode::OK,
|
||||
Some(HashMap::from([(
|
||||
String::from("Content-Type"),
|
||||
String::from("application/json"),
|
||||
)])),
|
||||
body,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return send_http_response(
|
||||
404,
|
||||
default_headers.clone(),
|
||||
"Not Found".to_string().as_bytes().to_vec(),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("chess: got request from unexpected source"));
|
||||
_ => Response::new()
|
||||
.ipc(serde_json::to_vec(&http::HttpResponse {
|
||||
status: 405,
|
||||
headers: HashMap::new(),
|
||||
})?)
|
||||
.send(),
|
||||
}
|
||||
}
|
||||
|
82
modules/chess/src/utils.rs
Normal file
82
modules/chess/src/utils.rs
Normal file
@ -0,0 +1,82 @@
|
||||
use crate::*;
|
||||
|
||||
pub fn save_chess_state(state: &ChessState) {
|
||||
let stored_state = convert_state(&state);
|
||||
set_state(&bincode::serialize(&stored_state).unwrap());
|
||||
}
|
||||
|
||||
fn convert_game(game: Game) -> StoredGame {
|
||||
StoredGame {
|
||||
id: game.id,
|
||||
turns: game.turns,
|
||||
board: game.board.fen(),
|
||||
white: game.white,
|
||||
black: game.black,
|
||||
ended: game.ended,
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_state(state: &ChessState) -> StoredChessState {
|
||||
StoredChessState {
|
||||
games: state
|
||||
.games
|
||||
.iter()
|
||||
.map(|(id, game)| (id.to_string(), convert_game(game.clone())))
|
||||
.collect(),
|
||||
records: state.records.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn json_game(game: &Game) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"id": game.id,
|
||||
"turns": game.turns,
|
||||
"board": game.board.fen(),
|
||||
"white": game.white,
|
||||
"black": game.black,
|
||||
"ended": game.ended,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send_ws_update(our: &Address, game: &Game) -> anyhow::Result<()> {
|
||||
Request::new()
|
||||
.target((&our.node, "http_server", "sys", "uqbar"))
|
||||
.ipc(
|
||||
serde_json::json!({
|
||||
"EncryptAndForward": {
|
||||
"channel_id": our.process.to_string(),
|
||||
"forward_to": {
|
||||
"node": our.node.clone(),
|
||||
"process": {
|
||||
"process_name": "http_server",
|
||||
"package_name": "sys",
|
||||
"publisher_node": "uqbar"
|
||||
}
|
||||
}, // node, process
|
||||
"json": Some(serde_json::json!({ // this is the JSON to forward
|
||||
"WebSocketPush": {
|
||||
"target": {
|
||||
"node": our.node.clone(),
|
||||
"id": "chess", // If the message passed in an ID then we could send to just that ID
|
||||
}
|
||||
}
|
||||
})),
|
||||
}
|
||||
|
||||
})
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
)
|
||||
.payload(Payload {
|
||||
mime: Some("application/json".to_string()),
|
||||
bytes: serde_json::json!({
|
||||
"kind": "game_update",
|
||||
"data": json_game(game),
|
||||
})
|
||||
.to_string()
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
})
|
||||
.send()
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import json
|
||||
import base64
|
||||
import os
|
||||
import shutil
|
||||
import http.client
|
||||
from urllib.parse import urlparse
|
||||
|
||||
### helpers
|
||||
def send_request(path, json_data):
|
||||
conn = http.client.HTTPConnection(HOST, PORT)
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
conn.request("POST", path, json_data, headers)
|
||||
|
||||
response = conn.getresponse()
|
||||
|
||||
conn.close()
|
||||
return response
|
||||
|
||||
def new_package(package_name, publisher_node, zip_file):
|
||||
request = {
|
||||
"node": NODE,
|
||||
"process": "main:app_store:uqbar",
|
||||
"inherit": False,
|
||||
"expects_response": None,
|
||||
"ipc": json.dumps({
|
||||
"NewPackage": {
|
||||
"package": {"package_name": package_name, "publisher_node": publisher_node },
|
||||
"mirror": True
|
||||
}
|
||||
}),
|
||||
"metadata": None,
|
||||
"context": None,
|
||||
"mime": "application/zip",
|
||||
"data": zip_file
|
||||
}
|
||||
return json.dumps(request)
|
||||
|
||||
|
||||
def install_package(package_name, publisher_node):
|
||||
request = {
|
||||
"node": NODE,
|
||||
"process": "main:app_store:uqbar",
|
||||
"inherit": False,
|
||||
"expects_response": None,
|
||||
"ipc": json.dumps({
|
||||
"Install": {"package_name": package_name, "publisher_node": publisher_node },
|
||||
}),
|
||||
"metadata": None,
|
||||
"context": None,
|
||||
"mime": None,
|
||||
"data": None,
|
||||
}
|
||||
return json.dumps(request)
|
||||
|
||||
# zip a directory
|
||||
def zip_directory(directory, zip_filename):
|
||||
shutil.make_archive(zip_filename, 'zip', directory)
|
||||
|
||||
# encode a file with base64
|
||||
def encode_file_in_base64(file_path):
|
||||
with open(file_path, 'rb') as file:
|
||||
return base64.b64encode(file.read()).decode('utf-8')
|
||||
|
||||
|
||||
|
||||
|
||||
# check if there are enough parameters provided.
|
||||
if len(sys.argv) < 3 or len(sys.argv) > 4:
|
||||
print("Usage: python3 start-package.py <url> <pkg-dir> [node-id]")
|
||||
sys.exit(1)
|
||||
|
||||
URL = sys.argv[1]
|
||||
PKG_DIR = os.path.abspath(sys.argv[2])
|
||||
|
||||
# If NODE is provided, use it. Otherwise, set it to None.
|
||||
NODE = sys.argv[3] if len(sys.argv) == 4 else None
|
||||
|
||||
parsed_url = urlparse(URL)
|
||||
HOST = parsed_url.hostname
|
||||
PORT = parsed_url.port
|
||||
|
||||
# parse metadata.json to get the package and publisher
|
||||
with open(f"{PKG_DIR}/metadata.json", 'r') as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
PACKAGE = metadata['package']
|
||||
PUBLISHER = metadata['publisher']
|
||||
PKG_PUBLISHER = f"{PACKAGE}:{PUBLISHER}"
|
||||
|
||||
print(PKG_PUBLISHER)
|
||||
|
||||
# create zip and put it in /target
|
||||
parent_dir = os.path.dirname(PKG_DIR)
|
||||
parent_dir = os.path.abspath(parent_dir)
|
||||
os.makedirs(os.path.join(parent_dir, 'target'), exist_ok=True)
|
||||
|
||||
zip_filename = os.path.join(parent_dir, 'target', PKG_PUBLISHER)
|
||||
zip_directory(PKG_DIR, zip_filename)
|
||||
|
||||
|
||||
encoded_zip_file = encode_file_in_base64(zip_filename + '.zip')
|
||||
|
||||
|
||||
# create a new package
|
||||
new_pkg = new_package(PACKAGE, PUBLISHER, encoded_zip_file)
|
||||
res = send_request("/rpc:sys:uqbar/message", new_pkg)
|
||||
|
||||
if not res.status == 200:
|
||||
print("Failed to send new package request, status: ", res.status)
|
||||
sys.exit(1)
|
||||
|
||||
# install/start/reboot the package
|
||||
install_pkg = install_package(PACKAGE, PUBLISHER)
|
||||
|
||||
resp = send_request("/rpc:sys:uqbar/message", install_pkg)
|
||||
if not resp.status == 200:
|
||||
print("Failed to send install package request, status: ", resp.status)
|
||||
sys.exit(1)
|
||||
|
||||
print("Successfully installed package: ", PKG_PUBLISHER)
|
||||
sys.exit(0)
|
Binary file not shown.
@ -177,6 +177,12 @@ async fn http_handler(
|
||||
) -> Result<impl warp::Reply, warp::Rejection> {
|
||||
// TODO this is all so dirty. Figure out what actually matters.
|
||||
|
||||
println!(
|
||||
"http_server: got request from {:?} for {}\r",
|
||||
socket_addr,
|
||||
path.as_str()
|
||||
);
|
||||
|
||||
// trim trailing "/"
|
||||
let original_path = normalize_path(path.as_str());
|
||||
let id: u64 = rand::random();
|
||||
@ -188,6 +194,8 @@ async fn http_handler(
|
||||
};
|
||||
let bound_path = route.handler();
|
||||
|
||||
println!("here1\r");
|
||||
|
||||
if bound_path.authenticated {
|
||||
let auth_token = serialized_headers
|
||||
.get("cookie")
|
||||
@ -198,6 +206,8 @@ async fn http_handler(
|
||||
}
|
||||
}
|
||||
|
||||
println!("here2\r");
|
||||
|
||||
let is_local = socket_addr
|
||||
.map(|addr| addr.ip().is_loopback())
|
||||
.unwrap_or(false);
|
||||
@ -206,6 +216,8 @@ async fn http_handler(
|
||||
return Ok(warp::reply::with_status(vec![], StatusCode::FORBIDDEN).into_response());
|
||||
}
|
||||
|
||||
println!("here3\r");
|
||||
|
||||
// if path has static content, serve it
|
||||
if let Some(static_content) = &bound_path.static_content {
|
||||
return Ok(warp::http::Response::builder()
|
||||
@ -221,6 +233,8 @@ async fn http_handler(
|
||||
.into_response());
|
||||
}
|
||||
|
||||
println!("here4\r");
|
||||
|
||||
// RPC functionality: if path is /rpc:sys:uqbar/message,
|
||||
// we extract message from base64 encoded bytes in data
|
||||
// and send it to the correct app.
|
||||
@ -376,7 +390,7 @@ async fn handle_rpc_message(
|
||||
async fn maintain_websocket(
|
||||
ws: WebSocket,
|
||||
our: Arc<String>,
|
||||
jwt_secret_bytes: Arc<Vec<u8>>,
|
||||
_jwt_secret_bytes: Arc<Vec<u8>>,
|
||||
ws_senders: WebSocketSenders,
|
||||
send_to_loop: MessageSender,
|
||||
) {
|
||||
@ -386,7 +400,7 @@ async fn maintain_websocket(
|
||||
// channel and verify their identity using JWT. Then we can forward their
|
||||
// messages to a specific process.
|
||||
|
||||
let owner_process: ProcessId = todo!();
|
||||
let owner_process: ProcessId = ProcessId::new(Some("chess"), "chess2", "uqbar");
|
||||
|
||||
let ws_channel_id: u64 = rand::random();
|
||||
let (ws_sender, mut ws_receiver) = tokio::sync::mpsc::channel(100);
|
||||
@ -399,17 +413,44 @@ async fn maintain_websocket(
|
||||
None => {
|
||||
// stream closed, remove and exit
|
||||
websocket_close(ws_channel_id, owner_process, &ws_senders, &send_to_loop).await;
|
||||
return;
|
||||
break;
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
// stream error, remove and exit
|
||||
println!("http_server websocket channel error: {e}");
|
||||
websocket_close(ws_channel_id, owner_process, &ws_senders, &send_to_loop).await;
|
||||
return;
|
||||
break;
|
||||
}
|
||||
Some(Ok(msg)) => {
|
||||
// forward message to process associated with this channel
|
||||
todo!();
|
||||
let _ = send_to_loop
|
||||
.send(KernelMessage {
|
||||
id: rand::random(),
|
||||
source: Address {
|
||||
node: our.to_string(),
|
||||
process: HTTP_SERVER_PROCESS_ID.clone(),
|
||||
},
|
||||
target: Address {
|
||||
node: our.to_string(),
|
||||
process: owner_process.clone(),
|
||||
},
|
||||
rsvp: None,
|
||||
message: Message::Request(Request {
|
||||
inherit: false,
|
||||
expects_response: None,
|
||||
ipc: serde_json::to_vec(&HttpServerAction::WebSocketPush {
|
||||
channel_id: ws_channel_id,
|
||||
message_type: WsMessageType::Binary,
|
||||
}).unwrap(),
|
||||
metadata: None,
|
||||
}),
|
||||
payload: Some(Payload {
|
||||
mime: None,
|
||||
bytes: msg.into_bytes(),
|
||||
}),
|
||||
signed_capabilities: None,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -421,12 +462,14 @@ async fn maintain_websocket(
|
||||
// stream error, remove and exit
|
||||
println!("http_server websocket channel error: {e}");
|
||||
websocket_close(ws_channel_id, owner_process, &ws_senders, &send_to_loop).await;
|
||||
return;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let stream = write_stream.reunite(read_stream).unwrap();
|
||||
let _ = stream.close().await;
|
||||
}
|
||||
|
||||
async fn websocket_close(
|
||||
@ -601,7 +644,6 @@ async fn handle_app_message(
|
||||
} => {
|
||||
let mut path_bindings = path_bindings.write().await;
|
||||
if km.source.process != "homepage:homepage:uqbar" {
|
||||
// TODO ???
|
||||
path = if path.starts_with('/') {
|
||||
format!("/{}{}", km.source.process, path)
|
||||
} else {
|
||||
|
Loading…
Reference in New Issue
Block a user