diff --git a/Cargo.lock b/Cargo.lock index 6808e5a..5ac641b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -391,15 +391,15 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "serde", - "windows-targets 0.48.5", + "windows-targets 0.52.0", ] [[package]] @@ -627,6 +627,15 @@ dependencies = [ "serde", ] +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -689,21 +698,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.1" @@ -850,7 +844,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.11", - "indexmap 2.1.0", + "indexmap 2.2.1", "slab", "tokio", "tokio-util", @@ -869,7 +863,7 @@ dependencies = [ "futures-sink", "futures-util", "http 1.0.0", - "indexmap 2.1.0", + "indexmap 2.2.1", "slab", "tokio", "tokio-util", @@ -1105,6 +1099,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.11", + "hyper 0.14.28", + "rustls", + "tokio", + "tokio-rustls", +] + [[package]] name = "hyper-timeout" version = "0.4.1" @@ -1180,14 +1188,20 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "433de089bd45971eecf4668ee0ee8f4cec17db4f8bd8f7bc3197a6ce37aa7d9b" dependencies = [ "equivalent", "hashbrown 0.14.3", ] +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "itertools" version = "0.10.5" @@ -1411,24 +1425,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c55d0c9dc43dedfd2414deb74ade67687749ef88b1d3482024d4c81d901a7a83" -[[package]] -name = "native-tls" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" -dependencies = [ - "lazy_static", - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nix" version = "0.27.1" @@ -1553,50 +1549,6 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" -[[package]] -name = "openssl" -version = "0.10.62" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671" -dependencies = [ - "bitflags 2.4.2", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.48", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "opentelemetry" version = "0.21.0" @@ -1605,7 +1557,7 @@ checksum = "1e32339a5dc40459130b3bd269e9892439f55b33e772d2a9d402a789baaf4e8a" dependencies = [ "futures-core", "futures-sink", - "indexmap 2.1.0", + "indexmap 2.2.1", "js-sys", "once_cell", "pin-project-lite", @@ -1773,18 +1725,18 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", @@ -1882,9 +1834,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.76" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] @@ -2129,13 +2081,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.2" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.3", + "regex-automata 0.4.5", "regex-syntax 0.8.2", ] @@ -2150,9 +2102,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", @@ -2171,6 +2123,46 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "reqwest" +version = "0.11.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.24", + "http 0.2.11", + "http-body 0.4.6", + "hyper 0.14.28", + "hyper-rustls", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] + [[package]] name = "ring" version = "0.17.7" @@ -2230,6 +2222,37 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.21.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustrict" version = "0.7.12" @@ -2259,15 +2282,6 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" -[[package]] -name = "schannel" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -2275,26 +2289,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "security-framework" -version = "2.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.9.1" +name = "sct" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "core-foundation-sys", - "libc", + "ring", + "untrusted", ] [[package]] @@ -2308,18 +2309,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.195" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.195" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", @@ -2328,9 +2329,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.111" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" dependencies = [ "itoa", "ryu", @@ -2392,9 +2393,9 @@ dependencies = [ [[package]] name = "shuttle-axum" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1037680b94f73044b9205ec27cd6fc8018ca0b8aebb707ef15191965ff3ebd1" +checksum = "b9ea1b505a5b2976647168561bc09224e600d30d7186ff53baa739a6ea61f321" dependencies = [ "axum 0.7.4", "shuttle-runtime", @@ -2402,9 +2403,9 @@ dependencies = [ [[package]] name = "shuttle-codegen" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "032ec76ecb7e3698d7a9e836e3649a88faa13e53f4d13eaa314030761f4ecac9" +checksum = "eb0b7a98b90227032415702ebd7aa920bfffac52704ede68705ab508d7b477da" dependencies = [ "proc-macro-error", "proc-macro2", @@ -2414,9 +2415,9 @@ dependencies = [ [[package]] name = "shuttle-common" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8e3ddec998dd953743ac0e47de7ae6d65fcb1ffc45e68a4ceecd2558f8cad1b" +checksum = "735deac187695b33410cd8cc18ef409db54bcfb27698500eb4624d30f45d7431" dependencies = [ "anyhow", "async-trait", @@ -2436,6 +2437,7 @@ dependencies = [ "opentelemetry-otlp", "opentelemetry_sdk", "pin-project", + "reqwest", "rustrict", "semver", "serde", @@ -2458,9 +2460,9 @@ dependencies = [ [[package]] name = "shuttle-proto" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ff4e554df686fb01ce42636bf768ba902800934ba4e22f5a5d96082f608d325" +checksum = "11950d438e79df346f0fa7f4ef6265310fade58eeb8122d054ed41f8e8c025ff" dependencies = [ "futures-core", "prost 0.12.3", @@ -2471,9 +2473,9 @@ dependencies = [ [[package]] name = "shuttle-runtime" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4e48b0112dc1ba7ae056197810808a27948a7352340506f355da7e4bdc6cb4" +checksum = "21e548acb39c387c657c059729a1a8b0e90176ef675b5c989e71b14711ee0dde" dependencies = [ "anyhow", "async-trait", @@ -2497,23 +2499,24 @@ dependencies = [ [[package]] name = "shuttle-service" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809db2a4b8511b07f363942737459bb4ec4f859d9adac7759c52677beabe9af4" +checksum = "02a13d269e942132d7936fa538f2e68af267f938a507c91b5e40a5b1b6881c04" dependencies = [ "anyhow", "async-trait", "serde", "shuttle-common", + "shuttle-proto", "strfmt", "thiserror", ] [[package]] name = "shuttle-shared-db" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fbf377957afb7c9a5b74b13a897c466252e46f6e78a4ae48b9fe2282baacdd" +checksum = "5d1953714724a537c668eef00b86d47d7b8c44a99048fab1588b2afdd2536037" dependencies = [ "async-trait", "serde", @@ -2669,13 +2672,14 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.1.0", + "indexmap 2.2.1", "log", "memchr", - "native-tls", "once_cell", "paste", "percent-encoding", + "rustls", + "rustls-pemfile", "serde", "serde_json", "sha2", @@ -2688,6 +2692,7 @@ dependencies = [ "tracing", "url", "uuid", + "webpki-roots", ] [[package]] @@ -2938,6 +2943,27 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.9.0" @@ -3067,6 +3093,16 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.14" @@ -3501,6 +3537,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.90" @@ -3530,6 +3578,16 @@ version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" +[[package]] +name = "web-sys" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "0.2.4" @@ -3540,6 +3598,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" + [[package]] name = "whoami" version = "1.4.1" @@ -3709,6 +3773,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/Cargo.toml b/Cargo.toml index ab315b8..a97f4cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,12 +6,12 @@ edition = "2021" [dependencies] askama = { version = "0.12.1", features = ["with-axum"] } askama_axum = "0.4.0" -axum = "0.7.3" +axum = { version = "^0.7", features = ["tokio"] } serde = { version = "1.0.195", features = ["derive"] } serde_json = "1.0.111" -shuttle-axum = "0.36.0" -shuttle-runtime = "0.36.0" -shuttle-shared-db = { version = "0.36.0", features = ["postgres"] } +shuttle-axum = "^0.37" +shuttle-runtime = "^0.37" +shuttle-shared-db = { version = "^0.37", features = ["sqlx", "postgres"] } sqlx = { version = "0.7.3", features = ["macros", "uuid", "time"] } thiserror = "1.0.56" time = { version = "0.3.31", features = ["serde"] } @@ -19,15 +19,9 @@ tokio = "1.28.2" tokio-stream = { version = "0.1.14", features = ["sync"] } tower-http = { version = "0.5.1", features = ["fs"] } uuid = { version = "1.7.0", features = ["serde"] } +pleco = "0.5.0" tracing = "^0.1" tracing-appender = "^0.2" tracing-futures = { version = "^0.2", default-features = false, features = ["std-future"] } -tracing-subscriber = { version = "^0.3", default-features = false, features = ["ansi", "env-filter", "fmt", "local-time", "time", "tracing"] } -pleco = "0.5.0" - - -# TODO: add prop tests for database types -# [dev-dependencies] -# proptest = "1.4.0" -# proptest-derive = "0.4.0" +tracing-subscriber = { version = "^0.3", default-features = false, features = ["ansi", "env-filter", "fmt", "local-time", "time", "tracing"] } \ No newline at end of file diff --git a/src/api/games/make_move.rs b/src/api/games/make_move.rs index ab33c97..7f531f4 100644 --- a/src/api/games/make_move.rs +++ b/src/api/games/make_move.rs @@ -6,10 +6,12 @@ use axum::{ }; use sqlx::types::Uuid; +use crate::api::models::ApiGameBoard; +use crate::api::templates::GameBoardTemplate; use crate::database::models::{Game, GameBoard, GameError}; use crate::AppState; -use super::watch_game_sse::{GameUpdate, GameUpdateStream}; +use super::watch_game_sse::GameUpdateStream; #[derive(serde::Deserialize, Debug)] pub struct MakeMoveRequest { @@ -34,9 +36,11 @@ pub async fn handler( // Returns the updated board if the move was valid. Otherwise, returns the latest board. GameBoard::make_move(&mut conn, game_id, &uci_move, resign).await?; + // Wow this really sucks, the client should just read this again + let api_game_board = ApiGameBoard::from(GameBoard::latest(&mut conn, game_id).await?); conn.commit().await?; - if tx.send(GameUpdate).is_err() { + if tx.send(GameBoardTemplate { api_game_board }).is_err() { tracing::warn!("failed to send game update: game_id={}", game_id); } diff --git a/src/api/games/mod.rs b/src/api/games/mod.rs index 22d670b..4c7d183 100644 --- a/src/api/games/mod.rs +++ b/src/api/games/mod.rs @@ -2,5 +2,4 @@ pub mod create_game; pub mod make_move; pub mod read_all_games; pub mod read_game; -pub mod read_game_board; pub mod watch_game_sse; diff --git a/src/api/games/read_game.rs b/src/api/games/read_game.rs index 771ecf4..3187136 100644 --- a/src/api/games/read_game.rs +++ b/src/api/games/read_game.rs @@ -1,4 +1,3 @@ -use askama::Template; use axum::{ extract::{Path, State}, response::{IntoResponse, Response}, @@ -6,6 +5,7 @@ use axum::{ use sqlx::types::Uuid; use crate::api::models::ApiGameBoard; +use crate::api::templates::GameIndexTemplate; use crate::database::models::{Game, GameBoard, GameError}; use crate::AppState; @@ -21,14 +21,8 @@ pub async fn handler( let game_board = GameBoard::latest(&mut conn, game_id).await?; let api_game_board = ApiGameBoard::from(game_board); - - Ok(TemplateApiGameBoard { api_game_board }) -} - -#[derive(Template)] -#[template(path = "game_index.html")] -struct TemplateApiGameBoard { - api_game_board: ApiGameBoard, + + Ok(GameIndexTemplate { api_game_board }) } #[derive(Debug, thiserror::Error)] diff --git a/src/api/games/watch_game_sse.rs b/src/api/games/watch_game_sse.rs index 1ea010c..525a4fa 100644 --- a/src/api/games/watch_game_sse.rs +++ b/src/api/games/watch_game_sse.rs @@ -1,22 +1,21 @@ use std::convert::Infallible; use std::time::Duration; +use askama::Template; use axum::{ extract::Path, response::{sse::Event, Sse}, Extension, }; -use serde::Serialize; use sqlx::types::Uuid; use tokio::sync::broadcast::Sender; use tokio_stream::wrappers::BroadcastStream; use tokio_stream::{Stream, StreamExt as _}; -pub type GameUpdateStream = Sender; - -#[derive(Clone, Serialize, Debug)] -pub struct GameUpdate; +use crate::api::templates::GameBoardTemplate; +// TODO: generalize and use the read_game_board handler +pub type GameUpdateStream = Sender; pub async fn handler( Path(game_id): Path, Extension(tx): Extension, @@ -28,7 +27,12 @@ pub async fn handler( // Catch all updata events for this game Sse::new( stream - .map(move |_| Event::default().event(format!("game-update-{}", game_id))) + .map(move |tagb| { + let tagb = tagb.unwrap(); + Event::default() + .event(format!("game-update-{}", game_id)) + .data(tagb.render().unwrap()) + }) .map(Ok), ) .keep_alive( diff --git a/src/api/mod.rs b/src/api/mod.rs index 608fd61..44a11f2 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,2 +1,3 @@ pub mod games; pub mod models; +pub mod templates; diff --git a/src/api/models/api_game_board.rs b/src/api/models/api_game_board.rs index ebf64ea..5b0f1c5 100644 --- a/src/api/models/api_game_board.rs +++ b/src/api/models/api_game_board.rs @@ -8,6 +8,7 @@ use crate::database::models::GameStatus; use crate::database::models::GameWinner; use crate::database::types::DatabaseBoard as Board; +#[derive(Clone)] pub struct ApiGameBoard { pub game_id: String, pub board: Board, diff --git a/src/api/templates/game_board.rs b/src/api/templates/game_board.rs new file mode 100644 index 0000000..ce215cf --- /dev/null +++ b/src/api/templates/game_board.rs @@ -0,0 +1,9 @@ +use askama::Template; + +use crate::api::models::ApiGameBoard; + +#[derive(Template, Clone)] +#[template(path = "game_board.html")] +pub struct GameBoardTemplate { + pub api_game_board: ApiGameBoard, +} diff --git a/src/api/templates/game_index.rs b/src/api/templates/game_index.rs new file mode 100644 index 0000000..5a8ba10 --- /dev/null +++ b/src/api/templates/game_index.rs @@ -0,0 +1,9 @@ +use askama::Template; + +use crate::api::models::ApiGameBoard; + +#[derive(Template)] +#[template(path = "game_index.html")] +pub struct GameIndexTemplate { + pub api_game_board: ApiGameBoard, +} diff --git a/src/api/templates/mod.rs b/src/api/templates/mod.rs new file mode 100644 index 0000000..2d7e2db --- /dev/null +++ b/src/api/templates/mod.rs @@ -0,0 +1,5 @@ +mod game_board; +mod game_index; + +pub use game_board::GameBoardTemplate; +pub use game_index::GameIndexTemplate; diff --git a/src/main.rs b/src/main.rs index 5395b5e..6adba57 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,8 @@ use tower_http::services::ServeDir; mod api; mod database; +use api::templates::GameBoardTemplate; + #[derive(Clone)] pub struct AppState { database: PgPool, @@ -37,7 +39,7 @@ async fn main( .expect("Looks like something went wrong with migrations :("); // Setup State let state = AppState::new(db); - let (tx, _rx) = channel::(10); + let (tx, _rx) = channel::(10); // Register panics as they happen register_panic_logger(); @@ -59,10 +61,6 @@ async fn main( "/games/:game_id/sse", get(api::games::watch_game_sse::handler), ) - .route( - "/games/:game_id/board", - get(api::games::read_game_board::handler), - ) .with_state(state) .layer(Extension(tx)) // Static assets diff --git a/static/js/board.js b/static/js/board.js index 978079e..738b12e 100644 --- a/static/js/board.js +++ b/static/js/board.js @@ -1,37 +1,104 @@ -document.addEventListener("DOMContentLoaded", function() { - let selectedPiece = null; - let fromSquare = null; - let toSquare = null; +// WOOO globabl state +let selectedPiece = null; +let fromSquare = null; +let toSquare = null; - // Add listeners to all squares +// (Overly) Simple function to check if a square has a piece +function squareHasPiece(square) { + return square.innerHTML !== ''; +} + +// Logic for moving a piece +function movePiece(fromSquare, toSquare) { + // Get the identifying class name (e.g. `chess-piece-P` or `chess-piece-p`) of the piece + let fromPieceClass = fromSquare.classList[1]; + let fromPiece = fromPieceClass.split('-')[2]; + + // Get the relevant squares + let fromPosition = fromSquare.getAttribute('id'); + let toPosition = toSquare.getAttribute('id'); + let toRank = toPosition[1]; + + // Determine the uci formatted move + let promotionHtml = null; + let promotionClass = null; + uciMove = `${fromPosition}${toPosition}`; + // Check if a pawn is being promoted + if ((fromPiece === 'P' && toRank === '8') || (fromPiece === 'p' && toRank === '1')) { + // TODO: allow user to select piece to promote to piece of their choice + uciMove += 'q'; // Promote to queen + if (fromPiece === 'P') { + promotionHtml = '♕'; + promotionClass = 'chess-piece-Q'; + } else { + promotionHtml = '♛'; + promotionClass = 'chess-piece-q'; + } + } + + // Update the board + let toPieceHtml = promotionHtml ?? fromSquare.innerHTML; + let toPieceClass = promotionClass ?? fromPieceClass; + toSquare.innerHTML = toPieceHtml; // Add the piece to the new square + if (toSquare.classList.length === 1) { + toSquare.classList.add(toPieceClass); + } else { + toSquare.classList.replace(toSquare.classList[1], toPieceClass); // Update the class of the new square + } + fromSquare.innerHTML = ''; // Remove the piece from the current square + sendMove(uciMove); +} + +function sendMove(uciMove) { + console.log(uciMove); + // Write our move to the hidden input field + document.getElementById('uciMoveInput').value = uciMove; + // Make the button visible + document.getElementById('moveForm').style.display = 'block'; +} + +initBoard = function() { + selectedPiece = null; + fromSquare = null; + toSquare = null; + + // Remove all event listeners document.querySelectorAll('[class*="chess-square-"]').forEach(square => { - // On click - square.addEventListener('click', function() { - if (!selectedPiece && squareHasPiece(this) ) { - // Select the piece - selectedPiece = this; - fromSquare = this; - this.classList.add('selected'); - } else if (selectedPiece) { - if (this === selectedPiece) { - // Deselect the piece - this.classList.remove('selected'); - selectedPiece = null; - return; - } + square.replaceWith(square.cloneNode(true)); + }); - toSquare = this; - // Move the piece to the new square - movePiece(selectedPiece, this); - selectedPiece.classList.remove('selected'); + document.getElementById('moveForm').style.display = 'none'; + document.getElementById('uciMoveInput').value = ''; + + // Assuming 'chessboard' is the ID of the parent element + const chessboard = document.getElementById('chessboard'); + chessboard.addEventListener('click', function(event) { + // Check if the clicked element is a chess square + const clickedSquare = event.target.closest('[class*="chess-square-"]'); + if (!clickedSquare) return; // Not a chess square, ignore the click + if (!selectedPiece && squareHasPiece(clickedSquare)) { + // Select the piece + selectedPiece = clickedSquare; + fromSquare = clickedSquare; + clickedSquare.classList.add('selected'); + } else if (selectedPiece) { + if (clickedSquare === selectedPiece) { + // Deselect the piece + clickedSquare.classList.remove('selected'); selectedPiece = null; + return; + } - }); + + toSquare = clickedSquare; + // Move the piece to the new square + movePiece(selectedPiece, clickedSquare); + selectedPiece.classList.remove('selected'); + selectedPiece = null; + } }); - // TODO: make this work -- so that we can reset from bad moves document.body.addEventListener('htmx:responseError', function(event) { - console.log(event); // Check if the event is for the element you're interested in if (event.target.id === 'submitMove') { // Swap the pieces back @@ -40,58 +107,14 @@ document.addEventListener("DOMContentLoaded", function() { toSquare.innerHTML = fromSqaureHtml; } }); +} - // (Overly) Simple function to check if a square has a piece - function squareHasPiece(square) { - return square.innerHTML !== ''; - } - - // Logic for moving a piece - function movePiece(fromSquare, toSquare) { - // Get the identifying class name (e.g. `chess-piece-P` or `chess-piece-p`) of the piece - let fromPieceClass = fromSquare.classList[1]; - let fromPiece = fromPieceClass.split('-')[2]; - - // Get the relevant squares - let fromPosition = fromSquare.getAttribute('id'); - let toPosition = toSquare.getAttribute('id'); - let toRank = toPosition[1]; - - // Determine the uci formatted move - let promotionHtml = null; - let promotionClass = null; - uciMove = `${fromPosition}${toPosition}`; - // Check if a pawn is being promoted - if ((fromPiece === 'P' && toRank === '8') || (fromPiece === 'p' && toRank === '1')) { - // TODO: allow user to select piece to promote to piece of their choice - uciMove += 'q'; // Promote to queen - if (fromPiece === 'P') { - promotionHtml = '♕'; - promotionClass = 'chess-piece-Q'; - } else { - promotionHtml = '♛'; - promotionClass = 'chess-piece-q'; - } - } - - // Update the board - let toPieceHtml = promotionHtml ?? fromSquare.innerHTML; - let toPieceClass = promotionClass ?? fromPieceClass; - toSquare.innerHTML = toPieceHtml; // Add the piece to the new square - if (toSquare.classList.length === 1) { - toSquare.classList.add(toPieceClass); - } else { - toSquare.classList.replace(toSquare.classList[1], toPieceClass); // Update the class of the new square - } - fromSquare.innerHTML = ''; // Remove the piece from the current square - sendMove(uciMove); - } - - function sendMove(uciMove) { - console.log(uciMove); - // Write our move to the hidden input field - document.getElementById('uciMoveInput').value = uciMove; - // Make the button visible - document.getElementById('moveForm').style.display = 'block'; - } +document.addEventListener('DOMContentLoaded', function() { + initBoard(); }); + +document.body.addEventListener('htmx:afterSwap', function(event) { + selectedPiece = null; + fromSquare = null; + toSquare = null; +}) \ No newline at end of file diff --git a/static/js/sse.js b/static/js/sse.js new file mode 100644 index 0000000..4aabc93 --- /dev/null +++ b/static/js/sse.js @@ -0,0 +1,306 @@ +/* +Copied from: https://raw.githubusercontent.com/bigskysoftware/htmx-extensions/main/ext/sse.js + +Server Sent Events Extension +============================ +This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions. + +*/ + +(function() { + /** @type {import("../htmx").HtmxInternalApi} */ + var api + + htmx.defineExtension('sse', { + + /** + * Init saves the provided reference to the internal HTMX API. + * + * @param {import("../htmx").HtmxInternalApi} api + * @returns void + */ + init: function(apiRef) { + // store a reference to the internal API. + api = apiRef + + // set a function in the public API for creating new EventSource objects + if (htmx.createEventSource == undefined) { + htmx.createEventSource = createEventSource + } + }, + + /** + * onEvent handles all events passed to this extension. + * + * @param {string} name + * @param {Event} evt + * @returns void + */ + onEvent: function(name, evt) { + switch (name) { + case 'htmx:beforeCleanupElement': + var internalData = api.getInternalData(evt.target) + // Try to remove remove an EventSource when elements are removed + if (internalData.sseEventSource) { + internalData.sseEventSource.close() + } + + return + + // Try to create EventSources when elements are processed + case 'htmx:afterProcessNode': + ensureEventSourceOnElement(evt.target) + registerSSE(evt.target) + } + } + }) + + /// //////////////////////////////////////////// + // HELPER FUNCTIONS + /// //////////////////////////////////////////// + + /** + * createEventSource is the default method for creating new EventSource objects. + * it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed. + * + * @param {string} url + * @returns EventSource + */ + function createEventSource(url) { + return new EventSource(url, { withCredentials: true }) + } + + /** + * registerSSE looks for attributes that can contain sse events, right + * now hx-trigger and sse-swap and adds listeners based on these attributes too + * the closest event source + * + * @param {HTMLElement} elt + */ + function registerSSE(elt) { + // Find closest existing event source + var sourceElement = api.getClosestMatch(elt, hasEventSource) + if (sourceElement == null) { + // api.triggerErrorEvent(elt, "htmx:noSSESourceError") + return null // no eventsource in parentage, orphaned element + } + + // Set internalData and source + var internalData = api.getInternalData(sourceElement) + var source = internalData.sseEventSource + + // Add message handlers for every `sse-swap` attribute + queryAttributeOnThisOrChildren(elt, 'sse-swap').forEach(function(child) { + var sseSwapAttr = api.getAttributeValue(child, 'sse-swap') + var sseEventNames = sseSwapAttr.split(',') + + for (var i = 0; i < sseEventNames.length; i++) { + var sseEventName = sseEventNames[i].trim() + var listener = function(event) { + // If the source is missing then close SSE + if (maybeCloseSSESource(sourceElement)) { + return + } + + // If the body no longer contains the element, remove the listener + if (!api.bodyContains(child)) { + source.removeEventListener(sseEventName, listener) + } + + // swap the response into the DOM and trigger a notification + if(!api.triggerEvent(elt, 'htmx:sseBeforeMessage', event)) { + return + } + swap(child, event.data) + api.triggerEvent(elt, 'htmx:sseMessage', event) + } + + // Register the new listener + api.getInternalData(child).sseEventListener = listener + source.addEventListener(sseEventName, listener) + } + }) + + // Add message handlers for every `hx-trigger="sse:*"` attribute + queryAttributeOnThisOrChildren(elt, 'hx-trigger').forEach(function(child) { + var sseEventName = api.getAttributeValue(child, 'hx-trigger') + if (sseEventName == null) { + return + } + + // Only process hx-triggers for events with the "sse:" prefix + if (sseEventName.slice(0, 4) != 'sse:') { + return + } + + var listener = function(event) { + if (maybeCloseSSESource(sourceElement)) { + return + } + + if (!api.bodyContains(child)) { + source.removeEventListener(sseEventName, listener) + } + + // Trigger events to be handled by the rest of htmx + htmx.trigger(child, sseEventName, event) + htmx.trigger(child, 'htmx:sseMessage', event) + } + + // Register the new listener + api.getInternalData(elt).sseEventListener = listener + source.addEventListener(sseEventName.slice(4), listener) + }) + } + + /** + * ensureEventSourceOnElement creates a new EventSource connection on the provided element. + * If a usable EventSource already exists, then it is returned. If not, then a new EventSource + * is created and stored in the element's internalData. + * @param {HTMLElement} elt + * @param {number} retryCount + * @returns {EventSource | null} + */ + function ensureEventSourceOnElement(elt, retryCount) { + if (elt == null) { + return null + } + + // handle extension source creation attribute + queryAttributeOnThisOrChildren(elt, 'sse-connect').forEach(function(child) { + var sseURL = api.getAttributeValue(child, 'sse-connect') + if (sseURL == null) { + return + } + + ensureEventSource(child, sseURL, retryCount) + }) + } + + function ensureEventSource(elt, url, retryCount) { + var source = htmx.createEventSource(url) + + source.onerror = function(err) { + // Log an error event + api.triggerErrorEvent(elt, 'htmx:sseError', { error: err, source }) + + // If parent no longer exists in the document, then clean up this EventSource + if (maybeCloseSSESource(elt)) { + return + } + + // Otherwise, try to reconnect the EventSource + if (source.readyState === EventSource.CLOSED) { + retryCount = retryCount || 0 + var timeout = Math.random() * (2 ^ retryCount) * 500 + window.setTimeout(function() { + ensureEventSourceOnElement(elt, Math.min(7, retryCount + 1)) + }, timeout) + } + } + + source.onopen = function(evt) { + api.triggerEvent(elt, 'htmx:sseOpen', { source }) + } + + api.getInternalData(elt).sseEventSource = source + } + + /** + * maybeCloseSSESource confirms that the parent element still exists. + * If not, then any associated SSE source is closed and the function returns true. + * + * @param {HTMLElement} elt + * @returns boolean + */ + function maybeCloseSSESource(elt) { + if (!api.bodyContains(elt)) { + var source = api.getInternalData(elt).sseEventSource + if (source != undefined) { + source.close() + // source = null + return true + } + } + return false + } + + /** + * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT. + * + * @param {HTMLElement} elt + * @param {string} attributeName + */ + function queryAttributeOnThisOrChildren(elt, attributeName) { + var result = [] + + // If the parent element also contains the requested attribute, then add it to the results too. + if (api.hasAttribute(elt, attributeName)) { + result.push(elt) + } + + // Search all child nodes that match the requested attribute + elt.querySelectorAll('[' + attributeName + '], [data-' + attributeName + ']').forEach(function(node) { + result.push(node) + }) + + return result + } + + /** + * @param {HTMLElement} elt + * @param {string} content + */ + function swap(elt, content) { + api.withExtensions(elt, function(extension) { + content = extension.transformResponse(content, null, elt) + }) + + var swapSpec = api.getSwapSpecification(elt) + var target = api.getTarget(elt) + var settleInfo = api.makeSettleInfo(elt) + + api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo) + + settleInfo.elts.forEach(function(elt) { + if (elt.classList) { + elt.classList.add(htmx.config.settlingClass) + } + api.triggerEvent(elt, 'htmx:beforeSettle') + }) + + // Handle settle tasks (with delay if requested) + if (swapSpec.settleDelay > 0) { + setTimeout(doSettle(settleInfo), swapSpec.settleDelay) + } else { + doSettle(settleInfo)() + } + } + + /** + * doSettle mirrors much of the functionality in htmx that + * settles elements after their content has been swapped. + * TODO: this should be published by htmx, and not duplicated here + * @param {import("../htmx").HtmxSettleInfo} settleInfo + * @returns () => void + */ + function doSettle(settleInfo) { + return function() { + settleInfo.tasks.forEach(function(task) { + task.call() + }) + + settleInfo.elts.forEach(function(elt) { + if (elt.classList) { + elt.classList.remove(htmx.config.settlingClass) + } + api.triggerEvent(elt, 'htmx:afterSettle') + }) + } + } + + function hasEventSource(node) { + return api.getInternalData(node).sseEventSource != null + } + })() + \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 82895b9..bb7aae4 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,10 +1,9 @@ - - + + + K-Chess {% block head %}{% endblock %} diff --git a/templates/game_board.html b/templates/game_board.html index 013fe89..a30f990 100644 --- a/templates/game_board.html +++ b/templates/game_board.html @@ -1,31 +1,32 @@ - -
- - +
{% let board_html = api_game_board.board_html() %} {% let game_id = api_game_board.game_id() %} - - {% if api_game_board.status() == "complete" %} -

Game over!

-

Winner: {{ api_game_board.winner() }}

-

Outcome: {{ api_game_board.outcome() }}

- {% else %} -

Turn: {{ api_game_board.turn() }}

- {% endif %} - {{ board_html|safe }} + {% if api_game_board.status() == "complete" %} +

Game over!

+

Winner: {{ api_game_board.winner() }}

+

Outcome: {{ api_game_board.outcome() }}

+ {% else %} +

Turn: {{ api_game_board.turn() }}

+ {% endif %} - {% if api_game_board.status() == "active" %} + {{ board_html|safe }} + + + + {% if api_game_board.status() == "active" || api_game_board.status() == "created" %} + {% endif %} + {% if api_game_board.status() == "active" %}
- +
{% endif %} diff --git a/templates/game_index.html b/templates/game_index.html index 174a3f9..04b8ca5 100644 --- a/templates/game_index.html +++ b/templates/game_index.html @@ -2,21 +2,20 @@ {% block content %} -

Board

+{% let game_id = api_game_board.game_id() %} + +

Game {{ game_id }}

- - -{% let game_id = api_game_board.game_id() %} -
- -
- - - {% include "game_board.html" %} -
+ + + + + +
+ {% include "game_board.html" %}
{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 67c39d2..697ab80 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,4 +1,3 @@ - {% extends "base.html" %} {% block content %}