commit f04f5e6e41a904b197db4188ae813175b053ec8e Author: Lionarius Date: Wed May 14 06:47:10 2025 +0300 . diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..b0de924 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +[target.x86_64-unknown-linux-gnu] +linker = "/usr/bin/clang" +rustflags = ["-C", "link-arg=--ld-path=/usr/bin/mold"] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30131ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/target +/.idea +/config.toml +/db +/logs + +.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..fa501b6 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4432 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "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 = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "axum" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +dependencies = [ + "axum-core", + "axum-macros", + "base64 0.22.1", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "axum_typed_multipart" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8986dff40423605627d31ebd1cc903d39001b6370c0c7337b02a8093ba484521" +dependencies = [ + "anyhow", + "async-trait", + "axum", + "axum_typed_multipart_macros", + "bytes", + "chrono", + "futures-core", + "futures-util", + "rust_decimal", + "tempfile", + "thiserror 2.0.12", + "tokio", + "uuid", +] + +[[package]] +name = "axum_typed_multipart_macros" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58e854cb9d7f3ea26701fbbbb38f6cec456d8129f86c1b91c7171711a5d30d7" +dependencies = [ + "darling", + "heck", + "proc-macro-error2", + "quote", + "syn 2.0.101", + "ubyte", +] + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +dependencies = [ + "serde", +] + +[[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 = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[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.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" +dependencies = [ + "shlex", +] + +[[package]] +name = "ccm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae3c82e4355234767756212c570e29833699ab63e6ffd161887314cc5b43847" +dependencies = [ + "aead", + "cipher", + "ctr", + "subtle", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595aae20e65c3be792d05818e8c63025294ac3cb7e200f11459063a352a6ef80" +dependencies = [ + "async-trait", + "convert_case 0.6.0", + "json5", + "pathdiff", + "ron", + "rust-ini", + "serde", + "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.16", + "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 = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[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", + "strsim", + "syn 2.0.101", +] + +[[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.101", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case 0.7.1", + "proc-macro2", + "quote", + "syn 2.0.101", + "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 = "diplom" +version = "0.1.0" +dependencies = [ + "anyhow", + "argon2", + "async-trait", + "axum", + "axum-extra", + "axum_typed_multipart", + "base64 0.22.1", + "chrono", + "config", + "dashmap", + "derive_more", + "futures", + "hex", + "jsonwebtoken", + "mime", + "rand 0.9.1", + "rand_core 0.6.4", + "regex", + "serde", + "serde_json", + "sha2", + "sqlx", + "tokio", + "tower-http", + "tracing", + "tracing-appender", + "tracing-subscriber", + "url", + "uuid", + "validator", + "webrtc", +] + +[[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.101", +] + +[[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 = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[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 = "errno" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +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" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.3", +] + +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +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.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown 0.15.3", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "interceptor" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ab04c530fd82e414e40394cabe5f0ebfe30d119f10fe29d6e3561926af412e" +dependencies = [ + "async-trait", + "bytes", + "log", + "portable-atomic", + "rand 0.8.5", + "rtcp", + "rtp", + "thiserror 1.0.69", + "tokio", + "waitgroup", + "webrtc-srtp", + "webrtc-util", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "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 = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[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 = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[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 = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +dependencies = [ + "memchr", + "thiserror 2.0.12", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "pest_meta" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +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 = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +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.101", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[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.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.9.0", + "serde", + "serde_derive", +] + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rtcp" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8306430fb118b7834bbee50e744dc34826eca1da2158657a3d6cbc70e24c2096" +dependencies = [ + "bytes", + "thiserror 1.0.69", + "webrtc-util", +] + +[[package]] +name = "rtp" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e68baca5b6cb4980678713f0d06ef3a432aa642baefcbfd0f4dd2ef9eb5ab550" +dependencies = [ + "bytes", + "memchr", + "portable-atomic", + "rand 0.8.5", + "serde", + "thiserror 1.0.69", + "webrtc-util", +] + +[[package]] +name = "rust-ini" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" +dependencies = [ + "cfg-if", + "ordered-multimap", + "trim-in-place", +] + +[[package]] +name = "rust_decimal" +version = "1.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdp" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a526161f474ae94b966ba622379d939a8fe46c930eebbadb73e339622599d5" +dependencies = [ + "rand 0.8.5", + "substring", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.12", + "time", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +dependencies = [ + "serde", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3a85280daca669cfd3bcb68a337882a8bc57ec882f72c5d13a430613a738e" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f743f2a3cea30a58cd479013f75550e879009e3a02f616f18ca699335aa248c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.3", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4200e0fde19834956d4252347c12a083bdcb237d7a1a1446bffd8768417dce" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.101", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ceaa29cade31beca7129b6beeb05737f44f82dbe2a9806ecea5a7093d00b7" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.101", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0afdd3aa7a629683c2d750c2df343025545087081ab5942593a5288855b1b7a7" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.9.0", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.12", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0bedbe1bbb5e2615ef347a5e9d8cd7680fb63e77d9dafc0f29be15e53f1ebe6" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.9.0", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.12", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c26083e9a520e8eb87a06b12347679b142dc2ea29e6e409f805644a7a979a5bc" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.12", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[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 = "stun" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea256fb46a13f9204e9dee9982997b2c3097db175a9fddaa8350310d03c4d5a3" +dependencies = [ + "base64 0.22.1", + "crc", + "lazy_static", + "md-5", + "rand 0.8.5", + "ring", + "subtle", + "thiserror 1.0.69", + "tokio", + "url", + "webrtc-util", +] + +[[package]] +name = "substring" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" +dependencies = [ + "autocfg", +] + +[[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.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +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.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" +dependencies = [ + "bitflags 2.9.0", + "bytes", + "http", + "http-body", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +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.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "chrono", + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trim-in-place" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.1", + "sha1", + "thiserror 2.0.12", + "utf-8", +] + +[[package]] +name = "turn" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0044fdae001dd8a1e247ea6289abf12f4fcea1331a2364da512f9cd680bbd8cb" +dependencies = [ + "async-trait", + "base64 0.22.1", + "futures", + "log", + "md-5", + "portable-atomic", + "rand 0.8.5", + "ring", + "stun", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "webrtc-util", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "ubyte" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea" + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.3", + "rand 0.9.1", + "serde", +] + +[[package]] +name = "validator" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[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 = "waitgroup" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1f50000a783467e6c0200f9d10642f4bc424e39efc1b770203e88b488f79292" +dependencies = [ + "atomic-waker", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[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.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.101", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "webrtc" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30367074d9f18231d28a74fab0120856b2b665da108d71a12beab7185a36f97b" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "cfg-if", + "hex", + "interceptor", + "lazy_static", + "log", + "portable-atomic", + "rand 0.8.5", + "rcgen", + "regex", + "ring", + "rtcp", + "rtp", + "rustls", + "sdp", + "serde", + "serde_json", + "sha2", + "smol_str", + "stun", + "thiserror 1.0.69", + "time", + "tokio", + "turn", + "url", + "waitgroup", + "webrtc-data", + "webrtc-dtls", + "webrtc-ice", + "webrtc-mdns", + "webrtc-media", + "webrtc-sctp", + "webrtc-srtp", + "webrtc-util", +] + +[[package]] +name = "webrtc-data" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec93b991efcd01b73c5b3503fa8adba159d069abe5785c988ebe14fcf8f05d1" +dependencies = [ + "bytes", + "log", + "portable-atomic", + "thiserror 1.0.69", + "tokio", + "webrtc-sctp", + "webrtc-util", +] + +[[package]] +name = "webrtc-dtls" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c9b89fc909f9da0499283b1112cd98f72fec28e55a54a9e352525ca65cd95c" +dependencies = [ + "aes", + "aes-gcm", + "async-trait", + "bincode", + "byteorder", + "cbc", + "ccm", + "der-parser", + "hkdf", + "hmac", + "log", + "p256", + "p384", + "portable-atomic", + "rand 0.8.5", + "rand_core 0.6.4", + "rcgen", + "ring", + "rustls", + "sec1", + "serde", + "sha1", + "sha2", + "subtle", + "thiserror 1.0.69", + "tokio", + "webrtc-util", + "x25519-dalek", + "x509-parser", +] + +[[package]] +name = "webrtc-ice" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348b28b593f7709ac98d872beb58c0009523df652c78e01b950ab9c537ff17d" +dependencies = [ + "arc-swap", + "async-trait", + "crc", + "log", + "portable-atomic", + "rand 0.8.5", + "serde", + "serde_json", + "stun", + "thiserror 1.0.69", + "tokio", + "turn", + "url", + "uuid", + "waitgroup", + "webrtc-mdns", + "webrtc-util", +] + +[[package]] +name = "webrtc-mdns" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6dfe9686c6c9c51428da4de415cb6ca2dc0591ce2b63212e23fd9cccf0e316b" +dependencies = [ + "log", + "socket2", + "thiserror 1.0.69", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-media" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e153be16b8650021ad3e9e49ab6e5fa9fb7f6d1c23c213fd8bbd1a1135a4c704" +dependencies = [ + "byteorder", + "bytes", + "rand 0.8.5", + "rtp", + "thiserror 1.0.69", +] + +[[package]] +name = "webrtc-sctp" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5faf3846ec4b7e64b56338d62cbafe084aa79806b0379dff5cc74a8b7a2b3063" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "crc", + "log", + "portable-atomic", + "rand 0.8.5", + "thiserror 1.0.69", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-srtp" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771db9993712a8fb3886d5be4613ebf27250ef422bd4071988bf55f1ed1a64fa" +dependencies = [ + "aead", + "aes", + "aes-gcm", + "byteorder", + "bytes", + "ctr", + "hmac", + "log", + "rtcp", + "rtp", + "sha1", + "subtle", + "thiserror 1.0.69", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-util" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1438a8fd0d69c5775afb4a71470af92242dbd04059c61895163aa3c1ef933375" +dependencies = [ + "async-trait", + "bitflags 1.3.2", + "bytes", + "ipnet", + "lazy_static", + "libc", + "log", + "nix", + "portable-atomic", + "rand 0.8.5", + "thiserror 1.0.69", + "tokio", + "winapi", +] + +[[package]] +name = "whoami" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" +dependencies = [ + "redox_syscall", + "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.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "yaml-rust2" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818913695e83ece1f8d2a1c52d54484b7b46d0f9c06beeb2649b9da50d9b512d" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b5ddacc --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "diplom" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0" +argon2 = "0.5" +axum = { version = "0.8", features = ["ws", "multipart", "macros"] } +axum-extra = { version = "0.10", features = ["typed-header"] } +chrono = { version = "0.4", features = ["serde"] } +config = "0.15" +derive_more = { version = "2.0", features = ["full"] } +jsonwebtoken = "9.3" +rand_core = { version = "0.6", features = ["getrandom"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sqlx = { version = "0.8", features = ["postgres", "runtime-tokio", "uuid", "chrono"] } +tokio = { version = "1.45", features = ["full"] } +tower-http = { version = "0.6", features = ["cors", "trace"] } +tracing = "0.1" +tracing-appender = "0.2" +tracing-subscriber = { version = "0.3", features = ["serde", "env-filter", "chrono"] } +uuid = { version = "1.16", features = ["fast-rng", "serde", "v7"] } +url = { version = "2.5", features = ["serde"] } +validator = { version = "0.20.0", features = ["derive"] } +regex = "1.11.1" +mime = "0.3.17" +axum_typed_multipart = "0.16.2" +async-trait = "0.1.88" +futures = "0.3.31" +webrtc = "0.12.0" +dashmap = "6.1.0" +rand = "0.9.1" +sha2 = "0.10.9" +hex = "0.4.3" +base64 = "0.22.1" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..32fac71 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +services: + database: + image: 'ghcr.io/craigpastro/pg_uuidv7:main' + ports: + - "15432:5432" + env_file: + - .env + volumes: + - ${PWD}/db/:/var/lib/postgresql/data/ + user: "1000:1000" \ No newline at end of file diff --git a/migrations/20250510183101_uuidv7.down.sql b/migrations/20250510183101_uuidv7.down.sql new file mode 100644 index 0000000..6960f25 --- /dev/null +++ b/migrations/20250510183101_uuidv7.down.sql @@ -0,0 +1 @@ +DROP EXTENSION pg_uuidv7; \ No newline at end of file diff --git a/migrations/20250510183101_uuidv7.up.sql b/migrations/20250510183101_uuidv7.up.sql new file mode 100644 index 0000000..3d6e61d --- /dev/null +++ b/migrations/20250510183101_uuidv7.up.sql @@ -0,0 +1 @@ +CREATE EXTENSION IF NOT EXISTS pg_uuidv7; \ No newline at end of file diff --git a/migrations/20250510183102_user.down.sql b/migrations/20250510183102_user.down.sql new file mode 100644 index 0000000..4b100bb --- /dev/null +++ b/migrations/20250510183102_user.down.sql @@ -0,0 +1,4 @@ +DROP TRIGGER trg_user_relation_update ON "user_relation"; +DROP FUNCTION fn_on_user_relation_update(); +DROP TABLE "user_relation"; +DROP TABLE "user"; \ No newline at end of file diff --git a/migrations/20250510183102_user.up.sql b/migrations/20250510183102_user.up.sql new file mode 100644 index 0000000..8e5e907 --- /dev/null +++ b/migrations/20250510183102_user.up.sql @@ -0,0 +1,46 @@ +CREATE TABLE IF NOT EXISTS "user" +( + "id" UUID NOT NULL PRIMARY KEY DEFAULT uuid_generate_v7(), + "avatar_url" VARCHAR, + "username" VARCHAR NOT NULL UNIQUE, + "display_name" VARCHAR, + "email" VARCHAR NOT NULL, + "password_hash" VARCHAR NOT NULL, + "bot" BOOLEAN NOT NULL DEFAULT FALSE, + "system" BOOLEAN NOT NULL DEFAULT FALSE, + "settings" JSONB NOT NULL DEFAULT '{}'::JSONB +); + +CREATE TABLE IF NOT EXISTS "user_relation" +( + "user_id" UUID NOT NULL CHECK (user_id <> other_id) REFERENCES "user" ("id") ON DELETE CASCADE, + "other_id" UUID NOT NULL CHECK (user_id <> other_id) REFERENCES "user" ("id") ON DELETE CASCADE, + "type" INT2 NOT NULL, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT now(), + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY ("user_id", "other_id") +); + +-- create system account +INSERT INTO "user" ("username", "display_name", "email", "password_hash", "bot", "system") +VALUES ('system', 'System', 'system@lionarius.ru', '', TRUE, TRUE); + +CREATE OR REPLACE FUNCTION fn_on_user_relation_update() + RETURNS TRIGGER + LANGUAGE plpgsql +AS +$$ +BEGIN + IF (TG_OP = 'UPDATE') THEN + NEW.updated_at := now(); + END IF; + + RETURN NEW; +END; +$$; + +CREATE TRIGGER trg_user_relation_update + BEFORE UPDATE + ON "user_relation" + FOR EACH ROW +EXECUTE FUNCTION fn_on_user_relation_update(); \ No newline at end of file diff --git a/migrations/20250510183125_server.down.sql b/migrations/20250510183125_server.down.sql new file mode 100644 index 0000000..3271cb2 --- /dev/null +++ b/migrations/20250510183125_server.down.sql @@ -0,0 +1 @@ +DROP TABLE "server"; \ No newline at end of file diff --git a/migrations/20250510183125_server.up.sql b/migrations/20250510183125_server.up.sql new file mode 100644 index 0000000..8d56ff0 --- /dev/null +++ b/migrations/20250510183125_server.up.sql @@ -0,0 +1,77 @@ +CREATE TABLE IF NOT EXISTS "server" +( + "id" UUID NOT NULL PRIMARY KEY DEFAULT uuid_generate_v7(), + "owner_id" UUID NOT NULL REFERENCES "user" ("id"), + "name" VARCHAR NOT NULL, + "icon_url" VARCHAR +); + +CREATE TABLE IF NOT EXISTS "server_role" +( + "id" UUID NOT NULL PRIMARY KEY DEFAULT uuid_generate_v7(), + "server_id" UUID NOT NULL REFERENCES "server" ("id") ON DELETE CASCADE, + "name" VARCHAR NOT NULL, + "color" VARCHAR, + "display" BOOLEAN NOT NULL DEFAULT FALSE, + "permissions" JSONB NOT NULL DEFAULT '{}'::JSONB, + "position" SMALLINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS "server_member" +( + "id" UUID NOT NULL PRIMARY KEY DEFAULT uuid_generate_v7(), + "server_id" UUID NOT NULL REFERENCES "server" ("id") ON DELETE CASCADE, + "user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE, + "nickname" VARCHAR, + "avatar_url" VARCHAR, + UNIQUE ("server_id", "user_id") +); + +CREATE TABLE IF NOT EXISTS "server_member_role" +( + "member_id" UUID NOT NULL REFERENCES "server_member" ("id") ON DELETE CASCADE, + "role_id" UUID NOT NULL REFERENCES "server_role" ("id") ON DELETE CASCADE, + PRIMARY KEY ("member_id", "role_id") +); + +CREATE TABLE IF NOT EXISTS "server_invite" +( + "code" VARCHAR NOT NULL PRIMARY KEY, + "server_id" UUID NOT NULL REFERENCES "server" ("id") ON DELETE CASCADE, + "inviter_id" UUID REFERENCES "user" ("id") ON DELETE SET NULL, + "expires_at" TIMESTAMPTZ +); + +CREATE OR REPLACE FUNCTION check_server_user_role_server_id() + RETURNS TRIGGER AS +$$ +DECLARE + member_server_id UUID; + role_server_id UUID; +BEGIN + -- Get server_id from server_user + SELECT server_id + INTO member_server_id + FROM server_member + WHERE id = NEW.member_id; + + -- Get server_id from server_role + SELECT server_id + INTO role_server_id + FROM server_role + WHERE id = NEW.role_id; + + -- Check if server_ids match + IF member_server_id != role_server_id THEN + RAISE EXCEPTION 'Cannot assign role from a different server: server_user server_id (%) does not match server_role server_id (%)', member_server_id, role_server_id; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER enforce_server_user_role_server_id + BEFORE INSERT OR UPDATE + ON server_member_role + FOR EACH ROW +EXECUTE FUNCTION check_server_user_role_server_id(); \ No newline at end of file diff --git a/migrations/20250510184011_channel_message.down.sql b/migrations/20250510184011_channel_message.down.sql new file mode 100644 index 0000000..e03cd29 --- /dev/null +++ b/migrations/20250510184011_channel_message.down.sql @@ -0,0 +1,2 @@ +DROP TABLE "message"; +DROP TABLE "channel"; \ No newline at end of file diff --git a/migrations/20250510184011_channel_message.up.sql b/migrations/20250510184011_channel_message.up.sql new file mode 100644 index 0000000..489ce45 --- /dev/null +++ b/migrations/20250510184011_channel_message.up.sql @@ -0,0 +1,97 @@ +CREATE TABLE IF NOT EXISTS "channel" +( + "id" UUID NOT NULL PRIMARY KEY DEFAULT uuid_generate_v7(), + "name" VARCHAR NOT NULL, + "type" INT2 NOT NULL, + "position" INT2 NOT NULL, + "server_id" UUID REFERENCES "server" ("id") ON DELETE CASCADE, -- only for server channels + "parent" UUID REFERENCES "channel" ("id") ON DELETE SET NULL, -- only for server channels + "owner_id" UUID REFERENCES "user" ("id") ON DELETE SET NULL -- only for group channels +); + +-- only for dm or group channels +CREATE TABLE IF NOT EXISTS "channel_recipient" +( + "channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE, + "user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE, + PRIMARY KEY ("channel_id", "user_id") +); + +CREATE TABLE IF NOT EXISTS "message" +( + "id" UUID NOT NULL PRIMARY KEY DEFAULT uuid_generate_v7(), + "author_id" UUID NOT NULL REFERENCES "user" ("id"), + "channel_id" UUID NOT NULL REFERENCES "channel" ("id"), + "content" TEXT NOT NULL +); + +ALTER TABLE "channel" + ADD COLUMN "last_message_id" UUID REFERENCES "message" ("id") ON DELETE SET NULL; + + +CREATE OR REPLACE FUNCTION create_dm_channel( + user1_id UUID, + user2_id UUID, + channel_type INT2 +) + RETURNS UUID + LANGUAGE plpgsql +AS +$$ +DECLARE + new_channel_id UUID; + channel_name VARCHAR; +BEGIN + -- Validate parameters + IF user1_id IS NULL OR user2_id IS NULL THEN + RAISE EXCEPTION 'Both user IDs must be provided'; + END IF; + + IF user1_id = user2_id THEN + RAISE EXCEPTION 'Cannot create a DM channel with the same user'; + END IF; + +-- Check if users exist + IF NOT exists (SELECT 1 FROM "user" WHERE id = user1_id) OR + NOT exists (SELECT 1 FROM "user" WHERE id = user2_id) THEN + RAISE EXCEPTION 'One or both users do not exist'; + END IF; + +-- Check if DM already exists between these users + IF exists (SELECT 1 + FROM channel c + JOIN channel_recipient cr1 ON c.id = cr1.channel_id AND cr1.user_id = user1_id + JOIN channel_recipient cr2 ON c.id = cr2.channel_id AND cr2.user_id = user2_id + WHERE c.type = channel_type + AND (SELECT count(*) FROM channel_recipient WHERE channel_id = c.id) = 2) THEN + -- Find and return the existing channel ID + SELECT c.id + INTO new_channel_id + FROM channel c + JOIN channel_recipient cr1 ON c.id = cr1.channel_id AND cr1.user_id = user1_id + JOIN channel_recipient cr2 ON c.id = cr2.channel_id AND cr2.user_id = user2_id + WHERE c.type = channel_type + AND (SELECT count(*) FROM channel_recipient WHERE channel_id = c.id) = 2 + LIMIT 1; + + RAISE NOTICE 'DM channel already exists between these users with ID: %', new_channel_id; + RETURN new_channel_id; + END IF; + +-- Generate channel name (conventionally uses user IDs in DMs) + channel_name := concat(user1_id, '-', user2_id); + + -- Create new channel + INSERT INTO "channel" ("name", "type", "position") + VALUES (channel_name, channel_type, 0) + RETURNING id INTO new_channel_id; + +-- Add both users as recipients + INSERT INTO "channel_recipient" ("channel_id", "user_id") + VALUES (new_channel_id, user1_id), + (new_channel_id, user2_id); + + RAISE NOTICE 'DM channel created with ID: %', new_channel_id; + RETURN new_channel_id; +END; +$$; \ No newline at end of file diff --git a/migrations/20250510184916_file.down.sql b/migrations/20250510184916_file.down.sql new file mode 100644 index 0000000..7731a0b --- /dev/null +++ b/migrations/20250510184916_file.down.sql @@ -0,0 +1,2 @@ +DROP TABLE "message_attachment"; +DROP TABLE "file"; \ No newline at end of file diff --git a/migrations/20250510184916_file.up.sql b/migrations/20250510184916_file.up.sql new file mode 100644 index 0000000..61da927 --- /dev/null +++ b/migrations/20250510184916_file.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS "file" +( + "id" UUID NOT NULL PRIMARY KEY DEFAULT uuid_generate_v7(), + "filename" VARCHAR NOT NULL, + "content_type" VARCHAR NOT NULL, + "url" VARCHAR NOT NULL, + "size" INT8 NOT NULL +); + +CREATE TABLE IF NOT EXISTS "message_attachment" +( + "message_id" UUID NOT NULL REFERENCES "message" ON DELETE CASCADE, + "attachment_id" UUID NOT NULL REFERENCES "file" ON DELETE CASCADE, + "order" INT2 NOT NULL, + PRIMARY KEY ("message_id", "attachment_id") +); \ No newline at end of file diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..b924699 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,7 @@ +match_block_trailing_comma = true +newline_style = "Unix" +style_edition = "2024" + +group_imports = "StdExternalCrate" +imports_granularity = "Module" +unstable_features = true \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..dd63430 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,64 @@ +use std::sync::OnceLock; + +use serde::Deserialize; + +pub fn config() -> &'static Config { + static INSTANCE: OnceLock = OnceLock::new(); + + INSTANCE.get_or_init(|| { + config::Config::builder() + .add_source(config::File::with_name("config")) + .add_source(config::Environment::with_prefix("DIPLOM_")) + .build() + .expect("config builder") + .try_deserialize() + .expect("config deserialize") + }) +} + +#[derive(Deserialize)] +pub struct Config { + pub port: u16, + pub jwt_secret: String, + pub database: DatabaseConfig, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +#[serde(deny_unknown_fields)] +pub enum DatabaseConfig { + Url { + url: url::Url, + }, + Full { + driver: String, + username: String, + password: String, + host: url::Host, + port: u16, + name: String, + }, +} + +impl DatabaseConfig { + pub fn url(&self) -> Option { + match self { + Self::Url { url } => Some(url.clone()), + Self::Full { + driver, + username, + password, + host, + port, + name, + } => { + let str = format!( + "{}://{}:{}@{}:{}/{}", + driver, username, password, host, port, name + ); + let url = url::Url::parse(&str).ok()?; + Some(url) + }, + } + } +} diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000..c73db5d --- /dev/null +++ b/src/database.rs @@ -0,0 +1,333 @@ +use crate::config::DatabaseConfig; +use crate::entity; + +#[derive(Clone, derive_more::AsRef)] +pub struct Database { + pool: sqlx::PgPool, +} + +pub type Result = std::result::Result; + +#[derive(Debug, derive_more::From, derive_more::Error, derive_more::Display)] +pub enum Error { + #[from] + Migrate(sqlx::migrate::MigrateError), + + #[from] + Sqlx(sqlx::Error), + + UserDoesNotExists, + UserAlreadyExists, + + ServerDoesNotExists, + + ChannelDoesNotExists, + + MessageDoesNotExists, +} + +impl Database { + pub async fn connect(config: &DatabaseConfig) -> sqlx::Result { + static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!(); + + let pool = sqlx::postgres::PgPoolOptions::new() + .connect(config.url().ok_or(sqlx::Error::BeginFailed)?.as_str()) + .await?; + + MIGRATOR.run(&pool).await?; + + Ok(Self { pool }) + } +} + +impl Database { + pub async fn insert_user( + &self, + username: &str, + display_name: Option<&str>, + email: &str, + password_hash: &str, + ) -> Result { + let user = match sqlx::query_as!( + entity::user::User, + r#"INSERT INTO "user"("username", "display_name", "email", "password_hash") VALUES ($1, $2, $3, $4) RETURNING "user".*"#, + username, + display_name, + email, + password_hash + ) + .fetch_one(&self.pool) + .await { + Ok(user) => user, + Err(sqlx::Error::Database(e)) if e.code() == Some("23505".into()) => { + return Err(Error::UserAlreadyExists); + } + Err(e) => return Err(e.into()), + }; + + Ok(user) + } + + pub async fn select_user_by_id(&self, user_id: entity::user::Id) -> Result { + let user = sqlx::query_as!( + entity::user::User, + r#"SELECT * FROM "user" WHERE "id" = $1"#, + user_id + ) + .fetch_optional(&self.pool) + .await? + .ok_or(Error::UserDoesNotExists)?; + + Ok(user) + } + + pub async fn select_users_by_ids( + &self, + user_ids: &[entity::user::Id], + ) -> Result> { + let mut query_builder = sqlx::QueryBuilder::new(r#"SELECT * FROM "user" WHERE "id" IN ("#); + + let mut separated = query_builder.separated(", "); + for id in user_ids.iter() { + separated.push_bind(id); + } + + query_builder.push(")"); + + let users = query_builder.build_query_as().fetch_all(&self.pool).await?; + + Ok(users) + } + + pub async fn select_user_by_username(&self, username: &str) -> Result { + let user = sqlx::query_as!( + entity::user::User, + r#"SELECT * FROM "user" WHERE "username" = $1"#, + username + ) + .fetch_optional(&self.pool) + .await? + .ok_or(Error::UserDoesNotExists)?; + + Ok(user) + } + + pub async fn select_server_by_id( + &self, + server_id: entity::server::Id, + ) -> Result { + let server = sqlx::query_as!( + entity::server::Server, + r#"SELECT * FROM "server" WHERE "id" = $1"#, + server_id + ) + .fetch_optional(&self.pool) + .await? + .ok_or(Error::ServerDoesNotExists)?; + + Ok(server) + } + + pub async fn select_user_servers( + &self, + user_id: entity::user::Id, + ) -> Result> { + let servers = sqlx::query_as!( + entity::server::Server, + r#"SELECT * FROM "server" WHERE "id" IN ( + SELECT "server_id" FROM "server_member" + WHERE "user_id" = $1 + )"#, + user_id + ) + .fetch_all(&self.pool) + .await?; + + Ok(servers) + } + + pub async fn select_user_channels( + &self, + user_id: entity::user::Id, + ) -> Result> { + // for some reason using macro overflows tokio stack + let channels = sqlx::query_as( + r#"SELECT * FROM "channel" WHERE "id" IN ( + SELECT "channel_id" FROM "channel_recipient" WHERE "user_id" = $1 + )"#, + ) + .bind(user_id) + .fetch_all(&self.pool) + .await?; + + Ok(channels) + } + + pub async fn insert_server( + &self, + name: &str, + icon_url: Option<&str>, + owner_id: entity::user::Id, + ) -> Result { + let server = sqlx::query_as!( + entity::server::Server, + r#"INSERT INTO "server"("name", "icon_url", "owner_id") VALUES ($1, $2, $3) RETURNING "server".*"#, + name, + icon_url, + owner_id + ) + .fetch_one(&self.pool) + .await?; + + Ok(server) + } + + pub async fn insert_server_role( + &self, + id: Option, + server_id: entity::server::Id, + name: &str, + color: Option<&str>, + display: bool, + permissions: serde_json::Value, + position: u16, + ) -> Result { + let role = sqlx::query_as!( + entity::server::role::ServerRole, + r#"INSERT INTO "server_role"("id", "server_id", "name", "color", "display", "permissions", "position") VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING "server_role".*"#, + id, + server_id, + name, + color, + display, + permissions, + position as i16 + ) + .fetch_one(&self.pool) + .await?; + + Ok(role) + } + + pub async fn insert_server_member( + &self, + server_id: entity::server::Id, + user_id: entity::user::Id, + ) -> Result { + let member = sqlx::query_as!( + entity::server::member::ServerMember, + r#"INSERT INTO "server_member"("server_id", "user_id") VALUES ($1, $2) RETURNING "server_member".*"#, + server_id, + user_id + ) + .fetch_one(&self.pool) + .await?; + + Ok(member) + } + + pub async fn insert_server_member_role( + &self, + server_member_id: entity::server::member::Id, + server_role_id: entity::server::role::Id, + ) -> Result<()> { + sqlx::query!( + r#"INSERT INTO "server_member_role"("member_id", "role_id") VALUES ($1, $2)"#, + server_member_id, + server_role_id + ) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn insert_server_channel( + &self, + server_id: entity::server::Id, + name: &str, + channel_type: entity::channel::ChannelType, + position: u16, + parent: Option, + ) -> Result { + let channel = sqlx::query_as!( + entity::channel::Channel, + r#"INSERT INTO "channel"("name", "type", "position", "server_id", "parent") VALUES ($1, $2, $3, $4, $5) RETURNING "channel".*"#, + name, + channel_type as i16, + position as i16, + server_id, + parent + ) + .fetch_one(&self.pool) + .await?; + + Ok(channel) + } + + pub async fn select_channel_by_id( + &self, + channel_id: entity::channel::Id, + ) -> Result { + let channel = sqlx::query_as(r#"SELECT * FROM "channel" WHERE "id" = $1"#) + .bind(channel_id) + .fetch_optional(&self.pool) + .await? + .ok_or(Error::ChannelDoesNotExists)?; + + Ok(channel) + } + + pub async fn select_channel_recipients( + &self, + channel_id: entity::channel::Id, + ) -> Result>> { + let recipients = sqlx::query_as!( + entity::user::User, + r#"SELECT * FROM "user" WHERE "id" IN ( + SELECT "user_id" FROM "channel_recipient" WHERE "channel_id" = $1 + )"#, + channel_id + ) + .fetch_all(&self.pool) + .await?; + + if recipients.is_empty() { + return Ok(None); + } + + Ok(Some(recipients)) + } + + pub async fn select_server_channels( + &self, + server_id: entity::server::Id, + ) -> Result> { + let channels = sqlx::query_as!( + entity::channel::Channel, + r#"SELECT * FROM "channel" WHERE "server_id" = $1"#, + server_id + ) + .fetch_all(&self.pool) + .await?; + + Ok(channels) + } + + pub async fn procedure_create_dm_channel( + &self, + user1_id: entity::user::Id, + user2_id: entity::user::Id, + ) -> Result> { + let channel_id = sqlx::query_scalar!( + r#"SELECT create_dm_channel($1, $2, $3)"#, + user1_id, + user2_id, + entity::channel::ChannelType::DirectMessage as i16 + ) + .fetch_one(&self.pool) + .await?; + + Ok(channel_id) + } +} diff --git a/src/entity/attachment.rs b/src/entity/attachment.rs new file mode 100644 index 0000000..7e83736 --- /dev/null +++ b/src/entity/attachment.rs @@ -0,0 +1,12 @@ +use serde::Serialize; + +pub type Id = uuid::Uuid; + +#[derive(Debug, Clone, sqlx::FromRow, Serialize)] +pub struct Attachment { + pub id: Id, + pub filename: String, + pub content_type: String, + pub url: String, + pub size: u64, +} diff --git a/src/entity/channel.rs b/src/entity/channel.rs new file mode 100644 index 0000000..0becc30 --- /dev/null +++ b/src/entity/channel.rs @@ -0,0 +1,41 @@ +use serde::Serialize; + +use crate::entity::{message, server, user}; + +pub type Id = uuid::Uuid; + +#[derive(Debug, Clone, sqlx::FromRow, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Channel { + pub id: Id, + pub name: String, + pub r#type: ChannelType, + pub position: i16, + #[serde(skip_serializing_if = "Option::is_none")] + pub server_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub parent: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub owner_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_message_id: Option, +} + +#[derive(Debug, Clone, sqlx::Type, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "snake_case")] +#[repr(i16)] +pub enum ChannelType { + ServerText = 1, + ServerVoice = 2, + ServerCategory = 3, + + DirectMessage = 4, + Group = 5, +} + +impl From for ChannelType { + fn from(value: i16) -> Self { + value.try_into().unwrap_or(ChannelType::ServerText) + } +} diff --git a/src/entity/message.rs b/src/entity/message.rs new file mode 100644 index 0000000..6597ffc --- /dev/null +++ b/src/entity/message.rs @@ -0,0 +1,15 @@ +use serde::Serialize; + +use crate::entity::{channel, user}; + +pub type Id = uuid::Uuid; + +#[derive(Debug, Clone, sqlx::FromRow, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Message { + pub id: Id, + pub author_id: user::Id, + pub channel_id: channel::Id, + pub content: String, + pub timestamp: chrono::DateTime, +} diff --git a/src/entity/mod.rs b/src/entity/mod.rs new file mode 100644 index 0000000..0767d0e --- /dev/null +++ b/src/entity/mod.rs @@ -0,0 +1,5 @@ +pub mod attachment; +pub mod channel; +pub mod message; +pub mod server; +pub mod user; diff --git a/src/entity/server.rs b/src/entity/server.rs new file mode 100644 index 0000000..4ed1f37 --- /dev/null +++ b/src/entity/server.rs @@ -0,0 +1,18 @@ +mod invite; +pub mod member; +pub mod role; + +use serde::Serialize; + +use crate::entity::user; + +pub type Id = uuid::Uuid; + +#[derive(Debug, Clone, sqlx::FromRow, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Server { + pub id: Id, + pub owner_id: user::Id, + pub name: String, + pub icon_url: Option, +} diff --git a/src/entity/server/invite.rs b/src/entity/server/invite.rs new file mode 100644 index 0000000..596e245 --- /dev/null +++ b/src/entity/server/invite.rs @@ -0,0 +1,13 @@ +use chrono::{DateTime, Utc}; +use serde::Serialize; + +use crate::entity::{server, user}; + +#[derive(Debug, Clone, sqlx::FromRow, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ServerInvite { + pub code: String, + pub server_id: server::Id, + pub inviter_id: Option, + pub expires_at: Option>, +} diff --git a/src/entity/server/member.rs b/src/entity/server/member.rs new file mode 100644 index 0000000..7783452 --- /dev/null +++ b/src/entity/server/member.rs @@ -0,0 +1,15 @@ +use serde::Serialize; + +use crate::entity::{server, user}; + +pub type Id = uuid::Uuid; + +#[derive(Debug, Clone, sqlx::FromRow, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ServerMember { + pub id: Id, + pub server_id: server::Id, + pub user_id: user::Id, + pub nickname: Option, + pub avatar_url: Option, +} diff --git a/src/entity/server/role.rs b/src/entity/server/role.rs new file mode 100644 index 0000000..c4b0186 --- /dev/null +++ b/src/entity/server/role.rs @@ -0,0 +1,17 @@ +use serde::Serialize; + +use crate::entity::server; + +pub type Id = uuid::Uuid; + +#[derive(Debug, Clone, sqlx::FromRow, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ServerRole { + pub id: Id, + pub server_id: server::Id, + pub name: String, + pub color: Option, + pub display: bool, + pub permissions: serde_json::Value, + pub position: i16, +} diff --git a/src/entity/user.rs b/src/entity/user.rs new file mode 100644 index 0000000..15a0260 --- /dev/null +++ b/src/entity/user.rs @@ -0,0 +1,21 @@ +use std::sync::LazyLock; + +use regex::Regex; + +pub static USERNAME_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9_.]+$").unwrap()); + +pub type Id = uuid::Uuid; + +#[derive(Debug, Clone, sqlx::FromRow)] +pub struct User { + pub id: Id, + pub avatar_url: Option, + pub username: String, + pub display_name: Option, + pub email: String, + pub password_hash: String, + pub bot: bool, + pub system: bool, + pub settings: serde_json::Value, +} diff --git a/src/jwt.rs b/src/jwt.rs new file mode 100644 index 0000000..ca373ee --- /dev/null +++ b/src/jwt.rs @@ -0,0 +1,55 @@ +#![allow(unused)] + +use std::collections::HashSet; + +use chrono::{Duration, Local, Utc}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; + +use crate::config; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub user_id: T, + pub iat: i64, +} + +pub fn generate_jwt(user_id: T) -> Result { + let claims = Claims { + user_id, + iat: Utc::now().timestamp_millis(), + }; + + let token = jsonwebtoken::encode( + &jsonwebtoken::Header::default(), + &claims, + &jsonwebtoken::EncodingKey::from_secret(config::config().jwt_secret.as_ref()), + ) + .map_err(|_| Error::CouldNotEncodeToken)?; + + Ok(token) +} + +pub fn verify_jwt(token: &str) -> Result { + tracing::debug!("verifying token: {}", token); + + let mut validation = jsonwebtoken::Validation::default(); + validation.required_spec_claims = HashSet::new(); + + let token_data = jsonwebtoken::decode::>( + token, + &jsonwebtoken::DecodingKey::from_secret(config::config().jwt_secret.as_ref()), + &validation, + ) + .map_err(|_| Error::CouldNotVerifyToken)?; + + Ok(token_data.claims.user_id) +} + +pub type Result = std::result::Result; + +#[derive(Debug, derive_more::Error, derive_more::Display)] +pub enum Error { + CouldNotEncodeToken, + CouldNotVerifyToken, +} diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..1b46f64 --- /dev/null +++ b/src/log.rs @@ -0,0 +1,43 @@ +use std::io; + +use tracing::level_filters::LevelFilter; +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::EnvFilter; +use tracing_subscriber::fmt::time::ChronoLocal; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; + +pub fn init_logging() -> io::Result<(WorkerGuard, WorkerGuard)> { + let logger_timer = ChronoLocal::new("%FT%H:%M:%S%.6f%:::z".to_string()); + + let file = tracing_appender::rolling::daily("./logs", "log"); + + let (file, file_guard) = tracing_appender::non_blocking(file); + + let file_logger = tracing_subscriber::fmt::layer() + .with_ansi(false) + .with_timer(logger_timer.clone()) + .with_writer(file); + + let (stdout, stdout_guard) = tracing_appender::non_blocking(io::stdout()); + + let stdout_logger = tracing_subscriber::fmt::layer() + .with_timer(logger_timer) + .with_writer(stdout); + #[cfg(debug_assertions)] + let stdout_logger = stdout_logger.pretty(); + + tracing_subscriber::registry() + .with( + EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(), + ) + .with(file_logger) + .with(stdout_logger) + .init(); + + tracing::info!("initialized logging"); + + Ok((file_guard, stdout_guard)) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..189bf60 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,35 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use argon2::Argon2; +use tokio::sync::RwLock; + +use crate::database::Database; +use crate::state::AppState; + +mod config; +mod database; +mod entity; +mod jwt; +mod log; +mod state; +mod util; +mod web; +mod webrtc; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let _guard = log::init_logging()?; + + let database = Database::connect(&config::config().database).await?; + let state = AppState { + database, + hasher: Arc::new(Argon2::default()), + event_connected_users: Arc::new(RwLock::new(HashMap::new())), + voice_rooms: Arc::new(RwLock::new(HashMap::new())), + }; + + web::run(state).await?; + + Ok(()) +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..982e369 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,51 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use argon2::Argon2; +use dashmap::DashMap; +use tokio::sync::{RwLock, mpsc}; +use uuid::Uuid; + +use crate::database::Database; +use crate::web::ws::gateway::{EventWsState, message}; +use crate::webrtc::OfferSignal; + +#[derive(Clone)] +pub struct AppState { + pub database: Database, + pub hasher: Arc>, + + pub event_connected_users: Arc>>, + + pub voice_rooms: Arc>>>, +} + +impl AppState { + pub async fn register_event_connected_user( + &self, + user_id: Uuid, + session_id: String, + event_sender: mpsc::UnboundedSender, + ) { + let mut connected_users = self.event_connected_users.write().await; + if let Some(state) = connected_users.get_mut(&user_id) { + state.connection_instance.insert(session_id, event_sender); + } else { + let state = EventWsState { + connection_instance: DashMap::new(), + }; + state.connection_instance.insert(session_id, event_sender); + connected_users.insert(user_id, state); + } + } + + pub async fn unregister_event_connected_user(&self, user_id: Uuid, session_id: &str) { + let mut connected_users = self.event_connected_users.write().await; + if let Some(state) = connected_users.get_mut(&user_id) { + state.connection_instance.remove(session_id); + if state.connection_instance.is_empty() { + connected_users.remove(&user_id); + } + } + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..00bb8f0 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,52 @@ +use axum::extract::multipart::Field; +use axum_typed_multipart::{FieldData, TryFromField, TypedMultipartError}; +use serde::Serialize; + +#[derive(Debug, derive_more::Deref)] +pub struct SerdeFieldData(pub FieldData); + +#[async_trait::async_trait] +impl TryFromField for SerdeFieldData { + async fn try_from_field( + field: Field<'_>, + limit_bytes: Option, + ) -> Result { + let field = FieldData::try_from_field(field, limit_bytes).await?; + + Ok(Self(field)) + } +} + +impl Serialize for SerdeFieldData { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + #[derive(Serialize)] + #[serde(rename_all = "camelCase")] + struct Metadata { + name: Option, + file_name: Option, + content_type: Option, + } + + let metadata = Metadata { + name: self.0.metadata.name.clone(), + file_name: self.0.metadata.file_name.clone(), + content_type: self.0.metadata.content_type.clone(), + }; + + metadata.serialize(serializer) + } +} + +pub fn serialize_duration_seconds( + duration: &std::time::Duration, + serializer: S, +) -> Result +where + S: serde::Serializer, +{ + let seconds = duration.as_secs(); + seconds.serialize(serializer) +} diff --git a/src/web/context.rs b/src/web/context.rs new file mode 100644 index 0000000..b256ba5 --- /dev/null +++ b/src/web/context.rs @@ -0,0 +1,35 @@ +use axum::extract::FromRequestParts; +use axum::http::request::Parts; + +use crate::entity; + +#[derive(Debug, Copy, Clone)] +pub struct UserContext { + pub user_id: entity::user::Id, +} + +pub type UserContextResult = Result; + +#[derive(Debug, Clone, Copy, derive_more::Display)] +pub enum Error { + NotInRequest, + NotInHeader, + BadCharacters, + WrongTokenType, + BadToken, + Model, +} + +impl FromRequestParts for UserContext { + type Rejection = super::error::Error; + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + let context = parts + .extensions + .get::() + .cloned() + .ok_or(Error::NotInRequest)??; + + Ok(context) + } +} diff --git a/src/web/error.rs b/src/web/error.rs new file mode 100644 index 0000000..50ff203 --- /dev/null +++ b/src/web/error.rs @@ -0,0 +1,147 @@ +use std::sync::Arc; + +use axum::http::StatusCode; +use axum::response::IntoResponse; + +use crate::web::context; +use crate::{database, jwt}; + +pub type Result = std::result::Result; + +#[derive(Debug, derive_more::From)] +pub enum Error { + #[from] + Context(context::Error), + + #[from] + Jwt(jwt::Error), + + #[from] + Hash(argon2::password_hash::Error), + + #[from] + Database(database::Error), + + #[from] + Json(serde_json::error::Error), + + #[from] + JsonRejection(axum::extract::rejection::JsonRejection), + + #[from] + Client(ClientError), +} + +#[derive(Debug, Clone, derive_more::From, serde::Serialize)] +#[serde(tag = "message", content = "details")] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ClientError { + UserAlreadyExists, + UserDoesNotExist, + + ServerDoesNotExist, + + ChannelDoesNotExist, + + MessageDoesNotExist, + + NotAuthorized, + WrongPassword, + NotAllowed, + + InvalidJson(JsonRejection), + + #[from] + ValidationFailed(validator::ValidationErrors), + + InternalServerError, +} + +#[derive(derive_more::Debug, Clone, serde::Serialize)] +pub struct JsonRejection { + #[serde(skip)] + status: StatusCode, + + reason: String, +} + +impl From<&axum::extract::rejection::JsonRejection> for JsonRejection { + fn from(value: &axum::extract::rejection::JsonRejection) -> Self { + use std::error::Error; + + use axum::extract::rejection::JsonRejection::*; + + let reason = match value { + JsonDataError(e) => e.source().map(ToString::to_string), + JsonSyntaxError(e) => e.source().map(ToString::to_string), + MissingJsonContentType(e) => e.source().map(ToString::to_string), + BytesRejection(e) => e.source().map(ToString::to_string), + _ => None, + } + .unwrap_or_else(|| value.body_text()); + + Self { + status: value.status(), + reason, + } + } +} + +impl Error { + pub fn as_client_error(&self) -> ClientError { + match self { + Error::Context(_) | Error::Jwt(_) => ClientError::NotAuthorized, + Error::Database(database::Error::UserAlreadyExists) => ClientError::UserAlreadyExists, + Error::Database(database::Error::UserDoesNotExists) => ClientError::UserDoesNotExist, + + Error::Database(database::Error::ServerDoesNotExists) => { + ClientError::ServerDoesNotExist + }, + + Error::Database(database::Error::ChannelDoesNotExists) => { + ClientError::ChannelDoesNotExist + }, + + Error::Database(database::Error::MessageDoesNotExists) => { + ClientError::MessageDoesNotExist + }, + // Error::WrongPassword => ClientError::WrongPassword, + // Error::NotAllowed => ClientError::NotAllowed, + Error::JsonRejection(e) => ClientError::InvalidJson(e.into()), + Error::Client(client_error) => client_error.clone(), + _ => ClientError::InternalServerError, + } + } +} + +impl IntoResponse for Error { + fn into_response(self) -> axum::response::Response { + let mut response = StatusCode::INTERNAL_SERVER_ERROR.into_response(); + + response.extensions_mut().insert(Arc::new(self)); + + response + } +} + +impl ClientError { + pub fn status_code(&self) -> StatusCode { + match self { + ClientError::UserAlreadyExists => StatusCode::CONFLICT, + ClientError::UserDoesNotExist + | ClientError::ServerDoesNotExist + | ClientError::ChannelDoesNotExist + | ClientError::MessageDoesNotExist => StatusCode::NOT_FOUND, + + ClientError::NotAuthorized | ClientError::WrongPassword => StatusCode::UNAUTHORIZED, + + ClientError::NotAllowed => StatusCode::FORBIDDEN, + + ClientError::ValidationFailed(_) => StatusCode::UNPROCESSABLE_ENTITY, + + ClientError::InvalidJson(e) => e.status, + + _ => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} diff --git a/src/web/middleware/auth.rs b/src/web/middleware/auth.rs new file mode 100644 index 0000000..0373760 --- /dev/null +++ b/src/web/middleware/auth.rs @@ -0,0 +1,63 @@ +use axum::extract::{Request, State}; +use axum::middleware::Next; +use axum::response::{IntoResponse, Response}; +use axum::{Extension, RequestExt}; +use axum_extra::TypedHeader; +use axum_extra::headers::Authorization; +use axum_extra::headers::authorization::Bearer; +use axum_extra::typed_header::TypedHeaderRejectionReason; + +use crate::jwt; +use crate::state::AppState; +use crate::web::{self, context}; + +pub async fn require_context( + Extension(context): Extension, + request: Request, + next: Next, +) -> web::error::Result { + context?; + + Ok(next.run(request).await) +} + +pub async fn resolve_context( + State(state): State, + mut request: Request, + next: Next, +) -> impl IntoResponse { + let context = get_context(&state, &mut request).await; + + request.extensions_mut().insert(context); + + next.run(request).await +} + +async fn get_context(state: &AppState, request: &mut Request) -> context::UserContextResult { + let bearer = request + .extract_parts::>>() + .await + .map_err(|error| match error.reason() { + TypedHeaderRejectionReason::Missing => context::Error::NotInHeader, + TypedHeaderRejectionReason::Error(_) => context::Error::WrongTokenType, + _ => context::Error::NotInHeader, + })?; + + let token = bearer.token(); + + let context = get_context_from_token(state, token).await?; + + Ok(context) +} + +pub async fn get_context_from_token(state: &AppState, token: &str) -> context::UserContextResult { + let user_id = jwt::verify_jwt(token).map_err(|_| context::Error::BadToken)?; + + let _ = state + .database + .select_user_by_id(user_id) + .await + .map_err(|_| context::Error::BadToken)?; + + Ok(context::UserContext { user_id }) +} diff --git a/src/web/middleware/mod.rs b/src/web/middleware/mod.rs new file mode 100644 index 0000000..7b59663 --- /dev/null +++ b/src/web/middleware/mod.rs @@ -0,0 +1,5 @@ +mod auth; +mod response_map; + +pub use auth::*; +pub use response_map::*; diff --git a/src/web/middleware/response_map.rs b/src/web/middleware/response_map.rs new file mode 100644 index 0000000..83f0aae --- /dev/null +++ b/src/web/middleware/response_map.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; + +use axum::Json; +use axum::extract::Request; +use axum::middleware::Next; +use axum::response::IntoResponse; +use serde_json::json; + +use crate::web; + +pub async fn response_map(request: Request, next: Next) -> impl IntoResponse { + let response = next.run(request).await; + + let error = response.extensions().get::>(); + + if error.is_some() { + tracing::error!("{:?}", error); + } + + let error_response = error.map(|error| { + let client_error = error.as_client_error(); + + ( + client_error.status_code(), + Json(json!({"error": client_error})), + ) + .into_response() + }); + + error_response.unwrap_or(response) +} diff --git a/src/web/mod.rs b/src/web/mod.rs new file mode 100644 index 0000000..5f2a826 --- /dev/null +++ b/src/web/mod.rs @@ -0,0 +1,80 @@ +mod context; +mod error; +mod middleware; +mod route; +pub mod ws; + +use tower_http::cors::{AllowHeaders, AllowMethods, AllowOrigin}; + +pub use self::error::{Error, Result}; +use crate::{config, state}; + +pub async fn run(state: state::AppState) -> anyhow::Result<()> { + let config = config::config(); + + let addr: std::net::SocketAddr = ([127, 0, 0, 1], config.port).into(); + tracing::info!("listening on {}", addr); + let listener = tokio::net::TcpListener::bind(addr).await?; + + axum::serve(listener, router(state)) + .with_graceful_shutdown(shutdown_signal()) + .await?; + + Ok(()) +} + +fn router(state: state::AppState) -> axum::Router { + use axum::Router; + use axum::routing::*; + + use self::route::*; + + let cors = tower_http::cors::CorsLayer::new() + .allow_origin(AllowOrigin::any()) + .allow_methods(AllowMethods::any()) + .allow_headers(AllowHeaders::any()); + + Router::new() + // websocket + .nest( + "/api/v1", + Router::new() + .route("/ws", get(ws::gateway::ws_handler)) + .route("/auth/login", post(auth::login)) + .route("/auth/register", post(auth::register)) + .merge(protected_router()) + .route_layer(axum::middleware::from_fn_with_state( + state.clone(), + middleware::resolve_context, + )), + ) + .layer(axum::middleware::from_fn(middleware::response_map)) + .layer(cors) + .layer(tower_http::trace::TraceLayer::new_for_http()) + .with_state(state.clone()) +} + +fn protected_router() -> axum::Router { + use axum::Router; + use axum::routing::*; + + use self::route::*; + + Router::new() + // user + .route("/users/@me", get(user::me)) + .route("/users/@me/channels", get(user::channel::list)) + .route("/users/{id}", get(user::get_by_id)) + // server + .route("/servers", get(server::list)) + .route("/servers", post(server::create)) + .route("/servers/{server_id}", get(server::get)) + .route("/servers/{server_id}/channels", get(server::channel::list)) + .route("/voice/{channel_id}/connect", post(voice::connect)) + // middleware + .route_layer(axum::middleware::from_fn(middleware::require_context)) +} + +async fn shutdown_signal() { + _ = tokio::signal::ctrl_c().await; +} diff --git a/src/web/route/auth/login.rs b/src/web/route/auth/login.rs new file mode 100644 index 0000000..10a0b8a --- /dev/null +++ b/src/web/route/auth/login.rs @@ -0,0 +1,49 @@ +use argon2::{PasswordHash, PasswordVerifier}; +use axum::Json; +use axum::extract::State; +use axum::response::IntoResponse; +use serde::{Deserialize, Serialize}; + +use crate::state::AppState; +use crate::web::route::user::FullUser; +use crate::{jwt, web}; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LoginPayload { + username: String, + password: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LoginResponse { + user: FullUser, + token: String, +} + +pub async fn login( + State(state): State, + Json(payload): Json, +) -> web::Result { + let user = state + .database + .select_user_by_username(&payload.username) + .await?; + + let password_hash = PasswordHash::new(&user.password_hash)?; + + state + .hasher + .verify_password(payload.password.as_bytes(), &password_hash) + .map_err(|_| web::error::ClientError::WrongPassword)?; + + let token = jwt::generate_jwt(user.id)?; + + let response = LoginResponse { + user: user.into(), + token, + }; + + Ok(Json(response)) +} diff --git a/src/web/route/auth/mod.rs b/src/web/route/auth/mod.rs new file mode 100644 index 0000000..3093e74 --- /dev/null +++ b/src/web/route/auth/mod.rs @@ -0,0 +1,5 @@ +pub mod login; +pub mod register; + +pub use login::*; +pub use register::*; diff --git a/src/web/route/auth/register.rs b/src/web/route/auth/register.rs new file mode 100644 index 0000000..5759d94 --- /dev/null +++ b/src/web/route/auth/register.rs @@ -0,0 +1,68 @@ +use argon2::password_hash::rand_core::OsRng; +use argon2::password_hash::{PasswordHasher, SaltString}; +use axum::Json; +use axum::extract::State; +use axum::response::IntoResponse; +use axum_extra::extract::WithRejection; +use serde::Deserialize; +use validator::Validate; + +use crate::state::AppState; +use crate::web; +use crate::web::error::ClientError; +use crate::web::route::user::FullUser; + +#[derive(Validate, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RegisterPayload { + #[validate(regex( + path = "crate::entity::user::USERNAME_REGEX", + code = "invalid_username" + ))] + #[validate(length(min = 3, max = 64))] + username: String, + + #[validate(length(min = 1, max = 64))] + display_name: Option, + + #[validate(email)] + #[validate(length(min = 3, max = 128))] + email: String, + + #[validate(length(min = 3, max = 128))] + password: String, +} + +pub async fn register( + State(state): State, + WithRejection(Json(payload), _): WithRejection, web::Error>, +) -> web::Result { + payload.validate().map_err(ClientError::ValidationFailed)?; + + let salt = SaltString::generate(&mut OsRng); + + let password_hash = state + .hasher + .hash_password(payload.password.as_bytes(), &salt)? + .to_string(); + + let user = state + .database + .insert_user( + &payload.username, + payload.display_name.as_ref().map(String::as_str), + &payload.email, + &password_hash, + ) + .await?; + + let system_user = state.database.select_user_by_username("system").await?; + if system_user.system { + state + .database + .procedure_create_dm_channel(user.id, system_user.id) + .await?; + } + + Ok(Json(FullUser::from(user))) +} diff --git a/src/web/route/mod.rs b/src/web/route/mod.rs new file mode 100644 index 0000000..5805195 --- /dev/null +++ b/src/web/route/mod.rs @@ -0,0 +1,4 @@ +pub mod auth; +pub mod server; +pub mod user; +pub mod voice; diff --git a/src/web/route/server/channel/list.rs b/src/web/route/server/channel/list.rs new file mode 100644 index 0000000..bf1330e --- /dev/null +++ b/src/web/route/server/channel/list.rs @@ -0,0 +1,17 @@ +use axum::Json; +use axum::extract::{Path, State}; +use axum::response::IntoResponse; + +use crate::state::AppState; +use crate::web::context::UserContext; +use crate::{entity, web}; + +pub async fn list( + State(state): State, + context: UserContext, + Path(id): Path, +) -> web::Result { + let channels = state.database.select_server_channels(id).await?; + + Ok(Json(channels)) +} diff --git a/src/web/route/server/channel/mod.rs b/src/web/route/server/channel/mod.rs new file mode 100644 index 0000000..4ce14ea --- /dev/null +++ b/src/web/route/server/channel/mod.rs @@ -0,0 +1,3 @@ +mod list; + +pub use list::*; diff --git a/src/web/route/server/create.rs b/src/web/route/server/create.rs new file mode 100644 index 0000000..cfed669 --- /dev/null +++ b/src/web/route/server/create.rs @@ -0,0 +1,83 @@ +use axum::Json; +use axum::body::Bytes; +use axum::extract::State; +use axum::response::IntoResponse; +use axum_typed_multipart::{TryFromMultipart, TypedMultipart}; +use validator::{Validate, ValidationError}; + +use crate::state::AppState; +use crate::util::SerdeFieldData; +use crate::web; +use crate::web::context::UserContext; +use crate::web::error::ClientError; +use crate::web::ws; + +#[derive(Debug, Validate, TryFromMultipart)] +#[try_from_multipart(rename_all = "camelCase")] +pub struct CreatePayload { + #[validate(length(min = 1, max = 32))] + name: String, + + #[validate(custom(function = "validate_icon_content_type"))] + #[form_data(limit = "10MB")] + icon: Option>, +} + +fn validate_icon_content_type(icon: &SerdeFieldData) -> Result<(), ValidationError> { + if let Some(content_type) = icon.metadata.content_type.as_deref() { + if !content_type.starts_with("image/") { + return Err(ValidationError::new("invalid_icon_content_type")); + } + } else { + return Err(ValidationError::new("missing_icon_content_type")); + } + + Ok(()) +} + +pub async fn create( + State(state): State, + context: UserContext, + TypedMultipart(payload): TypedMultipart, +) -> web::Result { + payload.validate().map_err(ClientError::ValidationFailed)?; + + let server = state + .database + .insert_server(&payload.name, None, context.user_id) + .await?; + + let role = state + .database + .insert_server_role( + Some(server.id.into()), + server.id, + "@everyone", + None, + false, + serde_json::json!({}), + 0, + ) + .await?; + + let member = state + .database + .insert_server_member(server.id, context.user_id) + .await?; + + state + .database + .insert_server_member_role(member.id, role.id) + .await?; + + ws::gateway::util::send_message( + &state, + context.user_id, + ws::gateway::message::Event::AddServer { + server: server.clone(), + }, + ) + .await; + + Ok(Json(server)) +} diff --git a/src/web/route/server/get.rs b/src/web/route/server/get.rs new file mode 100644 index 0000000..483b199 --- /dev/null +++ b/src/web/route/server/get.rs @@ -0,0 +1,15 @@ +use axum::Json; +use axum::extract::{Path, State}; +use axum::response::IntoResponse; + +use crate::state::AppState; +use crate::{entity, web}; + +pub async fn get( + State(state): State, + Path(id): Path, +) -> web::Result { + let server = state.database.select_server_by_id(id).await?; + + Ok(Json(server)) +} diff --git a/src/web/route/server/list.rs b/src/web/route/server/list.rs new file mode 100644 index 0000000..8d482c1 --- /dev/null +++ b/src/web/route/server/list.rs @@ -0,0 +1,16 @@ +use axum::Json; +use axum::extract::State; +use axum::response::IntoResponse; + +use crate::state::AppState; +use crate::web; +use crate::web::context::UserContext; + +pub async fn list( + State(state): State, + context: UserContext, +) -> web::Result { + let servers = state.database.select_user_servers(context.user_id).await?; + + Ok(Json(servers)) +} diff --git a/src/web/route/server/mod.rs b/src/web/route/server/mod.rs new file mode 100644 index 0000000..c65146e --- /dev/null +++ b/src/web/route/server/mod.rs @@ -0,0 +1,8 @@ +pub mod channel; +mod create; +mod get; +mod list; + +pub use create::*; +pub use get::*; +pub use list::*; diff --git a/src/web/route/user/channel/list.rs b/src/web/route/user/channel/list.rs new file mode 100644 index 0000000..8e146af --- /dev/null +++ b/src/web/route/user/channel/list.rs @@ -0,0 +1,48 @@ +use axum::Json; +use axum::extract::State; +use axum::response::IntoResponse; +use serde::Serialize; + +use crate::entity::channel; +use crate::state::AppState; +use crate::web; +use crate::web::context::UserContext; +use crate::web::route::user::PartialUser; + +#[derive(Debug, sqlx::FromRow, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RecipientChannel { + #[serde(flatten)] + pub channel: channel::Channel, + pub recipients: Vec, +} + +pub async fn list( + State(state): State, + context: UserContext, +) -> web::Result { + let channels = state.database.select_user_channels(context.user_id).await?; + + let mut recipient_channels = Vec::new(); + for channel in channels { + let recipients = state.database.select_channel_recipients(channel.id).await?; + + let recipients = match recipients { + Some(recipients) => recipients + .into_iter() + .filter(|user| user.id != context.user_id) + .map(PartialUser::from) + .collect(), + None => { + continue; + }, + }; + + recipient_channels.push(RecipientChannel { + channel, + recipients, + }); + } + + Ok(Json(recipient_channels)) +} diff --git a/src/web/route/user/channel/mod.rs b/src/web/route/user/channel/mod.rs new file mode 100644 index 0000000..4ce14ea --- /dev/null +++ b/src/web/route/user/channel/mod.rs @@ -0,0 +1,3 @@ +mod list; + +pub use list::*; diff --git a/src/web/route/user/get.rs b/src/web/route/user/get.rs new file mode 100644 index 0000000..d017ea7 --- /dev/null +++ b/src/web/route/user/get.rs @@ -0,0 +1,16 @@ +use axum::Json; +use axum::extract::{Path, State}; +use axum::response::IntoResponse; + +use crate::state::AppState; +use crate::web; +use crate::web::route::user::PartialUser; + +pub async fn get_by_id( + Path(id): Path, + State(state): State, +) -> web::Result { + let user = state.database.select_user_by_id(id).await?; + + Ok(Json(PartialUser::from(user))) +} diff --git a/src/web/route/user/me.rs b/src/web/route/user/me.rs new file mode 100644 index 0000000..d9e81b5 --- /dev/null +++ b/src/web/route/user/me.rs @@ -0,0 +1,17 @@ +use axum::Json; +use axum::extract::State; +use axum::response::IntoResponse; + +use crate::state::AppState; +use crate::web; +use crate::web::context::UserContext; +use crate::web::route::user::FullUser; + +pub async fn me( + context: UserContext, + State(state): State, +) -> web::Result { + let user = state.database.select_user_by_id(context.user_id).await?; + + Ok(Json(FullUser::from(user))) +} diff --git a/src/web/route/user/mod.rs b/src/web/route/user/mod.rs new file mode 100644 index 0000000..91132b1 --- /dev/null +++ b/src/web/route/user/mod.rs @@ -0,0 +1,60 @@ +pub mod channel; +mod get; +mod me; + +pub use get::*; +pub use me::*; + +use crate::entity::user; + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct FullUser { + pub id: user::Id, + pub avatar_url: Option, + pub username: String, + pub display_name: Option, + pub email: String, + pub bot: bool, + pub system: bool, + pub settings: serde_json::Value, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct PartialUser { + pub id: user::Id, + pub avatar_url: Option, + pub username: String, + pub display_name: Option, + pub bot: bool, + pub system: bool, +} + +impl From for FullUser { + fn from(user: user::User) -> Self { + Self { + id: user.id, + avatar_url: user.avatar_url, + username: user.username, + display_name: user.display_name, + email: user.email, + bot: user.bot, + system: user.system, + settings: user.settings, + } + } +} + +impl From for PartialUser { + fn from(user: user::User) -> Self { + Self { + id: user.id, + avatar_url: user.avatar_url, + username: user.username, + display_name: user.display_name, + bot: user.bot, + system: user.system, + } + } +} diff --git a/src/web/route/voice/connect.rs b/src/web/route/voice/connect.rs new file mode 100644 index 0000000..04b9b2d --- /dev/null +++ b/src/web/route/voice/connect.rs @@ -0,0 +1,91 @@ +use axum::Json; +use axum::extract::{Path, State}; +use axum::response::IntoResponse; +use axum_extra::extract::WithRejection; +use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; + +use crate::state::AppState; +use crate::web::context::UserContext; +use crate::{entity, web}; + +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Payload { + sdp: RTCSessionDescription, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Response { + sdp: RTCSessionDescription, +} + +pub async fn connect( + State(state): State, + context: UserContext, + Path(channel_id): Path, + WithRejection(Json(payload), _): WithRejection, web::Error>, +) -> web::Result { + tracing::debug!("connect to voice channel: {:?}", channel_id); + + let channel = state.database.select_channel_by_id(channel_id).await?; + let channel_id = channel.id; + + let room_sender = { + state + .voice_rooms + .read() + .await + .get(&channel_id) + .map(|room| room.clone()) + }; + + let room_sender = match room_sender { + Some(room) => room, + None => { + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + + let rooms = state.voice_rooms.clone(); + tokio::spawn(async move { + crate::webrtc::webrtc_task(channel_id, rx) + .await + .unwrap_or_else(|err| { + tracing::error!("webrtc task error: {:?}", err); + }); + + { + let mut rooms = rooms.write().await; + rooms.remove(&channel_id); + } + }); + + { + let mut rooms = state.voice_rooms.write().await; + rooms.insert(channel_id, tx.clone()); + } + + tx + }, + }; + + let offer = crate::webrtc::Offer { + peer_id: context.user_id, + sdp_offer: payload.sdp, + }; + + let (response_tx, response_rx) = tokio::sync::oneshot::channel(); + let _ = room_sender.send(crate::webrtc::OfferSignal { + offer, + response: response_tx, + }); + + let answer = response_rx + .await + .map_err(|_| web::error::ClientError::InternalServerError)?; + + let response = Response { + sdp: answer.sdp_answer, + }; + + Ok(Json(response)) +} diff --git a/src/web/route/voice/mod.rs b/src/web/route/voice/mod.rs new file mode 100644 index 0000000..08cbc12 --- /dev/null +++ b/src/web/route/voice/mod.rs @@ -0,0 +1,3 @@ +mod connect; + +pub use connect::*; diff --git a/src/web/ws/gateway/connection.rs b/src/web/ws/gateway/connection.rs new file mode 100644 index 0000000..2672931 --- /dev/null +++ b/src/web/ws/gateway/connection.rs @@ -0,0 +1,340 @@ +// src/web/ws/connection.rs + +use std::ops::ControlFlow; +use std::time::Duration; + +use axum::extract::ws::{Message as AxumMessage, WebSocket}; +use base64::Engine as _; // Bring trait into scope +use futures::stream::SplitStream; +use futures::{SinkExt, StreamExt}; +use sha2::{Digest, Sha256}; +use tokio::time::Instant; + +// Use items from sibling modules within `ws` +use super::error::{self, Error as WsError}; +// Assuming Event type is from ws::message +use super::message::Event as WsEvent; +use super::protocol::{ + SendWsMessage, WsClientMessage, WsServerMessage, deserialize_ws_message, serialize_ws_message, +}; +use super::state::{WsContext, WsState, WsUserContext}; +use crate::state::AppState; + +/// Main handler for an individual WebSocket connection's lifecycle. +/// Spawned by Axum upon successful WebSocket upgrade. +#[tracing::instrument(skip_all, name = "ws_connection_handler")] +pub async fn handle_socket_connection(websocket: WebSocket, app_state: AppState) { + let (ws_sink, ws_stream) = websocket.split(); + + let (internal_send_tx, mut internal_send_rx) = tokio::sync::mpsc::unbounded_channel(); + + // Writer task: consumes messages from MPSC channel and sends them to the WebSocket sink. + let writer_task = tokio::spawn(async move { + let mut ws_sink_mut = ws_sink; + while let Some(SendWsMessage { + message, + response_ch, + }) = internal_send_rx.recv().await + { + let send_result = match serialize_ws_message(message) { + Ok(ws_msg) => { + if ws_sink_mut.send(ws_msg).await.is_err() { + Err(WsError::WebSocketClosed) // Send to client failed + } else { + Ok(()) + } + }, + Err(e) => Err(e), // Serialization error itself + }; + + if let Some(ch) = response_ch { + if ch.send(send_result).is_err() { + // Log if the receiver of the acknowledgement was dropped, though this is unlikely + // if send_with_response is awaiting it. + tracing::debug!("Failed to send acknowledgement; receiver dropped."); + } + } else if let Err(e) = send_result { + // For fire-and-forget, log critical errors (not just WebSocketClosed). + if !matches!(e, WsError::WebSocketClosed) { + tracing::warn!("Error in fire-and-forget WebSocket send: {:?}", e); + } + } + } + // MPSC channel closed, attempt to gracefully close WebSocket. + if ws_sink_mut.close().await.is_err() { + tracing::debug!("Error closing WebSocket sink; connection might be already dead."); + } + }); + + let mut context = WsContext { + connection_state: WsState::Initialize, + user_context: None, + heartbeat_interval: std::time::Duration::from_secs(30), // Assuming config path + next_ping_deadline: Instant::now(), // Will be properly set before first use + event_channel: None, + }; + let processing_result = process_websocket_messages( + &mut context, + ws_stream, + &internal_send_tx, // Pass as reference + &app_state, + ) + .await; + + // --- Cleanup --- + if let Some(user_ctx_data) = &context.user_context { + app_state + .unregister_event_connected_user(user_ctx_data.user_id, &user_ctx_data.session_key) + .await; + tracing::info!(user_id = ?user_ctx_data.user_id, session_key = %user_ctx_data.session_key, "Unregistered WebSocket user."); + } + + // Drop our sender for the event channel; receiver in `process_websocket_messages` will see this. + drop(context.event_channel.take()); + + // If processing loop exited with an error (not a graceful close like WebSocketClosed or HeartbeatTimeout), + // try to send a final error message to the client. + if let Err(err_to_report) = &processing_result { + if !matches!( + err_to_report, + WsError::WebSocketClosed | WsError::HeartbeatTimeout + ) { + tracing::warn!( + "WebSocket processing error, attempting to notify client: {:?}", + err_to_report + ); + let client_err_code = err_to_report.into_client_error(); + let error_ws_message = WsServerMessage::Error { + code: client_err_code, + }; + // Use new_no_response for best-effort send during shutdown. + // Ignore result as internal_send_tx might already be closed if writer_task ended. + let _ = internal_send_tx.send(SendWsMessage::new_no_response(error_ws_message)); + } + } + + // Signal writer task to stop by dropping the MPSC sender. + drop(internal_send_tx); + // Wait for the writer task to complete its shutdown. + if let Err(e) = writer_task.await { + tracing::error!( + "WebSocket writer task panicked or encountered an error: {:?}", + e + ); + } + tracing::debug!(result = ?processing_result, "WebSocket connection handler finished."); +} + +/// Main loop for processing incoming WebSocket messages and outgoing application events. +/// Manages state transitions (Initialize -> Connected) and heartbeating. +#[tracing::instrument(skip_all, fields(state = ?context.connection_state, user_id = ?context.user_context.as_ref().map(|uc| uc.user_id)))] +async fn process_websocket_messages( + context: &mut WsContext, + mut ws_stream: SplitStream, + sender: &tokio::sync::mpsc::UnboundedSender, // Changed to reference + app_state: &AppState, +) -> error::Result<()> { + // Send initial heartbeat interval and set first deadline. + SendWsMessage::send_with_response( + sender, + WsServerMessage::HeartbeatInterval { + interval: context.heartbeat_interval, + }, + ) + .await?; + context.reset_deadline(); + + loop { + match context.connection_state { + WsState::Initialize => { + tokio::select! { + biased; // Prefer timeout check if multiple branches are ready + _ = tokio::time::sleep_until(context.next_ping_deadline) => { + tracing::warn!("Initial connection timeout (no Authenticate or Ping)."); + return Err(WsError::HeartbeatTimeout); + } + maybe_message = ws_stream.next() => { + match maybe_message { + Some(Ok(message)) => { + match handle_initial_message(context, message, sender, app_state).await { + Ok(ControlFlow::Continue(())) => {}, + Ok(ControlFlow::Break(new_state)) => { // Authenticated + context.connection_state = new_state; + tracing::info!(user_id = ?context.user_context.as_ref().unwrap().user_id, "User authenticated, WebSocket connected."); + }, + Err(e) => { // Auth failed critically or other error + return Err(e); + } + } + } + Some(Err(axum_ws_err)) => { + tracing::debug!("WebSocket stream error during Initialize: {:?}", axum_ws_err); + return Err(WsError::WebSocketClosed); + } + None => { // Stream closed by client + tracing::debug!("WebSocket stream ended by client during Initialize state."); + return Err(WsError::WebSocketClosed); + } + } + } + } + }, + WsState::Connected => { + let user_ctx = context + .user_context + .as_ref() + .expect("User context must be set in Connected state"); + let (_event_tx_ref, event_rx) = context + .event_channel + .as_mut() + .expect("Event channel must be set in Connected state"); + + tokio::select! { + biased; + _ = tokio::time::sleep_until(context.next_ping_deadline) => { + tracing::warn!(user_id = ?user_ctx.user_id, "Heartbeat timeout."); + return Err(WsError::HeartbeatTimeout); + } + // Listen for application events to send to the client + maybe_app_event = event_rx.recv() => { + if let Some(app_event_data) = maybe_app_event { + SendWsMessage::send_with_response(sender, WsServerMessage::Event { event: app_event_data }).await?; + // Sending an app event doesn't reset the client's ping requirement. + } else { + // Event channel closed (e.g., AppState unregistered, or system shutdown signal) + tracing::info!(user_id = ?user_ctx.user_id, "Event channel closed, closing WebSocket."); + return Ok(()); // Graceful shutdown signaled by closed event channel + } + } + // Listen for messages from the client (e.g., Ping) + maybe_ws_message = ws_stream.next() => { + match maybe_ws_message { + Some(Ok(message)) => { + handle_connected_message(context, message, sender).await?; + } + Some(Err(axum_ws_err)) => { + tracing::debug!(user_id = ?user_ctx.user_id, "WebSocket stream error during Connected: {:?}", axum_ws_err); + return Err(WsError::WebSocketClosed); + } + None => { // Stream closed by client + tracing::debug!(user_id = ?user_ctx.user_id, "WebSocket stream ended by client during Connected state."); + return Err(WsError::WebSocketClosed); + } + } + } + } + }, + } + } +} + +/// Handles messages received when the connection is in the `Initialize` state. +/// Expects `Authenticate` to transition to `Connected`, or `Ping` to stay in `Initialize`. +#[tracing::instrument(skip_all, fields(state = ?context.connection_state))] +async fn handle_initial_message( + context: &mut WsContext, + message: AxumMessage, + sender: &tokio::sync::mpsc::UnboundedSender, // Changed to reference + app_state: &AppState, +) -> error::Result> { + // Break(NewState) or Continue(()) + match deserialize_ws_message(message)? { + WsClientMessage::Authenticate { token } => { + // IMPORTANT: Adjust the call below to your actual token validation logic. + // Assuming `get_context_from_token` returns `Result` + match crate::web::middleware::get_context_from_token( + &app_state, // Example: Pass necessary parts of AppState + &token, + ) + .await + { + Ok(auth_user_context) => { + // auth_user_context is `crate::web::context::UserContext` + let user_id = auth_user_context.user_id; + + let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel::(); + context.event_channel = Some((event_tx.clone(), event_rx)); + + let random_key_part = rand::random::(); + let current_session_key = { + let mut hasher = Sha256::new(); + hasher.update(token.as_bytes()); + hasher.update(user_id.to_string().as_bytes()); + hasher.update(&random_key_part.to_be_bytes()); + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hasher.finalize()) + }; + + context.user_context = Some(WsUserContext { + user_id, + session_key: current_session_key.clone(), + }); + + app_state + .register_event_connected_user( + user_id, + current_session_key.clone(), + event_tx, // This is ws::state::EventSender -> mpsc::UnboundedSender + ) + .await; + + SendWsMessage::send_with_response( + sender, + WsServerMessage::AuthenticateAccepted { + user_id, + session_key: current_session_key.clone(), + }, + ) + .await?; + // Deadline is reset by the caller upon ControlFlow::Break + Ok(ControlFlow::Break(WsState::Connected)) + }, + Err(_auth_err) => { + tracing::warn!(token = %token, "Authentication failed for token."); + // Send AuthenticateDenied, then the connection will be closed by HeartbeatTimeout or by returning error. + // We send response to ensure client gets the denial before we might drop connection. + let _ = SendWsMessage::send_with_response( + sender, + WsServerMessage::AuthenticateDenied, + ) + .await; + Err(WsError::AuthenticationFailed) // This will terminate process_websocket_messages + }, + } + }, + WsClientMessage::Ping => { + context.reset_deadline(); // Reset deadline on successful ping + SendWsMessage::send_with_response(sender, WsServerMessage::Pong).await?; + Ok(ControlFlow::Continue(())) + }, + // Per original code, only Authenticate and Ping are expected in Initialize. + // If WsClientMessage has other variants, this might need adjustment. + #[allow(unreachable_patterns)] + _ => { + tracing::warn!("Unexpected message type received during Initialize state."); + Err(WsError::UnexpectedMessageType) + }, + } +} + +/// Handles messages received when the connection is in the `Connected` state. +/// Primarily expects `Ping` messages to keep the connection alive. +#[tracing::instrument(skip_all, fields(user_id = ?context.user_context.as_ref().map(|uc| uc.user_id)))] +async fn handle_connected_message( + context: &mut WsContext, // Although not heavily used here, good for consistency and tracing + message: AxumMessage, + sender: &tokio::sync::mpsc::UnboundedSender, // Changed to reference +) -> error::Result<()> { + match deserialize_ws_message(message)? { + WsClientMessage::Ping => { + tracing::debug!("Ping received."); + context.reset_deadline(); + SendWsMessage::send_with_response(sender, WsServerMessage::Pong).await?; + + Ok(()) + }, + other_message => { + tracing::warn!(message_type = ?other_message, "Unexpected message type received during Connected state."); + Err(WsError::UnexpectedMessageType) + }, + } +} diff --git a/src/web/ws/gateway/error.rs b/src/web/ws/gateway/error.rs new file mode 100644 index 0000000..03ebc5d --- /dev/null +++ b/src/web/ws/gateway/error.rs @@ -0,0 +1,46 @@ +pub type Result = std::result::Result; + +#[derive(Debug, derive_more::From, derive_more::Display)] +pub enum Error { + #[from] + Axum(axum::Error), + + #[from] + Json(serde_json::Error), + + #[from] + AcknowledgementError(tokio::sync::oneshot::error::RecvError), + + UnexpectedMessageType, + + WrongMessageType, + + WebSocketClosed, + + HeartbeatTimeout, + AuthenticationFailed, +} + +#[derive(Debug, Clone, serde::Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ClientError { + DeserializationError, + NotAuthenticated, + AlreadyAuthenticated, + HeartbeatTimeout, + + Unknown, +} + +impl Error { + pub fn into_client_error(&self) -> ClientError { + match self { + Error::HeartbeatTimeout => ClientError::HeartbeatTimeout, + Error::Json(_) => ClientError::DeserializationError, + Error::UnexpectedMessageType => ClientError::Unknown, + Error::WrongMessageType => ClientError::Unknown, + Error::WebSocketClosed => ClientError::Unknown, + _ => ClientError::Unknown, + } + } +} diff --git a/src/web/ws/gateway/message.rs b/src/web/ws/gateway/message.rs new file mode 100644 index 0000000..688d3f5 --- /dev/null +++ b/src/web/ws/gateway/message.rs @@ -0,0 +1,15 @@ +use crate::entity; + +#[derive(Debug, Clone, serde::Serialize)] +#[serde(tag = "type", content = "data")] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum Event { + AddServer { server: entity::server::Server }, + RemoveServer { server_id: entity::server::Id }, + + AddDmChannel { channel: entity::channel::Channel }, + RemoveDmChannel { channel_id: entity::channel::Id }, + + AddServerChannel { channel: entity::channel::Channel }, + RemoveServerChannel { channel_id: entity::channel::Id }, +} diff --git a/src/web/ws/gateway/mod.rs b/src/web/ws/gateway/mod.rs new file mode 100644 index 0000000..116cdad --- /dev/null +++ b/src/web/ws/gateway/mod.rs @@ -0,0 +1,29 @@ +use axum::extract::{State, WebSocketUpgrade}; +use axum::response::IntoResponse; +use base64::Engine; +use dashmap::DashMap; +use futures::{SinkExt, StreamExt}; +use sha2::Digest; + +use crate::state::AppState; +use crate::web::ws::gateway::connection::handle_socket_connection; +use crate::web::ws::gateway::state::EventSender; + +mod connection; +mod error; +pub mod message; +mod protocol; +mod state; +pub mod util; + +#[derive(Debug, Default)] +pub struct EventWsState { + pub connection_instance: DashMap, +} + +pub async fn ws_handler( + State(app_state): State, + ws: WebSocketUpgrade, +) -> crate::web::error::Result { + Ok(ws.on_upgrade(|socket| handle_socket_connection(socket, app_state))) +} diff --git a/src/web/ws/gateway/protocol.rs b/src/web/ws/gateway/protocol.rs new file mode 100644 index 0000000..95f857a --- /dev/null +++ b/src/web/ws/gateway/protocol.rs @@ -0,0 +1,99 @@ +// src/web/ws/protocol.rs + +use axum::extract::ws::Message as AxumMessage; +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +use super::error::{self, ClientError, Error as WsError}; +use super::message as ws_local_message; // For ws::message::Event +use crate::entity; +use crate::util as crate_root_util; // For crate::util::serialize_duration_seconds + +#[derive(Debug, Serialize)] +#[serde(tag = "type", content = "data")] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum WsServerMessage { + HeartbeatInterval { + #[serde(serialize_with = "crate_root_util::serialize_duration_seconds")] + interval: Duration, + }, + AuthenticateDenied, + #[serde(rename_all = "camelCase")] + AuthenticateAccepted { + user_id: entity::user::Id, + session_key: String, + }, + #[serde(rename_all = "camelCase")] + Event { + event: ws_local_message::Event, // Assumes Event is defined in ws::message + }, + Pong, + Error { + code: ClientError, + }, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type", content = "data")] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum WsClientMessage { + #[serde(rename_all = "camelCase")] + Authenticate { + token: String, + }, + Ping, +} + +/// Deserializes an Axum WebSocket message into a `WsClientMessage`. +pub fn deserialize_ws_message(message: AxumMessage) -> error::Result { + match message { + AxumMessage::Text(text) => serde_json::from_str(&text).map_err(WsError::from), + AxumMessage::Close(_) => Err(WsError::WebSocketClosed), + _ => Err(WsError::WrongMessageType), // e.g. Binary, Ping, Pong from axum::Message + } +} + +/// Serializes a `WsServerMessage` into an Axum WebSocket message. +pub fn serialize_ws_message(message: WsServerMessage) -> error::Result { + serde_json::to_string(&message) + .map(Into::into) + .map(AxumMessage::Text) + .map_err(WsError::from) +} + +/// Wrapper for messages sent over an internal MPSC channel to the WebSocket writer task. +/// Includes an optional one-shot channel for acknowledgements or error reporting back from the writer. +pub struct SendWsMessage { + pub message: WsServerMessage, + pub response_ch: Option>>, +} + +impl SendWsMessage { + /// Sends a message over the MPSC channel and awaits a response via a oneshot channel. + pub async fn send_with_response( + tx: &tokio::sync::mpsc::UnboundedSender, // Changed to reference + message: WsServerMessage, + ) -> error::Result<()> { + let (response_tx, response_rx) = tokio::sync::oneshot::channel(); + let send_message = SendWsMessage { + message, + response_ch: Some(response_tx), + }; + + if tx.send(send_message).is_err() { + Err(WsError::WebSocketClosed) // MPSC channel closed, writer task likely dead + } else { + // Wait for the writer task to acknowledge the send attempt. + // This will return Ok(Ok(())) on success, Ok(Err(e)) on write error, or Err on channel error. + response_rx.await? // Propagates RecvError into WsError::AcknowledgementError + } + } + + /// Creates a new message for fire-and-forget sending (no response/acknowledgement expected). + pub fn new_no_response(message: WsServerMessage) -> Self { + SendWsMessage { + message, + response_ch: None, + } + } +} \ No newline at end of file diff --git a/src/web/ws/gateway/state.rs b/src/web/ws/gateway/state.rs new file mode 100644 index 0000000..60e5b99 --- /dev/null +++ b/src/web/ws/gateway/state.rs @@ -0,0 +1,49 @@ +// src/web/ws/state.rs + +use std::time::{Duration}; + +use tokio::sync::mpsc; + +use super::message; +use crate::entity; // For entity::user::Id // For ws::message::Event used in EventSender/Receiver + +/// Represents the current state of a single WebSocket connection. +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub enum WsState { + Initialize, // Connection established, awaiting authentication + Connected, // Authenticated and operational +} + +/// Contextual information for an authenticated WebSocket user session. +#[derive(Debug, Clone)] // Clone might be useful +pub struct WsUserContext { + pub user_id: entity::user::Id, + pub session_key: String, // Unique key for this specific WebSocket session instance +} + +/// Sender part of an MPSC channel used to send `ws::message::Event`s to a connected client. +pub type EventSender = mpsc::UnboundedSender; +/// Receiver part of an MPSC channel used by a connection task to receive `ws::message::Event`s. +pub type EventReceiver = mpsc::UnboundedReceiver; + +/// Holds the full context for a single WebSocket connection's lifecycle. +/// This struct is managed per-connection. +pub struct WsContext { + pub connection_state: WsState, + pub user_context: Option, + pub heartbeat_interval: Duration, + pub next_ping_deadline: tokio::time::Instant, + /// Channel for receiving application-specific events to be sent to this client. + /// The `EventSender` (tx) part is given to `AppState` for broadcasting. + /// The `EventReceiver` (rx) part is polled by the connection task. + pub event_channel: Option<(EventSender, EventReceiver)>, +} + +impl WsContext { + /// Resets the ping deadline based on the current time and heartbeat interval. + /// This should be called after successfully receiving a ping from the client + /// or after sending a message that implies activity (like Pong or initial auth). + pub fn reset_deadline(&mut self) { + self.next_ping_deadline = tokio::time::Instant::now() + self.heartbeat_interval; + } +} diff --git a/src/web/ws/gateway/util.rs b/src/web/ws/gateway/util.rs new file mode 100644 index 0000000..4a23798 --- /dev/null +++ b/src/web/ws/gateway/util.rs @@ -0,0 +1,14 @@ +use crate::entity; +use crate::state::AppState; +use crate::web::ws::gateway::message; + +pub async fn send_message(state: &AppState, user_id: entity::user::Id, message: message::Event) { + let connected_users = state.event_connected_users.read().await; + if let Some(state) = connected_users.get(&user_id) { + for instance in state.connection_instance.iter() { + if let Err(e) = instance.send(message.clone()) { + tracing::error!("failed to send message: {}", e); + } + } + } +} diff --git a/src/web/ws/mod.rs b/src/web/ws/mod.rs new file mode 100644 index 0000000..4f27526 --- /dev/null +++ b/src/web/ws/mod.rs @@ -0,0 +1 @@ +pub mod gateway; diff --git a/src/webrtc/mod.rs b/src/webrtc/mod.rs new file mode 100644 index 0000000..50fbcfb --- /dev/null +++ b/src/webrtc/mod.rs @@ -0,0 +1,288 @@ +use std::sync::Arc; + +use dashmap::DashMap; +use tracing::Instrument; +use webrtc::api::interceptor_registry::register_default_interceptors; +use webrtc::api::media_engine::MIME_TYPE_OPUS; +use webrtc::api::{API, APIBuilder}; +use webrtc::interceptor::registry::Registry; +use webrtc::peer_connection::configuration::RTCConfiguration; +use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState; +use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; +use webrtc::rtp_transceiver::rtp_codec::{RTCRtpCodecCapability, RTPCodecType}; +use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; +use webrtc::track::track_local::{TrackLocal, TrackLocalWriter}; +use webrtc::track::track_remote::TrackRemote; + +use crate::entity; + +type PeerId = entity::user::Id; +type RoomId = entity::channel::Id; + +struct PeerState { + peer_id: PeerId, + peer_connection: Arc, + outgoing_audio_track: Arc, +} + +struct RoomState { + room_id: RoomId, + peers: DashMap, + close_signal: tokio::sync::mpsc::UnboundedSender<()>, +} + +#[derive(Debug)] +pub struct Offer { + pub peer_id: PeerId, + pub sdp_offer: RTCSessionDescription, +} + +#[derive(Debug)] +pub struct AnswerSignal { + pub sdp_answer: RTCSessionDescription, +} + +#[derive(Debug)] +pub struct OfferSignal { + pub offer: Offer, + pub response: tokio::sync::oneshot::Sender, +} + +#[tracing::instrument(skip(signal))] +pub async fn webrtc_task( + room_id: RoomId, + signal: tokio::sync::mpsc::UnboundedReceiver, +) -> anyhow::Result<()> { + tracing::info!("Starting WebRTC task"); + + let (close_signal, mut close_receiver) = tokio::sync::mpsc::unbounded_channel(); + + let state = Arc::new(RoomState { + room_id, + peers: DashMap::new(), + close_signal, + }); + + let mut signal = signal; + let mut media_engine = webrtc::api::media_engine::MediaEngine::default(); + media_engine.register_default_codecs()?; + let mut registry = Registry::new(); + registry = register_default_interceptors(registry, &mut media_engine)?; + + let api = APIBuilder::new() + .with_media_engine(media_engine) + .with_interceptor_registry(registry) + .build(); + + let api = Arc::new(api); + + loop { + tokio::select! { + Some(signal) = signal.recv() => { + let room_state = state.clone(); + let api = api.clone(); + + tokio::spawn(async move { + if let Err(e) = handle_peer(api, room_state, signal).await { + tracing::error!("error handling peer: {}", e); + } + }.instrument(tracing::Span::current())); + } + _ = close_receiver.recv() => { + tracing::debug!("WebRTC task stopped"); + break; + } + } + } + + Ok(()) +} + +#[tracing::instrument(skip(api, room_state, offer_signal), fields(peer_id = %offer_signal.offer.peer_id))] +async fn handle_peer( + api: Arc, + room_state: Arc, + offer_signal: OfferSignal, +) -> anyhow::Result<()> { + tracing::debug!("handling peer"); + let config = RTCConfiguration { + ..Default::default() + }; + + let outgoing_track = Arc::new(TrackLocalStaticRTP::new( + RTCRtpCodecCapability { + mime_type: MIME_TYPE_OPUS.to_string(), + ..Default::default() + }, + format!("audio-{}", offer_signal.offer.peer_id), + format!("room-{}", room_state.room_id), + )); + + let peer_connection = Arc::new(api.new_peer_connection(config).await?); + + let rtp_sender = peer_connection + .add_track(Arc::clone(&outgoing_track) as Arc) + .await?; + + tracing::debug!("added track to peer connection: {:?}", rtp_sender); + + // Read RTCP packets for the outgoing track (important for feedback like NACKs, PLI) + let outgoing_track_ = Arc::clone(&outgoing_track); + tokio::spawn( + async move { + let mut rtcp_buf = vec![0u8; 1500]; + while let Ok((_, _)) = rtp_sender.read(&mut rtcp_buf).await { + // Process RTCP if needed (e.g., bandwidth estimation, custom feedback) + } + tracing::debug!( + "RTCP reader loop for outgoing track {} ended", + outgoing_track_.id() + ); + } + .instrument(tracing::Span::current()), + ); + + let room_state_ = room_state.clone(); + peer_connection.on_peer_connection_state_change(Box::new(move |state| { + let room_state_ = room_state_.clone(); + Box::pin(async move { + tracing::debug!("peer connection state changed: {:?}", state); + if state == RTCPeerConnectionState::Disconnected + || state == RTCPeerConnectionState::Failed + { + tracing::debug!("peer connection closed"); + cleanup_peer(room_state_, offer_signal.offer.peer_id).await; + } + }) + })); + + let room_state_ = room_state.clone(); + peer_connection.on_track(Box::new(move |track, _receiver, _transceiver| { + tracing::debug!("track received: {:?}", track); + + let room_state_ = room_state_.clone(); + Box::pin(async move { + if track.kind() == RTPCodecType::Audio { + tracing::debug!("audio track received: {:?}", track); + tokio::spawn(async move { + if let Err(e) = + forward_audio_track(room_state_, offer_signal.offer.peer_id, track).await + { + tracing::error!("error forwarding audio track: {}", e); + } + }); + } else { + tracing::warn!("received non-audio track: {:?}", track); + } + }) + })); + + peer_connection + .set_remote_description(offer_signal.offer.sdp_offer) + .await?; + let answer = peer_connection.create_answer(None).await?; + + let mut gathering_complete = peer_connection.gathering_complete_promise().await; + + peer_connection.set_local_description(answer).await?; + + gathering_complete.recv().await; + + tracing::debug!("ICE gathering complete"); + + let local_description = peer_connection + .local_description() + .await + .ok_or_else(|| anyhow::anyhow!("failed to get local description after setting it"))?; + + let peer_state = PeerState { + peer_id: offer_signal.offer.peer_id, + peer_connection: Arc::clone(&peer_connection), + outgoing_audio_track: Arc::clone(&outgoing_track), + }; + + { + if let Some(old_peer) = room_state + .peers + .insert(offer_signal.offer.peer_id, peer_state) + { + let _ = old_peer.peer_connection.close().await; + } + } + + let _ = offer_signal.response.send(AnswerSignal { + sdp_answer: local_description, + }); + + Ok(()) +} + +#[tracing::instrument(skip(room_state, track), fields(room_id = %room_state.room_id, peer_id = %peer_id))] +async fn forward_audio_track( + room_state: Arc, + peer_id: PeerId, + track: Arc, +) -> anyhow::Result<()> { + let mut rtp_buf = vec![0u8; 1500]; + while let Ok((rtp_packet, _attr)) = track.read(&mut rtp_buf).await { + let other_peer_tracks = room_state + .peers + .iter() + .filter_map(|pair| { + let peer_state = pair.value(); + if peer_state.peer_id != peer_id { + Some(peer_state.outgoing_audio_track.clone()) + } else { + None + } + }) + .collect::>(); + + if other_peer_tracks.is_empty() { + // tracing::warn!("no other peers to forward audio track to"); + continue; + } + + let write_futures = other_peer_tracks + .iter() + .map(|outgoing_track| outgoing_track.write_rtp(&rtp_packet)); + + let results = futures::future::join_all(write_futures).await; + for result in results { + if let Err(e) = result { + tracing::error!("error writing RTP packet: {}", e); + } + } + } + tracing::debug!( + "RTP Read loop ended for track {} from peer {}", + track.id(), + peer_id + ); + + Ok(()) +} + +#[tracing::instrument(skip(room_state), fields(room_id = %room_state.room_id))] +async fn cleanup_peer(room_state: Arc, peer_id: PeerId) { + tracing::debug!("cleaning up peer"); + + if let Some((_, peer_state)) = room_state.peers.remove(&peer_id) { + tracing::debug!("removed peer"); + let pc = Arc::clone(&peer_state.peer_connection); + tokio::spawn(async move { + if let Err(e) = pc.close().await { + if !matches!(e, webrtc::Error::ErrConnectionClosed) { + tracing::warn!("error closing peer connection: {}", e); + } + } + + tracing::debug!("peer connection closed"); + }); + } + + if room_state.peers.is_empty() { + tracing::debug!("no more peers in room, closing room"); + let _ = room_state.close_signal.send(()); + } +}