From 47c33a3a6c81b1b1f6c60bf7091861054bab3b96 Mon Sep 17 00:00:00 2001 From: Nell Date: Sun, 3 May 2026 16:24:47 +0200 Subject: [PATCH] Init --- .gitignore | 2 + Cargo.lock | 4041 +++++++++++++++++ Cargo.toml | 29 + config.toml | 24 + event_bus/event_bus/Cargo.toml | 11 + event_bus/event_bus/src/bus.rs | 396 ++ event_bus/event_bus/src/mod.rs | 458 ++ event_bus/event_bus/src/receiver.rs | 199 + event_bus/event_bus/src/wildcard.rs | 66 + migration/Cargo.toml | 22 + migration/README.md | 41 + migration/src/lib.rs | 12 + .../src/m20220101_000001_create_table.rs | 675 +++ migration/src/main.rs | 6 + src/config.rs | 134 + src/database/database.rs | 34 + src/database/mod.rs | 3 + src/lib.rs | 4 + src/main.rs | 28 + src/models/attachment.rs | 45 + src/models/category.rs | 53 + src/models/channel.rs | 94 + src/models/channel_user.rs | 59 + src/models/group.rs | 53 + src/models/group_member.rs | 46 + src/models/message.rs | 77 + src/models/mod.rs | 14 + src/models/prelude.rs | 12 + src/models/server.rs | 55 + src/models/server_user.rs | 62 + src/models/user.rs | 57 + src/udp/metrics.rs | 230 + src/udp/mod.rs | 3 + src/udp/router.rs | 61 + src/udp/server.rs | 182 + 35 files changed, 7288 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 config.toml create mode 100644 event_bus/event_bus/Cargo.toml create mode 100644 event_bus/event_bus/src/bus.rs create mode 100644 event_bus/event_bus/src/mod.rs create mode 100644 event_bus/event_bus/src/receiver.rs create mode 100644 event_bus/event_bus/src/wildcard.rs create mode 100644 migration/Cargo.toml create mode 100644 migration/README.md create mode 100644 migration/src/lib.rs create mode 100644 migration/src/m20220101_000001_create_table.rs create mode 100644 migration/src/main.rs create mode 100644 src/config.rs create mode 100644 src/database/database.rs create mode 100644 src/database/mod.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/models/attachment.rs create mode 100644 src/models/category.rs create mode 100644 src/models/channel.rs create mode 100644 src/models/channel_user.rs create mode 100644 src/models/group.rs create mode 100644 src/models/group_member.rs create mode 100644 src/models/message.rs create mode 100644 src/models/mod.rs create mode 100644 src/models/prelude.rs create mode 100644 src/models/server.rs create mode 100644 src/models/server_user.rs create mode 100644 src/models/user.rs create mode 100644 src/udp/metrics.rs create mode 100644 src/udp/mod.rs create mode 100644 src/udp/router.rs create mode 100644 src/udp/server.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40d9aca --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/.idea \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..545d2bc --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4041 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[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 = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "arrow" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4754a624e5ae42081f464514be454b39711daae0458906dacde5f4c632f33a8" +dependencies = [ + "arrow-arith", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-ord", + "arrow-row", + "arrow-schema", + "arrow-select", + "arrow-string", +] + +[[package]] +name = "arrow-arith" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7b3141e0ec5145a22d8694ea8b6d6f69305971c4fa1c1a13ef0195aef2d678b" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "num-traits", +] + +[[package]] +name = "arrow-array" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8955af33b25f3b175ee10af580577280b4bd01f7e823d94c7cdef7cf8c9aef" +dependencies = [ + "ahash 0.8.12", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "half", + "hashbrown 0.16.1", + "num-complex", + "num-integer", + "num-traits", +] + +[[package]] +name = "arrow-buffer" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c697ddca96183182f35b3a18e50b9110b11e916d7b7799cbfd4d34662f2c56c2" +dependencies = [ + "bytes", + "half", + "num-bigint", + "num-traits", +] + +[[package]] +name = "arrow-cast" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "646bbb821e86fd57189c10b4fcdaa941deaf4181924917b0daa92735baa6ada5" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-ord", + "arrow-schema", + "arrow-select", + "atoi", + "base64", + "chrono", + "half", + "lexical-core", + "num-traits", + "ryu", +] + +[[package]] +name = "arrow-data" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fdd994a9d28e6365aa78e15da3f3950c0fdcea6b963a12fa1c391afb637b304" +dependencies = [ + "arrow-buffer", + "arrow-schema", + "half", + "num-integer", + "num-traits", +] + +[[package]] +name = "arrow-ord" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d8f1870e03d4cbed632959498bcc84083b5a24bded52905ae1695bd29da45b" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", +] + +[[package]] +name = "arrow-row" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18228633bad92bff92a95746bbeb16e5fc318e8382b75619dec26db79e4de4c0" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "half", +] + +[[package]] +name = "arrow-schema" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c872d36b7bf2a6a6a2b40de9156265f0242910791db366a2c17476ba8330d68" + +[[package]] +name = "arrow-select" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68bf3e3efbd1278f770d67e5dc410257300b161b93baedb3aae836144edcaf4b" +dependencies = [ + "ahash 0.8.12", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "num-traits", +] + +[[package]] +name = "arrow-string" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e968097061b3c0e9fe3079cf2e703e487890700546b5b0647f60fca1b5a8d8" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "memchr", + "num-traits", + "regex", + "regex-syntax", +] + +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", + "tokio", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-std" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" +dependencies = [ + "async-attributes", + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[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.117", +] + +[[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-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 = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bigdecimal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[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 = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel 2.5.0", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "borsh" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +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 = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[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 = "config" +version = "0.15.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68cfe19cd7d23ffde002c24ffa5cda73931913ef394d5eaaa32037dc940c0c" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "pathdiff", + "ron", + "rust-ini", + "serde-untagged", + "serde_core", + "serde_json", + "toml", + "winnow", + "yaml-rust2", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +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 = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +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 = "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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[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 = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", + "unicode-xid", +] + +[[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.117", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[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 = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[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 = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[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 = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.1", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[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 = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[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 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[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 = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[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 = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[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.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[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 = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "inherent" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lexical-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" +dependencies = [ + "lexical-parse-integer", + "lexical-util", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "lexical-util" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" + +[[package]] +name = "lexical-write-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" +dependencies = [ + "lexical-util", + "lexical-write-integer", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.7.4", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[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.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "value-bag", +] + +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix", + "serde", + "winapi", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[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.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "migration" +version = "0.1.0" +dependencies = [ + "async-std", + "sea-orm-migration", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[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 = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "ouroboros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "oxspeak_server" +version = "0.1.0" +dependencies = [ + "config", + "glob", + "log", + "migration", + "parking_lot", + "sea-orm", + "serde", + "serde_json", + "thiserror", + "tokio", + "toml", + "tracing", + "tracing-subscriber", + "utoipa", + "uuid", +] + +[[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 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[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 = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "pgvector" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b" +dependencies = [ + "serde", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[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.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "pluralizer" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b3eba432a00a1f6c16f39147847a870e94e2e9b992759b503e330efec778cbe" +dependencies = [ + "once_cell", + "regex", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[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.117", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "version_check", + "yansi", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +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 = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + +[[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_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ron" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc" +dependencies = [ + "bitflags", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rust_decimal" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.6", + "rkyv", + "serde", + "serde_json", + "wasm-bindgen", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[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.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sea-bae" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" +dependencies = [ + "heck 0.4.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sea-orm" +version = "2.0.0-rc.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b5428ce6a0c8f6b9858df21ad1aa00c2fb94e1c9f344a0436bc855391e5a225" +dependencies = [ + "async-stream", + "async-trait", + "bigdecimal", + "chrono", + "derive_more", + "futures-util", + "itertools", + "log", + "mac_address", + "ouroboros", + "pgvector", + "rust_decimal", + "sea-orm-arrow", + "sea-orm-macros", + "sea-query", + "sea-query-sqlx", + "sea-schema", + "serde", + "serde_json", + "sqlx", + "strum", + "thiserror", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sea-orm-arrow" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2eee8405f16c1f337fe3a83389361caea83c928d14dbd666a480407072c365" +dependencies = [ + "arrow", + "sea-query", + "thiserror", +] + +[[package]] +name = "sea-orm-cli" +version = "2.0.0-rc.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd42605c3b611785eed593406900f463b86c61792e723272e0434e77ed9cd8d" +dependencies = [ + "chrono", + "clap", + "dotenvy", + "glob", + "indoc", + "regex", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "sea-orm-macros" +version = "2.0.0-rc.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1374d83dd5b43f14dcc90fc726486c556f4db774b680b12b8c680af76e8233" +dependencies = [ + "heck 0.5.0", + "itertools", + "pluralizer", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.117", + "unicode-ident", +] + +[[package]] +name = "sea-orm-migration" +version = "2.0.0-rc.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f6ce467587c910bb2842cf001ea600ac6228ba5f3f39c1dc499929e34a8f29" +dependencies = [ + "async-trait", + "clap", + "dotenvy", + "sea-orm", + "sea-orm-cli", + "sea-schema", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "sea-query" +version = "1.0.0-rc.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b04cdb0135c16e829504e93fbe7880513578d56f07aaea152283526590111828" +dependencies = [ + "chrono", + "inherent", + "ordered-float", + "rust_decimal", + "sea-query-derive", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "sea-query-derive" +version = "1.0.0-rc.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d88ad44b6ad9788c8b9476b6b91f94c7461d1e19d39cd8ea37838b1e6ff5aa8" +dependencies = [ + "darling", + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.117", + "thiserror", +] + +[[package]] +name = "sea-query-sqlx" +version = "0.8.0-rc.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a04aeecfe00614fece56336fd35dc385bb9ffed0c75660695ba925e42a3991ef" +dependencies = [ + "sea-query", + "sqlx", +] + +[[package]] +name = "sea-schema" +version = "0.17.0-rc.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b363dd21c20fe4d1488819cb2bc7f8d4696c62dd9f39554f97639f54d57dd0ab" +dependencies = [ + "async-trait", + "sea-query", + "sea-query-sqlx", + "sea-schema-derive", + "sqlx", +] + +[[package]] +name = "sea-schema-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdc8729c37fdbf88472f97fd470393089f997a909e535ff67c544d18cfccf0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[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-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[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.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "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 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "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 = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.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 5.4.1", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rust_decimal", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "time", + "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.117", +] + +[[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 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.117", + "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.6", + "rsa", + "rust_decimal", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "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.6", + "rust_decimal", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "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", + "time", + "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 = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[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 = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" + +[[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", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[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.117", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +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.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +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.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "time", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[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.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "utoipa" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "uuid", +] + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "rand 0.10.1", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + +[[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.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[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.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "serde", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[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" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[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.117", +] + +[[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.117", +] + +[[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", +] + +[[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", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[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_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[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_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yaml-rust2" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2ced443 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "oxspeak_server" +version = "0.1.0" +edition = "2024" + +[lib] +name = "oxspeak_server_lib" +crate-type = ["rlib"] + +[workspace] +members = [".", "migration", "event_bus"] + +[dependencies] +tokio = { version = "1.52.1", features = ["full"] } +config = "0.15.22" +sea-orm = { version = "2.0.0-rc.38", features = ["sqlx-sqlite", "sqlx-postgres", "sqlx-mysql", "runtime-tokio", "with-chrono", "with-uuid", "with-json", "schema-sync"] } +migration = { path = "migration" } +event-bus = { path = "event_bus" } +parking_lot = "0.12.5" +serde = "1.0.228" +serde_json = "1.0.149" +toml = "1.1.2" +uuid = { version = "1.23.1", features = ["v4", "v7", "fast-rng", "serde"] } +tracing = "0.1.44" +tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "time"] } +thiserror = "2" +utoipa = { version = "5", features = ["uuid"] } +log = "0.4" +glob = "0.3" diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..663b8be --- /dev/null +++ b/config.toml @@ -0,0 +1,24 @@ +[network] +# IP address to bind to +host = "0.0.0.0" +# hostv6 = "::" +# TCP and UDP port can be the same +# HTTP port +tcp_port = 8080 +# Voice/Video port +udp_port = 8080 + +[database] +# DSN for database +# SQLite +url = "sqlite://oxspeak.db" +# PostgreSQL +# url = "postgresql://user:passwd@localhost:5432/db_name" +# MySQL +# url = "mysql://user:passwd@localhost:3306/db_name" + +[jwt] +secret = "changeme" +# Duration in seconds +duration = 86400 # 1 day +refresh_duration = 1296000 # 15 days diff --git a/event_bus/event_bus/Cargo.toml b/event_bus/event_bus/Cargo.toml new file mode 100644 index 0000000..204bc91 --- /dev/null +++ b/event_bus/event_bus/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "event-bus" +version = "0.1.0" +edition = "2024" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] diff --git a/event_bus/event_bus/src/bus.rs b/event_bus/event_bus/src/bus.rs new file mode 100644 index 0000000..34f4ebe --- /dev/null +++ b/event_bus/event_bus/src/bus.rs @@ -0,0 +1,396 @@ +use std::any::Any; +use std::future::Future; +use std::sync::Arc; + +use glob::Pattern; +use parking_lot::RwLock; +use std::collections::HashMap; +use tokio::sync::broadcast; +use tokio::task::JoinHandle; + +/// Type brut d'un événement : pointeur atomique vers n'importe quelle valeur. +pub type AnyEvent = Arc; + +/// Capacité par défaut du buffer de chaque canal broadcast. +const DEFAULT_CAPACITY: usize = 64; + +/// Le bus d'événements central. +/// +/// Partagez-le via `Arc` entre les modules. +/// Chaque topic possède son propre canal broadcast : seuls les abonnés du bon +/// topic sont réveillés lors d'un `emit` (wake-up ciblé). +/// +/// # Exemple minimal — callback sync +/// ```rust,no_run +/// use std::sync::Arc; +/// use oxspeak_server_lib::event_bus::EventBus; +/// +/// #[derive(Clone, Debug)] +/// struct User { name: String } +/// +/// # tokio_test::block_on(async { +/// let bus = Arc::new(EventBus::new()); +/// +/// bus.on::("user-connected", |user| { +/// println!("Connecté : {:?}", user); +/// }); +/// +/// bus.emit("user-connected", User { name: "Alice".into() }); +/// # tokio::time::sleep(std::time::Duration::from_millis(10)).await; +/// # }); +/// ``` +/// +/// # Exemple — callback async +/// ```rust,no_run +/// use std::sync::Arc; +/// use oxspeak_server_lib::event_bus::EventBus; +/// +/// #[derive(Clone, Debug)] +/// struct User { name: String } +/// +/// # tokio_test::block_on(async { +/// let bus = Arc::new(EventBus::new()); +/// +/// bus.on_async::("user-connected", |user| async move { +/// println!("(async) Connecté : {:?}", user); +/// }); +/// +/// bus.emit("user-connected", User { name: "Bob".into() }); +/// # tokio::time::sleep(std::time::Duration::from_millis(10)).await; +/// # }); +/// ``` +/// +/// # Exemple — pattern glob avec topic +/// ```rust,no_run +/// use std::sync::Arc; +/// use oxspeak_server_lib::event_bus::EventBus; +/// +/// #[derive(Clone, Debug)] +/// struct User { name: String } +/// +/// # tokio_test::block_on(async { +/// let bus = Arc::new(EventBus::new()); +/// +/// bus.on_pattern::("user-*", |topic, user| { +/// match topic.as_str() { +/// "user-created" => println!("Créé : {:?}", user), +/// "user-deleted" => println!("Supprimé : {:?}", user), +/// other => println!("{}: {:?}", other, user), +/// } +/// }); +/// +/// bus.emit("user-created", User { name: "Alice".into() }); +/// bus.emit("user-deleted", User { name: "Bob".into() }); +/// # tokio::time::sleep(std::time::Duration::from_millis(10)).await; +/// # }); +/// ``` +pub struct EventBus { + /// Canaux par topic exact. + channels: RwLock>>, + /// Canaux pour les souscriptions par pattern glob. + patterns: RwLock)>>, + capacity: usize, +} + +impl EventBus { + /// Crée un bus avec la capacité par défaut (64 messages par canal). + pub fn new() -> Self { + Self { + channels: RwLock::new(HashMap::new()), + patterns: RwLock::new(Vec::new()), + capacity: DEFAULT_CAPACITY, + } + } + + /// Crée un bus avec une capacité de buffer personnalisée. + pub fn with_capacity(capacity: usize) -> Self { + Self { + channels: RwLock::new(HashMap::new()), + patterns: RwLock::new(Vec::new()), + capacity, + } + } + + // ───────────────────────────────────────────────────────────────────────── + // Interne + // ───────────────────────────────────────────────────────────────────────── + + fn get_or_create_sender(&self, topic: &str) -> broadcast::Sender { + { + let channels = self.channels.read(); + if let Some(tx) = channels.get(topic) { + return tx.clone(); + } + } + let mut channels = self.channels.write(); + channels + .entry(topic.to_string()) + .or_insert_with(|| { + let (tx, _) = broadcast::channel(self.capacity); + tx + }) + .clone() + } + + // ───────────────────────────────────────────────────────────────────────── + // Émission + // ───────────────────────────────────────────────────────────────────────── + + /// Émet un événement sur un topic. + /// + /// - Pousse l'event dans le canal du topic exact (si des abonnés existent). + /// - Pousse l'event dans tous les canaux de pattern qui matchent le topic. + /// - Si personne n'écoute, l'événement est ignoré silencieusement. + /// + /// # Exemple + /// ```rust,no_run + /// # use std::sync::Arc; + /// # use oxspeak_server_lib::event_bus::EventBus; + /// # #[derive(Clone)] struct User; + /// # let bus = Arc::new(EventBus::new()); + /// bus.emit("user-connected", User); + /// bus.emit("user-deleted", uuid::Uuid::new_v4()); + /// ``` + pub fn emit(&self, topic: &str, event: T) { + let event: AnyEvent = Arc::new(event); + + // Abonnés au topic exact + if let Some(tx) = self.channels.read().get(topic) { + let _ = tx.send(Arc::clone(&event)); + } + + // Abonnés aux patterns glob correspondants + for (pattern, tx) in self.patterns.read().iter() { + if pattern.matches(topic) { + let _ = tx.send((topic.to_string(), Arc::clone(&event))); + } + } + } + + // ───────────────────────────────────────────────────────────────────────── + // Abonnement — callbacks (API principale) + // ───────────────────────────────────────────────────────────────────────── + + /// S'abonne à un topic et appelle `handler` à chaque événement de type `T`. + /// + /// Le handler est exécuté dans une tâche Tokio dédiée (fire-and-forget). + /// Les événements d'un autre type sont ignorés silencieusement. + /// Retourne un [`JoinHandle`] pour annuler l'abonnement si besoin. + /// + /// # Exemple + /// ```rust,no_run + /// # use std::sync::Arc; + /// # use oxspeak_server_lib::event_bus::EventBus; + /// # #[derive(Clone, Debug)] struct User { name: String } + /// # let bus = Arc::new(EventBus::new()); + /// bus.on::("user-connected", |user| { + /// println!("Connecté : {:?}", user); + /// }); + /// ``` + pub fn on(&self, topic: &str, handler: F) -> JoinHandle<()> + where + T: Any + Send + Sync + Clone + 'static, + F: Fn(T) + Send + Sync + 'static, + { + let mut rx = self.get_or_create_sender(topic).subscribe(); + + tokio::spawn(async move { + loop { + match rx.recv().await { + Ok(evt) => { + if let Some(typed) = evt.downcast_ref::() { + handler(typed.clone()); + } + } + Err(broadcast::error::RecvError::Lagged(_)) => {} + Err(broadcast::error::RecvError::Closed) => break, + } + } + }) + } + + /// S'abonne à un topic et appelle un handler **async** à chaque événement de type `T`. + /// + /// Parfait pour effectuer des opérations async dans le handler + /// (requête DB, appel HTTP, broadcast WebSocket…). + /// Retourne un [`JoinHandle`] pour annuler l'abonnement si besoin. + /// + /// # Exemple + /// ```rust,no_run + /// # use std::sync::Arc; + /// # use oxspeak_server_lib::event_bus::EventBus; + /// # #[derive(Clone, Debug)] struct User { name: String } + /// # let bus = Arc::new(EventBus::new()); + /// bus.on_async::("user-connected", |user| async move { + /// println!("(async) Connecté : {:?}", user); + /// // Ici tu peux faire du async : requête DB, HTTP, etc. + /// }); + /// ``` + pub fn on_async(&self, topic: &str, handler: F) -> JoinHandle<()> + where + T: Any + Send + Sync + Clone + 'static, + F: Fn(T) -> Fut + Send + Sync + 'static, + Fut: Future + Send + 'static, + { + let mut rx = self.get_or_create_sender(topic).subscribe(); + + tokio::spawn(async move { + loop { + match rx.recv().await { + Ok(evt) => { + if let Some(typed) = evt.downcast_ref::() { + handler(typed.clone()).await; + } + } + Err(broadcast::error::RecvError::Lagged(_)) => {} + Err(broadcast::error::RecvError::Closed) => break, + } + } + }) + } + + /// S'abonne à tous les topics correspondant à un pattern glob. + /// + /// Le handler reçoit `(topic, valeur)` — le nom du topic est inclus pour + /// distinguer `user-created` de `user-deleted` par exemple. + /// + /// **Aucun pré-enregistrement nécessaire** : les topics futurs sont couverts. + /// Supporte la syntaxe glob : `*` (toute séquence), `?` (un caractère), + /// `[abc]` (classe de caractères). + /// + /// Retourne un [`JoinHandle`] pour annuler l'abonnement si besoin. + /// + /// # Exemple + /// ```rust,no_run + /// # use std::sync::Arc; + /// # use oxspeak_server_lib::event_bus::EventBus; + /// # #[derive(Clone, Debug)] struct User { name: String } + /// # let bus = Arc::new(EventBus::new()); + /// bus.on_pattern::("user-*", |topic, user| { + /// match topic.as_str() { + /// "user-created" => println!("Créé : {:?}", user), + /// "user-deleted" => println!("Supprimé : {:?}", user), + /// other => println!("{}: {:?}", other, user), + /// } + /// }); + /// + /// bus.emit("user-created", User { name: "Alice".into() }); + /// bus.emit("user-deleted", User { name: "Bob".into() }); + /// ``` + pub fn on_pattern(&self, pattern: &str, handler: F) -> JoinHandle<()> + where + T: Any + Send + Sync + Clone + 'static, + F: Fn(String, T) + Send + Sync + 'static, + { + let glob = Pattern::new(pattern).expect("pattern glob invalide"); + let (tx, mut rx) = broadcast::channel(self.capacity); + self.patterns.write().push((glob, tx)); + + tokio::spawn(async move { + loop { + match rx.recv().await { + Ok((topic, evt)) => { + if let Some(typed) = evt.downcast_ref::() { + handler(topic, typed.clone()); + } + } + Err(broadcast::error::RecvError::Lagged(_)) => {} + Err(broadcast::error::RecvError::Closed) => break, + } + } + }) + } + + /// S'abonne à tous les topics correspondant à un pattern glob, avec un handler **async**. + /// + /// Retourne un [`JoinHandle`] pour annuler l'abonnement si besoin. + /// + /// # Exemple + /// ```rust,no_run + /// # use std::sync::Arc; + /// # use oxspeak_server_lib::event_bus::EventBus; + /// # #[derive(Clone, Debug)] struct User { name: String } + /// # let bus = Arc::new(EventBus::new()); + /// bus.on_pattern_async::("user-*", |topic, user| async move { + /// match topic.as_str() { + /// "user-created" => println!("(async) Créé : {:?}", user), + /// "user-deleted" => println!("(async) Supprimé : {:?}", user), + /// other => println!("(async) {}: {:?}", other, user), + /// } + /// }); + /// + /// bus.emit("user-created", User { name: "Alice".into() }); + /// ``` + pub fn on_pattern_async(&self, pattern: &str, handler: F) -> JoinHandle<()> + where + T: Any + Send + Sync + Clone + 'static, + F: Fn(String, T) -> Fut + Send + Sync + 'static, + Fut: Future + Send + 'static, + { + let glob = Pattern::new(pattern).expect("pattern glob invalide"); + let (tx, mut rx) = broadcast::channel(self.capacity); + self.patterns.write().push((glob, tx)); + + tokio::spawn(async move { + loop { + match rx.recv().await { + Ok((topic, evt)) => { + if let Some(typed) = evt.downcast_ref::() { + handler(topic, typed.clone()).await; + } + } + Err(broadcast::error::RecvError::Lagged(_)) => {} + Err(broadcast::error::RecvError::Closed) => break, + } + } + }) + } + + // ───────────────────────────────────────────────────────────────────────── + // Abonnement — accès bas niveau (cas avancés) + // ───────────────────────────────────────────────────────────────────────── + + /// Retourne un receiver brut ([`AnyEvent`]) pour gérer toi-même la boucle. + /// + /// Utile avec la macro [`match_event!`][crate::match_event] pour gérer + /// plusieurs types différents sur un même topic. + /// + /// # Exemple + /// ```rust,no_run + /// # use std::sync::Arc; + /// # use oxspeak_server_lib::event_bus::EventBus; + /// # use oxspeak_server_lib::match_event; + /// # #[derive(Clone, Debug)] struct User { name: String } + /// # #[derive(Clone, Debug)] struct UdpMetric { value: f32 } + /// # let bus = Arc::new(EventBus::new()); + /// # tokio_test::block_on(async { + /// let mut rx = bus.on_raw("user-connected"); + /// bus.emit("user-connected", User { name: "Alice".into() }); + /// + /// if let Ok(evt) = rx.recv().await { + /// match_event!(evt, + /// User => |u| println!("User: {:?}", u), + /// UdpMetric => |m| println!("Metric: {:?}", m), + /// ); + /// } + /// # }); + /// ``` + pub fn on_raw(&self, topic: &str) -> broadcast::Receiver { + self.get_or_create_sender(topic).subscribe() + } + + // ───────────────────────────────────────────────────────────────────────── + // Utilitaires + // ───────────────────────────────────────────────────────────────────────── + + /// Retourne la liste des topics actuellement enregistrés. + pub fn topics(&self) -> Vec { + self.channels.read().keys().cloned().collect() + } +} + +impl Default for EventBus { + fn default() -> Self { + Self::new() + } +} diff --git a/event_bus/event_bus/src/mod.rs b/event_bus/event_bus/src/mod.rs new file mode 100644 index 0000000..a6b3f5e --- /dev/null +++ b/event_bus/event_bus/src/mod.rs @@ -0,0 +1,458 @@ +//! # Event Bus +//! +//! Un bus d'événements asynchrone permettant de faire transiter des messages typés +//! entre plusieurs modules, sans couplage direct. +//! +//! ## Caractéristiques +//! +//! - **Association clé → événement** : chaque topic (`&str`) est indépendant +//! - **Sans restriction de type** : n'importe quel `T: Any + Send + Sync + Clone` +//! - **Wake-up ciblé** : seuls les abonnés du bon topic sont réveillés +//! - **API callback** : style JavaScript — `bus.on("topic", |payload| { ... })` +//! - **Pattern glob** : `on_pattern("user-*", |topic, payload| { ... })` +//! - **Handlers async** : `on_async` et `on_pattern_async` +//! +//! ## Exemple — callback sync +//! +//! ```rust,no_run +//! use std::sync::Arc; +//! use oxspeak_server_lib::event_bus::EventBus; +//! +//! #[derive(Clone, Debug)] +//! struct User { name: String } +//! +//! # tokio_test::block_on(async { +//! let bus = Arc::new(EventBus::new()); +//! +//! bus.on::("user-connected", |user| { +//! println!("Connecté : {:?}", user); +//! }); +//! +//! bus.emit("user-connected", User { name: "Alice".into() }); +//! # tokio::time::sleep(std::time::Duration::from_millis(10)).await; +//! # }); +//! ``` +//! +//! ## Exemple — callback async +//! +//! ```rust,no_run +//! use std::sync::Arc; +//! use oxspeak_server_lib::event_bus::EventBus; +//! +//! #[derive(Clone, Debug)] +//! struct User { name: String } +//! +//! # tokio_test::block_on(async { +//! let bus = Arc::new(EventBus::new()); +//! +//! bus.on_async::("user-connected", |user| async move { +//! println!("(async) Connecté : {:?}", user); +//! }); +//! +//! bus.emit("user-connected", User { name: "Bob".into() }); +//! # tokio::time::sleep(std::time::Duration::from_millis(10)).await; +//! # }); +//! ``` +//! +//! ## Exemple — pattern glob (topic inclus dans le callback) +//! +//! ```rust,no_run +//! use std::sync::Arc; +//! use oxspeak_server_lib::event_bus::EventBus; +//! +//! #[derive(Clone, Debug)] +//! struct User { name: String } +//! +//! # tokio_test::block_on(async { +//! let bus = Arc::new(EventBus::new()); +//! +//! bus.on_pattern::("user-*", |topic, user| { +//! match topic.as_str() { +//! "user-created" => println!("Créé : {:?}", user), +//! "user-deleted" => println!("Supprimé : {:?}", user), +//! other => println!("{}: {:?}", other, user), +//! } +//! }); +//! +//! bus.emit("user-created", User { name: "Alice".into() }); +//! bus.emit("user-deleted", User { name: "Bob".into() }); +//! # tokio::time::sleep(std::time::Duration::from_millis(10)).await; +//! # }); +//! ``` +//! +//! ## Exemple — multi-types avec `match_event!` (cas avancé) +//! +//! ```rust,no_run +//! use std::sync::Arc; +//! use oxspeak_server_lib::event_bus::EventBus; +//! use oxspeak_server_lib::match_event; +//! +//! #[derive(Clone, Debug)] struct User { name: String } +//! #[derive(Clone, Debug)] struct UdpMetric { value: f32 } +//! +//! # tokio_test::block_on(async { +//! let bus = Arc::new(EventBus::new()); +//! let mut rx = bus.on_raw("mixed-topic"); +//! +//! bus.emit("mixed-topic", User { name: "Alice".into() }); +//! +//! if let Ok(evt) = rx.recv().await { +//! match_event!(evt, +//! User => |u| println!("User: {:?}", u), +//! UdpMetric => |m| println!("Metric: {:?}", m), +//! ); +//! } +//! # }); +//! ``` + +mod bus; + +// Réexports publics +pub use bus::{AnyEvent, EventBus}; + +/// Downcaste un [`AnyEvent`] vers un ou plusieurs types concrets et exécute +/// la closure correspondante si le type correspond. +/// +/// Les branches non correspondantes sont ignorées silencieusement. +/// +/// # Syntaxe +/// ```text +/// match_event!(evt, Type1 => |val| { ... }, Type2 => |val| { ... }) +/// ``` +/// +/// # Exemple +/// ```rust,no_run +/// # use std::sync::Arc; +/// # use oxspeak_server_lib::event_bus::EventBus; +/// # use oxspeak_server_lib::match_event; +/// # #[derive(Clone, Debug)] struct User { name: String } +/// # #[derive(Clone, Debug)] struct UdpMetric { value: f32 } +/// # let bus = Arc::new(EventBus::new()); +/// # tokio_test::block_on(async { +/// let mut rx = bus.on_raw("user-connected"); +/// bus.emit("user-connected", User { name: "Alice".into() }); +/// +/// if let Ok(evt) = rx.recv().await { +/// match_event!(evt, +/// User => |u| println!("Utilisateur : {:?}", u), +/// UdpMetric => |m| println!("Metric : {:?}", m), +/// ); +/// } +/// # }); +/// ``` +#[macro_export] +macro_rules! match_event { + ($evt:expr, $($type:ty => $handler:expr),+ $(,)?) => { + $( + if let Some(val) = ($evt).downcast_ref::<$type>() { + ($handler)(val.clone()); + } else + )+ + { + // Aucun type ne correspond → on ignore silencieusement + } + }; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Tests +// ───────────────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; + use std::sync::Arc; + + use super::*; + + #[derive(Clone, Debug, PartialEq)] + struct User { + name: String, + } + + #[derive(Clone, Debug, PartialEq)] + struct UdpMetric { + value: f32, + } + + // ── on (callback sync) ──────────────────────────────────────────────────── + + #[tokio::test] + async fn test_on_callback_sync() { + let bus = Arc::new(EventBus::new()); + let received = Arc::new(AtomicBool::new(false)); + let flag = Arc::clone(&received); + + bus.on::("user-connected", move |user| { + if user.name == "Alice" { + flag.store(true, Ordering::SeqCst); + } + }); + + bus.emit( + "user-connected", + User { + name: "Alice".into(), + }, + ); + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + + assert!(received.load(Ordering::SeqCst)); + } + + #[tokio::test] + async fn test_on_targeted_wakeup() { + // Émettre sur "user-connected" ne doit pas réveiller "udp-metrics-updated" + let bus = Arc::new(EventBus::new()); + let metric_called = Arc::new(AtomicBool::new(false)); + let flag = Arc::clone(&metric_called); + + bus.on::("udp-metrics-updated", move |_| { + flag.store(true, Ordering::SeqCst); + }); + + bus.emit( + "user-connected", + User { + name: "Carol".into(), + }, + ); + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + + assert!(!metric_called.load(Ordering::SeqCst)); + } + + #[tokio::test] + async fn test_on_type_mismatch_ignored() { + // Émettre un UdpMetric sur un topic écouté en User → handler pas appelé + let bus = Arc::new(EventBus::new()); + let called = Arc::new(AtomicBool::new(false)); + let flag = Arc::clone(&called); + + bus.on::("mixed-topic", move |_| { + flag.store(true, Ordering::SeqCst); + }); + + bus.emit("mixed-topic", UdpMetric { value: 1.0 }); + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + + assert!(!called.load(Ordering::SeqCst)); + } + + #[tokio::test] + async fn test_on_multiple_subscribers_same_topic() { + let bus = Arc::new(EventBus::new()); + let count = Arc::new(AtomicU32::new(0)); + + for _ in 0..3 { + let c = Arc::clone(&count); + bus.on::("user-connected", move |_| { + c.fetch_add(1, Ordering::SeqCst); + }); + } + + bus.emit( + "user-connected", + User { + name: "Grace".into(), + }, + ); + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + + assert_eq!(count.load(Ordering::SeqCst), 3); + } + + // ── on_async ────────────────────────────────────────────────────────────── + + #[tokio::test] + async fn test_on_async_callback() { + let bus = Arc::new(EventBus::new()); + let received = Arc::new(AtomicBool::new(false)); + let flag = Arc::clone(&received); + + bus.on_async::("user-connected", move |user| { + let f = Arc::clone(&flag); + async move { + if user.name == "Async" { + f.store(true, Ordering::SeqCst); + } + } + }); + + bus.emit( + "user-connected", + User { + name: "Async".into(), + }, + ); + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + + assert!(received.load(Ordering::SeqCst)); + } + + // ── on_pattern ──────────────────────────────────────────────────────────── + + #[tokio::test] + async fn test_on_pattern_callback_receives_topic_and_payload() { + let bus = Arc::new(EventBus::new()); + let created = Arc::new(AtomicBool::new(false)); + let deleted = Arc::new(AtomicBool::new(false)); + let c = Arc::clone(&created); + let d = Arc::clone(&deleted); + + bus.on_pattern::("user-*", move |topic, user| match topic.as_str() { + "user-created" if user.name == "Dave" => c.store(true, Ordering::SeqCst), + "user-deleted" if user.name == "Eve" => d.store(true, Ordering::SeqCst), + _ => {} + }); + + bus.emit( + "user-created", + User { + name: "Dave".into(), + }, + ); + bus.emit("user-deleted", User { name: "Eve".into() }); + tokio::time::sleep(std::time::Duration::from_millis(30)).await; + + assert!(created.load(Ordering::SeqCst)); + assert!(deleted.load(Ordering::SeqCst)); + } + + #[tokio::test] + async fn test_on_pattern_does_not_match_other_topics() { + let bus = Arc::new(EventBus::new()); + let called = Arc::new(AtomicBool::new(false)); + let flag = Arc::clone(&called); + + bus.on_pattern::("user-*", move |_, _| { + flag.store(true, Ordering::SeqCst); + }); + + bus.emit( + "server-created", + User { + name: "Ghost".into(), + }, + ); + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + + assert!(!called.load(Ordering::SeqCst)); + } + + #[tokio::test] + async fn test_on_pattern_no_pre_registration_needed() { + // Le pattern est enregistré avant que le topic n'existe + let bus = Arc::new(EventBus::new()); + let received = Arc::new(AtomicBool::new(false)); + let flag = Arc::clone(&received); + + bus.on_pattern::("user-*", move |topic, user| { + if topic == "user-new-topic" && user.name == "Frank" { + flag.store(true, Ordering::SeqCst); + } + }); + + bus.emit( + "user-new-topic", + User { + name: "Frank".into(), + }, + ); + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + + assert!(received.load(Ordering::SeqCst)); + } + + // ── on_pattern_async ────────────────────────────────────────────────────── + + #[tokio::test] + async fn test_on_pattern_async_callback() { + let bus = Arc::new(EventBus::new()); + let received = Arc::new(AtomicBool::new(false)); + let flag = Arc::clone(&received); + + bus.on_pattern_async::("user-*", move |topic, user| { + let f = Arc::clone(&flag); + async move { + if topic == "user-created" && user.name == "Hank" { + f.store(true, Ordering::SeqCst); + } + } + }); + + bus.emit( + "user-created", + User { + name: "Hank".into(), + }, + ); + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + + assert!(received.load(Ordering::SeqCst)); + } + + // ── on_raw + match_event! ───────────────────────────────────────────────── + + #[tokio::test] + async fn test_on_raw_and_match_event_macro() { + let bus = Arc::new(EventBus::new()); + let mut rx = bus.on_raw("user-connected"); + + bus.emit( + "user-connected", + User { + name: "Frank".into(), + }, + ); + + let evt = rx.recv().await.unwrap(); + let mut received_name = String::new(); + match_event!(evt, + User => |u: User| { received_name = u.name.clone(); }, + UdpMetric => |_m: UdpMetric| { panic!("mauvais type"); } + ); + assert_eq!(received_name, "Frank"); + } + + // ── Utilitaires ──────────────────────────────────────────────────────────── + + #[test] + fn test_topics_list() { + let bus = EventBus::new(); + // on_raw enregistre le canal (get_or_create) + let _rx1 = bus.on_raw("user-connected"); + let _rx2 = bus.on_raw("udp-metrics-updated"); + + let mut topics = bus.topics(); + topics.sort(); + assert_eq!(topics, vec!["udp-metrics-updated", "user-connected"]); + } + + #[tokio::test] + async fn test_emit_multiple_types_same_bus() { + let bus = Arc::new(EventBus::new()); + let user_ok = Arc::new(AtomicBool::new(false)); + let metric_ok = Arc::new(AtomicBool::new(false)); + let u = Arc::clone(&user_ok); + let m = Arc::clone(&metric_ok); + + bus.on::("user-connected", move |user| { + if user.name == "Bob" { + u.store(true, Ordering::SeqCst); + } + }); + bus.on::("udp-metrics-updated", move |metric| { + if (metric.value - 3.14).abs() < 0.001 { + m.store(true, Ordering::SeqCst); + } + }); + + bus.emit("user-connected", User { name: "Bob".into() }); + bus.emit("udp-metrics-updated", UdpMetric { value: 3.14 }); + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + + assert!(user_ok.load(Ordering::SeqCst)); + assert!(metric_ok.load(Ordering::SeqCst)); + } +} diff --git a/event_bus/event_bus/src/receiver.rs b/event_bus/event_bus/src/receiver.rs new file mode 100644 index 0000000..99e9f52 --- /dev/null +++ b/event_bus/event_bus/src/receiver.rs @@ -0,0 +1,199 @@ +use std::any::Any; +use std::sync::Arc; + +use tokio::sync::broadcast; + +/// Type brut d'un événement : pointeur atomique vers n'importe quelle valeur. +pub type AnyEvent = Arc; + +// ───────────────────────────────────────────────────────────────────────────── +// Erreurs +// ───────────────────────────────────────────────────────────────────────────── + +/// Erreurs possibles lors d'un `recv().await`. +#[derive(Debug, thiserror::Error)] +pub enum RecvError { + #[error("le canal est fermé")] + Closed, + #[error("messages perdus (lag) : {0} ignorés")] + Lagged(u64), +} + +/// Erreurs possibles lors d'un `try_recv()`. +#[derive(Debug, thiserror::Error)] +pub enum TryRecvError { + #[error("aucun message disponible")] + Empty, + #[error("le canal est fermé")] + Closed, + #[error("messages perdus (lag) : {0} ignorés")] + Lagged(u64), +} + +impl From for RecvError { + fn from(e: broadcast::error::RecvError) -> Self { + match e { + broadcast::error::RecvError::Closed => RecvError::Closed, + broadcast::error::RecvError::Lagged(n) => RecvError::Lagged(n), + } + } +} + +impl From for TryRecvError { + fn from(e: broadcast::error::TryRecvError) -> Self { + match e { + broadcast::error::TryRecvError::Empty => TryRecvError::Empty, + broadcast::error::TryRecvError::Closed => TryRecvError::Closed, + broadcast::error::TryRecvError::Lagged(n) => TryRecvError::Lagged(n), + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// TypedReceiver +// ───────────────────────────────────────────────────────────────────────────── + +/// Receiver typé pour un topic précis. +/// +/// `recv().await` retourne directement un `T` — le downcast est automatique. +/// Les événements d'un type différent sont ignorés silencieusement. +/// +/// # Exemple +/// ```rust,no_run +/// # use std::sync::Arc; +/// # use oxspeak_server_lib::event_bus::EventBus; +/// # #[derive(Clone, Debug)] struct User { name: String } +/// # let bus = Arc::new(EventBus::new()); +/// # tokio_test::block_on(async { +/// let mut rx = bus.on::("user-connected"); +/// bus.emit("user-connected", User { name: "Alice".into() }); +/// let user = rx.recv().await.unwrap(); +/// println!("{:?}", user); +/// # }); +/// ``` +pub struct TypedReceiver { + pub(crate) inner: broadcast::Receiver, + pub(crate) _marker: std::marker::PhantomData, +} + +impl TypedReceiver +where + T: Any + Send + Sync + Clone + 'static, +{ + pub(crate) fn new(inner: broadcast::Receiver) -> Self { + Self { + inner, + _marker: std::marker::PhantomData, + } + } + + /// Attend le prochain événement de type `T` sur ce topic. + /// Les événements d'un autre type sont ignorés silencieusement. + pub async fn recv(&mut self) -> Result { + loop { + match self.inner.recv().await { + Ok(evt) => { + if let Some(val) = evt.downcast_ref::() { + return Ok(val.clone()); + } + // Mauvais type → on ignore et on attend le suivant + } + Err(e) => return Err(e.into()), + } + } + } + + /// Version non-bloquante. + pub fn try_recv(&mut self) -> Result { + loop { + match self.inner.try_recv() { + Ok(evt) => { + if let Some(val) = evt.downcast_ref::() { + return Ok(val.clone()); + } + } + Err(broadcast::error::TryRecvError::Empty) => return Err(TryRecvError::Empty), + Err(e) => return Err(e.into()), + } + } + } + + /// Accès au receiver brut sous-jacent. + pub fn into_inner(self) -> broadcast::Receiver { + self.inner + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// PatternReceiver +// ───────────────────────────────────────────────────────────────────────────── + +/// Receiver unique pour un pattern wildcard (ex: `"user-*"`). +/// +/// Un seul receiver reçoit les événements de **tous** les topics correspondants, +/// passés et futurs. `recv().await` retourne `(nom_du_topic, valeur)`. +/// +/// # Exemple +/// ```rust,no_run +/// # use std::sync::Arc; +/// # use oxspeak_server_lib::event_bus::EventBus; +/// # #[derive(Clone, Debug)] struct User { name: String } +/// # let bus = Arc::new(EventBus::new()); +/// # tokio_test::block_on(async { +/// let mut rx = bus.on_pattern::("user-*"); +/// +/// bus.emit("user-created", User { name: "Alice".into() }); +/// bus.emit("user-deleted", User { name: "Bob".into() }); +/// +/// let (topic, user) = rx.recv().await.unwrap(); +/// println!("{}: {:?}", topic, user); +/// # }); +/// ``` +pub struct PatternReceiver { + pub(crate) inner: broadcast::Receiver<(String, AnyEvent)>, + pub(crate) _marker: std::marker::PhantomData, +} + +impl PatternReceiver +where + T: Any + Send + Sync + Clone + 'static, +{ + pub(crate) fn new(inner: broadcast::Receiver<(String, AnyEvent)>) -> Self { + Self { + inner, + _marker: std::marker::PhantomData, + } + } + + /// Attend le prochain événement de type `T` sur n'importe quel topic du pattern. + /// Retourne `(nom_du_topic, valeur)`. + /// Les événements d'un type différent sont ignorés silencieusement. + pub async fn recv(&mut self) -> Result<(String, T), RecvError> { + loop { + match self.inner.recv().await { + Ok((topic, evt)) => { + if let Some(val) = evt.downcast_ref::() { + return Ok((topic, val.clone())); + } + // Mauvais type → on ignore et on attend le suivant + } + Err(e) => return Err(e.into()), + } + } + } + + /// Version non-bloquante. + pub fn try_recv(&mut self) -> Result<(String, T), TryRecvError> { + loop { + match self.inner.try_recv() { + Ok((topic, evt)) => { + if let Some(val) = evt.downcast_ref::() { + return Ok((topic, val.clone())); + } + } + Err(broadcast::error::TryRecvError::Empty) => return Err(TryRecvError::Empty), + Err(e) => return Err(e.into()), + } + } + } +} diff --git a/event_bus/event_bus/src/wildcard.rs b/event_bus/event_bus/src/wildcard.rs new file mode 100644 index 0000000..0922a65 --- /dev/null +++ b/event_bus/event_bus/src/wildcard.rs @@ -0,0 +1,66 @@ +/// Vérifie si `topic` correspond au `pattern` avec support des wildcards. +/// +/// - `*` correspond à n'importe quelle séquence de caractères (y compris vide) +/// - `?` correspond à exactement un caractère quelconque +/// +/// # Exemples +/// ``` +/// use oxspeak_server_lib::event_bus::wildcard_match; +/// +/// assert!(wildcard_match("user-*", "user-connected")); +/// assert!(wildcard_match("user-*", "user-created")); +/// assert!(!wildcard_match("user-*", "udp-metrics-updated")); +/// assert!(wildcard_match("*-updated", "udp-metrics-updated")); +/// assert!(wildcard_match("user-?", "user-a")); +/// assert!(!wildcard_match("user-?", "user-ab")); +/// ``` +pub fn wildcard_match(pattern: &str, topic: &str) -> bool { + let p: Vec = pattern.chars().collect(); + let t: Vec = topic.chars().collect(); + wildcard_match_inner(&p, &t) +} + +fn wildcard_match_inner(pattern: &[char], topic: &[char]) -> bool { + match (pattern.first(), topic.first()) { + (None, None) => true, + (Some(&'*'), _) => { + // '*' peut correspondre à 0 ou plusieurs caractères + wildcard_match_inner(&pattern[1..], topic) + || (!topic.is_empty() && wildcard_match_inner(pattern, &topic[1..])) + } + (Some(&'?'), Some(_)) => wildcard_match_inner(&pattern[1..], &topic[1..]), + (Some(p), Some(t)) if p == t => wildcard_match_inner(&pattern[1..], &topic[1..]), + _ => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_wildcard_exact() { + assert!(wildcard_match("user-connected", "user-connected")); + assert!(!wildcard_match("user-connected", "user-created")); + } + + #[test] + fn test_wildcard_star() { + assert!(wildcard_match("user-*", "user-connected")); + assert!(wildcard_match("user-*", "user-created")); + assert!(wildcard_match("user-*", "user-deleted")); + assert!(!wildcard_match("user-*", "udp-metrics-updated")); + } + + #[test] + fn test_wildcard_question_mark() { + assert!(wildcard_match("user-?", "user-a")); + assert!(!wildcard_match("user-?", "user-ab")); + } + + #[test] + fn test_wildcard_star_anywhere() { + assert!(wildcard_match("*-updated", "udp-metrics-updated")); + assert!(wildcard_match("*metrics*", "udp-metrics-updated")); + } +} diff --git a/migration/Cargo.toml b/migration/Cargo.toml new file mode 100644 index 0000000..29fa19c --- /dev/null +++ b/migration/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "migration" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +async-std = { version = "1", features = ["attributes", "tokio1"] } + +[dependencies.sea-orm-migration] +version = "2.0.0-rc.38" +features = [ + # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI. + # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime. + # e.g. + # "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature + # "sqlx-postgres", # `DATABASE_DRIVER` feature +] diff --git a/migration/README.md b/migration/README.md new file mode 100644 index 0000000..460df2a --- /dev/null +++ b/migration/README.md @@ -0,0 +1,41 @@ +# Running Migrator CLI + +- Generate a new migration file + ```sh + cargo run -- generate MIGRATION_NAME + ``` +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/migration/src/lib.rs b/migration/src/lib.rs new file mode 100644 index 0000000..865fb47 --- /dev/null +++ b/migration/src/lib.rs @@ -0,0 +1,12 @@ +pub use sea_orm_migration::prelude::*; + +mod m20220101_000001_create_table; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m20220101_000001_create_table::Migration)] + } +} diff --git a/migration/src/m20220101_000001_create_table.rs b/migration/src/m20220101_000001_create_table.rs new file mode 100644 index 0000000..11b08df --- /dev/null +++ b/migration/src/m20220101_000001_create_table.rs @@ -0,0 +1,675 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // Create table `server` + manager + .create_table( + Table::create() + .table(Alias::new("server")) + .if_not_exists() + .col(ColumnDef::new("id").uuid().primary_key().not_null()) + .col(ColumnDef::new("name").string().not_null()) + .col(ColumnDef::new("password").string().null()) + .col( + ColumnDef::new(Alias::new("created_at")) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col( + ColumnDef::new(Alias::new("updated_at")) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col( + ColumnDef::new(Alias::new("default_permissions")) + .big_integer() + .not_null() + .default(0), + ) + .to_owned(), + ) + .await?; + + // Create table `category` + manager + .create_table( + Table::create() + .table(Alias::new("category")) + .if_not_exists() + .col( + ColumnDef::new(Alias::new("id")) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(Alias::new("server_id")).uuid().not_null()) + .col(ColumnDef::new(Alias::new("name")).string().not_null()) + .col( + ColumnDef::new(Alias::new("position")) + .integer() + .not_null() + .default(0), + ) + .col( + ColumnDef::new(Alias::new("created_at")) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col( + ColumnDef::new(Alias::new("updated_at")) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + // L'index sera créé après via manager.create_index + .foreign_key( + ForeignKey::create() + .name("fk_category_server") + .from(Alias::new("category"), Alias::new("server_id")) + .to(Alias::new("server"), Alias::new("id")) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + // Create table `channel` + manager + .create_table( + Table::create() + .table(Alias::new("channel")) + .if_not_exists() + .col( + ColumnDef::new(Alias::new("id")) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(Alias::new("server_id")).uuid().null()) + .col(ColumnDef::new(Alias::new("category_id")).uuid().null()) + .col( + ColumnDef::new(Alias::new("position")) + .integer() + .not_null() + .default(0), + ) + .col( + ColumnDef::new(Alias::new("channel_type")) + .integer() + .not_null(), + ) + .col(ColumnDef::new(Alias::new("name")).string().null()) + .col( + ColumnDef::new(Alias::new("created_at")) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col( + ColumnDef::new(Alias::new("updated_at")) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col( + ColumnDef::new(Alias::new("default_permissions")) + .big_integer() + .null(), + ) + // Indexes créés après + .foreign_key( + ForeignKey::create() + .name("fk_channel_server") + .from(Alias::new("channel"), Alias::new("server_id")) + .to(Alias::new("server"), Alias::new("id")) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_channel_category") + .from(Alias::new("channel"), Alias::new("category_id")) + .to(Alias::new("category"), Alias::new("id")) + .on_delete(ForeignKeyAction::SetNull), + ) + .to_owned(), + ) + .await?; + + // Create table `user` + manager + .create_table( + Table::create() + .table(Alias::new("user")) + .if_not_exists() + .col( + ColumnDef::new(Alias::new("id")) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(Alias::new("username")).string().not_null()) + .col(ColumnDef::new(Alias::new("password")).string().not_null()) + .col( + ColumnDef::new(Alias::new("pub_key")) + .text() + .not_null() + .unique_key(), + ) + .col( + ColumnDef::new(Alias::new("created_at")) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col( + ColumnDef::new(Alias::new("updated_at")) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col( + ColumnDef::new(Alias::new("is_superuser")) + .boolean() + .not_null() + .default(false), + ) + .to_owned(), + ) + .await?; + + // Create table `message` + manager + .create_table( + Table::create() + .table(Alias::new("message")) + .if_not_exists() + .col( + ColumnDef::new(Alias::new("id")) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(Alias::new("channel_id")).uuid().not_null()) + .col(ColumnDef::new(Alias::new("user_id")).uuid().not_null()) + .col(ColumnDef::new(Alias::new("content")).text().not_null()) + .col( + ColumnDef::new(Alias::new("created_at")) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col( + ColumnDef::new(Alias::new("updated_at")) + .timestamp_with_time_zone() + .null(), + ) + .col(ColumnDef::new(Alias::new("reply_to_id")).uuid().null()) + // Indexes créés après + .foreign_key( + ForeignKey::create() + .name("fk_message_channel") + .from(Alias::new("message"), Alias::new("channel_id")) + .to(Alias::new("channel"), Alias::new("id")) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_message_user") + .from(Alias::new("message"), Alias::new("user_id")) + .to(Alias::new("user"), Alias::new("id")) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_message_reply_to") + .from(Alias::new("message"), Alias::new("reply_to_id")) + .to(Alias::new("message"), Alias::new("id")) + .on_delete(ForeignKeyAction::SetNull), + ) + .to_owned(), + ) + .await?; + + // Create table `attachment` + manager + .create_table( + Table::create() + .table(Alias::new("attachment")) + .if_not_exists() + .col( + ColumnDef::new(Alias::new("id")) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(Alias::new("message_id")).uuid().not_null()) + .col(ColumnDef::new(Alias::new("filename")).string().not_null()) + .col( + ColumnDef::new(Alias::new("file_size")) + .big_integer() + .not_null(), + ) + .col(ColumnDef::new(Alias::new("mime_type")).string().not_null()) + .col( + ColumnDef::new(Alias::new("created_at")) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + // Index créé après + .foreign_key( + ForeignKey::create() + .name("fk_attachment_message") + .from(Alias::new("attachment"), Alias::new("message_id")) + .to(Alias::new("message"), Alias::new("id")) + .on_delete(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + // Create M2M table `server_user` + manager + .create_table( + Table::create() + .table(Alias::new("server_user")) + .if_not_exists() + .col( + ColumnDef::new(Alias::new("id")) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(Alias::new("server_id")).uuid().not_null()) + .col(ColumnDef::new(Alias::new("user_id")).uuid().not_null()) + .col(ColumnDef::new(Alias::new("username")).string().null()) + .col( + ColumnDef::new(Alias::new("joined_at")) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col( + ColumnDef::new(Alias::new("updated_at")) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col( + ColumnDef::new(Alias::new("is_admin")) + .boolean() + .not_null() + .default(false), + ) + .col( + ColumnDef::new(Alias::new("is_owner")) + .boolean() + .not_null() + .default(false), + ) + .col( + ColumnDef::new(Alias::new("permissions")) + .big_integer() + .not_null() + .default(0), + ) + // Indexes créés après + .foreign_key( + ForeignKey::create() + .name("fk_server_user_server") + .from(Alias::new("server_user"), Alias::new("server_id")) + .to(Alias::new("server"), Alias::new("id")) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_server_user_user") + .from(Alias::new("server_user"), Alias::new("user_id")) + .to(Alias::new("user"), Alias::new("id")) + .on_delete(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + // Create M2M table `channel_user` + manager + .create_table( + Table::create() + .table(Alias::new("channel_user")) + .if_not_exists() + .col( + ColumnDef::new(Alias::new("id")) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(Alias::new("channel_id")).uuid().not_null()) + .col(ColumnDef::new(Alias::new("user_id")).uuid().not_null()) + .col( + ColumnDef::new(Alias::new("role")) + .string() + .not_null() + .default("member".to_owned()), + ) + .col( + ColumnDef::new(Alias::new("permissions")) + .big_integer() + .not_null() + .default(0), + ) + .col( + ColumnDef::new(Alias::new("joined_at")) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + // Indexes créés après + .foreign_key( + ForeignKey::create() + .name("fk_channel_user_channel") + .from(Alias::new("channel_user"), Alias::new("channel_id")) + .to(Alias::new("channel"), Alias::new("id")) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_channel_user_user") + .from(Alias::new("channel_user"), Alias::new("user_id")) + .to(Alias::new("user"), Alias::new("id")) + .on_delete(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + // -------------------------------------------------------------------- + // Création des INDEX après les tables + // -------------------------------------------------------------------- + + // category(server_id) + manager + .create_index( + Index::create() + .name("idx_category_server_id") + .table(Alias::new("category")) + .col(Alias::new("server_id")) + .to_owned(), + ) + .await?; + + // channel(server_id) + manager + .create_index( + Index::create() + .name("idx_channel_server_id") + .table(Alias::new("channel")) + .col(Alias::new("server_id")) + .to_owned(), + ) + .await?; + + // channel(category_id) + manager + .create_index( + Index::create() + .name("idx_channel_category_id") + .table(Alias::new("channel")) + .col(Alias::new("category_id")) + .to_owned(), + ) + .await?; + + // message(channel_id) + manager + .create_index( + Index::create() + .name("idx_message_channel_id") + .table(Alias::new("message")) + .col(Alias::new("channel_id")) + .to_owned(), + ) + .await?; + + // message(user_id) + manager + .create_index( + Index::create() + .name("idx_message_user_id") + .table(Alias::new("message")) + .col(Alias::new("user_id")) + .to_owned(), + ) + .await?; + + // message(reply_to_id) + manager + .create_index( + Index::create() + .name("idx_message_reply_to_id") + .table(Alias::new("message")) + .col(Alias::new("reply_to_id")) + .to_owned(), + ) + .await?; + + // attachment(message_id) + manager + .create_index( + Index::create() + .name("idx_attachment_message_id") + .table(Alias::new("attachment")) + .col(Alias::new("message_id")) + .to_owned(), + ) + .await?; + + // server_user(server_id) + manager + .create_index( + Index::create() + .name("idx_server_user_server_id") + .table(Alias::new("server_user")) + .col(Alias::new("server_id")) + .to_owned(), + ) + .await?; + + // server_user(user_id) + manager + .create_index( + Index::create() + .name("idx_server_user_user_id") + .table(Alias::new("server_user")) + .col(Alias::new("user_id")) + .to_owned(), + ) + .await?; + + // unique (server_id, user_id) + manager + .create_index( + Index::create() + .name("uk_server_user_server_user") + .table(Alias::new("server_user")) + .col(Alias::new("server_id")) + .col(Alias::new("user_id")) + .unique() + .to_owned(), + ) + .await?; + + // channel_user(channel_id) + manager + .create_index( + Index::create() + .name("idx_channel_user_channel_id") + .table(Alias::new("channel_user")) + .col(Alias::new("channel_id")) + .to_owned(), + ) + .await?; + + // channel_user(user_id) + manager + .create_index( + Index::create() + .name("idx_channel_user_user_id") + .table(Alias::new("channel_user")) + .col(Alias::new("user_id")) + .to_owned(), + ) + .await?; + + // unique (channel_id, user_id) + manager + .create_index( + Index::create() + .name("uk_channel_user_channel_user") + .table(Alias::new("channel_user")) + .col(Alias::new("channel_id")) + .col(Alias::new("user_id")) + .unique() + .to_owned(), + ) + .await?; + + // Create table `group` + manager + .create_table( + Table::create() + .table(Alias::new("group")) + .if_not_exists() + .col(ColumnDef::new("id").uuid().primary_key().not_null()) + .col(ColumnDef::new("server_id").uuid().not_null()) + .col(ColumnDef::new("name").string().not_null()) + .col( + ColumnDef::new("permissions") + .big_integer() + .not_null() + .default(0), + ) + .col( + ColumnDef::new("is_default") + .boolean() + .not_null() + .default(false), + ) + .col( + ColumnDef::new("created_at") + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .foreign_key( + ForeignKey::create() + .name("fk_group_server") + .from(Alias::new("group"), Alias::new("server_id")) + .to(Alias::new("server"), Alias::new("id")) + .on_delete(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + // Create table `group_member` + manager + .create_table( + Table::create() + .table(Alias::new("group_member")) + .if_not_exists() + .col(ColumnDef::new("group_id").uuid().not_null()) + .col(ColumnDef::new("user_id").uuid().not_null()) + .primary_key( + Index::create() + .col(Alias::new("group_id")) + .col(Alias::new("user_id")), + ) + .foreign_key( + ForeignKey::create() + .name("fk_group_member_group") + .from(Alias::new("group_member"), Alias::new("group_id")) + .to(Alias::new("group"), Alias::new("id")) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_group_member_user") + .from(Alias::new("group_member"), Alias::new("user_id")) + .to(Alias::new("user"), Alias::new("id")) + .on_delete(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + // Index: idx_group_server_id + manager + .create_index( + Index::create() + .name("idx_group_server_id") + .table(Alias::new("group")) + .col(Alias::new("server_id")) + .to_owned(), + ) + .await?; + + // Index: idx_group_member_user_id + manager + .create_index( + Index::create() + .name("idx_group_member_user_id") + .table(Alias::new("group_member")) + .col(Alias::new("user_id")) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Alias::new("group_member")).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(Alias::new("group")).to_owned()) + .await?; + + manager + .drop_table(Table::drop().table(Alias::new("channel_user")).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(Alias::new("server_user")).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(Alias::new("attachment")).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(Alias::new("message")).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(Alias::new("channel")).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(Alias::new("category")).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(Alias::new("user")).to_owned()) + .await?; + manager + .drop_table(Table::drop().table(Alias::new("server")).to_owned()) + .await?; + + Ok(()) + } +} diff --git a/migration/src/main.rs b/migration/src/main.rs new file mode 100644 index 0000000..ae9daa1 --- /dev/null +++ b/migration/src/main.rs @@ -0,0 +1,6 @@ +use sea_orm_migration::prelude::*; + +#[async_std::main] +async fn main() { + cli::run_cli(migration::Migrator).await; +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..30e0abb --- /dev/null +++ b/src/config.rs @@ -0,0 +1,134 @@ +use config::{Config, ConfigError, File, FileFormat}; +use serde::Deserialize; +use std::error::Error; +use std::net::{Ipv4Addr, Ipv6Addr}; +use std::path::Path; +use std::{fmt, fs}; + +#[derive(Debug)] +pub enum AppConfigError { + Io(std::io::Error), + Config(ConfigError), +} + +impl fmt::Display for AppConfigError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Io(err) => write!(f, "failed to access config file: {err}"), + Self::Config(err) => write!(f, "failed to load config: {err}"), + } + } +} + +impl Error for AppConfigError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Io(err) => Some(err), + Self::Config(err) => Some(err), + } + } +} + +impl From for AppConfigError { + fn from(err: std::io::Error) -> Self { + Self::Io(err) + } +} + +impl From for AppConfigError { + fn from(err: ConfigError) -> Self { + Self::Config(err) + } +} + +pub const DEFAULT_CONFIG_TOML: &str = r#"[network] +# IP address to bind to +host = "0.0.0.0" +# hostv6 = "::" +# TCP and UDP port can be the same +# HTTP port +tcp_port = 8080 +# Voice/Video port +udp_port = 8080 + +[database] +# DSN for database +# SQLite +url = "sqlite://oxspeak.db" +# PostgreSQL +# url = "postgresql://user:passwd@localhost:5432/db_name" +# MySQL +# url = "mysql://user:passwd@localhost:3306/db_name" + +[jwt] +secret = "changeme" +# Duration in seconds +duration = 86400 # 1 day +refresh_duration = 1296000 # 15 days +"#; + +#[derive(Debug, Clone, Deserialize)] +pub struct AppConfig { + pub network: NetworkConfig, + pub database: DatabaseConfig, + pub jwt: JwtConfig, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct NetworkConfig { + pub host: Ipv4Addr, + pub hostv6: Option, + pub tcp_port: u16, + pub udp_port: u16, +} + +#[derive(Clone, Deserialize)] +pub struct DatabaseConfig { + pub url: String, +} + +impl fmt::Debug for DatabaseConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DatabaseConfig") + .field("url", &"") + .finish() + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct JwtConfig { + pub secret: String, + pub duration: u64, + pub refresh_duration: u64, +} + +impl AppConfig { + pub fn file_exists() -> bool { + Path::new("config.toml").exists() + } + + pub fn load() -> Result { + let settings = Config::builder() + .add_source(File::new("config.toml", FileFormat::Toml)) + .build()?; + + Ok(settings.try_deserialize()?) + } + + pub fn gen_config() -> Result<(), AppConfigError> { + if !Self::file_exists() { + fs::write("config.toml", DEFAULT_CONFIG_TOML)?; + } + + Ok(()) + } + + pub fn load_or_generate() -> Result { + if !Self::file_exists() { + Self::gen_config()?; + tracing::info!(config_path = "config.toml", "Generated"); + } + + Self::load() + } +} diff --git a/src/database/database.rs b/src/database/database.rs new file mode 100644 index 0000000..20752af --- /dev/null +++ b/src/database/database.rs @@ -0,0 +1,34 @@ +use std::time::Duration; +use sea_orm::{ConnectOptions, Database as SeaDatabase, DatabaseConnection, DbErr}; +use migration::{Migrator, MigratorTrait}; + +#[derive(Clone)] +pub struct Database { + pub connection: DatabaseConnection, +} + +impl Database { + pub async fn init(dsn: &str) -> Result { + let mut opt = ConnectOptions::new(dsn); + opt.max_connections(100) + .min_connections(5) + .connect_timeout(Duration::from_secs(8)) + .acquire_timeout(Duration::from_secs(8)) + .sqlx_logging(true) + .sqlx_logging_level(log::LevelFilter::Debug); + + let connection = SeaDatabase::connect(opt).await?; + + // On lance les migrations ici. + // Si ça échoue, le programme s'arrête proprement à l'init. + Migrator::up(&connection, None).await?; + + Ok(Self { connection }) + } + + // Tu peux ajouter ici tes méthodes helpers si tu veux encapsuler SeaORM + // ex: pub async fn find_user(...) + pub fn get_connection(&self) -> &DatabaseConnection { + &self.connection + } +} \ No newline at end of file diff --git a/src/database/mod.rs b/src/database/mod.rs new file mode 100644 index 0000000..410e680 --- /dev/null +++ b/src/database/mod.rs @@ -0,0 +1,3 @@ +pub mod database; + +pub use database::Database; \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..fcc8efa --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +pub mod config; +pub mod database; +pub mod models; +pub mod udp; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..672c477 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,28 @@ +use oxspeak_server_lib::config::AppConfig; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Enable tracing + tracing_subscriber::fmt() + .with_env_filter( + std::env::var("RUST_LOG") + .unwrap_or_else(|_| "info,sqlx=debug,sea_orm=debug,sea_orm_migration=info".into()), + ) + .with_target(true) + .with_level(true) + .with_thread_ids(false) + .with_thread_names(false) + .init(); + + // Load or generate config + if !AppConfig::file_exists() { + AppConfig::gen_config()?; + tracing::info!(config_path = "config.toml", "Generated"); + tracing::info!("Config generated, please edit config.toml"); + return Ok(()) + } + let config = AppConfig::load()?; + tracing::info!(?config, "Loaded config"); + + Ok(()) +} \ No newline at end of file diff --git a/src/models/attachment.rs b/src/models/attachment.rs new file mode 100644 index 0000000..2a534a9 --- /dev/null +++ b/src/models/attachment.rs @@ -0,0 +1,45 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use sea_orm::prelude::async_trait::async_trait; +use sea_orm::Set; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "attachment")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub message_id: Uuid, + pub filename: String, + pub file_size: i32, + pub mime_type: String, + pub created_at: DateTimeUtc, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::message::Entity", + from = "Column::MessageId", + to = "super::message::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Message, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Message.def() + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel { + fn new() -> Self { + Self { + id: Set(Uuid::new_v4()), + ..ActiveModelTrait::default() + } + } +} diff --git a/src/models/category.rs b/src/models/category.rs new file mode 100644 index 0000000..12ffbd9 --- /dev/null +++ b/src/models/category.rs @@ -0,0 +1,53 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use sea_orm::prelude::async_trait::async_trait; +use sea_orm::Set; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "category")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub server_id: Uuid, + pub name: String, + pub position: i32, + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::channel::Entity")] + Channel, + #[sea_orm( + belongs_to = "super::server::Entity", + from = "Column::ServerId", + to = "super::server::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + Server, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Channel.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Server.def() + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel { + fn new() -> Self { + Self { + id: Set(Uuid::new_v4()), + ..ActiveModelTrait::default() + } + } +} diff --git a/src/models/channel.rs b/src/models/channel.rs new file mode 100644 index 0000000..76f3e15 --- /dev/null +++ b/src/models/channel.rs @@ -0,0 +1,94 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use sea_orm::prelude::async_trait::async_trait; +use sea_orm::Set; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive( + Debug, Clone, Copy, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema, +)] +#[sea_orm(rs_type = "i32", db_type = "Integer")] +#[serde(rename_all = "snake_case")] +pub enum ChannelType { + #[sea_orm(num_value = 0)] + Text, + #[sea_orm(num_value = 1)] + Voice, + #[sea_orm(num_value = 3)] + DM, +} + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "channel")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub server_id: Option, + pub category_id: Option, + pub position: i32, + pub channel_type: ChannelType, + pub name: Option, + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, + pub default_permissions: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::category::Entity", + from = "Column::CategoryId", + to = "super::category::Column::Id", + on_update = "NoAction", + on_delete = "SetNull" + )] + Category, + #[sea_orm(has_many = "super::channel_user::Entity")] + ChannelUser, + #[sea_orm(has_many = "super::message::Entity")] + Message, + #[sea_orm( + belongs_to = "super::server::Entity", + from = "Column::ServerId", + to = "super::server::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Server, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Category.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ChannelUser.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Message.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Server.def() + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel { + fn new() -> Self { + Self { + id: Set(Uuid::new_v4()), + ..ActiveModelTrait::default() + } + } +} diff --git a/src/models/channel_user.rs b/src/models/channel_user.rs new file mode 100644 index 0000000..a557580 --- /dev/null +++ b/src/models/channel_user.rs @@ -0,0 +1,59 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use sea_orm::prelude::async_trait::async_trait; +use sea_orm::Set; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "channel_user")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub channel_id: Uuid, + pub user_id: Uuid, + pub role: String, + pub permissions: u64, + pub joined_at: DateTimeUtc, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::channel::Entity", + from = "Column::ChannelId", + to = "super::channel::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Channel, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Channel.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel { + fn new() -> Self { + Self { + id: Set(Uuid::new_v4()), + ..ActiveModelTrait::default() + } + } +} diff --git a/src/models/group.rs b/src/models/group.rs new file mode 100644 index 0000000..821e1ce --- /dev/null +++ b/src/models/group.rs @@ -0,0 +1,53 @@ +//! `SeaORM` Entity. + +use sea_orm::entity::prelude::*; +use sea_orm::prelude::async_trait::async_trait; +use sea_orm::Set; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "group")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub server_id: Uuid, + pub name: String, + pub permissions: u64, + pub is_default: bool, + pub created_at: DateTimeUtc, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::server::Entity", + from = "Column::ServerId", + to = "super::server::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Server, + #[sea_orm(has_many = "super::group_member::Entity")] + GroupMember, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Server.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::GroupMember.def() + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel { + fn new() -> Self { + Self { + id: Set(Uuid::new_v4()), + ..ActiveModelTrait::default() + } + } +} diff --git a/src/models/group_member.rs b/src/models/group_member.rs new file mode 100644 index 0000000..847c178 --- /dev/null +++ b/src/models/group_member.rs @@ -0,0 +1,46 @@ +//! `SeaORM` Entity. + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "group_member")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub group_id: Uuid, + #[sea_orm(primary_key, auto_increment = false)] + pub user_id: Uuid, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::group::Entity", + from = "Column::GroupId", + to = "super::group::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Group, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Group.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/models/message.rs b/src/models/message.rs new file mode 100644 index 0000000..2811b3e --- /dev/null +++ b/src/models/message.rs @@ -0,0 +1,77 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use sea_orm::prelude::async_trait::async_trait; +use sea_orm::Set; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "message")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub channel_id: Uuid, + pub user_id: Uuid, + #[sea_orm(column_type = "Text")] + pub content: String, + pub created_at: DateTimeUtc, + pub updated_at: Option, + pub reply_to_id: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::attachment::Entity")] + Attachment, + #[sea_orm( + belongs_to = "super::channel::Entity", + from = "Column::ChannelId", + to = "super::channel::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Channel, + #[sea_orm( + belongs_to = "Entity", + from = "Column::ReplyToId", + to = "Column::Id", + on_update = "NoAction", + on_delete = "SetNull" + )] + SelfRef, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Attachment.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Channel.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel { + fn new() -> Self { + Self { + id: Set(Uuid::now_v7()), + ..ActiveModelTrait::default() + } + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..55cb265 --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,14 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +pub mod prelude; + +pub mod attachment; +pub mod category; +pub mod channel; +pub mod channel_user; +pub mod group; +pub mod group_member; +pub mod message; +pub mod server; +pub mod server_user; +pub mod user; diff --git a/src/models/prelude.rs b/src/models/prelude.rs new file mode 100644 index 0000000..12070eb --- /dev/null +++ b/src/models/prelude.rs @@ -0,0 +1,12 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +pub use super::attachment::Entity as Attachment; +pub use super::category::Entity as Category; +pub use super::channel::Entity as Channel; +pub use super::channel_user::Entity as ChannelUser; +pub use super::group::Entity as Group; +pub use super::group_member::Entity as GroupMember; +pub use super::message::Entity as Message; +pub use super::server::Entity as Server; +pub use super::server_user::Entity as ServerUser; +pub use super::user::Entity as User; diff --git a/src/models/server.rs b/src/models/server.rs new file mode 100644 index 0000000..17e37d4 --- /dev/null +++ b/src/models/server.rs @@ -0,0 +1,55 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use sea_orm::prelude::async_trait::async_trait; +use sea_orm::Set; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "server")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub name: String, + pub password: Option, + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, + pub default_permissions: u64, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::category::Entity")] + Category, + #[sea_orm(has_many = "super::channel::Entity")] + Channel, + #[sea_orm(has_many = "super::server_user::Entity")] + ServerUser, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Category.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Channel.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ServerUser.def() + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel { + fn new() -> Self { + Self { + id: Set(Uuid::new_v4()), + ..ActiveModelTrait::default() + } + } +} diff --git a/src/models/server_user.rs b/src/models/server_user.rs new file mode 100644 index 0000000..fa70190 --- /dev/null +++ b/src/models/server_user.rs @@ -0,0 +1,62 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use sea_orm::prelude::async_trait::async_trait; +use sea_orm::Set; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "server_user")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub server_id: Uuid, + pub user_id: Uuid, + pub username: Option, + pub joined_at: DateTimeUtc, + pub updated_at: DateTimeUtc, + pub is_admin: bool, + pub is_owner: bool, + pub permissions: u64, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::server::Entity", + from = "Column::ServerId", + to = "super::server::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Server, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Server.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel { + fn new() -> Self { + Self { + id: Set(Uuid::new_v4()), + ..ActiveModelTrait::default() + } + } +} diff --git a/src/models/user.rs b/src/models/user.rs new file mode 100644 index 0000000..b78be53 --- /dev/null +++ b/src/models/user.rs @@ -0,0 +1,57 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use sea_orm::prelude::async_trait::async_trait; +use sea_orm::Set; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "user")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub username: String, + pub password: String, + #[sea_orm(column_type = "Text", unique)] + pub pub_key: String, + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, + pub is_superuser: bool, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::channel_user::Entity")] + ChannelUser, + #[sea_orm(has_many = "super::message::Entity")] + Message, + #[sea_orm(has_many = "super::server_user::Entity")] + ServerUser, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ChannelUser.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Message.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ServerUser.def() + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel { + fn new() -> Self { + Self { + id: Set(Uuid::new_v4()), + ..ActiveModelTrait::default() + } + } +} diff --git a/src/udp/metrics.rs b/src/udp/metrics.rs new file mode 100644 index 0000000..504f6f0 --- /dev/null +++ b/src/udp/metrics.rs @@ -0,0 +1,230 @@ +//! Métrologie du serveur UDP. +//! +//! Ce module expose : +//! - [`UdpMetrics`] : compteurs atomiques lock-free (pas de contention dans la +//! boucle de routage). +//! - [`UdpMetricsSnapshot`] : lecture cohérente de tous les compteurs à un +//! instant T, utilisable pour calculer des deltas. +//! - [`UdpRates`] : taux moyens par seconde calculés entre deux snapshots. +//! - [`spawn_reporter`] : tâche tokio de reporting périodique via `tracing`. +//! +//! # Métriques collectées +//! +//! | Compteur | Description | +//! |--------------------|-----------------------------------------------| +//! | `packets_received` | Datagrammes reçus | +//! | `bytes_received` | Octets reçus (payload uniquement) | +//! | `packets_sent` | Datagrammes retransmis vers des abonnés | +//! | `bytes_sent` | Octets retransmis | +//! | `packets_dropped` | Paquets ignorés (canal sans abonnés) | +//! | `send_errors` | Échecs `send_to` | +//! | `recv_errors` | Échecs `recv_from` (avant erreur fatale) | +//! +//! Chaque métrique est également disponible en taux moyen par seconde via +//! [`UdpMetricsSnapshot::rates_since`]. + +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +// ── Compteurs ──────────────────────────────────────────────────────────────── + +/// Compteurs atomiques du serveur UDP. +/// +/// Partagé via [`Arc`] entre la boucle de routage et le reporter périodique. +/// Tous les accès utilisent [`Ordering::Relaxed`] : on accepte que les lectures +/// voient des valeurs légèrement décalées entre compteurs (suffisant pour de la +/// métrologie), ce qui évite tout overhead de synchronisation. +#[derive(Debug, Default)] +pub struct UdpMetrics { + /// Nombre total de datagrammes reçus. + pub packets_received: AtomicU64, + /// Volume total d'octets reçus (payload des datagrammes). + pub bytes_received: AtomicU64, + /// Nombre total de datagrammes retransmis (somme sur tous les abonnés). + pub packets_sent: AtomicU64, + /// Volume total d'octets retransmis. + pub bytes_sent: AtomicU64, + /// Paquets ignorés car le canal ne possède aucun abonné. + pub packets_dropped: AtomicU64, + /// Nombre d'erreurs `send_to` (non fatales). + pub send_errors: AtomicU64, + /// Nombre d'erreurs `recv_from` enregistrées avant arrêt du serveur. + pub recv_errors: AtomicU64, +} + +impl UdpMetrics { + /// Crée un jeu de métriques vide enroulé dans un [`Arc`]. + pub fn new() -> Arc { + Arc::new(Self::default()) + } + + /// Enregistre la réception d'un datagramme de `bytes` octets. + #[inline] + pub fn inc_received(&self, bytes: u64) { + self.packets_received.fetch_add(1, Ordering::Relaxed); + self.bytes_received.fetch_add(bytes, Ordering::Relaxed); + } + + /// Enregistre l'émission d'un datagramme de `bytes` octets vers un client. + #[inline] + pub fn inc_sent(&self, bytes: u64) { + self.packets_sent.fetch_add(1, Ordering::Relaxed); + self.bytes_sent.fetch_add(bytes, Ordering::Relaxed); + } + + /// Enregistre un paquet ignoré (canal sans abonnés). + #[inline] + pub fn inc_dropped(&self) { + self.packets_dropped.fetch_add(1, Ordering::Relaxed); + } + + /// Enregistre un échec `send_to` non fatal. + #[inline] + pub fn inc_send_error(&self) { + self.send_errors.fetch_add(1, Ordering::Relaxed); + } + + /// Enregistre un échec `recv_from`. + #[inline] + pub fn inc_recv_error(&self) { + self.recv_errors.fetch_add(1, Ordering::Relaxed); + } + + /// Prend un instantané cohérent de tous les compteurs. + pub fn snapshot(&self) -> UdpMetricsSnapshot { + UdpMetricsSnapshot { + packets_received: self.packets_received.load(Ordering::Relaxed), + bytes_received: self.bytes_received.load(Ordering::Relaxed), + packets_sent: self.packets_sent.load(Ordering::Relaxed), + bytes_sent: self.bytes_sent.load(Ordering::Relaxed), + packets_dropped: self.packets_dropped.load(Ordering::Relaxed), + send_errors: self.send_errors.load(Ordering::Relaxed), + recv_errors: self.recv_errors.load(Ordering::Relaxed), + } + } +} + +// ── Snapshot ───────────────────────────────────────────────────────────────── + +/// Lecture cohérente de l'ensemble des compteurs à un instant T. +/// +/// Permet de calculer des deltas et des taux entre deux points dans le temps +/// sans bloquer la boucle de routage. +#[derive(Debug, Clone, Copy, Default)] +pub struct UdpMetricsSnapshot { + pub packets_received: u64, + pub bytes_received: u64, + pub packets_sent: u64, + pub bytes_sent: u64, + pub packets_dropped: u64, + pub send_errors: u64, + pub recv_errors: u64, +} + +impl UdpMetricsSnapshot { + /// Calcule les taux moyens par seconde depuis un snapshot précédent. + /// + /// `elapsed` est la durée réelle écoulée entre les deux snapshots. + pub fn rates_since(&self, previous: &Self, elapsed: Duration) -> UdpRates { + // On évite la division par zéro si l'interval est infinitésimal. + let secs = elapsed.as_secs_f64().max(f64::EPSILON); + + UdpRates { + packets_received_per_sec: self + .packets_received + .saturating_sub(previous.packets_received) + as f64 + / secs, + bytes_received_per_sec: self.bytes_received.saturating_sub(previous.bytes_received) + as f64 + / secs, + packets_sent_per_sec: self.packets_sent.saturating_sub(previous.packets_sent) as f64 + / secs, + bytes_sent_per_sec: self.bytes_sent.saturating_sub(previous.bytes_sent) as f64 / secs, + packets_dropped_per_sec: self + .packets_dropped + .saturating_sub(previous.packets_dropped) + as f64 + / secs, + } + } +} + +// ── Taux ───────────────────────────────────────────────────────────────────── + +/// Taux moyens par seconde calculés entre deux [`UdpMetricsSnapshot`]. +#[derive(Debug, Clone, Copy)] +pub struct UdpRates { + /// Paquets reçus par seconde. + pub packets_received_per_sec: f64, + /// Octets reçus par seconde. + pub bytes_received_per_sec: f64, + /// Paquets envoyés par seconde. + pub packets_sent_per_sec: f64, + /// Octets envoyés par seconde. + pub bytes_sent_per_sec: f64, + /// Paquets ignorés par seconde. + pub packets_dropped_per_sec: f64, +} + +// ── Reporter périodique ─────────────────────────────────────────────────────── + +/// Lance une tâche Tokio qui logue les métriques toutes les `interval`. +/// +/// Chaque rapport inclut les compteurs cumulatifs **et** les taux moyens sur +/// la fenêtre écoulée depuis le rapport précédent. +/// +/// # Exemple +/// ```no_run +/// use std::time::Duration; +/// use std::sync::Arc; +/// use oxspeak_server_lib::udp::metrics::{UdpMetrics, spawn_reporter}; +/// +/// #[tokio::main] +/// async fn main() { +/// let metrics = UdpMetrics::new(); +/// spawn_reporter(Arc::clone(&metrics), Duration::from_secs(5)); +/// } +/// ``` +pub fn spawn_reporter(metrics: Arc, interval: Duration) { + tokio::spawn(async move { + let mut ticker = tokio::time::interval(interval); + // Le premier tick est immédiat ; on le consomme pour démarrer à t=0. + ticker.tick().await; + + let mut prev_snapshot = metrics.snapshot(); + let mut prev_instant = Instant::now(); + + loop { + ticker.tick().await; + + let now = Instant::now(); + let current = metrics.snapshot(); + let elapsed = now.duration_since(prev_instant); + let rates = current.rates_since(&prev_snapshot, elapsed); + + tracing::info!( + // ── Cumulatifs ── + pkts_rx = current.packets_received, + bytes_rx = current.bytes_received, + pkts_tx = current.packets_sent, + bytes_tx = current.bytes_sent, + pkts_dropped = current.packets_dropped, + send_errors = current.send_errors, + recv_errors = current.recv_errors, + // ── Taux / s ── + pkts_rx_s = format!("{:.1}", rates.packets_received_per_sec), + bytes_rx_s = format!("{:.0}", rates.bytes_received_per_sec), + pkts_tx_s = format!("{:.1}", rates.packets_sent_per_sec), + bytes_tx_s = format!("{:.0}", rates.bytes_sent_per_sec), + pkts_dropped_s = format!("{:.1}", rates.packets_dropped_per_sec), + interval_ms = elapsed.as_millis(), + "UDP metrics" + ); + + prev_snapshot = current; + prev_instant = now; + } + }); +} diff --git a/src/udp/mod.rs b/src/udp/mod.rs new file mode 100644 index 0000000..7ab1abf --- /dev/null +++ b/src/udp/mod.rs @@ -0,0 +1,3 @@ +pub mod metrics; +pub mod router; +pub mod server; diff --git a/src/udp/router.rs b/src/udp/router.rs new file mode 100644 index 0000000..7780037 --- /dev/null +++ b/src/udp/router.rs @@ -0,0 +1,61 @@ +use std::collections::HashMap; +use std::net::SocketAddr; + +/// Identifiant d'un canal de routage (ex: room ID, channel name…). +pub type ChannelId = String; + +/// Table de routage UDP. +/// +/// Associe un identifiant de canal à la liste des clients (adresses IP/port) +/// actuellement abonnés à ce canal. Le serveur UDP utilise cette table pour +/// décider où retransmettre les paquets entrants. +/// +/// # Note future +/// Ce type est intentionnellement simple pour démarrer. La prochaine étape +/// sera d'y associer une logique de dispatch (ex: forwarding sélectif, +/// authentification du client, etc.). +#[derive(Debug, Default)] +pub struct RoutingTable { + channels: HashMap>, +} + +impl RoutingTable { + /// Crée une table de routage vide. + pub fn new() -> Self { + Self::default() + } + + /// Inscrit un client dans un canal. + /// + /// Si le canal n'existe pas encore, il est créé automatiquement. + /// Si le client est déjà inscrit dans ce canal, l'appel est sans effet. + pub fn join(&mut self, channel: impl Into, client: SocketAddr) { + let clients = self.channels.entry(channel.into()).or_default(); + if !clients.contains(&client) { + clients.push(client); + } + } + + /// Retire un client d'un canal. + /// + /// Si le canal devient vide après le retrait, il est supprimé de la table. + pub fn leave(&mut self, channel: &str, client: &SocketAddr) { + if let Some(clients) = self.channels.get_mut(channel) { + clients.retain(|c| c != client); + if clients.is_empty() { + self.channels.remove(channel); + } + } + } + + /// Retourne la liste des clients abonnés à un canal, ou `None` si le + /// canal n'existe pas. + pub fn subscribers(&self, channel: &str) -> Option<&[SocketAddr]> { + self.channels.get(channel).map(Vec::as_slice) + } + + /// Retourne tous les canaux connus et leurs abonnés. + pub fn channels(&self) -> &HashMap> { + &self.channels + } +} diff --git a/src/udp/server.rs b/src/udp/server.rs new file mode 100644 index 0000000..f09d2ed --- /dev/null +++ b/src/udp/server.rs @@ -0,0 +1,182 @@ +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::net::UdpSocket; +use tokio::sync::broadcast; + +use super::metrics::UdpMetrics; +use super::router::RoutingTable; +use crate::config::NetworkConfig; + +/// Taille du buffer de lecture pour chaque datagramme UDP entrant. +/// +/// La RFC 768 limite les datagrammes UDP à 65 507 octets (payload max avec +/// en-têtes IP+UDP). Pour de la voix/vidéo compressée, les paquets réels +/// seront bien plus petits, mais on alloue le maximum une seule fois pour +/// éviter toute troncature silencieuse. +const UDP_READ_BUFFER_SIZE: usize = 65_507; + +/// Erreurs pouvant survenir pendant l'opération du serveur UDP. +#[derive(Debug, thiserror::Error)] +pub enum UdpServerError { + #[error("failed to bind UDP socket to {addr}: {source}")] + Bind { + addr: SocketAddr, + #[source] + source: std::io::Error, + }, + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), +} + +/// Serveur UDP asynchrone agissant comme routeur de paquets. +/// +/// Reçoit des datagrammes entrants et les route vers les clients inscrits +/// dans les canaux correspondants via une [`RoutingTable`]. +/// +/// La configuration réseau est fournie par [`NetworkConfig`] (issue de +/// [`AppConfig`][crate::config::AppConfig]), qui centralise la lecture du +/// fichier TOML. +/// +/// Les métriques sont collectées dans un [`UdpMetrics`] partageable via +/// [`Arc`] — passez-le à [`metrics::spawn_reporter`][super::metrics::spawn_reporter] +/// pour un reporting périodique automatique. +/// +/// # Exemple +/// ```no_run +/// use std::time::Duration; +/// use oxspeak_server_lib::config::{AppConfig, NetworkConfig}; +/// use oxspeak_server_lib::udp::server::UdpServer; +/// use oxspeak_server_lib::udp::metrics::{UdpMetrics, spawn_reporter}; +/// +/// #[tokio::main] +/// async fn main() { +/// let config = AppConfig::load().unwrap(); +/// let metrics = UdpMetrics::new(); +/// spawn_reporter(metrics.clone(), Duration::from_secs(10)); +/// let (server, _shutdown_tx) = UdpServer::new(config.network, metrics); +/// server.run().await.unwrap(); +/// } +/// ``` +pub struct UdpServer { + bind_addr: SocketAddr, + routing_table: RoutingTable, + metrics: Arc, + shutdown_rx: broadcast::Receiver<()>, +} + +impl UdpServer { + /// Crée un nouveau [`UdpServer`] depuis la configuration réseau globale. + /// + /// `metrics` est un [`Arc`] vers le jeu de métriques à alimenter. + /// Conservez-en un clone si vous souhaitez l'observer depuis l'extérieur + /// (reporter, API de santé, etc.). + /// + /// Retourne le serveur et un [`broadcast::Sender`] pour déclencher le + /// shutdown gracieux. + pub fn new(network: NetworkConfig, metrics: Arc) -> (Self, broadcast::Sender<()>) { + let bind_addr = SocketAddr::new(network.host.into(), network.udp_port); + let (shutdown_tx, shutdown_rx) = broadcast::channel(1); + ( + Self { + bind_addr, + routing_table: RoutingTable::new(), + metrics, + shutdown_rx, + }, + shutdown_tx, + ) + } + + /// Expose la table de routage de façon mutable pour y inscrire des + /// clients avant ou pendant l'exécution (via partage d'état ou messages). + pub fn routing_table_mut(&mut self) -> &mut RoutingTable { + &mut self.routing_table + } + + /// Retourne une référence aux métriques du serveur. + pub fn metrics(&self) -> &Arc { + &self.metrics + } + + /// Bind le socket et démarre la boucle de routage. + /// + /// Pour chaque datagramme reçu, le paquet est retransmis inline (sans + /// spawn de tâche) vers tous les clients abonnés au canal identifié. + /// La future se résout lorsqu'un signal de shutdown est reçu ou qu'une + /// erreur I/O fatale survient. + pub async fn run(mut self) -> Result<(), UdpServerError> { + let socket = + UdpSocket::bind(self.bind_addr) + .await + .map_err(|source| UdpServerError::Bind { + addr: self.bind_addr, + source, + })?; + + tracing::info!(addr = %self.bind_addr, "UDP server listening"); + + let mut buf = vec![0u8; UDP_READ_BUFFER_SIZE]; + + loop { + tokio::select! { + result = socket.recv_from(&mut buf) => { + match result { + Ok((len, peer)) => { + self.metrics.inc_received(len as u64); + self.route_packet(&socket, &buf[..len], peer).await; + } + Err(err) => { + self.metrics.inc_recv_error(); + tracing::error!(%err, "recv_from failed"); + return Err(UdpServerError::Io(err)); + } + } + } + _ = self.shutdown_rx.recv() => { + tracing::info!("UDP server shutting down"); + break; + } + } + } + + Ok(()) + } + + /// Route un paquet entrant vers les abonnés du canal correspondant. + /// + /// # Logique actuelle (placeholder) + /// En l'absence de protocole applicatif défini, tous les paquets reçus + /// sont logués. Branche ici la logique d'identification du canal + /// (ex: lire un header de paquet pour extraire le `channel_id`). + async fn route_packet(&self, socket: &UdpSocket, data: &[u8], sender: SocketAddr) { + tracing::debug!(%sender, bytes = data.len(), "datagram received"); + + // TODO: extraire le channel_id depuis le header du paquet applicatif. + // Pour l'instant on utilise un canal de démonstration statique. + let channel_id = "default"; + + match self.routing_table.subscribers(channel_id) { + Some(clients) => { + for &client in clients { + // Ne pas renvoyer au sender lui-même. + if client == sender { + continue; + } + match socket.send_to(data, client).await { + Ok(_) => { + self.metrics.inc_sent(data.len() as u64); + } + Err(err) => { + self.metrics.inc_send_error(); + tracing::warn!(%client, %err, "failed to forward packet"); + } + } + } + } + None => { + self.metrics.inc_dropped(); + tracing::debug!(%sender, channel = channel_id, "no subscribers, packet dropped"); + } + } + } +}