diff --git a/.gitignore b/.gitignore index 8483bc435..1e8bce961 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,6 @@ venv/ .vercel .DS_Store -.idea \ No newline at end of file +livekit.yml +.idea +start \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 05033c65a..da5066d70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" @@ -87,9 +87,12 @@ dependencies = [ [[package]] name = "aligned-vec" -version = "0.5.0" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] [[package]] name = "allocator-api2" @@ -166,7 +169,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -311,7 +314,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -379,7 +382,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -396,7 +399,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -421,9 +424,9 @@ checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" [[package]] name = "atomic" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" dependencies = [ "bytemuck", ] @@ -471,7 +474,7 @@ dependencies = [ "revolt_rocket_okapi", "rocket", "rust-argon2", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "sha1", @@ -492,14 +495,14 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" dependencies = [ - "autocfg 1.4.0", + "autocfg 1.5.0", ] [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "av1-grain" @@ -517,18 +520,18 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" +checksum = "19135c0c7a60bfee564dbe44ab5ce0557c6bf3884e5291a50be76a15640c4fbd" dependencies = [ "arrayvec", ] [[package]] name = "aws-config" -version = "1.6.3" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02a18fd934af6ae7ca52410d4548b98eb895aab0f1ea417d168d85db1434a141" +checksum = "c18d005c70d2b9c0c1ea8876c039db0ec7fb71164d25c73ccea21bf41fd02171" dependencies = [ "aws-credential-types", "aws-runtime", @@ -591,9 +594,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.7" +version = "1.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c4063282c69991e57faab9e5cb21ae557e59f5b0fb285c196335243df8dc25c" +checksum = "4f6c68419d8ba16d9a7463671593c54f81ba58cab466e9b759418da606dcc2e2" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -616,9 +619,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.91.0" +version = "1.94.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10c7d58f9c99e7d33e5a9b288ec84db24de046add7ba4c1e98baf6b3a5b37fde" +checksum = "cc24d9d761bd464534d9e477a9f724c118ca2557a95444097cf6ce71f3229a72" dependencies = [ "aws-credential-types", "aws-runtime", @@ -650,9 +653,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.72.0" +version = "1.74.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13118ad30741222f67b1a18e5071385863914da05124652b38e172d6d3d9ce31" +checksum = "e0a69de9c1b9272da2872af60c7402683e7f45c06267735b4332deacb203239b" dependencies = [ "aws-credential-types", "aws-runtime", @@ -672,9 +675,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.73.0" +version = "1.75.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f879a8572b4683a8f84f781695bebf2f25cf11a81a2693c31fc0e0215c2c1726" +checksum = "f0b161d836fac72bdd5ac1a4cd1cdc38ab888c7af26cfd95f661be4409505e63" dependencies = [ "aws-credential-types", "aws-runtime", @@ -694,9 +697,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.73.0" +version = "1.76.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1e9c3c24e36183e2f698235ed38dcfbbdff1d09b9232dc866c4be3011e0b47e" +checksum = "cb1cd79a3412751a341a28e2cd0d6fa4345241976da427b075a0c0cd5409f886" dependencies = [ "aws-credential-types", "aws-runtime", @@ -717,9 +720,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.3.2" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3734aecf9ff79aa401a6ca099d076535ab465ff76b46440cf567c8e70b65dc13" +checksum = "ddfb9021f581b71870a17eac25b52335b82211cdc092e02b6876b2bcefa61666" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -756,9 +759,9 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.63.3" +version = "0.63.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f77a921dbd2c78ebe70726799787c1d110a2245dd65e39b20923dfdfb2deee" +checksum = "244f00666380d35c1c76b90f7b88a11935d11b84076ac22a4c014ea0939627af" dependencies = [ "aws-smithy-http", "aws-smithy-types", @@ -776,9 +779,9 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.8" +version = "0.60.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c45d3dddac16c5c59d553ece225a88870cf81b7b813c9cc17b78cf4685eac7a" +checksum = "338a3642c399c0a5d157648426110e199ca7fd1c689cc395676b81aa563700c4" dependencies = [ "aws-smithy-types", "bytes 1.10.1", @@ -808,15 +811,15 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.0.3" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "073d330f94bdf1f47bb3e0f5d45dda1e372a54a553c39ab6e9646902c8c81594" +checksum = "f108f1ca850f3feef3009bdcc977be201bca9a91058864d9de0684e64514bee0" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", "aws-smithy-types", "h2 0.3.26", - "h2 0.4.10", + "h2 0.4.11", "http 0.2.12", "http 1.3.1", "http-body 0.4.6", @@ -827,7 +830,7 @@ dependencies = [ "hyper-util", "pin-project-lite 0.2.16", "rustls 0.21.12", - "rustls 0.23.27", + "rustls 0.23.28", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio 1.45.1", @@ -837,9 +840,9 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.61.3" +version = "0.61.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92144e45819cae7dc62af23eac5a038a58aa544432d2102609654376a900bd07" +checksum = "a16e040799d29c17412943bdbf488fd75db04112d0c0d4b9290bacf5ae0014b9" dependencies = [ "aws-smithy-types", ] @@ -889,9 +892,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e5d9e3a80a18afa109391fb5ad09c3daf887b516c6fd805a157c6ea7994a57" +checksum = "bd8531b6d8882fd8f48f82a9754e682e29dd44cff27154af51fa3eb730f59efb" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -906,9 +909,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40076bd09fadbc12d5e026ae080d0930defa606856186e31d83ccc6a255eeaf3" +checksum = "d498595448e43de7f4296b7b7a18a8a02c61ec9349128c80a368f7c3b4ab11a8" dependencies = [ "base64-simd", "bytes 1.10.1", @@ -932,9 +935,9 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.9" +version = "0.60.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" +checksum = "3db87b96cb1b16c024980f133968d52882ca0daaee3a086c6decc500f6c99728" dependencies = [ "xmlparser", ] @@ -1041,7 +1044,7 @@ checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1070,10 +1073,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11eeb275b20a4c750c9fe7bf5a750e97e7944563003efd1c82e70c229a612ca1" dependencies = [ "darling 0.20.11", - "heck", + "heck 0.5.0", "proc-macro-error", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", "ubyte", ] @@ -1174,7 +1177,7 @@ dependencies = [ "bitflags 2.9.1", "cexpr", "clang-sys", - "itertools", + "itertools 0.12.1", "lazy_static", "lazycell", "log", @@ -1184,7 +1187,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.101", + "syn 2.0.104", "which", ] @@ -1281,7 +1284,7 @@ dependencies = [ "getrandom 0.2.16", "getrandom 0.3.3", "hex", - "indexmap 2.9.0", + "indexmap 2.10.0", "js-sys", "once_cell", "rand 0.9.1", @@ -1300,15 +1303,15 @@ checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" [[package]] name = "bumpalo" -version = "3.18.1" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" [[package]] name = "byteorder" @@ -1388,9 +1391,9 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" [[package]] name = "cc" -version = "1.2.26" +version = "1.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" dependencies = [ "jobserver", "libc", @@ -1429,9 +1432,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "chrono" @@ -1671,11 +1674,10 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc-fast" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68fcb2be5386ffb77e30bf10820934cb89a628bcb976e7cc632dcd88c059ebea" +checksum = "6bf62af4cc77d8fe1c22dde4e721d87f2f54056139d8c412e1366b740305f56f" dependencies = [ - "cc", "crc", "digest", "libc", @@ -1743,9 +1745,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" @@ -1802,7 +1804,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1920,7 +1922,7 @@ dependencies = [ "proc-macro2", "quote 1.0.40", "strsim 0.11.1", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -1953,7 +1955,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2069,18 +2071,18 @@ checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "derive-where" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e73f2692d4bd3cac41dca28934a39894200c9fabf49586d77d0e5954af1d7902" +checksum = "510c292c8cf384b1a340b816a9a6cf2599eb8f566a44949024af88418000c50b" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2093,7 +2095,7 @@ dependencies = [ "proc-macro2", "quote 1.0.40", "rustc_version", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2126,7 +2128,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2149,7 +2151,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2323,10 +2325,10 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2346,7 +2348,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2362,6 +2364,26 @@ dependencies = [ "termcolor", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote 1.0.40", + "syn 2.0.104", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -2379,12 +2401,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -2502,7 +2524,7 @@ version = "0.10.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" dependencies = [ - "atomic 0.6.0", + "atomic 0.6.1", "pear", "serde", "toml 0.8.23", @@ -2522,11 +2544,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", @@ -2749,7 +2777,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2821,7 +2849,7 @@ dependencies = [ "libc", "log", "rustversion", - "windows 0.61.1", + "windows 0.61.3", ] [[package]] @@ -2846,9 +2874,9 @@ dependencies = [ [[package]] name = "getopts" -version = "0.2.21" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" dependencies = [ "unicode-width", ] @@ -2862,7 +2890,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -2882,14 +2910,14 @@ dependencies = [ [[package]] name = "getset" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3586f256131df87204eb733da72e3d3eb4f343c639f4b7be279ac7c48baeafe" +checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" dependencies = [ "proc-macro-error2", "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -2904,9 +2932,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" dependencies = [ "color_quant", "weezl", @@ -2983,7 +3011,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.9.0", + "indexmap 2.10.0", "slab", "tokio 1.45.1", "tokio-util", @@ -2992,9 +3020,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" dependencies = [ "atomic-waker", "bytes 1.10.1", @@ -3002,7 +3030,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.9.0", + "indexmap 2.10.0", "slab", "tokio 1.45.1", "tokio-util", @@ -3060,9 +3088,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ "allocator-api2", "equivalent", @@ -3093,6 +3121,12 @@ dependencies = [ "http 1.3.1", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -3110,9 +3144,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -3238,7 +3272,7 @@ dependencies = [ "markup5ever", "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -3351,7 +3385,7 @@ dependencies = [ "bytes 1.10.1", "futures-channel", "futures-util", - "h2 0.4.10", + "h2 0.4.11", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -3406,7 +3440,7 @@ dependencies = [ "http 1.3.1", "hyper 1.6.0", "hyper-util", - "rustls 0.23.27", + "rustls 0.23.28", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio 1.45.1", @@ -3654,7 +3688,7 @@ dependencies = [ "color_quant", "exr", "gif", - "image-webp 0.2.1", + "image-webp 0.2.3", "num-traits", "png", "qoi", @@ -3678,9 +3712,9 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" +checksum = "f6970fe7a5300b4b42e62c52efa0187540a5bef546c60edaf554ef595d2e6f0b" dependencies = [ "byteorder-lite", "quick-error 2.0.1", @@ -3710,19 +3744,19 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ - "autocfg 1.4.0", + "autocfg 1.5.0", "hashbrown 0.12.3", "serde", ] [[package]] name = "indexmap" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown 0.15.3", + "hashbrown 0.15.4", "serde", ] @@ -3767,7 +3801,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -3804,9 +3838,9 @@ version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ - "hermit-abi 0.5.1", + "hermit-abi 0.5.2", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3845,11 +3879,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d4e5d712dd664b11e778d1cfc06c79ba2700d6bc1771e44fb7b6a4656b487d" dependencies = [ "generic-array 1.2.0", - "schemars", + "schemars 0.8.22", "serde", "time", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.12.1" @@ -3877,9 +3920,9 @@ dependencies = [ [[package]] name = "jpeg-decoder" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" [[package]] name = "js-sys" @@ -3902,6 +3945,19 @@ dependencies = [ "serde", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "ring", + "serde", + "serde_json", +] + [[package]] name = "jwt-simple" version = "0.11.9" @@ -4157,9 +4213,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.172" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libfuzzer-sys" @@ -4190,7 +4246,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.0", + "windows-targets 0.53.2", ] [[package]] @@ -4273,13 +4329,75 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "livekit-api" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc95de3f9a3aee87733789fb7a79643871eb05e5ae037d4876e70a6143c3d9e4" +dependencies = [ + "base64 0.21.7", + "http 0.2.12", + "jsonwebtoken", + "livekit-protocol", + "log", + "parking_lot", + "pbjson-types", + "prost", + "rand 0.9.1", + "reqwest 0.11.27", + "scopeguard", + "serde", + "serde_json", + "sha2", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "livekit-protocol" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c90494efa508ec40228f870affec648826d23e1a9621a4430f67d187901eb4" +dependencies = [ + "futures-util", + "livekit-runtime 0.4.0", + "parking_lot", + "pbjson", + "pbjson-types", + "prost", + "prost-types", + "serde", + "thiserror 1.0.69", + "tokio 1.45.1", +] + +[[package]] +name = "livekit-runtime" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ae53eb874eb86e96e8ccffc31b5a00926d46174cbcb1c24ce4e57b1fcc5c8f6" +dependencies = [ + "tokio 1.45.1", + "tokio-stream", +] + +[[package]] +name = "livekit-runtime" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532e84c6cdc5fe774f2b5d9912597b5f3bea561927a48296d03e24549d21c3f6" +dependencies = [ + "tokio 1.45.1", + "tokio-stream", +] + [[package]] name = "lock_api" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ - "autocfg 1.4.0", + "autocfg 1.5.0", "scopeguard", ] @@ -4314,7 +4432,7 @@ dependencies = [ "quote 1.0.40", "regex-syntax 0.8.5", "rustc_version", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -4387,7 +4505,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.3", + "hashbrown 0.15.4", ] [[package]] @@ -4420,7 +4538,7 @@ dependencies = [ "macro_magic_core", "macro_magic_macros", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -4434,7 +4552,7 @@ dependencies = [ "macro_magic_core_macros", "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -4445,7 +4563,7 @@ checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -4456,7 +4574,7 @@ checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" dependencies = [ "macro_magic_core", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -4522,9 +4640,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memmap2" @@ -4559,9 +4677,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", @@ -4574,7 +4692,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -4632,9 +4750,9 @@ dependencies = [ [[package]] name = "mongodb" -version = "3.2.3" +version = "3.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf4261933e5113914caec01c4bb16a7502bdaa9cf80fd87191765e7d9ff16b2" +checksum = "d0f8c69f13acf07eae386a2974f48ffd9187ea2aba8defbea9aa34e7e272c5f3" dependencies = [ "async-trait", "base64 0.13.1", @@ -4664,7 +4782,7 @@ dependencies = [ "serde", "serde_bytes", "serde_with", - "sha-1", + "sha1", "sha2", "socket2 0.5.10", "stringprep", @@ -4681,14 +4799,14 @@ dependencies = [ [[package]] name = "mongodb-internal-macros" -version = "3.2.3" +version = "3.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "619176c99deef0d50be51ce3193e9efd6a56ab0f4e6a38d5fd614880d148c7ae" +checksum = "b9202de265a3a8bbb43f9fe56db27c93137d4f9fb04c093f47e9c7de0c61ac7d" dependencies = [ "macro_magic", "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -4710,6 +4828,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "mutate_once" version = "0.1.1" @@ -4824,7 +4948,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -4842,7 +4966,7 @@ version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ - "autocfg 1.4.0", + "autocfg 1.5.0", "num-integer", "num-traits", ] @@ -4864,7 +4988,7 @@ version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "autocfg 1.4.0", + "autocfg 1.5.0", "libm", ] @@ -4874,7 +4998,7 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi 0.5.1", + "hermit-abi 0.5.2", "libc", ] @@ -4917,7 +5041,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -4973,7 +5097,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -5006,11 +5130,12 @@ dependencies = [ [[package]] name = "os_info" -version = "3.11.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fc863e2ca13dc2d5c34fb22ea4a588248ac14db929616ba65c45f21744b1e9" +checksum = "d0e1ac5fde8d43c34139135df8ea9ee9465394b2d8d20f032d38998f64afffc3" dependencies = [ "log", + "plist", "serde", "windows-sys 0.52.0", ] @@ -5103,6 +5228,43 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pbjson" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1030c719b0ec2a2d25a5df729d6cff1acf3cc230bf766f4f97833591f7577b90" +dependencies = [ + "base64 0.21.7", + "serde", +] + +[[package]] +name = "pbjson-build" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2580e33f2292d34be285c5bc3dba5259542b083cfad6037b6d70345f24dcb735" +dependencies = [ + "heck 0.4.1", + "itertools 0.11.0", + "prost", + "prost-types", +] + +[[package]] +name = "pbjson-types" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f596653ba4ac51bdecbb4ef6773bc7f56042dc13927910de1684ad3d32aa12" +dependencies = [ + "bytes 1.10.1", + "chrono", + "pbjson", + "pbjson-build", + "prost", + "prost-build", + "serde", +] + [[package]] name = "pbkdf2" version = "0.11.0" @@ -5132,7 +5294,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -5182,9 +5344,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", "thiserror 2.0.12", @@ -5193,9 +5355,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" dependencies = [ "pest", "pest_generator", @@ -5203,28 +5365,37 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "pest_meta" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" dependencies = [ - "once_cell", "pest", "sha2", ] +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.10.0", +] + [[package]] name = "phf" version = "0.10.1" @@ -5294,7 +5465,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -5338,7 +5509,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -5408,6 +5579,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plist" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d77244ce2d584cd84f6a15f86195b8c9b2a0dfbfd817c09e0464244091a58ed" +dependencies = [ + "base64 0.22.1", + "indexmap 2.10.0", + "quick-xml", + "serde", + "time", +] + [[package]] name = "png" version = "0.17.16" @@ -5427,7 +5611,7 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ - "autocfg 1.4.0", + "autocfg 1.5.0", "bitflags 1.3.2", "cfg-if", "concurrent-queue", @@ -5445,7 +5629,7 @@ checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.5.1", + "hermit-abi 0.5.2", "pin-project-lite 0.2.16", "rustix 1.0.7", "tracing", @@ -5512,12 +5696,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.33" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d" +checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" dependencies = [ "proc-macro2", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -5582,7 +5766,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -5602,28 +5786,28 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", "version_check", "yansi", ] [[package]] name = "profiling" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" dependencies = [ "profiling-procmacros", ] [[package]] name = "profiling-procmacros" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -5640,6 +5824,59 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes 1.10.1", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" +dependencies = [ + "bytes 1.10.1", + "heck 0.5.0", + "itertools 0.12.1", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.104", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote 1.0.40", + "syn 2.0.104", +] + +[[package]] +name = "prost-types" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +dependencies = [ + "prost", +] + [[package]] name = "qoi" version = "0.4.1" @@ -5667,6 +5904,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "0.3.15" @@ -5690,9 +5936,9 @@ checksum = "5a3866219251662ec3b26fc217e3e05bf9c4f84325234dfb96bf0bf840889e49" [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "radium" @@ -5880,7 +6126,7 @@ dependencies = [ "built", "cfg-if", "interpolate_name", - "itertools", + "itertools 0.12.1", "libc", "libfuzzer-sys", "log", @@ -5903,9 +6149,9 @@ dependencies = [ [[package]] name = "ravif" -version = "0.11.12" +version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6a5f31fcf7500f9401fea858ea4ab5525c99f2322cfcee732c0e6c74208c0c6" +checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" dependencies = [ "avif-serialize", "imgref", @@ -5998,9 +6244,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" dependencies = [ "bitflags 2.9.1", ] @@ -6022,7 +6268,7 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -6117,15 +6363,15 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.19" +version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "base64 0.22.1", "bytes 1.10.1", "encoding_rs", "futures-core", - "h2 0.4.10", + "h2 0.4.11", "http 1.3.1", "http-body 1.0.1", "http-body-util", @@ -6133,12 +6379,10 @@ dependencies = [ "hyper-rustls 0.27.7", "hyper-tls 0.6.0", "hyper-util", - "ipnet", "js-sys", "log", "mime", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite 0.2.16", "rustls-pki-types", @@ -6297,6 +6541,9 @@ dependencies = [ "isahc", "iso8601-timestamp", "linkify 0.8.1", + "livekit-api", + "livekit-protocol", + "livekit-runtime 0.3.1", "log", "lru 0.11.1", "mongodb", @@ -6316,7 +6563,7 @@ dependencies = [ "revolt_optional_struct", "revolt_rocket_okapi", "rocket", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "ulid 1.2.1", @@ -6343,6 +6590,8 @@ dependencies = [ "iso8601-timestamp", "lettre", "linkify 0.6.0", + "livekit-api", + "livekit-protocol", "log", "lru 0.7.8", "nanoid", @@ -6364,7 +6613,7 @@ dependencies = [ "rocket_cors", "rocket_empty", "rocket_prometheus", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "ulid 0.4.1", @@ -6408,7 +6657,7 @@ dependencies = [ "mime", "moka", "regex", - "reqwest 0.12.19", + "reqwest 0.12.22", "revolt-config", "revolt-files", "revolt-models", @@ -6437,7 +6686,7 @@ dependencies = [ "revolt-permissions", "revolt_optional_struct", "rocket", - "schemars", + "schemars 0.8.22", "serde", "utoipa", "validator 0.16.1", @@ -6461,7 +6710,7 @@ dependencies = [ "num_enum 0.6.1", "once_cell", "revolt-result", - "schemars", + "schemars 0.8.22", "serde", ] @@ -6510,15 +6759,42 @@ name = "revolt-result" version = "0.8.8" dependencies = [ "axum", + "log", "revolt_okapi", "revolt_rocket_okapi", "rocket", - "schemars", + "schemars 0.8.22", "serde", "serde_json", "utoipa", ] +[[package]] +name = "revolt-voice-ingress" +version = "0.7.1" +dependencies = [ + "async-std", + "futures", + "livekit-api", + "livekit-protocol", + "livekit-runtime 0.3.1", + "log", + "lru 0.7.8", + "redis-kiss", + "revolt-config", + "revolt-database", + "revolt-models", + "revolt-permissions", + "revolt-result", + "rmp-serde", + "rocket", + "rocket_empty", + "sentry", + "serde", + "serde_json", + "ulid 0.5.0", +] + [[package]] name = "revolt_a2" version = "0.10.1" @@ -6556,7 +6832,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23bfdf7ae769c3042fe727f6e5c17363b02a64b4b33ad60c3e5f73b26df7835b" dependencies = [ "log", - "schemars", + "schemars 0.8.22", "serde", "serde_json", ] @@ -6582,7 +6858,7 @@ dependencies = [ "revolt_okapi", "revolt_rocket_okapi_codegen", "rocket", - "schemars", + "schemars 0.8.22", "serde", "serde_json", ] @@ -6680,7 +6956,7 @@ dependencies = [ "either", "figment", "futures", - "indexmap 2.9.0", + "indexmap 2.10.0", "log", "memchr", "multer", @@ -6716,7 +6992,7 @@ dependencies = [ "revolt_rocket_okapi", "rocket", "rocket_empty", - "schemars", + "schemars 0.8.22", "serde", ] @@ -6728,11 +7004,11 @@ checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46" dependencies = [ "devise", "glob", - "indexmap 2.9.0", + "indexmap 2.10.0", "proc-macro2", "quote 1.0.40", "rocket_http", - "syn 2.0.101", + "syn 2.0.104", "unicode-xid 0.2.6", "version_check", ] @@ -6775,7 +7051,7 @@ dependencies = [ "futures", "http 0.2.12", "hyper 0.14.32", - "indexmap 2.9.0", + "indexmap 2.10.0", "log", "memchr", "pear", @@ -6863,9 +7139,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustc-hash" @@ -6902,7 +7178,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6915,7 +7191,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6946,9 +7222,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.27" +version = "0.23.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" dependencies = [ "aws-lc-rs", "once_cell", @@ -7094,6 +7370,30 @@ dependencies = [ "serde_json", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1375ba8ef45a6f15d83fa8748f1079428295d403d6ea991d09ab100155fbc06d" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schemars_derive" version = "0.8.22" @@ -7103,7 +7403,7 @@ dependencies = [ "proc-macro2", "quote 1.0.40", "serde_derive_internals", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -7443,7 +7743,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -7454,7 +7754,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -7463,7 +7763,7 @@ version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "itoa", "memchr", "ryu", @@ -7503,15 +7803,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.9.0", + "indexmap 2.10.0", + "schemars 0.9.0", + "schemars 1.0.3", "serde", "serde_derive", "serde_json", @@ -7521,14 +7823,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" dependencies = [ "darling 0.20.11", "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -7667,12 +7969,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg 1.4.0", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "slotmap" @@ -7833,11 +8132,11 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote 1.0.40", "rustversion", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -7880,9 +8179,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.101" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote 1.0.40", @@ -7933,7 +8232,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -7999,7 +8298,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ "cfg-expr", - "heck", + "heck 0.5.0", "pkg-config", "toml 0.8.23", "version-compare", @@ -8039,7 +8338,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix 1.0.7", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -8088,7 +8387,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -8099,17 +8398,16 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -8253,7 +8551,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -8293,7 +8591,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.27", + "rustls 0.23.28", "tokio 1.45.1", ] @@ -8358,7 +8656,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "toml_datetime", "winnow 0.5.40", ] @@ -8369,12 +8667,12 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "serde", "serde_spanned", "toml_datetime", "toml_write", - "winnow 0.7.10", + "winnow 0.7.11", ] [[package]] @@ -8471,13 +8769,13 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -8721,9 +9019,9 @@ checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] name = "unicode-xid" @@ -8838,7 +9136,7 @@ version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "serde", "serde_json", "utoipa-gen", @@ -8854,7 +9152,7 @@ dependencies = [ "proc-macro2", "quote 1.0.40", "regex", - "syn 2.0.101", + "syn 2.0.104", "ulid 1.2.1", ] @@ -8884,9 +9182,9 @@ dependencies = [ [[package]] name = "v_frame" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" dependencies = [ "aligned-vec", "num-traits", @@ -9021,9 +9319,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" @@ -9040,7 +9338,7 @@ version = "0.12.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d" dependencies = [ - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] @@ -9065,7 +9363,7 @@ dependencies = [ "log", "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -9100,7 +9398,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -9178,14 +9476,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.0", + "webpki-roots 1.0.1", ] [[package]] name = "webpki-roots" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" +checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" dependencies = [ "rustls-pki-types", ] @@ -9236,7 +9534,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -9256,9 +9554,9 @@ dependencies = [ [[package]] name = "windows" -version = "0.61.1" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", "windows-core", @@ -9308,7 +9606,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -9319,14 +9617,14 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-numerics" @@ -9340,9 +9638,9 @@ dependencies = [ [[package]] name = "windows-registry" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ "windows-link", "windows-result", @@ -9394,6 +9692,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -9427,9 +9734,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", @@ -9599,9 +9906,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" dependencies = [ "memchr", ] @@ -9690,7 +9997,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", "synstructure 0.13.2", ] @@ -9707,7 +10014,7 @@ dependencies = [ "http 0.2.12", "hyper 0.14.32", "hyper-rustls 0.24.2", - "itertools", + "itertools 0.12.1", "log", "percent-encoding", "rustls 0.22.4", @@ -9723,22 +10030,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -9758,7 +10065,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", "synstructure 0.13.2", ] @@ -9798,7 +10105,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.101", + "syn 2.0.104", ] [[package]] @@ -9818,9 +10125,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4a518c0ea2576f4da876349d7f67a7be489297cd77c2cf9e04c2e05fcd3974" +checksum = "7384255a918371b5af158218d131530f694de9ad3815ebdd0453a940485cb0fa" dependencies = [ "zune-core", ] diff --git a/Revolt.toml b/Revolt.toml index 33e044348..76f908f38 100644 --- a/Revolt.toml +++ b/Revolt.toml @@ -26,6 +26,11 @@ january = "http://local.revolt.chat:14705" voso_legacy = "" voso_legacy_ws = "" +# Public urls for livekit nodes +# each entry here should have a corresponding entry under `api.livekit.nodes` +[hosts.livekit] +worldwide = "ws://local.revolt.chat:14706" + [api] [api.smtp] @@ -40,6 +45,18 @@ port = 14025 use_tls = false use_starttls = false +[api.livekit] + +# Config for livekit nodes +# Make sure to change the secret when deploying +# The key and secret should match the values livekit is using +[api.livekit.nodes.worldwide] +url = "http://livekit" +lat = 0.0 +lon = 0.0 +key = "worldwide_key" +secret = "ZjCofRlfm6GGtjlifmNpCDkcQbEIIVC0" + [files.s3] # S3 protocol endpoint endpoint = "http://127.0.0.1:14009" diff --git a/crates/bonfire/src/config.rs b/crates/bonfire/src/config.rs index 5642d3baa..04cfd5ca6 100644 --- a/crates/bonfire/src/config.rs +++ b/crates/bonfire/src/config.rs @@ -93,6 +93,7 @@ impl ProtocolConfiguration { ReadyPayloadFields::Channels, ReadyPayloadFields::Members, ReadyPayloadFields::Emoji, + ReadyPayloadFields::VoiceStates, ] } } diff --git a/crates/bonfire/src/events/impl.rs b/crates/bonfire/src/events/impl.rs index 160e7c6c0..e2bd14566 100644 --- a/crates/bonfire/src/events/impl.rs +++ b/crates/bonfire/src/events/impl.rs @@ -4,6 +4,7 @@ use futures::future::join_all; use revolt_database::{ events::client::{EventV1, ReadyPayloadFields}, util::permissions::DatabasePermissionQuery, + voice::{delete_voice_state, get_voice_channel_members, get_voice_state, get_channel_voice_state}, Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus, }; use revolt_models::v0; @@ -11,12 +12,14 @@ use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_presence::filter_online; use revolt_result::Result; + use super::state::{Cache, State}; /// Cache Manager impl Cache { /// Check whether the current user can view a channel pub async fn can_view_channel(&self, db: &Database, channel: &Channel) -> bool { + #[allow(deprecated)] match &channel { Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => { let member = self.members.get(server); @@ -240,6 +243,21 @@ impl State { self.insert_subscription(channel.id().to_string()).await; } + let voice_states = if fields.contains(&ReadyPayloadFields::VoiceStates) { + // fetch voice states for all the channels we can see + let mut voice_states = Vec::new(); + + for channel in &channels { + if let Ok(Some(voice_state)) = get_channel_voice_state(channel).await { + voice_states.push(voice_state) + } + } + + Some(voice_states) + } else { + None + }; + Ok(EventV1::Ready { users: if fields.contains(&ReadyPayloadFields::Users) { Some(users) @@ -262,6 +280,7 @@ impl State { None }, emojis: emojis.map(|vec| vec.into_iter().map(Into::into).collect()), + voice_states, user_settings, channel_unreads: channel_unreads.map(|vec| vec.into_iter().map(Into::into).collect()), @@ -279,6 +298,7 @@ impl State { let id = &id.to_string(); for (channel_id, channel) in &self.cache.channels { + #[allow(deprecated)] match channel { Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => { if server == id { @@ -459,6 +479,7 @@ impl State { server, channels, emojis: _, + voice_states: _, } => { self.insert_subscription(id.clone()).await; diff --git a/crates/bonfire/src/main.rs b/crates/bonfire/src/main.rs index f4e8a3870..3f68e4729 100644 --- a/crates/bonfire/src/main.rs +++ b/crates/bonfire/src/main.rs @@ -1,7 +1,9 @@ -use std::env; +use std::{env, sync::Arc}; use async_std::net::TcpListener; use revolt_presence::clear_region; +use once_cell::sync::OnceCell; +use revolt_database::voice::VoiceClient; #[macro_use] extern crate log; @@ -12,6 +14,15 @@ pub mod events; mod database; mod websocket; +pub static VOICE_CLIENT: OnceCell> = OnceCell::new(); + +pub fn get_voice_client() -> Arc { + VOICE_CLIENT + .get() + .expect("get_voice_client called before set") + .clone() +} + #[async_std::main] async fn main() { // Configure requirements for Bonfire. @@ -24,6 +35,8 @@ async fn main() { clear_region(None).await; } + VOICE_CLIENT.set(Arc::new(VoiceClient::from_revolt_config().await)).unwrap(); + // Setup a TCP listener to accept WebSocket connections on. // By default, we bind to port 14703 on all interfaces. let bind = env::var("HOST").unwrap_or_else(|_| "0.0.0.0:14703".into()); diff --git a/crates/core/config/Revolt.toml b/crates/core/config/Revolt.toml index e3a04f2ae..e3e8ec977 100644 --- a/crates/core/config/Revolt.toml +++ b/crates/core/config/Revolt.toml @@ -22,6 +22,8 @@ january = "http://local.revolt.chat/january" voso_legacy = "" voso_legacy_ws = "" +[hosts.livekit] + [rabbit] host = "rabbit" port = 5672 @@ -66,8 +68,11 @@ hcaptcha_sitekey = "" # Maximum concurrent connections (to proxy server) max_concurrent_connections = 50 -[api.users] +[api.livekit] +[api.livekit.nodes] + +[api.users] [pushd] # this changes the names of the queues to not overlap @@ -273,6 +278,7 @@ process_message_delay_limit = 5 # Configuration for Sentry error reporting api = "" events = "" +voice_ingress = "" files = "" proxy = "" pushd = "" diff --git a/crates/core/config/src/lib.rs b/crates/core/config/src/lib.rs index 7c0504997..2371aa7e7 100644 --- a/crates/core/config/src/lib.rs +++ b/crates/core/config/src/lib.rs @@ -123,8 +123,7 @@ pub struct Hosts { pub events: String, pub autumn: String, pub january: String, - pub voso_legacy: String, - pub voso_legacy_ws: String, + pub livekit: HashMap } #[derive(Deserialize, Debug, Clone)] @@ -195,6 +194,24 @@ pub struct ApiWorkers { pub max_concurrent_connections: usize, } +#[derive(Deserialize, Debug, Clone)] +pub struct ApiLiveKit { + pub nodes: HashMap +} + +#[derive(Deserialize, Debug, Clone)] +pub struct LiveKitNode { + pub url: String, + pub lat: f64, + pub lon: f64, + pub key: String, + pub secret: String, + + // whether to hide the node in the nodes list + #[serde(default)] + pub private: bool, +} + #[derive(Deserialize, Debug, Clone)] pub struct ApiUsers { pub early_adopter_cutoff: Option, @@ -206,6 +223,7 @@ pub struct Api { pub smtp: ApiSmtp, pub security: ApiSecurity, pub workers: ApiWorkers, + pub livekit: ApiLiveKit, pub users: ApiUsers, } @@ -359,6 +377,7 @@ pub struct Features { pub struct Sentry { pub api: String, pub events: String, + pub voice_ingress: String, pub files: String, pub proxy: String, pub pushd: String, diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml index 7f2e09730..7794b0d70 100644 --- a/crates/core/database/Cargo.toml +++ b/crates/core/database/Cargo.toml @@ -101,3 +101,8 @@ authifier = { version = "1.0.15", features = ["rocket_impl"] } # RabbitMQ amqprs = { version = "1.7.0" } + +# Voice +livekit-api = "0.4.4" +livekit-protocol = "0.4.0" +livekit-runtime = { version = "0.3.1", features = ["tokio"] } diff --git a/crates/core/database/src/drivers/mod.rs b/crates/core/database/src/drivers/mod.rs index 1c9ae9675..e05734847 100644 --- a/crates/core/database/src/drivers/mod.rs +++ b/crates/core/database/src/drivers/mod.rs @@ -31,7 +31,7 @@ pub enum DatabaseInfo { } /// Database -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum Database { /// Mock database Reference(ReferenceDb), diff --git a/crates/core/database/src/drivers/mongodb.rs b/crates/core/database/src/drivers/mongodb.rs index 64a2250bb..e12e60fb8 100644 --- a/crates/core/database/src/drivers/mongodb.rs +++ b/crates/core/database/src/drivers/mongodb.rs @@ -12,6 +12,7 @@ use serde::Serialize; database_derived!( #[cfg(feature = "mongodb")] /// MongoDB implementation + #[derive(Debug)] pub struct MongoDb(pub ::mongodb::Client, pub String); ); diff --git a/crates/core/database/src/drivers/reference.rs b/crates/core/database/src/drivers/reference.rs index ff18f2533..e02eae644 100644 --- a/crates/core/database/src/drivers/reference.rs +++ b/crates/core/database/src/drivers/reference.rs @@ -10,7 +10,7 @@ use crate::{ database_derived!( /// Reference implementation - #[derive(Default)] + #[derive(Default, Debug)] pub struct ReferenceDb { pub bots: Arc>>, pub channels: Arc>>, diff --git a/crates/core/database/src/events/client.rs b/crates/core/database/src/events/client.rs index a887e9f12..9d630406b 100644 --- a/crates/core/database/src/events/client.rs +++ b/crates/core/database/src/events/client.rs @@ -3,10 +3,7 @@ use revolt_result::Error; use serde::{Deserialize, Serialize}; use revolt_models::v0::{ - AppendMessage, Channel, ChannelUnread, Emoji, FieldsChannel, FieldsMember, FieldsMessage, - FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, - PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, - PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings, Webhook, + AppendMessage, Channel, ChannelUnread, ChannelVoiceState, Emoji, FieldsChannel, FieldsMember, FieldsMessage, FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, PartialUserVoiceState, PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings, UserVoiceState, Webhook }; use crate::Database; @@ -27,6 +24,7 @@ pub enum ReadyPayloadFields { Channels, Members, Emoji, + VoiceStates, UserSettings(Vec), ChannelUnreads, @@ -57,6 +55,8 @@ pub enum EventV1 { members: Option>, #[serde(skip_serializing_if = "Option::is_none")] emojis: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + voice_states: Option>, #[serde(skip_serializing_if = "Option::is_none")] user_settings: Option, @@ -122,6 +122,7 @@ pub enum EventV1 { server: Server, channels: Vec, emojis: Vec, + voice_states: Vec }, /// Update existing server @@ -248,6 +249,31 @@ pub enum EventV1 { /// Auth events Auth(AuthifierEvent), + + /// Voice events + VoiceChannelJoin { + id: String, + state: UserVoiceState, + }, + VoiceChannelLeave { + id: String, + user: String, + }, + VoiceChannelMove { + user: String, + from: String, + to: String, + state: UserVoiceState + }, + UserVoiceStateUpdate { + id: String, + channel_id: String, + data: PartialUserVoiceState, + }, + UserMoveVoiceChannel { + node: String, + token: String + } } impl EventV1 { diff --git a/crates/core/database/src/lib.rs b/crates/core/database/src/lib.rs index 9e26336d1..8889f196c 100644 --- a/crates/core/database/src/lib.rs +++ b/crates/core/database/src/lib.rs @@ -108,6 +108,9 @@ pub mod tasks; mod amqp; pub use amqp::amqp::AMQP; +pub mod voice; + + /// Utility function to check if a boolean value is false pub fn if_false(t: &bool) -> bool { !t diff --git a/crates/core/database/src/models/admin_migrations/ops/mongodb/scripts.rs b/crates/core/database/src/models/admin_migrations/ops/mongodb/scripts.rs index d164cab10..7e8c02682 100644 --- a/crates/core/database/src/models/admin_migrations/ops/mongodb/scripts.rs +++ b/crates/core/database/src/models/admin_migrations/ops/mongodb/scripts.rs @@ -26,7 +26,7 @@ struct MigrationInfo { revision: i32, } -pub const LATEST_REVISION: i32 = 42; // MUST BE +1 to last migration +pub const LATEST_REVISION: i32 = 44; // MUST BE +1 to last migration pub async fn migrate_database(db: &MongoDb) { let migrations = db.col::("migrations"); @@ -914,6 +914,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 { } if revision <= 26 { + // Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`. info!("Running migration [revision 26 / 15-05-2024]: fix invites being incorrectly serialized with wrong enum tagging."); auto_derived!( @@ -1091,6 +1092,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 { .await; for webhook in webhooks { + #[allow(deprecated)] match db.fetch_channel(&webhook.channel_id).await { Ok(channel) => { let creator_id = match channel { @@ -1170,8 +1172,42 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 { } if revision <= 41 { + info!("Running migration [revision 32 / 26-01-2025]: Add `is_publishing` and `is_receiving` to members"); + + db.col::("server_members") + .update_many( + doc! {}, + doc! { + "$set": { + "is_publishing": true, + "is_receiving": true + } + } + ) + .await + .expect("Failed to update members"); + } + + if revision <= 42 { + info!("Running migration [revision 33 / 29-04-2025]: Convert all `VoiceChannel`'s into `TextChannel` "); + + db.col::("channels") + .update_many( + doc! { "channel_type": "VoiceChannel" }, + doc! { + "$set": { + "channel_type": "TextChannel", + "voice": {} + } + } + ) + .await + .expect("Failed to update voice channels"); + }; + + if revision <= 43 { info!( - "Running migration [revision 41 / 05-06-2025]: convert role ranks to uniform numbers." + "Running migration [revision 43 / 05-06-2025]: convert role ranks to uniform numbers." ); #[derive(Serialize, Deserialize, Clone)] diff --git a/crates/core/database/src/models/channel_invites/model.rs b/crates/core/database/src/models/channel_invites/model.rs index e3af921ab..a5d877fee 100644 --- a/crates/core/database/src/models/channel_invites/model.rs +++ b/crates/core/database/src/models/channel_invites/model.rs @@ -69,7 +69,7 @@ impl Invite { creator: creator.id.clone(), channel: id.clone(), }), - Channel::TextChannel { id, server, .. } | Channel::VoiceChannel { id, server, .. } => { + Channel::TextChannel { id, server, .. } => { Ok(Invite::Server { code, creator: creator.id.clone(), diff --git a/crates/core/database/src/models/channels/model.rs b/crates/core/database/src/models/channels/model.rs index 536632a70..1c1edafd2 100644 --- a/crates/core/database/src/models/channels/model.rs +++ b/crates/core/database/src/models/channels/model.rs @@ -1,7 +1,8 @@ -use std::collections::HashMap; +#![allow(deprecated)] +use std::{borrow::Cow, collections::HashMap}; use revolt_config::config; -use revolt_models::v0::{self, MessageAuthor}; +use revolt_models::v0::{self, MessageAuthor, VoiceInformation}; use revolt_permissions::OverrideField; use revolt_result::Result; use serde::{Deserialize, Serialize}; @@ -103,8 +104,12 @@ auto_derived!( /// Whether this channel is marked as not safe for work #[serde(skip_serializing_if = "crate::if_false", default)] nsfw: bool, + + /// Voice Information for when this channel is also a voice channel + #[serde(skip_serializing_if = "Option::is_none")] + voice: Option }, - /// Voice channel belonging to a server + #[deprecated = "Use TextChannel { voice } instead"] VoiceChannel { /// Unique Id #[serde(rename = "_id")] @@ -161,6 +166,8 @@ auto_derived!( pub default_permissions: Option, #[serde(skip_serializing_if = "Option::is_none")] pub last_message_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub voice: Option } /// Optional fields on channel object @@ -219,16 +226,19 @@ impl Channel { default_permissions: None, role_permissions: HashMap::new(), nsfw: data.nsfw.unwrap_or(false), + voice: data.voice }, - v0::LegacyServerChannelType::Voice => Channel::VoiceChannel { + v0::LegacyServerChannelType::Voice => Channel::TextChannel { id: id.clone(), server: server.id.to_owned(), name: data.name, description: data.description, icon: None, + last_message_id: None, default_permissions: None, role_permissions: HashMap::new(), nsfw: data.nsfw.unwrap_or(false), + voice: Some(data.voice.unwrap_or_default()) }, }; @@ -434,6 +444,23 @@ impl Channel { } } + /// Clone this channel's server id + pub fn server(&self) -> Option<&str> { + match self { + Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => Some(server), + _ => None + } + } + + /// Gets this channel's voice information + pub fn voice(&self) -> Option> { + match self { + Self::DirectMessage { .. } | Channel::VoiceChannel { .. } => Some(Cow::Owned(v0::VoiceInformation::default())), + Self::TextChannel { voice: Some(voice), .. } => Some(Cow::Borrowed(voice)), + _ => None + } + } + /// Set role permission on a channel pub async fn set_role_permission( &mut self, @@ -447,12 +474,6 @@ impl Channel { server, role_permissions, .. - } - | Channel::VoiceChannel { - id, - server, - role_permissions, - .. } => { db.set_channel_role_permission(id, role_id, permissions) .await?; @@ -499,7 +520,7 @@ impl Channel { clear: remove.into_iter().map(|v| v.into()).collect(), } .p(match self { - Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => server.clone(), + Self::TextChannel { server, .. } => server.clone(), _ => id, }) .await; @@ -512,16 +533,14 @@ impl Channel { match field { FieldsChannel::Description => match self { Self::Group { description, .. } - | Self::TextChannel { description, .. } - | Self::VoiceChannel { description, .. } => { + | Self::TextChannel { description, .. } => { description.take(); } _ => {} }, FieldsChannel::Icon => match self { Self::Group { icon, .. } - | Self::TextChannel { icon, .. } - | Self::VoiceChannel { icon, .. } => { + | Self::TextChannel { icon, .. } => { icon.take(); } _ => {} @@ -530,10 +549,6 @@ impl Channel { Self::TextChannel { default_permissions, .. - } - | Self::VoiceChannel { - default_permissions, - .. } => { default_permissions.take(); } @@ -550,6 +565,7 @@ impl Channel { } /// Apply partial channel to channel + #[allow(deprecated)] pub fn apply_options(&mut self, partial: PartialChannel) { match self { Self::SavedMessages { .. } => {} @@ -598,15 +614,7 @@ impl Channel { nsfw, default_permissions, role_permissions, - .. - } - | Self::VoiceChannel { - name, - description, - icon, - nsfw, - default_permissions, - role_permissions, + voice, .. } => { if let Some(v) = partial.name { @@ -632,7 +640,12 @@ impl Channel { if let Some(v) = partial.default_permissions { default_permissions.replace(v); } - } + + if let Some(v) = partial.voice { + voice.replace(v); + } + }, + Self::VoiceChannel { .. } => {} } } diff --git a/crates/core/database/src/models/channels/ops/mongodb.rs b/crates/core/database/src/models/channels/ops/mongodb.rs index b247a6834..79612f8d7 100644 --- a/crates/core/database/src/models/channels/ops/mongodb.rs +++ b/crates/core/database/src/models/channels/ops/mongodb.rs @@ -184,7 +184,7 @@ impl AbstractChannels for MongoDb { async fn delete_channel(&self, channel: &Channel) -> Result<()> { let id = channel.id().to_string(); let server_id = match channel { - Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => { + Channel::TextChannel { server, .. } => { Some(server) } _ => None, diff --git a/crates/core/database/src/models/channels/ops/reference.rs b/crates/core/database/src/models/channels/ops/reference.rs index 7d5c9a59f..518808bcc 100644 --- a/crates/core/database/src/models/channels/ops/reference.rs +++ b/crates/core/database/src/models/channels/ops/reference.rs @@ -94,9 +94,6 @@ impl AbstractChannels for ReferenceDb { match &mut channel { Channel::TextChannel { role_permissions, .. - } - | Channel::VoiceChannel { - role_permissions, .. } => { if role_permissions.get(role_id).is_some() { role_permissions.remove(role_id); diff --git a/crates/core/database/src/models/messages/model.rs b/crates/core/database/src/models/messages/model.rs index 2c24567a1..458eb927a 100644 --- a/crates/core/database/src/models/messages/model.rs +++ b/crates/core/database/src/models/messages/model.rs @@ -112,6 +112,8 @@ auto_derived!( MessagePinned { id: String, by: String }, #[serde(rename = "message_unpinned")] MessageUnpinned { id: String, by: String }, + #[serde(rename = "call_started")] + CallStarted { by: String }, } /// Name and / or avatar override information @@ -476,6 +478,7 @@ impl Message { // Validate the mentions go to users in the channel/server if !user_mentions.is_empty() { + #[allow(deprecated)] match channel { Channel::DirectMessage { ref recipients, .. } | Channel::Group { ref recipients, .. } => { @@ -830,6 +833,9 @@ impl Message { v0::SystemMessage::MessageUnpinned { by, .. } => { users.push(by.clone()); } + v0::SystemMessage::CallStarted { by } => { + users.push(by.clone()) + } } } users diff --git a/crates/core/database/src/models/server_members/model.rs b/crates/core/database/src/models/server_members/model.rs index 25b6979a1..4e8521f19 100644 --- a/crates/core/database/src/models/server_members/model.rs +++ b/crates/core/database/src/models/server_members/model.rs @@ -1,10 +1,11 @@ use iso8601_timestamp::Timestamp; use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_result::{create_error, Result}; +use crate::voice::get_channel_voice_state; use crate::{ - events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, File, - Server, SystemMessage, User, + events::client::EventV1, if_false, util::permissions::DatabasePermissionQuery, Channel, + Database, File, Server, SystemMessage, User, }; auto_derived_partial!( @@ -30,6 +31,13 @@ auto_derived_partial!( /// Timestamp this member is timed out until #[serde(skip_serializing_if = "Option::is_none")] pub timeout: Option, + + /// Whether the member is server-wide voice muted + #[serde(skip_serializing_if = "Option::is_none")] + pub can_publish: Option, + /// Whether the member is server-wide voice deafened + #[serde(skip_serializing_if = "Option::is_none")] + pub can_receive: Option, }, "PartialMember" ); @@ -50,6 +58,8 @@ auto_derived!( Avatar, Roles, Timeout, + CanReceive, + CanPublish, } /// Member removal intention @@ -69,6 +79,8 @@ impl Default for Member { avatar: None, roles: vec![], timeout: None, + can_publish: None, + can_receive: None, } } } @@ -121,6 +133,14 @@ impl Member { let emojis = db.fetch_emoji_by_parent_id(&server.id).await?; + let mut voice_states = Vec::new(); + + for channel in &channels { + if let Ok(Some(voice_state)) = get_channel_voice_state(channel).await { + voice_states.push(voice_state) + } + } + EventV1::ServerMemberJoin { id: server.id.clone(), user: user.id.clone(), @@ -137,6 +157,7 @@ impl Member { .map(|channel| channel.into()) .collect(), emojis: emojis.into_iter().map(|emoji| emoji.into()).collect(), + voice_states } .private(user.id.clone()) .await; @@ -190,6 +211,8 @@ impl Member { FieldsMember::Nickname => self.nickname = None, FieldsMember::Roles => self.roles.clear(), FieldsMember::Timeout => self.timeout = None, + FieldsMember::CanReceive => self.can_receive = None, + FieldsMember::CanPublish => self.can_publish = None, } } diff --git a/crates/core/database/src/models/server_members/ops/mongodb.rs b/crates/core/database/src/models/server_members/ops/mongodb.rs index 07d8a8196..5634eec79 100644 --- a/crates/core/database/src/models/server_members/ops/mongodb.rs +++ b/crates/core/database/src/models/server_members/ops/mongodb.rs @@ -247,6 +247,8 @@ impl IntoDocumentPath for FieldsMember { FieldsMember::Nickname => "nickname", FieldsMember::Roles => "roles", FieldsMember::Timeout => "timeout", + FieldsMember::CanPublish => "is_publishing", + FieldsMember::CanReceive => "is_receiving", }) } } diff --git a/crates/core/database/src/models/users/model.rs b/crates/core/database/src/models/users/model.rs index 0c2b6cee8..881cee309 100644 --- a/crates/core/database/src/models/users/model.rs +++ b/crates/core/database/src/models/users/model.rs @@ -7,6 +7,7 @@ use futures::future::join_all; use iso8601_timestamp::Timestamp; use once_cell::sync::Lazy; use rand::seq::SliceRandom; +use redis_kiss::{get_connection, AsyncCommands}; use revolt_config::{config, FeaturesLimits}; use revolt_models::v0::{self, UserBadges, UserFlags}; use revolt_presence::filter_online; @@ -639,6 +640,19 @@ impl User { } } + /// Gets current voice channel + /// + /// current_context: Either the channel id if a dm or group, or server_id if in a server + pub async fn current_voice_channel(&self, current_context: &str) -> Result> { + let mut conn = get_connection() + .await + .map_err(|_| create_error!(InternalError))?; + + conn.get::<_, Option>(format!("vc-{}-{current_context}", &self.id)) + .await + .map_err(|_| create_error!(InternalError)) + } + /// Update user data pub async fn update( &mut self, diff --git a/crates/core/database/src/util/bridge/v0.rs b/crates/core/database/src/util/bridge/v0.rs index d667094f9..a4ab20448 100644 --- a/crates/core/database/src/util/bridge/v0.rs +++ b/crates/core/database/src/util/bridge/v0.rs @@ -143,6 +143,7 @@ impl From for FieldsWebhook { } impl From for Channel { + #[allow(deprecated)] fn from(value: crate::Channel) -> Self { match value { crate::Channel::SavedMessages { id, user } => Channel::SavedMessages { id, user }, @@ -188,6 +189,7 @@ impl From for Channel { default_permissions, role_permissions, nsfw, + voice, } => Channel::TextChannel { id, server, @@ -198,6 +200,7 @@ impl From for Channel { default_permissions, role_permissions, nsfw, + voice, }, crate::Channel::VoiceChannel { id, @@ -223,6 +226,7 @@ impl From for Channel { } impl From for crate::Channel { + #[allow(deprecated)] fn from(value: Channel) -> crate::Channel { match value { Channel::SavedMessages { id, user } => crate::Channel::SavedMessages { id, user }, @@ -268,6 +272,7 @@ impl From for crate::Channel { default_permissions, role_permissions, nsfw, + voice, } => crate::Channel::TextChannel { id, server, @@ -278,6 +283,7 @@ impl From for crate::Channel { default_permissions, role_permissions, nsfw, + voice, }, Channel::VoiceChannel { id, @@ -315,6 +321,7 @@ impl From for PartialChannel { role_permissions: value.role_permissions, default_permissions: value.default_permissions, last_message_id: value.last_message_id, + voice: value.voice } } } @@ -332,6 +339,7 @@ impl From for crate::PartialChannel { role_permissions: value.role_permissions, default_permissions: value.default_permissions, last_message_id: value.last_message_id, + voice: value.voice } } } @@ -543,6 +551,7 @@ impl From for SystemMessage { crate::SystemMessage::UserRemove { id, by } => Self::UserRemove { id, by }, crate::SystemMessage::MessagePinned { id, by } => Self::MessagePinned { id, by }, crate::SystemMessage::MessageUnpinned { id, by } => Self::MessageUnpinned { id, by }, + crate::SystemMessage::CallStarted { by } => Self::CallStarted { by } } } } @@ -639,6 +648,8 @@ impl From for Member { avatar: value.avatar.map(|f| f.into()), roles: value.roles, timeout: value.timeout, + can_publish: value.can_publish, + can_receive: value.can_receive, } } } @@ -652,6 +663,8 @@ impl From for crate::Member { avatar: value.avatar.map(|f| f.into()), roles: value.roles, timeout: value.timeout, + can_publish: value.can_publish, + can_receive: value.can_receive, } } } @@ -665,6 +678,8 @@ impl From for PartialMember { avatar: value.avatar.map(|f| f.into()), roles: value.roles, timeout: value.timeout, + can_publish: value.can_publish, + can_receive: value.can_receive, } } } @@ -678,6 +693,8 @@ impl From for crate::PartialMember { avatar: value.avatar.map(|f| f.into()), roles: value.roles, timeout: value.timeout, + can_publish: value.can_publish, + can_receive: value.can_receive, } } } @@ -707,6 +724,8 @@ impl From for FieldsMember { crate::FieldsMember::Nickname => FieldsMember::Nickname, crate::FieldsMember::Roles => FieldsMember::Roles, crate::FieldsMember::Timeout => FieldsMember::Timeout, + crate::FieldsMember::CanReceive => FieldsMember::CanReceive, + crate::FieldsMember::CanPublish => FieldsMember::CanPublish, } } } @@ -718,6 +737,8 @@ impl From for crate::FieldsMember { FieldsMember::Nickname => crate::FieldsMember::Nickname, FieldsMember::Roles => crate::FieldsMember::Roles, FieldsMember::Timeout => crate::FieldsMember::Timeout, + FieldsMember::CanReceive => crate::FieldsMember::CanReceive, + FieldsMember::CanPublish => crate::FieldsMember::CanPublish, } } } diff --git a/crates/core/database/src/util/bulk_permissions.rs b/crates/core/database/src/util/bulk_permissions.rs index 9e7a1f7a1..99dda7e6c 100644 --- a/crates/core/database/src/util/bulk_permissions.rs +++ b/crates/core/database/src/util/bulk_permissions.rs @@ -144,10 +144,6 @@ impl<'z> BulkDatabasePermissionQuery<'z> { Channel::TextChannel { default_permissions, .. - } - | Channel::VoiceChannel { - default_permissions, - .. } => default_permissions.unwrap_or_default().into(), _ => Default::default(), } @@ -156,7 +152,7 @@ impl<'z> BulkDatabasePermissionQuery<'z> { } } - #[allow(dead_code)] + #[allow(dead_code, deprecated)] fn get_channel_type(&mut self) -> ChannelType { if let Some(channel) = &self.channel { match channel { @@ -179,9 +175,6 @@ impl<'z> BulkDatabasePermissionQuery<'z> { match channel { Channel::TextChannel { role_permissions, .. - } - | Channel::VoiceChannel { - role_permissions, .. } => role_permissions, _ => panic!("Not supported for non-server channels"), } @@ -208,12 +201,6 @@ async fn calculate_members_permissions<'a>( role_permissions, default_permissions, .. - } - | Channel::VoiceChannel { - id, - role_permissions, - default_permissions, - .. } => (id, role_permissions, default_permissions), _ => panic!("Calculation of member permissions must be done on a server channel"), }; diff --git a/crates/core/database/src/util/permissions.rs b/crates/core/database/src/util/permissions.rs index e592988eb..992001053 100644 --- a/crates/core/database/src/util/permissions.rs +++ b/crates/core/database/src/util/permissions.rs @@ -185,9 +185,26 @@ impl PermissionQuery for DatabasePermissionQuery<'_> { } } + async fn do_we_have_publish_overwrites(&mut self) -> bool { + if let Some(member) = &self.member { + member.can_publish.unwrap_or(false) + } else { + false + } + } + + async fn do_we_have_receive_overwrites(&mut self) -> bool { + if let Some(member) = &self.member { + member.can_receive.unwrap_or(false) + } else { + false + } + } + // * For calculating channel permission /// Get the type of the channel + #[allow(deprecated)] async fn get_channel_type(&mut self) -> ChannelType { if let Some(channel) = &self.channel { match channel { @@ -225,14 +242,6 @@ impl PermissionQuery for DatabasePermissionQuery<'_> { | Cow::Owned(Channel::TextChannel { default_permissions, .. - }) - | Cow::Borrowed(Channel::VoiceChannel { - default_permissions, - .. - }) - | Cow::Owned(Channel::VoiceChannel { - default_permissions, - .. }) => default_permissions.unwrap_or_default().into(), _ => Default::default(), } @@ -250,12 +259,6 @@ impl PermissionQuery for DatabasePermissionQuery<'_> { }) | Cow::Owned(Channel::TextChannel { role_permissions, .. - }) - | Cow::Borrowed(Channel::VoiceChannel { - role_permissions, .. - }) - | Cow::Owned(Channel::VoiceChannel { - role_permissions, .. }) => { if let Some(server) = &self.server { let member_roles = self @@ -343,6 +346,7 @@ impl PermissionQuery for DatabasePermissionQuery<'_> { /// (this will only ever be called for server channels, use unimplemented!() for other code paths) async fn set_server_from_channel(&mut self) { if let Some(channel) = &self.channel { + #[allow(deprecated)] match channel { Cow::Borrowed(Channel::TextChannel { server, .. }) | Cow::Owned(Channel::TextChannel { server, .. }) diff --git a/crates/core/database/src/voice/mod.rs b/crates/core/database/src/voice/mod.rs new file mode 100644 index 000000000..ea41a3fd0 --- /dev/null +++ b/crates/core/database/src/voice/mod.rs @@ -0,0 +1,390 @@ + +use livekit_protocol::ParticipantPermission; +use redis_kiss::{get_connection as _get_connection, Conn, redis::Pipeline, AsyncCommands}; +use crate::{events::client::EventV1, models::{Channel, User}, util::{permissions::DatabasePermissionQuery, reference::Reference}, Database, Server}; +use revolt_models::v0::{self, PartialUserVoiceState, UserVoiceState}; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission, PermissionValue}; +use revolt_result::{create_error, Result, ToRevoltError}; + +mod voice_client; +pub use voice_client::VoiceClient; + +async fn get_connection() -> Result { + _get_connection().await.to_internal_error() +} + +pub async fn raise_if_in_voice(user: &User, target: &str) -> Result<()> { + let mut conn = get_connection().await?; + + if user.bot.is_some() + // bots can be in as many voice channels as it wants so we just check if its already connected to the one its trying to connect to + && conn.sismember(format!("vc:{}", &user.id), target) + .await + .to_internal_error()? + { + Err(create_error!(AlreadyConnected)) + } else if conn + .scard::<_, u32>(format!("vc:{}", &user.id)) // check if the current vc set is empty + .await + .to_internal_error()? + > 0 + { + Err(create_error!(NotConnected)) + } else { + Ok(()) + } +} + +pub async fn set_channel_node(channel: &str, node: &str) -> Result<()> { + get_connection() + .await? + .set(format!("node:{channel}"), node) + .await + .to_internal_error() +} + +pub async fn get_channel_node(channel: &str) -> Result> { + get_connection() + .await? + .get(format!("node:{channel}")) + .await + .to_internal_error() +} + +pub async fn get_user_voice_channels(user_id: &str) -> Result> { + get_connection() + .await? + .smembers(format!("vc:{user_id}")) + .await + .to_internal_error() +} + +pub async fn set_user_moved_from_voice(old_channel: &str, new_channel: &str, user_id: &str) -> Result<()> { + get_connection() + .await? + .set_ex(format!("moved_to:{user_id}:{new_channel}"), old_channel, 10) + .await + .to_internal_error() +} + +pub async fn get_user_moved_from_voice(channel_id: &str, user_id: &str) -> Result> { + get_connection() + .await? + .get(format!("moved_to:{user_id}:{channel_id}")) + .await + .to_internal_error() +} + +pub async fn is_in_voice_channel(user_id: &str, channel_id: &str) -> Result { + get_connection() + .await? + .sismember(format!("vc:{}", user_id), channel_id) + .await + .to_internal_error() +} + +pub async fn get_user_voice_channel_in_server( + user_id: &str, + server_id: &str, +) -> Result> { + let mut conn = get_connection().await?; + + let unique_key = format!("{}:{}", user_id, server_id); + + conn.get::<&str, Option>(&unique_key) + .await + .to_internal_error() +} + +pub fn get_allowed_sources(permissions: PermissionValue) -> Vec<&'static str> { + let mut allowed_sources = Vec::new(); + + if permissions.has(ChannelPermission::Speak as u64) { + allowed_sources.push("microphone") + }; + + if permissions.has(ChannelPermission::Video as u64) { + allowed_sources.extend(["camera", "screen_share", "screen_share_audio"]); + }; + + allowed_sources +} + +pub async fn create_voice_state( + channel_id: &str, + server_id: Option<&str>, + user_id: &str, +) -> Result { + let unique_key = format!("{}:{}", &user_id, server_id.unwrap_or(channel_id)); + + let voice_state = UserVoiceState { + id: user_id.to_string(), + is_receiving: true, + is_publishing: false, + screensharing: false, + camera: false, + }; + + Pipeline::new() + .sadd(format!("vc_members:{channel_id}"), user_id) + .sadd(format!("vc:{user_id}"), channel_id) + .set(&unique_key, channel_id) + .set( + format!("is_publishing:{unique_key}"), + voice_state.is_publishing, + ) + .set( + format!("is_receiving:{unique_key}"), + voice_state.is_receiving, + ) + .set( + format!("screensharing:{unique_key}"), + voice_state.screensharing, + ) + .set(format!("camera:{unique_key}"), voice_state.camera) + .query_async::<_, ()>(&mut get_connection().await?.into_inner()) + .await + .to_internal_error()?; + + Ok(voice_state) +} + +pub async fn delete_voice_state( + channel_id: &str, + server_id: Option<&str>, + user_id: &str, +) -> Result<()> { + let unique_key = format!("{}:{}", &user_id, server_id.unwrap_or(channel_id)); + + Pipeline::new() + .srem(format!("vc_members:{channel_id}"), user_id) + .srem(format!("vc:{user_id}"), channel_id) + .del(&[ + format!("is_publishing:{unique_key}"), + format!("is_receiving:{unique_key}"), + format!("screensharing:{unique_key}"), + format!("camera:{unique_key}"), + unique_key.clone(), + ]) + .query_async(&mut get_connection().await?.into_inner()) + .await + .to_internal_error() +} + +pub async fn update_voice_state_tracks( + channel_id: &str, + server_id: Option<&str>, + user_id: &str, + added: bool, + track: i32, +) -> Result { + let partial = match track { + /* TrackSource::Unknown */ 0 => PartialUserVoiceState::default(), + /* TrackSource::Camera */ + 1 => PartialUserVoiceState { + camera: Some(added), + ..Default::default() + }, + /* TrackSource::Microphone */ + 2 => PartialUserVoiceState { + is_publishing: Some(added), + ..Default::default() + }, + /* TrackSource::ScreenShare | TrackSource::ScreenShareAudio */ + 3 | 4 => PartialUserVoiceState { + screensharing: Some(added), + ..Default::default() + }, + _ => unreachable!(), + }; + + update_voice_state(channel_id, server_id, user_id, &partial).await?; + + Ok(partial) +} + +pub async fn update_voice_state( + channel_id: &str, + server_id: Option<&str>, + user_id: &str, + partial: &PartialUserVoiceState, +) -> Result<()> { + let unique_key = format!("{}:{}", &user_id, server_id.unwrap_or(channel_id)); + + let mut pipeline = Pipeline::new(); + + if let Some(camera) = &partial.camera { + pipeline.set(format!("camera:{unique_key}"), camera); + }; + + if let Some(is_publishing) = &partial.is_publishing { + pipeline.set(format!("is_publishing:{unique_key}"), is_publishing); + } + + if let Some(is_receiving) = &partial.is_receiving { + pipeline.set(format!("is_receiving:{unique_key}"), is_receiving); + } + + if let Some(screensharing) = &partial.screensharing { + pipeline.set(format!("screensharing:{unique_key}"), screensharing); + } + + pipeline + .query_async(&mut get_connection().await?.into_inner()) + .await + .to_internal_error() +} + +pub async fn get_voice_channel_members(channel_id: &str) -> Result>> { + get_connection() + .await? + .smembers::<_, Option>>(format!("vc_members:{}", channel_id)) + .await + .to_internal_error() + .map(|opt| opt.and_then(|v| if v.is_empty() { + None + } else { + Some(v) + })) +} + +pub async fn get_voice_state( + channel_id: &str, + server_id: Option<&str>, + user_id: &str, +) -> Result> { + let unique_key = format!("{}:{}", user_id, server_id.unwrap_or(channel_id)); + + let (is_publishing, is_receiving, screensharing, camera) = get_connection() + .await? + .mget::<_, (Option, Option, Option, Option)>(&[ + format!("is_publishing:{unique_key}"), + format!("is_receiving:{unique_key}"), + format!("screensharing:{unique_key}"), + format!("camera:{unique_key}"), + ]) + .await + .to_internal_error()?; + + match (is_publishing, is_receiving, screensharing, camera) { + (Some(is_publishing), Some(is_receiving), Some(screensharing), Some(camera)) => { + Ok(Some(v0::UserVoiceState { + id: user_id.to_string(), + is_receiving, + is_publishing, + screensharing, + camera, + })) + } + _ => Ok(None), + } +} + +pub async fn get_channel_voice_state(channel: &Channel) -> Result> { + let members = get_voice_channel_members(channel.id()).await?; + + let server = channel.server(); + + if let Some(members) = members { + let mut participants = Vec::with_capacity(members.len()); + + for user_id in members { + if let Some(voice_state) = get_voice_state(channel.id(), server, &user_id).await? { + participants.push(voice_state); + } else { + log::info!("Voice state not found but member in voice channel members, removing."); + + delete_voice_state(channel.id(), server, &user_id).await?; + } + } + + Ok(Some(v0::ChannelVoiceState { + id: channel.id().to_string(), + participants + })) + } else { + Ok(None) + } +} + +pub async fn move_user(user: &str, from: &str, to: &str) -> Result<()> { + get_connection() + .await? + .smove( + format!("vc-members-{from}"), + format!("vc-members-{to}"), + user, + ) + .await + .to_internal_error() +} + +pub async fn sync_voice_permissions(db: &Database, voice_client: &VoiceClient, channel: &Channel, server: Option<&Server>, role_id: Option<&str>) -> Result<()> { + let node = get_channel_node(channel.id()).await?.unwrap(); + + for user_id in get_voice_channel_members(channel.id()).await?.iter().flatten() { + let user = Reference::from_unchecked(user_id.clone()).as_user(db).await?; + + sync_user_voice_permissions(db, voice_client, &node, &user, channel, server, role_id).await?; + }; + + Ok(()) +} + +pub async fn sync_user_voice_permissions(db: &Database, voice_client: &VoiceClient, node: &str, user: &User, channel: &Channel, server: Option<&Server>, role_id: Option<&str>) -> Result<()> { + let channel_id = channel.id(); + let server_id: Option<&str> = server.as_ref().map(|s| s.id.as_str()); + + let member = match server_id { + Some(server_id) => Some(Reference::from_unchecked(user.id.clone()).as_member(db, server_id).await?), + None => None + }; + + if role_id.is_none_or(|role_id| member.as_ref().is_none_or(|member| member.roles.iter().any(|r| r == role_id))) { + let voice_state = get_voice_state(channel_id, server_id, &user.id).await?.unwrap(); + + let mut query = DatabasePermissionQuery::new(db, user) + .channel(channel) + .user(user); + + if let (Some(server), Some(member)) = (server, member.as_ref()) { + query = query.member(member).server(server) + } + + let permissions = calculate_channel_permissions(&mut query).await; + + let mut update_event = PartialUserVoiceState { + id: Some(user.id.clone()), + ..Default::default() + }; + + let before = update_event.clone(); + + let can_video = permissions.has_channel_permission(ChannelPermission::Video); + let can_speak = permissions.has_channel_permission(ChannelPermission::Speak); + let can_listen = permissions.has_channel_permission(ChannelPermission::Listen); + + update_event.camera = voice_state.camera.then_some(can_video); + update_event.screensharing = voice_state.screensharing.then_some(can_video); + update_event.is_publishing = voice_state.is_publishing.then_some(can_speak); + + update_voice_state(channel_id, server_id, &user.id, &update_event).await?; + + voice_client.update_permissions(node, user, channel_id, ParticipantPermission { + can_subscribe: can_listen, + can_publish: can_speak, + can_publish_data: can_speak, + ..Default::default() + }).await?; + + if update_event != before { + EventV1::UserVoiceStateUpdate { + id: user.id.clone(), + channel_id: channel_id.to_string(), + data: update_event + }.p(channel_id.to_string()).await; + }; + }; + + Ok(()) +} \ No newline at end of file diff --git a/crates/core/database/src/voice/voice_client.rs b/crates/core/database/src/voice/voice_client.rs new file mode 100644 index 000000000..e6067f790 --- /dev/null +++ b/crates/core/database/src/voice/voice_client.rs @@ -0,0 +1,142 @@ +use livekit_api::{ + access_token::{AccessToken, VideoGrants}, + services::room::{CreateRoomOptions, RoomClient as InnerRoomClient, UpdateParticipantOptions}, +}; +use livekit_protocol::{ParticipantInfo, ParticipantPermission, Room}; +use revolt_config::{config, LiveKitNode}; +use crate::models::{Channel, User}; +use revolt_models::v0; +use revolt_permissions::{ChannelPermission, PermissionValue}; +use revolt_result::{create_error, Result, ToRevoltError}; +use std::{borrow::Cow, collections::HashMap, time::Duration}; + +use super::get_allowed_sources; + + +#[derive(Debug)] +pub struct RoomClient { + pub client: InnerRoomClient, + pub node: LiveKitNode +} + +#[derive(Debug)] +pub struct VoiceClient { + pub rooms: HashMap +} + +impl VoiceClient { + pub fn new(nodes: HashMap) -> Self { + Self { + rooms: nodes + .into_iter() + .map(|(name, node)| + ( + name, + RoomClient { + client: InnerRoomClient::with_api_key(&node.url, &node.key, &node.secret), + node + } + ) + ) + .collect() + } + } + + pub fn is_enabled(&self) -> bool { + !self.rooms.is_empty() + } + + pub async fn from_revolt_config() -> Self { + let config = config().await; + + Self::new(config.api.livekit.nodes.clone()) + } + + pub fn get_node(&self, name: &str) -> Result<&RoomClient> { + self.rooms.get(name) + .ok_or_else(|| create_error!(UnknownNode)) + } + + pub fn create_token( + &self, + node: &str, + user: &User, + permissions: PermissionValue, + channel: &Channel, + ) -> Result { + let room = self.get_node(node)?; + + let allowed_sources = get_allowed_sources(permissions); + + AccessToken::with_api_key(&room.node.key, &room.node.secret) + .with_name(&format!("{}#{}", user.username, user.discriminator)) + .with_identity(&user.id) + .with_metadata(&serde_json::to_string(&user).to_internal_error()?) + .with_ttl(Duration::from_secs(10)) + .with_grants(VideoGrants { + room_join: true, + can_publish: true, + can_publish_sources: allowed_sources.into_iter().map(ToString::to_string).collect(), + can_subscribe: permissions.has_channel_permission(ChannelPermission::Listen), + room: channel.id().to_string(), + ..Default::default() + }) + .to_jwt() + .to_internal_error() + } + + pub async fn create_room(&self, node: &str, channel: &Channel) -> Result { + let room = self.get_node(node)?; + + let voice = match channel { + Channel::DirectMessage { .. } => Some(Cow::Owned(v0::VoiceInformation::default())), + Channel::TextChannel { voice: Some(voice), .. } => Some(Cow::Borrowed(voice)), + _ => None + } + .ok_or_else(|| create_error!(NotAVoiceChannel))?; + + room.client + .create_room( + channel.id(), + CreateRoomOptions { + max_participants: voice.max_users.unwrap_or(u32::MAX), + empty_timeout: 5 * 60, // 5 minutes, + ..Default::default() + }, + ) + .await + .to_internal_error() + } + + pub async fn update_permissions( + &self, + node: &str, + user: &User, + channel_id: &str, + new_permissions: ParticipantPermission, + ) -> Result { + let room = self.get_node(node)?; + + room.client + .update_participant( + channel_id, + &user.id, + UpdateParticipantOptions { + permission: Some(new_permissions), + attributes: HashMap::new(), + name: "".to_string(), + metadata: "".to_string(), + }, + ) + .await + .to_internal_error() + } + + pub async fn remove_user(&self, node: &str, user_id: &str, channel_id: &str) -> Result<()> { + let room = self.get_node(node)?; + + room.client.remove_participant(channel_id, user_id) + .await + .to_internal_error() + } +} diff --git a/crates/core/models/src/v0/channels.rs b/crates/core/models/src/v0/channels.rs index 70e9e4443..39db5cb5b 100644 --- a/crates/core/models/src/v0/channels.rs +++ b/crates/core/models/src/v0/channels.rs @@ -1,4 +1,5 @@ -use super::File; +#![allow(deprecated)] +use super::{File, UserVoiceState}; use revolt_permissions::{Override, OverrideField}; use std::collections::{HashMap, HashSet}; @@ -107,8 +108,12 @@ auto_derived!( serde(skip_serializing_if = "crate::if_false", default) )] nsfw: bool, + + /// Voice Information for when this channel is also a voice channel + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + voice: Option }, - /// Voice channel belonging to a server + #[deprecated = "Use TextChannel { voice } instead"] VoiceChannel { /// Unique Id #[cfg_attr(feature = "serde", serde(rename = "_id"))] @@ -147,6 +152,15 @@ auto_derived!( }, } + /// Voice information for a channel + #[derive(Default)] + #[cfg_attr(feature = "validator", derive(validator::Validate))] + pub struct VoiceInformation { + /// Maximium amount of users allowed in the voice channel at once + #[cfg_attr(feature = "validator", validate(range(min = 1)))] + pub max_users: Option + } + /// Partial representation of a channel #[derive(Default)] pub struct PartialChannel { @@ -170,6 +184,8 @@ auto_derived!( pub default_permissions: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub last_message_id: Option, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub voice: Option, } /// Optional fields on channel object @@ -205,6 +221,9 @@ auto_derived!( /// Whether this channel is archived pub archived: Option, + /// Voice Information for voice channels + pub voice: Option, + /// Fields to remove from channel #[cfg_attr(feature = "serde", serde(default))] pub remove: Option>, @@ -260,6 +279,10 @@ auto_derived!( /// Whether this channel is age restricted #[serde(skip_serializing_if = "Option::is_none")] pub nsfw: Option, + + /// Voice Information for when this channel is also a voice channel + #[serde(skip_serializing_if = "Option::is_none")] + pub voice: Option } /// New default permissions @@ -289,10 +312,25 @@ auto_derived!( } /// Voice server token response - pub struct LegacyCreateVoiceUserResponse { + pub struct CreateVoiceUserResponse { /// Token for authenticating with the voice server - token: String, + pub token: String, + /// Url of the livekit server to connect to + pub url: String, + } + + /// Voice state for a channel + pub struct ChannelVoiceState { + pub id: String, + /// The states of the users who are connected to the channel + pub participants: Vec, } + + /// Join a voice channel + pub struct DataJoinCall { + pub node: Option, + } + ); impl Channel { diff --git a/crates/core/models/src/v0/messages.rs b/crates/core/models/src/v0/messages.rs index 1ef8aab03..442d4e5b5 100644 --- a/crates/core/models/src/v0/messages.rs +++ b/crates/core/models/src/v0/messages.rs @@ -132,6 +132,8 @@ auto_derived!( MessagePinned { id: String, by: String }, #[serde(rename = "message_unpinned")] MessageUnpinned { id: String, by: String }, + #[serde(rename = "call_started")] + CallStarted { by: String }, } /// Name and / or avatar override information @@ -445,6 +447,7 @@ impl From for String { } SystemMessage::MessagePinned { .. } => "Message pinned.".to_string(), SystemMessage::MessageUnpinned { .. } => "Message unpinned.".to_string(), + SystemMessage::CallStarted { .. } => "Call started.".to_string(), } } } diff --git a/crates/core/models/src/v0/server_members.rs b/crates/core/models/src/v0/server_members.rs index 5b26b88a2..d1cbe9651 100644 --- a/crates/core/models/src/v0/server_members.rs +++ b/crates/core/models/src/v0/server_members.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use super::{File, Role, User}; +use crate::if_false; use iso8601_timestamp::Timestamp; use once_cell::sync::Lazy; @@ -57,6 +58,13 @@ auto_derived_partial!( /// Timestamp this member is timed out until #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub timeout: Option, + + /// Whether the member is server-wide voice muted + #[serde(skip_serializing_if = "Option::is_none")] + pub can_publish: Option, + /// Whether the member is server-wide voice deafened + #[serde(skip_serializing_if = "Option::is_none")] + pub can_receive: Option, }, "PartialMember" ); @@ -77,6 +85,8 @@ auto_derived!( Avatar, Roles, Timeout, + CanReceive, + CanPublish, } /// Member removal intention @@ -123,8 +133,15 @@ auto_derived!( pub roles: Option>, /// Timestamp this member is timed out until pub timeout: Option, + /// server-wide voice muted + pub can_publish: Option, + /// server-wide voice deafened + pub can_receive: Option, + /// voice channel to move to if already in a voice channel + pub voice_channel: Option, /// Fields to remove from channel object #[cfg_attr(feature = "validator", validate(length(min = 1)))] pub remove: Option>, + } ); diff --git a/crates/core/models/src/v0/users.rs b/crates/core/models/src/v0/users.rs index ba913dd0f..d5e65673c 100644 --- a/crates/core/models/src/v0/users.rs +++ b/crates/core/models/src/v0/users.rs @@ -277,6 +277,18 @@ auto_derived!( } ); +auto_derived_partial!( + /// Voice State information for a user + pub struct UserVoiceState { + pub id: String, + pub is_receiving: bool, + pub is_publishing: bool, + pub screensharing: bool, + pub camera: bool, + }, + "PartialUserVoiceState" +); + pub trait CheckRelationship { fn with(&self, user: &str) -> RelationshipStatus; } diff --git a/crates/core/permissions/src/impl.rs b/crates/core/permissions/src/impl.rs index 9975eeda5..f175d6bd4 100644 --- a/crates/core/permissions/src/impl.rs +++ b/crates/core/permissions/src/impl.rs @@ -61,6 +61,14 @@ pub async fn calculate_server_permissions(query: &mut P) -> permissions.apply(role_override); } + if !query.do_we_have_publish_overwrites().await { + permissions.revoke(ChannelPermission::Speak as u64); + } + + if !query.do_we_have_receive_overwrites().await { + permissions.revoke(ChannelPermission::Listen as u64); + } + if query.are_we_timed_out().await { permissions.restrict(*ALLOW_IN_TIMEOUT); } diff --git a/crates/core/permissions/src/models/channel.rs b/crates/core/permissions/src/models/channel.rs index aca22b5c2..9e9160263 100644 --- a/crates/core/permissions/src/models/channel.rs +++ b/crates/core/permissions/src/models/channel.rs @@ -89,6 +89,8 @@ pub enum ChannelPermission { DeafenMembers = 1 << 34, /// Move members between voice channels MoveMembers = 1 << 35, + /// Listen to other users + Listen = 1 << 36, // * Channel permissions two electric boogaloo /// Mention everyone and online members @@ -130,7 +132,9 @@ pub static DEFAULT_PERMISSION: Lazy = Lazy::new(|| { + ChannelPermission::SendEmbeds + ChannelPermission::UploadFiles + ChannelPermission::Connect - + ChannelPermission::Speak, + + ChannelPermission::Speak + + ChannelPermission::Listen + + ChannelPermission::Video ) }); diff --git a/crates/core/permissions/src/models/mod.rs b/crates/core/permissions/src/models/mod.rs index 330fe7562..b8788230b 100644 --- a/crates/core/permissions/src/models/mod.rs +++ b/crates/core/permissions/src/models/mod.rs @@ -8,7 +8,7 @@ pub use server::*; pub use user::*; /// Holds a permission value to manipulate. -#[derive(Clone, Debug)] +#[derive(Copy, Clone, Debug)] pub struct PermissionValue(u64); impl PermissionValue { diff --git a/crates/core/permissions/src/test.rs b/crates/core/permissions/src/test.rs index e4dadd485..93e1dea2c 100644 --- a/crates/core/permissions/src/test.rs +++ b/crates/core/permissions/src/test.rs @@ -64,6 +64,14 @@ async fn validate_user_permissions() { unreachable!() } + async fn do_we_have_publish_overwrites(&mut self) -> bool { + true + } + + async fn do_we_have_receive_overwrites(&mut self) -> bool { + true + } + async fn get_channel_type(&mut self) -> ChannelType { ChannelType::DirectMessage } @@ -153,6 +161,14 @@ async fn validate_group_permissions() { unreachable!() } + async fn do_we_have_publish_overwrites(&mut self) -> bool { + true + } + + async fn do_we_have_receive_overwrites(&mut self) -> bool { + true + } + async fn get_channel_type(&mut self) -> ChannelType { ChannelType::Group } @@ -254,6 +270,14 @@ async fn validate_server_permissions() { false } + async fn do_we_have_publish_overwrites(&mut self) -> bool { + true + } + + async fn do_we_have_receive_overwrites(&mut self) -> bool { + true + } + async fn get_channel_type(&mut self) -> ChannelType { ChannelType::ServerChannel } @@ -346,6 +370,14 @@ async fn validate_timed_out_member() { true } + async fn do_we_have_publish_overwrites(&mut self) -> bool { + true + } + + async fn do_we_have_receive_overwrites(&mut self) -> bool { + true + } + async fn get_channel_type(&mut self) -> ChannelType { ChannelType::ServerChannel } diff --git a/crates/core/permissions/src/trait.rs b/crates/core/permissions/src/trait.rs index d4a7c3d36..96b42caf8 100644 --- a/crates/core/permissions/src/trait.rs +++ b/crates/core/permissions/src/trait.rs @@ -39,6 +39,12 @@ pub trait PermissionQuery { /// Is our perspective user timed out on this server? async fn are_we_timed_out(&mut self) -> bool; + /// Is the member muted? + async fn do_we_have_publish_overwrites(&mut self) -> bool; + + /// Is the member deafend? + async fn do_we_have_receive_overwrites(&mut self) -> bool; + // * For calculating channel permission /// Get the type of the channel diff --git a/crates/core/result/Cargo.toml b/crates/core/result/Cargo.toml index 3a25d206f..90ffd639d 100644 --- a/crates/core/result/Cargo.toml +++ b/crates/core/result/Cargo.toml @@ -32,5 +32,7 @@ rocket = { optional = true, version = "0.5.0-rc.2", default-features = false } revolt_rocket_okapi = { version = "0.10.0", optional = true } revolt_okapi = { version = "0.9.1", optional = true } +# utilities +log = "0.4" # Axum axum = { version = "0.7.5", optional = true } diff --git a/crates/core/result/src/axum.rs b/crates/core/result/src/axum.rs index e4caf8183..8750ef5f1 100644 --- a/crates/core/result/src/axum.rs +++ b/crates/core/result/src/axum.rs @@ -75,6 +75,11 @@ impl IntoResponse for Error { ErrorType::NotFound => StatusCode::NOT_FOUND, ErrorType::NoEffect => StatusCode::OK, ErrorType::FailedValidation { .. } => StatusCode::BAD_REQUEST, + ErrorType::LiveKitUnavailable => StatusCode::BAD_REQUEST, + ErrorType::NotConnected => StatusCode::BAD_REQUEST, + ErrorType::NotAVoiceChannel => StatusCode::BAD_REQUEST, + ErrorType::AlreadyConnected => StatusCode::BAD_REQUEST, + ErrorType::UnknownNode => StatusCode::BAD_REQUEST, ErrorType::InvalidFlagValue => StatusCode::BAD_REQUEST, ErrorType::FeatureDisabled { .. } => StatusCode::BAD_REQUEST, diff --git a/crates/core/result/src/lib.rs b/crates/core/result/src/lib.rs index 10b56d5d4..9fcfcd284 100644 --- a/crates/core/result/src/lib.rs +++ b/crates/core/result/src/lib.rs @@ -1,3 +1,4 @@ +use std::panic::Location; use std::fmt::Display; #[cfg(feature = "serde")] @@ -158,6 +159,12 @@ pub enum ErrorType { error: String, }, + // ? Voice errors + LiveKitUnavailable, + NotAVoiceChannel, + AlreadyConnected, + NotConnected, + UnknownNode, // ? Micro-service errors ProxyError, FileTooSmall, @@ -197,6 +204,57 @@ macro_rules! create_database_error { }; } +#[macro_export] +#[cfg(debug_assertions)] +macro_rules! query { + ( $self: ident, $type: ident, $collection: expr, $($rest:expr),+ ) => { + Ok($self.$type($collection, $($rest),+).await.unwrap()) + }; +} + +#[macro_export] +#[cfg(not(debug_assertions))] +macro_rules! query { + ( $self: ident, $type: ident, $collection: expr, $($rest:expr),+ ) => { + $self.$type($collection, $($rest),+).await + .map_err(|_| create_database_error!(stringify!($type), $collection)) + }; +} + +pub trait ToRevoltError { + #[track_caller] + fn to_internal_error(self) -> Result; +} + +impl ToRevoltError for Result { + #[track_caller] + fn to_internal_error(self) -> Result { + let loc = Location::caller(); + + self + .map_err(|_| { + Error { + error_type: ErrorType::InternalError, + location: format!("{}:{}:{}", loc.file(), loc.line(), loc.column()) + } + }) + } +} + +impl ToRevoltError for Option { + #[track_caller] + fn to_internal_error(self) -> Result { + let loc = Location::caller(); + + self.ok_or_else(|| { + Error { + error_type: ErrorType::InternalError, + location: format!("{}:{}:{}", loc.file(), loc.line(), loc.column()) + } + }) + } +} + #[cfg(test)] mod tests { use crate::ErrorType; diff --git a/crates/core/result/src/rocket.rs b/crates/core/result/src/rocket.rs index 1d996a173..6e95ec241 100644 --- a/crates/core/result/src/rocket.rs +++ b/crates/core/result/src/rocket.rs @@ -78,10 +78,14 @@ impl<'r> Responder<'r, 'static> for Error { ErrorType::InvalidSession => Status::Unauthorized, ErrorType::NotAuthenticated => Status::Unauthorized, ErrorType::DuplicateNonce => Status::Conflict, - ErrorType::VosoUnavailable => Status::BadRequest, ErrorType::NotFound => Status::NotFound, ErrorType::NoEffect => Status::Ok, ErrorType::FailedValidation { .. } => Status::BadRequest, + ErrorType::LiveKitUnavailable => Status::BadRequest, + ErrorType::NotAVoiceChannel => Status::BadRequest, + ErrorType::AlreadyConnected => Status::BadRequest, + ErrorType::NotConnected => Status::BadRequest, + ErrorType::UnknownNode => Status::BadRequest, ErrorType::FeatureDisabled { .. } => Status::BadRequest, ErrorType::ProxyError => Status::BadRequest, @@ -90,6 +94,7 @@ impl<'r> Responder<'r, 'static> for Error { ErrorType::FileTypeNotAllowed => Status::BadRequest, ErrorType::ImageProcessingFailed => Status::InternalServerError, ErrorType::NoEmbedData => Status::BadRequest, + ErrorType::VosoUnavailable => Status::BadRequest, }; // Serialize the error data structure into JSON. diff --git a/crates/daemons/pushd/src/consumers/outbound/apn.rs b/crates/daemons/pushd/src/consumers/outbound/apn.rs index 7cc43c76a..cf8cea17d 100644 --- a/crates/daemons/pushd/src/consumers/outbound/apn.rs +++ b/crates/daemons/pushd/src/consumers/outbound/apn.rs @@ -63,6 +63,7 @@ impl ApnsOutboundConsumer { // in a dm it should just be "Sendername". // not sure how feasible all those are given the PushNotification object as it currently stands. + #[allow(deprecated)] match ¬ification.channel { Channel::DirectMessage { .. } => notification.author.clone(), Channel::Group { name, .. } => format!("{}, #{}", notification.author, name), diff --git a/crates/daemons/pushd/src/consumers/outbound/fcm.rs b/crates/daemons/pushd/src/consumers/outbound/fcm.rs index 1e3f1d2ad..10e174d4c 100644 --- a/crates/daemons/pushd/src/consumers/outbound/fcm.rs +++ b/crates/daemons/pushd/src/consumers/outbound/fcm.rs @@ -27,6 +27,7 @@ impl FcmOutboundConsumer { // in a dm it should just be "Sendername". // not sure how feasible all those are given the PushNotification object as it currently stands. + #[allow(deprecated)] match ¬ification.channel { Channel::DirectMessage { .. } => notification.author.clone(), Channel::Group { name, .. } => format!("{}, #{}", notification.author, name), diff --git a/crates/daemons/voice-ingress/.gitignore b/crates/daemons/voice-ingress/.gitignore new file mode 100644 index 000000000..c41cc9e35 --- /dev/null +++ b/crates/daemons/voice-ingress/.gitignore @@ -0,0 +1 @@ +/target \ No newline at end of file diff --git a/crates/daemons/voice-ingress/Cargo.toml b/crates/daemons/voice-ingress/Cargo.toml new file mode 100644 index 000000000..21393e26d --- /dev/null +++ b/crates/daemons/voice-ingress/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "revolt-voice-ingress" +version = "0.7.1" +license = "AGPL-3.0-or-later" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# util +log = "*" +sentry = "0.31.5" +lru = "0.7.6" +ulid = "0.5.0" +redis-kiss = "0.1.4" + +# serde +serde_json = "1.0.79" +rmp-serde = "1.0.0" +serde = "1.0.136" + +# http +rocket = { version = "0.5.0-rc.2", features = ["json"] } +rocket_empty = "0.1.1" + +# async +futures = "0.3.21" +async-std = { version = "1.8.0", features = [ + "tokio1", + "tokio02", + "attributes", +] } + +# core +revolt-result = { path = "../../core/result" } +revolt-models = { path = "../../core/models" } +revolt-config = { path = "../../core/config" } +revolt-database = { path = "../../core/database" } +revolt-permissions = { path = "../../core/permissions" } + +# voice +livekit-api = "0.4.4" +livekit-protocol = "0.4.0" +livekit-runtime = { version = "0.3.1", features = ["tokio"] } diff --git a/crates/daemons/voice-ingress/Dockerfile b/crates/daemons/voice-ingress/Dockerfile new file mode 100644 index 000000000..4749d5cc9 --- /dev/null +++ b/crates/daemons/voice-ingress/Dockerfile @@ -0,0 +1,11 @@ +# Build Stage +FROM ghcr.io/revoltchat/base:latest AS builder +FROM debian:12 AS debian + +# Bundle Stage +FROM gcr.io/distroless/cc-debian12:nonroot +COPY --from=builder /home/rust/src/target/release/revolt-voice-ingress ./ +COPY --from=debian /usr/bin/uname /usr/bin/uname + +USER nonroot +CMD ["./revolt-voice-ingress"] \ No newline at end of file diff --git a/crates/daemons/voice-ingress/src/main.rs b/crates/daemons/voice-ingress/src/main.rs new file mode 100644 index 000000000..9602fa1ca --- /dev/null +++ b/crates/daemons/voice-ingress/src/main.rs @@ -0,0 +1,162 @@ +use std::env; + + +use livekit_protocol::TrackType; +use livekit_api::{access_token::TokenVerifier, webhooks::WebhookReceiver}; +use revolt_database::{ + events::client::EventV1, util::reference::Reference, voice::{create_voice_state, delete_voice_state, get_user_moved_from_voice, update_voice_state_tracks, VoiceClient}, Database, DatabaseInfo +}; +use rocket::{build, http::Status, post, request::{FromRequest, Outcome}, routes, Config, Request, State}; +use rocket_empty::EmptyResponse; +use std::net::Ipv4Addr; +use revolt_result::{create_error, Error, Result, ToRevoltError}; + +#[rocket::main] +async fn main() -> Result<(), rocket::Error> { + revolt_config::configure!(voice_ingress); + + let database = DatabaseInfo::Auto.connect().await.unwrap(); + let voice_client = VoiceClient::from_revolt_config().await; + let _rocket = build() + .manage(database) + .manage(voice_client) + .mount("/", routes![ingress]) + .configure(Config { + port: 8500, + address: Ipv4Addr::new(0, 0, 0, 0).into(), + ..Default::default() + }) + .ignite() + .await? + .launch() + .await?; + + Ok(()) +} + +struct AuthHeader<'a>(&'a str); + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for AuthHeader<'r> { + type Error = Error; + + async fn from_request(request: &'r Request<'_>) -> Outcome { + match request.headers().get("Authorization").next() { + Some(token) => Outcome::Success(Self(token)), + None => Outcome::Error((Status::Unauthorized, create_error!(NotAuthenticated))) + } + } +} + +impl std::ops::Deref for AuthHeader<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.0 + } +} + +#[post("/", data = "")] +async fn ingress(db: &State, node: &str, voice_client: &State, auth_header: AuthHeader<'_>, body: &str) -> Result { + log::debug!("received event: {body:?}"); + + let config = revolt_config::config().await; + + let node_info = config.api.livekit.nodes.get(node) + .ok_or_else(|| create_error!(NotAuthenticated))?; + + let webhook_receiver = WebhookReceiver::new(TokenVerifier::with_api_key(&node_info.key, &node_info.secret)); + let event = webhook_receiver.receive(body, &auth_header).to_internal_error()?; + + let channel_id = event.room.as_ref().map(|r| &r.name); + let user_id = event.participant.as_ref().map(|r| &r.identity); + + match event.event.as_str() { + "participant_joined" => { + let channel_id = channel_id.to_internal_error()?; + let user_id = user_id.to_internal_error()?; + + let channel = Reference::from_unchecked(channel_id.clone()) + .as_channel(db) + .await?; + + let voice_state = create_voice_state(channel_id, channel.server(), user_id).await?; + + if let Some(moved_from) = get_user_moved_from_voice(channel_id, user_id).await? { + EventV1::VoiceChannelMove { + user: user_id.clone(), + from: moved_from, + to: channel_id.clone(), + state: voice_state + } + .p(channel_id.clone()) + .await; + } else { + EventV1::VoiceChannelJoin { + id: channel_id.clone(), + state: voice_state, + } + .p(channel_id.clone()) + .await; + }; + } + "participant_left" => { + let channel_id = channel_id.to_internal_error()?; + let user_id = user_id.to_internal_error()?; + + let channel = Reference::from_unchecked(channel_id.clone()) + .as_channel(db) + .await?; + + delete_voice_state(channel_id, channel.server(), user_id).await?; + + EventV1::VoiceChannelLeave { + id: channel_id.clone(), + user: user_id.clone(), + } + .p(channel_id.clone()) + .await + } + "track_published" | "track_unpublished" => { + let channel_id = channel_id.to_internal_error()?; + let user_id = user_id.to_internal_error()?; + let track = event.track.as_ref().to_internal_error()?; + + let channel = Reference::from_unchecked(channel_id.clone()) + .as_channel(db) + .await?; + + // remove the user if they try publish a video larger than 1080x720 or they publish data + // TODO: move to config + if event.event == "track_published" + && ( + // handle any size which goes over the limit of "1080x720" to stop people from making too tall or too wide and bypassing the limit + (track.r#type == TrackType::Video as i32 && (track.width * track.height) >= (1080 * 720)) + | (track.r#type == TrackType::Data as i32) + ) + { + voice_client.remove_user(node, user_id, channel_id).await?; + delete_voice_state(channel_id, channel.server(), user_id).await?; + } + + let partial = update_voice_state_tracks( + channel_id, + channel.server(), + user_id, + event.event == "track_published", // to avoid duplicating this entire case twice + track.source + ).await?; + + EventV1::UserVoiceStateUpdate { + id: user_id.clone(), + channel_id: channel_id.clone(), + data: partial + } + .p(channel_id.clone()) + .await; + } + _ => {} + }; + + Ok(EmptyResponse) +} diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index d07b44bb9..05ac9115a 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -82,5 +82,9 @@ revolt-presence = { path = "../core/presence" } revolt-result = { path = "../core/result", features = ["rocket", "okapi"] } revolt-permissions = { path = "../core/permissions", features = ["schemas"] } +# voice +livekit-api = "0.4.4" +livekit-protocol = "0.4.0" + [build-dependencies] vergen = "7.5.0" diff --git a/crates/delta/src/main.rs b/crates/delta/src/main.rs index 9cce39ce4..90fe8b80d 100644 --- a/crates/delta/src/main.rs +++ b/crates/delta/src/main.rs @@ -24,6 +24,7 @@ use amqprs::{ use async_std::channel::unbounded; use authifier::AuthifierEvent; use rocket::data::ToByteUnit; +use revolt_database::voice::VoiceClient; pub async fn web() -> Rocket { // Get settings @@ -34,6 +35,7 @@ pub async fn web() -> Rocket { // Setup database let db = revolt_database::DatabaseInfo::Auto.connect().await.unwrap(); + log::info!("database_here {db:?}"); db.migrate_database().await.unwrap(); // Setup Authifier event channel @@ -89,6 +91,16 @@ pub async fn web() -> Rocket { ) .into(); + let swagger_0_8 = revolt_rocket_okapi::swagger_ui::make_swagger_ui( + &revolt_rocket_okapi::swagger_ui::SwaggerUIConfig { + url: "/0.8/openapi.json".to_owned(), + ..Default::default() + }, + ) + .into(); + + // Voice handler + let voice_client = VoiceClient::new(config.api.livekit.nodes.clone()); // Configure Rabbit let connection = Connection::open(&OpenConnectionArguments::new( &config.rabbit.host, @@ -133,6 +145,7 @@ pub async fn web() -> Rocket { .manage(db) .manage(amqp) .manage(cors.clone()) + .manage(voice_client) .attach(util::ratelimiter::RatelimitFairing) .attach(cors) .configure(rocket::Config { diff --git a/crates/delta/src/routes/channels/channel_delete.rs b/crates/delta/src/routes/channels/channel_delete.rs index ca09ae2b6..2c7008e0c 100644 --- a/crates/delta/src/routes/channels/channel_delete.rs +++ b/crates/delta/src/routes/channels/channel_delete.rs @@ -1,6 +1,5 @@ use revolt_database::{ - util::{permissions::DatabasePermissionQuery, reference::Reference}, - Channel, Database, PartialChannel, User, AMQP, + util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{delete_voice_state, get_channel_node, get_voice_channel_members, VoiceClient}, Channel, Database, PartialChannel, User, AMQP }; use revolt_models::v0; use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; @@ -15,6 +14,7 @@ use rocket_empty::EmptyResponse; #[delete("/?")] pub async fn delete( db: &State, + voice_client: &State, amqp: &State, user: User, target: Reference, @@ -26,8 +26,9 @@ pub async fn delete( permissions.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?; + #[allow(deprecated)] match &channel { - Channel::SavedMessages { .. } => Err(create_error!(NoEffect)), + Channel::SavedMessages { .. } => Err(create_error!(NoEffect))?, Channel::DirectMessage { .. } => channel .update( db, @@ -37,8 +38,7 @@ pub async fn delete( }, vec![], ) - .await - .map(|_| EmptyResponse), + .await?, Channel::Group { .. } => channel .remove_user_from_group( db, @@ -47,13 +47,23 @@ pub async fn delete( None, options.leave_silently.unwrap_or_default(), ) - .await - .map(|_| EmptyResponse), + .await?, Channel::TextChannel { .. } | Channel::VoiceChannel { .. } => { permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?; - channel.delete(db).await.map(|_| EmptyResponse) + channel.delete(db).await? } - } + }; + + if let Some(users) = get_voice_channel_members(channel.id()).await? { + let node = get_channel_node(channel.id()).await?.unwrap(); + + for user in users { + voice_client.remove_user(&node, &user, channel.id()).await?; + delete_voice_state(channel.id(), channel.server(), &user).await?; + } + }; + + Ok(EmptyResponse) } #[cfg(test)] diff --git a/crates/delta/src/routes/channels/channel_edit.rs b/crates/delta/src/routes/channels/channel_edit.rs index 1977ec9ba..d69fb8b3e 100644 --- a/crates/delta/src/routes/channels/channel_edit.rs +++ b/crates/delta/src/routes/channels/channel_edit.rs @@ -95,22 +95,6 @@ pub async fn edit( icon, nsfw, .. - } - | Channel::TextChannel { - id, - name, - description, - icon, - nsfw, - .. - } - | Channel::VoiceChannel { - id, - name, - description, - icon, - nsfw, - .. } => { if let Some(fields) = &data.remove { if fields.contains(&v0::FieldsChannel::Icon) { @@ -153,77 +137,129 @@ pub async fn edit( } // Send out mutation system messages. - if let Channel::Group { .. } = &channel { - if let Some(name) = &partial.name { - SystemMessage::ChannelRenamed { - name: name.to_string(), - by: user.id.clone(), - } - .into_message(channel.id().to_string()) - .send( - db, - Some(amqp), - user.as_author_for_system(), - None, - None, - &channel, - false, - ) - .await - .ok(); + if let Some(name) = &partial.name { + SystemMessage::ChannelRenamed { + name: name.to_string(), + by: user.id.clone(), } + .into_message(channel.id().to_string()) + .send( + db, + Some(amqp), + user.as_author_for_system(), + None, + None, + &channel, + false, + ) + .await + .ok(); + } - if partial.description.is_some() { - SystemMessage::ChannelDescriptionChanged { - by: user.id.clone(), + if partial.description.is_some() { + SystemMessage::ChannelDescriptionChanged { + by: user.id.clone(), + } + .into_message(channel.id().to_string()) + .send( + db, + Some(amqp), + user.as_author_for_system(), + None, + None, + &channel, + false, + ) + .await + .ok(); + } + + if partial.icon.is_some() { + SystemMessage::ChannelIconChanged { + by: user.id.clone(), + } + .into_message(channel.id().to_string()) + .send( + db, + Some(amqp), + user.as_author_for_system(), + None, + None, + &channel, + false, + ) + .await + .ok(); + } + } + Channel::TextChannel { + id, + name, + description, + icon, + nsfw, + voice, + .. + } => { + if let Some(fields) = &data.remove { + if fields.contains(&v0::FieldsChannel::Icon) { + if let Some(icon) = &icon { + db.mark_attachment_as_deleted(&icon.id).await?; } - .into_message(channel.id().to_string()) - .send( - db, - Some(amqp), - user.as_author_for_system(), - None, - None, - &channel, - false, - ) - .await - .ok(); } - if partial.icon.is_some() { - SystemMessage::ChannelIconChanged { - by: user.id.clone(), + for field in fields { + match field { + v0::FieldsChannel::Description => { + description.take(); + } + v0::FieldsChannel::Icon => { + icon.take(); + } + _ => {} } - .into_message(channel.id().to_string()) - .send( - db, - Some(amqp), - user.as_author_for_system(), - None, - None, - &channel, - false, - ) - .await - .ok(); } } - channel - .update( - db, - partial, - data.remove - .unwrap_or_default() - .into_iter() - .map(|f| f.into()) - .collect(), - ) - .await?; + if let Some(icon_id) = data.icon { + partial.icon = Some(File::use_channel_icon(db, &icon_id, id, &user.id).await?); + *icon = partial.icon.clone(); + } + + if let Some(new_name) = data.name { + *name = new_name.clone(); + partial.name = Some(new_name); + } + + if let Some(new_description) = data.description { + partial.description = Some(new_description); + *description = partial.description.clone(); + } + + if let Some(new_nsfw) = data.nsfw { + *nsfw = new_nsfw; + partial.nsfw = Some(new_nsfw); + } + + if let Some(new_voice) = data.voice { + *voice = Some(new_voice.clone()); + partial.voice = Some(new_voice); + } } _ => return Err(create_error!(InvalidOperation)), }; + channel + .update( + db, + partial, + data.remove + .unwrap_or_default() + .into_iter() + .map(|f| f.into()) + .collect(), + ) + .await?; + Ok(Json(channel.into())) } diff --git a/crates/delta/src/routes/channels/group_remove_member.rs b/crates/delta/src/routes/channels/group_remove_member.rs index 90cccfefa..fc63acc16 100644 --- a/crates/delta/src/routes/channels/group_remove_member.rs +++ b/crates/delta/src/routes/channels/group_remove_member.rs @@ -1,4 +1,4 @@ -use revolt_database::{util::reference::Reference, Channel, Database, User, AMQP}; +use revolt_database::{util::reference::Reference, voice::{delete_voice_state, get_channel_node, is_in_voice_channel, raise_if_in_voice, VoiceClient}, Channel, Database, User, AMQP}; use revolt_permissions::ChannelPermission; use revolt_result::{create_error, Result}; @@ -12,6 +12,7 @@ use rocket_empty::EmptyResponse; #[delete("//recipients/")] pub async fn remove_member( db: &State, + voice_client: &State, amqp: &State, user: User, target: Reference, @@ -23,32 +24,37 @@ pub async fn remove_member( let channel = target.as_channel(db).await?; - match &channel { - Channel::Group { - owner, recipients, .. - } => { - if &user.id != owner { - return Err(create_error!(MissingPermission { - permission: ChannelPermission::ManageChannel.to_string() - })); - } - - let member = member.as_user(db).await?; - if user.id == member.id { - return Err(create_error!(CannotRemoveYourself)); - } - - if !recipients.iter().any(|x| *x == member.id) { - return Err(create_error!(NotInGroup)); - } - - channel - .remove_user_from_group(db, amqp, &member, Some(&user.id), false) - .await - .map(|_| EmptyResponse) + if let Channel::Group { owner, recipients, .. } = &channel { + if &user.id != owner { + return Err(create_error!(MissingPermission { + permission: ChannelPermission::ManageChannel.to_string() + })); } - _ => Err(create_error!(InvalidOperation)), - } + + let member = member.as_user(db).await?; + if user.id == member.id { + return Err(create_error!(CannotRemoveYourself)); + } + + if !recipients.iter().any(|x| *x == member.id) { + return Err(create_error!(NotInGroup)); + } + + channel + .remove_user_from_group(db, amqp, &member, Some(&user.id), false) + .await?; + } else { + return Err(create_error!(InvalidOperation)) + }; + + if is_in_voice_channel(&user.id, channel.id()).await? { + let node = get_channel_node(channel.id()).await?.unwrap(); + + voice_client.remove_user(&node, &user.id, channel.id()).await?; + delete_voice_state(channel.id(), None, &user.id).await?; + }; + + Ok(EmptyResponse) } #[cfg(test)] diff --git a/crates/delta/src/routes/channels/message_query.rs b/crates/delta/src/routes/channels/message_query.rs index 5193bd02a..2ab6e8da5 100644 --- a/crates/delta/src/routes/channels/message_query.rs +++ b/crates/delta/src/routes/channels/message_query.rs @@ -65,6 +65,7 @@ pub async fn query( }, &user, include_users, + #[allow(deprecated)] match channel { Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => { Some(server) diff --git a/crates/delta/src/routes/channels/message_search.rs b/crates/delta/src/routes/channels/message_search.rs index eb173bc05..b3b90071e 100644 --- a/crates/delta/src/routes/channels/message_search.rs +++ b/crates/delta/src/routes/channels/message_search.rs @@ -69,6 +69,7 @@ pub async fn search( }, &user, include_users, + #[allow(deprecated)] match channel { Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => { Some(server) diff --git a/crates/delta/src/routes/channels/message_send.rs b/crates/delta/src/routes/channels/message_send.rs index 704b91017..0db142bb0 100644 --- a/crates/delta/src/routes/channels/message_send.rs +++ b/crates/delta/src/routes/channels/message_send.rs @@ -151,6 +151,7 @@ mod test { name: "Hidden Channel".to_string(), description: None, nsfw: Some(false), + voice: None }, true, ) @@ -193,6 +194,7 @@ mod test { d: ChannelPermission::ViewChannel as i64, }), last_message_id: None, + voice: None, }; locked_channel .update(&harness.db, partial, vec![]) @@ -287,6 +289,8 @@ mod test { avatar: None, timeout: None, roles: Some(second_member_roles), + can_publish: None, + can_receive: None }; second_member .update(&harness.db, partial, vec![]) @@ -613,6 +617,8 @@ mod test { nickname: None, roles: Some(vec![role_id.clone()]), timeout: None, + can_publish: None, + can_receive: None }, vec![], ) diff --git a/crates/delta/src/routes/channels/permissions_set.rs b/crates/delta/src/routes/channels/permissions_set.rs index 70ded35db..d5ba55cf4 100644 --- a/crates/delta/src/routes/channels/permissions_set.rs +++ b/crates/delta/src/routes/channels/permissions_set.rs @@ -1,6 +1,5 @@ use revolt_database::{ - util::{permissions::DatabasePermissionQuery, reference::Reference}, - Database, User, + util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{sync_voice_permissions, VoiceClient}, Channel, Database, User }; use revolt_models::v0; use revolt_permissions::{calculate_channel_permissions, ChannelPermission, Override}; @@ -16,14 +15,15 @@ use rocket::{serde::json::Json, State}; #[put("//permissions/", data = "", rank = 2)] pub async fn set_role_permissions( db: &State, + voice_client: &State, user: User, target: Reference, role_id: String, data: Json, ) -> Result> { - let mut channel = target.as_channel(db).await?; + let channel = target.as_channel(db).await?; let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); - let permissions = calculate_channel_permissions(&mut query).await; + let permissions: revolt_permissions::PermissionValue = calculate_channel_permissions(&mut query).await; permissions.throw_if_lacking_channel_permission(ChannelPermission::ManagePermissions)?; @@ -38,11 +38,15 @@ pub async fn set_role_permissions( .throw_permission_override(current_value, &data.permissions) .await?; - channel + let mut new_channel = channel.clone(); + + new_channel .set_role_permission(db, &role_id, data.permissions.clone().into()) .await?; - Ok(Json(channel.into())) + sync_voice_permissions(db, voice_client, &new_channel, Some(server), Some(&role_id)).await?; + + Ok(Json(new_channel.into())) } else { Err(create_error!(NotFound)) } diff --git a/crates/delta/src/routes/channels/permissions_set_default.rs b/crates/delta/src/routes/channels/permissions_set_default.rs index 5957426ab..14386281c 100644 --- a/crates/delta/src/routes/channels/permissions_set_default.rs +++ b/crates/delta/src/routes/channels/permissions_set_default.rs @@ -1,6 +1,5 @@ use revolt_database::{ - util::{permissions::DatabasePermissionQuery, reference::Reference}, - Channel, Database, PartialChannel, User, + util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{sync_voice_permissions, VoiceClient}, Channel, Database, PartialChannel, User }; use revolt_models::v0::{self, DataDefaultChannelPermissions}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; @@ -16,6 +15,7 @@ use rocket::{serde::json::Json, State}; #[put("//permissions/default", data = "", rank = 1)] pub async fn set_default_permissions( db: &State, + voice_client: &State, user: User, target: Reference, data: Json, @@ -48,10 +48,6 @@ pub async fn set_default_permissions( Channel::TextChannel { default_permissions, .. - } - | Channel::VoiceChannel { - default_permissions, - .. } => { if let DataDefaultChannelPermissions::Field { permissions: field } = data { permissions @@ -75,5 +71,12 @@ pub async fn set_default_permissions( _ => return Err(create_error!(InvalidOperation)), } + let server = match channel.server() { + Some(server_id) => Some(Reference::from_unchecked(server_id.to_string()).as_server(db).await?), + None => None + }; + + sync_voice_permissions(db, voice_client, &channel, server.as_ref(), None).await?; + Ok(Json(channel.into())) } diff --git a/crates/delta/src/routes/channels/voice_join.rs b/crates/delta/src/routes/channels/voice_join.rs index b15d59c0c..7c3c86cd2 100644 --- a/crates/delta/src/routes/channels/voice_join.rs +++ b/crates/delta/src/routes/channels/voice_join.rs @@ -1,105 +1,66 @@ use revolt_config::config; -use revolt_database::{ - util::{permissions::DatabasePermissionQuery, reference::Reference}, - Channel, Database, User, -}; use revolt_models::v0; +use revolt_database::{util::{permissions::perms, reference::Reference}, voice::{get_channel_node, raise_if_in_voice, VoiceClient}, Channel, Database, SystemMessage, User, AMQP}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_result::{create_error, Result}; + use rocket::{serde::json::Json, State}; /// # Join Call /// /// Asks the voice server for a token to join the call. #[openapi(tag = "Voice")] -#[post("//join_call")] -pub async fn call( - db: &State, - user: User, - target: Reference, -) -> Result> { - let channel = target.as_channel(db).await?; - let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); - calculate_channel_permissions(&mut query) - .await - .throw_if_lacking_channel_permission(ChannelPermission::Connect)?; +#[post("//join_call", data="")] +pub async fn call(db: &State, amqp: &State, voice: &State, user: User, target: Reference, data: Json) -> Result> { + if !voice.is_enabled() { + return Err(create_error!(LiveKitUnavailable)) + } let config = config().await; - if config.api.security.voso_legacy_token.is_empty() { - return Err(create_error!(VosoUnavailable)); - } - match channel { - Channel::SavedMessages { .. } | Channel::TextChannel { .. } => { - return Err(create_error!(CannotJoinCall)) - } - _ => {} - } + let channel = target.as_channel(db).await?; + + raise_if_in_voice(&user, channel.id()).await?; + + let mut permissions = perms(db, &user).channel(&channel); - // To join a call: - // - Check if the room exists. - // - If not, create it. - let client = reqwest::Client::new(); - let result = client - .get(format!( - "{}/room/{}", - config.hosts.voso_legacy, - channel.id() - )) - .header( - reqwest::header::AUTHORIZATION, - config.api.security.voso_legacy_token.clone(), - ) - .send() - .await; + let current_permissions = calculate_channel_permissions(&mut permissions).await; + current_permissions.throw_if_lacking_channel_permission(ChannelPermission::Connect)?; - match result { - Err(_) => return Err(create_error!(VosoUnavailable)), - Ok(result) => match result.status() { - reqwest::StatusCode::OK => (), - reqwest::StatusCode::NOT_FOUND => { - if (client - .post(format!( - "{}/room/{}", - config.hosts.voso_legacy, - channel.id() - )) - .header( - reqwest::header::AUTHORIZATION, - config.api.security.voso_legacy_token.clone(), - ) - .send() - .await) - .is_err() - { - return Err(create_error!(VosoUnavailable)); - } + let existing_node = get_channel_node(channel.id()).await?; + + let node = existing_node.or(data.into_inner().node) + .ok_or_else(|| create_error!(UnknownNode))?; + + let node_host = config.hosts.livekit.get(&node) + .ok_or_else(|| create_error!(UnknownNode))? + .clone(); + + let token = voice.create_token(&node, &user, current_permissions, &channel)?; + let room = voice.create_room(&node, &channel).await?; + + log::debug!("Created room {}", room.name); + + match &channel { + Channel::DirectMessage { .. } | Channel::Group { .. } => { + SystemMessage::CallStarted { + by: user.id.clone() } - _ => return Err(create_error!(VosoUnavailable)), - }, - } + .into_message(channel.id().to_string()) + .send( + db, + Some(amqp), + v0::MessageAuthor::System { + username: &user.username, + avatar: user.avatar.as_ref().map(|file| file.id.as_ref()), + }, + None, + None, + &channel, false + ).await?; + } + _ => {} + }; - // Then create a user for the room. - if let Ok(response) = client - .post(format!( - "{}/room/{}/user/{}", - config.hosts.voso_legacy, - channel.id(), - user.id - )) - .header( - reqwest::header::AUTHORIZATION, - config.api.security.voso_legacy_token, - ) - .send() - .await - { - response - .json() - .await - .map_err(|_| create_error!(InvalidOperation)) - .map(Json) - } else { - Err(create_error!(VosoUnavailable)) - } + Ok(Json(v0::CreateVoiceUserResponse { token, url: node_host.clone() })) } diff --git a/crates/delta/src/routes/invites/invite_fetch.rs b/crates/delta/src/routes/invites/invite_fetch.rs index 306a680e7..641741b9a 100644 --- a/crates/delta/src/routes/invites/invite_fetch.rs +++ b/crates/delta/src/routes/invites/invite_fetch.rs @@ -23,13 +23,6 @@ pub async fn fetch(db: &State, target: Reference) -> Result { let server = db.fetch_server(&server).await?; @@ -202,6 +195,7 @@ mod test { name: "Voice Channel".to_string(), description: None, nsfw: Some(false), + voice: None }, true, ) diff --git a/crates/delta/src/routes/root.rs b/crates/delta/src/routes/root.rs index c90c53c0b..aa3293cd3 100644 --- a/crates/delta/src/routes/root.rs +++ b/crates/delta/src/routes/root.rs @@ -21,15 +21,22 @@ pub struct Feature { pub url: String, } +/// # Information about a livekit node +#[derive(Serialize, JsonSchema, Debug)] +pub struct VoiceNode { + pub name: String, + pub lat: f64, + pub lon: f64, + pub public_url: String +} + /// # Voice Server Configuration #[derive(Serialize, JsonSchema, Debug)] pub struct VoiceFeature { /// Whether voice is enabled pub enabled: bool, - /// URL pointing to the voice API - pub url: String, - /// URL pointing to the voice WebSocket server - pub ws: String, + /// All livekit nodes + pub nodes: Vec, } /// # Feature Configuration @@ -46,7 +53,7 @@ pub struct RevoltFeatures { /// Proxy service configuration pub january: Feature, /// Voice server configuration - pub voso: VoiceFeature, + pub livekit: VoiceFeature, } /// # Build Information @@ -94,22 +101,32 @@ pub async fn root() -> Result> { features: RevoltFeatures { captcha: CaptchaFeature { enabled: !config.api.security.captcha.hcaptcha_key.is_empty(), - key: config.api.security.captcha.hcaptcha_sitekey, + key: config.api.security.captcha.hcaptcha_sitekey.clone(), }, email: !config.api.smtp.host.is_empty(), invite_only: config.api.registration.invite_only, autumn: Feature { enabled: !config.hosts.autumn.is_empty(), - url: config.hosts.autumn, + url: config.hosts.autumn.clone(), }, january: Feature { enabled: !config.hosts.january.is_empty(), - url: config.hosts.january, + url: config.hosts.january.clone(), }, - voso: VoiceFeature { - enabled: !config.hosts.voso_legacy.is_empty(), - url: config.hosts.voso_legacy, - ws: config.hosts.voso_legacy_ws, + livekit: VoiceFeature { + enabled: !config.hosts.livekit.is_empty(), + nodes: config.api.livekit.nodes + .iter() + .filter(|(_, node)| !node.private) + .map(|(name, value)| { + VoiceNode { + name: name.clone(), + lat: value.lat, + lon: value.lon, + public_url: config.hosts.livekit.get(name).expect("Missing corresponding host for voice node").clone() + } + }) + .collect() }, }, ws: config.hosts.events, diff --git a/crates/delta/src/routes/servers/ban_create.rs b/crates/delta/src/routes/servers/ban_create.rs index a73be1142..6e530b1a4 100644 --- a/crates/delta/src/routes/servers/ban_create.rs +++ b/crates/delta/src/routes/servers/ban_create.rs @@ -1,5 +1,6 @@ use revolt_database::{ util::{permissions::DatabasePermissionQuery, reference::Reference}, + voice::{delete_voice_state, get_channel_node, get_user_voice_channel_in_server, VoiceClient}, Database, RemovalIntention, ServerBan, User, }; use revolt_models::v0; @@ -16,6 +17,7 @@ use validator::Validate; #[put("//bans/", data = "")] pub async fn ban( db: &State, + voice_client: &State, user: User, server: Reference, target: Reference, @@ -54,6 +56,13 @@ pub async fn ban( member .remove(db, &server, RemovalIntention::Ban, false) .await?; + + // If the member is in a voice channel while banned kick them from the voice channel + if let Some(channel_id) = get_user_voice_channel_in_server(&user.id, &server.id).await? { + let node = get_channel_node(&channel_id).await?.unwrap(); + voice_client.remove_user(&node, &user.id, &channel_id).await?; + delete_voice_state(&channel_id, Some(&server.id), &user.id).await? + } } ServerBan::create(db, &server, &target.id, data.reason) diff --git a/crates/delta/src/routes/servers/member_edit.rs b/crates/delta/src/routes/servers/member_edit.rs index 005e65c7c..89a74b0e0 100644 --- a/crates/delta/src/routes/servers/member_edit.rs +++ b/crates/delta/src/routes/servers/member_edit.rs @@ -1,14 +1,20 @@ use std::collections::HashSet; +use futures::TryFutureExt; +use livekit_api::services::room::{RoomClient, UpdateParticipantOptions}; +use livekit_protocol::ParticipantPermission; +use redis_kiss::{get_connection, redis::Pipeline, AsyncCommands}; use revolt_database::{ - util::{permissions::DatabasePermissionQuery, reference::Reference}, + events::client::EventV1, + util::{permissions::{perms, DatabasePermissionQuery}, reference::Reference}, + voice::{get_channel_node, get_user_voice_channel_in_server, move_user, set_channel_node, set_user_moved_from_voice, sync_user_voice_permissions, VoiceClient}, Database, File, PartialMember, User, }; -use revolt_models::v0; +use revolt_models::v0::{self, FieldsMember, PartialUserVoiceState}; -use revolt_permissions::{calculate_server_permissions, ChannelPermission}; -use revolt_result::{create_error, Result}; -use rocket::{serde::json::Json, State}; +use revolt_permissions::{calculate_channel_permissions, calculate_server_permissions, ChannelPermission}; +use revolt_result::{create_error, Result, ToRevoltError}; +use rocket::{form::validate::Contains, serde::json::Json, State}; use validator::Validate; /// # Edit Member @@ -18,6 +24,7 @@ use validator::Validate; #[patch("//members/", data = "")] pub async fn edit( db: &State, + voice_client: &State, user: User, server: Reference, member: Reference, @@ -32,6 +39,7 @@ pub async fn edit( // Fetch server and member let mut server = server.as_server(db).await?; + let target_user = member.as_user(&db).await?; let mut member = member.as_member(db, &server.id).await?; // Fetch our currrent permissions @@ -91,6 +99,41 @@ pub async fn edit( permissions.throw_if_lacking_channel_permission(ChannelPermission::TimeoutMembers)?; } + if data.can_publish.is_some() { + permissions.throw_if_lacking_channel_permission(ChannelPermission::MuteMembers)?; + } + + if data.can_receive.is_some() { + permissions.throw_if_lacking_channel_permission(ChannelPermission::DeafenMembers)?; + } + + let new_voice_channel = if let Some(new_channel) = &data.voice_channel { + if !voice_client.is_enabled() { + return Err(create_error!(LiveKitUnavailable)) + }; + + permissions.throw_if_lacking_channel_permission(ChannelPermission::MoveMembers)?; + + // ensure the channel we are moving them to is in the server and is a voice channel + + let channel = Reference::from_unchecked(new_channel.clone()) + .as_channel(db) + .await + .map_err(|_| create_error!(UnknownChannel))?; + + if channel.server().is_none_or(|v| v != member.id.server) { + Err(create_error!(UnknownChannel))? + } + + if get_user_voice_channel_in_server(&target_user.id, &server.id).await?.is_none() { + Err(create_error!(NotConnected))? + }; + + Some(channel) + } else { + None + }; + // Resolve our ranking let our_ranking = query.get_member_rank().unwrap_or(i64::MIN); @@ -126,12 +169,17 @@ pub async fn edit( roles, timeout, remove, + can_publish, + can_receive, + voice_channel: _, } = data; let mut partial = PartialMember { nickname, roles, timeout, + can_publish, + can_receive, ..Default::default() }; @@ -149,6 +197,11 @@ pub async fn edit( partial.avatar = Some(File::use_user_avatar(db, &avatar, &user.id, &user.id).await?); } + let remove_contains_voice = remove + .as_ref() + .map(|r| r.contains(FieldsMember::CanPublish) || r.contains(FieldsMember::CanReceive)) + .unwrap_or_default(); + member .update( db, @@ -159,5 +212,43 @@ pub async fn edit( ) .await?; + if let Some(new_voice_channel) = new_voice_channel { + if let Some(channel) = get_user_voice_channel_in_server(&target_user.id, &server.id).await? { + let old_node = get_channel_node(&channel).await?.unwrap(); + + let new_node = match get_channel_node(new_voice_channel.id()).await? { + Some(node) => node, + None => { + set_channel_node(new_voice_channel.id(), &old_node).await?; + old_node.clone() + } + }; + + set_user_moved_from_voice(&channel, new_voice_channel.id(), &target_user.id).await?; + + let mut query = perms(db, &target_user).channel(&new_voice_channel); + let permissions = calculate_channel_permissions(&mut query).await; + + voice_client.create_room(&new_node, &new_voice_channel).await?; + let token = voice_client.create_token(&new_node, &target_user, permissions, &new_voice_channel)?; + + voice_client.remove_user(&old_node, &target_user.id, &channel).await?; + + EventV1::UserMoveVoiceChannel { + node: new_node, + token + } + .p_user(target_user.id.clone(), db) + .await; + }; + } else if can_publish.is_some() || can_receive.is_some() || remove_contains_voice { + if let Some(channel) = get_user_voice_channel_in_server(&target_user.id, &server.id).await? { + let node = get_channel_node(&channel).await?.unwrap(); + let channel = Reference::from_unchecked(channel).as_channel(db).await?; + + sync_user_voice_permissions(db, voice_client, &node, &user, &channel, Some(&server), None).await?; + }; + }; + Ok(Json(member.into())) } diff --git a/crates/delta/src/routes/servers/member_remove.rs b/crates/delta/src/routes/servers/member_remove.rs index a7e2f50af..1f8ea9eef 100644 --- a/crates/delta/src/routes/servers/member_remove.rs +++ b/crates/delta/src/routes/servers/member_remove.rs @@ -1,5 +1,6 @@ use revolt_database::{ util::{permissions::DatabasePermissionQuery, reference::Reference}, + voice::{delete_voice_state, get_channel_node, get_user_voice_channel_in_server, VoiceClient}, Database, RemovalIntention, User, }; use revolt_permissions::{calculate_server_permissions, ChannelPermission}; @@ -14,6 +15,7 @@ use rocket_empty::EmptyResponse; #[delete("//members/")] pub async fn kick( db: &State, + voice_client: &State, user: User, target: Reference, member: Reference, @@ -40,6 +42,13 @@ pub async fn kick( return Err(create_error!(NotElevated)); } + if let Some(channel_id) = get_user_voice_channel_in_server(&user.id, &server.id).await? { + let node = get_channel_node(&channel_id).await?.unwrap(); + + voice_client.remove_user(&node, &user.id, &channel_id).await?; + delete_voice_state(&channel_id, Some(&server.id), &user.id).await?; + } + member .remove(db, &server, RemovalIntention::Kick, false) .await diff --git a/crates/delta/src/routes/servers/permissions_set.rs b/crates/delta/src/routes/servers/permissions_set.rs index 0c293aa89..3d80d3873 100644 --- a/crates/delta/src/routes/servers/permissions_set.rs +++ b/crates/delta/src/routes/servers/permissions_set.rs @@ -1,6 +1,5 @@ use revolt_database::{ - util::{permissions::DatabasePermissionQuery, reference::Reference}, - Database, User, + util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{sync_voice_permissions, VoiceClient}, Database, User }; use revolt_models::v0; use revolt_permissions::{calculate_server_permissions, ChannelPermission, Override}; @@ -14,6 +13,7 @@ use rocket::{serde::json::Json, State}; #[put("//permissions/", data = "", rank = 2)] pub async fn set_role_permission( db: &State, + voice_client: &State, user: User, target: Reference, role_id: String, @@ -40,6 +40,12 @@ pub async fn set_role_permission( .throw_permission_override(current_value, &data.permissions) .await?; + for channel_id in &server.channels { + let channel = Reference::from_unchecked(channel_id.clone()).as_channel(db).await?; + + sync_voice_permissions(db, voice_client, &channel, Some(&server), Some(&role_id)).await?; + }; + server .set_role_permission(db, &role_id, data.permissions.into()) .await?; diff --git a/crates/delta/src/routes/servers/permissions_set_default.rs b/crates/delta/src/routes/servers/permissions_set_default.rs index 96bda2de9..42c364d83 100644 --- a/crates/delta/src/routes/servers/permissions_set_default.rs +++ b/crates/delta/src/routes/servers/permissions_set_default.rs @@ -1,6 +1,5 @@ use revolt_database::{ - util::{permissions::DatabasePermissionQuery, reference::Reference}, - Database, PartialServer, User, + util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{sync_voice_permissions, VoiceClient}, Database, PartialServer, Server, User }; use revolt_models::v0; use revolt_permissions::{ @@ -16,6 +15,7 @@ use rocket::{serde::json::Json, State}; #[put("//permissions/default", data = "", rank = 1)] pub async fn set_default_permissions( db: &State, + voice_client: &State, user: User, target: Reference, data: Json, @@ -39,6 +39,12 @@ pub async fn set_default_permissions( ) .await?; + for channel_id in &server.channels { + let channel = Reference::from_unchecked(channel_id.clone()).as_channel(db).await?; + + sync_voice_permissions(db, voice_client, &channel, Some(&server), None).await?; + }; + server .update( db, diff --git a/crates/delta/src/routes/servers/roles_delete.rs b/crates/delta/src/routes/servers/roles_delete.rs index 9063dae24..38b408597 100644 --- a/crates/delta/src/routes/servers/roles_delete.rs +++ b/crates/delta/src/routes/servers/roles_delete.rs @@ -1,6 +1,7 @@ use revolt_database::{ util::{permissions::DatabasePermissionQuery, reference::Reference}, - Database, User, + voice::{sync_voice_permissions, VoiceClient}, + Database, User }; use revolt_permissions::{calculate_server_permissions, ChannelPermission}; use revolt_result::{create_error, Result}; @@ -17,6 +18,7 @@ pub async fn delete( user: User, target: Reference, role_id: String, + voice_client: &State ) -> Result { let mut server = target.as_server(db).await?; let mut query = DatabasePermissionQuery::new(db, &user).server(&server); @@ -31,6 +33,12 @@ pub async fn delete( return Err(create_error!(NotElevated)); } + for channel_id in &server.channels { + let channel = Reference::from_unchecked(channel_id.clone()).as_channel(db).await?; + + sync_voice_permissions(db, voice_client, &channel, Some(&server), Some(&role_id)).await?; + }; + role.delete(db, &server.id, &role_id) .await .map(|_| EmptyResponse) diff --git a/crates/delta/src/routes/servers/roles_edit.rs b/crates/delta/src/routes/servers/roles_edit.rs index f8152efe2..db0fca9d2 100644 --- a/crates/delta/src/routes/servers/roles_edit.rs +++ b/crates/delta/src/routes/servers/roles_edit.rs @@ -1,6 +1,7 @@ use revolt_database::{ util::{permissions::DatabasePermissionQuery, reference::Reference}, - Database, PartialRole, User, + voice::{sync_voice_permissions, VoiceClient}, + Database, PartialRole, User }; use revolt_models::v0; use revolt_permissions::{calculate_server_permissions, ChannelPermission}; @@ -15,6 +16,7 @@ use validator::Validate; #[patch("//roles/", data = "", rank = 1)] pub async fn edit( db: &State, + voice_client: &State, user: User, target: Reference, role_id: String, @@ -67,6 +69,12 @@ pub async fn edit( ) .await?; + for channel_id in &server.channels { + let channel = Reference::from_unchecked(channel_id.clone()).as_channel(db).await?; + + sync_voice_permissions(db, voice_client, &channel, Some(&server), Some(&role_id)).await?; + }; + Ok(Json(role.into())) } else { Err(create_error!(NotFound)) diff --git a/crates/delta/src/routes/servers/server_delete.rs b/crates/delta/src/routes/servers/server_delete.rs index 28a3bf42a..667a171fa 100644 --- a/crates/delta/src/routes/servers/server_delete.rs +++ b/crates/delta/src/routes/servers/server_delete.rs @@ -1,4 +1,8 @@ -use revolt_database::{util::reference::Reference, Database, RemovalIntention, User}; +use revolt_database::{ + util::reference::Reference, + voice::{delete_voice_state, get_channel_node, get_user_voice_channel_in_server, get_voice_channel_members, VoiceClient}, + Database, RemovalIntention, User, +}; use revolt_models::v0; use revolt_result::Result; use rocket::State; @@ -12,6 +16,7 @@ use rocket_empty::EmptyResponse; #[delete("/?")] pub async fn delete( db: &State, + voice_client: &State, user: User, target: Reference, options: v0::OptionsServerDelete, @@ -20,8 +25,27 @@ pub async fn delete( let member = db.fetch_member(&target.id, &user.id).await?; if server.owner == user.id { + for channel_id in &server.channels { + if let Some(users) = get_voice_channel_members(channel_id).await? { + let node = get_channel_node(channel_id).await?.unwrap(); + + for user in users { + voice_client.remove_user(&node, &user, channel_id).await?; + delete_voice_state(channel_id, Some(&server.id), &user).await?; + } + } + } + server.delete(db).await } else { + if let Some(channel_id) = get_user_voice_channel_in_server(&user.id, &server.id).await? { + if server.channels.iter().any(|c| c == &channel_id) { + let node = get_channel_node(&channel_id).await?.unwrap(); + voice_client.remove_user(&node, &user.id, &channel_id).await?; + delete_voice_state(&channel_id, Some(&server.id), &user.id).await?; + } + }; + member .remove( db, diff --git a/crates/delta/src/util/test.rs b/crates/delta/src/util/test.rs index 067f8bd0a..b90a1a5cd 100644 --- a/crates/delta/src/util/test.rs +++ b/crates/delta/src/util/test.rs @@ -165,6 +165,7 @@ impl TestHarness { name: "Test Channel".to_string(), description: None, nsfw: Some(false), + voice: None }, true, ) diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 000000000..31578d3bf --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" \ No newline at end of file