commit 486b6705565a23d25eea46291ce1c28d44b1a159 Author: emilis Date: Wed Oct 29 08:51:52 2025 +0000 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3751403 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +target +target-* +calendar/dist +.vscode +build-and-send.fish +.sqlx diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..469a83e --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3647 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "anymap2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" +dependencies = [ + "axum-core 0.5.5", + "axum-macros", + "base64", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.3.1", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.3.1", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" +dependencies = [ + "axum", + "axum-core 0.5.5", + "bytes", + "futures-util", + "headers", + "http 1.3.1", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde_core", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-limit" +version = "0.1.0-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3faa0b4a7a9fe9c072b3acde27ea8884cdc7a61982b5db9f402679631c2549ab" +dependencies = [ + "async-trait", + "axum-core 0.4.5", + "dashmap", + "http 1.3.1", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "boolinator" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.2.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "chrono-humanize" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799627e6b4d27827a814e837b9d8a504832086081806d45b1afa34dc982b023b" +dependencies = [ + "chrono", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.17", +] + +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "convert_case" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "gloo" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d" +dependencies = [ + "gloo-console 0.2.3", + "gloo-dialogs 0.1.1", + "gloo-events 0.1.2", + "gloo-file 0.2.3", + "gloo-history 0.1.5", + "gloo-net 0.3.1", + "gloo-render 0.1.1", + "gloo-storage 0.2.2", + "gloo-timers 0.2.6", + "gloo-utils 0.1.7", + "gloo-worker 0.2.1", +] + +[[package]] +name = "gloo" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd35526c28cc55c1db77aed6296de58677dbab863b118483a27845631d870249" +dependencies = [ + "gloo-console 0.3.0", + "gloo-dialogs 0.2.0", + "gloo-events 0.2.0", + "gloo-file 0.3.0", + "gloo-history 0.2.2", + "gloo-net 0.4.0", + "gloo-render 0.2.0", + "gloo-storage 0.3.0", + "gloo-timers 0.3.0", + "gloo-utils 0.2.0", + "gloo-worker 0.4.0", +] + +[[package]] +name = "gloo" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372" +dependencies = [ + "gloo-console 0.3.0", + "gloo-dialogs 0.2.0", + "gloo-events 0.2.0", + "gloo-file 0.3.0", + "gloo-history 0.2.2", + "gloo-net 0.5.0", + "gloo-render 0.2.0", + "gloo-storage 0.3.0", + "gloo-timers 0.3.0", + "gloo-utils 0.2.0", + "gloo-worker 0.5.0", +] + +[[package]] +name = "gloo-console" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f" +dependencies = [ + "gloo-utils 0.1.7", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-console" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261" +dependencies = [ + "gloo-utils 0.2.0", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-dialogs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-dialogs" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4748e10122b01435750ff530095b1217cf6546173459448b83913ebe7815df" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-events" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-events" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c26fb45f7c385ba980f5fa87ac677e363949e065a083722697ef1b2cc91e41" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-file" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7" +dependencies = [ + "gloo-events 0.1.2", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-file" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f" +dependencies = [ + "futures-channel", + "gloo-events 0.2.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-history" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85725d90bf0ed47063b3930ef28e863658a7905989e9929a8708aab74a1d5e7f" +dependencies = [ + "gloo-events 0.1.2", + "gloo-utils 0.1.7", + "serde", + "serde-wasm-bindgen 0.5.0", + "serde_urlencoded", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-history" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6" +dependencies = [ + "getrandom 0.2.16", + "gloo-events 0.2.0", + "gloo-utils 0.2.0", + "serde", + "serde-wasm-bindgen 0.6.5", + "serde_urlencoded", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66b4e3c7d9ed8d315fd6b97c8b1f74a7c6ecbbc2320e65ae7ed38b7068cc620" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.1.7", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ac9e8288ae2c632fa9f8657ac70bfe38a1530f345282d7ba66a1f70b72b7dc4" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.2.0", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.2.0", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56008b6744713a8e8d98ac3dcb7d06543d5662358c9c805b4ce2167ad4649833" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480" +dependencies = [ + "gloo-utils 0.1.7", + "js-sys", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" +dependencies = [ + "gloo-utils 0.2.0", + "js-sys", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13471584da78061a28306d1359dd0178d8d6fc1c7c80e5e35d27260346e0516a" +dependencies = [ + "anymap2", + "bincode", + "gloo-console 0.2.3", + "gloo-utils 0.1.7", + "js-sys", + "serde", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76495d3dd87de51da268fa3a593da118ab43eb7f8809e17eb38d3319b424e400" +dependencies = [ + "bincode", + "futures", + "gloo-utils 0.2.0", + "gloo-worker-macros", + "js-sys", + "pinned", + "serde", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085f262d7604911c8150162529cefab3782e91adb20202e8658f7275d2aefe5d" +dependencies = [ + "bincode", + "futures", + "gloo-utils 0.2.0", + "gloo-worker-macros", + "js-sys", + "pinned", + "serde", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-worker-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956caa58d4857bc9941749d55e4bd3000032d8212762586fa5705632967140e7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http 1.3.1", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http 1.3.1", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.3.1", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http 1.3.1", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "implicit-clone" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a9aa791c7b5a71b636b7a68207fdebf171ddfc593d9c8506ec4cbc527b6a84" +dependencies = [ + "implicit-clone-derive", + "indexmap", +] + +[[package]] +name = "implicit-clone-derive" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "699c1b6d335e63d0ba5c1e1c7f647371ce989c3bcbe1f7ed2b85fa56e3bd1a21" +dependencies = [ + "quote", + "syn 2.0.107", +] + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime-sniffer" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b8b2a64cd735f1d5f17ff6701ced3cc3c54851f9448caf454cd9c923d812408" +dependencies = [ + "mime", + "url", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pinned" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a829027bd95e54cfe13e3e258a1ae7b645960553fb82b75ff852c29688ee595b" +dependencies = [ + "futures", + "rustversion", + "thiserror 1.0.69", +] + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plan" +version = "0.1.0" +dependencies = [ + "chrono", + "chrono-humanize", + "ciborium", + "futures", + "gloo 0.11.0", + "instant", + "log", + "once_cell", + "plan-proto", + "postcard", + "serde", + "thiserror 2.0.17", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-logger", + "web-sys", + "yew", + "yew-router", +] + +[[package]] +name = "plan-macros" +version = "0.1.0" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "plan-proto" +version = "0.1.0" +dependencies = [ + "argon2", + "axum", + "axum-extra", + "bytes", + "chrono", + "ciborium", + "log", + "serde", + "sqlx", + "thiserror 2.0.17", + "uuid", +] + +[[package]] +name = "plan-server" +version = "0.1.0" +dependencies = [ + "anyhow", + "argon2", + "axum", + "axum-extra", + "axum-limit", + "chrono", + "ciborium", + "colored", + "futures", + "log", + "mime-sniffer", + "plan-macros", + "plan-proto", + "pretty_env_logger", + "rand 0.9.2", + "serde", + "serde_json", + "sqlx", + "thiserror 2.0.17", + "tokio", + "tower", + "tower-http", + "uuid", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "heapless", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_env_logger" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.107", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prokio" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b55e106e5791fa5a13abd13c85d6127312e8e09098059ca2bc9b03ca4cf488" +dependencies = [ + "futures", + "gloo 0.8.1", + "num_cpus", + "once_cell", + "pin-project", + "pinned", + "tokio", + "tokio-stream", + "wasm-bindgen-futures", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "route-recognizer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.107", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.107", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.17", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.17", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +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 = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tokio-stream", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "http 1.3.1", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http 1.3.1", + "httparse", + "log", + "rand 0.9.2", + "sha1", + "thiserror 2.0.17", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.107", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-logger" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074649a66bb306c8f2068c9016395fa65d8e08d2affcbf95acf3c24c3ab19718" +dependencies = [ + "log", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yew" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f1a03f255c70c7aa3e9c62e15292f142ede0564123543c1cc0c7a4f31660cac" +dependencies = [ + "console_error_panic_hook", + "futures", + "gloo 0.10.0", + "implicit-clone", + "indexmap", + "js-sys", + "prokio", + "rustversion", + "serde", + "slab", + "thiserror 1.0.69", + "tokio", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "yew-macro", +] + +[[package]] +name = "yew-macro" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fd8ca5166d69e59f796500a2ce432ff751edecbbb308ca59fd3fe4d0343de2" +dependencies = [ + "boolinator", + "once_cell", + "prettyplease", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "yew-router" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca1d5052c96e6762b4d6209a8aded597758d442e6c479995faf0c7b5538e0c6" +dependencies = [ + "gloo 0.10.0", + "js-sys", + "route-recognizer", + "serde", + "serde_urlencoded", + "tracing", + "urlencoding", + "wasm-bindgen", + "web-sys", + "yew", + "yew-router-macro", +] + +[[package]] +name = "yew-router-macro" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42bfd190a07ca8cfde7cd4c52b3ac463803dc07323db8c34daa697e86365978c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2e11071 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +resolver = "3" +members = ["plan", "plan-proto", "plan-server", "plan-macros"] diff --git a/migrations/1_init.sql b/migrations/1_init.sql new file mode 100644 index 0000000..1166cae --- /dev/null +++ b/migrations/1_init.sql @@ -0,0 +1,113 @@ +drop table if exists users cascade; +create table users ( + id uuid not null default gen_random_uuid() primary key, + name text, + username text not null, + password_hash text not null, + + created_at timestamp with time zone not null, + updated_at timestamp with time zone not null, + + check (created_at <= updated_at) +); +drop index if exists users_username_idx; +create index users_username_idx on users (username); +drop index if exists users_username_unique; +create unique index users_username_unique on users (lower(username)); + +drop table if exists plans cascade; +create table plans ( + id uuid not null default gen_random_uuid() primary key, + title text, + created_by uuid not null references users(id), + start_time timestamp with time zone not null, + + created_at timestamp with time zone not null, + updated_at timestamp with time zone not null, + + check (created_at <= updated_at) +); + +drop type if exists half_hour cascade; +create type half_hour as enum ( + 'Hour0Min0', + 'Hour0Min30', + 'Hour1Min0', + 'Hour1Min30', + 'Hour2Min0', + 'Hour2Min30', + 'Hour3Min0', + 'Hour3Min30', + 'Hour4Min0', + 'Hour4Min30', + 'Hour5Min0', + 'Hour5Min30', + 'Hour6Min0', + 'Hour6Min30', + 'Hour7Min0', + 'Hour7Min30', + 'Hour8Min0', + 'Hour8Min30', + 'Hour9Min0', + 'Hour9Min30', + 'Hour10Min0', + 'Hour10Min30', + 'Hour11Min0', + 'Hour11Min30', + 'Hour12Min0', + 'Hour12Min30', + 'Hour13Min0', + 'Hour13Min30', + 'Hour14Min0', + 'Hour14Min30', + 'Hour15Min0', + 'Hour15Min30', + 'Hour16Min0', + 'Hour16Min30', + 'Hour17Min0', + 'Hour17Min30', + 'Hour18Min0', + 'Hour18Min30', + 'Hour19Min0', + 'Hour19Min30', + 'Hour20Min0', + 'Hour20Min30', + 'Hour21Min0', + 'Hour21Min30', + 'Hour22Min0', + 'Hour22Min30', + 'Hour23Min0', + 'Hour23Min30' +); + +drop table if exists plan_days cascade; +create table plan_days ( + id uuid not null default gen_random_uuid() primary key, + plan_id uuid not null references plans(id), + day_offset smallint not null, + day_start half_hour not null, + day_end half_hour not null, + + check (day_offset >= 0), + unique(plan_id, day_offset) +); + +drop table if exists day_tiles cascade; +create table day_tiles ( + day_id uuid not null references plan_days(id), + user_id uuid not null references users(id), + tiles half_hour[] not null, + updated_at timestamp with time zone not null default now(), + + unique(day_id, user_id) +); + +drop table if exists login_tokens cascade; +create table login_tokens ( + token text not null primary key, + user_id uuid not null references users(id), + created_at timestamp with time zone not null, + expires_at timestamp with time zone not null, + + check (created_at < expires_at) +); diff --git a/pkg/plan.service b/pkg/plan.service new file mode 100644 index 0000000..76cbe32 --- /dev/null +++ b/pkg/plan.service @@ -0,0 +1,19 @@ +[Unit] +Description=group plan +After=network.target + +[Service] +Type=simple + +User=plan +Group=plan + +WorkingDirectory=/home/plan +Environment=RUST_LOG=info +Environment=PORT=3028 +ExecStart=/home/plan/plan-server + +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/plan-macros/Cargo.toml b/plan-macros/Cargo.toml new file mode 100644 index 0000000..04d5ef5 --- /dev/null +++ b/plan-macros/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "plan-macros" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["full", "extra-traits"] } +convert_case = { version = "0.8" } diff --git a/plan-macros/src/hashlist.rs b/plan-macros/src/hashlist.rs new file mode 100644 index 0000000..7a0c72a --- /dev/null +++ b/plan-macros/src/hashlist.rs @@ -0,0 +1,36 @@ +use std::collections::HashMap; +use std::hash::Hash; + +pub struct HashList(HashMap>); + +impl core::fmt::Debug for HashList { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("HashList").field(&self.0).finish() + } +} + +impl Clone for HashList { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl HashList { + pub fn new() -> Self { + Self(HashMap::new()) + } + + pub fn add(&mut self, key: K, value: V) { + match self.0.get_mut(&key) { + Some(values) => values.push(value), + None => { + self.0.insert(key, vec![value]); + } + } + } + pub fn decompose(&mut self) -> Box<[(K, Vec)]> { + let mut body = HashMap::new(); + core::mem::swap(&mut self.0, &mut body); + body.into_iter().collect() + } +} diff --git a/plan-macros/src/lib.rs b/plan-macros/src/lib.rs new file mode 100644 index 0000000..9d1ce82 --- /dev/null +++ b/plan-macros/src/lib.rs @@ -0,0 +1,491 @@ +use core::error::Error; +use std::{ + io, + path::{Path, PathBuf}, +}; + +use convert_case::Casing; +use proc_macro2::Span; +use quote::{ToTokens, quote}; +use syn::{parse::Parse, parse_macro_input}; + +pub(crate) mod hashlist; +mod targets; + +struct IncludePath { + dir_path: PathBuf, + modules: Vec, +} + +fn display_err(span: Span, err: impl Error) -> syn::Error { + syn::Error::new(span, err.to_string().as_str()) +} + +fn read_modules(span: Span, path: &Path) -> syn::Result> { + let read_dir = std::fs::read_dir(path).map_err(|err| display_err(span, err))?; + + Ok(read_dir + .map(|file| file.map_err(|err| display_err(span, err))) + .collect::, _>>()? + .into_iter() + .filter(|file| file.file_type().map(|ft| ft.is_file()).unwrap_or_default()) + .filter_map(|file| { + file.file_name() + .to_string_lossy() + .strip_suffix(".rs") + .map(|f| f.to_string()) + }) + .map(|module_name| syn::Ident::new(&module_name, span)) + .collect::>()) +} + +impl Parse for IncludePath { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let directory = input.parse::()?; + + let dir_path = std::env::current_dir() + .unwrap() + .join(directory.value()) + .to_path_buf(); + + let modules = read_modules(directory.span(), &dir_path)?; + + Ok(Self { modules, dir_path }) + } +} +impl IncludePath { + fn migrations_tokens(&self) -> proc_macro2::TokenStream { + let dir = std::fs::read_dir(&self.dir_path).unwrap(); + let mut files = dir + .into_iter() + .filter_map(|d| { + d.ok().and_then(|d| { + d.file_name() + .to_string_lossy() + .ends_with(".sql") + .then_some(d.path()) + }) + }) + .map(|path| { + let timestamp = path + .file_name() + .unwrap() + .to_string_lossy() + .split_once('.') + .and_then(|c| c.0.parse::().ok()); + match timestamp { + Some(ts) => Ok((ts, path)), + None => Err(syn::Error::new( + Span::call_site(), + format!("migration at [{path:?}] doesn't have a valid timestamp").as_str(), + )), + } + }) + .collect::, syn::Error>>() + .unwrap(); + files.sort_by_key(|f| f.0); + let migrations_len = files.len(); + let includes = files.into_iter().map(|(_, path)| { + let path = path.to_str().unwrap(); + quote! { + include_str!(#path), + } + }); + quote! { + const MIGRATIONS: [&'static str, #migrations_len] = [ + #(#includes)* + ]; + } + } + fn posts_tokens(&self) -> proc_macro2::TokenStream { + let mod_decl = self.modules.iter().map(|module| { + let path = self.dir_path.join(format!("{module}.rs")); + let path = path.to_str().unwrap(); + quote! { + mod #module { + include!(#path); + }} + }); + let posts = self.modules.iter().map(|module| { + quote! { + #module::POST.into_post() + } + }); + + quote! { + #(#mod_decl)* + + pub const POSTS: &[Post] = &[#(#posts),*]; + } + } + fn normal_tokens(&self) -> proc_macro2::TokenStream { + let mod_decl = self.modules.iter().map(|module| { + quote! {mod #module;} + }); + let pub_use = self.modules.iter().map(|module| { + quote! { + #module::* + } + }); + + quote! { + #(#mod_decl)* + pub use {#(#pub_use),*}; + } + } +} +enum IncludeOutput { + Normal(IncludePath), + Posts(IncludePath), + Migrations(IncludePath), +} + +impl ToTokens for IncludeOutput { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.extend(match self { + IncludeOutput::Normal(include_path) => include_path.normal_tokens(), + IncludeOutput::Posts(include_path) => include_path.posts_tokens(), + IncludeOutput::Migrations(include_path) => include_path.migrations_tokens(), + }); + } +} + +struct FileWithPath { + // bytes: Vec, + include: proc_macro2::TokenStream, + rel_path: String, +} +impl FileWithPath { + fn from_path(path: impl AsRef, origin_path: impl AsRef) -> Result { + let abs_path = path + .as_ref() + .canonicalize() + .ok() + .and_then(|p| p.to_str().map(|t| t.to_string())) + .ok_or(io::Error::new(io::ErrorKind::InvalidData, "invalid path"))?; + let include = quote! { + include_bytes!(#abs_path) + }; + let origin_abs = origin_path.as_ref().canonicalize().and_then(|p| { + p.to_str() + .ok_or(io::Error::new(io::ErrorKind::InvalidData, "invalid path")) + .map(|t| t.to_string()) + })?; + let rel_path = abs_path.replace(&origin_abs, ""); + + Ok(Self { include, rel_path }) + } +} + +fn find_matching_files_recursive( + p: P, + include_in_rerun: F, + directory_filter: D, + out: &mut Vec, + origin_path: &PathBuf, +) -> Result<(), io::Error> +where + P: AsRef, + F: Fn(&str) -> bool + Copy, + D: Fn(&str) -> bool + Copy, +{ + for item in std::fs::read_dir(p.as_ref())? { + let item = item?; + if item.file_type()?.is_dir() { + if directory_filter(item.file_name().to_str().ok_or(io::Error::new( + io::ErrorKind::InvalidData, + "file has no name", + ))?) { + find_matching_files_recursive( + item.path(), + include_in_rerun, + directory_filter, + out, + origin_path, + )?; + } + continue; + } + if let Some(file_name) = item.file_name().to_str() + && include_in_rerun(file_name) + { + out.push(FileWithPath::from_path(item.path(), origin_path)?); + } + } + + Ok(()) +} + +struct IncludeDist { + name: syn::Ident, + files: Vec, +} + +impl IncludeDist { + fn read_dist_path( + name: syn::Ident, + path_span: Span, + path: &Path, + origin_path: &PathBuf, + ) -> syn::Result { + let mut files = Vec::new(); + find_matching_files_recursive(path, |_| true, |_| true, &mut files, origin_path) + .map_err(|err| syn::Error::new(path_span, err.to_string()))?; + Ok(Self { name, files }) + } +} + +impl Parse for IncludeDist { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let name = input.parse::()?; + input.parse::()?; + let directory = input.parse::()?; + + let dir_path = std::env::current_dir() + .unwrap() + .join(directory.value()) + .to_path_buf(); + + Self::read_dist_path(name, directory.span(), &dir_path, &dir_path) + } +} + +impl ToTokens for IncludeDist { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + // std::collections::HashMap + let array_contents = self.files.iter().map(|file| { + let FileWithPath { include, rel_path } = file; + + quote! { + (#rel_path, #include) + } + }); + let name = &self.name; + tokens.extend(quote! { + pub const #name: &[(&'static str, &'static [u8])] = &[#(#array_contents),*]; + }); + } +} + +struct StaticLinks { + collect_into_const: Option, + relative_to: PathBuf, + files: Vec<(String, PathBuf)>, +} + +impl Parse for StaticLinks { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let collect_into_const = if input.peek(syn::Ident) { + let out = Some(input.parse()?); + input.parse::]>()?; + out + } else { + None + }; + const EXPECTED: &str = "expected 'relative to'"; + let directory = input.parse::()?; + { + let rel = input + .parse::() + .map_err(|err| syn::Error::new(err.span(), EXPECTED))?; + if rel != "relative" { + return Err(syn::Error::new(rel.span(), EXPECTED)); + } + } + { + let to = input + .parse::() + .map_err(|err| syn::Error::new(err.span(), EXPECTED))?; + if to != "to" { + return Err(syn::Error::new(to.span(), EXPECTED)); + } + } + let current_dir = std::env::current_dir().unwrap(); + let relative_to = current_dir + .join(input.parse::()?.value()) + .canonicalize() + .expect("cannonicalize relative to path"); + + let span = directory.span(); + + let path_dir = current_dir + .join(directory.value()) + .canonicalize() + .expect("canonicalize base path") + .to_path_buf(); + + let read_dir = std::fs::read_dir(&path_dir).map_err(|err| display_err(span, err))?; + + let files = read_dir + .map(|file| file.map_err(|err| display_err(span, err))) + .collect::, _>>()? + .into_iter() + .filter(|file| file.file_type().map(|ft| ft.is_file()).unwrap_or_default()) + .map(|file| { + ( + file.file_name().to_str().expect("bad filename").to_string(), + file.path(), + ) + }) + .collect::>(); + Ok(StaticLinks { + collect_into_const, + relative_to, + files, + }) + } +} + +impl ToTokens for StaticLinks { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let base_path = self.relative_to.to_str().expect("base path to_string"); + let ident_with_decl = self.files.iter().map(|(file, path)| { + let file_ident = syn::Ident::new( + file.replace(['.', ' ', '-'], "_").to_uppercase().as_str(), + Span::call_site(), + ); + let filepath = path + .to_str() + .expect("path to string") + .replace(base_path, ""); + let decl = quote! { + pub const #file_ident: &'static str = #filepath; + }; + (file_ident, decl) + }); + let files = ident_with_decl.clone().map(|(_, q)| q); + + tokens.extend(quote! { + #(#files)* + }); + if let Some(collect_into_const) = &self.collect_into_const { + let idents = ident_with_decl.map(|(ident, _)| ident); + tokens.extend(quote! { + pub const #collect_into_const: &[&str] = &[#(#idents),*]; + }); + } + } +} + +impl StaticLinks { + fn into_demand_metadata(self) -> StaticLinksDemandMetadata { + let StaticLinks { + collect_into_const, + relative_to, + files, + } = self; + StaticLinksDemandMetadata { + collect_into_const, + relative_to, + files, + } + } +} + +struct StaticLinksDemandMetadata { + collect_into_const: Option, + relative_to: PathBuf, + files: Vec<(String, PathBuf)>, +} + +impl ToTokens for StaticLinksDemandMetadata { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let base_path = self.relative_to.to_str().expect("base path to_string"); + let ident_with_decl = self.files.iter().map(|(file, path)| { + let file_ident = syn::Ident::new( + file.replace(['.', ' ', '-'], "_").to_uppercase().as_str(), + Span::call_site(), + ); + let filepath = path + .to_str() + .expect("path to string") + .replace(base_path, ""); + let decl = quote! { + pub const #file_ident: &'static str = #filepath; + }; + (file_ident, filepath, decl) + }); + let files = ident_with_decl.clone().map(|(_, _, q)| q); + + tokens.extend(quote! { + #(#files)* + }); + if let Some(collect_into_const) = &self.collect_into_const { + let idents = ident_with_decl.clone().map(|(ident, _, _)| ident); + tokens.extend(quote! { + pub const #collect_into_const: &[&str] = &[#(#idents),*]; + }); + } + + let (paths_and_snake_idents, members): (Vec<_>, Vec<_>) = ident_with_decl + .clone() + .map(|(i, filepath, _)| { + let snake_ident = syn::Ident::new( + i.to_string().to_case(convert_case::Case::Snake).as_str(), + i.span(), + ); + let member = quote! { + pub #snake_ident: Metadata, + }; + ((filepath, snake_ident), member) + }) + .unzip(); + + let find_by_key = paths_and_snake_idents.iter().map(|(path, ident)| { + quote! { + #path => &self.#ident + } + }); + + tokens.extend(quote! { + pub struct MetadataMap { + #(#members)* + } + + impl MetadataMap { + pub fn find_by_key(&self, key: &str) -> Option<&Metadata> { + Some(match key { + #(#find_by_key,)* + _ => return None, + }) + } + } + }); + } +} + +#[proc_macro] +pub fn include_dist(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let incl_dist = parse_macro_input!(input as IncludeDist); + quote! {#incl_dist}.into() +} + +#[proc_macro] +pub fn posts(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let pubuse = IncludeOutput::Posts(parse_macro_input!(input as IncludePath)); + quote! {#pubuse}.into() +} + +#[proc_macro] +pub fn include_path(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let incl_path = IncludeOutput::Normal(parse_macro_input!(input as IncludePath)); + quote! {#incl_path}.into() +} + +#[proc_macro] +pub fn static_links(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let static_links = parse_macro_input!(input as StaticLinks); + quote! {#static_links}.into() +} + +#[proc_macro] +pub fn static_links_demand_metadata(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let demand_metadata = parse_macro_input!(input as StaticLinks).into_demand_metadata(); + quote! {#demand_metadata}.into() +} + +#[proc_macro] +pub fn migrations(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let incl_path = IncludeOutput::Migrations(parse_macro_input!(input as IncludePath)); + quote! {#incl_path}.into() +} diff --git a/plan-macros/src/targets.rs b/plan-macros/src/targets.rs new file mode 100644 index 0000000..7d53820 --- /dev/null +++ b/plan-macros/src/targets.rs @@ -0,0 +1,294 @@ +use convert_case::{Case, Casing}; +use proc_macro2::TokenStream; +use quote::{ToTokens, quote, quote_spanned}; +use syn::spanned::Spanned; + +use crate::hashlist::HashList; + +const TARGETS_PATH: &str = "extract"; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum VariantFieldIndex { + Ident(syn::Ident), + Numeric(syn::LitInt), + None, +} + +impl syn::parse::Parse for VariantFieldIndex { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + if input.is_empty() { + Ok(VariantFieldIndex::None) + } else if input.peek(syn::LitInt) { + Ok(VariantFieldIndex::Numeric(input.parse()?)) + } else { + Ok(VariantFieldIndex::Ident(input.parse()?)) + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct VariantAttr { + index: VariantFieldIndex, + alias: Option, +} + +impl syn::parse::Parse for VariantAttr { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let index = if input.peek(syn::token::As) { + VariantFieldIndex::None + } else { + input.parse::()? + }; + if !input.peek(syn::token::As) { + return Ok(Self { index, alias: None }); + } + input.parse::()?; + + Ok(Self { + index, + alias: Some(input.parse()?), + }) + } +} + +#[derive(Debug, Clone)] +struct TargetVariant { + attr: VariantAttr, + access: TokenStream, + ty: syn::Type, + variant: syn::Variant, +} + +impl TargetVariant { + fn name(&self) -> syn::Ident { + if let Some(alias) = self.attr.alias.as_ref() { + return alias.clone(); + } + match &self.attr.index { + VariantFieldIndex::Ident(ident) => ident.clone(), + VariantFieldIndex::Numeric(_) | VariantFieldIndex::None => self.variant.ident.clone(), + } + } +} + +#[derive(Debug)] +pub struct Targets { + name: syn::Ident, + targets: Vec, + total_fields: usize, +} + +impl Targets { + pub fn parse(input: syn::DeriveInput) -> Result { + let name = input.ident; + let mut targets: Vec = Vec::new(); + let total_fields; + match &input.data { + syn::Data::Enum(data) => { + total_fields = data.variants.len(); + for field in data.variants.iter() { + let field_ident = &field.ident; + for attr in field.attrs.iter().filter(|attr| { + attr.path() + .get_ident() + .map(|id| id.to_string().as_str() == TARGETS_PATH) + .unwrap_or_default() + }) { + let attr_span = attr.span(); + let attr = attr.parse_args::()?; + let (access, ty) = match &field.fields { + syn::Fields::Named(fields) => match &attr.index { + VariantFieldIndex::Ident(ident) => { + let mut matching_val = None; + let mut accesses = Vec::new(); + for field in fields.named.iter() { + let matching = field + .ident + .as_ref() + .map(|i| i == ident) + .unwrap_or_default(); + if matching_val.is_some() && matching { + panic!("duplicate?") + } + + let ident = field.ident.clone().unwrap(); + if matching { + matching_val = Some((ident.clone(), field.ty.clone())); + accesses.push(quote! { + #ident + }); + } else { + accesses.push(quote! { + #ident: _ + }); + } + } + let (mut matching_ident, matching_ty) = matching_val.ok_or( + syn::Error::new(ident.span(), "no such variant field"), + )?; + matching_ident.set_span(ident.span()); + + ( + quote! { + #name::#field_ident { #(#accesses),* } => &#matching_ident, + }, + matching_ty, + ) + } + + VariantFieldIndex::Numeric(num) => { + return Err(syn::Error::new( + num.span(), + "cannot used numeric index for a variant with named fields", + )); + } + VariantFieldIndex::None => { + if fields.named.len() == 1 { + let field = fields.named.iter().next().unwrap(); + let field_ident = field.ident.as_ref().unwrap(); + ( + quote! { + #name::#field_ident { #field_ident } => &#field_ident, + }, + field.ty.clone(), + ) + } else { + return Err(syn::Error::new( + fields.named.span(), + "unnamed field index with more than one field", + )); + } + } + }, + syn::Fields::Unnamed(fields) => { + if let VariantFieldIndex::Numeric(num) = &attr.index { + let num = num.base10_parse::()? as usize; + let field = fields + .unnamed + .iter() + .nth(num) + .ok_or(syn::Error::new( + attr_span, + "field index out of range", + ))? + .clone(); + let left = (0..num).map(|_| quote! {_}); + let right = (num..fields.unnamed.len()).map(|_| quote! {_}); + ( + quote! { + #name::#field_ident(#(#left),*, val, #(#right),*) => &val, + }, + field.ty.clone(), + ) + } else if fields.unnamed.len() == 1 { + ( + quote! { + #name::#field_ident(val) => &val, + }, + fields.unnamed.iter().next().unwrap().ty.clone(), + ) + } else { + return Err(syn::Error::new( + fields.span(), + "unnamed fields without numeric index", + )); + } + } + syn::Fields::Unit => { + return Err(syn::Error::new( + field.span(), + "target cannot be a unit field", + )); + } + }; + let target = TargetVariant { + attr, + ty, + access, + variant: field.clone(), + }; + // if targets.iter().any(|t| t.name() == target.name()) { + // return Err(syn::Error::new(attr_span, "duplicate target field")); + // } + // target.variant.ident.set_span(attr_span); + + targets.push(target); + } + } + } + + _ => todo!(), + }; + + Ok(Self { + name, + targets, + total_fields, + }) + } +} + +impl ToTokens for Targets { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + // this is so horrid and haphazard but idc rn + // commenting to make the criticizing voice stfu + let mut by_target = { + let mut out = HashList::new(); + for target in self.targets.iter() { + out.add(target.name().to_string(), target.clone()); + } + out + }; + let fns = by_target.decompose().into_iter().map(|(_, targets)| { + if targets.is_empty() { + return quote! {}; + } + let all_variants = targets.len() == self.total_fields; + let fn_ret = { + let ty = targets.first().unwrap().ty.clone(); + if all_variants { + quote! {&#ty} + } else { + quote! {Option<&#ty>} + } + }; + let fn_name = { + let name = targets.first().unwrap().name(); + syn::Ident::new(name.to_string().to_case(Case::Snake).as_str(), name.span()) + }; + let raw_accesses = targets.iter().map(|t| { + let access = &t.access; + + quote_spanned! { t.attr.alias.as_ref().unwrap().span() => + #access + } + }); + let accesses = if all_variants { + quote! { + match self { + #(#raw_accesses)* + } + } + } else { + quote! { + Some(match self { + #(#raw_accesses)* + _ => return None, + }) + } + }; + + quote! { + pub const fn #fn_name(&self) -> #fn_ret { + #accesses + } + } + }); + let name = &self.name; + tokens.extend(quote! { + impl #name { + #(#fns)* + } + }); + } +} diff --git a/plan-proto/Cargo.toml b/plan-proto/Cargo.toml new file mode 100644 index 0000000..f132b9d --- /dev/null +++ b/plan-proto/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "plan-proto" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = { version = "1", features = ["derive"] } +chrono = { version = "0.4", features = ["serde"] } +thiserror = { version = "2" } +axum = { version = "*", optional = true } +argon2 = { version = "*", optional = true } +sqlx = { version = "*", optional = true } +ciborium = { version = "*", optional = true } +bytes = { version = "1.10.1", features = ["serde"], optional = true } +axum-extra = { version = "*", optional = true } +uuid = { version = "1", features = ["serde", "v4"] } +log = { version = "0.4", optional = true } + +[features] +server = [ + "dep:axum", + "dep:sqlx", + "dep:argon2", + "dep:ciborium", + "dep:bytes", + "dep:axum-extra", + "dep:log", +] +client = ["uuid/js"] diff --git a/plan-proto/src/cbor.rs b/plan-proto/src/cbor.rs new file mode 100644 index 0000000..ef47f39 --- /dev/null +++ b/plan-proto/src/cbor.rs @@ -0,0 +1,156 @@ +use axum::{ + body::Bytes, + extract::{FromRequest, Request, rejection::BytesRejection}, + http::{HeaderMap, HeaderValue, StatusCode, header}, + response::{IntoResponse, Response}, +}; +use axum_extra::headers::Mime; +use bytes::{BufMut, BytesMut}; +use core::fmt::Display; +use serde::{Serialize, de::DeserializeOwned}; + +const CBOR_CONTENT_TYPE: &str = "application/cbor"; +const PLAIN_CONTENT_TYPE: &str = "text/plain"; + +#[must_use] +pub struct Cbor(pub T); + +impl Cbor { + pub const fn new(t: T) -> Self { + Self(t) + } +} + +impl FromRequest for Cbor +where + T: DeserializeOwned, + S: Send + Sync, +{ + type Rejection = CborRejection; + + async fn from_request(req: Request, state: &S) -> Result { + if !cbor_content_type(req.headers()) { + return Err(CborRejection::MissingCborContentType); + } + + let bytes = Bytes::from_request(req, state).await?; + Ok(Self(ciborium::from_reader::(&*bytes)?)) + } +} + +impl IntoResponse for Cbor +where + T: Serialize, +{ + fn into_response(self) -> axum::response::Response { + // Extracted into separate fn so it's only compiled once for all T. + fn make_response(buf: BytesMut, ser_result: Result<(), CborRejection>) -> Response { + match ser_result { + Ok(()) => ( + [( + header::CONTENT_TYPE, + HeaderValue::from_static("application/cbor"), + )], + buf.freeze(), + ) + .into_response(), + Err(err) => err.into_response(), + } + } + + // Use a small initial capacity of 128 bytes like serde_json::to_vec + // https://docs.rs/serde_json/1.0.82/src/serde_json/ser.rs.html#2189 + let mut buf = BytesMut::with_capacity(128).writer(); + let res = ciborium::into_writer(&self.0, &mut buf) + .map_err(|err| CborRejection::SerdeRejection(err.to_string())); + make_response(buf.into_inner(), res) + } +} + +#[derive(Debug)] +pub enum CborRejection { + MissingCborContentType, + BytesRejection(BytesRejection), + DeserializeRejection(String), + SerdeRejection(String), +} +impl From> for CborRejection { + fn from(value: ciborium::de::Error) -> Self { + Self::SerdeRejection(match value { + ciborium::de::Error::Io(err) => format!("i/o: {err}"), + ciborium::de::Error::Syntax(offset) => format!("syntax error at {offset}"), + ciborium::de::Error::Semantic(offset, err) => format!( + "semantic parse: {err}{}", + offset + .map(|offset| format!(" at {offset}")) + .unwrap_or_default(), + ), + ciborium::de::Error::RecursionLimitExceeded => { + String::from("the input caused serde to recurse too much") + } + }) + } +} + +impl From for CborRejection { + fn from(value: BytesRejection) -> Self { + Self::BytesRejection(value) + } +} + +impl IntoResponse for CborRejection { + fn into_response(self) -> axum::response::Response { + match self { + CborRejection::MissingCborContentType => ( + StatusCode::BAD_REQUEST, + [( + header::CONTENT_TYPE, + HeaderValue::from_static(PLAIN_CONTENT_TYPE), + )], + String::from("missing cbor content type"), + ), + CborRejection::BytesRejection(err) => ( + err.status(), + [( + header::CONTENT_TYPE, + HeaderValue::from_static(PLAIN_CONTENT_TYPE), + )], + format!("bytes rejection: {}", err.body_text()), + ), + CborRejection::SerdeRejection(err) => ( + StatusCode::BAD_REQUEST, + [( + header::CONTENT_TYPE, + HeaderValue::from_static(PLAIN_CONTENT_TYPE), + )], + err, + ), + CborRejection::DeserializeRejection(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + [( + header::CONTENT_TYPE, + HeaderValue::from_static(PLAIN_CONTENT_TYPE), + )], + err, + ), + } + .into_response() + } +} + +fn cbor_content_type(headers: &HeaderMap) -> bool { + let Some(content_type) = headers.get(header::CONTENT_TYPE) else { + return false; + }; + + let Ok(content_type) = content_type.to_str() else { + return false; + }; + + let Ok(mime) = content_type.parse::() else { + return false; + }; + + mime.type_() == "application" + && (mime.subtype() == "cbor" || mime.suffix().is_some_and(|name| name == "cbor")) +} diff --git a/plan-proto/src/error.rs b/plan-proto/src/error.rs new file mode 100644 index 0000000..5a819ed --- /dev/null +++ b/plan-proto/src/error.rs @@ -0,0 +1,148 @@ +use core::fmt::Display; + +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::plan::PlanError; + +#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)] +pub enum ServerError { + #[error("database error: {0}")] + DatabaseError(DatabaseError), + #[error("invalid credentials")] + InvalidCredentials, + #[error("token expired")] + ExpiredToken, + #[error("internal server error: {0}")] + InternalServerError(String), + #[error("connection error")] + ConnectionError, + #[error("invalid request: {0}")] + InvalidRequest(String), + #[error("not found")] + NotFound, +} + +impl> From for ServerError { + fn from(value: I) -> Self { + let database_err: DatabaseError = value.into(); + if let DatabaseError::NotFound = &database_err { + return Self::NotFound; + } + Self::DatabaseError(database_err) + } +} + +impl From for ServerError { + fn from(value: PlanError) -> Self { + Self::InvalidRequest(value.to_string()) + } +} + +#[cfg(feature = "server")] +impl From> for ServerError { + fn from(_: ciborium::de::Error) -> Self { + Self::InvalidRequest(String::from("could not decode request")) + } +} + +#[cfg(feature = "server")] +impl axum::response::IntoResponse for ServerError { + fn into_response(self) -> axum::response::Response { + use axum::http::StatusCode; + + use crate::cbor::Cbor; + + match self { + ServerError::ExpiredToken => { + (StatusCode::UNAUTHORIZED, Cbor(ServerError::ExpiredToken)).into_response() + } + ServerError::NotFound | ServerError::DatabaseError(DatabaseError::NotFound) => { + (StatusCode::NOT_FOUND, Cbor(ServerError::NotFound)).into_response() + } + ServerError::DatabaseError(DatabaseError::UserAlreadyExists) => ( + StatusCode::BAD_REQUEST, + Cbor(ServerError::InvalidRequest(String::from("username taken"))), + ) + .into_response(), + ServerError::DatabaseError(err) => { + use uuid::Uuid; + + let error_id = Uuid::new_v4(); + log::error!("database error[{error_id}]: {err}"); + + ( + StatusCode::INTERNAL_SERVER_ERROR, + Cbor(ServerError::InternalServerError(format!( + "internal server error. error id: {error_id}" + ))), + ) + .into_response() + } + ServerError::InvalidCredentials => ( + StatusCode::UNAUTHORIZED, + Cbor(ServerError::InvalidCredentials), + ) + .into_response(), + ServerError::InternalServerError(_) => { + (StatusCode::INTERNAL_SERVER_ERROR, Cbor(self)).into_response() + } + ServerError::ConnectionError => { + (StatusCode::BAD_REQUEST, Cbor(ServerError::ConnectionError)).into_response() + } + ServerError::InvalidRequest(reason) => ( + StatusCode::BAD_REQUEST, + Cbor(ServerError::InvalidRequest(reason)), + ) + .into_response(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)] +pub enum DatabaseError { + #[error("user already exists")] + UserAlreadyExists, + #[error("password hashing error: {0}")] + PasswordHashError(String), + #[error("sqlx error: {0}")] + SqlxError(String), + #[error("not found")] + NotFound, +} + +#[cfg(feature = "server")] +impl axum::response::IntoResponse for DatabaseError { + fn into_response(self) -> axum::response::Response { + use axum::http::StatusCode; + + use crate::cbor::Cbor; + + ( + match self { + DatabaseError::UserAlreadyExists => StatusCode::BAD_REQUEST, + DatabaseError::NotFound => StatusCode::NOT_FOUND, + _ => StatusCode::INTERNAL_SERVER_ERROR, + }, + Cbor(self), + ) + .into_response() + } +} + +#[cfg(feature = "server")] +impl From for DatabaseError { + fn from(err: sqlx::Error) -> Self { + match err { + sqlx::Error::RowNotFound => Self::NotFound, + _ => Self::SqlxError(err.to_string()), + } + } +} + +#[cfg(feature = "server")] +impl From for DatabaseError { + fn from(err: argon2::password_hash::Error) -> Self { + Self::PasswordHashError(err.to_string()) + } +} diff --git a/plan-proto/src/lib.rs b/plan-proto/src/lib.rs new file mode 100644 index 0000000..2075244 --- /dev/null +++ b/plan-proto/src/lib.rs @@ -0,0 +1,465 @@ +#![feature(step_trait)] + +#[cfg(feature = "server")] +pub mod cbor; +pub mod error; +pub mod limited; +pub mod message; +pub mod plan; +pub mod token; +pub mod user; +use core::{fmt::Display, ops::RangeBounds}; + +use chrono::{DateTime, NaiveDate, NaiveTime, TimeZone, Timelike, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive( + Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Serialize, Deserialize, Hash, Default, +)] +#[cfg_attr(feature = "server", derive(sqlx::Type))] +#[cfg_attr(feature = "server", sqlx(type_name = "half_hour"))] +// #[derive( +// Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Serialize, Deserialize, Hash, sqlx::Type, +// )] +// #[sqlx(type_name = "half_hour")] +pub enum HalfHour { + #[default] + Hour0Min0, + Hour0Min30, + Hour1Min0, + Hour1Min30, + Hour2Min0, + Hour2Min30, + Hour3Min0, + Hour3Min30, + Hour4Min0, + Hour4Min30, + Hour5Min0, + Hour5Min30, + Hour6Min0, + Hour6Min30, + Hour7Min0, + Hour7Min30, + Hour8Min0, + Hour8Min30, + Hour9Min0, + Hour9Min30, + Hour10Min0, + Hour10Min30, + Hour11Min0, + Hour11Min30, + Hour12Min0, + Hour12Min30, + Hour13Min0, + Hour13Min30, + Hour14Min0, + Hour14Min30, + Hour15Min0, + Hour15Min30, + Hour16Min0, + Hour16Min30, + Hour17Min0, + Hour17Min30, + Hour18Min0, + Hour18Min30, + Hour19Min0, + Hour19Min30, + Hour20Min0, + Hour20Min30, + Hour21Min0, + Hour21Min30, + Hour22Min0, + Hour22Min30, + Hour23Min0, + Hour23Min30, +} + +impl HalfHour { + pub const fn previous_half_hour(&self) -> HalfHour { + match self { + HalfHour::Hour0Min0 => Self::Hour23Min30, + HalfHour::Hour0Min30 => Self::Hour0Min0, + HalfHour::Hour1Min0 => Self::Hour0Min30, + HalfHour::Hour1Min30 => Self::Hour1Min0, + HalfHour::Hour2Min0 => Self::Hour1Min30, + HalfHour::Hour2Min30 => Self::Hour2Min0, + HalfHour::Hour3Min0 => Self::Hour2Min30, + HalfHour::Hour3Min30 => Self::Hour3Min0, + HalfHour::Hour4Min0 => Self::Hour3Min30, + HalfHour::Hour4Min30 => Self::Hour4Min0, + HalfHour::Hour5Min0 => Self::Hour4Min30, + HalfHour::Hour5Min30 => Self::Hour5Min0, + HalfHour::Hour6Min0 => Self::Hour5Min30, + HalfHour::Hour6Min30 => Self::Hour6Min0, + HalfHour::Hour7Min0 => Self::Hour6Min30, + HalfHour::Hour7Min30 => Self::Hour7Min0, + HalfHour::Hour8Min0 => Self::Hour7Min30, + HalfHour::Hour8Min30 => Self::Hour8Min0, + HalfHour::Hour9Min0 => Self::Hour8Min30, + HalfHour::Hour9Min30 => Self::Hour9Min0, + HalfHour::Hour10Min0 => Self::Hour9Min30, + HalfHour::Hour10Min30 => Self::Hour10Min0, + HalfHour::Hour11Min0 => Self::Hour10Min30, + HalfHour::Hour11Min30 => Self::Hour11Min0, + HalfHour::Hour12Min0 => Self::Hour11Min30, + HalfHour::Hour12Min30 => Self::Hour12Min0, + HalfHour::Hour13Min0 => Self::Hour12Min30, + HalfHour::Hour13Min30 => Self::Hour13Min0, + HalfHour::Hour14Min0 => Self::Hour13Min30, + HalfHour::Hour14Min30 => Self::Hour14Min0, + HalfHour::Hour15Min0 => Self::Hour14Min30, + HalfHour::Hour15Min30 => Self::Hour15Min0, + HalfHour::Hour16Min0 => Self::Hour15Min30, + HalfHour::Hour16Min30 => Self::Hour16Min0, + HalfHour::Hour17Min0 => Self::Hour16Min30, + HalfHour::Hour17Min30 => Self::Hour17Min0, + HalfHour::Hour18Min0 => Self::Hour17Min30, + HalfHour::Hour18Min30 => Self::Hour18Min0, + HalfHour::Hour19Min0 => Self::Hour18Min30, + HalfHour::Hour19Min30 => Self::Hour19Min0, + HalfHour::Hour20Min0 => Self::Hour19Min30, + HalfHour::Hour20Min30 => Self::Hour20Min0, + HalfHour::Hour21Min0 => Self::Hour20Min30, + HalfHour::Hour21Min30 => Self::Hour21Min0, + HalfHour::Hour22Min0 => Self::Hour21Min30, + HalfHour::Hour22Min30 => Self::Hour22Min0, + HalfHour::Hour23Min0 => Self::Hour22Min30, + HalfHour::Hour23Min30 => Self::Hour23Min0, + } + } + pub const fn next_half_hour(&self) -> HalfHour { + match self { + HalfHour::Hour0Min0 => Self::Hour0Min30, + HalfHour::Hour0Min30 => Self::Hour1Min0, + HalfHour::Hour1Min0 => Self::Hour1Min30, + HalfHour::Hour1Min30 => Self::Hour2Min0, + HalfHour::Hour2Min0 => Self::Hour2Min30, + HalfHour::Hour2Min30 => Self::Hour3Min0, + HalfHour::Hour3Min0 => Self::Hour3Min30, + HalfHour::Hour3Min30 => Self::Hour4Min0, + HalfHour::Hour4Min0 => Self::Hour4Min30, + HalfHour::Hour4Min30 => Self::Hour5Min0, + HalfHour::Hour5Min0 => Self::Hour5Min30, + HalfHour::Hour5Min30 => Self::Hour6Min0, + HalfHour::Hour6Min0 => Self::Hour6Min30, + HalfHour::Hour6Min30 => Self::Hour7Min0, + HalfHour::Hour7Min0 => Self::Hour7Min30, + HalfHour::Hour7Min30 => Self::Hour8Min0, + HalfHour::Hour8Min0 => Self::Hour8Min30, + HalfHour::Hour8Min30 => Self::Hour9Min0, + HalfHour::Hour9Min0 => Self::Hour9Min30, + HalfHour::Hour9Min30 => Self::Hour10Min0, + HalfHour::Hour10Min0 => Self::Hour10Min30, + HalfHour::Hour10Min30 => Self::Hour11Min0, + HalfHour::Hour11Min0 => Self::Hour11Min30, + HalfHour::Hour11Min30 => Self::Hour12Min0, + HalfHour::Hour12Min0 => Self::Hour12Min30, + HalfHour::Hour12Min30 => Self::Hour13Min0, + HalfHour::Hour13Min0 => Self::Hour13Min30, + HalfHour::Hour13Min30 => Self::Hour14Min0, + HalfHour::Hour14Min0 => Self::Hour14Min30, + HalfHour::Hour14Min30 => Self::Hour15Min0, + HalfHour::Hour15Min0 => Self::Hour15Min30, + HalfHour::Hour15Min30 => Self::Hour16Min0, + HalfHour::Hour16Min0 => Self::Hour16Min30, + HalfHour::Hour16Min30 => Self::Hour17Min0, + HalfHour::Hour17Min0 => Self::Hour17Min30, + HalfHour::Hour17Min30 => Self::Hour18Min0, + HalfHour::Hour18Min0 => Self::Hour18Min30, + HalfHour::Hour18Min30 => Self::Hour19Min0, + HalfHour::Hour19Min0 => Self::Hour19Min30, + HalfHour::Hour19Min30 => Self::Hour20Min0, + HalfHour::Hour20Min0 => Self::Hour20Min30, + HalfHour::Hour20Min30 => Self::Hour21Min0, + HalfHour::Hour21Min0 => Self::Hour21Min30, + HalfHour::Hour21Min30 => Self::Hour22Min0, + HalfHour::Hour22Min0 => Self::Hour22Min30, + HalfHour::Hour22Min30 => Self::Hour23Min0, + HalfHour::Hour23Min0 => Self::Hour23Min30, + HalfHour::Hour23Min30 => Self::Hour0Min0, + } + } +} + +impl From for chrono::NaiveTime { + fn from(value: HalfHour) -> Self { + match value { + HalfHour::Hour0Min0 => chrono::NaiveTime::from_hms_opt(0, 0, 0), + HalfHour::Hour0Min30 => chrono::NaiveTime::from_hms_opt(0, 30, 0), + HalfHour::Hour1Min0 => chrono::NaiveTime::from_hms_opt(1, 0, 0), + HalfHour::Hour1Min30 => chrono::NaiveTime::from_hms_opt(1, 30, 0), + HalfHour::Hour2Min0 => chrono::NaiveTime::from_hms_opt(2, 0, 0), + HalfHour::Hour2Min30 => chrono::NaiveTime::from_hms_opt(2, 30, 0), + HalfHour::Hour3Min0 => chrono::NaiveTime::from_hms_opt(3, 0, 0), + HalfHour::Hour3Min30 => chrono::NaiveTime::from_hms_opt(3, 30, 0), + HalfHour::Hour4Min0 => chrono::NaiveTime::from_hms_opt(4, 0, 0), + HalfHour::Hour4Min30 => chrono::NaiveTime::from_hms_opt(4, 30, 0), + HalfHour::Hour5Min0 => chrono::NaiveTime::from_hms_opt(5, 0, 0), + HalfHour::Hour5Min30 => chrono::NaiveTime::from_hms_opt(5, 30, 0), + HalfHour::Hour6Min0 => chrono::NaiveTime::from_hms_opt(6, 0, 0), + HalfHour::Hour6Min30 => chrono::NaiveTime::from_hms_opt(6, 30, 0), + HalfHour::Hour7Min0 => chrono::NaiveTime::from_hms_opt(7, 0, 0), + HalfHour::Hour7Min30 => chrono::NaiveTime::from_hms_opt(7, 30, 0), + HalfHour::Hour8Min0 => chrono::NaiveTime::from_hms_opt(8, 0, 0), + HalfHour::Hour8Min30 => chrono::NaiveTime::from_hms_opt(8, 30, 0), + HalfHour::Hour9Min0 => chrono::NaiveTime::from_hms_opt(9, 0, 0), + HalfHour::Hour9Min30 => chrono::NaiveTime::from_hms_opt(9, 30, 0), + HalfHour::Hour10Min0 => chrono::NaiveTime::from_hms_opt(10, 0, 0), + HalfHour::Hour10Min30 => chrono::NaiveTime::from_hms_opt(10, 30, 0), + HalfHour::Hour11Min0 => chrono::NaiveTime::from_hms_opt(11, 0, 0), + HalfHour::Hour11Min30 => chrono::NaiveTime::from_hms_opt(11, 30, 0), + HalfHour::Hour12Min0 => chrono::NaiveTime::from_hms_opt(12, 0, 0), + HalfHour::Hour12Min30 => chrono::NaiveTime::from_hms_opt(12, 30, 0), + HalfHour::Hour13Min0 => chrono::NaiveTime::from_hms_opt(13, 0, 0), + HalfHour::Hour13Min30 => chrono::NaiveTime::from_hms_opt(13, 30, 0), + HalfHour::Hour14Min0 => chrono::NaiveTime::from_hms_opt(14, 0, 0), + HalfHour::Hour14Min30 => chrono::NaiveTime::from_hms_opt(14, 30, 0), + HalfHour::Hour15Min0 => chrono::NaiveTime::from_hms_opt(15, 0, 0), + HalfHour::Hour15Min30 => chrono::NaiveTime::from_hms_opt(15, 30, 0), + HalfHour::Hour16Min0 => chrono::NaiveTime::from_hms_opt(16, 0, 0), + HalfHour::Hour16Min30 => chrono::NaiveTime::from_hms_opt(16, 30, 0), + HalfHour::Hour17Min0 => chrono::NaiveTime::from_hms_opt(17, 0, 0), + HalfHour::Hour17Min30 => chrono::NaiveTime::from_hms_opt(17, 30, 0), + HalfHour::Hour18Min0 => chrono::NaiveTime::from_hms_opt(18, 0, 0), + HalfHour::Hour18Min30 => chrono::NaiveTime::from_hms_opt(18, 30, 0), + HalfHour::Hour19Min0 => chrono::NaiveTime::from_hms_opt(19, 0, 0), + HalfHour::Hour19Min30 => chrono::NaiveTime::from_hms_opt(19, 30, 0), + HalfHour::Hour20Min0 => chrono::NaiveTime::from_hms_opt(20, 0, 0), + HalfHour::Hour20Min30 => chrono::NaiveTime::from_hms_opt(20, 30, 0), + HalfHour::Hour21Min0 => chrono::NaiveTime::from_hms_opt(21, 0, 0), + HalfHour::Hour21Min30 => chrono::NaiveTime::from_hms_opt(21, 30, 0), + HalfHour::Hour22Min0 => chrono::NaiveTime::from_hms_opt(22, 0, 0), + HalfHour::Hour22Min30 => chrono::NaiveTime::from_hms_opt(22, 30, 0), + HalfHour::Hour23Min0 => chrono::NaiveTime::from_hms_opt(23, 0, 0), + HalfHour::Hour23Min30 => chrono::NaiveTime::from_hms_opt(23, 30, 0), + } + .unwrap() + } +} + +impl std::iter::Step for HalfHour { + fn steps_between(start: &Self, end: &Self) -> (usize, Option) { + let mut steps = 0usize; + let mut start = *start; + while start != *end { + steps += 1; + start = start.next_half_hour(); + } + + (steps, Some(steps)) + } + + fn forward_checked(mut start: Self, count: usize) -> Option { + for _ in 0..count { + start = start.next_half_hour(); + } + Some(start) + } + + fn backward_checked(mut start: Self, count: usize) -> Option { + for _ in 0..count { + start = start.previous_half_hour(); + } + Some(start) + } +} + +impl Display for HalfHour { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + HalfHour::Hour0Min0 => "00:00", + HalfHour::Hour0Min30 => "00:30", + HalfHour::Hour1Min0 => "01:00", + HalfHour::Hour1Min30 => "01:30", + HalfHour::Hour2Min0 => "02:00", + HalfHour::Hour2Min30 => "02:30", + HalfHour::Hour3Min0 => "03:00", + HalfHour::Hour3Min30 => "03:30", + HalfHour::Hour4Min0 => "04:00", + HalfHour::Hour4Min30 => "04:30", + HalfHour::Hour5Min0 => "05:00", + HalfHour::Hour5Min30 => "05:30", + HalfHour::Hour6Min0 => "06:00", + HalfHour::Hour6Min30 => "06:30", + HalfHour::Hour7Min0 => "07:00", + HalfHour::Hour7Min30 => "07:30", + HalfHour::Hour8Min0 => "08:00", + HalfHour::Hour8Min30 => "08:30", + HalfHour::Hour9Min0 => "09:00", + HalfHour::Hour9Min30 => "09:30", + HalfHour::Hour10Min0 => "10:00", + HalfHour::Hour10Min30 => "10:30", + HalfHour::Hour11Min0 => "11:00", + HalfHour::Hour11Min30 => "11:30", + HalfHour::Hour12Min0 => "12:00", + HalfHour::Hour12Min30 => "12:30", + HalfHour::Hour13Min0 => "13:00", + HalfHour::Hour13Min30 => "13:30", + HalfHour::Hour14Min0 => "14:00", + HalfHour::Hour14Min30 => "14:30", + HalfHour::Hour15Min0 => "15:00", + HalfHour::Hour15Min30 => "15:30", + HalfHour::Hour16Min0 => "16:00", + HalfHour::Hour16Min30 => "16:30", + HalfHour::Hour17Min0 => "17:00", + HalfHour::Hour17Min30 => "17:30", + HalfHour::Hour18Min0 => "18:00", + HalfHour::Hour18Min30 => "18:30", + HalfHour::Hour19Min0 => "19:00", + HalfHour::Hour19Min30 => "19:30", + HalfHour::Hour20Min0 => "20:00", + HalfHour::Hour20Min30 => "20:30", + HalfHour::Hour21Min0 => "21:00", + HalfHour::Hour21Min30 => "21:30", + HalfHour::Hour22Min0 => "22:00", + HalfHour::Hour22Min30 => "22:30", + HalfHour::Hour23Min0 => "23:00", + HalfHour::Hour23Min30 => "23:30", + }) + } +} + +impl Iterator for HalfHour { + type Item = HalfHour; + + fn next(&mut self) -> Option { + Some(self.next_half_hour()) + } +} + +impl From for HalfHour { + fn from(time: NaiveTime) -> Self { + let minute = time.minute(); + macro_rules! match_hour_minute { + ($($hour:literal: $min0:ident, $min30:ident;)*) => { + match time.hour().min(23) { + $( + $hour => if minute < 30{ + HalfHour::$min0 + } else { + HalfHour::$min30 + } + )* + hour => unreachable!("got hour {hour}") + } + }; + } + match_hour_minute!( + 0: Hour0Min0, Hour0Min30; + 1: Hour1Min0, Hour1Min30; + 2: Hour2Min0, Hour2Min30; + 3: Hour3Min0, Hour3Min30; + 4: Hour4Min0, Hour4Min30; + 5: Hour5Min0, Hour5Min30; + 6: Hour6Min0, Hour6Min30; + 7: Hour7Min0, Hour7Min30; + 8: Hour8Min0, Hour8Min30; + 9: Hour9Min0, Hour9Min30; + 10: Hour10Min0, Hour10Min30; + 11: Hour11Min0, Hour11Min30; + 12: Hour12Min0, Hour12Min30; + 13: Hour13Min0, Hour13Min30; + 14: Hour14Min0, Hour14Min30; + 15: Hour15Min0, Hour15Min30; + 16: Hour16Min0, Hour16Min30; + 17: Hour17Min0, Hour17Min30; + 18: Hour18Min0, Hour18Min30; + 19: Hour19Min0, Hour19Min30; + 20: Hour20Min0, Hour20Min30; + 21: Hour21Min0, Hour21Min30; + 22: Hour22Min0, Hour22Min30; + 23: Hour23Min0, Hour23Min30; + ) + } +} + +#[macro_export] +macro_rules! id_impl { + ($name:ident) => { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] + pub struct $name(uuid::Uuid); + + #[cfg(feature = "server")] + impl sqlx::TypeInfo for $name { + fn is_null(&self) -> bool { + self.0 == uuid::Uuid::nil() + } + + fn name(&self) -> &str { + "uuid" + } + } + + #[cfg(feature = "server")] + impl sqlx::Type for $name { + fn type_info() -> ::TypeInfo { + >::type_info() + } + } + + #[cfg(feature = "server")] + impl<'q> sqlx::Encode<'q, sqlx::Postgres> for $name { + fn encode_by_ref( + &self, + buf: &mut ::ArgumentBuffer<'q>, + ) -> Result { + self.0.encode_by_ref(buf) + } + } + + #[cfg(feature = "server")] + impl<'r> sqlx::Decode<'r, sqlx::Postgres> for $name { + fn decode( + value: ::ValueRef<'r>, + ) -> Result { + Ok(Self(uuid::Uuid::decode(value)?)) + } + } + + impl From for $name { + fn from(value: uuid::Uuid) -> Self { + Self::from_uuid(value) + } + } + + impl From<$name> for uuid::Uuid { + fn from(value: $name) -> Self { + value.into_uuid() + } + } + + impl Default for $name { + fn default() -> Self { + Self::new() + } + } + + impl $name { + pub fn new() -> Self { + Self(uuid::Uuid::new_v4()) + } + + pub const fn from_uuid(uuid: uuid::Uuid) -> Self { + Self(uuid) + } + + pub const fn into_uuid(self) -> uuid::Uuid { + self.0 + } + } + + impl core::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } + } + + impl core::str::FromStr for $name { + type Err = uuid::Error; + + fn from_str(s: &str) -> Result { + Ok(Self(uuid::Uuid::from_str(s)?)) + } + } + }; +} diff --git a/plan-proto/src/limited.rs b/plan-proto/src/limited.rs new file mode 100644 index 0000000..5b90efe --- /dev/null +++ b/plan-proto/src/limited.rs @@ -0,0 +1,129 @@ +use core::{ + fmt::Display, + ops::{Deref, RangeInclusive}, +}; + +use serde::{Deserialize, Deserializer, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FixedLenString(String); + +impl Display for FixedLenString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl FixedLenString { + pub fn new(s: String) -> Option { + (s.chars().take(LEN + 1).count() == LEN).then_some(Self(s)) + } +} + +impl Deref for FixedLenString { + type Target = String; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'de, const LEN: usize> Deserialize<'de> for FixedLenString { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct ExpectedLen(usize); + impl serde::de::Expected for ExpectedLen { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "a string exactly {} characters long", self.0) + } + } + ::deserialize(deserializer).and_then(|s| { + let char_count = s.chars().take(LEN.saturating_add(1)).count(); + if char_count != LEN { + Err(serde::de::Error::invalid_length( + char_count, + &ExpectedLen(LEN), + )) + } else { + Ok(Self(s)) + } + }) + } +} + +impl Serialize for FixedLenString { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.0.as_str()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ClampedString(String); + +impl ClampedString { + pub fn new(s: String) -> Result> { + let str_len = s.chars().take(MAX.saturating_add(1)).count(); + (str_len >= MIN && str_len <= MAX) + .then_some(Self(s)) + .ok_or(MIN..=MAX) + } + + pub fn into_inner(self) -> String { + self.0 + } +} + +impl Display for ClampedString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Deref for ClampedString { + type Target = String; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'de, const MIN: usize, const MAX: usize> Deserialize<'de> for ClampedString { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct ExpectedLen(usize, usize); + impl serde::de::Expected for ExpectedLen { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "a string between {} and {} characters long", + self.0, self.1 + ) + } + } + ::deserialize(deserializer).and_then(|s| { + let char_count = s.chars().take(MAX.saturating_add(1)).count(); + if char_count < MIN || char_count > MAX { + Err(serde::de::Error::invalid_length( + char_count, + &ExpectedLen(MIN, MAX), + )) + } else { + Ok(Self(s)) + } + }) + } +} + +impl Serialize for ClampedString { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.0.as_str()) + } +} diff --git a/plan-proto/src/message.rs b/plan-proto/src/message.rs new file mode 100644 index 0000000..39ee77a --- /dev/null +++ b/plan-proto/src/message.rs @@ -0,0 +1,23 @@ +use core::ops::Deref; + +use serde::{Deserialize, Deserializer, Serialize}; + +use crate::{ + HalfHour, + error::ServerError, + plan::{Plan, PlanDay, UpdateTiles}, +}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ServerMessage { + Error(ServerError), + DayUpdate { offset: u8, day: PlanDay }, + PlanInfo(Plan), +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ClientMessage { + MarkTile { day_offset: u8, tile: HalfHour }, + UnmarkTile { day_offset: u8, tile: HalfHour }, + GetPlan, +} diff --git a/plan-proto/src/plan.rs b/plan-proto/src/plan.rs new file mode 100644 index 0000000..77c6f78 --- /dev/null +++ b/plan-proto/src/plan.rs @@ -0,0 +1,96 @@ +use core::num::NonZeroU8; +use std::collections::HashMap; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::{HalfHour, limited::ClampedString}; + +#[derive(Debug, Clone, Copy, PartialEq, Error)] +pub enum PlanError { + #[error("start after end")] + StartAfterEnd, + #[error("day 0 ends before the plan starts")] + Day0EndsBeforePlanStarts, +} + +crate::id_impl!(PlanId); + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CreatePlan { + pub start_time: DateTime, + pub title: Option>, + pub days: HashMap, +} +impl CreatePlan { + pub fn check(&self) -> Result<(), PlanError> { + let start_half_hour: HalfHour = self.start_time.time().into(); + + if let Some(day0) = self.days.get(&0u8) + && day0.day_end < start_half_hour + { + return Err(PlanError::Day0EndsBeforePlanStarts); + } + self.days.values().try_for_each(|v| v.check())?; + + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CreatePlanDay { + pub day_start: HalfHour, + pub day_end: HalfHour, +} + +impl CreatePlanDay { + pub fn check(&self) -> Result<(), PlanError> { + if self.day_start > self.day_end { + return Err(PlanError::StartAfterEnd); + } + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Plan { + pub created_by: String, + pub title: Option, + pub start_time: DateTime, + pub days: HashMap, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PlanDay { + pub day_start: HalfHour, + pub day_end: HalfHour, + pub tiles_count: HashMap, + pub your_tiles: Box<[HalfHour]>, + pub users_available: Box<[LastUpdatedAvailability]>, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LastUpdatedAvailability { + pub username: String, + pub last_updated_availability: DateTime, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UpdateTiles { + pub day_offset: u8, + pub tiles: Box<[HalfHour]>, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PlanHeadline { + pub id: PlanId, + pub title: Option, + pub date: DateTime, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UserPlans { + pub created_plans: Box<[PlanHeadline]>, + pub participating_in: Box<[PlanHeadline]>, +} diff --git a/plan-proto/src/token.rs b/plan-proto/src/token.rs new file mode 100644 index 0000000..0f32b72 --- /dev/null +++ b/plan-proto/src/token.rs @@ -0,0 +1,40 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::{limited::FixedLenString, user::Username}; + +pub const TOKEN_LEN: usize = 0x20; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Token { + pub token: FixedLenString, + pub username: Username, + pub created_at: DateTime, + pub expires_at: DateTime, +} + +impl Token { + pub fn login_token(&self) -> TokenLogin { + TokenLogin(self.token.clone()) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TokenLogin(pub FixedLenString); + +#[cfg(feature = "server")] +impl axum_extra::headers::authorization::Credentials for TokenLogin { + const SCHEME: &'static str = "Bearer"; + + fn decode(value: &axum::http::HeaderValue) -> Option { + value + .to_str() + .ok() + .and_then(|v| FixedLenString::new(v.strip_prefix("Bearer ").unwrap_or(v).to_string())) + .map(Self) + } + + fn encode(&self) -> axum::http::HeaderValue { + axum::http::HeaderValue::from_str(self.0.as_str()).expect("bearer token encode") + } +} diff --git a/plan-proto/src/user.rs b/plan-proto/src/user.rs new file mode 100644 index 0000000..f7d2c79 --- /dev/null +++ b/plan-proto/src/user.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +use crate::limited::ClampedString; + +pub type Username = ClampedString<1, 0x40>; +pub type Password = ClampedString<6, 0x100>; + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct UserLogin { + pub username: Username, + pub password: Password, +} + +crate::id_impl!(UserId); diff --git a/plan-server/Cargo.toml b/plan-server/Cargo.toml new file mode 100644 index 0000000..aa943eb --- /dev/null +++ b/plan-server/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "plan-server" +version = "0.1.0" +edition = "2024" + +[dependencies] +axum = { version = "0.8", features = ["ws", "macros"] } +tokio = { version = "1.47", features = ["full"] } +log = { version = "0.4" } +pretty_env_logger = { version = "0.5" } +futures = "0.3.31" +anyhow = { version = "1" } +mime-sniffer = { version = "0.1" } +chrono = { version = "0.4" } +axum-extra = { version = "0.10", features = ["typed-header"] } +rand = { version = "0.9" } +serde_json = { version = "1.0" } +serde = { version = "1.0", features = ["derive"] } +thiserror = { version = "2" } +ciborium = { version = "0.2" } +colored = { version = "3.0" } +plan-macros = { path = "../plan-macros" } +plan-proto = { path = "../plan-proto", features = ["server"] } +uuid = { version = "1.18", features = ["v4"] } +sqlx = { version = "0.8", features = [ + "runtime-tokio", + "postgres", + "derive", + "macros", + "uuid", + "chrono", +] } +argon2 = { version = "0.5" } +tower-http = { version = "0.6", features = ["cors"] } +tower = { version = "0.5.2", features = [ + "limit", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "buffer", + "timeout", +] } +axum-limit = "0.1.0-alpha.2" diff --git a/plan-server/build.rs b/plan-server/build.rs new file mode 100644 index 0000000..6afcdf4 --- /dev/null +++ b/plan-server/build.rs @@ -0,0 +1,129 @@ +#![feature(unix_send_signal)] +use core::time::Duration; +use std::{ + io::Read, + os::unix::process::ChildExt, + path::Path, + process::{ChildStdout, Command, ExitStatus, Stdio}, + thread::sleep, + time::Instant, +}; + +fn main() { + // add_from_dir("../"); + return; + let server_path = std::env::current_dir().unwrap_or_else(|_| { + std::env::var("CARGO_MANIFEST_DIR") + .expect("CARGO_MANIFEST_DIR") + .into() + }); + unsafe { + std::env::set_var( + "CARGO_TARGET_DIR", + server_path + .parent() + .unwrap() + .join("target-server") + .to_str() + .unwrap(), + ) + }; + let web_path = server_path.parent().expect("no parent").join("calendar"); + add_from_dir(&web_path); + build_calendar(&web_path); +} + +fn build_calendar(web_path: &Path) { + let mut cmd = Command::new("trunk"); + let target_dir = web_path.parent().unwrap().join("target-calendar"); + cmd.current_dir(web_path) + // .env_clear() + // .env("PATH", std::env::var("PATH").unwrap_or_default()) + .arg("build") + .arg("--release") + .arg("--color") + .arg("never") + .arg("--verbose") + .env("CARGO_TARGET_DIR", target_dir.to_str().unwrap()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .stdin(Stdio::null()); + let mut child = cmd.spawn().unwrap(); + + let start = Instant::now(); + let status = loop { + if let Some(status) = child.try_wait().unwrap() { + break Some(status); + } + if (Instant::now() - start) > Duration::from_secs(30) { + eprintln!("build_calendar timed out"); + if let Err(err) = child.send_signal(9) { + panic!("killing build_calendar build: {err}"); + } + // panic!("a"); + break None; + } + sleep(Duration::from_millis(100)); + }; + let status_code = status.and_then(|status| status.code()).unwrap_or(!0u8 as _); + + if status_code != 0 { + eprintln!("trunk build status code: {status_code}"); + // // if let Some(mut stdout) = child.stdout.take() { + // // // let mut buf = vec![]; + // // let mut buf = [0u8; 1]; + // // let mut cnt = 0usize; + // // while let Ok(len) = stdout.read(&mut buf) { + // // if cnt == 2269 { + // // panic!("A"); + // // } + // // if len == 0 { + // // break; + // // } else { + // // eprint!("{}", buf[0] as char); + // // cnt += 1; + // // } + // // } + // // // panic!("a"); + // // // stdout.read_to_end(&mut buf).unwrap(); + // // // eprintln!("trunk build stdout: {}", String::from_utf8(buf).unwrap()); + // // } + // if let Some(mut stderr) = child.stderr.take() { + // // let mut buf = vec![]; + // // stderr.read_to_end(&mut buf).unwrap(); + // let mut buf = [0u8; 1]; + // let mut cnt = 0usize; + // while let Ok(len) = stderr.read(&mut buf) { + // if len == 0 { + // break; + // } else { + // eprint!("{}", buf[0] as char); + // cnt += 1; + // } + // if cnt == 50 { + // panic!("A"); + // } + // } + // panic!("a"); + // // eprintln!("trunk build stderr: {}", String::from_utf8(buf).unwrap()); + // } + eprintln!("calendar build failed"); + } +} + +#[allow(unused)] +fn add_from_dir(dir: &Path) { + for item in std::fs::read_dir(dir) + .unwrap_or_else(|err| panic!("could not read calendar source directory ({dir:?}): {err}")) + { + let item = item.unwrap_or_else(|err| panic!("item in {dir:?} source directory: {err}")); + if let Some(file_name) = item.file_name().to_str() + && file_name.ends_with(".rs") + { + let path = dir.join(file_name); + println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); + } else if item.file_type().map(|t| t.is_dir()).unwrap_or_default() { + add_from_dir(&item.path()); + } + } +} diff --git a/plan-server/src/db.rs b/plan-server/src/db.rs new file mode 100644 index 0000000..1892992 --- /dev/null +++ b/plan-server/src/db.rs @@ -0,0 +1,40 @@ +pub mod plan; +pub mod user; +use plan_proto::error::DatabaseError; +use sqlx::{Pool, Postgres}; + +use crate::db::{plan::PlanDatabase, user::UserDatabase}; + +type Result = core::result::Result; + +#[derive(Debug, Clone)] +pub struct Database { + pool: Pool, +} + +impl Database { + pub const fn new(pool: Pool) -> Self { + Self { pool } + } + + pub fn user(&self) -> UserDatabase { + UserDatabase { + pool: self.pool.clone(), + } + } + + pub fn plan(&self) -> PlanDatabase { + PlanDatabase { + pool: self.pool.clone(), + } + } + + pub async fn migrate(&self) { + log::info!("running migrations"); + sqlx::migrate!("../migrations") + .run(&self.pool) + .await + .expect("run migrations"); + log::info!("migrations done"); + } +} diff --git a/plan-server/src/db/plan.rs b/plan-server/src/db/plan.rs new file mode 100644 index 0000000..9981c04 --- /dev/null +++ b/plan-server/src/db/plan.rs @@ -0,0 +1,399 @@ +use core::num::NonZeroU8; +use std::collections::HashMap; + +use chrono::{DateTime, Utc}; +use plan_proto::{ + HalfHour, + error::{DatabaseError, ServerError}, + plan::{CreatePlan, LastUpdatedAvailability, Plan, PlanDay, PlanHeadline, PlanId, UpdateTiles}, + user::UserId, +}; +use sqlx::{Pool, Postgres, query, query_as}; +use uuid::Uuid; + +type Result = core::result::Result; + +#[derive(Debug, Clone)] +pub struct PlanDatabase { + pub(super) pool: Pool, +} + +pub enum TileOperation { + Mark, + Unmark, +} + +impl PlanDatabase { + pub async fn get_participating_plans(&self, user_id: UserId) -> Result> { + Ok(query_as!( + PlanHeadline, + r#" select + p.id, p.title, p.start_time as "date" + from + plans p + join + ( + select + d.plan_id, max(t.updated_at) as last_modified + from + day_tiles t + join + plan_days d on t.day_id = d.id + join + plans p on p.id = d.plan_id + where + p.created_by != $1 + and + t.user_id = $1 + group by + d.plan_id + ) participated on participated.plan_id = p.id + order by + participated.last_modified desc"#, + user_id.into_uuid(), + ) + .fetch_all(&self.pool) + .await? + .into_boxed_slice()) + } + pub async fn get_user_plans(&self, user: UserId) -> Result> { + Ok(query_as!( + PlanHeadline, + r#" select + id, title, start_time as "date" + from + plans + where + created_by = $1 + order by + created_at desc"#, + user.into_uuid(), + ) + .fetch_all(&self.pool) + .await? + .into_boxed_slice()) + } + + pub async fn create_plan(&self, creator: UserId, create: CreatePlan) -> Result { + create.check()?; + let mut tx = self.pool.begin().await?; + let created_at = Utc::now(); + let plan_id = PlanId::from_uuid( + query!( + r#" insert into plans + (title, created_by, start_time, created_at, updated_at) + values + ($1, $2, $3, $4, $5) + returning id"#, + create.title.map(|t| t.into_inner()), + creator.into_uuid(), + create.start_time, + created_at, + created_at, + ) + .fetch_one(&mut *tx) + .await? + .id, + ); + for (day_offset, day) in &create.days { + query!( + r#" insert into plan_days + (plan_id, day_offset, day_start, day_end) + values + ($1, $2, $3, $4)"#, + plan_id.into_uuid(), + (*day_offset) as i16, + day.day_start as _, + day.day_end as _, + ) + .execute(&mut *tx) + .await?; + } + + tx.commit().await?; + Ok(plan_id) + } + + pub async fn mark_day_tile( + &self, + id: PlanId, + user_id: UserId, + day_offset: u8, + tile: HalfHour, + operation: TileOperation, + ) -> Result<()> { + let day_id = query!( + "select id from plan_days where plan_id = $1 and day_offset = $2", + id.into_uuid(), + day_offset as i16, + ) + .fetch_one(&self.pool) + .await?; + + let mut tx = self.pool.begin().await?; + let mut current_tiles = match sqlx::query!( + r#" select + tiles as "tiles: Vec" + from + day_tiles + where + day_id = $1 + and + user_id = $2"#, + day_id.id, + user_id.into_uuid(), + ) + .fetch_one(&mut *tx) + .await + .map_err(Into::::into) + { + Ok(tiles) => tiles.tiles, + Err(ServerError::NotFound) => Vec::new(), + Err(err) => return Err(err), + }; + match operation { + TileOperation::Mark => { + if !current_tiles.contains(&tile) { + current_tiles.push(tile); + } + } + TileOperation::Unmark => current_tiles.retain(|t| *t != tile), + } + + if current_tiles.is_empty() { + sqlx::query!( + "delete from day_tiles where day_id = $1 and user_id = $2", + day_id.id, + user_id.into_uuid() + ) + .execute(&mut *tx) + .await?; + tx.commit().await?; + return Ok(()); + } + + sqlx::query( + r#" insert into + day_tiles (day_id, user_id, tiles, updated_at) + values + ($1, $2, $3, now()) + on conflict (day_id, user_id) do update + set tiles = $3, updated_at = now()"#, + ) + .bind(day_id.id) + .bind(user_id.into_uuid()) + .bind(current_tiles) + .execute(&mut *tx) + .await?; + + tx.commit().await?; + + Ok(()) + } + + pub async fn update_day_tiles( + &self, + id: PlanId, + user_id: UserId, + update: UpdateTiles, + ) -> Result { + let day_id = query!( + "select id from plan_days where plan_id = $1 and day_offset = $2", + id.into_uuid(), + update.day_offset as i16, + ) + .fetch_one(&self.pool) + .await?; + + sqlx::query( + r#" insert into + day_tiles (day_id, user_id, tiles) + values + ($1, $2, $3) + on conflict update"#, + ) + .bind(day_id.id) + .bind(user_id.into_uuid()) + .bind(update.tiles.to_vec()) + .execute(&self.pool) + .await?; + + let day = query_as!( + Day, + r#" select + id, day_offset, day_start as "day_start: HalfHour", day_end as "day_end: HalfHour" + from + plan_days + where + plan_id = $1 + and + day_offset = $2"#, + id.into_uuid(), + update.day_offset as i16, + ) + .fetch_one(&self.pool) + .await?; + + self.get_day_tiles(day, user_id).await + } + + pub async fn get_day(&self, plan_id: PlanId, day_offset: u8) -> Result { + Ok(query_as!( + Day, + r#" select + id, day_offset, day_start as "day_start: HalfHour", day_end as "day_end: HalfHour" + from + plan_days + where + plan_id = $1 + and + day_offset = $2"#, + plan_id.into_uuid(), + day_offset as i16, + ) + .fetch_one(&self.pool) + .await?) + } + + pub async fn get_day_tiles(&self, day: Day, requested_by: UserId) -> Result { + let day_tiles = query!( + r#" select + d.tiles as "tiles: Vec", d.user_id, u.username, d.updated_at + from + day_tiles d + join + users u on u.id = d.user_id + where + day_id = $1"#, + day.id + ) + .fetch_all(&self.pool) + .await?; + let mut tiles_count: HashMap> = HashMap::new(); + for day_tile in day_tiles.iter().flat_map(|t| t.tiles.iter()) { + match tiles_count.get_mut(day_tile) { + Some(cnt) => { + if let Some(next) = cnt.checked_add(1) { + *cnt = next; + } + } + None => { + tiles_count.insert(*day_tile, NonZeroU8::new(1).unwrap()); + } + } + } + let users_available = { + let mut users_available = day_tiles + .iter() + .map(|t| LastUpdatedAvailability { + username: t.username.clone(), + last_updated_availability: t.updated_at, + }) + .collect::>(); + users_available.sort_by_key(|u| u.last_updated_availability.timestamp()); + users_available.reverse(); + users_available + }; + + let your_tiles = day_tiles + .into_iter() + .find_map(|d| { + (d.user_id == requested_by.into_uuid()).then(|| d.tiles.into_boxed_slice()) + }) + .unwrap_or_default(); + + Ok(PlanDay { + your_tiles, + tiles_count, + users_available, + day_end: day.day_end, + day_start: day.day_start, + }) + } + + pub async fn day_tiles_per_user( + &self, + plan_id: PlanId, + day_offset: u8, + ) -> Result>> { + let day = query!( + "select id from plan_days where plan_id = $1 and day_offset = $2", + plan_id.into_uuid(), + day_offset as i16, + ) + .fetch_one(&self.pool) + .await?; + let tiles = query!( + r#" select + user_id, tiles as "tiles: Vec" + from + day_tiles + where + day_id = $1"#, + day.id, + ) + .fetch_all(&self.pool) + .await?; + Ok(tiles + .into_iter() + .map(|tile| { + ( + UserId::from_uuid(tile.user_id), + tile.tiles.into_boxed_slice(), + ) + }) + .collect()) + } + + pub async fn get_plan(&self, id: PlanId, requested_by: UserId) -> Result { + let plan = query!( + r#" select + p.id, p.title, users.username, p.start_time + from + plans p + join + users on users.id = p.created_by + where + p.id = $1"#, + id.into_uuid() + ) + .fetch_one(&self.pool) + .await?; + let mut plan = Plan { + created_by: plan.username, + start_time: plan.start_time, + days: HashMap::new(), + title: plan.title, + }; + + let days = query_as!( + Day, + r#" select + id, day_offset, day_start as "day_start: HalfHour", day_end as "day_end: HalfHour" + from + plan_days + where + plan_id = $1"#, + id.into_uuid() + ) + .fetch_all(&self.pool) + .await?; + for day in days { + let offset = day.day_offset as u8; + let day_id = day.id; + let day = self.get_day_tiles(day, requested_by).await?; + if let Some(dupe) = plan.days.insert(offset, day) { + log::warn!("duplicate day (plan id: {id}; day id: {day_id}): {dupe:?}",); + } + } + + Ok(plan) + } +} + +pub struct Day { + pub id: Uuid, + pub day_offset: i16, + pub day_start: HalfHour, + pub day_end: HalfHour, +} diff --git a/plan-server/src/db/user.rs b/plan-server/src/db/user.rs new file mode 100644 index 0000000..181c7d2 --- /dev/null +++ b/plan-server/src/db/user.rs @@ -0,0 +1,213 @@ +use super::Result; +use argon2::{ + Argon2, PasswordHash, PasswordVerifier, + password_hash::{PasswordHasher, SaltString, rand_core::OsRng}, +}; +use chrono::{TimeDelta, Utc}; +use plan_proto::{ + error::{DatabaseError, ServerError}, + token, + user::UserId, +}; +use rand::distr::SampleString; +use sqlx::{Decode, Encode, Pool, Postgres, prelude::FromRow, query, query_as}; + +#[derive(Debug, Clone)] +pub struct UserDatabase { + pub(super) pool: Pool, +} + +#[derive(Debug, Clone, FromRow)] +pub struct LoginToken { + pub token: String, + pub user_id: UserId, + + pub created_at: chrono::DateTime, + pub expires_at: chrono::DateTime, +} + +impl LoginToken { + const TOKEN_LONGEVITY: TimeDelta = TimeDelta::days(30); + + pub fn new(user_id: UserId) -> Self { + let created_at = Utc::now(); + let expires_at = created_at + .checked_add_signed(Self::TOKEN_LONGEVITY) + .unwrap_or_else(|| { + panic!( + "could not add {} time to {created_at}", + Self::TOKEN_LONGEVITY + ) + }); + + let token = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), token::TOKEN_LEN); + + Self { + token, + user_id, + created_at, + expires_at, + } + } +} + +pub enum GetUserBy<'a> { + Username(&'a str), + Id(UserId), +} + +impl UserDatabase { + pub async fn create(&self, username: &str, password: &str) -> Result { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + let password_hash = argon2 + .hash_password(password.as_bytes(), &salt)? + .to_string(); + + let now = chrono::offset::Utc::now(); + + let user = User { + id: UserId::new(), + username: username.into(), + password_hash, + created_at: now, + updated_at: now, + }; + + query!( + r#"insert into users + (id, username, password_hash, created_at, updated_at) + values + ($1, $2, $3, $4, $5)"#, + user.id.into_uuid(), + user.username, + user.password_hash, + user.created_at, + user.updated_at + ) + .execute(&self.pool) + .await + .map_err(|err| { + if let sqlx::Error::Database(db_err) = &err + && let Some(constraint) = db_err.constraint() + && constraint == "users_username_unique" + { + DatabaseError::UserAlreadyExists + } else { + err.into() + } + })?; + + Ok(user) + } + + pub async fn get_user(&self, get_user_by: GetUserBy<'_>) -> Result { + Ok(match get_user_by { + GetUserBy::Username(username) => { + query_as!( + User, + r#" + select + id, username, password_hash, + created_at, updated_at + from + users + where + username = $1"#, + username + ) + .fetch_one(&self.pool) + .await? + } + GetUserBy::Id(id) => { + query_as!( + User, + r#" + select + id, username, password_hash, + created_at, updated_at + from + users + where + id = $1"#, + id.into_uuid() + ) + .fetch_one(&self.pool) + .await? + } + }) + } + + pub async fn login( + &self, + username: &str, + password: &str, + ) -> core::result::Result { + let user = self.get_user(GetUserBy::Username(username)).await?; + + let parsed_hash = PasswordHash::new(&user.password_hash).map_err(DatabaseError::from)?; + Argon2::default() + .verify_password(password.as_bytes(), &parsed_hash) + .map_err(|err| match err { + argon2::password_hash::Error::Password => ServerError::InvalidCredentials, + err => ServerError::DatabaseError(err.into()), + })?; + + let token = LoginToken::new(user.id); + + query!( + r#" insert into login_tokens + (token, user_id, created_at, expires_at) + values + ($1, $2, $3, $4)"#, + token.token, + token.user_id.into_uuid(), + token.created_at, + token.expires_at + ) + .execute(&self.pool) + .await + .map_err(Into::::into)?; + + Ok(token) + } + + pub async fn check_token(&self, token: &str) -> core::result::Result { + let token = query_as!( + LoginToken, + r#" select + token, user_id, created_at, expires_at + from + login_tokens + where + token = $1 + and + expires_at > now() + "#, + token + ) + .fetch_one(&self.pool) + .await + .map_err(Into::::into) + .map_err(|err| match err { + DatabaseError::NotFound => ServerError::ExpiredToken, + _ => err.into(), + })?; + + if Utc::now() >= token.expires_at { + return Err(ServerError::ExpiredToken); + } + + Ok(self.get_user(GetUserBy::Id(token.user_id)).await?) + } +} + +#[derive(Debug, Clone, FromRow, Encode, Decode)] +pub struct User { + pub id: UserId, + pub username: String, + pub password_hash: String, + + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, +} diff --git a/plan-server/src/identity.rs b/plan-server/src/identity.rs new file mode 100644 index 0000000..bc9ec51 --- /dev/null +++ b/plan-server/src/identity.rs @@ -0,0 +1,8 @@ +use plan_proto::{plan::PlanId, user::UserId}; + +#[derive(Debug, Clone, PartialEq)] +pub struct SessionIdentity { + pub user_id: UserId, + pub user_name: String, + pub plan_id: PlanId, +} diff --git a/plan-server/src/main.rs b/plan-server/src/main.rs new file mode 100644 index 0000000..7d60fc6 --- /dev/null +++ b/plan-server/src/main.rs @@ -0,0 +1,372 @@ +mod db; +mod identity; +mod runner; +mod session; + +use axum::{ + BoxError, Router, debug_handler, + error_handling::HandleErrorLayer, + extract::State, + http::{Request, StatusCode, header}, + response::IntoResponse, + routing::{any, get, post, put}, +}; +use axum_extra::{ + TypedHeader, + handler::HandlerCallWithExtractors, + headers::{self, Authorization}, +}; +use core::{fmt::Display, net::SocketAddr, str::FromStr, time::Duration}; +use plan_proto::{ + cbor::Cbor, + error::ServerError, + limited::FixedLenString, + message::ClientMessage, + plan::{CreatePlan, UserPlans}, + token::{Token, TokenLogin}, + user::UserLogin, +}; +use sqlx::postgres::PgPoolOptions; +use std::io::Write; +use tokio::sync::mpsc::UnboundedSender; +use tower::{ + ServiceBuilder, + buffer::BufferLayer, + limit::{RateLimit, RateLimitLayer, rate::Rate}, +}; + +use crate::{db::Database, identity::SessionIdentity, runner::Runner, session::SessionManager}; + +const fn parse_port(port: &str) -> u16 { + const fn parse_char(c: u8) -> u16 { + match c { + b'0' => 0, + b'1' => 1, + b'2' => 2, + b'3' => 3, + b'4' => 4, + b'5' => 5, + b'6' => 6, + b'7' => 7, + b'8' => 8, + b'9' => 9, + _ => panic!("not a decimal number"), + } + } + let port_bytes = port.as_bytes(); + match port.len() { + 0 => panic!("port too short"), + 1 => parse_char(port_bytes[0]), + 2 => (parse_char(port_bytes[0]) * 10) + parse_char(port_bytes[1]), + 3 => { + (parse_char(port_bytes[0]) * 100) + + (parse_char(port_bytes[1]) * 10) + + parse_char(port_bytes[2]) + } + 4 => { + (parse_char(port_bytes[0]) * 1000) + + (parse_char(port_bytes[1]) * 100) + + (parse_char(port_bytes[2]) * 10) + + parse_char(port_bytes[3]) + } + 5 => { + (parse_char(port_bytes[0]) * 10000) + + (parse_char(port_bytes[1]) * 1000) + + (parse_char(port_bytes[2]) * 100) + + (parse_char(port_bytes[3]) * 10) + + parse_char(port_bytes[4]) + } + _ => panic!("port too long"), + } +} + +const PORT: u16 = match option_env!("PORT") { + Some(port) => parse_port(port), + None => 8080, +}; +const HOST: &str = match option_env!("HOST") { + Some(host) => host, + None => "127.0.0.1", +}; + +const DEFAULT_MAX_PG_CONNECTIONS: u32 = 30; +const DEFAULT_PG_CONN_STRING: &str = "postgres://emilis@localhost/calendar"; + +#[tokio::main] +async fn main() { + // pretty_env_logger::init(); + use colored::Colorize; + pretty_env_logger::formatted_builder() + .parse_default_env() + .format(|f, record| { + let time = chrono::Local::now().time().to_string().dimmed(); + + match record.file() { + Some(file) => { + let file = format!( + "[{file}{}]", + record + .line() + .map(|l| format!(":{l}")) + .unwrap_or_else(String::new), + ) + .dimmed(); + let level = match record.level() { + log::Level::Error => "[err]".red().bold(), + log::Level::Warn => "[warn]".yellow().bold(), + log::Level::Info => "[info]".white().bold(), + log::Level::Debug => "[debug]".dimmed().bold(), + log::Level::Trace => "[trace]".dimmed(), + }; + let args = record.args(); + + let arrow = "➢".bold().magenta(); + writeln!( + f, + "{time} {file}\n{level} {arrow} {args}", + // "⇗⇘⇗⇘⇗⇘".bold().dimmed(), + ) + } + _ => writeln!(f, "{time} [{}] {}", record.level(), record.args()), + } + }) + .try_init() + .unwrap(); + + let default_panic = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + default_panic(info); + std::process::exit(1); + })); + + let listen_addr = + SocketAddr::from_str(format!("{HOST}:{PORT}").as_str()).expect("invalid host/port"); + + let pg_pool = PgPoolOptions::new() + .max_connections( + std::env::var("MAX_DB_CONNECTIONS") + .ok() + .and_then(|val| u32::from_str(&val).ok()) + .unwrap_or(DEFAULT_MAX_PG_CONNECTIONS), + ) + .connect( + std::env::var("PG_CONN_STRING") + .unwrap_or_else(|_| String::from(DEFAULT_PG_CONN_STRING)) + .as_str(), + ) + .await + .expect("could not init db"); + let db = Database::new(pg_pool); + + db.migrate().await; + + let (send_server, recv_server) = tokio::sync::mpsc::unbounded_channel(); + let state = AppState { + db: db.clone(), + message_sender: send_server, + session_manager: SessionManager::new(), + }; + let sessions = state.session_manager.clone(); + tokio::spawn(async move { + Runner::new(recv_server, db, sessions).run().await; + log::error!("runner ended"); + std::process::exit(0x40); + }); + + let app = Router::new() + .route("/s/users", put(signup)) + .route("/s/tokens", post(signin)) + // .route("/s/tokens/check", get(check_token)) + .route("/s/plans/{id}", any(session::calendar_session)) + .route("/s/plans", get(my_plans)) + .route("/s/plans", post(new_plan)) + .route( + "/s/tokens/check", + get(check_token).layer( + ServiceBuilder::new() + .layer(HandleErrorLayer::new(|err: BoxError| async move { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Unhandled error: {}", err), + ) + })) + .layer(BufferLayer::new(0x1000)) + .layer(RateLimitLayer::new(100, Duration::from_secs(10))), + ), + ) + .with_state(state) + .layer(tower_http::cors::CorsLayer::permissive().allow_headers([header::AUTHORIZATION])) + .fallback(get(handle_http_static)); + let listener = tokio::net::TcpListener::bind(listen_addr).await.unwrap(); + log::info!("listening on {}", listener.local_addr().unwrap()); + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .await + .unwrap(); +} + +async fn new_plan( + State(AppState { db, .. }): State, + TypedHeader(Authorization(login)): TypedHeader>, + Cbor(plan): Cbor, +) -> Result { + let user = db.user().check_token(&login.0).await?; + let plan_id = db.plan().create_plan(user.id, plan).await?; + Ok((StatusCode::CREATED, Cbor(plan_id))) +} + +#[debug_handler] +async fn check_token( + State(AppState { db, .. }): State, + TypedHeader(Authorization(login)): TypedHeader>, +) -> Result { + db.user().check_token(&login.0).await?; + Ok(StatusCode::OK) +} + +async fn my_plans( + State(AppState { db, .. }): State, + TypedHeader(Authorization(login)): TypedHeader>, +) -> Result { + let user = db.user().check_token(&login.0).await?; + Ok(Cbor(UserPlans { + created_plans: db.plan().get_user_plans(user.id).await?, + participating_in: db.plan().get_participating_plans(user.id).await?, + })) +} + +async fn signin( + State(AppState { db, .. }): State, + Cbor(UserLogin { username, password }): Cbor, +) -> Result { + let token = db.user().login(&username, &password).await?; + + Ok(Cbor(Token { + username, + token: FixedLenString::new(token.token.clone()).ok_or_else(|| { + ServerError::InternalServerError(format!( + "could not get a fixed len string for token [{}]", + token.token + )) + })?, + created_at: token.created_at, + expires_at: token.expires_at, + }) + .into_response()) +} + +async fn signup( + State(AppState { db, .. }): State, + Cbor(UserLogin { username, password }): Cbor, +) -> Result { + db.user().create(&username, &password).await?; + Ok(StatusCode::CREATED) +} + +#[derive(Debug, Clone)] +struct AppState { + db: Database, + session_manager: SessionManager, + message_sender: UnboundedSender<(SessionIdentity, ClientMessage)>, +} + +async fn handle_http_static(req: Request) -> impl IntoResponse { + use mime_sniffer::MimeTypeSniffer; + const INDEX_FILE: &[u8] = include_bytes!("../../plan/dist/index.html"); + let path = req.uri().path(); + + plan_macros::include_dist!(DIST_FILES, "plan/dist"); + + let file = if let Some(file) = DIST_FILES.iter().find_map(|(file_path, file)| { + if *file_path == path { + Some(*file) + } else { + None + } + }) { + file + } else { + return ( + [(header::CONTENT_TYPE, "text/html".to_string())], + INDEX_FILE, + ); + }; + + let mime = if path.ends_with(".js") { + "text/javascript".to_string() + } else if path.ends_with(".css") { + "text/css".to_string() + } else if path.ends_with(".wasm") { + "application/wasm".to_string() + } else if path.ends_with(".svg") { + "image/svg+xml".to_string() + } else { + file.sniff_mime_type() + .unwrap_or("application/octet-stream") + .to_string() + }; + + ([(header::CONTENT_TYPE, mime)], file) +} + +struct XForwardedFor(String); + +impl Display for XForwardedFor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.0.as_str()) + } +} + +impl headers::Header for XForwardedFor { + fn name() -> &'static header::HeaderName { + static NAME: header::HeaderName = header::HeaderName::from_static("x-forwarded-for"); + &NAME + } + + fn decode<'i, I>(values: &mut I) -> Result + where + Self: Sized, + I: Iterator, + { + Ok(Self( + values + .next() + .and_then(|v| v.to_str().ok()) + .ok_or(headers::Error::invalid())? + .to_string(), + )) + } + + fn encode>(&self, _: &mut E) {} +} + +pub trait LogError { + fn log_warn(self); + fn log_err(self); + fn log_debug(self); +} + +impl LogError for Result +where + E: Display, +{ + fn log_warn(self) { + if let Err(err) = self { + log::warn!("{err}"); + } + } + + fn log_err(self) { + if let Err(err) = self { + log::error!("{err}"); + } + } + + fn log_debug(self) { + if let Err(err) = self { + log::debug!("{err}") + } + } +} diff --git a/plan-server/src/runner.rs b/plan-server/src/runner.rs new file mode 100644 index 0000000..7e744c6 --- /dev/null +++ b/plan-server/src/runner.rs @@ -0,0 +1,179 @@ +use std::collections::HashMap; + +use plan_proto::{ + HalfHour, + error::ServerError, + message::{ClientMessage, ServerMessage}, + plan::{Plan, PlanDay, PlanId}, + user::UserId, +}; +use tokio::sync::mpsc::UnboundedReceiver; +use uuid::Uuid; + +use crate::{ + LogError, + db::{ + Database, + plan::{Day, TileOperation}, + }, + identity::SessionIdentity, + session::SessionManager, +}; + +pub struct Runner { + recv: UnboundedReceiver<(SessionIdentity, ClientMessage)>, + database: Database, + session: SessionManager, +} + +pub(crate) enum ActionOutcome { + UpdateDay { + offset: u8, + day: PlanDay, + tiles: HashMap>, + }, + SendPlan(Plan), +} + +impl Runner { + pub const fn new( + recv: UnboundedReceiver<(SessionIdentity, ClientMessage)>, + database: Database, + session: SessionManager, + ) -> Self { + Self { + recv, + database, + session, + } + } + + pub async fn run(mut self) { + loop { + let (ident, msg) = match self.recv.recv().await { + Some(next) => next, + None => panic!("runner recv channel closed"), + }; + let outcome = match self.run_inner(ident.clone(), msg).await { + Ok(outcome) => outcome, + Err(err) => { + log::debug!( + "run_inner for {} ({}): {err}", + ident.user_name, + ident.user_id, + ); + let message = if let ServerError::DatabaseError(err) = &err { + let error_id = Uuid::new_v4(); + log::error!("database error[{error_id}]: {err}"); + + ServerMessage::Error(ServerError::InternalServerError(format!( + "internal server error. error id: {error_id}" + ))) + } else { + ServerMessage::Error(err) + }; + self.session + .send(ident.plan_id, ident.user_id, message) + .await; + continue; + } + }; + match outcome { + ActionOutcome::UpdateDay { offset, day, tiles } => { + let senders = self.session.get_all_plan_senders(ident.plan_id).await; + + for (user, send) in senders { + send.send(ServerMessage::DayUpdate { + offset, + day: PlanDay { + day_start: day.day_start, + day_end: day.day_end, + tiles_count: day.tiles_count.clone(), + users_available: day.users_available.clone(), + your_tiles: tiles.get(&user).cloned().unwrap_or_default(), + }, + }) + .await + .log_debug(); + } + } + ActionOutcome::SendPlan(plan) => { + self.session + .send(ident.plan_id, ident.user_id, ServerMessage::PlanInfo(plan)) + .await + } + } + } + } + + pub(crate) async fn run_inner( + &mut self, + ident: SessionIdentity, + message: ClientMessage, + ) -> Result { + match message { + ClientMessage::GetPlan => Ok(ActionOutcome::SendPlan( + self.database + .plan() + .get_plan(ident.plan_id, ident.user_id) + .await?, + )), + ClientMessage::MarkTile { + day_offset: offset, + tile, + } => { + self.database + .plan() + .mark_day_tile( + ident.plan_id, + ident.user_id, + offset, + tile, + TileOperation::Mark, + ) + .await?; + let tiles = self + .database + .plan() + .day_tiles_per_user(ident.plan_id, offset) + .await?; + let day = self.database.plan().get_day(ident.plan_id, offset).await?; + let day = self + .database + .plan() + .get_day_tiles(day, ident.user_id) + .await?; + + Ok(ActionOutcome::UpdateDay { offset, day, tiles }) + } + ClientMessage::UnmarkTile { + day_offset: offset, + tile, + } => { + self.database + .plan() + .mark_day_tile( + ident.plan_id, + ident.user_id, + offset, + tile, + TileOperation::Unmark, + ) + .await?; + let tiles = self + .database + .plan() + .day_tiles_per_user(ident.plan_id, offset) + .await?; + let day = self.database.plan().get_day(ident.plan_id, offset).await?; + let day = self + .database + .plan() + .get_day_tiles(day, ident.user_id) + .await?; + + Ok(ActionOutcome::UpdateDay { offset, day, tiles }) + } + } + } +} diff --git a/plan-server/src/session.rs b/plan-server/src/session.rs new file mode 100644 index 0000000..127391f --- /dev/null +++ b/plan-server/src/session.rs @@ -0,0 +1,350 @@ +use core::net::SocketAddr; +use std::{collections::HashMap, sync::Arc}; + +use axum::{ + extract::{ + ConnectInfo, Path, State, WebSocketUpgrade, + ws::{self, WebSocket}, + }, + response::IntoResponse, +}; +use axum_extra::{TypedHeader, headers}; +use colored::Colorize; +use futures::{SinkExt, lock::Mutex}; +use plan_proto::{ + error::ServerError, + message::{ClientMessage, ServerMessage}, + plan::PlanId, + token::TokenLogin, + user::UserId, +}; +use thiserror::Error; +use tokio::sync::mpsc::{Receiver, Sender, UnboundedSender}; +use uuid::Uuid; + +use crate::{ + AppState, LogError, XForwardedFor, + db::{Database, user::User}, + identity::SessionIdentity, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ConnectionId(u32); + +impl ConnectionId { + fn new() -> Self { + Self(rand::random()) + } +} + +#[allow(clippy::type_complexity)] +#[derive(Debug, Clone)] +pub struct SessionManager(Arc>>>); + +pub struct Connection { + pub plan_id: PlanId, + pub sender: Sender, +} + +#[derive(Debug, Clone, Copy, PartialEq, Error)] +pub enum SessionError { + #[error("the given connection id already exists for this user")] + ConnectionIdExists, +} + +pub struct ConnectionDropToken(SessionManager, UserId, ConnectionId); + +impl Drop for ConnectionDropToken { + fn drop(&mut self) { + let (s, u, c) = (self.0.clone(), self.1, self.2); + tokio::spawn(async move { + s.drop_connection(u, c).await; + }); + } +} + +impl SessionManager { + pub fn new() -> Self { + Self(Arc::new(Mutex::new(HashMap::new()))) + } + + pub async fn send(&self, plan_id: PlanId, user_id: UserId, message: ServerMessage) { + for sender in self + .0 + .lock() + .await + .get(&user_id) + .iter() + .flat_map(|conns| conns.values()) + .filter_map(|conn| (conn.plan_id == plan_id).then_some(&conn.sender)) + { + sender.send(message.clone()).await.log_debug(); + } + } + + pub async fn get_all_plan_senders( + &self, + plan_id: PlanId, + ) -> Box<[(UserId, Sender)]> { + self.0 + .lock() + .await + .iter() + .flat_map(|(user, conns)| { + conns + .values() + .filter_map(|c| (c.plan_id == plan_id).then_some((*user, c.sender.clone()))) + }) + .collect::>() + } + + pub async fn broadcast(&self, plan_id: PlanId, message: ServerMessage) { + for sender in self + .0 + .lock() + .await + .iter() + .flat_map(|(_, conns)| { + conns + .values() + .filter_map(|c| (c.plan_id == plan_id).then_some(c.sender.clone())) + }) + // Call collect due to [#98380](https://github.com/rust-lang/rust/issues/98380) + .collect::>() + { + sender.send(message.clone()).await.log_debug(); + } + } + + pub async fn add_connection( + &self, + user_id: UserId, + connection_id: ConnectionId, + plan_id: PlanId, + sender: Sender, + ) -> Result { + let mut sessions = self.0.lock().await; + let user_sessions = match sessions.get_mut(&user_id) { + Some(s) => s, + None => { + sessions.insert(user_id, HashMap::new()); + sessions.get_mut(&user_id).unwrap() + } + }; + if user_sessions.contains_key(&connection_id) { + return Err(SessionError::ConnectionIdExists); + } + user_sessions.insert(connection_id, Connection { plan_id, sender }); + + Ok(ConnectionDropToken(self.clone(), user_id, connection_id)) + } + + async fn drop_connection(&self, user_id: UserId, connection_id: ConnectionId) { + let mut sessions = self.0.lock().await; + let user_sessions = match sessions.get_mut(&user_id) { + Some(sessions) => sessions, + None => return, + }; + let _ = user_sessions.remove(&connection_id); + } +} + +pub async fn calendar_session( + ws: WebSocketUpgrade, + user_agent: Option>, + x_forwarded_for: Option>, + ConnectInfo(addr): ConnectInfo, + State(state): State, + Path(plan_id): Path, +) -> impl IntoResponse { + let who = x_forwarded_for + .map(|x| x.to_string()) + .unwrap_or_else(|| addr.to_string()) + .italic(); + + // log::debug!( + // "{who}{} connected.", + // user_agent + // .map(|agent| format!(" (User-Agent: {})", agent.as_str())) + // .unwrap_or_default(), + // ); + + // finalize the upgrade process by returning upgrade callback. + // we can customize the callback by sending additional info such as address. + ws.on_upgrade(move |mut socket| async move { + let ident = match get_identification(&mut socket, &who, state.db.clone()).await { + Ok(ident) => ident, + Err(err) => { + socket + .send({ + ws::Message::Binary({ + let mut v = Vec::new(); + if let Err(err) = + ciborium::into_writer(&ServerMessage::Error(err.clone()), &mut v) + { + log::info!("encoding error message for {who}: {err}"); + return; + } + v.into() + }) + }) + .await + .log_debug(); + log::info!("identification failed for {who}: {err}"); + if let Err(err) = socket.close().await { + log::warn!("closing socket on identification fail: {err}") + } + return; + } + }; + + // check if the plan is real + if let Err(err) = state.db.plan().get_plan(plan_id, ident.id).await { + log::info!("getting plan: {err}; rejecting connection"); + socket + .send({ + ws::Message::Binary({ + let mut v = Vec::new(); + if let Err(err) = + ciborium::into_writer(&ServerMessage::Error(err.clone()), &mut v) + { + log::info!("encoding error message for {who}: {err}"); + return; + } + v.into() + }) + }) + .await + .log_debug(); + } + // log::debug!("connected {who} as {ident}"); + let (send, recv) = tokio::sync::mpsc::channel(100); + let mut connection_id = ConnectionId::new(); + let token = loop { + match state + .session_manager + .add_connection(ident.id, connection_id, plan_id, send.clone()) + .await + { + Ok(token) => break token, + Err(SessionError::ConnectionIdExists) => connection_id = ConnectionId::new(), + } + }; + Session::new( + socket, + SessionIdentity { + plan_id, + user_id: ident.id, + user_name: ident.username.clone(), + }, + token, + recv, + state.message_sender, + ) + .run() + .await; + + // log::debug!("ending connection with {who}"); + }) +} + +async fn get_identification( + socket: &mut WebSocket, + who: &str, + db: Database, +) -> Result { + db.user() + .check_token( + &ciborium::from_reader::( + &socket + .recv() + .await + .ok_or(ServerError::ConnectionError)? + .map_err(|err| { + ServerError::InvalidRequest(format!("parse token request: {err}")) + })? + .into_data(), + )? + .0, + ) + .await +} + +struct Session { + socket: WebSocket, + identity: SessionIdentity, + connection_token: ConnectionDropToken, + recv: Receiver, + send: UnboundedSender<(SessionIdentity, ClientMessage)>, +} + +impl Session { + const fn new( + socket: WebSocket, + identity: SessionIdentity, + connection_token: ConnectionDropToken, + recv: Receiver, + send: UnboundedSender<(SessionIdentity, ClientMessage)>, + ) -> Self { + Self { + send, + recv, + socket, + identity, + connection_token, + } + } + + async fn handle_message(&mut self, message: ServerMessage) -> Result<(), anyhow::Error> { + self.socket + .send({ + ws::Message::Binary({ + let mut v = Vec::new(); + ciborium::into_writer(&message, &mut v)?; + v.into() + }) + }) + .await?; + Ok(()) + } + + async fn run(mut self) { + loop { + tokio::select! { + r = self.recv.recv() => { + match r { + Some(msg) => self.handle_message(msg).await.log_debug(), + None => { + log::info!("recv channel closed"); + return; + }, + } + } + r = self.socket.recv() => { + match r { + Some(Ok(msg)) => match ciborium::from_reader::(msg.into_data().iter().as_slice()) { + Ok(msg) => self.send.send((self.identity.clone(), msg)).log_debug(), + Err(err) => { + self.handle_message(ServerMessage::Error(ServerError::InvalidRequest( + err.to_string(), + ))) + .await + .log_debug(); + log::info!("error decoding client message: {err}"); + continue; + } + }, + Some(Err(err)) => { + log::warn!("socket error: {err}"); + return; + } + None => { + log::info!("socket closed"); + return; + }, + } + } + } + } + } +} diff --git a/plan/Cargo.lock b/plan/Cargo.lock new file mode 100644 index 0000000..1e6a637 --- /dev/null +++ b/plan/Cargo.lock @@ -0,0 +1,1531 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anymap2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "boolinator" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "calendar" +version = "0.1.0" +dependencies = [ + "chrono", + "futures", + "gloo 0.11.0", + "instant", + "log", + "once_cell", + "postcard", + "serde", + "thiserror 2.0.17", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-logger", + "web-sys", + "yew", + "yew-router", +] + +[[package]] +name = "cc" +version = "1.2.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.17", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gloo" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d" +dependencies = [ + "gloo-console 0.2.3", + "gloo-dialogs 0.1.1", + "gloo-events 0.1.2", + "gloo-file 0.2.3", + "gloo-history 0.1.5", + "gloo-net 0.3.1", + "gloo-render 0.1.1", + "gloo-storage 0.2.2", + "gloo-timers 0.2.6", + "gloo-utils 0.1.7", + "gloo-worker 0.2.1", +] + +[[package]] +name = "gloo" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd35526c28cc55c1db77aed6296de58677dbab863b118483a27845631d870249" +dependencies = [ + "gloo-console 0.3.0", + "gloo-dialogs 0.2.0", + "gloo-events 0.2.0", + "gloo-file 0.3.0", + "gloo-history 0.2.2", + "gloo-net 0.4.0", + "gloo-render 0.2.0", + "gloo-storage 0.3.0", + "gloo-timers 0.3.0", + "gloo-utils 0.2.0", + "gloo-worker 0.4.0", +] + +[[package]] +name = "gloo" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372" +dependencies = [ + "gloo-console 0.3.0", + "gloo-dialogs 0.2.0", + "gloo-events 0.2.0", + "gloo-file 0.3.0", + "gloo-history 0.2.2", + "gloo-net 0.5.0", + "gloo-render 0.2.0", + "gloo-storage 0.3.0", + "gloo-timers 0.3.0", + "gloo-utils 0.2.0", + "gloo-worker 0.5.0", +] + +[[package]] +name = "gloo-console" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f" +dependencies = [ + "gloo-utils 0.1.7", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-console" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261" +dependencies = [ + "gloo-utils 0.2.0", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-dialogs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-dialogs" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4748e10122b01435750ff530095b1217cf6546173459448b83913ebe7815df" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-events" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-events" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c26fb45f7c385ba980f5fa87ac677e363949e065a083722697ef1b2cc91e41" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-file" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7" +dependencies = [ + "gloo-events 0.1.2", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-file" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f" +dependencies = [ + "futures-channel", + "gloo-events 0.2.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-history" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85725d90bf0ed47063b3930ef28e863658a7905989e9929a8708aab74a1d5e7f" +dependencies = [ + "gloo-events 0.1.2", + "gloo-utils 0.1.7", + "serde", + "serde-wasm-bindgen 0.5.0", + "serde_urlencoded", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-history" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6" +dependencies = [ + "getrandom", + "gloo-events 0.2.0", + "gloo-utils 0.2.0", + "serde", + "serde-wasm-bindgen 0.6.5", + "serde_urlencoded", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66b4e3c7d9ed8d315fd6b97c8b1f74a7c6ecbbc2320e65ae7ed38b7068cc620" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.1.7", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ac9e8288ae2c632fa9f8657ac70bfe38a1530f345282d7ba66a1f70b72b7dc4" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.2.0", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.2.0", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56008b6744713a8e8d98ac3dcb7d06543d5662358c9c805b4ce2167ad4649833" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480" +dependencies = [ + "gloo-utils 0.1.7", + "js-sys", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" +dependencies = [ + "gloo-utils 0.2.0", + "js-sys", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13471584da78061a28306d1359dd0178d8d6fc1c7c80e5e35d27260346e0516a" +dependencies = [ + "anymap2", + "bincode", + "gloo-console 0.2.3", + "gloo-utils 0.1.7", + "js-sys", + "serde", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76495d3dd87de51da268fa3a593da118ab43eb7f8809e17eb38d3319b424e400" +dependencies = [ + "bincode", + "futures", + "gloo-utils 0.2.0", + "gloo-worker-macros", + "js-sys", + "pinned", + "serde", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085f262d7604911c8150162529cefab3782e91adb20202e8658f7275d2aefe5d" +dependencies = [ + "bincode", + "futures", + "gloo-utils 0.2.0", + "gloo-worker-macros", + "js-sys", + "pinned", + "serde", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-worker-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956caa58d4857bc9941749d55e4bd3000032d8212762586fa5705632967140e7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "implicit-clone" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a9aa791c7b5a71b636b7a68207fdebf171ddfc593d9c8506ec4cbc527b6a84" +dependencies = [ + "implicit-clone-derive", + "indexmap", +] + +[[package]] +name = "implicit-clone-derive" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "699c1b6d335e63d0ba5c1e1c7f647371ce989c3bcbe1f7ed2b85fa56e3bd1a21" +dependencies = [ + "quote", + "syn 2.0.106", +] + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pinned" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a829027bd95e54cfe13e3e258a1ae7b645960553fb82b75ff852c29688ee595b" +dependencies = [ + "futures", + "rustversion", + "thiserror 1.0.69", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "heapless", + "serde", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.106", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prokio" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b55e106e5791fa5a13abd13c85d6127312e8e09098059ca2bc9b03ca4cf488" +dependencies = [ + "futures", + "gloo 0.8.1", + "num_cpus", + "once_cell", + "pin-project", + "pinned", + "tokio", + "tokio-stream", + "wasm-bindgen-futures", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "route-recognizer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-logger" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074649a66bb306c8f2068c9016395fa65d8e08d2affcbf95acf3c24c3ab19718" +dependencies = [ + "log", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "yew" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f1a03f255c70c7aa3e9c62e15292f142ede0564123543c1cc0c7a4f31660cac" +dependencies = [ + "console_error_panic_hook", + "futures", + "gloo 0.10.0", + "implicit-clone", + "indexmap", + "js-sys", + "prokio", + "rustversion", + "serde", + "slab", + "thiserror 1.0.69", + "tokio", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "yew-macro", +] + +[[package]] +name = "yew-macro" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fd8ca5166d69e59f796500a2ce432ff751edecbbb308ca59fd3fe4d0343de2" +dependencies = [ + "boolinator", + "once_cell", + "prettyplease", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "yew-router" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca1d5052c96e6762b4d6209a8aded597758d442e6c479995faf0c7b5538e0c6" +dependencies = [ + "gloo 0.10.0", + "js-sys", + "route-recognizer", + "serde", + "serde_urlencoded", + "tracing", + "urlencoding", + "wasm-bindgen", + "web-sys", + "yew", + "yew-router-macro", +] + +[[package]] +name = "yew-router-macro" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42bfd190a07ca8cfde7cd4c52b3ac463803dc07323db8c34daa697e86365978c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] diff --git a/plan/Cargo.toml b/plan/Cargo.toml new file mode 100644 index 0000000..eab9809 --- /dev/null +++ b/plan/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "plan" +version = "0.1.0" +edition = "2024" + +[dependencies] +web-sys = { version = "0.3.77", features = [ + "HtmlTableCellElement", + "Event", + "EventTarget", + "HtmlImageElement", + "HtmlDivElement", + "HtmlSelectElement", + "DomException", + "KeyboardEvent", + "Navigator", + "Permissions", + "PermissionDescriptor", + "PermissionName", + "PermissionStatus", + "PermissionState", + "AbortController", + "HtmlSpanElement", + "ReadableStreamDefaultReader", +] } +log = "0.4" +yew = { version = "0.21", features = ["csr"] } +yew-router = "0.18.0" +serde = { version = "1.0", features = ["derive"] } +gloo = "0.11" +wasm-logger = "0.2" +instant = { version = "0.1", features = ["wasm-bindgen"] } +once_cell = "1" +chrono = "0.4.40" +futures = "0.3.31" +wasm-bindgen-futures = "0.4.50" +wasm-bindgen = { version = "0.2.100" } +postcard = "1.0.0" +thiserror = { version = "2" } +plan-proto = { path = "../plan-proto", features = ["client"] } +ciborium = { version = "0.2" } +chrono-humanize = { version = "0.2.3", features = ["wasmbind"] } diff --git a/plan/Trunk.toml b/plan/Trunk.toml new file mode 100644 index 0000000..104387a --- /dev/null +++ b/plan/Trunk.toml @@ -0,0 +1,15 @@ +[build] +target = "index.html" # The index HTML file to drive the bundling process. +html_output = "index.html" # The name of the output HTML file. +# release = false # Build in release mode. +release = true # Build in release mode. +dist = "dist" # The output dir for all final assets. +public_url = "/" # The public URL from which assets are to be served. +filehash = true # Whether to include hash values in the output file names. +inject_scripts = true # Whether to inject scripts (and module preloads) into the finalized output. +offline = false # Run without network access +frozen = false # Require Cargo.lock and cache are up to date +locked = false # Require Cargo.lock is up to date +minify = "on_release" # Control minification: can be one of: never, on_release, always +# minify = "always" # Control minification: can be one of: never, on_release, always +no_sri = false # Allow disabling sub-resource integrity (SRI) diff --git a/plan/dist/index-1cee5a016147b48c.css b/plan/dist/index-1cee5a016147b48c.css new file mode 100644 index 0000000..d482889 --- /dev/null +++ b/plan/dist/index-1cee5a016147b48c.css @@ -0,0 +1 @@ +body{background-color:#000;color:#fff;margin:0}body *{font-family:"Cute Font"}.content{margin:8px;display:flex;flex-direction:column;flex-wrap:nowrap;align-items:center}.nfc-form{font-size:2em;display:flex;flex-direction:column;flex-wrap:nowrap;align-items:center}.nfc-form button{margin-top:10px;font-size:1em}button{background-color:#000;color:#fff;border:1px solid #fff;cursor:pointer}button:hover{background-color:#fff;color:#000}button:disabled{border-color:rgba(255,255,255,.3);color:rgba(255,255,255,.3)}button:disabled:hover{background-color:rgba(255,255,255,.1)}.content{flex-direction:column;flex-wrap:nowrap;align-items:center}.error{flex-direction:column;flex-wrap:nowrap;align-items:center;background-color:rgba(255,0,0,.1);border:1px solid rgba(255,0,0,.3);text-align:center}.calendar{user-select:none;width:max-content}.date-span{font-size:2rem}.week{display:flex;flex-direction:row;flex-wrap:wrap;list-style:none;gap:1vw;max-width:80vw}@media only screen and (max-width: 999px){.day{width:50vw}}.day{padding:10px 30px 10px 30px;border:1px solid rgba(255,255,255,.3);display:flex;flex-direction:column;flex-wrap:nowrap;align-items:center}.date{font-size:1.2rem}.day-tiles{padding-left:0;display:flex;flex-direction:column;flex-wrap:nowrap;list-style:none}.tile{flex-grow:1;flex-shrink:1;padding:10px;cursor:pointer;color:rgba(255,255,255,.7)}.tile.pending{background-color:red;color:#000}.tile.pending:hover{background-color:rgba(255,255,255,.7)}.tile.selected-mine{color:#fff}.tile.selected-mine.border-middle{border-left:1px solid #fff;border-right:1px solid #fff}.tile.selected-mine.border-top{border-left:1px solid #fff;border-right:1px solid #fff;border-top:1px solid #fff}.tile.selected-mine.border-bottom{border-left:1px solid #fff;border-right:1px solid #fff;border-bottom:1px solid #fff}.tile.selected-mine.border-lone{border:1px solid #fff}.tile[style]{background-color:var(--color)}.tile:hover{backdrop-filter:invert(30%)}nav.user-nav{display:flex;flex-direction:row;flex-wrap:nowrap;gap:20px;user-select:none;width:max-content;padding:10px;margin:0;align-items:baseline;font-size:1rem}nav.user-nav .username{width:max-content}nav.user-nav .sign-out{position:absolute;right:20px}nav.user-nav button{padding-top:5px;padding-bottom:5px;font-size:1rem}.new-plan{display:flex;flex-direction:column;flex-wrap:nowrap;align-items:center;gap:10px}.field{display:flex;flex-direction:column;flex-wrap:nowrap;font-size:1.5em;width:100%}.days{display:flex;flex-direction:row;flex-wrap:wrap;gap:10px}.faint{filter:opacity(50%);font-size:.5em}.create-day{border:1px solid #fff;padding:10px;display:flex;flex-direction:column;align-items:flex-start;flex-wrap:nowrap}.create-day .remove{width:100%;text-align:center}.set-date{display:flex;flex-direction:row;flex-wrap:nowrap}.set-date>button{flex-grow:1}.set-date>.date{padding-left:5px;padding-right:5px}.date-detail{width:100%;text-align:center}.message{display:flex;flex-direction:column;width:100%;align-items:center;flex-wrap:nowrap}.click-backdrop{z-index:4;background-color:rgba(0,0,0,.7);position:fixed;top:0;left:0;height:200vh;width:100vw;background-size:cover}.dialog{z-index:5;width:100vw;height:100vh;display:flex;flex-direction:column;flex-wrap:nowrap;position:fixed;top:0;left:0;align-items:center;justify-content:center}.dialog .dialog-box{font-size:2rem;border:1px solid #fff;display:flex;flex-direction:column;flex-wrap:nowrap;align-items:center;padding-left:30px;padding-right:30px;padding-top:10px;padding-bottom:10px;background-color:#000}.dialog .dialog-box .options{display:flex;flex-direction:row;flex-wrap:wrap;gap:20px}.dialog .dialog-box .options>button{min-width:4cm;font-size:1em}.users-available{list-style:none}.users-available .user{display:flex;flex-direction:column;flex-wrap:nowrap;align-items:center}.users-available .user:hover{background-color:rgba(255,255,255,.3)}.users-available [last_available]:hover::after{display:block;position:absolute;content:attr(last_available);border:1px solid #fff;background:rgba(0,0,0,.7);padding:.25em}.signup,.signin{display:flex;flex-direction:column;align-items:center;font-size:1.5rem}.signup input,.signin input{background-color:#000;border:1px solid #fff;color:#fff}.signup .submit,.signin .submit{margin-top:30px;font-size:1.5rem}.fields{display:flex;align-items:center;flex-direction:column;gap:10px}@media only screen and (max-width: 999px){.created-plans,.participating-plans{width:100%}}@media only screen and (min-width: 1000px){.created-plans,.participating-plans{width:40vw}}.created-plans,.participating-plans{padding:0 30px 0 30px;align-items:center;display:flex;gap:10px;flex-direction:column}.plan-column{width:100%}.main-plans{user-select:none;text-align:center;display:flex;flex-direction:row;flex-wrap:wrap;gap:30px}.plans{list-style:none;display:flex;flex-direction:column;gap:10px;padding-left:0}.plans .plan-headline button{width:100%}.plans .plan-detail{display:flex;flex-direction:row;gap:30px;font-size:1.5rem;padding:10px}.plans .plan-detail .start-date{filter:opacity(50%)}.splash{display:flex;flex-direction:column;flex-wrap:nowrap;align-items:center;width:100%}.splash .options{display:flex;flex-direction:row;flex-wrap:wrap;gap:1cm}.splash .options button{font-size:2rem} diff --git a/plan/dist/index.html b/plan/dist/index.html new file mode 100644 index 0000000..e7e4500 --- /dev/null +++ b/plan/dist/index.html @@ -0,0 +1,8 @@ +plan \ No newline at end of file diff --git a/plan/dist/plan-9b4f658762b65072.js b/plan/dist/plan-9b4f658762b65072.js new file mode 100644 index 0000000..07e54bc --- /dev/null +++ b/plan/dist/plan-9b4f658762b65072.js @@ -0,0 +1 @@ +let T=`boolean`,a5=977,V=`function`,K=null,N=`utf-8`,_=`undefined`,Q=1,R=3,U=`string`,L=0,S=`number`,P=4,X=`Object`,a0=4294967297,W=Array,a3=Date,Y=Error,Z=FinalizationRegistry,$=Number,a1=Object,a6=Object.getPrototypeOf,a2=Reflect,M=Uint8Array,a4=globalThis,O=undefined;var k=(a=>a===O||a===K);var z=((b,c)=>{a.wasm_bindgen__convert__closures_____invoke__h829941794c876fed(b,c)});var x=((b,c,d)=>{a.closure1018_externref_shim(b,c,d)});var u=((b,c,d,e)=>{const f={a:b,b:c,cnt:Q,dtor:d};const g=(...b)=>{f.cnt++;const c=f.a;f.a=L;try{return e(c,f.b,...b)}finally{if(--f.cnt===L){a.__wbindgen_export_7.get(f.dtor)(c,f.b);t.unregister(f)}else{f.a=c}}};g.original=f;t.register(g,f,f);return g});var g=((a,b)=>{f+=b;if(f>=e){d=new TextDecoder(N,{ignoreBOM:!0,fatal:!0});d.decode();f=b};return d.decode(c().subarray(a,a+ b))});var c=(()=>{if(b===K||b.byteLength===L){b=new M(a.memory.buffer)};return b});var A=((b,c,d)=>{a.closure872_externref_shim(b,c,d)});var q=((a,b,d)=>{if(d===O){const d=p.encode(a);const e=b(d.length,Q)>>>L;c().subarray(e,e+ d.length).set(d);o=d.length;return e};let e=a.length;let f=b(e,Q)>>>L;const g=c();let h=L;for(;h127)break;g[f+ h]=b};if(h!==e){if(h!==L){a=a.slice(h)};f=d(f,e,e=h+ a.length*R,Q)>>>L;const b=c().subarray(f+ h,f+ e);const g=p.encodeInto(a,b);h+=g.written;f=d(f,e,h,Q)>>>L};o=h;return f});var m=(()=>{if(l===K||l.buffer.detached===!0||l.buffer.detached===O&&l.buffer!==a.memory.buffer){l=new DataView(a.memory.buffer)};return l});var F=(()=>{const b={};b.wbg={};b.wbg.__wbg_Error_e17e777aac105295=((a,b)=>{const c=Y(h(a,b));return c});b.wbg.__wbg_Number_998bea33bd87c3e0=(a=>{const b=$(a);return b});b.wbg.__wbg_addEventListener_775911544ac9d643=function(){return j(((a,b,c,d)=>{a.addEventListener(h(b,c),d)}),arguments)};b.wbg.__wbg_addEventListener_d1c39a5c2329c624=function(){return j(((a,b,c,d,e)=>{a.addEventListener(h(b,c),d,e)}),arguments)};b.wbg.__wbg_arrayBuffer_9c99b8e2809e8cbb=function(){return j((a=>{const b=a.arrayBuffer();return b}),arguments)};b.wbg.__wbg_bubbles_c2875b63b0f1f311=(a=>{const b=a.bubbles;return b});b.wbg.__wbg_cachekey_57601dac16343711=(a=>{const b=a.__yew_subtree_cache_key;return k(b)?a0:b>>>L});b.wbg.__wbg_call_13410aac570ffff7=function(){return j(((a,b)=>{const c=a.call(b);return c}),arguments)};b.wbg.__wbg_cancelBubble_a4c48803e199b5e8=(a=>{const b=a.cancelBubble;return b});b.wbg.__wbg_childNodes_5c44c2ec67a90732=(a=>{const b=a.childNodes;return b});b.wbg.__wbg_clearTimeout_96804de0ab838f26=(a=>{const b=clearTimeout(a);return b});b.wbg.__wbg_cloneNode_79d46b18d5619863=function(){return j((a=>{const b=a.cloneNode();return b}),arguments)};b.wbg.__wbg_close_6437264570d2d37f=function(){return j((a=>{a.close()}),arguments)};b.wbg.__wbg_code_177e3bed72688e58=(a=>{const b=a.code;return b});b.wbg.__wbg_code_89056d52bf1a8bb0=(a=>{const b=a.code;return b});b.wbg.__wbg_composedPath_e5b3f0b3e8415bb5=(a=>{const b=a.composedPath();return b});b.wbg.__wbg_createElementNS_ffbb8bb20b2a7e4c=function(){return j(((a,b,c,d,e)=>{const f=a.createElementNS(b===L?O:h(b,c),h(d,e));return f}),arguments)};b.wbg.__wbg_createElement_4909dfa2011f2abe=function(){return j(((a,b,c)=>{const d=a.createElement(h(b,c));return d}),arguments)};b.wbg.__wbg_createTextNode_c71a51271fadf515=((a,b,c)=>{const d=a.createTextNode(h(b,c));return d});b.wbg.__wbg_data_9ab529722bcc4e6c=(a=>{const b=a.data;return b});b.wbg.__wbg_debug_7f3000e7358ea482=((a,b,c,d)=>{console.debug(a,b,c,d)});b.wbg.__wbg_dispatchEvent_cb7e5ff30130cf80=function(){return j(((a,b)=>{const c=a.dispatchEvent(b);return c}),arguments)};b.wbg.__wbg_document_7d29d139bd619045=(a=>{const b=a.document;return k(b)?L:i(b)});b.wbg.__wbg_entries_2be2f15bd5554996=(a=>{const b=a1.entries(a);return b});b.wbg.__wbg_error_0889f151acea569e=((a,b,c,d)=>{console.error(a,b,c,d)});b.wbg.__wbg_error_3c7d958458bf649b=((b,c)=>{var d=n(b,c).slice();a.__wbindgen_free(b,c*P,P);console.error(...d)});b.wbg.__wbg_error_7534b8e9a36f1ab4=((b,c)=>{let d;let e;try{d=b;e=c;console.error(h(b,c))}finally{a.__wbindgen_free(d,e,Q)}});b.wbg.__wbg_error_99981e16d476aa5c=(a=>{console.error(a)});b.wbg.__wbg_fetch_44b6058021aef5e3=((a,b)=>{const c=a.fetch(b);return c});b.wbg.__wbg_fetch_87aed7f306ec6d63=((a,b)=>{const c=a.fetch(b);return c});b.wbg.__wbg_focus_8541343802c6721b=function(){return j((a=>{a.focus()}),arguments)};b.wbg.__wbg_from_88bc52ce20ba6318=(a=>{const b=W.from(a);return b});b.wbg.__wbg_getItem_9fc74b31b896f95a=function(){return j(((b,c,d,e)=>{const f=c.getItem(h(d,e));var g=k(f)?L:q(f,a.__wbindgen_malloc,a.__wbindgen_realloc);var i=o;m().setInt32(b+ P*Q,i,!0);m().setInt32(b+ P*L,g,!0)}),arguments)};b.wbg.__wbg_getTime_6bb3f64e0f18f817=(a=>{const b=a.getTime();return b});b.wbg.__wbg_getTimezoneOffset_1e3ddc1382e7c8b0=(a=>{const b=a.getTimezoneOffset();return b});b.wbg.__wbg_get_0da715ceaecea5c8=((a,b)=>{const c=a[b>>>L];return c});b.wbg.__wbg_get_458e874b43b18b25=function(){return j(((a,b)=>{const c=a2.get(a,b);return c}),arguments)};b.wbg.__wbg_getwithrefkey_1dc361bd10053bfe=((a,b)=>{const c=a[b];return c});b.wbg.__wbg_hash_0ce7fe010ac2cc6b=((b,c)=>{const d=c.hash;const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)});b.wbg.__wbg_hash_61a93e84f71459f5=function(){return j(((b,c)=>{const d=c.hash;const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)}),arguments)};b.wbg.__wbg_history_bf9f443b5be043de=function(){return j((a=>{const b=a.history;return b}),arguments)};b.wbg.__wbg_host_484d55073e076054=(a=>{const b=a.host;return b});b.wbg.__wbg_href_33fba0e78b8c3084=((b,c)=>{const d=c.href;const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)});b.wbg.__wbg_href_65a798194bf5ead5=function(){return j(((b,c)=>{const d=c.href;const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)}),arguments)};b.wbg.__wbg_id_cf626ed5d83cb98d=((b,c)=>{const d=c.id;const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)});b.wbg.__wbg_info_15c3631232fceddb=((a,b,c,d)=>{console.info(a,b,c,d)});b.wbg.__wbg_insertBefore_30228206e8f1d3fb=function(){return j(((a,b,c)=>{const d=a.insertBefore(b,c);return d}),arguments)};b.wbg.__wbg_instanceof_ArrayBuffer_67f3012529f6a2dd=(a=>{let b;try{b=a instanceof ArrayBuffer}catch(a){b=!1}const c=b;return c});b.wbg.__wbg_instanceof_Element_162e4334c7d6f450=(a=>{let b;try{b=a instanceof Element}catch(a){b=!1}const c=b;return c});b.wbg.__wbg_instanceof_Error_76149ae9b431750e=(a=>{let b;try{b=a instanceof Y}catch(a){b=!1}const c=b;return c});b.wbg.__wbg_instanceof_HtmlElement_d60c51c41eb8699a=(a=>{let b;try{b=a instanceof HTMLElement}catch(a){b=!1}const c=b;return c});b.wbg.__wbg_instanceof_HtmlInputElement_486d1ca974d99353=(a=>{let b;try{b=a instanceof HTMLInputElement}catch(a){b=!1}const c=b;return c});b.wbg.__wbg_instanceof_HtmlSelectElement_432da62d310182dc=(a=>{let b;try{b=a instanceof HTMLSelectElement}catch(a){b=!1}const c=b;return c});b.wbg.__wbg_instanceof_Response_50fde2cd696850bf=(a=>{let b;try{b=a instanceof Response}catch(a){b=!1}const c=b;return c});b.wbg.__wbg_instanceof_ShadowRoot_f3723967133597a3=(a=>{let b;try{b=a instanceof ShadowRoot}catch(a){b=!1}const c=b;return c});b.wbg.__wbg_instanceof_Uint8Array_9a8378d955933db7=(a=>{let b;try{b=a instanceof M}catch(a){b=!1}const c=b;return c});b.wbg.__wbg_instanceof_Window_12d20d558ef92592=(a=>{let b;try{b=a instanceof Window}catch(a){b=!1}const c=b;return c});b.wbg.__wbg_instanceof_WorkerGlobalScope_85d487cc157fd065=(a=>{let b;try{b=a instanceof WorkerGlobalScope}catch(a){b=!1}const c=b;return c});b.wbg.__wbg_isSafeInteger_1c0d1af5542e102a=(a=>{const b=$.isSafeInteger(a);return b});b.wbg.__wbg_is_8346b6c36feaf71a=((a,b)=>{const c=a1.is(a,b);return c});b.wbg.__wbg_key_caac8fafdd6d5317=((b,c)=>{const d=c.key;const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)});b.wbg.__wbg_lastChild_0f60bc497b807d25=(a=>{const b=a.lastChild;return k(b)?L:i(b)});b.wbg.__wbg_length_186546c51cd61acd=(a=>{const b=a.length;return b});b.wbg.__wbg_length_6bb7e81f9d7713e4=(a=>{const b=a.length;return b});b.wbg.__wbg_listenerid_ed1678830a5b97ec=(a=>{const b=a.__yew_listener_id;return k(b)?a0:b>>>L});b.wbg.__wbg_localStorage_9330af8bf39365ba=function(){return j((a=>{const b=a.localStorage;return k(b)?L:i(b)}),arguments)};b.wbg.__wbg_location_92d89c32ae076cab=(a=>{const b=a.location;return b});b.wbg.__wbg_log_ddbf5bc3d4dae44c=((a,b,c,d)=>{console.log(a,b,c,d)});b.wbg.__wbg_message_125a1b2998b3552a=(a=>{const b=a.message;return b});b.wbg.__wbg_message_5481231e71ccaf7b=((b,c)=>{const d=c.message;const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)});b.wbg.__wbg_name_f733db82b3c2804d=(a=>{const b=a.name;return b});b.wbg.__wbg_name_f75f535832c8ea6b=((b,c)=>{const d=c.name;const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)});b.wbg.__wbg_namespaceURI_020a81e6d28c2c96=((b,c)=>{const d=c.namespaceURI;var e=k(d)?L:q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);var f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)});b.wbg.__wbg_new0_b0a0a38c201e6df5=(()=>{const a=new a3();return a});b.wbg.__wbg_new_19c25a3f2fa63a02=(()=>{const a=new a1();return a});b.wbg.__wbg_new_5a2ae4557f92b50e=(a=>{const b=new a3(a);return b});b.wbg.__wbg_new_638ebfaedbf32a5e=(a=>{const b=new M(a);return b});b.wbg.__wbg_new_8a6f238a6ece86ea=(()=>{const a=new Y();return a});b.wbg.__wbg_new_95e31b8bc5de31d6=function(){return j(((a,b)=>{const c=new URL(h(a,b));return c}),arguments)};b.wbg.__wbg_new_a9b6e13df060b671=function(){return j(((a,b)=>{const c=new MouseEvent(h(a,b));return c}),arguments)};b.wbg.__wbg_new_e213f63d18b0de01=function(){return j(((a,b)=>{const c=new WebSocket(h(a,b));return c}),arguments)};b.wbg.__wbg_new_e8b27dfd3785875f=function(){return j((()=>{const a=new URLSearchParams();return a}),arguments)};b.wbg.__wbg_new_f6e53210afea8e45=function(){return j((()=>{const a=new Headers();return a}),arguments)};b.wbg.__wbg_newnoargs_254190557c45b4ec=((a,b)=>{const c=new Function(h(a,b));return c});b.wbg.__wbg_newwithbase_96f007ba18c568ff=function(){return j(((a,b,c,d)=>{const e=new URL(h(a,b),h(c,d));return e}),arguments)};b.wbg.__wbg_newwitheventinitdict_fe21d8ccd6054401=function(){return j(((a,b,c)=>{const d=new CloseEvent(h(a,b),c);return d}),arguments)};b.wbg.__wbg_newwithstr_1bc70be98f2e7425=function(){return j(((a,b)=>{const c=new Request(h(a,b));return c}),arguments)};b.wbg.__wbg_newwithstrandinit_b5d168a29a3fd85f=function(){return j(((a,b,c)=>{const d=new Request(h(a,b),c);return d}),arguments)};b.wbg.__wbg_newwithyearmonthdayhrminsec_3194e83daea43c6d=((a,b,c,d,e,f)=>{const g=new a3(a>>>L,b,c,d,e,f);return g});b.wbg.__wbg_nextSibling_1fb03516719cac0f=(a=>{const b=a.nextSibling;return k(b)?L:i(b)});b.wbg.__wbg_ok_2eac216b65d90573=(a=>{const b=a.ok;return b});b.wbg.__wbg_outerHTML_5fe297cb1fc146f2=((b,c)=>{const d=c.outerHTML;const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)});b.wbg.__wbg_parentElement_48440a9a6ba034a8=(a=>{const b=a.parentElement;return k(b)?L:i(b)});b.wbg.__wbg_parentNode_cc820baee7401ca3=(a=>{const b=a.parentNode;return k(b)?L:i(b)});b.wbg.__wbg_pathname_e7278f48b2a6a5ad=((b,c)=>{const d=c.pathname;const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)});b.wbg.__wbg_pathname_fdb9cca2dd58c31b=function(){return j(((b,c)=>{const d=c.pathname;const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)}),arguments)};b.wbg.__wbg_prototypesetcall_3d4a26c1ed734349=((a,b,c)=>{M.prototype.set.call(r(a,b),c)});b.wbg.__wbg_querySelector_2bda8764de90ea1d=function(){return j(((a,b,c)=>{const d=a.querySelector(h(b,c));return k(d)?L:i(d)}),arguments)};b.wbg.__wbg_querySelector_438f2df379fbb8d2=function(){return j(((a,b,c)=>{const d=a.querySelector(h(b,c));return k(d)?L:i(d)}),arguments)};b.wbg.__wbg_queueMicrotask_25d0739ac89e8c88=(a=>{queueMicrotask(a)});b.wbg.__wbg_queueMicrotask_4488407636f5bf24=(a=>{const b=a.queueMicrotask;return b});b.wbg.__wbg_readyState_b0d20ca4531d3797=(a=>{const b=a.readyState;return b});b.wbg.__wbg_reason_97efd955be6394bd=((b,c)=>{const d=c.reason;const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)});b.wbg.__wbg_reload_9fde301bc335e70d=function(){return j((a=>{a.reload()}),arguments)};b.wbg.__wbg_removeAttribute_cf35412842be6ae4=function(){return j(((a,b,c)=>{a.removeAttribute(h(b,c))}),arguments)};b.wbg.__wbg_removeChild_1c094e96ff042c2d=function(){return j(((a,b)=>{const c=a.removeChild(b);return c}),arguments)};b.wbg.__wbg_removeEventListener_6d5be9c2821a511e=function(){return j(((a,b,c,d)=>{a.removeEventListener(h(b,c),d)}),arguments)};b.wbg.__wbg_removeEventListener_b370c9d66874eec6=function(){return j(((a,b,c,d,e)=>{a.removeEventListener(h(b,c),d,e!==L)}),arguments)};b.wbg.__wbg_removeItem_487c5a070c7adaf7=function(){return j(((a,b,c)=>{a.removeItem(h(b,c))}),arguments)};b.wbg.__wbg_resolve_4055c623acdd6a1b=(a=>{const b=Promise.resolve(a);return b});b.wbg.__wbg_search_3a02a8f2a1a2e604=((b,c)=>{const d=c.search;const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)});b.wbg.__wbg_search_73c5c4925b506661=function(){return j(((b,c)=>{const d=c.search;const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)}),arguments)};b.wbg.__wbg_selectedIndex_71fb145a2e58f226=(a=>{const b=a.selectedIndex;return b});b.wbg.__wbg_send_aa9cb445685f0fd0=function(){return j(((a,b,c)=>{a.send(r(b,c))}),arguments)};b.wbg.__wbg_send_bdda9fac7465e036=function(){return j(((a,b,c)=>{a.send(h(b,c))}),arguments)};b.wbg.__wbg_setAttribute_d1baf9023ad5696f=function(){return j(((a,b,c,d,e)=>{a.setAttribute(h(b,c),h(d,e))}),arguments)};b.wbg.__wbg_setItem_7add5eb06a28b38f=function(){return j(((a,b,c,d,e)=>{a.setItem(h(b,c),h(d,e))}),arguments)};b.wbg.__wbg_setTimeout_eefe7f4c234b0c6b=function(){return j(((a,b)=>{const c=setTimeout(a,b);return c}),arguments)};b.wbg.__wbg_set_1c17f9738fac2718=function(){return j(((a,b,c,d,e)=>{a.set(h(b,c),h(d,e))}),arguments)};b.wbg.__wbg_set_453345bcda80b89a=function(){return j(((a,b,c)=>{const d=a2.set(a,b,c);return d}),arguments)};b.wbg.__wbg_setbinaryType_37f3cd35d7775a47=((a,b)=>{a.binaryType=C[b]});b.wbg.__wbg_setbody_c8460bdf44147df8=((a,b)=>{a.body=b});b.wbg.__wbg_setcachekey_bb5f908a0e3ee714=((a,b)=>{a.__yew_subtree_cache_key=b>>>L});b.wbg.__wbg_setcapture_9a5af32fdb4619e5=((a,b)=>{a.capture=b!==L});b.wbg.__wbg_setchecked_0c6fdca893f4a16f=((a,b)=>{a.checked=b!==L});b.wbg.__wbg_setcode_3d8eb30649405e81=((a,b)=>{a.code=b});b.wbg.__wbg_setheaders_0052283e2f3503d1=((a,b)=>{a.headers=b});b.wbg.__wbg_sethref_07131e420ded2edd=function(){return j(((a,b,c)=>{a.href=h(b,c)}),arguments)};b.wbg.__wbg_setinnerHTML_34e240d6b8e8260c=((a,b,c)=>{a.innerHTML=h(b,c)});b.wbg.__wbg_setlistenerid_3d14d37a42484593=((a,b)=>{a.__yew_listener_id=b>>>L});b.wbg.__wbg_setmethod_9b504d5b855b329c=((a,b,c)=>{a.method=h(b,c)});b.wbg.__wbg_setnodeValue_629799145cb84fd8=((a,b,c)=>{a.nodeValue=b===L?O:h(b,c)});b.wbg.__wbg_setonce_5b860c1f79d40d3b=((a,b)=>{a.once=b!==L});b.wbg.__wbg_setpassive_be202eba558bf454=((a,b)=>{a.passive=b!==L});b.wbg.__wbg_setreason_d56f0662fe6c410f=((a,b,c)=>{a.reason=h(b,c)});b.wbg.__wbg_setsearch_fbee2174e0389ccd=((a,b,c)=>{a.search=h(b,c)});b.wbg.__wbg_setsubtreeid_32b8ceff55862e29=((a,b)=>{a.__yew_subtree_id=b>>>L});b.wbg.__wbg_settitle_af9e0fec4dbb74d6=((a,b,c)=>{a.title=h(b,c)});b.wbg.__wbg_setvalue_06e05f2cb17fefdd=((a,b,c)=>{a.value=h(b,c)});b.wbg.__wbg_setvalue_4c91a711c0108335=((a,b,c)=>{a.value=h(b,c)});b.wbg.__wbg_stack_0ed75d68575b0f3c=((b,c)=>{const d=c.stack;const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)});b.wbg.__wbg_state_a77e7af597629742=function(){return j((a=>{const b=a.state;return b}),arguments)};b.wbg.__wbg_static_accessor_GLOBAL_8921f820c2ce3f12=(()=>{const a=typeof global===_?K:global;return k(a)?L:i(a)});b.wbg.__wbg_static_accessor_GLOBAL_THIS_f0a4409105898184=(()=>{const a=typeof a4===_?K:a4;return k(a)?L:i(a)});b.wbg.__wbg_static_accessor_SELF_995b214ae681ff99=(()=>{const a=typeof self===_?K:self;return k(a)?L:i(a)});b.wbg.__wbg_static_accessor_WINDOW_cde3890479c675ea=(()=>{const a=typeof window===_?K:window;return k(a)?L:i(a)});b.wbg.__wbg_statusText_c285fe96dbd990df=((b,c)=>{const d=c.statusText;const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)});b.wbg.__wbg_status_3fea3036088621d6=(a=>{const b=a.status;return b});b.wbg.__wbg_subtreeid_e65dfcc52d403fd9=(a=>{const b=a.__yew_subtree_id;return k(b)?a0:b>>>L});b.wbg.__wbg_tagName_0052175ec2444f12=((b,c)=>{const d=c.tagName;const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)});b.wbg.__wbg_target_f2c963b447be6283=(a=>{const b=a.target;return k(b)?L:i(b)});b.wbg.__wbg_textContent_4e2b2a6c46694642=((b,c)=>{const d=c.textContent;var e=k(d)?L:q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);var f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)});b.wbg.__wbg_then_b33a773d723afa3e=((a,b,c)=>{const d=a.then(b,c);return d});b.wbg.__wbg_then_e22500defe16819f=((a,b)=>{const c=a.then(b);return c});b.wbg.__wbg_toString_78df35411a4fd40c=(a=>{const b=a.toString();return b});b.wbg.__wbg_toString_d8f537919ef401d6=(a=>{const b=a.toString();return b});b.wbg.__wbg_url_79bd91c4e84e8270=((b,c)=>{const d=c.url;const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)});b.wbg.__wbg_value_57637cc189f7a639=((b,c)=>{const d=c.value;const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)});b.wbg.__wbg_value_fdf54c7557edc2e8=((b,c)=>{const d=c.value;const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)});b.wbg.__wbg_warn_90eb15d986910fe9=((a,b,c,d)=>{console.warn(a,b,c,d)});b.wbg.__wbg_wasClean_ffb515fbcbcbdd3d=(a=>{const b=a.wasClean;return b});b.wbg.__wbg_wbindgenbooleanget_3fe6f642c7d97746=(a=>{const b=a;const c=typeof b===T?b:O;return k(c)?16777215:c?Q:L});b.wbg.__wbg_wbindgencbdrop_eb10308566512b88=(a=>{const b=a.original;if(b.cnt--==Q){b.a=L;return !0};const c=!1;return c});b.wbg.__wbg_wbindgendebugstring_99ef257a3ddda34d=((b,c)=>{const d=s(c);const e=q(d,a.__wbindgen_malloc,a.__wbindgen_realloc);const f=o;m().setInt32(b+ P*Q,f,!0);m().setInt32(b+ P*L,e,!0)});b.wbg.__wbg_wbindgenin_d7a1ee10933d2d55=((a,b)=>{const c=a in b;return c});b.wbg.__wbg_wbindgenisfunction_8cee7dce3725ae74=(a=>{const b=typeof a===V;return b});b.wbg.__wbg_wbindgenisobject_307a53c6bd97fbf8=(a=>{const b=a;const c=typeof b===`object`&&b!==K;return c});b.wbg.__wbg_wbindgenisstring_d4fa939789f003b0=(a=>{const b=typeof a===U;return b});b.wbg.__wbg_wbindgenisundefined_c4b71d073b92f3c5=(a=>{const b=a===O;return b});b.wbg.__wbg_wbindgenjsvallooseeq_9bec8c9be826bed1=((a,b)=>{const c=a==b;return c});b.wbg.__wbg_wbindgennumberget_f74b4c7525ac05cb=((a,b)=>{const c=b;const d=typeof c===S?c:O;m().setFloat64(a+ 8*Q,k(d)?L:d,!0);m().setInt32(a+ P*L,!k(d),!0)});b.wbg.__wbg_wbindgenstringget_0f16a6ddddef376f=((b,c)=>{const d=c;const e=typeof d===U?d:O;var f=k(e)?L:q(e,a.__wbindgen_malloc,a.__wbindgen_realloc);var g=o;m().setInt32(b+ P*Q,g,!0);m().setInt32(b+ P*L,f,!0)});b.wbg.__wbg_wbindgenthrow_451ec1a8469d7eb6=((a,b)=>{throw new Y(h(a,b))});b.wbg.__wbindgen_cast_00b46a24780e5aba=((a,b)=>{const c=u(a,b,a5,y);return c});b.wbg.__wbindgen_cast_18c348f059880d1c=((a,b)=>{const c=u(a,b,1029,w);return c});b.wbg.__wbindgen_cast_2241b6af4c4b2941=((a,b)=>{const c=h(a,b);return c});b.wbg.__wbindgen_cast_6eb4b54fc77bcc6f=((a,b)=>{const c=u(a,b,a5,z);return c});b.wbg.__wbindgen_cast_710e797c1e89866e=((a,b)=>{const c=v(a,b,871,A);return c});b.wbg.__wbindgen_cast_77bc3e92745e9a35=((b,c)=>{var d=r(b,c).slice();a.__wbindgen_free(b,c*Q,Q);const e=d;return e});b.wbg.__wbindgen_cast_969d5d473d4a5a92=((a,b)=>{const c=u(a,b,a5,y);return c});b.wbg.__wbindgen_cast_a2c1d45f46cd4c19=((a,b)=>{const c=u(a,b,a5,y);return c});b.wbg.__wbindgen_cast_acac365039f9bb7c=((a,b)=>{const c=u(a,b,945,B);return c});b.wbg.__wbindgen_cast_d6cd19b81560fd6e=(a=>{const b=a;return b});b.wbg.__wbindgen_cast_e2b597ec861af5b7=((a,b)=>{const c=u(a,b,1017,x);return c});b.wbg.__wbindgen_init_externref_table=(()=>{const b=a.__wbindgen_export_2;const c=b.grow(P);b.set(L,O);b.set(c+ L,O);b.set(c+ Q,K);b.set(c+ 2,!0);b.set(c+ R,!1)});return b});var v=((b,c,d,e)=>{const f={a:b,b:c,cnt:Q,dtor:d};const g=(...b)=>{f.cnt++;try{return e(f.a,f.b,...b)}finally{if(--f.cnt===L){a.__wbindgen_export_7.get(f.dtor)(f.a,f.b);f.a=L;t.unregister(f)}}};g.original=f;t.register(g,f,f);return g});var w=((b,c,d)=>{a.closure1030_externref_shim(b,c,d)});var I=(b=>{if(a!==O)return a;if(typeof b!==_){if(a6(b)===a1.prototype){({module:b}=b)}else{console.warn(`using deprecated parameters for \`initSync()\`; pass a single object instead`)}};const c=F();G(c);if(!(b instanceof WebAssembly.Module)){b=new WebAssembly.Module(b)};const d=new WebAssembly.Instance(b,c);return H(d,b)});var E=(async(a,b)=>{if(typeof Response===V&&a instanceof Response){if(typeof WebAssembly.instantiateStreaming===V){try{return await WebAssembly.instantiateStreaming(a,b)}catch(b){const c=a.ok&&D.has(a.type);if(c&&a.headers.get(`Content-Type`)!==`application/wasm`){console.warn(`\`WebAssembly.instantiateStreaming\` failed because your server does not serve Wasm with \`application/wasm\` MIME type. Falling back to \`WebAssembly.instantiate\` which is slower. Original error:\\n`,b)}else{throw b}}};const c=await a.arrayBuffer();return await WebAssembly.instantiate(c,b)}else{const c=await WebAssembly.instantiate(a,b);if(c instanceof WebAssembly.Instance){return {instance:c,module:a}}else{return c}}});var y=((b,c,d)=>{a.closure978_externref_shim(b,c,d)});var h=((a,b)=>{a=a>>>L;return g(a,b)});var J=(async(b)=>{if(a!==O)return a;if(typeof b!==_){if(a6(b)===a1.prototype){({module_or_path:b}=b)}else{console.warn(`using deprecated parameters for the initialization function; pass a single object instead`)}};if(typeof b===_){b=new URL(`plan_bg.wasm`,import.meta.url)};const c=F();if(typeof b===U||typeof Request===V&&b instanceof Request||typeof URL===V&&b instanceof URL){b=fetch(b)};G(c);const {instance:d,module:e}=await E(await b,c);return H(d,e)});var s=(a=>{const b=typeof a;if(b==S||b==T||a==K){return `${a}`};if(b==U){return `"${a}"`};if(b==`symbol`){const b=a.description;if(b==K){return `Symbol`}else{return `Symbol(${b})`}};if(b==V){const b=a.name;if(typeof b==U&&b.length>L){return `Function(${b})`}else{return `Function`}};if(W.isArray(a)){const b=a.length;let c=`[`;if(b>L){c+=s(a[L])};for(let d=Q;dQ){d=c[Q]}else{return toString.call(a)};if(d==X){try{return `Object(`+ JSON.stringify(a)+ `)`}catch(a){return X}};if(a instanceof Y){return `${a.name}: ${a.message}\n${a.stack}`};return d});var i=(b=>{const c=a.__externref_table_alloc();a.__wbindgen_export_2.set(c,b);return c});var G=((a,b)=>{});var H=((c,d)=>{a=c.exports;J.__wbindgen_wasm_module=d;l=K;b=K;a.__wbindgen_start();return a});var n=((b,c)=>{b=b>>>L;const d=m();const e=[];for(let f=b;f{a=a>>>L;return c().subarray(a/Q,a/Q+ b)});var B=((b,c)=>{a.wasm_bindgen__convert__closures_____invoke__h8c3f7762626cfd0d(b,c)});function j(b,c){try{return b.apply(this,c)}catch(b){const c=i(b);a.__wbindgen_exn_store(c)}}let a;let b=K;let d=new TextDecoder(N,{ignoreBOM:!0,fatal:!0});d.decode();const e=2146435072;let f=L;let l=K;let o=L;const p=new TextEncoder();if(!(`encodeInto` in p)){p.encodeInto=((a,b)=>{const c=p.encode(a);b.set(c);return {read:a.length,written:c.length}})};const t=typeof Z===_?{register:()=>{},unregister:()=>{}}:new Z(b=>{a.__wbindgen_export_7.get(b.dtor)(b.a,b.b)});const C=[`blob`,`arraybuffer`];const D=new Set([`basic`,`cors`,`default`]);export default J;export{I as initSync} \ No newline at end of file diff --git a/plan/dist/plan-9b4f658762b65072_bg.wasm b/plan/dist/plan-9b4f658762b65072_bg.wasm new file mode 100644 index 0000000..47169e3 Binary files /dev/null and b/plan/dist/plan-9b4f658762b65072_bg.wasm differ diff --git a/plan/index.html b/plan/index.html new file mode 100644 index 0000000..aefe204 --- /dev/null +++ b/plan/index.html @@ -0,0 +1,15 @@ + + + + + + + plan + + + + + + + + diff --git a/plan/index.scss b/plan/index.scss new file mode 100644 index 0000000..44f5323 --- /dev/null +++ b/plan/index.scss @@ -0,0 +1,477 @@ +@use 'sass:color'; + +body { + background-color: black; + color: white; + margin: 0; + + & * { + font-family: 'Cute Font'; + } +} + +.content { + margin: 8px; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + align-items: center; +} + +.nfc-form { + font-size: 2em; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + align-items: center; + + button { + margin-top: 10px; + font-size: 1em; + } +} + +button { + background-color: black; + color: white; + border: 1px solid white; + cursor: pointer; + + &:hover { + background-color: white; + color: black; + } + + &:disabled { + border-color: rgba(255, 255, 255, 0.3); + color: rgba(255, 255, 255, 0.3); + + &:hover { + background-color: rgba(255, 255, 255, 0.1); + } + } +} + +.content { + flex-direction: column; + flex-wrap: nowrap; + align-items: center; +} + +.error { + flex-direction: column; + flex-wrap: nowrap; + align-items: center; + background-color: rgba(255, 0, 0, 0.1); + border: 1px solid rgba(255, 0, 0, 0.3); + text-align: center; + + .details {} +} + +.error-container {} + +.calendar { + user-select: none; + width: max-content; +} + +.date-span { + font-size: 2rem; +} + +.week { + display: flex; + flex-direction: row; + flex-wrap: wrap; + list-style: none; + gap: 1vw; + max-width: 80vw; +} + +@media only screen and (max-width : 999px) { + .day { + width: 50vw; + } +} + +// @media only screen and (min-width : 1000px) { +// .content { +// margin-left: 5vw; +// margin-right: 5vw; +// display: flex; +// flex-basis: content; +// min-height: 100vh; +// } +// } + + +.day { + padding: 10px 30px 10px 30px; + border: 1px solid rgba(255, 255, 255, 0.3); + display: flex; + flex-direction: column; + flex-wrap: nowrap; + align-items: center; +} + +.date { + font-size: 1.2rem; +} + +.day-tiles { + padding-left: 0; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + list-style: none; + +} + +.tile { + flex-grow: 1; + flex-shrink: 1; + // border: 1px solid white; + padding: 10px; + cursor: pointer; + color: rgba(255, 255, 255, 0.7); // &:hover { + // background-color: rgba(255, 255, 255, 0.3); + // } + + &.pending { + background-color: red; + color: black; + + &:hover { + background-color: rgba(255, 255, 255, 0.7); + } + } + + &.selected-mine { + // border: 1px solid white; + $border_color: white; + color: white; + + &.border-middle { + border-left: 1px solid $border_color; + border-right: 1px solid $border_color; + } + + &.border-top { + border-left: 1px solid $border_color; + border-right: 1px solid $border_color; + border-top: 1px solid $border_color; + } + + &.border-bottom { + border-left: 1px solid $border_color; + border-right: 1px solid $border_color; + border-bottom: 1px solid $border_color; + } + + &.border-lone { + border: 1px solid $border_color; + } + + } + + &[style] { + background-color: var(--color); + } +} + +.tile:hover { + // background-color: rgba(255, 255, 255, 0.3); + backdrop-filter: invert(30%); +} + +nav.user-nav { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + gap: 20px; + user-select: none; + width: max-content; + padding: 10px; + margin: 0; + // border: 1px solid white; + align-items: baseline; + + .username { + width: max-content; + } + + .sign-out { + position: absolute; + right: 20px; + } + + font-size: 1rem; + + button { + padding-top: 5px; + padding-bottom: 5px; + font-size: 1rem; + } +} + +.new-plan { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + align-items: center; + gap: 10px; + + +} + +.field { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + // width: max-content; + // min-width: 60%; + font-size: 1.5em; + width: 100%; +} + +.days { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 10px; +} + +.faint { + filter: opacity(50%); + font-size: 0.5em; +} + +.create-day { + border: 1px solid white; + padding: 10px; + + display: flex; + flex-direction: column; + align-items: flex-start; + flex-wrap: nowrap; + // gap: 10px; + + .remove { + width: 100%; + text-align: center; + } +} + +.set-date { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + + &>button { + flex-grow: 1; + } + + &>.date { + padding-left: 5px; + padding-right: 5px; + } +} + +.date-detail { + width: 100%; + text-align: center; +} + +.message { + display: flex; + flex-direction: column; + width: 100%; + align-items: center; + flex-wrap: nowrap; +} + +.click-backdrop { + z-index: 4; + background-color: rgba(0, 0, 0, 0.7); + position: fixed; + top: 0; + left: 0; + height: 200vh; + width: 100vw; + background-size: cover; +} + +.dialog { + z-index: 5; + width: 100vw; + height: 100vh; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + position: fixed; + top: 0; + left: 0; + align-items: center; + justify-content: center; + + .dialog-box { + font-size: 2rem; + border: 1px solid white; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + align-items: center; + padding-left: 30px; + padding-right: 30px; + padding-top: 10px; + padding-bottom: 10px; + background-color: black; + + .options { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 20px; + + &>button { + min-width: 4cm; + font-size: 1em; + } + } + } +} + + +.users-available { + list-style: none; + + .user { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + align-items: center; + } + + .user:hover { + background-color: rgba(255, 255, 255, 0.3); + } + + [last_available]:hover::after { + display: block; + position: absolute; + content: attr(last_available); + border: 1px solid white; + background: rgba(0, 0, 0, 0.7); + padding: .25em; + } +} + +.signup, +.signin { + display: flex; + flex-direction: column; + align-items: center; + font-size: 1.5rem; + + input { + background-color: black; + border: 1px solid white; + color: white; + } + + .submit { + margin-top: 30px; + font-size: 1.5rem; + } +} + +.fields { + display: flex; + align-items: center; + flex-direction: column; + gap: 10px; +} + +@media only screen and (max-width : 999px) { + + .created-plans, + .participating-plans { + width: 100%; + } +} + +@media only screen and (min-width : 1000px) { + + .created-plans, + .participating-plans { + width: 40vw; + } +} + +.created-plans, +.participating-plans { + padding: 0 30px 0 30px; + align-items: center; + display: flex; + gap: 10px; + flex-direction: column; +} + +.plan-column { + width: 100%; +} + +.main-plans { + user-select: none; + text-align: center; + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 30px; +} + +.plans { + list-style: none; + display: flex; + flex-direction: column; + gap: 10px; + padding-left: 0; + + .plan-headline { + button { + width: 100%; + } + } + + .plan-detail { + display: flex; + flex-direction: row; + gap: 30px; + font-size: 1.5rem; + padding: 10px; + + .start-date { + filter: opacity(50%); + } + } +} + +.splash { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + align-items: center; + width: 100%; + + .options { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 1cm; + + & button { + font-size: 2rem; + } + } +} diff --git a/plan/src/components/dialog.rs b/plan/src/components/dialog.rs new file mode 100644 index 0000000..3a699e8 --- /dev/null +++ b/plan/src/components/dialog.rs @@ -0,0 +1,55 @@ +use std::collections::HashMap; + +use yew::prelude::*; + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct DialogProps { + pub message: String, + pub options: Box<[String]>, + #[prop_or_default] + pub cancel_callback: Option>, + pub callback: Callback, +} + +#[function_component] +pub fn Dialog( + DialogProps { + message, + options, + cancel_callback, + callback, + }: &DialogProps, +) -> Html { + let options = options + .iter() + .map(|opt| { + let callback = callback.clone(); + let option = opt.clone(); + let cb = Callback::from(move |_| { + callback.emit(option.clone()); + }); + html! { + + } + }) + .collect::(); + let backdrop_click = cancel_callback.clone().map(|cancel_callback| { + Callback::from(move |_| { + cancel_callback.emit(()); + }) + }); + html! { +
+
+
+
+

{message.clone()}

+
+
+ {options} +
+
+
+
+ } +} diff --git a/plan/src/components/error.rs b/plan/src/components/error.rs new file mode 100644 index 0000000..b400d6a --- /dev/null +++ b/plan/src/components/error.rs @@ -0,0 +1,25 @@ +use yew::prelude::*; + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct ErrorDisplayProps { + pub state: UseStateHandle>, +} + +#[function_component] +pub fn ErrorDisplay(ErrorDisplayProps { state }: &ErrorDisplayProps) -> Html { + state + .as_ref() + .map(|err| { + let setter = state.setter(); + let on_click = Callback::from(move |_| { + setter.set(None); + }); + html! { +
+ {err.clone()} + +
+ } + }) + .unwrap_or_default() +} diff --git a/plan/src/components/half_hour_range_select.rs b/plan/src/components/half_hour_range_select.rs new file mode 100644 index 0000000..b5cc44c --- /dev/null +++ b/plan/src/components/half_hour_range_select.rs @@ -0,0 +1,54 @@ +use core::ops::{Range, RangeInclusive}; + +use plan_proto::HalfHour; +use web_sys::HtmlSelectElement; +use yew::prelude::*; + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct HalfHourRangeSelectProps { + pub id: String, + pub range: RangeInclusive, + #[prop_or_default] + pub selected: Option, + pub on_change: Callback, +} + +#[function_component] +pub fn HalfHourRangeSelect( + HalfHourRangeSelectProps { + id, + range, + selected, + on_change, + }: &HalfHourRangeSelectProps, +) -> Html { + let options = range + .clone() + .map(|half_hour| { + let selected = Some(half_hour) == *selected; + html! { + + } + }) + .collect::(); + let cb = on_change.clone(); + let on_change_range = range.clone(); + let on_change = Callback::from(move |ev: Event| { + if let Some(select) = ev.target_dyn_into::() { + let selected = select.selected_index(); + if selected == -1 { + return; + } + if let Some(selected) = on_change_range.clone().nth(selected as _) { + cb.emit(selected); + } + } + }); + html! { + + } +} diff --git a/plan/src/components/nav.rs b/plan/src/components/nav.rs new file mode 100644 index 0000000..05f767d --- /dev/null +++ b/plan/src/components/nav.rs @@ -0,0 +1,77 @@ +use plan_proto::token::Token; +use yew::prelude::*; + +use crate::{components::dialog::Dialog, storage::StorageKey}; + +#[function_component] +pub fn Nav() -> Html { + let token = Token::load_from_storage().ok(); + let user = token + .as_ref() + .map(|token| { + html! { +
+ {(*token.username).clone()} +
+ } + }) + .unwrap_or_else(|| { + html! { + <> + + + + } + }); + let dialog = use_state(|| false); + let logged_in_buttons = token.is_some().then(|| { + let confirm_signout = { + let dialog = dialog.clone(); + Callback::from(move |_| { + dialog.set(true); + }) + }; + + let dialog = dialog.then(|| { + let cancel_signout = { + let dialog = dialog.clone(); + Callback::from(move |_| { + dialog.set(false); + }) + }; + let callback = Callback::from(move |option: String| match option.as_str() { + "yes" => { + Token::delete(); + let _ = gloo::utils::window().location().reload(); + } + "no" => { + dialog.set(false); + } + _ => {} + }); + let options: Box<[String]> = Box::new([String::from("yes"), String::from("no")]); + html! { + + } + }); + html! { + <> + + + {dialog} + + } + }); + html! { + + } +} diff --git a/plan/src/components/planview.rs b/plan/src/components/planview.rs new file mode 100644 index 0000000..e01c45f --- /dev/null +++ b/plan/src/components/planview.rs @@ -0,0 +1,314 @@ +use core::{ + num::NonZeroU8, + ops::{Not, Range}, +}; + +use chrono::{Days, NaiveDate, NaiveTime, Utc}; +use plan_proto::{ + HalfHour, + message::ClientMessage, + plan::{Plan, PlanDay, UpdateTiles}, +}; +use serde::{Deserialize, Serialize}; +use web_sys::HtmlSpanElement; +use yew::prelude::*; + +use crate::weekday::FullWeekday; + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct PlanViewProps { + pub plan: Plan, + pub update_day: Callback, +} + +#[function_component] +pub fn PlanView( + PlanViewProps { + plan: + Plan { + created_by, + title, + start_time, + days, + }, + update_day, + }: &PlanViewProps, +) -> Html { + if let Some(title) = title.as_ref() { + gloo::utils::document().set_title(title); + } + let date_span = title + .as_ref() + .map(|title| { + html! { + + {title.clone()} + {format!(" by {created_by}")} + + } + }) + .unwrap_or_else(|| { + html! { + + {start_time.date_naive().to_string()} + {format!(" by {created_by}")} + + } + }); + let max_count = days + .values() + .map(|day| { + day.tiles_count + .values() + .map(|v| v.get()) + .max() + .unwrap_or_default() + }) + .max() + .unwrap_or_default(); + let days = { + let mut days = days + .iter() + .map(|(offset, plan_day)| { + let date = start_time + .checked_add_days(Days::new(*offset as _)) + .unwrap_or_default() + .naive_local() + .date(); + ( + *offset, + html! { + + }, + ) + }) + .collect::>(); + days.sort_by_key(|(offset, _)| *offset); + days.into_iter().map(|(_, vnode)| vnode).collect::() + }; + + html! { +
+ {date_span} +
    + {days} +
