diff --git a/Cargo.lock b/Cargo.lock index 43ea549..07b2d37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -282,9 +282,9 @@ dependencies = [ [[package]] name = "async-tungstenite" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6cd055774e8b7f2ff9e5f9646efb298f180c3b886cdf20ef27f5a0bfbe2677b" +checksum = "1c348fb0b6d132c596eca3dcd941df48fb597aafcb07a738ec41c004b087dc99" dependencies = [ "atomic-waker", "futures-core", @@ -516,6 +516,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +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 = "crc32fast" version = "1.4.2" @@ -722,6 +737,19 @@ dependencies = [ "regex", ] +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "env_logger" version = "0.11.5" @@ -741,6 +769,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "erased-serde" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" +dependencies = [ + "serde", + "typeid", +] + [[package]] name = "errno" version = "0.3.10" @@ -806,6 +844,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -821,6 +874,21 @@ 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" @@ -828,6 +896,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -895,6 +964,7 @@ 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", @@ -906,6 +976,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -968,7 +1047,7 @@ dependencies = [ "chrono", "dashmap", "dotenvy", - "env_logger", + "env_logger 0.11.5", "flate2", "futures-util", "jsonwebtoken", @@ -977,10 +1056,14 @@ dependencies = [ "mongodb", "num_cpus", "once_cell", + "pulse-api", "rand", + "rapid", "redis", "rmp-serde", + "rmpv", "serde", + "typetag", "ulid", "x25519-dalek", ] @@ -1314,6 +1397,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inventory" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767" + [[package]] name = "ipconfig" version = "0.3.2" @@ -1332,6 +1421,17 @@ version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +[[package]] +name = "is-terminal" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +dependencies = [ + "hermit-abi 0.4.0", + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1451,6 +1551,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +[[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 = "md-5" version = "0.10.6" @@ -1546,6 +1655,16 @@ dependencies = [ "syn 2.0.90", ] +[[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" @@ -1611,6 +1730,60 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "openssl-src" +version = "300.4.1+3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking" version = "2.2.1" @@ -1694,6 +1867,12 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "polling" version = "3.7.4" @@ -1736,6 +1915,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pretty_env_logger" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" +dependencies = [ + "env_logger 0.10.2", + "log", +] + [[package]] name = "proc-macro2" version = "1.0.92" @@ -1745,6 +1934,38 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulse" +version = "0.1.0" +dependencies = [ + "async-std", + "async-tungstenite", + "dashmap", + "dotenvy", + "futures", + "lazy_static", + "log", + "once_cell", + "pretty_env_logger", + "pulse-api", + "rand", + "redis", + "rmp-serde", + "serde", + "str0m", + "tracing-subscriber", + "ulid", +] + +[[package]] +name = "pulse-api" +version = "0.1.0" +dependencies = [ + "redis", + "rmp-serde", + "serde", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -1796,6 +2017,29 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rapid" +version = "0.1.0" +dependencies = [ + "async-std", + "async-trait", + "async-tungstenite", + "chrono", + "dashmap", + "env_logger 0.11.5", + "futures-util", + "lazy_static", + "log", + "once_cell", + "rand", + "rmp-serde", + "rmpv", + "serde", + "typetag", + "ulid", + "x25519-dalek", +] + [[package]] name = "redis" version = "0.27.6" @@ -1838,8 +2082,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "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]] @@ -1850,9 +2103,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "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" @@ -1906,6 +2165,18 @@ dependencies = [ "serde", ] +[[package]] +name = "rmpv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58450723cd9ee93273ce44a20b6ec4efe17f8ed2e3631474387bfdecf18bb2a9" +dependencies = [ + "num-traits", + "rmp", + "serde", + "serde_bytes", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1997,6 +2268,21 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sctp-proto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4dea4fe3384a24652f065296ac333c810dfd0c5b39b98a2214762c16aaadc3c" +dependencies = [ + "bytes", + "crc", + "fxhash", + "log", + "rand", + "slab", + "thiserror", +] + [[package]] name = "semver" version = "1.0.23" @@ -2095,6 +2381,16 @@ dependencies = [ "cfg-if", "cpufeatures", "digest", + "sha1-asm", +] + +[[package]] +name = "sha1-asm" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "286acebaf8b67c1130aedffad26f594eff0c1292389158135327d2e23aed582b" +dependencies = [ + "cc", ] [[package]] @@ -2114,6 +2410,15 @@ dependencies = [ "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" @@ -2178,6 +2483,27 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "str0m" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6110f29fbdda0516ae4ca52b62d802fbb6f874275db717a340ebb0ddd86e51c" +dependencies = [ + "combine", + "crc", + "fastrand", + "hmac", + "libc", + "once_cell", + "openssl", + "openssl-sys", + "sctp-proto", + "serde", + "sha1", + "thiserror", + "tracing", +] + [[package]] name = "stringprep" version = "0.1.5" @@ -2246,6 +2572,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2266,6 +2601,16 @@ dependencies = [ "syn 2.0.90", ] +[[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.37" @@ -2403,6 +2748,36 @@ 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 = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -2434,12 +2809,42 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "typeid" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "typetag" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ba3b6e86ffe0054b2c44f2d86407388b933b16cb0a70eea3929420db1d9bbe" +dependencies = [ + "erased-serde", + "inventory", + "once_cell", + "serde", + "typetag-impl", +] + +[[package]] +name = "typetag-impl" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70b20a22c42c8f1cd23ce5e34f165d4d37038f5b663ad20fb6adbdf029172483" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "ulid" version = "1.1.3" @@ -2539,12 +2944,24 @@ dependencies = [ "serde", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "value-bag" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" +[[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" @@ -2672,6 +3089,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index bb10461..6f685a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,37 +1,3 @@ -[package] -name = "harmony" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -async-std = { version = "1.12.0", features = ["attributes", "tokio1"] } -async-trait = "0.1.73" -futures-util = "0.3.28" - -dashmap = "6.0.0" -lazy_static = "1.4.0" -once_cell = "1.18.0" - -chrono = "0.4.26" -rand = "0.8.5" -num_cpus = "1.16.0" -ulid = "1.0.0" - -dotenvy = "0.15.7" -env_logger = "0.11.0" -log = "0.4.20" - -async-tungstenite = "0.28.0" - -mongodb = "3.0.0" -jsonwebtoken = "9.0.0" -redis = { version = "0.27.0", features = ["async-std-comp"] } - -serde = { version = "1.0.183", features = ["derive"] } -rmp-serde = "1.1.2" - -aes-gcm = "0.10.2" -flate2 = "1.0.27" -x25519-dalek = "2.0.0" +[workspace] +members = ["crates/*"] +resolver = "2" \ No newline at end of file diff --git a/README.md b/README.md index 5acd009..bde6960 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # Harmony ## Description -Harmony aims to provide secure, robust, and open source encrypted communication with high call quality. It is designed for individuals, communities, as well as enterprises. Similar to Discord and Slack, it is designed with a space and channel structure. In addition to providing a secure communication platform, developers may build upon the platform much easier than other platforms. +Harmony aims to provide secure, robust, and open source encrypted communication with high call quality. It is designed for individuals, communities, as well as enterprises with a space and channel structure. In addition to providing a secure communication platform, developers may build upon the platform much easier than other platforms. -This repository includes the core server software. It's free to self-host or use any hosted instance. Enterprise customers will have the option to purchase support services and hosted instances. Authentication is meant to be used with the [Nextflow SSO authentication service](https://github.com/Nextflow-Cloud/sso-system). OAuth2 will be supported in the future. +This repository includes the core server software. It's free to self-host or use any hosted instance. Enterprise customers will have the option to purchase support services and hosted instances. Authentication is meant to be used with the [Nextania account services](https://github.com/nextania/account). OAuth2 will be supported in the future. The Harmony client currently only exists for browsers. Other clients will be developed in the future. Note: Harmony is not a federated service for the sake of simplicity. It is a centralized service that can be self-hosted. ## License -This project is licensed under the [GNU Affero General Public License v3.0](https://github.com/Nextflow-Cloud/harmony/blob/main/LICENSE). +This project is licensed under the [GNU Affero General Public License v3.0](https://github.com/nextania/harmony/blob/main/LICENSE). diff --git a/crates/harmony/Cargo.lock b/crates/harmony/Cargo.lock new file mode 100644 index 0000000..43ea549 --- /dev/null +++ b/crates/harmony/Cargo.lock @@ -0,0 +1,2987 @@ +# 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.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[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 = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.3.1", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", + "tokio", +] + +[[package]] +name = "async-io" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-std" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615" +dependencies = [ + "async-attributes", + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "async-tungstenite" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6cd055774e8b7f2ff9e5f9646efb298f180c3b886cdf20ef27f5a0bfbe2677b" +dependencies = [ + "atomic-waker", + "futures-core", + "futures-io", + "futures-task", + "futures-util", + "log", + "pin-project-lite", + "tungstenite", +] + +[[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 = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[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 = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel 2.3.1", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bson" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "068208f2b6fcfa27a7f1ee37488d2bb8ba2640f68f5475d08e1d9130696aba59" +dependencies = [ + "ahash", + "base64 0.13.1", + "bitvec", + "hex", + "indexmap 2.7.0", + "js-sys", + "once_cell", + "rand", + "serde", + "serde_bytes", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + +[[package]] +name = "cc" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[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 = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[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 = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[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.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[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", + "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.90", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.90", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.90", +] + +[[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.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.90", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.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-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[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.90", +] + +[[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-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", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[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 = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "harmony" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "async-std", + "async-trait", + "async-tungstenite", + "chrono", + "dashmap", + "dotenvy", + "env_logger", + "flate2", + "futures-util", + "jsonwebtoken", + "lazy_static", + "log", + "mongodb", + "num_cpus", + "once_cell", + "rand", + "redis", + "rmp-serde", + "serde", + "ulid", + "x25519-dalek", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hickory-proto" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447afdcdb8afb9d0a852af6dc65d9b285ce720ed7a59e42a8bf2e931c67bc1b5" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2e2aba9c389ce5267d31cf1e4dace82390ae276b0b364ea55630b1fa1b44b4" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "lru-cache", + "once_cell", + "parking_lot", + "rand", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "http" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "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 = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", + "serde", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "js-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.168" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[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.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +dependencies = [ + "value-bag", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[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 = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "mongodb" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c857d71f918b38221baf2fdff7207fec9984b4504901544772b1edf0302d669f" +dependencies = [ + "async-trait", + "base64 0.13.1", + "bitflags 1.3.2", + "bson", + "chrono", + "derivative", + "derive_more", + "futures-core", + "futures-executor", + "futures-io", + "futures-util", + "hex", + "hickory-proto", + "hickory-resolver", + "hmac", + "md-5", + "mongodb-internal-macros", + "once_cell", + "pbkdf2", + "percent-encoding", + "rand", + "rustc_version_runtime", + "rustls", + "rustls-pemfile", + "serde", + "serde_bytes", + "serde_with", + "sha-1", + "sha2", + "socket2", + "stringprep", + "strsim", + "take_mut", + "thiserror", + "tokio", + "tokio-rustls", + "tokio-util", + "typed-builder", + "uuid", + "webpki-roots", +] + +[[package]] +name = "mongodb-internal-macros" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6dbc533e93429a71c44a14c04547ac783b56d3f22e6c4f12b1b994cf93844e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[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-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-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[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 = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", +] + +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "polling" +version = "3.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.59.0", +] + +[[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 = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[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", + "rand_core", +] + +[[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", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redis" +version = "0.27.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d8f99a4090c89cc489a94833c901ead69bfbf3877b4867d5482e321ee875bc" +dependencies = [ + "arc-swap", + "async-std", + "async-trait", + "bytes", + "combine", + "futures-util", + "itertools", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.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", + "regex-syntax", +] + +[[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", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[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 = "rustc_version_runtime" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dd18cd2bae1820af0b6ad5e54f4a51d0f3fcc53b05f845675074efcc7af071d" +dependencies = [ + "rustc_version", + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "serde_json" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +dependencies = [ + "indexmap 2.7.0", + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.7.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[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 = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "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.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +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" + +[[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 = "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.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[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.90", +] + +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +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.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "utf-8", +] + +[[package]] +name = "typed-builder" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89851716b67b937e393b3daa8423e67ddfc4bbbf1654bcf05488e95e0828db0c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ulid" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f903f293d11f31c0c29e4148f6dc0d033a7f80cebc0282bea147611667d289" +dependencies = [ + "getrandom", + "rand", + "web-time", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[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 = "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", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "value-bag" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.90", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + +[[package]] +name = "web-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + +[[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.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[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 = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[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", + "serde", + "zeroize", +] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "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.90", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] diff --git a/crates/harmony/Cargo.toml b/crates/harmony/Cargo.toml new file mode 100644 index 0000000..92f4d0c --- /dev/null +++ b/crates/harmony/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "harmony" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-std = { version = "1.12.0", features = ["attributes", "tokio1"] } +async-trait = "0.1.73" +futures-util = "0.3.28" + +dashmap = "6.0.0" +lazy_static = "1.4.0" +once_cell = "1.18.0" + +chrono = "0.4.26" +rand = "0.8.5" +num_cpus = "1.16.0" +ulid = "1.0.0" + +dotenvy = "0.15.7" +env_logger = "0.11.0" +log = "0.4.20" + +async-tungstenite = "0.28.0" + +mongodb = "3.0.0" +jsonwebtoken = "9.0.0" +redis = { version = "0.27.0", features = ["async-std-comp"] } + +serde = { version = "1.0.183", features = ["derive"] } +rmp-serde = "1.1.2" + +aes-gcm = "0.10.2" +flate2 = "1.0.27" +x25519-dalek = "2.0.0" + +rapid = { path = "../rapid" } +typetag = "0.2.18" +rmpv = "1.3.0" +pulse-api = { path = "../pulse-api" } \ No newline at end of file diff --git a/crates/harmony/src/authentication.rs b/crates/harmony/src/authentication.rs new file mode 100644 index 0000000..2d0f7d1 --- /dev/null +++ b/crates/harmony/src/authentication.rs @@ -0,0 +1,66 @@ +use std::{any::Any, collections::HashSet, sync::Arc}; + +use dashmap::DashMap; +use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; +use rapid::socket::{RpcClient, RpcResponder}; +use rmpv::{ext::to_value, Value}; +use serde::Deserialize; + +use crate::{errors::{Error, Result}, services::{database::users::User, environment::JWT_SECRET}}; + +#[derive(Deserialize)] +struct UserJwt { + // TODO: Find the other properties + id: String, + issued_at: u128, + expires_at: u128, +} + + +// Important: This only accepts a token and will not sign a token. +// The token is to be obtained from a separate login server +// (e.g. AS) +// TODO: fetch real valid token information from AS +pub async fn authenticate(token: String) -> rapid::errors::Result> { + // println!("Public key: {:?}", self.public_key); + println!("Token: {:?}", token); + let mut validation = Validation::new(Algorithm::HS256); + validation.required_spec_claims = HashSet::new(); + validation.validate_exp = false; + let token_message = decode::( + &token, + &DecodingKey::from_secret(JWT_SECRET.as_ref()), + &validation, + ) + .map_err(|_| rapid::errors::Error::InvalidToken)?; + let time = chrono::Utc::now().timestamp_millis() as u128; + if time > token_message.claims.expires_at { + return Err(rapid::errors::Error::InvalidToken); + } + let user = User::get(&token_message.claims.id).await; + let user = if let Err(Error::NotFound) = user { + User::create(token_message.claims.id).await.map_err(|_| rapid::errors::Error::InternalError)? + } else { + user.map_err(|_| rapid::errors::Error::InternalError)? + }; + Ok(Box::new(user)) +} + + +pub fn check_authenticated( + clients: Arc>, + id: &str, +) -> Result> { + let client = clients.get(id).expect("Failed to get client"); + if let Some(x) = client.get_user::() { + Ok(x.clone().into()) + } else { + Err(Error::NotAuthenticated) + } +} + +impl RpcResponder for Error { + fn into_value(&self) -> Value { + to_value(self).unwrap() + } +} diff --git a/src/errors.rs b/crates/harmony/src/errors.rs similarity index 100% rename from src/errors.rs rename to crates/harmony/src/errors.rs diff --git a/src/main.rs b/crates/harmony/src/main.rs similarity index 62% rename from src/main.rs rename to crates/harmony/src/main.rs index 5f4826b..ce47fea 100644 --- a/src/main.rs +++ b/crates/harmony/src/main.rs @@ -1,15 +1,18 @@ #![allow(dead_code)] pub mod errors; -pub mod globals; pub mod methods; pub mod services; +pub mod authentication; +use authentication::authenticate; +use rapid::socket::RpcServer; use services::database; -use services::socket; +use services::redis; // use services::webrtc; use log::info; +use services::webrtc; use crate::services::environment::LISTEN_ADDRESS; @@ -25,10 +28,12 @@ async fn main() { // run DB migrations as necessary - // webrtc::create_workers().await; - // println!("SFU workers have spawned"); + redis::connect().await; + info!("Connected to Redis"); + webrtc::spawn_check_available_nodes(); let listen_address = LISTEN_ADDRESS.to_owned(); info!("Starting server at {listen_address}"); - socket::start_server().await; + let server = RpcServer::new(Box::new(|token| Box::pin(authenticate(token)))); + server.start(listen_address).await; } diff --git a/src/methods/channels.rs b/crates/harmony/src/methods/channels.rs similarity index 55% rename from src/methods/channels.rs rename to crates/harmony/src/methods/channels.rs index 0daa5dd..af559ed 100644 --- a/src/methods/channels.rs +++ b/crates/harmony/src/methods/channels.rs @@ -1,16 +1,13 @@ use std::sync::Arc; -use async_trait::async_trait; use dashmap::DashMap; +use rapid::socket::{RpcClient, RpcResponder, RpcValue}; use serde::{Deserialize, Serialize}; use crate::{ - errors::{Error, Result}, - services::{database::channels::Channel, socket::RpcClient}, + authentication::check_authenticated, errors::{Error, Result}, services::database::channels::Channel }; -use super::{authentication::check_authenticated, Respond, Response}; - #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct GetChannelMethod { @@ -20,41 +17,38 @@ pub struct GetChannelMethod { space_id: Option, } -#[async_trait] -impl Respond for GetChannelMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - let user = check_authenticated(clients, &id)?; - let channel = Channel::get(&self.id).await?; - match channel { - Channel::PrivateChannel { .. } | Channel::GroupChannel { .. } => { - if self.space_id.is_some() { +pub async fn get_channel( + clients: Arc>, + id: String, + data: GetChannelMethod, +) -> impl RpcResponder { + let user = check_authenticated(clients, &id)?; + let channel = Channel::get(&data.id).await?; + match channel { + Channel::PrivateChannel { .. } | Channel::GroupChannel { .. } => { + if data.space_id.is_some() { + return Err(Error::NotFound); + } + let in_channel = user.in_channel(&channel).await?; + if !in_channel { + return Err(Error::NotFound); + } + Ok(RpcValue(GetChannelResponse { channel })) + } + Channel::InformationChannel { ref space_id, .. } + | Channel::AnnouncementChannel { ref space_id, .. } + | Channel::ChatChannel { ref space_id, .. } => { + if let Some(request_space_id) = &data.space_id { + if request_space_id != space_id { return Err(Error::NotFound); } - let in_channel = user.in_channel(&channel).await?; - if !in_channel { + let user_in_space = user.in_space(space_id).await?; + if !user_in_space { return Err(Error::NotFound); } - Ok(Response::GetChannel(GetChannelResponse { channel })) - } - Channel::InformationChannel { ref space_id, .. } - | Channel::AnnouncementChannel { ref space_id, .. } - | Channel::ChatChannel { ref space_id, .. } => { - if let Some(request_space_id) = &self.space_id { - if request_space_id != space_id { - return Err(Error::NotFound); - } - let user_in_space = user.in_space(space_id).await?; - if !user_in_space { - return Err(Error::NotFound); - } - Ok(Response::GetChannel(GetChannelResponse { channel })) - } else { - Err(Error::NotFound) - } + Ok(RpcValue(GetChannelResponse { channel })) + } else { + Err(Error::NotFound) } } } @@ -73,17 +67,14 @@ pub struct GetChannelsMethod { scope_id: Option, } -#[async_trait] -impl Respond for GetChannelsMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - let user = check_authenticated(clients, &id)?; - let channels = user.get_channels().await?; - Ok(Response::GetChannels(GetChannelsResponse { channels })) - } +async fn get_channels( + clients: Arc>, + id: String, + _: GetChannelsMethod, +) -> impl RpcResponder { + let user = check_authenticated(clients, &id)?; + let channels = user.get_channels().await?; + Ok::<_, Error>(RpcValue(GetChannelsResponse { channels })) } #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/src/methods/events.rs b/crates/harmony/src/methods/events.rs similarity index 100% rename from src/methods/events.rs rename to crates/harmony/src/methods/events.rs diff --git a/crates/harmony/src/methods/invites.rs b/crates/harmony/src/methods/invites.rs new file mode 100644 index 0000000..3d42937 --- /dev/null +++ b/crates/harmony/src/methods/invites.rs @@ -0,0 +1,227 @@ +use std::sync::Arc; + +use dashmap::DashMap; +use rapid::socket::{RpcClient, RpcResponder, RpcValue}; +use serde::{Deserialize, Serialize}; + +use crate::{ + authentication::check_authenticated, errors::{Error, Result}, services::{ + database::{ + channels::Channel, + infractions::is_banned, + invites::Invite, + members::Member, + spaces::Space, + }, + permissions::Permission, + } +}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CreateInviteMethod { + channel_id: String, + max_uses: Option, + expires_at: Option, + authorized_users: Option>, + space_id: Option, + scope_id: Option, +} + +async fn create_invite( + clients: Arc>, + id: String, + data: CreateInviteMethod, +) -> impl RpcResponder { + let user = check_authenticated(clients, &id)?; + let invite = Invite::create( + data.channel_id.clone(), + user.id.clone(), + data.expires_at, + data.max_uses, + data.authorized_users.clone(), + data.space_id.clone(), + data.scope_id.clone(), + ) + .await?; + Ok::<_, Error>(RpcValue(CreateInviteResponse { invite })) +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CreateInviteResponse { + invite: Invite, +} + +// pub struct UpdateInviteMethod { +// code: String, +// max_uses: Option, +// expires_at: Option, +// } + +// #[async_trait] +// impl Respond for UpdateInviteMethod { +// async fn respond(&self, clients: Arc>, id: String) -> Response { +// let client = clients.get(&id.clone()).unwrap(); +// let member = get_member(id).await; +// match member { +// Ok(member) => { +// let permissions = permissions_for(member).await; +// if !permissions.has_permission(Permission::ManageInvites) { +// return Response::Error(ErrorResponse { error: "You do not have permission to manage invites".to_string() }); +// } else { + +// } +// }, +// Err(error) => Response::Error(ErrorResponse { error }) +// } +// } +// } + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct DeleteInviteMethod { + id: String, + space_id: String, +} + +async fn delete_invite( + clients: Arc>, + id: String, + data: DeleteInviteMethod, +) -> impl RpcResponder { + let user = check_authenticated(clients, &id)?; + let member = Member::get(&user.id, &data.space_id).await?; + let permissions = member.get_permissions().await?; + if !permissions.has_permission(Permission::ManageInvites) { + return Err(Error::MissingPermission { + permission: Permission::ManageInvites, + }); + } else { + let invite = Invite::get(&data.id).await?; + invite.delete().await?; + Ok(RpcValue(DeleteInviteResponse {})) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DeleteInviteResponse {} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetInviteMethod { + code: String, +} + +async fn get_invite( + clients: Arc>, + id: String, + data: GetInviteMethod, +) -> impl RpcResponder { + let user = check_authenticated(clients, &id)?; + let invite = Invite::get(&data.code).await?; + if let Some(space_id) = invite.space_id { + let space = Space::get(&space_id).await?; + let banned = is_banned(user.id.clone(), space.id).await?; + Ok(RpcValue(GetInviteResponse { + invite: InviteInformation::Space { + name: space.name, + description: space.description, + inviter_id: invite.creator, + banned, + authorized: invite + .authorized_users + .unwrap_or_else(|| vec![user.id.clone()]) + .contains(&user.id), + member_count: space.members.len() as i32, + }, + })) + } else { + let channel = Channel::get(&invite.channel_id).await?; + if let Channel::GroupChannel { + name, + description, + members, + .. + } = channel + { + Ok(RpcValue(GetInviteResponse { + invite: InviteInformation::Group { + name, + description, + inviter_id: invite.creator, + authorized: invite + .authorized_users + .unwrap_or_else(|| vec![user.id.clone()]) + .contains(&user.id), + member_count: members.len() as i32, + }, + })) + } else { + Err(Error::InvalidInvite) + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")] +pub enum InviteInformation { + #[serde(rename_all = "camelCase")] + Group { + name: String, + description: String, + inviter_id: String, + authorized: bool, + member_count: i32, + }, + #[serde(rename_all = "camelCase")] + Space { + name: String, + description: String, + inviter_id: String, + banned: bool, + authorized: bool, + member_count: i32, + }, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetInviteResponse { + #[serde(flatten)] + invite: InviteInformation, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetInvitesMethod { + channel_id: String, + space_id: Option, + scope_id: Option, +} + +async fn get_invites( + clients: Arc>, + id: String, + data: GetInvitesMethod, +) -> impl RpcResponder { + let user = check_authenticated(clients, &id)?; + if let Some(space_id) = &data.space_id { + let member = Member::get(&user.id, space_id).await?; + let permissions = member.get_permissions().await?; + if !permissions.has_permission(Permission::ManageInvites) { + return Err(Error::MissingPermission { + permission: Permission::ManageInvites, + }); + } + } + let channel = Channel::get(&data.channel_id).await?; + let invites = channel.get_invites().await?; + Ok(RpcValue(GetInvitesResponse { invites })) +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetInvitesResponse { + invites: Vec, +} + +// TODO: Invite manager built-in diff --git a/crates/harmony/src/methods/messages.rs b/crates/harmony/src/methods/messages.rs new file mode 100644 index 0000000..0761083 --- /dev/null +++ b/crates/harmony/src/methods/messages.rs @@ -0,0 +1,107 @@ +use std::{sync::Arc, time::Duration}; + +use async_std::future; +use dashmap::DashMap; +use rapid::socket::{RpcClient, RpcResponder, RpcValue}; +use rmp_serde::Serializer; +use serde::{Deserialize, Serialize}; + +use crate::{ + authentication::check_authenticated, errors::{Error, Result}, services::database::{channels::Channel, messages::Message, users::User} +}; + +use super::{Event, NewMessageEvent, RpcApiEvent}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetMessagesMethod { + channel_id: String, + limit: Option, + latest: Option, + before: Option, + after: Option, +} + +async fn get_messages( + clients: Arc>, + id: String, + data: GetMessagesMethod, +) -> impl RpcResponder { + check_authenticated(clients, &id)?; + let channel = Channel::get(&data.channel_id).await?; + let messages = channel + .get_messages( + data.limit, + data.latest, + data.before.clone(), + data.after.clone(), + ) + .await?; + Ok::<_, Error>(RpcValue(GetMessagesResponse { messages })) +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetMessagesResponse { + messages: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SendMessageMethod { + channel_id: String, + content: String, +} + +async fn send_message( + clients: Arc>, + id: String, + data: SendMessageMethod, +) -> impl RpcResponder { + let user = check_authenticated(clients.clone(), &id)?; + let trimmed = data.content.trim(); + if trimmed.len() > 4096 { + return Err(Error::MessageTooLong); + } + if trimmed.is_empty() { + return Err(Error::MessageEmpty); + } + let message = + Message::create(data.channel_id.clone(), user.id.clone(), trimmed.to_owned()).await?; + for x in clients.clone().iter_mut() { + // TODO: Check if user is in channel + if let Some(u) = x.get_user::() { + println!("Sending message to {}", u.id); + let mut value_buffer = Vec::new(); + let value = RpcApiEvent { + event: Event::NewMessage(NewMessageEvent { + message: message.clone(), + channel_id: data.channel_id.clone(), + }), + }; + value + .serialize(&mut Serializer::new(&mut value_buffer).with_struct_map()) + .unwrap(); + println!("Serialized"); + let y = x.socket.clone(); + future::timeout(Duration::from_millis(5000), async move { + y.send(async_tungstenite::tungstenite::Message::Binary( + value_buffer, + )) + .await + }) + .await + .unwrap_or(Ok(())) + .unwrap_or_else(|e| println!("{e:?}")); + } + } + Ok(RpcValue(SendMessageResponse { + message_id: message.id, + })) +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SendMessageResponse { + message_id: String, +} diff --git a/crates/harmony/src/methods/mod.rs b/crates/harmony/src/methods/mod.rs new file mode 100644 index 0000000..83b05a1 --- /dev/null +++ b/crates/harmony/src/methods/mod.rs @@ -0,0 +1,160 @@ + +use serde::{Deserialize, Serialize}; + +use crate::services::database::messages::Message; + +pub mod channels; +pub mod events; +pub mod invites; +pub mod messages; +pub mod roles; +pub mod spaces; +pub mod users; +pub mod webrtc; + +// #[derive(Clone, Debug, Deserialize, Serialize)] +// #[serde(tag = "type", content = "data", rename_all = "SCREAMING_SNAKE_CASE")] +// #[repr(i8)] +// pub enum Method { +// Identify(IdentifyMethod) = 1, +// Heartbeat(HeartbeatMethod) = 2, +// GetId(GetIdMethod) = 3, + +// // WebRTC: 10-19 +// StartCall(StartCallMethod) = 10, +// JoinCall(JoinCallMethod) = 11, +// LeaveCall(LeaveCallMethod) = 12, +// EndCall(EndCallMethod) = 13, + +// GetMessages(GetMessagesMethod) = 20, +// SendMessage(SendMessageMethod) = 22, + +// GetChannel(GetChannelMethod) = 30, +// GetChannels(GetChannelsMethod) = 31, +// // CreateChannel(CreateChannelMethod) = 32, +// // EditChannel(EditChannelMethod) = 33, +// // DeleteChannel(DeleteChannelMethod) = 34, +// GetSpace(GetSpaceMethod) = 40, +// CreateSpace(CreateSpaceMethod) = 41, +// EditSpace(EditSpaceMethod) = 42, +// DeleteSpace(DeleteSpaceMethod) = 43, + +// // AddFriend(AddFriendMethod) = 50, +// // RemoveFriend(RemoveFriendMethod) = 51, +// // GetFriends(GetFriendsMethod) = 52, +// // GetFriendRequests(GetFriendRequestsMethod) = 53, +// // AcknowledgeFriendRequest(AcknowledgeFriendRequestMethod) = 55, +// CreateInvite(CreateInviteMethod) = 60, +// DeleteInvite(DeleteInviteMethod) = 61, +// GetInvite(GetInviteMethod) = 62, +// GetInvites(GetInvitesMethod) = 63, + +// CreateRole(CreateRoleMethod) = 70, +// EditRole(EditRoleMethod) = 71, +// DeleteRole(DeleteRoleMethod) = 72, +// // GetRoles(GetRolesMethod) = 73, +// } + + +// #[derive(Debug, Deserialize, Serialize)] +// pub struct RpcApiMethod { +// pub(crate) id: Option, +// #[serde(flatten)] +// pub(crate) method: Method, +// } + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AddFriendMethod { + channel_id: String, + friend_id: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoveFriendMethod { + channel_id: String, + friend_id: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetFriendsMethod {} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetFriendRequestsMethod {} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AcknowledgeFriendRequestMethod { + channel_id: String, + friend_id: String, +} + +// #[derive(Clone, Debug, Deserialize, Serialize)] +// pub struct RpcApiResponse { +// #[serde(skip_serializing_if = "Option::is_none")] +// pub(crate) id: Option, +// #[serde(flatten)] +// #[serde(skip_serializing_if = "Option::is_none")] +// pub(crate) response: Option, +// #[serde(flatten)] +// #[serde(skip_serializing_if = "Option::is_none")] +// pub(crate) error: Option, +// } + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[repr(i8)] +#[serde(tag = "type", content = "data", rename_all = "SCREAMING_SNAKE_CASE")] +pub enum Event { + Hello(HelloEvent) = 0, + + // WebRTC: 10-19 + NewMessage(NewMessageEvent) = 21, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RpcApiEvent { + #[serde(flatten)] + pub(crate) event: Event, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct HelloEvent { + pub(crate) public_key: Vec, + pub(crate) request_ids: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NewMessageEvent { + message: Message, + channel_id: String, +} + +pub enum CreateChannelType { + PrivateChannel { + peer_id: String, + scope_id: String, + }, + GroupChannel { + name: String, + description: String, + members: Vec, + scope_id: String, + }, + InformationChannel { + name: String, + description: String, + nexus_id: String, + scope_id: String, + }, + TextChannel { + name: String, + description: String, + nexus_id: String, + scope_id: String, + }, +} diff --git a/crates/harmony/src/methods/roles.rs b/crates/harmony/src/methods/roles.rs new file mode 100644 index 0000000..a17217c --- /dev/null +++ b/crates/harmony/src/methods/roles.rs @@ -0,0 +1,118 @@ +use std::sync::Arc; + +use dashmap::DashMap; +use rapid::socket::{RpcClient, RpcResponder, RpcValue}; +use serde::{Deserialize, Serialize}; + +use crate::{ + authentication::check_authenticated, errors::{Error, Result}, services::{ + database::{ + members::Member, + roles::{Color, Role}, + spaces::Space, + }, + permissions::{can_modify_role, Permission}, + } +}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateRoleMethod { + name: String, + permissions: i64, + color: Color, + space_id: String, +} + +async fn create_role( + clients: Arc>, + id: String, + data: CreateRoleMethod, +) -> impl RpcResponder { + check_authenticated(clients, &id)?; + let space = Space::get(&data.space_id).await?; + let role = Role::create( + &space, + data.name.clone(), + data.permissions, + data.color.clone(), + ) + .await?; + Ok::<_, Error>(RpcValue(CreateRoleResponse { role })) +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateRoleResponse { + role: Role, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EditRoleMethod { + id: String, + name: String, + permissions: i64, + color: Color, + space_id: String, + scope_id: Option, +} + +async fn edit_role( + clients: Arc>, + id: String, + data: EditRoleMethod, +) -> impl RpcResponder { + check_authenticated(clients, &id)?; + let role = Role::get(&data.id).await?; + if role.space_id != data.space_id { + return Err(Error::NotFound); + } + if let Some(scope_id) = &data.scope_id { + if role.scope_id != scope_id.clone() { + return Err(Error::NotFound); + } + } + let member = Member::get(&id, &data.space_id).await?; + let can_modify = can_modify_role(&member, &role).await?; + if !can_modify { + return Err(Error::MissingPermission { + permission: Permission::ManageRoles, + }); + } + let role = role + .update(data.name.clone(), data.permissions, data.color.clone()) + .await?; + Ok(RpcValue(EditRoleResponse { role })) +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EditRoleResponse { + role: Role, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DeleteRoleMethod { + id: String, +} + +async fn delete_role( + clients: Arc>, + id: String, + data: DeleteRoleMethod, +) -> impl RpcResponder { + check_authenticated(clients, &id)?; + let role = Role::get(&data.id).await?; + role.delete().await?; + Ok::<_, Error>(RpcValue(DeleteRoleResponse { + id: data.id.clone(), + })) +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DeleteRoleResponse { + id: String, +} diff --git a/crates/harmony/src/methods/spaces.rs b/crates/harmony/src/methods/spaces.rs new file mode 100644 index 0000000..8294c68 --- /dev/null +++ b/crates/harmony/src/methods/spaces.rs @@ -0,0 +1,207 @@ +use std::sync::Arc; + +use dashmap::DashMap; +use rapid::socket::{RpcClient, RpcResponder, RpcValue}; +use serde::{Deserialize, Serialize}; + +use crate::{ + authentication::check_authenticated, errors::Error, services::database::spaces::Space +}; + + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetSpaceMethod { + space_id: String, +} + +async fn get_space( + clients: Arc>, + id: String, + data: GetSpaceMethod, +) -> impl RpcResponder { + let user = check_authenticated(clients, &id)?; + let space = Space::get(&data.space_id).await?; + let user_in_space = user.in_space(&data.space_id).await?; + if !user_in_space { + return Err(Error::NotFound); + } + Ok(RpcValue(GetSpaceResponse { space })) +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetSpaceResponse { + space: Space, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateSpaceMethod { + // space: Space, + name: String, + description: Option, + scope: Option, +} + +async fn create_space( + clients: Arc>, + id: String, + data: CreateSpaceMethod, +) -> impl RpcResponder { + let user = check_authenticated(clients, &id)?; + let trimmed = data.name.trim(); + if trimmed.len() > 32 { + return Err(Error::NameTooLong); + } + if trimmed.is_empty() { + return Err(Error::NameEmpty); + } + let space = Space::create( + data.name.clone(), + data.description.clone(), + user.id.clone(), + data.scope.clone(), + ) + .await?; + Ok(RpcValue(CreateSpaceResponse { space })) +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateSpaceResponse { + space: Space, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct JoinSpaceMethod { + code: String, +} + +async fn join_space( + clients: Arc>, + id: String, + data: JoinSpaceMethod, +) -> impl RpcResponder { + let user = check_authenticated(clients, &id)?; + let space = user.accept_invite(&data.code).await?; + space.add_member(&id).await?; + Ok::<_, Error>(RpcValue(JoinSpaceResponse { space })) +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct JoinSpaceResponse { + space: Space, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LeaveSpaceMethod { + space_id: String, +} + +async fn leave_space( + clients: Arc>, + id: String, + data: LeaveSpaceMethod, +) -> impl RpcResponder { + let user = check_authenticated(clients, &id)?; + let user_in_space = user.in_space(&data.space_id).await?; + if !user_in_space { + return Err(Error::NotFound); + } + let space = Space::get(&data.space_id).await?; + space.remove_member(&id).await?; + Ok(RpcValue(LeaveSpaceResponse { + space_id: data.space_id.clone(), + })) +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LeaveSpaceResponse { + space_id: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetSpacesMethod {} + +async fn get_spaces( + clients: Arc>, + id: String, + _: GetSpacesMethod, +) -> impl RpcResponder { + let user = check_authenticated(clients, &id)?; + let spaces = user.get_spaces().await?; + Ok::<_, Error>(RpcValue(GetSpacesResponse { spaces })) +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetSpacesResponse { + spaces: Vec, +} + +// FIXME: permissions + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EditSpaceMethod { + space_id: String, + name: Option, + description: Option, + base_permissions: Option, +} +// TODO: logger +async fn edit_space( + clients: Arc>, + id: String, + data: EditSpaceMethod, +) -> impl RpcResponder { + check_authenticated(clients, &id)?; + let space = Space::get(&data.space_id).await?; + let space = space + .update( + data.name.clone(), + data.description.clone(), + data.base_permissions, + ) + .await?; + Ok::<_, Error>(RpcValue(EditSpaceResponse { space })) +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EditSpaceResponse { + space: Space, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DeleteSpaceMethod { + space_id: String, +} + +async fn delete_space( + clients: Arc>, + id: String, + data: DeleteSpaceMethod, +) -> impl RpcResponder { + check_authenticated(clients, &id)?; + let space = Space::get(&data.space_id).await?; + space.delete().await?; + Ok::<_, Error>(RpcValue(DeleteSpaceResponse { + id: data.space_id.clone(), + })) +} + +// FIXME: sudo mode + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DeleteSpaceResponse { + id: String, +} diff --git a/src/methods/users.rs b/crates/harmony/src/methods/users.rs similarity index 100% rename from src/methods/users.rs rename to crates/harmony/src/methods/users.rs diff --git a/crates/harmony/src/methods/webrtc.rs b/crates/harmony/src/methods/webrtc.rs new file mode 100644 index 0000000..ea85a2b --- /dev/null +++ b/crates/harmony/src/methods/webrtc.rs @@ -0,0 +1,179 @@ +use std::sync::Arc; + +use dashmap::DashMap; +use rapid::socket::{RpcClient, RpcResponder, RpcValue}; +use serde::{Deserialize, Serialize}; + +use crate::authentication::check_authenticated; +use crate::errors::{Error, Result}; +use crate::services::database::members::Member; +use crate::services::database::spaces::Space; +use crate::services::permissions::Permission; +use crate::services::webrtc::ActiveCall; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct JoinCallMethod { + id: String, + space_id: Option, + sdp: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct JoinCallResponse { + sdp: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RtcAuthorization { + channel_id: String, + user_id: String, + space_id: Option, +} + +async fn join_call( + clients: Arc>, + id: String, + data: JoinCallMethod, +) -> impl RpcResponder { + check_authenticated(clients, &id)?; // TODO: check rate limit, permissions req'd + if let Some(_space_id) = &data.space_id { + // let space = Space::get(space_id).await?; + // if !space.members.contains(&id) { + // return Err(Error::NotFound); // unauthorized + // } + // let member = Member::get(&id, &space.id).await?; + // let channel = space.get_channel(&self.id).await?; + // let permission = member + // .get_permission_in_channel(&channel, Permission::JoinCalls) + // .await?; + // if !permission { + // return Err(Error::MissingPermission { + // permission: Permission::JoinCalls, + // }); + // } + // let call = ActiveCall::get_in_channel(space_id, &self.id).await?; + // if let Some(mut call) = call { + // call.join_user(id.clone()).await?; + // let token = call.get_token(&id).await?; + // Ok(Response::JoinCall(JoinCallResponse { token })) + // } else { + // Err(Error::NotFound) + // } + Err::, _>(Error::NoVoiceNodesAvailable) + } else { + Err(Error::Unimplemented) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct StartCallMethod { + id: String, + space_id: Option, +} + +async fn start_call( + clients: Arc>, + id: String, + data: StartCallMethod, +) -> impl RpcResponder { + check_authenticated(clients, &id)?; + if let Some(space_id) = &data.space_id { + let space = Space::get(space_id).await?; + if !space.members.contains(&id) { + return Err(Error::NotFound); + } + let member = Member::get(&id, &space.id).await?; + let channel = space.get_channel(&data.id).await?; + let permission = member + .get_permission_in_channel(&channel, Permission::StartCalls) + .await?; + if !permission { + return Err(Error::MissingPermission { + permission: Permission::StartCalls, + }); + } + let call = ActiveCall::create(space_id, &data.id, &id).await?; + let token = call.get_token(&id).await?; + Ok(RpcValue(StartCallResponse { token })) + } else { + Err(Error::Unimplemented) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct StartCallResponse { + token: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct EndCallMethod { + id: String, + space_id: Option, +} + +async fn end_call( + clients: Arc>, + id: String, + data: EndCallMethod, +) -> impl RpcResponder { + check_authenticated(clients, &id)?; + if let Some(space_id) = &data.space_id { + let space = Space::get(space_id).await?; + if !space.members.contains(&id) { + return Err(Error::NotFound); + } + let member = Member::get(&id, &space.id).await?; + let channel = space.get_channel(&data.id).await?; + let permission = member + .get_permission_in_channel(&channel, Permission::ManageCalls) + .await?; + if !permission { + return Err(Error::MissingPermission { + permission: Permission::ManageCalls, + }); + } + let call = ActiveCall::get_in_channel(space_id, &data.id).await?; + if let Some(call) = call { + call.end().await?; + Ok(RpcValue(EndCallResponse {})) + } else { + Err(Error::NotFound) + } + } else { + Err(Error::Unimplemented) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct EndCallResponse {} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct LeaveCallMethod { + id: String, + space_id: Option, +} + +async fn leave_call( + clients: Arc>, + id: String, + data: LeaveCallMethod, +) -> impl RpcResponder { + check_authenticated(clients, &id)?; + if let Some(space_id) = &data.space_id { + let call = ActiveCall::get_in_channel(space_id, &data.id).await?; + if let Some(mut call) = call { + if call.members.contains(&id) { + return Err(Error::NotFound); + } + call.leave_user(&id.clone()).await?; + Ok(RpcValue(LeaveCallResponse {})) + } else { + Err(Error::NotFound) + } + } else { + Err(Error::Unimplemented) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct LeaveCallResponse {} diff --git a/src/services/database/calls.rs b/crates/harmony/src/services/database/calls.rs similarity index 100% rename from src/services/database/calls.rs rename to crates/harmony/src/services/database/calls.rs diff --git a/src/services/database/channels.rs b/crates/harmony/src/services/database/channels.rs similarity index 86% rename from src/services/database/channels.rs rename to crates/harmony/src/services/database/channels.rs index 276cad0..0d8fba0 100644 --- a/src/services/database/channels.rs +++ b/crates/harmony/src/services/database/channels.rs @@ -7,7 +7,7 @@ use crate::{ services::permissions::PermissionSet, }; -use super::messages::Message; +use super::{invites::Invite, messages::Message}; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type")] @@ -106,6 +106,29 @@ impl Channel { _ => Err(Error::NotFound), } } + + + pub async fn get_invites(&self) -> Result> { + match self { + Channel::AnnouncementChannel { id, space_id, .. } | Channel::ChatChannel { id, space_id, .. } => { + let database = super::get_database(); + let query = doc! { + "channel_id": &id, + "space_id": space_id, + }; + let invites: std::result::Result, _> = database + .collection::("invites") + .find(query) + .await? + .collect::>() + .await + .into_iter() + .collect(); + Ok(invites?) + } + _ => Err(Error::NotFound), + } + } } #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/src/services/database/emojis.rs b/crates/harmony/src/services/database/emojis.rs similarity index 100% rename from src/services/database/emojis.rs rename to crates/harmony/src/services/database/emojis.rs diff --git a/src/services/database/events.rs b/crates/harmony/src/services/database/events.rs similarity index 100% rename from src/services/database/events.rs rename to crates/harmony/src/services/database/events.rs diff --git a/src/services/database/infractions.rs b/crates/harmony/src/services/database/infractions.rs similarity index 100% rename from src/services/database/infractions.rs rename to crates/harmony/src/services/database/infractions.rs diff --git a/src/services/database/invites.rs b/crates/harmony/src/services/database/invites.rs similarity index 81% rename from src/services/database/invites.rs rename to crates/harmony/src/services/database/invites.rs index fcf43d1..5aceb10 100644 --- a/src/services/database/invites.rs +++ b/crates/harmony/src/services/database/invites.rs @@ -88,21 +88,3 @@ pub fn generate_code() -> String { // } -pub async fn get_invites(channel_id: String, space_id: Option) -> Result> { - let database = super::get_database(); - let mut query = doc! { - "channel_id": channel_id, - }; - if let Some(space_id) = space_id { - query.insert("space_id", space_id); - } - let invites: std::result::Result, _> = database - .collection::("invites") - .find(query) - .await? - .collect::>() - .await - .into_iter() - .collect(); - Ok(invites?) -} diff --git a/src/services/database/members.rs b/crates/harmony/src/services/database/members.rs similarity index 100% rename from src/services/database/members.rs rename to crates/harmony/src/services/database/members.rs diff --git a/src/services/database/messages.rs b/crates/harmony/src/services/database/messages.rs similarity index 100% rename from src/services/database/messages.rs rename to crates/harmony/src/services/database/messages.rs diff --git a/src/services/database/mod.rs b/crates/harmony/src/services/database/mod.rs similarity index 100% rename from src/services/database/mod.rs rename to crates/harmony/src/services/database/mod.rs diff --git a/src/services/database/roles.rs b/crates/harmony/src/services/database/roles.rs similarity index 100% rename from src/services/database/roles.rs rename to crates/harmony/src/services/database/roles.rs diff --git a/src/services/database/scopes.rs b/crates/harmony/src/services/database/scopes.rs similarity index 100% rename from src/services/database/scopes.rs rename to crates/harmony/src/services/database/scopes.rs diff --git a/src/services/database/spaces.rs b/crates/harmony/src/services/database/spaces.rs similarity index 100% rename from src/services/database/spaces.rs rename to crates/harmony/src/services/database/spaces.rs diff --git a/src/services/database/users.rs b/crates/harmony/src/services/database/users.rs similarity index 100% rename from src/services/database/users.rs rename to crates/harmony/src/services/database/users.rs diff --git a/src/services/encryption.rs b/crates/harmony/src/services/encryption.rs similarity index 86% rename from src/services/encryption.rs rename to crates/harmony/src/services/encryption.rs index 76ff759..81c3ccc 100644 --- a/src/services/encryption.rs +++ b/crates/harmony/src/services/encryption.rs @@ -6,6 +6,8 @@ use aes_gcm::aead::Aead; use aes_gcm::{Aes256Gcm, Nonce}; use flate2::read::ZlibDecoder; use flate2::write::ZlibEncoder; +use rmp_serde::{Deserializer, Serializer}; +use serde::{Deserialize, Serialize}; use std::io::{Cursor, Read}; @@ -97,3 +99,13 @@ pub fn decode(mut buffer: Vec, compress: bool, encrypt: Option) - buffer } } +pub fn serialize(value: &T) -> Result, rmp_serde::encode::Error> { + let mut buf = Vec::new(); + value.serialize(&mut Serializer::new(&mut buf).with_struct_map())?; + Ok(buf) +} + +pub fn deserialize Deserialize<'a>>(buf: &[u8]) -> Result { + let mut deserializer = Deserializer::new(buf); + Deserialize::deserialize(&mut deserializer) +} diff --git a/src/services/environment.rs b/crates/harmony/src/services/environment.rs similarity index 59% rename from src/services/environment.rs rename to crates/harmony/src/services/environment.rs index ca31e05..1b6a7b6 100644 --- a/src/services/environment.rs +++ b/crates/harmony/src/services/environment.rs @@ -13,5 +13,14 @@ lazy_static! { .expect("MAX_SPACE_COUNT must be an integer"); pub static ref LISTEN_ADDRESS: String = env::var("LISTEN_ADDRESS").unwrap_or_else(|_| "0.0.0.0:9000".to_string()); + // pub static ref REDIS_HOST: String = env::var("REDIS_HOST").expect("REDIS_HOST must be set"); + // pub static ref REDIS_PORT: u16 = env::var("REDIS_PORT") + // .unwrap_or_else(|_| "6379".to_string()) + // .parse::() + // .expect("REDIS_PORT must be an integer"); + // pub static ref REDIS_USERNAME: String = + // env::var("REDIS_USERNAME").unwrap_or_else(|_| "".to_string()); + // pub static ref REDIS_PASSWORD: String = + // env::var("REDIS_PASSWORD").unwrap_or_else(|_| "password".to_string()); pub static ref REDIS_URI: String = env::var("REDIS_URI").expect("REDIS_URI must be set"); } diff --git a/src/services/mod.rs b/crates/harmony/src/services/mod.rs similarity index 89% rename from src/services/mod.rs rename to crates/harmony/src/services/mod.rs index 77df2b6..ecb7cde 100644 --- a/src/services/mod.rs +++ b/crates/harmony/src/services/mod.rs @@ -3,6 +3,5 @@ pub mod encryption; pub mod environment; pub mod permissions; pub mod redis; -pub mod socket; pub mod webrtc; // pub mod logger; diff --git a/src/services/permissions.rs b/crates/harmony/src/services/permissions.rs similarity index 100% rename from src/services/permissions.rs rename to crates/harmony/src/services/permissions.rs diff --git a/src/services/redis.rs b/crates/harmony/src/services/redis.rs similarity index 100% rename from src/services/redis.rs rename to crates/harmony/src/services/redis.rs diff --git a/src/services/webrtc.rs b/crates/harmony/src/services/webrtc.rs similarity index 71% rename from src/services/webrtc.rs rename to crates/harmony/src/services/webrtc.rs index 5c52b8c..db39114 100644 --- a/src/services/webrtc.rs +++ b/crates/harmony/src/services/webrtc.rs @@ -3,28 +3,22 @@ use dashmap::DashMap; use futures_util::StreamExt; use jsonwebtoken::{encode, EncodingKey, Header}; use lazy_static::lazy_static; +use log::info; +use pulse_api::{NodeDescription, NodeEvent, NodeEventKind, Region}; use redis::{AsyncCommands, FromRedisValue, ToRedisArgs}; use serde::{Deserialize, Serialize}; use crate::errors::{Error, Result}; use super::{ - database::calls::Call, - environment::JWT_SECRET, - redis::{get_connection, get_pubsub}, - socket::{deserialize, serialize}, + database::calls::Call, encryption::{deserialize, serialize}, environment::JWT_SECRET, redis::{get_connection, get_pubsub} }; lazy_static! { pub static ref AVAILABLE_NODES: DashMap = DashMap::new(); } -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct NodeDescription { - id: String, - region: Region, -} - +#[derive(Clone, Debug)] pub struct Node { id: String, region: Region, @@ -35,101 +29,59 @@ impl Node { pub fn suppress(&self) { // TODO: disable node and clean up calls (move to other server if possible) } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct NodeEvent { - id: String, - #[serde(flatten)] - event: NodeEventKind, -} -#[derive(Clone, Debug, Deserialize, Serialize)] -pub enum NodeEventKind { - Description(NodeDescription), // on node connect - Ping, // on node ping - Disconnect, // on node disconnect - Query, // on server connect - UserConnect, // server -> node on user connect - UserCreate, // node -> server on new user - StartProduce, // server -> node on user start produce - StopProduce,// server -> node on user start produce - StartConsume, // server -> node on user start consume - StopConsume, // server -> node on user stop consume - UserDisconnect, // server -> node on user disconnect - UserDelete, // node -> server on user delete - TrackAvailable, - TrackUnavailable, -} - -impl ToRedisArgs for NodeEvent { - fn write_redis_args(&self, out: &mut W) - where - W: ?Sized + redis::RedisWrite, - { - let data = serialize(self).unwrap(); - out.write_arg(data.as_slice()); - } -} - -impl From for Node { - fn from(node: NodeDescription) -> Self { + pub fn new(id: String, description: NodeDescription) -> Self { let time = chrono::Utc::now().timestamp_millis(); Node { - id: node.id, - region: node.region, + id, + region: description.region, last_ping: time, } } } -#[derive(Clone, Debug, Deserialize, Serialize)] -pub enum Region { - Canada, - UsCentral, - UsEast, - UsWest, - Europe, - Asia, - SouthAmerica, - Australia, - Africa, -} pub fn spawn_check_available_nodes() { spawn(async move { let mut pubsub = get_pubsub().await; pubsub.subscribe("nodes").await.unwrap(); let mut connection = get_connection().await; - connection.publish::<&str, NodeEvent, NodeEvent>("nodes", NodeEvent { + connection.publish::<&str, NodeEvent, ()>("nodes", NodeEvent { event: NodeEventKind::Query, id: "server".to_owned(), - }).await.expect("Failed to broadcast query"); + }).await.expect("Failed to publish"); while let Some(msg) = pubsub.on_message().next().await { - let payload: NodeEvent = msg.get_payload().unwrap(); + let payload: Vec = msg.get_payload().unwrap(); + let payload: NodeEvent = deserialize(&payload).unwrap(); match payload { NodeEvent { + id, event: NodeEventKind::Description(description), .. } => { - let node: Node = description.into(); + let node: Node = Node::new(id, description); if AVAILABLE_NODES.contains_key(&node.id) { continue; } + let i = node.id.clone(); AVAILABLE_NODES.insert(node.id.clone(), node); + info!("Node {} connected", i); } NodeEvent { id, event: NodeEventKind::Ping, } => { - let mut node = AVAILABLE_NODES.get_mut(&id).unwrap(); - node.last_ping = chrono::Utc::now().timestamp_millis(); + let node = AVAILABLE_NODES.get_mut(&id); + if let Some(mut node) = node { + node.last_ping = chrono::Utc::now().timestamp_millis(); + } } NodeEvent { id, event: NodeEventKind::Disconnect, } => { AVAILABLE_NODES.remove(&id); + info!("Node {} disconnected", id); } // NodeEvent { // event: NodeEventKind::Timeout(user), @@ -150,30 +102,20 @@ pub fn spawn_check_available_nodes() { .. } => {} } - for node in AVAILABLE_NODES.iter() { - if node.last_ping + 10000 < chrono::Utc::now().timestamp_millis() { - node.suppress(); - // Remove node - let id = node.id.clone(); - drop(node); - AVAILABLE_NODES.remove(&id); - } - } } }); spawn(async move { loop { let time = chrono::Utc::now().timestamp_millis(); - let nodes = AVAILABLE_NODES.iter(); - for node in nodes { - if node.value().last_ping + 10000 < time { - node.value().suppress(); - // Remove node - let id = node.key().clone(); - drop(node); - AVAILABLE_NODES.remove(&id); + AVAILABLE_NODES.retain(|id, node| { + if node.last_ping + 10000 < time { + node.suppress(); + info!("Node {} timed out", id); + false // Remove node + } else { + true // Keep node } - } + }); // Don't deadlock sleep(std::time::Duration::from_millis(1000)).await; } @@ -222,28 +164,6 @@ impl FromRedisValue for ActiveCall { } } -impl FromRedisValue for NodeEvent { - fn from_redis_value(v: &redis::Value) -> redis::RedisResult { - match *v { - redis::Value::BulkString(ref bytes) => { - let data = deserialize(bytes); - match data { - Ok(data) => Ok(data), - Err(_) => Err(redis::RedisError::from(( - redis::ErrorKind::TypeError, - "Deserialization error", - ))), - } - } - - _ => Err(redis::RedisError::from(( - redis::ErrorKind::TypeError, - "Format error", - ))), - } - } -} - impl ToRedisArgs for ActiveCall { fn write_redis_args(&self, out: &mut W) where diff --git a/crates/pulse-api/Cargo.toml b/crates/pulse-api/Cargo.toml new file mode 100644 index 0000000..c6ab609 --- /dev/null +++ b/crates/pulse-api/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "pulse-api" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +redis = "0.27.6" +serde = { version = "1.0.144", features = ["derive"] } +rmp-serde = "1.1.2" \ No newline at end of file diff --git a/crates/pulse-api/src/lib.rs b/crates/pulse-api/src/lib.rs new file mode 100644 index 0000000..27035bb --- /dev/null +++ b/crates/pulse-api/src/lib.rs @@ -0,0 +1,149 @@ + + +use std::str::FromStr; + +use redis::FromRedisValue; + +use redis::ToRedisArgs; +use rmp_serde::Deserializer; +use rmp_serde::Serializer; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct NodeDescription { + pub region: Region, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct NodeEvent { + pub id: String, + #[serde(flatten)] + pub event: NodeEventKind, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "type")] +pub enum NodeEventKind { + Description(NodeDescription), // on node connect + Ping, // on node ping + Disconnect, // on node disconnect + Query, // on server connect + UserConnect { + session_id: String, + call_id: String, + sdp: SessionDescription + }, // server -> node on user connect + UserCreate { + sdp: SessionDescription, + }, // node -> server on new user + StartProduce { + track: String + }, // server -> node on user start produce + StopProduce { + track: String + + },// server -> node on user start produce + StartConsume { + track: String + }, // server -> node on user start consume + StopConsume { + track: String + }, // server -> node on user stop consume + UserDisconnect { + id: String + }, // server -> node on user disconnect + UserDelete { + id: String + }, // node -> server on user delete + TrackAvailable { + id: String + }, + TrackUnavailable { + id: String + }, +} + +impl ToRedisArgs for NodeEvent { + fn write_redis_args(&self, out: &mut W) + where + W: ?Sized + redis::RedisWrite, + { + let data = serialize(self).unwrap(); + out.write_arg(data.as_slice()); + } +} + +impl FromRedisValue for NodeEvent { + fn from_redis_value(v: &redis::Value) -> redis::RedisResult { + match *v { + redis::Value::BulkString(ref bytes) => { + let data = deserialize(bytes); + match data { + Ok(data) => Ok(data), + Err(_) => Err(redis::RedisError::from(( + redis::ErrorKind::TypeError, + "Deserialization error", + ))), + } + } + + _ => Err(redis::RedisError::from(( + redis::ErrorKind::TypeError, + "Format error", + ))), + } + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub enum Region { + Canada, + UsCentral, + UsEast, + UsWest, + Europe, + Asia, + SouthAmerica, + Australia, + Africa, +} + +impl FromStr for Region { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "canada" => Ok(Region::Canada), + "us-central" => Ok(Region::UsCentral), + "us-east" => Ok(Region::UsEast), + "us-west" => Ok(Region::UsWest), + "europe" => Ok(Region::Europe), + "asia" => Ok(Region::Asia), + "south-america" => Ok(Region::SouthAmerica), + "australia" => Ok(Region::Australia), + "africa" => Ok(Region::Africa), + _ => Err(()), + } + } +} + + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "sdp")] +pub enum SessionDescription { + #[serde(rename = "offer")] + Offer(String), + #[serde(rename = "answer")] + Answer(String), +} +pub fn serialize(value: &T) -> Result, rmp_serde::encode::Error> { + let mut buf = Vec::new(); + value.serialize(&mut Serializer::new(&mut buf).with_struct_map())?; + Ok(buf) +} + +pub fn deserialize Deserialize<'a>>(buf: &[u8]) -> Result { + let mut deserializer = Deserializer::new(buf); + Deserialize::deserialize(&mut deserializer) +} diff --git a/crates/pulse/Cargo.toml b/crates/pulse/Cargo.toml new file mode 100644 index 0000000..5779d1c --- /dev/null +++ b/crates/pulse/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "pulse" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0.144", features = ["derive"] } +rmp-serde = "1.1.2" + +futures = "0.3.24" +async-std = { version = "1.12.0", features = ["attributes", "tokio1"] } +async-tungstenite = "0.28.2" + +log = "0.4.17" +dashmap = "6.1.0" +lazy_static = "1.4.0" +once_cell = "1.18.0" +pretty_env_logger = "0.5.0" +tracing-subscriber = { version="0.3.18", features = ["env-filter"] } + +redis = { version = "0.27.6", features = ["async-std-comp"] } + +str0m = "0.6.1" + +ulid = "1.1.3" +rand = "0.8.5" +dotenvy = "0.15.7" + +pulse-api = { path = "../pulse-api" } diff --git a/crates/pulse/README.md b/crates/pulse/README.md new file mode 100644 index 0000000..54514ac --- /dev/null +++ b/crates/pulse/README.md @@ -0,0 +1,7 @@ +# Pulse + +## Description +Pulse is a WebRTC voice node for Harmony. Under the hood, it is a SFU (selective forwarding unit) that allows for multiple users to connect to a single voice channel and communicate with each other in real time. It is built using [str0m](https://github.com/algesten/str0m). It seamlessly connects with the Harmony core server using Redis. + +## License +This project is licensed under the [GNU Affero General Public License v3.0](https://github.com/Nextflow-Cloud/harmony/blob/main/LICENSE). diff --git a/crates/pulse/src/environment.rs b/crates/pulse/src/environment.rs new file mode 100644 index 0000000..a9cd02e --- /dev/null +++ b/crates/pulse/src/environment.rs @@ -0,0 +1,17 @@ +use std::{env, net::IpAddr}; + +use lazy_static::lazy_static; +use pulse_api::Region; + +lazy_static! { + pub static ref LISTEN_ADDRESS: String = + env::var("LISTEN_ADDRESS").unwrap_or("0.0.0.0:3001".to_string()); + pub static ref SOCKET_ADDRESS: IpAddr = env::var("SOCKET_ADDRESS") + .unwrap_or("209.145.60.11".to_string()) + .parse() + .unwrap(); + pub static ref PUBLIC_ADDRESS: String = + env::var("PUBLIC_ADDRESS").unwrap_or("209.145.60.11".to_string()); + pub static ref REDIS_URI: String = env::var("REDIS_URI").expect("REDIS_URI must be set"); + pub static ref REGION: Region = env::var("REGION").expect("REGION must be set").parse().expect("Invalid region"); +} diff --git a/crates/pulse/src/errors.rs b/crates/pulse/src/errors.rs new file mode 100644 index 0000000..5c97bdc --- /dev/null +++ b/crates/pulse/src/errors.rs @@ -0,0 +1,73 @@ +use std::fmt; + +use async_std::channel::SendError; +use str0m::error::SdpError; + +#[derive(Debug)] +pub enum Error { + InvalidCall, + FailedToAuthenticate, + AlreadyConnected, + RtcError, + SocketError, + SerializeError, +} + +impl std::error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::InvalidCall => write!(f, "Invalid call"), + Error::FailedToAuthenticate => write!(f, "Failed to authenticate"), + Error::AlreadyConnected => write!(f, "Already connected"), + Error::RtcError => write!(f, "RTC error"), + Error::SocketError => write!(f, "Socket error"), + Error::SerializeError => write!(f, "Serialize error"), + } + } +} + +impl From for Error { + fn from(e: async_tungstenite::tungstenite::Error) -> Self { + error!("{}", e); + Error::SocketError + } +} + +impl From for Error { + fn from(e: rmp_serde::encode::Error) -> Self { + error!("{}", e); + Error::SerializeError + } +} + +impl From for Error { + fn from(e: rmp_serde::decode::Error) -> Self { + error!("{}", e); + Error::SerializeError + } +} + +impl From for Error { + fn from(e: async_std::io::Error) -> Self { + error!("{}", e); + Error::SocketError + } +} + +impl From for Error { + fn from(e: SdpError) -> Self { + error!("{}", e); + Error::RtcError + } +} + +impl From> for Error { + fn from(e: SendError) -> Self { + error!("{}", e); + Error::SocketError + } +} + +pub type Result = std::result::Result; diff --git a/crates/pulse/src/main.rs b/crates/pulse/src/main.rs new file mode 100644 index 0000000..961cee7 --- /dev/null +++ b/crates/pulse/src/main.rs @@ -0,0 +1,34 @@ +#[macro_use] +extern crate log; + +use std::env; + +use pretty_env_logger::formatted_builder; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; + +pub mod environment; +pub mod errors; +pub mod redis; +pub mod rtc; +pub mod socket; + +use crate::errors::Result; + +#[async_std::main] +async fn main() -> Result<()> { + dotenvy::dotenv().ok(); + // let mut builder = formatted_builder(); + // builder.parse_filters("debug"); + // builder.try_init().unwrap(); + if env::var("RUST_LOG").is_err() { + env::set_var("RUST_LOG", "pulse=debug,str0m=debug"); + } + tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::from_default_env()) + .init(); + + redis::connect().await; + redis::listen().await; + Ok(()) +} diff --git a/crates/pulse/src/redis.rs b/crates/pulse/src/redis.rs new file mode 100644 index 0000000..9aed0a1 --- /dev/null +++ b/crates/pulse/src/redis.rs @@ -0,0 +1,112 @@ +use std::time::Duration; + +use async_std::{channel, stream::StreamExt, task}; +use dashmap::DashMap; +use lazy_static::lazy_static; +use once_cell::sync::OnceCell; +use pulse_api::{NodeDescription, NodeEvent, NodeEventKind, SessionDescription}; +use redis::{aio::MultiplexedConnection, AsyncCommands, Client}; +use str0m::change::SdpOffer; +use ulid::Ulid; + +use crate::{environment::{REDIS_URI, REGION}, rtc::peer::{ClientApi, ClientApiIn}, socket::server::{create_new_user, UserCapabilities, UserInformation}}; + +static REDIS: OnceCell = OnceCell::new(); + +pub async fn connect() { + let client = Client::open(&**REDIS_URI).expect("Failed to connect"); + REDIS.set(client).expect("Failed to set client"); +} + +pub fn get_client() -> &'static Client { + REDIS.get().expect("Failed to get client") +} + +pub async fn get_connection() -> MultiplexedConnection { + get_client() + .get_multiplexed_async_std_connection() + .await + .expect("Failed to get connection") +} + +pub async fn get_pubsub() -> redis::aio::PubSub { + get_client() + .get_async_pubsub() + .await + .expect("Failed to get connection") +} + +lazy_static! { + pub static ref CLIENTS: DashMap = DashMap::new(); +} + +pub async fn listen() -> () { + let mut pubsub = get_pubsub().await; + pubsub.subscribe("nodes").await.expect("Failed to subscribe"); + let instance_id = Ulid::new().to_string(); + let mut connection = get_connection().await; + connection.publish::<&str, NodeEvent, NodeEvent>("nodes", NodeEvent { + event: NodeEventKind::Description(NodeDescription { + region: *REGION, + }), + id: instance_id.clone(), + }).await; + let mut c = connection.clone(); + let i = instance_id.clone(); + task::spawn(async move { + loop { + c.publish::<&str, NodeEvent, NodeEvent>("nodes", NodeEvent { + event: NodeEventKind::Ping, + id: i.clone() + }).await; + task::sleep(Duration::from_secs(5)).await; + } + }); + while let Some(msg) = pubsub.on_message().next().await { + let payload: NodeEvent = msg.get_payload().unwrap(); + if payload.id == instance_id { + continue; + } + println!("Received: {:?}", payload); + match payload { + NodeEvent { + event: NodeEventKind::Query, + .. + } => { + connection.publish::<&str, NodeEvent, ()>("nodes", NodeEvent { + event: NodeEventKind::Description(NodeDescription { + region: *REGION, + }), + id: instance_id.clone(), + }).await.expect("Failed to publish"); + }, + NodeEvent { + event: NodeEventKind::UserConnect { + session_id, + call_id, + sdp + }, + .. + } => { + let (send, recv) = channel::unbounded::(); + let user = create_new_user(UserInformation { + id: session_id.clone(), + capabilities: UserCapabilities { + audio: true, + video: true, + screenshare: true, + }, + }, call_id, recv).await; + if let Ok(user) = user { + let SessionDescription::Offer(offer) = sdp else { + continue; + }; + user.send.send(ClientApiIn::Offer(SdpOffer::from_sdp_string(&offer).unwrap())).await.expect("Failed to send offer"); + CLIENTS.insert(session_id.clone(), user); + } + } + _ => {} + } + } + () +} \ No newline at end of file diff --git a/crates/pulse/src/rtc/call.rs b/crates/pulse/src/rtc/call.rs new file mode 100644 index 0000000..ad42693 --- /dev/null +++ b/crates/pulse/src/rtc/call.rs @@ -0,0 +1,116 @@ +use std::sync::Arc; + +use async_std::{ + channel::{self, Receiver, Sender}, + sync::Mutex, +}; +use dashmap::DashMap; +use lazy_static::lazy_static; + +use crate::socket::events::{MediaType, RemoteTrack}; + +use super::{ + peer::{Client, Track}, + udp::Propagated, +}; + +#[derive(Clone, Debug)] +pub enum CallEvent { + CreateTrack(RemoteTrack), + RemoveTrack { removed_tracks: Vec }, + + // Origin client ID + Propagate(Propagated), +} + +#[derive(Debug)] +pub struct Call { + id: String, + pub senders: Arc>>>, + pub tracks: DashMap>, + pub clients: DashMap, +} + +lazy_static! { + pub static ref CALLS: DashMap> = DashMap::new(); +} + +impl Call { + fn new(id: String) -> Self { + Call { + id, + senders: Default::default(), + // user_tracks: Default::default(), + tracks: Default::default(), + clients: Default::default(), + } + } + + pub fn destroy(&self) { + // for client in self.clients.iter() { + // self.remove_user(&client.id); + // } + CALLS.remove(&self.id); + } + + pub fn get(id: &str) -> Arc { + if let Some(call) = CALLS.get(id) { + call.clone() + } else { + let call: Arc = Arc::new(Call::new(id.to_string())); + CALLS.insert(id.to_string(), call.clone()); + + call + } + } + + pub async fn publish(&self, event: CallEvent) { + // self.sender.clone().try_send(event).ok(); + for sender in self.senders.lock().await.iter() { + sender.try_send(event.clone()).ok(); + } + } + + pub async fn listener(&self) -> Receiver { + let (sender, receiver) = channel::unbounded(); + self.senders.lock().await.push(sender); + receiver + } + + pub async fn get_available_tracks(&self) -> Vec { + let mut tracks = vec![]; + + for item in &self.tracks { + let id = item.key(); + let track = item.value(); + + // TODO: more detailed track info (structure: [{ user: id, tracks: [{ id, type }] }]) + tracks.push(RemoteTrack { + id: id.to_owned(), + media_type: MediaType::Video, // FIXME: get media type from track + user_id: track.producer.to_owned(), + }); + } + + tracks + } + + pub fn get_user_ids(&self) -> Vec { + self.clients.iter().map(|c| c.key().to_owned()).collect() + } + + pub async fn remove_user(&self, id: &str) { + let Some((_, mut client)) = self.clients.remove(id) else { + return; + }; + // if let Some (client) = client.upgrade() { + client.close(); + // } + + // TODO: emit event to redis + + if self.clients.is_empty() { + self.destroy(); + } + } +} diff --git a/crates/pulse/src/rtc/mod.rs b/crates/pulse/src/rtc/mod.rs new file mode 100644 index 0000000..07f13a4 --- /dev/null +++ b/crates/pulse/src/rtc/mod.rs @@ -0,0 +1,3 @@ +pub mod call; +pub mod peer; +pub mod udp; diff --git a/crates/pulse/src/rtc/peer.rs b/crates/pulse/src/rtc/peer.rs new file mode 100644 index 0000000..b9cdae9 --- /dev/null +++ b/crates/pulse/src/rtc/peer.rs @@ -0,0 +1,624 @@ +use std::{ + collections::VecDeque, + net::SocketAddr, + sync::{Arc, Weak}, + time::{Duration, Instant}, +}; + +use async_std::{ + channel::{self, Receiver, Sender}, + future::timeout, + net::UdpSocket, + task, +}; +use str0m::{ + change::{SdpAnswer, SdpOffer, SdpPendingOffer}, + channel::{ChannelData, ChannelId}, + media::{Direction, KeyframeRequest, KeyframeRequestKind, MediaData, MediaKind, Mid, Rid}, + Candidate, Event, IceConnectionState, Input, Output, Rtc, +}; +use ulid::Ulid; + +use crate::{ + errors::Error, redis::get_connection, socket::events::{MediaType, RemoteTrack} +}; + +use super::{ + call::{Call, CallEvent}, + udp::{poll_until_timeout, read_socket_input, Propagated}, +}; + +#[derive(Debug)] +pub struct TrackIn { + origin: String, + mid: Mid, + kind: MediaKind, +} + +#[derive(Debug)] +pub struct TrackInEntry { + pub id: Arc, + last_keyframe_request: Option, + pub active: bool, +} + +#[derive(Debug)] +pub struct TrackOut { + track_in: Weak, + state: TrackOutState, +} + +#[derive(Debug)] +pub struct Track { + track_in: Weak, + consumers: Vec>, + pub producer: String, + pub id: String, + media_type: MediaType, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TrackOutState { + ToOpen, + Negotiating(Mid), + Open(Mid), +} + +impl TrackOut { + fn mid(&self) -> Option { + match self.state { + TrackOutState::ToOpen => None, + TrackOutState::Negotiating(m) | TrackOutState::Open(m) => Some(m), + } + } +} + +pub enum ClientApiOut { + RemoveUser(String), + Answer(SdpAnswer), + // NewTrack(String), +} +pub enum ClientApiIn { + Offer(SdpOffer), + NewTrack(String, MediaType), + AddTrack(String), + RemoveTrack(String), + Destroy, +} + +#[derive(Debug)] +pub struct ClientApi { + pub send: Sender, + pub recv: Receiver, +} + +impl ClientApi { + pub fn new( + session_id: String, + socket: UdpSocket, + local_addr: SocketAddr, + call: Arc, + ) -> Self { + let (in_send, in_recv) = channel::unbounded(); + let (out_send, out_recv) = channel::unbounded(); + task::spawn(async move { + let mut client = Client::new(session_id, local_addr, call.clone()); + let mut to_propagate: VecDeque = VecDeque::new(); + let mut buf = vec![0; 2000]; + info!("Client {} started", client.session_id); + let receiver = call.listener().await; + let redis = get_connection().await; + loop { + if !client.rtc.is_alive() { + out_send + .send(ClientApiOut::RemoveUser(client.session_id.clone())) + .await?; + break; + // self.call.remove_user(&self.session_id); + } + + let msg = in_recv.try_recv(); + match msg { + Ok(ClientApiIn::Offer(offer)) => { + out_send + .send(ClientApiOut::Answer(client.negotiate(offer))) + .await?; + } + Ok(ClientApiIn::Destroy) => { + client.close(); + out_send + .send(ClientApiOut::RemoveUser(client.session_id.clone())) + .await?; + break; + } + Ok(ClientApiIn::NewTrack(track_id, media_type)) => { + // check if such track exists on rtc + let Some(track) = client + .tracks_in + .iter_mut() + .find(|t| t.id.mid.to_string() == track_id) + else { + warn!("WARNING: track not found"); + // TODO: handle error + continue; + }; + track.active = true; + let call_track = Track { + track_in: Arc::downgrade(&track.id.clone()), + consumers: vec![], + producer: client.session_id.clone(), + id: track_id.clone(), + media_type: media_type.clone(), + }; + client + .call + .tracks + .insert(track_id.clone(), Arc::new(call_track)); + debug!("New track: {:?}", track_id); + // FIXME: + call.publish(CallEvent::CreateTrack(RemoteTrack { + id: track_id.clone(), + user_id: client.session_id.clone(), + media_type, + })) + .await; + } + Ok(ClientApiIn::AddTrack(track_id)) => { + let Some(track_in) = call.tracks.iter().find(|t| t.id == track_id) else { + warn!("WARNING: track not found"); + continue; + }; + client.handle_track_open(track_in.track_in.clone()); + drop(track_in); + } + Ok(ClientApiIn::RemoveTrack(track_id)) => {} + Err(e) => { + // error!("{}", e); + } + } + + // Receive propagated events from other clients + if let Ok(event) = receiver.try_recv() { + match event { + CallEvent::Propagate(propagated) => { + let Some(client_id) = propagated.client_id() else { + // If the event doesn't have a client id, it can't be propagated, + // (it's either a noop or a timeout). + continue; + }; + + if client.session_id == client_id { + // Do not propagate to originating client. + continue; + } + + match propagated { + //TODO:more detailed description of track (producer id) + Propagated::MediaData(_, data) => { + client.handle_media_data_out(client_id.clone(), &data) + } + Propagated::KeyframeRequest(_, req, origin, mid_in) => { + // Only one origin client handles the keyframe request. + if *origin == client.session_id { + client.handle_keyframe_request(req, mid_in) + } + } + Propagated::Noop | Propagated::Timeout(_) => {} + } + }, + CallEvent::CreateTrack(_) => todo!(), + CallEvent::RemoveTrack { removed_tracks } => todo!(), + // CallEvent + } + } + + // Poll for output from RTC library + // Data to be sent out will be sent out + poll_until_timeout(&mut client, &mut to_propagate, &socket).await; + + // What the RTC library tells us to propagate to other clients + if let Some(p) = to_propagate.pop_front() { + call.publish(CallEvent::Propagate(p)).await; + continue; + } + + if let Ok(Some(input)) = + timeout(Duration::from_secs(1), read_socket_input(&socket, &mut buf)).await + { + client.handle_input(input); + } + + // Drive time forward in all clients. + let now = Instant::now(); + client.handle_input(Input::Timeout(now)); + } + Ok::<(), Error>(()) + }); + Self { + send: in_send, + recv: out_recv, + } + } +} + +#[derive(Debug)] +pub struct Client { + pub session_id: String, + pub rtc: Rtc, + pub pending: Option, + pub cid: Option, + pub tracks_in: Vec, + pub tracks_out: Vec, + pub chosen_rid: Option, + pub local_addr: SocketAddr, + pub call: Arc, +} + +impl Client { + pub fn new(session_id: String, local_addr: SocketAddr, call: Arc) -> Self { + let rtc = Rtc::new(); + Self { + session_id, + rtc, + pending: None, + cid: None, + tracks_in: vec![], + tracks_out: vec![], + chosen_rid: None, + local_addr, + call, + } + } + + pub fn negotiate(&mut self, offer: SdpOffer) -> SdpAnswer { + let candidate = Candidate::host(self.local_addr, "udp").expect("host candidate"); + self.rtc.add_local_candidate(candidate); + let answer = self + .rtc + .sdp_api() + .accept_offer(offer) + .expect("accept offer"); + answer + } + + pub fn close(&mut self) { + self.rtc.disconnect(); + } + + pub fn accepts(&self, input: &Input) -> bool { + self.rtc.accepts(input) + } + + pub fn handle_input(&mut self, input: Input) { + if !self.rtc.is_alive() { + return; + } + + if let Err(e) = self.rtc.handle_input(input) { + warn!("Client ({}) disconnected: {:?}", self.session_id, e); + self.rtc.disconnect(); + } + } + + pub async fn poll_output(&mut self, socket: &UdpSocket) -> Propagated { + if !self.rtc.is_alive() { + return Propagated::Noop; + } + + // Incoming tracks from other clients cause new entries in track_out that + // need SDP negotiation with the remote peer. + if self.negotiate_if_needed() { + return Propagated::Noop; + } + + match self.rtc.poll_output() { + Ok(output) => self.handle_output(output, socket).await, + Err(e) => { + warn!("Client ({}) poll_output failed: {:?}", self.session_id, e); + self.rtc.disconnect(); + Propagated::Noop + } + } + } + + async fn handle_output(&mut self, output: Output, socket: &UdpSocket) -> Propagated { + match output { + Output::Transmit(transmit) => { + socket + .send_to(&transmit.contents, transmit.destination) + .await + .expect("sending UDP data"); + Propagated::Noop + } + Output::Timeout(t) => Propagated::Timeout(t), + Output::Event(e) => match e { + Event::IceConnectionStateChange(v) => { + if v == IceConnectionState::Disconnected { + // Ice disconnect could result in trying to establish a new connection, + // but this impl just disconnects directly. + self.rtc.disconnect(); + } + Propagated::Noop + } + Event::MediaAdded(e) => self.handle_media_added(e.mid, e.kind), + Event::MediaData(data) => self.handle_media_data_in(data), + Event::KeyframeRequest(req) => self.handle_incoming_keyframe_req(req), + Event::ChannelOpen(cid, _) => { + info!("Channel open: {:?}", cid); + self.cid = Some(cid); + Propagated::Noop + } + Event::ChannelData(data) => self.handle_channel_data(data), + Event::StreamPaused(s) => { + // This is where we would pause the stream. + info!("Stream paused: {:?}", s); + // TODO: implement pausing of the stream (ignore for now) + // idea is that eventually we would terminate the stream immediately if the stream disconnects + // but for now: + // - when user leaves, terminate stream + // - or when user manually terminates stream + Propagated::Noop + } + + // NB: To see statistics, uncomment set_stats_interval() above. + Event::MediaIngressStats(data) => { + info!("{:?}", data); + Propagated::Noop + } + Event::MediaEgressStats(data) => { + info!("{:?}", data); + Propagated::Noop + } + Event::PeerStats(data) => { + info!("{:?}", data); + Propagated::Noop + } + _ => Propagated::Noop, + }, + } + } + + fn handle_media_added(&mut self, mid: Mid, kind: MediaKind) -> Propagated { + // let track_id = Ulid::new().to_string(); + let track_in = TrackInEntry { + id: Arc::new(TrackIn { + origin: self.session_id.clone(), + mid: mid.clone(), + kind, + }), + last_keyframe_request: None, + active: false, + }; + + // The Client instance owns the strong reference to the incoming + // track, all other clients have a weak reference. + // let weak = Arc::downgrade(&track_in.id); + self.tracks_in.push(track_in); + if let Some(mut channel) = self.cid.and_then(|id| self.rtc.channel(id)) { + let json = rmp_serde::to_vec(&mid.to_string()).unwrap(); + channel + .write(true, json.as_slice()) + .expect("to write answer"); + }; + + Propagated::Noop + } + + fn handle_media_data_in(&mut self, data: MediaData) -> Propagated { + if !data.contiguous { + self.request_keyframe_throttled(data.mid, data.rid, KeyframeRequestKind::Fir); + } + + Propagated::MediaData(self.session_id.clone(), Arc::new(data)) + } + + fn request_keyframe_throttled( + &mut self, + mid: Mid, + rid: Option, + kind: KeyframeRequestKind, + ) { + let Some(mut writer) = self.rtc.writer(mid) else { + return; + }; + + let Some(track_entry) = self.tracks_in.iter_mut().find(|t| t.id.mid == mid) else { + return; + }; + + if track_entry + .last_keyframe_request + .map(|t| t.elapsed() < Duration::from_secs(1)) + .unwrap_or(false) + { + return; + } + + _ = writer.request_keyframe(rid, kind); + + track_entry.last_keyframe_request = Some(Instant::now()); + } + + fn handle_incoming_keyframe_req(&self, mut req: KeyframeRequest) -> Propagated { + // Need to figure out the track_in mid that needs to handle the keyframe request. + let Some(track_out) = self.tracks_out.iter().find(|t| t.mid() == Some(req.mid)) else { + return Propagated::Noop; + }; + let Some(track_in) = track_out.track_in.upgrade() else { + return Propagated::Noop; + }; + + // This is the rid picked from incoming mediadata, and to which we need to + // send the keyframe request. + req.rid = self.chosen_rid; + + Propagated::KeyframeRequest( + self.session_id.clone(), + req, + track_in.origin.clone(), + track_in.mid, + ) + } + + fn negotiate_if_needed(&mut self) -> bool { + if self.cid.is_none() || self.pending.is_some() { + debug!("no data channel!!!!!!!!!!!!!!!"); + // Don't negotiate if there is no data channel, or if we have pending changes already. + return false; + } + + let mut change = self.rtc.sdp_api(); + + for track in &mut self.tracks_out { + if let TrackOutState::ToOpen = track.state { + if let Some(track_in) = track.track_in.upgrade() { + let stream_id = track_in.origin.to_string(); + let mid = + change.add_media(track_in.kind, Direction::SendOnly, Some(stream_id), None); + track.state = TrackOutState::Negotiating(mid); + } + } + } + + if !change.has_changes() { + return false; + } + + let Some((offer, pending)) = change.apply() else { + return false; + }; + + let Some(mut channel) = self.cid.and_then(|id| self.rtc.channel(id)) else { + return false; + }; + + let json = rmp_serde::to_vec_named(&offer).unwrap(); + channel + .write(true, json.as_slice()) + .expect("to write answer"); + + self.pending = Some(pending); + + true + } + + fn handle_channel_data(&mut self, d: ChannelData) -> Propagated { + if let Ok(offer) = rmp_serde::from_slice::<'_, SdpOffer>(&d.data) { + self.handle_offer(offer); + } else if let Ok(answer) = rmp_serde::from_slice::<'_, SdpAnswer>(&d.data) { + self.handle_answer(answer); + } + println!("we got something"); + Propagated::Noop + } + + fn handle_offer(&mut self, offer: SdpOffer) { + let answer = self + .rtc + .sdp_api() + .accept_offer(offer) + .expect("offer to be accepted"); + + // Keep local track state in sync, cancelling any pending negotiation + // so we can redo it after this offer is handled. + for track in &mut self.tracks_out { + if let TrackOutState::Negotiating(_) = track.state { + track.state = TrackOutState::ToOpen; + } + } + + let mut channel = self + .cid + .and_then(|id| self.rtc.channel(id)) + .expect("channel to be open"); + + let json = rmp_serde::to_vec_named(&answer).unwrap(); + channel + .write(true, json.as_slice()) + .expect("to write answer"); + } + + fn handle_answer(&mut self, answer: SdpAnswer) { + if let Some(pending) = self.pending.take() { + self.rtc + .sdp_api() + .accept_answer(pending, answer) + .expect("answer to be accepted"); + + for track in &mut self.tracks_out { + if let TrackOutState::Negotiating(m) = track.state { + track.state = TrackOutState::Open(m); + } + } + } + } + + pub fn handle_track_open(&mut self, track_in: Weak) { + let track_out = TrackOut { + track_in, + state: TrackOutState::ToOpen, + }; + self.tracks_out.push(track_out); + } + + pub fn handle_media_data_out(&mut self, origin: String, data: &MediaData) { + // Figure out which outgoing track maps to the incoming media data. + let Some(mid) = self + .tracks_out + .iter() + .find(|o| { + o.track_in + .upgrade() + .filter(|i| i.origin == origin && i.mid == data.mid) + .is_some() + }) + .and_then(|o| o.mid()) + else { + return; + }; + + if data.rid.is_some() && data.rid != Some("h".into()) { + // This is where we plug in a selection strategy for simulcast. For + // now either let rid=None through (which would be no simulcast layers) + // or "h" if we have simulcast (see commented out code in chat.html). + return; + } + // TODO: option to disable leave sound + // Remember this value for keyframe requests. + if self.chosen_rid != data.rid { + self.chosen_rid = data.rid; + } + + let Some(writer) = self.rtc.writer(mid) else { + return; + }; + + // Match outgoing pt to incoming codec. + let Some(pt) = writer.match_params(data.params) else { + return; + }; + + if let Err(e) = writer.write(pt, data.network_time, data.time, data.data.clone()) { + warn!("Client ({}) failed: {:?}", self.session_id, e); + self.rtc.disconnect(); + } + } + + pub fn handle_keyframe_request(&mut self, req: KeyframeRequest, mid_in: Mid) { + let has_incoming_track = self.tracks_in.iter().any(|i| i.id.mid == mid_in); + + // This will be the case for all other client but the one where the track originates. + if !has_incoming_track { + return; + } + + let Some(mut writer) = self.rtc.writer(mid_in) else { + return; + }; + + if let Err(e) = writer.request_keyframe(req.rid, req.kind) { + // This can fail if the rid doesn't match any media. + info!("request_keyframe failed: {:?}", e); + } + } +} diff --git a/crates/pulse/src/rtc/udp.rs b/crates/pulse/src/rtc/udp.rs new file mode 100644 index 0000000..3ab9bd8 --- /dev/null +++ b/crates/pulse/src/rtc/udp.rs @@ -0,0 +1,96 @@ +use std::{collections::VecDeque, io::ErrorKind, sync::Arc, time::Instant}; + +use async_std::net::UdpSocket; +use str0m::{ + media::{KeyframeRequest, MediaData, Mid}, + net::{Protocol, Receive}, + Input, +}; + +use super::peer::Client; + +#[derive(Clone, Debug)] +pub enum Propagated { + /// When we have nothing to propagate. + Noop, + + /// Poll client has reached timeout. + Timeout(Instant), + + /// A new incoming track opened. + // TrackOpen(String, String), + + /// Data to be propagated from one client to another. + MediaData(String, Arc), + + /// A keyframe request from one client to the source. + KeyframeRequest(String, KeyframeRequest, String, Mid), +} + +impl Propagated { + /// Get client id, if the propagated event has a client id. + pub fn client_id(&self) -> Option { + match self { + // Propagated::TrackOpen(c, _) + Propagated::MediaData(c, _) | Propagated::KeyframeRequest(c, _, _, _) => { + Some(c.clone()) + } + _ => None, + } + } +} + +pub async fn read_socket_input<'a>(socket: &UdpSocket, buf: &'a mut Vec) -> Option> { + buf.resize(2000, 0); + + match socket.recv_from(buf).await { + Ok((n, source)) => { + buf.truncate(n); + + // Parse data to a DatagramRecv, which help preparse network data to + // figure out the multiplexing of all protocols on one UDP port. + let Ok(contents) = buf.as_slice().try_into() else { + return None; + }; + + return Some(Input::Receive( + Instant::now(), + Receive { + proto: Protocol::Udp, + source, + destination: socket.local_addr().unwrap(), + contents, + }, + )); + } + + Err(e) => match e.kind() { + // Expected error for set_read_timeout(). One for windows, one for the rest. + ErrorKind::WouldBlock | ErrorKind::TimedOut => None, + _ => panic!("UdpSocket read failed: {e:?}"), + }, + } +} + +/// Poll all the output from the client until it returns a timeout. +/// Collect any output in the queue, transmit data on the socket, return the timeout +pub async fn poll_until_timeout( + client: &mut Client, + queue: &mut VecDeque, + socket: &UdpSocket, +) -> Instant { + loop { + if !client.rtc.is_alive() { + // This client will be cleaned up in the next run of the main loop. + return Instant::now(); + } + + let propagated = client.poll_output(socket).await; + + if let Propagated::Timeout(t) = propagated { + return t; + } + + queue.push_back(propagated) + } +} diff --git a/crates/pulse/src/socket/client.rs b/crates/pulse/src/socket/client.rs new file mode 100644 index 0000000..fc49cf6 --- /dev/null +++ b/crates/pulse/src/socket/client.rs @@ -0,0 +1,67 @@ +use std::sync::Arc; + +use crate::{ + environment, errors::Result, rtc::peer::ClientApi +}; +use async_std::{ + channel::Receiver, net::UdpSocket +}; +use pulse_api::NodeEvent; +use ulid::Ulid; + +use crate::rtc::call::Call; + +use super:: + server::UserInformation +; + +#[derive(Debug)] +pub struct CallUser { + user: UserInformation, + call: Arc, + pub session_id: String, // TODO: set this value + client: ClientApi, +} + +impl CallUser { + pub async fn create(user: UserInformation, call_id: String) -> Result { + let call = Call::get(&call_id); + info!("Created a new client for {user:?} in call {call_id}."); + + // let udp_server = UdpSocket::bind(format!("{}:0", *environment::SOCKET_ADDRESS)).await?; + //bind to any port in 10000-11000 range + let udp_server = UdpSocket::bind(format!( + "{}:{}", + *environment::SOCKET_ADDRESS, + 10000 + rand::random::() % 1000 + )) + .await?; + let local_addr = udp_server.local_addr()?; + + let session_id = Ulid::new().to_string(); + + let client = ClientApi::new(session_id.clone(), udp_server, local_addr, call.clone()); + + Ok(Self { + user, + call, + session_id, + client, + }) + } + + pub async fn run(mut self, stream: Receiver) -> Result<()> { + // debug!("Announcing current state to client"); + // write + // .send(ServerEvent::Accept { + // available_tracks: self.call.get_available_tracks().await, + // user_ids: self.call.get_user_ids(), + // }) + // .await?; + + debug!("Listening for events"); + info!("User {} disconnected", self.user.id); + self.call.remove_user(&self.user.id).await; + Ok(()) + } +} diff --git a/crates/pulse/src/socket/events.rs b/crates/pulse/src/socket/events.rs new file mode 100644 index 0000000..78b7b8f --- /dev/null +++ b/crates/pulse/src/socket/events.rs @@ -0,0 +1,60 @@ +use rmp_serde::{Deserializer, Serializer}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub enum MediaType { + Audio, + Video, + ScreenAudio, + ScreenVideo, +} + +#[derive(Debug, Clone, Serialize)] +pub struct RemoteTrack { + pub id: String, + pub user_id: String, + pub media_type: MediaType, +} + +// #[derive(Debug, Clone, Serialize, Deserialize)] +// #[serde(rename_all = "camelCase")] +// pub struct IceCandidate { +// pub candidate: String, +// #[serde(default)] +// pub sdp_mid: String, +// #[serde(default)] +// pub sdp_mline_index: u16, +// #[serde(default)] +// pub username_fragment: String, +// } + +// #[derive(Debug, Clone, Serialize, Deserialize)] +// pub struct Negotiation { +// pub description: SessionDescription, +// } + + +// impl std::fmt::Display for MediaType { +// fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { +// match self { +// MediaType::Audio => write!(f, "Audio"), +// MediaType::Video => write!(f, "Video"), +// MediaType::ScreenAudio => write!(f, "ScreenAudio"), +// MediaType::ScreenVideo => write!(f, "ScreenVideo"), +// } +// } +// } +pub fn serialize( + value: &T, +) -> std::result::Result, rmp_serde::encode::Error> { + let mut buf = Vec::new(); + value.serialize(&mut Serializer::new(&mut buf).with_struct_map())?; + Ok(buf) +} + +pub fn deserialize Deserialize<'a>>( + buf: &[u8], +) -> std::result::Result { + let mut deserializer = Deserializer::new(buf); + Deserialize::deserialize(&mut deserializer) +} diff --git a/crates/pulse/src/socket/mod.rs b/crates/pulse/src/socket/mod.rs new file mode 100644 index 0000000..39403be --- /dev/null +++ b/crates/pulse/src/socket/mod.rs @@ -0,0 +1,3 @@ +pub mod client; +pub mod events; +pub mod server; diff --git a/crates/pulse/src/socket/server.rs b/crates/pulse/src/socket/server.rs new file mode 100644 index 0000000..b014d72 --- /dev/null +++ b/crates/pulse/src/socket/server.rs @@ -0,0 +1,42 @@ +use async_std::{channel::Receiver, net::UdpSocket}; +use pulse_api::NodeEvent; +use ulid::Ulid; + +use crate::{environment, errors::Result, rtc::{call::Call, peer::ClientApi}}; + +use super::client::CallUser; + +#[derive(Default, Debug)] +pub struct UserCapabilities { + pub audio: bool, + pub video: bool, + pub screenshare: bool, +} + +#[derive(Debug)] +pub struct UserInformation { + pub id: String, + pub capabilities: UserCapabilities, +} + +pub async fn create_new_user(user: UserInformation, call_id: String, recv: Receiver) -> Result { + info!("User {} joined {call_id}", user.id); + let call = Call::get(&call_id); + + // let udp_server = UdpSocket::bind(format!("{}:0", *environment::SOCKET_ADDRESS)).await?; + //bind to any port in 10000-11000 range + let udp_server = UdpSocket::bind(format!( + "{}:{}", + *environment::SOCKET_ADDRESS, + 10000 + rand::random::() % 1000 + )) + .await?; + let local_addr = udp_server.local_addr()?; + + let session_id = Ulid::new().to_string(); + + let client = ClientApi::new(session_id.clone(), udp_server, local_addr, call.clone()); + // let client = CallUser::create(user, call_id).await?; + // client.run(recv).await + Ok(client) +} diff --git a/crates/rapid/Cargo.toml b/crates/rapid/Cargo.toml new file mode 100644 index 0000000..02f852e --- /dev/null +++ b/crates/rapid/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "rapid" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-std = { version = "1.12.0", features = ["attributes", "tokio1"] } +async-trait = "0.1.73" +futures-util = "0.3.28" + +dashmap = "6.0.0" +lazy_static = "1.4.0" +once_cell = "1.18.0" + +chrono = "0.4.26" +rand = "0.8.5" +ulid = "1.0.0" + +env_logger = "0.11.0" +log = "0.4.20" + +async-tungstenite = "0.28.0" + +serde = { version = "1.0.183", features = ["derive"] } +rmp-serde = "1.1.2" +typetag = "0.2.18" +rmpv = { version = "1.3.0", features = ["with-serde"] } +x25519-dalek = "2.0.0" diff --git a/crates/rapid/README.md b/crates/rapid/README.md new file mode 100644 index 0000000..86e96ff --- /dev/null +++ b/crates/rapid/README.md @@ -0,0 +1,2 @@ +# Rapid +R**api**d is an opinionated RPC API framework for Rust built upon async-tungstenite. diff --git a/crates/rapid/src/errors.rs b/crates/rapid/src/errors.rs new file mode 100644 index 0000000..280a366 --- /dev/null +++ b/crates/rapid/src/errors.rs @@ -0,0 +1,44 @@ +use std::fmt; + +use serde::{Deserialize, Serialize}; + +pub type Result = std::result::Result; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "error", rename_all = "SCREAMING_SNAKE_CASE")] +pub enum Error { + // Generic errors + NotFound, + Unimplemented, + InvalidMethod, + InvalidRequestId, + InternalError, + + // Authentication errors + InvalidToken, + NotAuthenticated, + SerializeError, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::NotFound => write!(f, "Not found"), + Error::Unimplemented => write!(f, "Unimplemented"), + Error::InvalidMethod => write!(f, "Invalid method"), + Error::InvalidRequestId => write!(f, "Invalid request id"), + Error::InternalError => write!(f, "Internal error"), + Error::InvalidToken => write!(f, "Invalid token"), + Error::NotAuthenticated => write!(f, "Not authenticated"), + Error::SerializeError => write!(f, "Serialize error"), + } + } +} + +impl From for Error { + fn from(_: rmpv::ext::Error) -> Self { + Error::SerializeError + } +} + +impl std::error::Error for Error {} diff --git a/crates/rapid/src/lib.rs b/crates/rapid/src/lib.rs new file mode 100644 index 0000000..b7d8aff --- /dev/null +++ b/crates/rapid/src/lib.rs @@ -0,0 +1,4 @@ + +pub mod socket; +mod utilities; +pub mod errors; diff --git a/crates/rapid/src/socket.rs b/crates/rapid/src/socket.rs new file mode 100644 index 0000000..576e414 --- /dev/null +++ b/crates/rapid/src/socket.rs @@ -0,0 +1,423 @@ +use std::{any::Any, future::Future, pin::Pin, sync::Arc}; + +use async_std::{ + channel::{unbounded, Sender}, + future, + net::{TcpListener, TcpStream}, + task::spawn, +}; +use async_tungstenite::{accept_async, tungstenite::Message}; +use dashmap::DashMap; +use futures_util::{future::BoxFuture, SinkExt, StreamExt}; +use log::debug; +use rand::rngs::OsRng; +use rmp_serde::{Deserializer, Serializer}; +use rmpv::{ext::{from_value, to_value}, Value}; +use serde::{Deserialize, Serialize}; +use x25519_dalek::{EphemeralSecret, PublicKey}; + +use crate::{errors::Error, utilities::{generate_id, HEARTBEAT_TIMEOUT}}; + +#[derive(Clone)] +pub struct RpcClient { + pub id: String, + pub socket: Arc>, + pub user: Option>>, + pub request_ids: Vec, + pub heartbeat_tx: Arc>, +} + +impl RpcClient { + // pub fn send(&self, data: Vec) { + // self.socket + // .send(Message::Binary(data)) + // .expect("Failed to send message"); + // } + pub fn get_user(&self) -> Option<&T> { + self.user.as_ref().and_then(|u| u.downcast_ref()) + } +} + +// pub type RpcMethod = dyn Fn(Arc>, String, T) -> impl RpcResponder; + +pub trait RpcResponder { + fn into_value(&self) -> Value; +} + +pub struct RpcValue(pub T); + +impl RpcResponder for RpcValue { + fn into_value(&self) -> Value { + to_value(&self.0).unwrap() + } +} +impl RpcResponder for Result { + fn into_value(&self) -> Value { + match self { + Ok(value) => value.into_value(), + Err(error) => error.into_value(), + } + } +} +pub trait RpcRequest { + fn from_value(value: Value) -> Result + where + Self: Sized; +} + +impl RpcValue { + pub fn into_inner(self) -> T { + self.0 + } +} + +impl Deserialize<'a>> RpcRequest for RpcValue { + fn from_value(value: Value) -> Result { + let val = from_value::(value); + match val { + Ok(v) => Ok(RpcValue(v)), + Err(e) => Err(e.into()), + } + } +} + +pub type AuthenticateFn = Box; +pub trait CloneableAuthenticateFn: Fn(String) -> BoxFuture<'static, Result, Error>> + Send + Sync { + fn clone_box<'a>(&self) -> Box + where + Self: 'a; +} +impl CloneableAuthenticateFn for F +where + F: Fn(String) -> BoxFuture<'static, Result, Error>> + Clone + Send + Sync, +{ + fn clone_box<'a>(&self) -> Box + where + Self: 'a, + { + Box::new(self.clone()) + } +} +impl<'a> Clone for Box { + fn clone(&self) -> Self { + (**self).clone_box() + } +} + + + + +pub trait MethodFn: Fn(Arc>, String, Value) -> BoxFuture<'static, Value> + Send + Sync { + fn clone_box<'a>(&self) -> Box + where + Self: 'a; +} +impl MethodFn for F +where + F: Fn(Arc>, String, Value) -> BoxFuture<'static, Value> + Clone + Send + Sync, +{ + fn clone_box<'a>(&self) -> Box + where + Self: 'a, + { + Box::new(self.clone()) + } +} +impl<'a> Clone for Box { + fn clone(&self) -> Self { + (**self).clone_box() + } +} + + + +pub trait Handler: Clone + 'static { + type Output; + type Future: Future; + fn call (&self, clients: Arc>, name: String, request: G) -> Self::Future; +} + +impl Handler for F +where + F: Fn(Arc>, String, G) -> Fut + Clone + 'static, + Fut: Future, +{ + type Output = Fut::Output; + type Future = Fut; + fn call(&self, clients: Arc>, name: String, request: G) -> Self::Future { + self(clients, name, request) + } +} + + +pub struct RpcServer { + clients: Arc>, + authenticate: AuthenticateFn, + methods: Arc>>, +} + +impl RpcServer { + pub fn new(authenticate: AuthenticateFn) -> Self { + Self { + clients: Arc::new(DashMap::new()), + authenticate, + methods: Arc::new(DashMap::new()), + } + } + + pub fn register(&self, name: String, method: F) -> () where + F: Handler + Sync + Send, + G: RpcRequest + Send, + F::Output: RpcResponder + 'static, + F::Future: Send + 'static, + { + let x = Box::new(move |clients: Arc>, id: String, val: Value| { + let method = method.clone(); + let n: Pin + Send>> = Box::pin(async move { + let g = G::from_value(val); + let g = match g { + Ok(g) => g, + Err(e) => return RpcValue(e).into_value(), + }; + let res = method.call(clients, id, g).await; + res.into_value() + }); + n + }); + self.methods.insert(name, x); + } + + pub async fn start(&self, address: String) { + let server = TcpListener::bind(address).await.unwrap(); + let mut incoming = server.incoming(); + while let Some(stream) = incoming.next().await { + let clients = self.clients.clone(); + let fnc = self.authenticate.clone(); + let methods = self.methods.clone(); + spawn(async move { start_client(stream, clients, fnc, methods).await }); + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum RpcApiRequest { + Identify { + token: String, + public_key: Vec, + }, + Heartbeat {}, + GetId {}, + Message { + id: String, + method: String, + data: Value, + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct HelloEvent { + public_key: Vec, + request_ids: Vec, +} + +async fn start_client( + stream: Result, + clients: Arc>, + authenticate: AuthenticateFn, + methods: Arc>>, +) { + let connection = stream.unwrap(); + println!("Socket connected: {}", connection.peer_addr().unwrap()); + let ws_stream = accept_async(connection).await.expect("Failed to accept"); + let (mut write, mut read) = ws_stream.split(); + let (s, r) = unbounded::(); + spawn(async move { + while let Ok(msg) = r.recv().await { + write.send(msg).await.expect("Failed to send message"); + } + write.close().await.expect("Failed to close"); + }); + let id = generate_id(); + let mut request_ids = Vec::new(); + for _ in 0..20 { + request_ids.push(generate_id()); + } + let secret = EphemeralSecret::random_from_rng(OsRng); + let public_key = PublicKey::from(&secret); + let val = HelloEvent { + public_key: public_key.to_bytes().to_vec(), + request_ids: request_ids.clone(), + }; + s.send(Message::Binary( + serialize(&val).expect("Failed to serialize"), + )) + .await + .expect("Failed to send message"); + + let (tx, rx) = unbounded::<()>(); + let clients_moved = clients.clone(); + let id_moved = id.clone(); + spawn(async move { + while future::timeout( + std::time::Duration::from_millis(*HEARTBEAT_TIMEOUT), + rx.recv(), + ) + .await + .is_ok() + {} + if let Some((_, client)) = clients_moved.remove(&id_moved) { + client.socket.close(); + } + }); + let client = RpcClient { + id: id.clone(), + socket: Arc::new(s), + user: None, + request_ids, + heartbeat_tx: Arc::new(tx), + }; + clients.insert(id.clone(), client); + while let Some(data) = read.next().await { + match data.unwrap() { + Message::Binary(bin) => { + debug!("Received binary data"); + let response = handle_packet(bin, &clients, &id, authenticate.clone(), methods.clone()).await; + let client = clients.get(&id.clone()).unwrap(); + client + .socket + .send(Message::Binary( + serialize(&response).expect("Failed to serialize"), + )) + .await + .expect("Failed to send message"); + } + Message::Close(_) => { + debug!("Received close"); + } + _ => { + debug!("Received unknown message"); + if let Some((_, client)) = clients.remove(&id.clone()) { + client.socket.close(); + } + } + } + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct RpcApiResponse { + id: Option, + response: Option, +} + +#[derive(Clone, Debug, Serialize)] +pub struct IdentifyResponse {} + +#[derive(Clone, Debug, Serialize)] +pub struct HeartbeatResponse {} + +#[derive(Clone, Debug, Serialize)] +pub struct GetIdResponse { + request_ids: Vec, +} + +impl Into for Error { + fn into(self) -> Value { + to_value(&self).unwrap() + } +} + +#[derive(Clone, Debug, Serialize)] +struct RpcApiError { + error: Error, +} + +impl Into for RpcApiError { + fn into(self) -> Value { + to_value(&self).unwrap() + } +} + +pub async fn handle_packet( + bin: Vec, + clients: &Arc>, + user_id: &String, + authenticate: AuthenticateFn, + methods: Arc>>, +) -> RpcApiResponse { + let result = deserialize::(bin.as_slice()); + if let Ok(r) = result { + debug!("Received: {:?}", r); + match r { + RpcApiRequest::Identify { token, public_key: _ } => { + authenticate(token.clone()).await.map(|user| { + let mut client = clients.get_mut(user_id).unwrap(); + client.user = Some(Arc::new(user)); + let response = IdentifyResponse {}; + return RpcApiResponse { + id: None, + response: Some(to_value(response).unwrap()), + }; + }).unwrap_or_else(|e| RpcApiResponse { + id: None, + response: Some(RpcApiError { error: e.into() }.into()), + }) + }, + RpcApiRequest::Heartbeat { } => { + let client = clients.get(user_id).unwrap(); + client.heartbeat_tx.send(()).await.unwrap(); + let response = HeartbeatResponse {}; + RpcApiResponse { + response: Some(to_value(response).unwrap()), + id: None, + } + }, + RpcApiRequest::GetId { } => { + let mut client = clients.get_mut(user_id).unwrap(); + let mut new_request_ids = Vec::new(); + for _ in 0..20 { + let id = generate_id(); + client.request_ids.push(id.clone()); + new_request_ids.push(id); + } + let response = GetIdResponse { + request_ids: new_request_ids, + }; + RpcApiResponse { + response: Some(to_value(response).unwrap()), + id: None, + } + }, + RpcApiRequest::Message { id, method, data } => { + let method = methods.get(&method); + let Some(method) = method else { + return RpcApiResponse { + id: Some(id), + response: Some(RpcApiError { error: Error::InvalidMethod }.into()), + }; + }; + let result = method(clients.clone(), id.clone(), data).await; + RpcApiResponse { + id: Some(id), + response: Some(result), + } + }, + } + } else { + RpcApiResponse { + id: None, + response: Some(RpcApiError { error: Error::InvalidMethod }.into()), + } + } +} + +pub fn serialize(value: &T) -> Result, rmp_serde::encode::Error> { + let mut buf = Vec::new(); + value.serialize(&mut Serializer::new(&mut buf).with_struct_map())?; + Ok(buf) +} + +pub fn deserialize Deserialize<'a>>(buf: &[u8]) -> Result { + let mut deserializer = Deserializer::new(buf); + Deserialize::deserialize(&mut deserializer) +} diff --git a/crates/rapid/src/utilities.rs b/crates/rapid/src/utilities.rs new file mode 100644 index 0000000..ead8f2a --- /dev/null +++ b/crates/rapid/src/utilities.rs @@ -0,0 +1,46 @@ +use rand::{rngs::StdRng, Rng, SeedableRng}; + +pub fn random_number(size: usize) -> Vec { + let mut rng = StdRng::from_entropy(); + let mut result: Vec = vec![0; size]; + rng.fill(&mut result[..]); + result +} + +pub fn generate(alphabet: &[char], size: usize) -> String { + assert!( + alphabet.len() <= u8::MAX as usize, + "The alphabet cannot be longer than a `u8` (to comply with the `random` function)" + ); + let mask = alphabet.len().next_power_of_two() - 1; + let step: usize = 8 * size / 5; + let mut id = String::with_capacity(size); + loop { + let bytes = random_number(step); + for &byte in &bytes { + let byte = byte as usize & mask; + if alphabet.len() > byte { + id.push(alphabet[byte]); + if id.len() == size { + return id; + } + } + } + } +} + +pub fn generate_id() -> String { + const ALPHABET: &[char] = &[ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', + 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + ]; + const LENGTH: usize = 10; + generate(ALPHABET, LENGTH) +} + +use lazy_static::lazy_static; + +lazy_static! { + pub static ref HEARTBEAT_INTERVAL: u64 = 30000; + pub static ref HEARTBEAT_TIMEOUT: u64 = 60000; +} diff --git a/src/globals.rs b/src/globals.rs deleted file mode 100644 index a06073c..0000000 --- a/src/globals.rs +++ /dev/null @@ -1,6 +0,0 @@ -use lazy_static::lazy_static; - -lazy_static! { - pub static ref HEARTBEAT_INTERVAL: u64 = 30000; - pub static ref HEARTBEAT_TIMEOUT: u64 = 60000; -} diff --git a/src/methods/authentication.rs b/src/methods/authentication.rs deleted file mode 100644 index 385e802..0000000 --- a/src/methods/authentication.rs +++ /dev/null @@ -1,139 +0,0 @@ -use std::collections::HashSet; -use std::sync::Arc; - -use async_trait::async_trait; -use dashmap::DashMap; -use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; -use serde::{Deserialize, Serialize}; - -use crate::errors::{Error, Result}; -use crate::methods::Response; -use crate::services::database::users::User; -use crate::services::encryption::generate_id; -use crate::services::environment::JWT_SECRET; -use crate::services::socket::RpcClient; - -use super::Respond; - -#[derive(Deserialize)] -struct UserJwt { - // TODO: Find the other properties - id: String, - issued_at: u128, - expires_at: u128, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct IdentifyMethod { - pub public_key: Vec, - pub token: String, -} - -// Important: This only accepts a token and will not sign a token. -// The token is to be obtained from a separate login server -// (e.g. SSO system) -#[async_trait] -impl Respond for IdentifyMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - println!("Public key: {:?}", self.public_key); - println!("Token: {:?}", self.token); - let mut validation = Validation::new(Algorithm::HS256); - validation.required_spec_claims = HashSet::new(); - validation.validate_exp = false; - let token_message = decode::( - &self.token, - &DecodingKey::from_secret(JWT_SECRET.as_ref()), - &validation, - ) - .map_err(|_| Error::InvalidToken)?; - let time = chrono::Utc::now().timestamp_millis() as u128; - if time > token_message.claims.expires_at { - return Err(Error::InvalidToken); - } - let mut client = clients.get_mut(&id).unwrap(); - let user = User::get(&token_message.claims.id).await; - let user = if let Err(Error::NotFound) = user { - User::create(token_message.claims.id).await? - } else { - user? - }; - client.user = Some(Arc::new(user)); - Ok(Response::Identify(IdentifyResponse { success: true })) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct IdentifyResponse { - pub success: bool, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct HeartbeatMethod {} - -#[async_trait] -impl Respond for HeartbeatMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - let client = clients.get(&id).unwrap(); - client.heartbeat_tx.send(()).await.unwrap(); - Ok(Response::Heartbeat(HeartbeatResponse { ack: true })) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct HeartbeatResponse { - ack: bool, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetIdMethod {} - -#[async_trait] -impl Respond for GetIdMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - let mut client = clients.get_mut(&id).unwrap(); - let mut new_request_ids = Vec::new(); - for _ in 0..20 { - let id = generate_id(); - client.request_ids.push(id.clone()); - new_request_ids.push(id); - } - Ok(Response::GetId(GetIdResponse { - request_ids: new_request_ids, - })) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetIdResponse { - pub request_ids: Vec, -} - -pub fn check_authenticated( - clients: Arc>, - id: &str, -) -> Result> { - let client = clients.get(id).expect("Failed to get client"); - if let Some(x) = &client.user { - Ok(x.clone()) - } else { - Err(Error::NotAuthenticated) - } -} diff --git a/src/methods/invites.rs b/src/methods/invites.rs deleted file mode 100644 index ff3bc78..0000000 --- a/src/methods/invites.rs +++ /dev/null @@ -1,242 +0,0 @@ -use std::sync::Arc; - -use async_trait::async_trait; -use dashmap::DashMap; -use serde::{Deserialize, Serialize}; - -use crate::{ - errors::{Error, Result}, - services::{ - database::{ - channels::Channel, - infractions::is_banned, - invites::{get_invites, Invite}, - members::Member, - spaces::Space, - }, - permissions::Permission, - socket::RpcClient, - }, -}; - -use super::{authentication::check_authenticated, Respond, Response}; - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct CreateInviteMethod { - channel_id: String, - max_uses: Option, - expires_at: Option, - authorized_users: Option>, - space_id: Option, - scope_id: Option, -} - -#[async_trait] -impl Respond for CreateInviteMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - let user = check_authenticated(clients, &id)?; - let invite = Invite::create( - self.channel_id.clone(), - user.id.clone(), - self.expires_at, - self.max_uses, - self.authorized_users.clone(), - self.space_id.clone(), - self.scope_id.clone(), - ) - .await?; - Ok(Response::CreateInvite(CreateInviteResponse { invite })) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct CreateInviteResponse { - invite: Invite, -} - -// pub struct UpdateInviteMethod { -// code: String, -// max_uses: Option, -// expires_at: Option, -// } - -// #[async_trait] -// impl Respond for UpdateInviteMethod { -// async fn respond(&self, clients: Arc>, id: String) -> Response { -// let client = clients.get(&id.clone()).unwrap(); -// let member = get_member(id).await; -// match member { -// Ok(member) => { -// let permissions = permissions_for(member).await; -// if !permissions.has_permission(Permission::ManageInvites) { -// return Response::Error(ErrorResponse { error: "You do not have permission to manage invites".to_string() }); -// } else { - -// } -// }, -// Err(error) => Response::Error(ErrorResponse { error }) -// } -// } -// } - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct DeleteInviteMethod { - id: String, - space_id: String, -} - -#[async_trait] -impl Respond for DeleteInviteMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - let user = check_authenticated(clients, &id)?; - let member = Member::get(&user.id, &self.space_id).await?; - let permissions = member.get_permissions().await?; - if !permissions.has_permission(Permission::ManageInvites) { - return Err(Error::MissingPermission { - permission: Permission::ManageInvites, - }); - } else { - let invite = Invite::get(&self.id).await?; - invite.delete().await?; - Ok(Response::DeleteInvite(DeleteInviteResponse {})) - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct DeleteInviteResponse {} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetInviteMethod { - code: String, -} - -#[async_trait] -impl Respond for GetInviteMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - let user = check_authenticated(clients, &id)?; - let invite = Invite::get(&self.code).await?; - if let Some(space_id) = invite.space_id { - let space = Space::get(&space_id).await?; - let banned = is_banned(user.id.clone(), space.id).await?; - Ok(Response::GetInvite(GetInviteResponse { - invite: InviteInformation::Space { - name: space.name, - description: space.description, - inviter_id: invite.creator, - banned, - authorized: invite - .authorized_users - .unwrap_or_else(|| vec![user.id.clone()]) - .contains(&user.id), - member_count: space.members.len() as i32, - }, - })) - } else { - let channel = Channel::get(&invite.channel_id).await?; - if let Channel::GroupChannel { - name, - description, - members, - .. - } = channel - { - Ok(Response::GetInvite(GetInviteResponse { - invite: InviteInformation::Group { - name, - description, - inviter_id: invite.creator, - authorized: invite - .authorized_users - .unwrap_or_else(|| vec![user.id.clone()]) - .contains(&user.id), - member_count: members.len() as i32, - }, - })) - } else { - Err(Error::InvalidInvite) - } - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")] -pub enum InviteInformation { - #[serde(rename_all = "camelCase")] - Group { - name: String, - description: String, - inviter_id: String, - authorized: bool, - member_count: i32, - }, - #[serde(rename_all = "camelCase")] - Space { - name: String, - description: String, - inviter_id: String, - banned: bool, - authorized: bool, - member_count: i32, - }, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetInviteResponse { - #[serde(flatten)] - invite: InviteInformation, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetInvitesMethod { - channel_id: String, - space_id: Option, - scope_id: Option, -} - -#[async_trait] -impl Respond for GetInvitesMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - let user = check_authenticated(clients, &id)?; - if let Some(space_id) = &self.space_id { - let member = Member::get(&user.id, space_id).await?; - let permissions = member.get_permissions().await?; - if !permissions.has_permission(Permission::ManageInvites) { - return Err(Error::MissingPermission { - permission: Permission::ManageInvites, - }); - } - } - let invites = get_invites(self.channel_id.clone(), self.space_id.clone()).await?; - Ok(Response::GetInvites(GetInvitesResponse { invites })) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetInvitesResponse { - invites: Vec, -} - -// TODO: Invite manager built-in diff --git a/src/methods/messages.rs b/src/methods/messages.rs deleted file mode 100644 index c4b404a..0000000 --- a/src/methods/messages.rs +++ /dev/null @@ -1,117 +0,0 @@ -use std::{sync::Arc, time::Duration}; - -use async_std::future; -use async_trait::async_trait; -use dashmap::DashMap; -use rmp_serde::Serializer; -use serde::{Deserialize, Serialize}; - -use crate::{ - errors::{Error, Result}, - services::{ - database::{channels::Channel, messages::Message}, - socket::RpcClient, - }, -}; - -use super::{Event, NewMessageEvent, Respond, Response, RpcApiEvent}; - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetMessagesMethod { - channel_id: String, - limit: Option, - latest: Option, - before: Option, - after: Option, -} - -#[async_trait] -impl Respond for GetMessagesMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - super::authentication::check_authenticated(clients, &id)?; - let channel = Channel::get(&self.channel_id).await?; - let messages = channel - .get_messages( - self.limit, - self.latest, - self.before.clone(), - self.after.clone(), - ) - .await?; - Ok(Response::GetMessages(GetMessagesResponse { messages })) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetMessagesResponse { - messages: Vec, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SendMessageMethod { - channel_id: String, - content: String, -} - -#[async_trait] -impl Respond for SendMessageMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - let user = super::authentication::check_authenticated(clients.clone(), &id)?; - let trimmed = self.content.trim(); - if trimmed.len() > 4096 { - return Err(Error::MessageTooLong); - } - if trimmed.is_empty() { - return Err(Error::MessageEmpty); - } - let message = - Message::create(self.channel_id.clone(), user.id.clone(), trimmed.to_owned()).await?; - for x in clients.clone().iter_mut() { - // TODO: Check if user is in channel - if let Some(u) = &x.user { - println!("Sending message to {}", u.id); - let mut value_buffer = Vec::new(); - let value = RpcApiEvent { - event: Event::NewMessage(NewMessageEvent { - message: message.clone(), - channel_id: self.channel_id.clone(), - }), - }; - value - .serialize(&mut Serializer::new(&mut value_buffer).with_struct_map()) - .unwrap(); - println!("Serialized"); - let y = x.socket.clone(); - future::timeout(Duration::from_millis(5000), async move { - y.send(async_tungstenite::tungstenite::Message::Binary( - value_buffer, - )) - .await - }) - .await - .unwrap_or(Ok(())) - .unwrap_or_else(|e| println!("{e:?}")); - } - } - Ok(Response::SendMessage(SendMessageResponse { - message_id: message.id, - })) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SendMessageResponse { - message_id: String, -} diff --git a/src/methods/mod.rs b/src/methods/mod.rs deleted file mode 100644 index 6781cec..0000000 --- a/src/methods/mod.rs +++ /dev/null @@ -1,276 +0,0 @@ -use std::sync::Arc; - -use async_trait::async_trait; -use dashmap::DashMap; -use serde::{Deserialize, Serialize}; - -use crate::{ - errors::{Error, Result}, - services::{database::messages::Message, socket::RpcClient}, -}; - -use self::{ - authentication::{ - GetIdMethod, GetIdResponse, HeartbeatMethod, HeartbeatResponse, IdentifyMethod, - IdentifyResponse, - }, - channels::{GetChannelMethod, GetChannelResponse, GetChannelsMethod, GetChannelsResponse}, - invites::{ - CreateInviteMethod, CreateInviteResponse, DeleteInviteMethod, DeleteInviteResponse, - GetInviteMethod, GetInviteResponse, GetInvitesMethod, GetInvitesResponse, - }, - messages::{GetMessagesMethod, GetMessagesResponse, SendMessageMethod, SendMessageResponse}, - roles::{ - CreateRoleMethod, CreateRoleResponse, DeleteRoleMethod, DeleteRoleResponse, EditRoleMethod, - EditRoleResponse, - }, - spaces::{ - CreateSpaceMethod, CreateSpaceResponse, DeleteSpaceMethod, DeleteSpaceResponse, - EditSpaceMethod, EditSpaceResponse, GetSpaceMethod, GetSpaceResponse, GetSpacesResponse, - JoinSpaceResponse, LeaveSpaceResponse, - }, - webrtc::{ - EndCallMethod, EndCallResponse, JoinCallMethod, JoinCallResponse, LeaveCallMethod, - LeaveCallResponse, StartCallMethod, StartCallResponse, - }, -}; - -pub mod authentication; -pub mod channels; -pub mod events; -pub mod invites; -pub mod messages; -pub mod roles; -pub mod spaces; -pub mod users; -pub mod webrtc; - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(tag = "type", content = "data", rename_all = "SCREAMING_SNAKE_CASE")] -#[repr(i8)] -pub enum Method { - Identify(IdentifyMethod) = 1, - Heartbeat(HeartbeatMethod) = 2, - GetId(GetIdMethod) = 3, - - // WebRTC: 10-19 - StartCall(StartCallMethod) = 10, - JoinCall(JoinCallMethod) = 11, - LeaveCall(LeaveCallMethod) = 12, - EndCall(EndCallMethod) = 13, - - GetMessages(GetMessagesMethod) = 20, - SendMessage(SendMessageMethod) = 22, - - GetChannel(GetChannelMethod) = 30, - GetChannels(GetChannelsMethod) = 31, - // CreateChannel(CreateChannelMethod) = 32, - // EditChannel(EditChannelMethod) = 33, - // DeleteChannel(DeleteChannelMethod) = 34, - GetSpace(GetSpaceMethod) = 40, - CreateSpace(CreateSpaceMethod) = 41, - EditSpace(EditSpaceMethod) = 42, - DeleteSpace(DeleteSpaceMethod) = 43, - - // AddFriend(AddFriendMethod) = 50, - // RemoveFriend(RemoveFriendMethod) = 51, - // GetFriends(GetFriendsMethod) = 52, - // GetFriendRequests(GetFriendRequestsMethod) = 53, - // AcknowledgeFriendRequest(AcknowledgeFriendRequestMethod) = 55, - CreateInvite(CreateInviteMethod) = 60, - DeleteInvite(DeleteInviteMethod) = 61, - GetInvite(GetInviteMethod) = 62, - GetInvites(GetInvitesMethod) = 63, - - CreateRole(CreateRoleMethod) = 70, - EditRole(EditRoleMethod) = 71, - DeleteRole(DeleteRoleMethod) = 72, - // GetRoles(GetRolesMethod) = 73, -} - -#[async_trait] -pub trait Respond { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result; -} - -pub fn get_respond(m: Method) -> Box { - match m { - Method::Identify(m) => Box::new(m), - Method::Heartbeat(m) => Box::new(m), - Method::GetId(m) => Box::new(m), - Method::StartCall(m) => Box::new(m), - Method::JoinCall(m) => Box::new(m), - Method::LeaveCall(m) => Box::new(m), - Method::EndCall(m) => Box::new(m), - Method::GetMessages(m) => Box::new(m), - Method::SendMessage(m) => Box::new(m), - Method::GetChannel(m) => Box::new(m), - Method::GetChannels(m) => Box::new(m), - // Method::CreateChannel(m) => m, - // Method::EditChannel(m) => m, - // Method::DeleteChannel(m) => m, - Method::GetSpace(m) => Box::new(m), - Method::CreateSpace(m) => Box::new(m), - Method::EditSpace(m) => Box::new(m), - Method::DeleteSpace(m) => Box::new(m), - // Method::AddFriend(m) => m, - // Method::RemoveFriend(m) => m, - // Method::GetFriends(m) => m, - // Method::GetFriendRequests(m) => m, - // Method::AcknowledgeFriendRequest(m) => m, - Method::CreateInvite(m) => Box::new(m), - Method::CreateRole(m) => Box::new(m), - Method::EditRole(m) => Box::new(m), - Method::DeleteRole(m) => Box::new(m), - Method::DeleteInvite(m) => Box::new(m), - Method::GetInvite(m) => Box::new(m), - Method::GetInvites(m) => Box::new(m), - } -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct RpcApiMethod { - pub(crate) id: Option, - #[serde(flatten)] - pub(crate) method: Method, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct AddFriendMethod { - channel_id: String, - friend_id: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct RemoveFriendMethod { - channel_id: String, - friend_id: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetFriendsMethod {} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetFriendRequestsMethod {} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct AcknowledgeFriendRequestMethod { - channel_id: String, - friend_id: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[repr(i8)] -#[serde(tag = "type", content = "data", rename_all = "SCREAMING_SNAKE_CASE")] -pub enum Response { - Identify(IdentifyResponse) = 1, - Heartbeat(HeartbeatResponse) = 2, - GetId(GetIdResponse) = 3, - - // WebRTC: 10-19 - StartCall(StartCallResponse) = 10, - JoinCall(JoinCallResponse) = 11, - LeaveCall(LeaveCallResponse) = 12, - EndCall(EndCallResponse) = 13, - - GetMessages(GetMessagesResponse) = 20, - SendMessage(SendMessageResponse) = 22, - - GetChannel(GetChannelResponse) = 30, - GetChannels(GetChannelsResponse) = 31, - // CreateChannel(CreateChannelResponse) = 32, - // EditChannel(EditChannelResponse) = 33, - // DeleteChannel(DeleteChannelResponse) = 34, - GetSpace(GetSpaceResponse) = 40, - CreateSpace(CreateSpaceResponse) = 41, - EditSpace(EditSpaceResponse) = 42, - DeleteSpace(DeleteSpaceResponse) = 43, - JoinSpace(JoinSpaceResponse) = 44, - LeaveSpace(LeaveSpaceResponse) = 45, - GetSpaces(GetSpacesResponse) = 46, - - CreateInvite(CreateInviteResponse) = 60, - DeleteInvite(DeleteInviteResponse) = 61, - GetInvite(GetInviteResponse) = 62, - GetInvites(GetInvitesResponse) = 63, - - CreateRole(CreateRoleResponse) = 70, - EditRole(EditRoleResponse) = 71, - DeleteRole(DeleteRoleResponse) = 72, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct RpcApiResponse { - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) id: Option, - #[serde(flatten)] - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) response: Option, - #[serde(flatten)] - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) error: Option, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[repr(i8)] -#[serde(tag = "type", content = "data", rename_all = "SCREAMING_SNAKE_CASE")] -pub enum Event { - Hello(HelloEvent) = 0, - - // WebRTC: 10-19 - NewMessage(NewMessageEvent) = 21, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct RpcApiEvent { - #[serde(flatten)] - pub(crate) event: Event, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct HelloEvent { - pub(crate) public_key: Vec, - pub(crate) request_ids: Vec, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct NewMessageEvent { - message: Message, - channel_id: String, -} - -pub enum CreateChannelType { - PrivateChannel { - peer_id: String, - scope_id: String, - }, - GroupChannel { - name: String, - description: String, - members: Vec, - scope_id: String, - }, - InformationChannel { - name: String, - description: String, - nexus_id: String, - scope_id: String, - }, - TextChannel { - name: String, - description: String, - nexus_id: String, - scope_id: String, - }, -} diff --git a/src/methods/roles.rs b/src/methods/roles.rs deleted file mode 100644 index f6808a7..0000000 --- a/src/methods/roles.rs +++ /dev/null @@ -1,131 +0,0 @@ -use std::sync::Arc; - -use async_trait::async_trait; -use dashmap::DashMap; -use serde::{Deserialize, Serialize}; - -use crate::{ - errors::{Error, Result}, - services::{ - database::{ - members::Member, - roles::{Color, Role}, - spaces::Space, - }, - permissions::{can_modify_role, Permission}, - socket::RpcClient, - }, -}; - -use super::{Respond, Response}; - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateRoleMethod { - name: String, - permissions: i64, - color: Color, - space_id: String, -} - -#[async_trait] -impl Respond for CreateRoleMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - super::authentication::check_authenticated(clients, &id)?; - let space = Space::get(&self.space_id).await?; - let role = Role::create( - &space, - self.name.clone(), - self.permissions, - self.color.clone(), - ) - .await?; - Ok(Response::CreateRole(CreateRoleResponse { role })) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateRoleResponse { - role: Role, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EditRoleMethod { - id: String, - name: String, - permissions: i64, - color: Color, - space_id: String, - scope_id: Option, -} - -#[async_trait] -impl Respond for EditRoleMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - super::authentication::check_authenticated(clients, &id)?; - let role = Role::get(&self.id).await?; - if role.space_id != self.space_id { - return Err(Error::NotFound); - } - if let Some(scope_id) = &self.scope_id { - if role.scope_id != scope_id.clone() { - return Err(Error::NotFound); - } - } - let member = Member::get(&id, &self.space_id).await?; - let can_modify = can_modify_role(&member, &role).await?; - if !can_modify { - return Err(Error::MissingPermission { - permission: Permission::ManageRoles, - }); - } - let role = role - .update(self.name.clone(), self.permissions, self.color.clone()) - .await?; - Ok(Response::EditRole(EditRoleResponse { role })) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EditRoleResponse { - role: Role, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct DeleteRoleMethod { - id: String, -} - -#[async_trait] -impl Respond for DeleteRoleMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - super::authentication::check_authenticated(clients, &id)?; - let role = Role::get(&self.id).await?; - role.delete().await?; - Ok(Response::DeleteRole(DeleteRoleResponse { - id: self.id.clone(), - })) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct DeleteRoleResponse { - id: String, -} diff --git a/src/methods/spaces.rs b/src/methods/spaces.rs deleted file mode 100644 index 72b98e5..0000000 --- a/src/methods/spaces.rs +++ /dev/null @@ -1,230 +0,0 @@ -use std::sync::Arc; - -use async_trait::async_trait; -use dashmap::DashMap; -use serde::{Deserialize, Serialize}; - -use crate::{ - errors::{Error, Result}, - services::{database::spaces::Space, socket::RpcClient}, -}; - -use super::{Respond, Response}; - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetSpaceMethod { - space_id: String, -} - -#[async_trait] -impl Respond for GetSpaceMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - let user = super::authentication::check_authenticated(clients, &id)?; - let space = Space::get(&self.space_id).await?; - let user_in_space = user.in_space(&self.space_id).await?; - if !user_in_space { - return Err(Error::NotFound); - } - Ok(Response::GetSpace(GetSpaceResponse { space })) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetSpaceResponse { - space: Space, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateSpaceMethod { - // space: Space, - name: String, - description: Option, - scope: Option, -} - -#[async_trait] -impl Respond for CreateSpaceMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - let user = super::authentication::check_authenticated(clients, &id)?; - let trimmed = self.name.trim(); - if trimmed.len() > 32 { - return Err(Error::NameTooLong); - } - if trimmed.is_empty() { - return Err(Error::NameEmpty); - } - let space = Space::create( - self.name.clone(), - self.description.clone(), - user.id.clone(), - self.scope.clone(), - ) - .await?; - Ok(Response::CreateSpace(CreateSpaceResponse { space })) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateSpaceResponse { - space: Space, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct JoinSpaceMethod { - code: String, -} - -#[async_trait] -impl Respond for JoinSpaceMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - let user = super::authentication::check_authenticated(clients, &id)?; - let space = user.accept_invite(&self.code).await?; - space.add_member(&id).await?; - Ok(Response::JoinSpace(JoinSpaceResponse { space })) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct JoinSpaceResponse { - space: Space, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct LeaveSpaceMethod { - space_id: String, -} - -#[async_trait] -impl Respond for LeaveSpaceMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - let user = super::authentication::check_authenticated(clients, &id)?; - let user_in_space = user.in_space(&self.space_id).await?; - if !user_in_space { - return Err(Error::NotFound); - } - let space = Space::get(&self.space_id).await?; - space.remove_member(&id).await?; - Ok(Response::LeaveSpace(LeaveSpaceResponse { - space_id: self.space_id.clone(), - })) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct LeaveSpaceResponse { - space_id: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetSpacesMethod {} - -#[async_trait] -impl Respond for GetSpacesMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - let user = super::authentication::check_authenticated(clients, &id)?; - let spaces = user.get_spaces().await?; - Ok(Response::GetSpaces(GetSpacesResponse { spaces })) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetSpacesResponse { - spaces: Vec, -} - -// FIXME: permissions - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EditSpaceMethod { - space_id: String, - name: Option, - description: Option, - base_permissions: Option, -} -// TODO: logger -#[async_trait] -impl Respond for EditSpaceMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - super::authentication::check_authenticated(clients, &id)?; - let space = Space::get(&self.space_id).await?; - let space = space - .update( - self.name.clone(), - self.description.clone(), - self.base_permissions, - ) - .await?; - Ok(Response::EditSpace(EditSpaceResponse { space })) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EditSpaceResponse { - space: Space, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct DeleteSpaceMethod { - space_id: String, -} - -#[async_trait] -impl Respond for DeleteSpaceMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - super::authentication::check_authenticated(clients, &id)?; - let space = Space::get(&self.space_id).await?; - space.delete().await?; - Ok(Response::DeleteSpace(DeleteSpaceResponse { - id: self.space_id.clone(), - })) - } -} - -// FIXME: sudo mode - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct DeleteSpaceResponse { - id: String, -} diff --git a/src/methods/webrtc.rs b/src/methods/webrtc.rs deleted file mode 100644 index 681554d..0000000 --- a/src/methods/webrtc.rs +++ /dev/null @@ -1,193 +0,0 @@ -use std::sync::Arc; - -use async_trait::async_trait; -use dashmap::DashMap; -use serde::{Deserialize, Serialize}; - -use crate::errors::{Error, Result}; -use crate::services::database::members::Member; -use crate::services::database::spaces::Space; -use crate::services::permissions::Permission; -use crate::services::socket::RpcClient; -use crate::services::webrtc::ActiveCall; - -use super::{Respond, Response}; - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct JoinCallMethod { - id: String, - space_id: Option, - sdp: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct JoinCallResponse { - sdp: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct RtcAuthorization { - channel_id: String, - user_id: String, - space_id: Option, -} - -#[async_trait] -impl Respond for JoinCallMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - super::authentication::check_authenticated(clients, &id)?; // TODO: check rate limit, permissions req'd - if let Some(_space_id) = &self.space_id { - // let space = Space::get(space_id).await?; - // if !space.members.contains(&id) { - // return Err(Error::NotFound); // unauthorized - // } - // let member = Member::get(&id, &space.id).await?; - // let channel = space.get_channel(&self.id).await?; - // let permission = member - // .get_permission_in_channel(&channel, Permission::JoinCalls) - // .await?; - // if !permission { - // return Err(Error::MissingPermission { - // permission: Permission::JoinCalls, - // }); - // } - // let call = ActiveCall::get_in_channel(space_id, &self.id).await?; - // if let Some(mut call) = call { - // call.join_user(id.clone()).await?; - // let token = call.get_token(&id).await?; - // Ok(Response::JoinCall(JoinCallResponse { token })) - // } else { - // Err(Error::NotFound) - // } - Err(Error::NoVoiceNodesAvailable) - } else { - Err(Error::Unimplemented) - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct StartCallMethod { - id: String, - space_id: Option, -} - -#[async_trait] -impl Respond for StartCallMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - super::authentication::check_authenticated(clients, &id)?; - if let Some(space_id) = &self.space_id { - let space = Space::get(space_id).await?; - if !space.members.contains(&id) { - return Err(Error::NotFound); - } - let member = Member::get(&id, &space.id).await?; - let channel = space.get_channel(&self.id).await?; - let permission = member - .get_permission_in_channel(&channel, Permission::StartCalls) - .await?; - if !permission { - return Err(Error::MissingPermission { - permission: Permission::StartCalls, - }); - } - let call = ActiveCall::create(space_id, &self.id, &id).await?; - let token = call.get_token(&id).await?; - Ok(Response::StartCall(StartCallResponse { token })) - } else { - Err(Error::Unimplemented) - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct StartCallResponse { - token: String, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct EndCallMethod { - id: String, - space_id: Option, -} - -#[async_trait] -impl Respond for EndCallMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - super::authentication::check_authenticated(clients, &id)?; - if let Some(space_id) = &self.space_id { - let space = Space::get(space_id).await?; - if !space.members.contains(&id) { - return Err(Error::NotFound); - } - let member = Member::get(&id, &space.id).await?; - let channel = space.get_channel(&self.id).await?; - let permission = member - .get_permission_in_channel(&channel, Permission::ManageCalls) - .await?; - if !permission { - return Err(Error::MissingPermission { - permission: Permission::ManageCalls, - }); - } - let call = ActiveCall::get_in_channel(space_id, &self.id).await?; - if let Some(call) = call { - call.end().await?; - Ok(Response::EndCall(EndCallResponse {})) - } else { - Err(Error::NotFound) - } - } else { - Err(Error::Unimplemented) - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct EndCallResponse {} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct LeaveCallMethod { - id: String, - space_id: Option, -} - -#[async_trait] -impl Respond for LeaveCallMethod { - async fn respond( - &self, - clients: Arc>, - id: String, - ) -> Result { - super::authentication::check_authenticated(clients, &id)?; - if let Some(space_id) = &self.space_id { - let call = ActiveCall::get_in_channel(space_id, &self.id).await?; - if let Some(mut call) = call { - if call.members.contains(&id) { - return Err(Error::NotFound); - } - call.leave_user(&id.clone()).await?; - Ok(Response::LeaveCall(LeaveCallResponse {})) - } else { - Err(Error::NotFound) - } - } else { - Err(Error::Unimplemented) - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct LeaveCallResponse {} diff --git a/src/services/socket.rs b/src/services/socket.rs deleted file mode 100644 index c95b5e1..0000000 --- a/src/services/socket.rs +++ /dev/null @@ -1,195 +0,0 @@ -use std::sync::Arc; - -use async_std::{ - channel::{unbounded, Sender}, - future, - net::{TcpListener, TcpStream}, - task::spawn, -}; -use async_tungstenite::{accept_async, tungstenite::Message}; -use dashmap::DashMap; -use futures_util::{SinkExt, StreamExt}; -use rand::rngs::OsRng; -use rmp_serde::{Deserializer, Serializer}; -use serde::{Deserialize, Serialize}; -use x25519_dalek::{EphemeralSecret, PublicKey}; - -use crate::{ - errors::Error, - globals::HEARTBEAT_TIMEOUT, - methods::{get_respond, Event, HelloEvent, RpcApiEvent, RpcApiMethod, RpcApiResponse}, - services::encryption::generate_id, -}; - -use super::{database::users::User, environment::LISTEN_ADDRESS}; - -#[derive(Clone)] -pub struct RpcClient { - pub id: String, - pub socket: Arc>, - pub user: Option>, - pub request_ids: Vec, - pub heartbeat_tx: Arc>, -} - -pub async fn start_server() { - let server = TcpListener::bind(LISTEN_ADDRESS.to_owned()).await.unwrap(); - let clients: Arc> = Arc::new(DashMap::new()); - let mut incoming = server.incoming(); - while let Some(stream) = incoming.next().await { - let clients = clients.clone(); - spawn(async move { start_client(stream, clients).await }); - } -} - -async fn start_client( - stream: Result, - clients: Arc>, -) { - let connection = stream.unwrap(); - println!("Socket connected: {}", connection.peer_addr().unwrap()); - let ws_stream = accept_async(connection).await.expect("Failed to accept"); - let (mut write, mut read) = ws_stream.split(); - let (s, r) = unbounded::(); - spawn(async move { - while let Ok(msg) = r.recv().await { - write.send(msg).await.expect("Failed to send message"); - } - write.close().await.expect("Failed to close"); - }); - let id = generate_id(); - let mut request_ids = Vec::new(); - for _ in 0..20 { - request_ids.push(generate_id()); - } - let secret = EphemeralSecret::random_from_rng(OsRng); - let public_key = PublicKey::from(&secret); - let val = RpcApiEvent { - event: Event::Hello(HelloEvent { - public_key: public_key.to_bytes().to_vec(), - request_ids: request_ids.clone(), - }), - }; - s.send(Message::Binary( - serialize(&val).expect("Failed to serialize"), - )) - .await - .expect("Failed to send message"); - - let (tx, rx) = unbounded::<()>(); - let clients_moved = clients.clone(); - let id_moved = id.clone(); - spawn(async move { - while future::timeout( - std::time::Duration::from_millis(*HEARTBEAT_TIMEOUT), - rx.recv(), - ) - .await - .is_ok() - {} - if let Some((_, client)) = clients_moved.remove(&id_moved) { - client.socket.close(); - } - }); - let client = RpcClient { - id: id.clone(), - socket: Arc::new(s), - user: None, - request_ids, - heartbeat_tx: Arc::new(tx), - }; - clients.insert(id.clone(), client); - while let Some(data) = read.next().await { - match data.unwrap() { - Message::Binary(bin) => { - println!("Received binary data"); - let response = handle_packet(bin, &clients, &id).await; - let client = clients.get(&id.clone()).unwrap(); - client - .socket - .send(Message::Binary( - serialize(&response).expect("Failed to serialize"), - )) - .await - .expect("Failed to send message"); - } - Message::Ping(bin) => { - println!("Received ping"); - let client = clients.get(&id.clone()).unwrap(); - client.socket.send(Message::Pong(bin)).await.unwrap(); - } - Message::Close(_) => { - println!("Received close"); - } - _ => { - println!("Received unknown message"); - if let Some((_, client)) = clients.remove(&id.clone()) { - client.socket.close(); - } - } - } - } -} - -pub async fn handle_packet( - bin: Vec, - clients: &Arc>, - id: &String, -) -> RpcApiResponse { - let result = deserialize::(bin.as_slice()); - if let Ok(r) = result { - println!("Received: {:?}", r.method); - if let Some(request_id) = r.id { - let mut client = clients.get_mut(id).unwrap(); - if client.request_ids.contains(&request_id) { - client.request_ids.retain(|x| x != &request_id); - } else { - return RpcApiResponse { - id: None, - response: None, - error: Some(Error::InvalidRequestId), - }; - } - drop(client); - let dispatch = get_respond(r.method) - .respond(clients.clone(), id.clone()) - .await; - if let Ok(dispatch) = dispatch { - RpcApiResponse { - id: Some(request_id), - response: Some(dispatch), - error: None, - } - } else { - RpcApiResponse { - id: Some(request_id), - response: None, - error: Some(dispatch.unwrap_err()), - } - } - } else { - RpcApiResponse { - id: None, - response: None, - error: Some(Error::InvalidRequestId), - } - } - } else { - RpcApiResponse { - id: None, - response: None, - error: Some(Error::InvalidMethod), - } - } -} - -pub fn serialize(value: &T) -> Result, rmp_serde::encode::Error> { - let mut buf = Vec::new(); - value.serialize(&mut Serializer::new(&mut buf).with_struct_map())?; - Ok(buf) -} - -pub fn deserialize Deserialize<'a>>(buf: &[u8]) -> Result { - let mut deserializer = Deserializer::new(buf); - Deserialize::deserialize(&mut deserializer) -}