+
+ } +} + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct DayProps { + pub offset: u8, + pub day: PlanDay, + pub date: NaiveDate, + pub update: Callback, + pub max_count: u8, +} + +#[function_component] +pub fn Day( + DayProps { + offset, + day: + PlanDay { + day_start, + day_end, + tiles_count, + your_tiles, + users_available, + }, + date, + update, + max_count, + }: &DayProps, +) -> Html { + let pointer_state = use_state(|| false); + let on_selected_update = { + let update = update.clone(); + let day_offset = *offset; + Callback::from(move |(tile, new_state)| { + let message = match new_state { + true => ClientMessage::MarkTile { tile, day_offset }, + false => ClientMessage::UnmarkTile { tile, day_offset }, + }; + + update.emit(message); + }) + }; + + let range = *day_start..=*day_end; + let tiles = range + .clone() + .map(|half_hour| { + let count = tiles_count + .get(&half_hour) + .map(|t| t.get()) + .unwrap_or_default(); + let selected = your_tiles.contains(&half_hour); + let previous_selected = half_hour != HalfHour::Hour0Min0 + && your_tiles.contains(&half_hour.previous_half_hour()); + let next_selected = half_hour != HalfHour::Hour23Min30 + && your_tiles.contains(&half_hour.next_half_hour()); + let border = match (selected, previous_selected, next_selected) { + (false, _, _) => None, + (true, false, false) => Some(DayTileBorder::Lone), + (true, true, false) => Some(DayTileBorder::Bottom), + (true, false, true) => Some(DayTileBorder::Top), + (true, true, true) => Some(DayTileBorder::Middle), + }; + + html! { + + } + }) + .collect::(); + + let on_pointerdown_state = pointer_state.setter(); + let on_pointerdown = Callback::from(move |_| { + on_pointerdown_state.set(true); + }); + let on_pointerup_state = pointer_state.setter(); + let on_pointerup = Callback::from(move |_| { + on_pointerup_state.set(false); + }); + let users_available = users_available + .iter() + .map(|u| { + use chrono_humanize::Humanize; + let delta = u.last_updated_availability - Utc::now(); + let last_seen = format!("last seen {}", delta.humanize()); + html! { +
  • + {u.username.clone()} + {format!("({})", delta.humanize())} +
  • + } + }) + .collect::(); + + html! { +
    + {date.to_string()} + {date.full_weekday()} +
      + {tiles} +
    +
      + {users_available} +
    +
    + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum DayTileBorder { + Lone, + Top, + Middle, + Bottom, +} + +impl DayTileBorder { + pub const fn class(&self) -> &'static str { + match self { + DayTileBorder::Lone => "border-lone", + DayTileBorder::Top => "border-top", + DayTileBorder::Middle => "border-middle", + DayTileBorder::Bottom => "border-bottom", + } + } +} + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct DayTileProps { + pub pointer_state: UseStateHandle, + pub half_hour: HalfHour, + pub on_selected_update: Callback<(HalfHour, bool)>, + pub count: u8, + pub max_count: u8, + pub selected_mine: bool, + pub border: Option, +} + +#[function_component] +pub fn DayTile( + DayTileProps { + pointer_state, + half_hour, + on_selected_update, + count, + max_count, + selected_mine, + border, + }: &DayTileProps, +) -> Html { + let pct = ((*count as f64) * (255.0 / 3.5)) / (*max_count as f64); + let pct = pct + .is_nan() + .not() + .then(|| format!("--color: #00FF00{:0>2X};", pct as u8)); + let pending = use_state(|| false); + let border = border.as_ref().map(DayTileBorder::class); + let span = format!("{half_hour} — {}", half_hour.clone().next().unwrap()); + let pending_class = (*pending && !*selected_mine).then_some("pending"); + let pointer_state = pointer_state.clone(); + let on_pointer_enter = { + let pending = pending.clone(); + let selected = *selected_mine; + let update = on_selected_update.clone(); + let half_hour = *half_hour; + Callback::from(move |_| { + if !*pointer_state { + return; + } + let new_state = !selected; + pending.set(new_state); + update.emit((half_hour, new_state)); + }) + }; + + let on_pointer_down = { + let update = on_selected_update.clone(); + let half_hour = *half_hour; + let selected = *selected_mine; + let pending = pending.clone(); + Callback::from(move |_| { + let new_state = !selected; + pending.set(new_state); + update.emit((half_hour, new_state)); + }) + }; + let selected_mine = selected_mine.then_some("selected-mine"); + + html! { +
  • + {span} +
  • + } +} diff --git a/plan/src/components/state_input.rs b/plan/src/components/state_input.rs new file mode 100644 index 0000000..a0f5cf8 --- /dev/null +++ b/plan/src/components/state_input.rs @@ -0,0 +1,61 @@ +use web_sys::HtmlInputElement; +use yew::prelude::*; + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum InputType { + #[default] + Text, + Password, + Date, + Time, +} + +impl InputType { + const fn input_type(&self) -> &'static str { + match self { + InputType::Text => "text", + InputType::Password => "password", + InputType::Date => "date", + InputType::Time => "time", + } + } +} + +#[derive(Debug, Clone, PartialEq, Properties)] +pub struct StateInputProps { + #[prop_or_default] + pub name: String, + #[prop_or_default] + pub id: Option, + pub state: UseStateHandle, + #[prop_or_default] + pub input_type: InputType, +} + +#[function_component] +pub fn StateInput( + StateInputProps { + name, + id, + state, + input_type, + }: &StateInputProps, +) -> Html { + let set = state.clone(); + + let on_input = Callback::from(move |ev: InputEvent| { + if let Some(input) = ev.target_dyn_into::() { + set.set(input.value()); + } + }); + + html! { + + } +} diff --git a/plan/src/error.rs b/plan/src/error.rs new file mode 100644 index 0000000..3ffd559 --- /dev/null +++ b/plan/src/error.rs @@ -0,0 +1,50 @@ +use gloo::storage::errors::StorageError; +use thiserror::Error; +use wasm_bindgen::JsValue; +use web_sys::DomException; + +#[derive(Debug, Error)] +pub enum JsError { + #[error("abort: {0}")] + AbortError(String), + #[error("not allowed: {0}")] + NotAllowedError(String), + #[error("not supported: {0}")] + NotSupportedError(String), + #[error("not readable: {0}")] + NotReadableError(String), + #[error("network: {0}")] + NetworkError(String), + #[error("invalid state: {0}")] + InvalidState(String), + #[error("storage error: {0}")] + StorageError(#[from] StorageError), + #[error("type error: {0}")] + TypeError(String), + #[error("other: {code} - {name} - {message}")] + Other { + name: String, + code: u16, + message: String, + }, +} + +impl From for JsError { + fn from(value: JsValue) -> Self { + let exception = DomException::from(value); + match exception.name().as_str() { + "AbortError" => JsError::AbortError(exception.message()), + "NotAllowedError" => JsError::NotAllowedError(exception.message()), + "NotSupportedError" => JsError::NotSupportedError(exception.message()), + "NotReadableError" => JsError::NotReadableError(exception.message()), + "NetworkError" => JsError::NetworkError(exception.message()), + "InvalidStateError" => JsError::InvalidState(exception.message()), + "TypeError" => JsError::TypeError(exception.message()), + _ => JsError::Other { + name: exception.name(), + code: exception.code(), + message: exception.message(), + }, + } + } +} diff --git a/plan/src/login.rs b/plan/src/login.rs new file mode 100644 index 0000000..0cf704a --- /dev/null +++ b/plan/src/login.rs @@ -0,0 +1,52 @@ +use gloo::net::http::Request; +use plan_proto::{error::ServerError, plan::PlanId, token::Token}; +use yew::prelude::*; + +use crate::{ + SERVER_URL, + request::RequestError, + storage::{LastPlanVisitedLoggedOut, StorageKey}, +}; + +pub async fn login(body: Box<[u8]>, on_error: UseStateSetter>) { + let req = match Request::post(format!("{SERVER_URL}/s/tokens").as_str()) + .header("content-type", crate::CBOR_CONTENT_TYPE) + .body(body) + { + Ok(req) => req, + Err(err) => { + on_error.set(Some(format!("creating login request: {err}"))); + return; + } + }; + let token = match crate::request::exec_request::(req).await { + Ok(token) => token, + Err(err) => { + on_error.set(Some(err.to_string())); + return; + } + }; + + if let Err(err) = token.save_to_storage() { + on_error.set(Some(format!("saving login token: {err}"))); + return; + } + match LastPlanVisitedLoggedOut::load_from_storage() { + Ok(LastPlanVisitedLoggedOut(plan)) => { + gloo::utils::window() + .location() + .set_href(format!("/plans/{plan}").as_str()) + .unwrap(); + } + Err(_) => gloo::utils::window().location().set_href("/").unwrap(), + } +} + +pub async fn check_token(token: Token) -> Result<(), RequestError> { + let req = Request::get(format!("{SERVER_URL}/s/tokens/check").as_str()) + .header("authorization", format!("Bearer {}", token.token).as_str()) + .build() + .expect("could not build auth request"); + + crate::request::exec_request(req).await +} diff --git a/plan/src/main.rs b/plan/src/main.rs new file mode 100644 index 0000000..cee655b --- /dev/null +++ b/plan/src/main.rs @@ -0,0 +1,227 @@ +mod pages { + pub mod mainpage; + pub mod new_plan; + pub mod not_found; + pub mod plan; + pub mod signin; + pub mod signup; +} +mod components { + pub mod dialog; + pub mod error; + pub mod half_hour_range_select; + pub mod nav; + pub mod planview; + pub mod state_input; +} +mod error; +mod login; +mod request; +mod storage; +mod weekday; +use core::{ + fmt::Display, + ops::{Bound, RangeBounds, RangeInclusive, Sub}, +}; + +use futures::FutureExt; +use gloo::net::http::Request; +use plan_proto::{plan::PlanId, token::Token}; +use yew::prelude::*; +use yew_router::{BrowserRouter, Routable, Switch}; + +const CBOR_CONTENT_TYPE: &str = "application/cbor"; + +use crate::{ + components::nav::Nav, + pages::{ + mainpage::{MainPage, SignedOutMainPage}, + new_plan::NewPlan, + not_found::NotFound, + plan::PlanPage, + signin::Signin, + signup::Signup, + }, + storage::{LastPlanVisitedLoggedOut, StorageKey}, +}; + +fn main() { + wasm_logger::init(wasm_logger::Config::new(log::Level::Trace)); + let document = gloo::utils::document(); + let app_element = document.query_selector("app").unwrap().unwrap(); + + yew::Renderer::
    ::with_root(app_element).render(); +} + +#[derive(Clone, Copy, Routable, PartialEq)] +enum Route { + #[at("/")] + Home, + #[at("/plans/new")] + NewPlan, + #[at("/plans/:id")] + Plan { id: PlanId }, + #[at("/signup")] + Signup, + #[at("/signin")] + Signin, + #[not_found] + #[at("/404")] + NotFound, +} + +fn route(route: Route) -> Html { + match route { + Route::Signup => { + return html! { + <> +