forked from lavina/lavina
1
0
Fork 0

Compare commits

..

No commits in common. "dc2446abaf8b763d8d04cea78dd5a58ae875aa2a" and "57b6af87326ec68e2698710feacd467efdb25693" have entirely different histories.

26 changed files with 594 additions and 1086 deletions

244
Cargo.lock generated
View File

@ -41,9 +41,9 @@ dependencies = [
[[package]] [[package]]
name = "allocator-api2" name = "allocator-api2"
version = "0.2.18" version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
[[package]] [[package]]
name = "android-tzdata" name = "android-tzdata"
@ -110,9 +110,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.82" version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
[[package]] [[package]]
name = "assert_matches" name = "assert_matches"
@ -140,15 +140,15 @@ dependencies = [
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.2.0" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.71" version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
dependencies = [ dependencies = [
"addr2line", "addr2line",
"cc", "cc",
@ -165,12 +165,6 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
[[package]] [[package]]
name = "base64ct" name = "base64ct"
version = "1.6.0" version = "1.6.0"
@ -203,9 +197,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.16.0" version = "3.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
@ -221,15 +215,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.6.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.94" version = "1.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -239,23 +233,23 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.38" version = "0.4.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e"
dependencies = [ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
"num-traits", "num-traits",
"wasm-bindgen", "wasm-bindgen",
"windows-targets 0.52.5", "windows-targets 0.52.4",
] ]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.4" version = "4.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -275,14 +269,14 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.4" version = "4.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.60", "syn 2.0.53",
] ]
[[package]] [[package]]
@ -326,9 +320,9 @@ dependencies = [
[[package]] [[package]]
name = "crc" name = "crc"
version = "3.2.1" version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe"
dependencies = [ dependencies = [
"crc-catalog", "crc-catalog",
] ]
@ -366,9 +360,9 @@ dependencies = [
[[package]] [[package]]
name = "der" name = "der"
version = "0.7.9" version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c"
dependencies = [ dependencies = [
"const-oid", "const-oid",
"pem-rfc7468", "pem-rfc7468",
@ -408,9 +402,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]] [[package]]
name = "either" name = "either"
version = "1.11.0" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -450,15 +444,15 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.0.2" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]] [[package]]
name = "figment" name = "figment"
version = "0.10.18" version = "0.10.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d032832d74006f99547004d49410a4b4218e4c33382d56ca3ff89df74f86b953" checksum = "7270677e7067213e04f323b55084586195f18308cd7546cfac9f873344ccceb6"
dependencies = [ dependencies = [
"atomic", "atomic",
"pear", "pear",
@ -552,7 +546,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.60", "syn 2.0.53",
] ]
[[package]] [[package]]
@ -596,9 +590,9 @@ dependencies = [
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.14" version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
@ -732,9 +726,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.3.1" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
@ -805,9 +799,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.2.6" version = "2.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
@ -836,9 +830,9 @@ dependencies = [
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.11" version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
@ -956,9 +950,9 @@ dependencies = [
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.2" version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]] [[package]]
name = "mgmt-api" name = "mgmt-api"
@ -1152,7 +1146,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"proc-macro2-diagnostics", "proc-macro2-diagnostics",
"quote", "quote",
"syn 2.0.60", "syn 2.0.53",
] ]
[[package]] [[package]]
@ -1187,14 +1181,14 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.60", "syn 2.0.53",
] ]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.14" version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]] [[package]]
name = "pin-utils" name = "pin-utils"
@ -1237,9 +1231,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.81" version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -1252,7 +1246,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.60", "syn 2.0.53",
"version_check", "version_check",
"yansi", "yansi",
] ]
@ -1263,7 +1257,6 @@ version = "0.0.2-dev"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags 2.5.0", "bitflags 2.5.0",
"chrono",
"futures-util", "futures-util",
"lavina-core", "lavina-core",
"nonempty", "nonempty",
@ -1349,9 +1342,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.36" version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -1397,9 +1390,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.10.4" version = "1.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -1420,17 +1413,17 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.3" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.3" version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e6cc1e89e689536eb5aeede61520e874df5a4707df811cd5da4aa5fbb2aae19" checksum = "58b48d98d932f4ee75e541614d32a7f44c889b72bd9c2e04d95edd135989df88"
dependencies = [ dependencies = [
"base64 0.22.0", "base64",
"bytes", "bytes",
"futures-core", "futures-core",
"futures-util", "futures-util",
@ -1540,7 +1533,7 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [ dependencies = [
"base64 0.21.7", "base64",
] ]
[[package]] [[package]]
@ -1564,7 +1557,7 @@ name = "sasl"
version = "0.0.2-dev" version = "0.0.2-dev"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.22.0", "base64",
] ]
[[package]] [[package]]
@ -1591,29 +1584,29 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.198" version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.198" version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.60", "syn 2.0.53",
] ]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.116" version = "1.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -1702,9 +1695,9 @@ dependencies = [
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.13.2" version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
[[package]] [[package]]
name = "socket2" name = "socket2"
@ -1846,7 +1839,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418"
dependencies = [ dependencies = [
"atoi", "atoi",
"base64 0.21.7", "base64",
"bitflags 2.5.0", "bitflags 2.5.0",
"byteorder", "byteorder",
"bytes", "bytes",
@ -1888,7 +1881,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e"
dependencies = [ dependencies = [
"atoi", "atoi",
"base64 0.21.7", "base64",
"bitflags 2.5.0", "bitflags 2.5.0",
"byteorder", "byteorder",
"crc", "crc",
@ -1955,9 +1948,9 @@ dependencies = [
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
[[package]] [[package]]
name = "subtle" name = "subtle"
@ -1978,9 +1971,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.60" version = "2.0.53"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2022,7 +2015,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.60", "syn 2.0.53",
] ]
[[package]] [[package]]
@ -2052,9 +2045,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.37.0" version = "1.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@ -2077,7 +2070,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.60", "syn 2.0.53",
] ]
[[package]] [[package]]
@ -2113,9 +2106,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.11" version = "0.22.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb686a972ccef8537b39eead3968b0e8616cb5040dbb9bba93007c8e07c9215f" checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
@ -2172,7 +2165,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.60", "syn 2.0.53",
] ]
[[package]] [[package]]
@ -2362,7 +2355,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.60", "syn 2.0.53",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -2396,7 +2389,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.60", "syn 2.0.53",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -2455,7 +2448,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [ dependencies = [
"windows-targets 0.52.5", "windows-targets 0.52.4",
] ]
[[package]] [[package]]
@ -2473,7 +2466,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [ dependencies = [
"windows-targets 0.52.5", "windows-targets 0.52.4",
] ]
[[package]] [[package]]
@ -2493,18 +2486,17 @@ dependencies = [
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.5" version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
dependencies = [ dependencies = [
"windows_aarch64_gnullvm 0.52.5", "windows_aarch64_gnullvm 0.52.4",
"windows_aarch64_msvc 0.52.5", "windows_aarch64_msvc 0.52.4",
"windows_i686_gnu 0.52.5", "windows_i686_gnu 0.52.4",
"windows_i686_gnullvm", "windows_i686_msvc 0.52.4",
"windows_i686_msvc 0.52.5", "windows_x86_64_gnu 0.52.4",
"windows_x86_64_gnu 0.52.5", "windows_x86_64_gnullvm 0.52.4",
"windows_x86_64_gnullvm 0.52.5", "windows_x86_64_msvc 0.52.4",
"windows_x86_64_msvc 0.52.5",
] ]
[[package]] [[package]]
@ -2515,9 +2507,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.52.5" version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
@ -2527,9 +2519,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.52.5" version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
@ -2539,15 +2531,9 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.52.5" version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
@ -2557,9 +2543,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.52.5" version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
@ -2569,9 +2555,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.52.5" version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
@ -2581,9 +2567,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.52.5" version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
@ -2593,24 +2579,24 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.52.5" version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.6" version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
[[package]] [[package]]
name = "winreg" name = "winreg"
version = "0.52.0" version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-sys 0.48.0", "windows-sys 0.48.0",
@ -2639,7 +2625,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.60", "syn 2.0.53",
] ]
[[package]] [[package]]

View File

@ -27,11 +27,10 @@ clap = { version = "4.4.4", features = ["derive"] }
serde = { version = "1.0.152", features = ["rc", "serde_derive"] } serde = { version = "1.0.152", features = ["rc", "serde_derive"] }
tracing = "0.1.37" # logging & tracing api tracing = "0.1.37" # logging & tracing api
prometheus = { version = "0.13.3", default-features = false } prometheus = { version = "0.13.3", default-features = false }
base64 = "0.22.0" base64 = "0.21.3"
lavina-core = { path = "crates/lavina-core" } lavina-core = { path = "crates/lavina-core" }
tracing-subscriber = "0.3.16" tracing-subscriber = "0.3.16"
sasl = { path = "crates/sasl" } sasl = { path = "crates/sasl" }
chrono = "0.4.37"
[package] [package]
name = "lavina" name = "lavina"

View File

@ -5,9 +5,9 @@ version.workspace = true
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
sqlx = { version = "0.7.4", features = ["sqlite", "migrate"] } sqlx = { version = "0.7.0-alpha.2", features = ["sqlite", "migrate"] }
serde.workspace = true serde.workspace = true
tokio.workspace = true tokio.workspace = true
tracing.workspace = true tracing.workspace = true
prometheus.workspace = true prometheus.workspace = true
chrono.workspace = true chrono = "0.4.37"

View File

@ -7,17 +7,16 @@
//! //!
//! A player actor is a serial handler of commands from a single player. It is preferable to run all per-player validations in the player actor, //! A player actor is a serial handler of commands from a single player. It is preferable to run all per-player validations in the player actor,
//! so that they don't overload the room actor. //! so that they don't overload the room actor.
use std::collections::{HashMap, HashSet}; use std::{
use std::sync::Arc; collections::{HashMap, HashSet},
sync::{Arc, RwLock},
};
use chrono::{DateTime, Utc};
use prometheus::{IntGauge, Registry as MetricsRegistry}; use prometheus::{IntGauge, Registry as MetricsRegistry};
use serde::Serialize; use serde::Serialize;
use tokio::sync::mpsc::{channel, Receiver, Sender}; use tokio::sync::mpsc::{channel, Receiver, Sender};
use tokio::sync::RwLock;
use crate::prelude::*; use crate::prelude::*;
use crate::repo::Storage;
use crate::room::{RoomHandle, RoomId, RoomInfo, RoomRegistry}; use crate::room::{RoomHandle, RoomId, RoomInfo, RoomRegistry};
use crate::table::{AnonTable, Key as AnonKey}; use crate::table::{AnonTable, Key as AnonKey};
@ -58,7 +57,7 @@ pub struct PlayerConnection {
} }
impl PlayerConnection { impl PlayerConnection {
/// Handled in [Player::send_message]. /// Handled in [Player::send_message].
pub async fn send_message(&mut self, room_id: RoomId, body: Str) -> Result<SendMessageResult> { pub async fn send_message(&mut self, room_id: RoomId, body: Str) -> Result<()> {
let (promise, deferred) = oneshot(); let (promise, deferred) = oneshot();
let cmd = ClientCommand::SendMessage { room_id, body, promise }; let cmd = ClientCommand::SendMessage { room_id, body, promise };
self.player_handle.send(ActorCommand::ClientCommand(cmd, self.connection_id.clone())).await; self.player_handle.send(ActorCommand::ClientCommand(cmd, self.connection_id.clone())).await;
@ -164,7 +163,7 @@ pub enum ClientCommand {
SendMessage { SendMessage {
room_id: RoomId, room_id: RoomId,
body: Str, body: Str,
promise: Promise<SendMessageResult>, promise: Promise<()>,
}, },
ChangeTopic { ChangeTopic {
room_id: RoomId, room_id: RoomId,
@ -178,15 +177,9 @@ pub enum ClientCommand {
pub enum JoinResult { pub enum JoinResult {
Success(RoomInfo), Success(RoomInfo),
AlreadyJoined,
Banned, Banned,
} }
pub enum SendMessageResult {
Success(DateTime<Utc>),
NoSuchRoom,
}
/// Player update event type which is sent to a player actor and from there to a connection handler. /// Player update event type which is sent to a player actor and from there to a connection handler.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Updates { pub enum Updates {
@ -198,7 +191,6 @@ pub enum Updates {
room_id: RoomId, room_id: RoomId,
author_id: PlayerId, author_id: PlayerId,
body: Str, body: Str,
created_at: DateTime<Utc>,
}, },
RoomJoined { RoomJoined {
room_id: RoomId, room_id: RoomId,
@ -216,41 +208,36 @@ pub enum Updates {
#[derive(Clone)] #[derive(Clone)]
pub struct PlayerRegistry(Arc<RwLock<PlayerRegistryInner>>); pub struct PlayerRegistry(Arc<RwLock<PlayerRegistryInner>>);
impl PlayerRegistry { impl PlayerRegistry {
pub fn empty( pub fn empty(room_registry: RoomRegistry, metrics: &mut MetricsRegistry) -> Result<PlayerRegistry> {
room_registry: RoomRegistry,
storage: Storage,
metrics: &mut MetricsRegistry,
) -> Result<PlayerRegistry> {
let metric_active_players = IntGauge::new("chat_players_active", "Number of alive player actors")?; let metric_active_players = IntGauge::new("chat_players_active", "Number of alive player actors")?;
metrics.register(Box::new(metric_active_players.clone()))?; metrics.register(Box::new(metric_active_players.clone()))?;
let inner = PlayerRegistryInner { let inner = PlayerRegistryInner {
room_registry, room_registry,
storage,
players: HashMap::new(), players: HashMap::new(),
metric_active_players, metric_active_players,
}; };
Ok(PlayerRegistry(Arc::new(RwLock::new(inner)))) Ok(PlayerRegistry(Arc::new(RwLock::new(inner))))
} }
pub async fn get_or_launch_player(&mut self, id: &PlayerId) -> PlayerHandle { pub async fn get_or_create_player(&mut self, id: PlayerId) -> PlayerHandle {
let mut inner = self.0.write().await; let mut inner = self.0.write().unwrap();
if let Some((handle, _)) = inner.players.get(id) { if let Some((handle, _)) = inner.players.get(&id) {
handle.clone() handle.clone()
} else { } else {
let (handle, fiber) = Player::launch(id.clone(), inner.room_registry.clone(), inner.storage.clone()).await; let (handle, fiber) = Player::launch(id.clone(), inner.room_registry.clone());
inner.players.insert(id.clone(), (handle.clone(), fiber)); inner.players.insert(id, (handle.clone(), fiber));
inner.metric_active_players.inc(); inner.metric_active_players.inc();
handle handle
} }
} }
pub async fn connect_to_player(&mut self, id: &PlayerId) -> PlayerConnection { pub async fn connect_to_player(&mut self, id: PlayerId) -> PlayerConnection {
let player_handle = self.get_or_launch_player(id).await; let player_handle = self.get_or_create_player(id).await;
player_handle.subscribe().await player_handle.subscribe().await
} }
pub async fn shutdown_all(&mut self) -> Result<()> { pub async fn shutdown_all(&mut self) -> Result<()> {
let mut inner = self.0.write().await; let mut inner = self.0.write().unwrap();
for (i, (k, j)) in inner.players.drain() { for (i, (k, j)) in inner.players.drain() {
k.send(ActorCommand::Stop).await; k.send(ActorCommand::Stop).await;
drop(k); drop(k);
@ -265,8 +252,6 @@ impl PlayerRegistry {
/// The player registry state representation. /// The player registry state representation.
struct PlayerRegistryInner { struct PlayerRegistryInner {
room_registry: RoomRegistry, room_registry: RoomRegistry,
storage: Storage,
/// Active player actors.
players: HashMap<PlayerId, (PlayerHandle, JoinHandle<Player>)>, players: HashMap<PlayerId, (PlayerHandle, JoinHandle<Player>)>,
metric_active_players: IntGauge, metric_active_players: IntGauge,
} }
@ -274,49 +259,32 @@ struct PlayerRegistryInner {
/// Player actor inner state representation. /// Player actor inner state representation.
struct Player { struct Player {
player_id: PlayerId, player_id: PlayerId,
storage_id: u32,
connections: AnonTable<Sender<Updates>>, connections: AnonTable<Sender<Updates>>,
my_rooms: HashMap<RoomId, RoomHandle>, my_rooms: HashMap<RoomId, RoomHandle>,
banned_from: HashSet<RoomId>, banned_from: HashSet<RoomId>,
rx: Receiver<ActorCommand>, rx: Receiver<ActorCommand>,
handle: PlayerHandle, handle: PlayerHandle,
rooms: RoomRegistry, rooms: RoomRegistry,
storage: Storage,
} }
impl Player { impl Player {
async fn launch(player_id: PlayerId, rooms: RoomRegistry, storage: Storage) -> (PlayerHandle, JoinHandle<Player>) { fn launch(player_id: PlayerId, rooms: RoomRegistry) -> (PlayerHandle, JoinHandle<Player>) {
let (tx, rx) = channel(32); let (tx, rx) = channel(32);
let handle = PlayerHandle { tx }; let handle = PlayerHandle { tx };
let handle_clone = handle.clone(); let handle_clone = handle.clone();
let storage_id = storage.retrieve_user_id_by_name(player_id.as_inner()).await.unwrap().unwrap();
let player = Player { let player = Player {
player_id, player_id,
storage_id,
// connections are empty when the actor is just started
connections: AnonTable::new(), connections: AnonTable::new(),
// room handlers will be loaded later in the started task
my_rooms: HashMap::new(), my_rooms: HashMap::new(),
// TODO implement and load bans banned_from: HashSet::from([RoomId::from("Empty").unwrap()]),
banned_from: HashSet::new(),
rx, rx,
handle, handle,
rooms, rooms,
storage,
}; };
let fiber = tokio::task::spawn(player.main_loop()); let fiber = tokio::task::spawn(player.main_loop());
(handle_clone, fiber) (handle_clone, fiber)
} }
async fn main_loop(mut self) -> Self { async fn main_loop(mut self) -> Self {
let rooms = self.storage.get_rooms_of_a_user(self.storage_id).await.unwrap();
for room_id in rooms {
let room = self.rooms.get_room(&room_id).await;
if let Some(room) = room {
self.my_rooms.insert(room_id, room);
} else {
tracing::error!("Room #{room_id:?} not found");
}
}
while let Some(cmd) = self.rx.recv().await { while let Some(cmd) = self.rx.recv().await {
match cmd { match cmd {
ActorCommand::AddConnection { sender, promise } => { ActorCommand::AddConnection { sender, promise } => {
@ -374,8 +342,8 @@ impl Player {
let _ = promise.send(()); let _ = promise.send(());
} }
ClientCommand::SendMessage { room_id, body, promise } => { ClientCommand::SendMessage { room_id, body, promise } => {
let result = self.send_message(connection_id, room_id, body).await; self.send_message(connection_id, room_id, body).await;
let _ = promise.send(result); let _ = promise.send(());
} }
ClientCommand::ChangeTopic { ClientCommand::ChangeTopic {
room_id, room_id,
@ -396,9 +364,6 @@ impl Player {
if self.banned_from.contains(&room_id) { if self.banned_from.contains(&room_id) {
return JoinResult::Banned; return JoinResult::Banned;
} }
if self.my_rooms.contains_key(&room_id) {
return JoinResult::AlreadyJoined;
}
let room = match self.rooms.get_or_create_room(room_id.clone()).await { let room = match self.rooms.get_or_create_room(room_id.clone()).await {
Ok(room) => room, Ok(room) => room,
@ -407,8 +372,7 @@ impl Player {
todo!(); todo!();
} }
}; };
room.add_member(&self.player_id, self.storage_id).await; room.subscribe(self.player_id.clone(), self.handle.clone()).await;
room.subscribe(&self.player_id, self.handle.clone()).await;
self.my_rooms.insert(room_id.clone(), room.clone()); self.my_rooms.insert(room_id.clone(), room.clone());
let room_info = room.get_room_info().await; let room_info = room.get_room_info().await;
let update = Updates::RoomJoined { let update = Updates::RoomJoined {
@ -423,7 +387,6 @@ impl Player {
let room = self.my_rooms.remove(&room_id); let room = self.my_rooms.remove(&room_id);
if let Some(room) = room { if let Some(room) = room {
room.unsubscribe(&self.player_id).await; room.unsubscribe(&self.player_id).await;
room.remove_member(&self.player_id, self.storage_id).await;
} }
let update = Updates::RoomLeft { let update = Updates::RoomLeft {
room_id, room_id,
@ -432,29 +395,28 @@ impl Player {
self.broadcast_update(update, connection_id).await; self.broadcast_update(update, connection_id).await;
} }
async fn send_message(&mut self, connection_id: ConnectionId, room_id: RoomId, body: Str) -> SendMessageResult { async fn send_message(&mut self, connection_id: ConnectionId, room_id: RoomId, body: Str) {
let Some(room) = self.my_rooms.get(&room_id) else { let room = self.rooms.get_room(&room_id).await;
if let Some(room) = room {
room.send_message(self.player_id.clone(), body.clone()).await;
} else {
tracing::info!("no room found"); tracing::info!("no room found");
return SendMessageResult::NoSuchRoom; }
};
let created_at = chrono::Utc::now();
room.send_message(&self.player_id, body.clone(), created_at.clone()).await;
let update = Updates::NewMessage { let update = Updates::NewMessage {
room_id, room_id,
author_id: self.player_id.clone(), author_id: self.player_id.clone(),
body, body,
created_at,
}; };
self.broadcast_update(update, connection_id).await; self.broadcast_update(update, connection_id).await;
SendMessageResult::Success(created_at)
} }
async fn change_topic(&mut self, connection_id: ConnectionId, room_id: RoomId, new_topic: Str) { async fn change_topic(&mut self, connection_id: ConnectionId, room_id: RoomId, new_topic: Str) {
let Some(room) = self.my_rooms.get(&room_id) else { let room = self.rooms.get_room(&room_id).await;
if let Some(mut room) = room {
room.set_topic(self.player_id.clone(), new_topic.clone()).await;
} else {
tracing::info!("no room found"); tracing::info!("no room found");
return; }
};
room.set_topic(&self.player_id, new_topic.clone()).await;
let update = Updates::RoomTopicChanged { room_id, new_topic }; let update = Updates::RoomTopicChanged { room_id, new_topic };
self.broadcast_update(update, connection_id).await; self.broadcast_update(update, connection_id).await;
} }

View File

@ -4,7 +4,6 @@ use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use anyhow::anyhow; use anyhow::anyhow;
use chrono::{DateTime, Utc};
use serde::Deserialize; use serde::Deserialize;
use sqlx::sqlite::SqliteConnectOptions; use sqlx::sqlite::SqliteConnectOptions;
use sqlx::{ConnectOptions, Connection, FromRow, Sqlite, SqliteConnection, Transaction}; use sqlx::{ConnectOptions, Connection, FromRow, Sqlite, SqliteConnection, Transaction};
@ -12,9 +11,6 @@ use tokio::sync::Mutex;
use crate::prelude::*; use crate::prelude::*;
mod room;
mod user;
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct StorageConfig { pub struct StorageConfig {
pub db_path: String, pub db_path: String,
@ -52,7 +48,7 @@ impl Storage {
Ok(res) Ok(res)
} }
pub async fn retrieve_room_by_name(&self, name: &str) -> Result<Option<StoredRoom>> { pub async fn retrieve_room_by_name(&mut self, name: &str) -> Result<Option<StoredRoom>> {
let mut executor = self.conn.lock().await; let mut executor = self.conn.lock().await;
let res = sqlx::query_as( let res = sqlx::query_as(
"select id, name, topic, message_count "select id, name, topic, message_count
@ -81,14 +77,7 @@ impl Storage {
Ok(id) Ok(id)
} }
pub async fn insert_message( pub async fn insert_message(&mut self, room_id: u32, id: u32, content: &str, author_id: &str) -> Result<()> {
&mut self,
room_id: u32,
id: u32,
content: &str,
author_id: &str,
created_at: &DateTime<Utc>,
) -> Result<()> {
let mut executor = self.conn.lock().await; let mut executor = self.conn.lock().await;
let res: Option<(u32,)> = sqlx::query_as("select id from users where name = ?;") let res: Option<(u32,)> = sqlx::query_as("select id from users where name = ?;")
.bind(author_id) .bind(author_id)
@ -106,7 +95,7 @@ impl Storage {
.bind(id) .bind(id)
.bind(content) .bind(content)
.bind(author_id) .bind(author_id)
.bind(created_at.to_string()) .bind(chrono::Utc::now().to_string())
.bind(room_id) .bind(room_id)
.execute(&mut *executor) .execute(&mut *executor)
.await?; .await?;

View File

@ -1,48 +0,0 @@
use anyhow::Result;
use crate::repo::Storage;
impl Storage {
pub async fn add_room_member(&self, room_id: u32, player_id: u32) -> Result<()> {
let mut executor = self.conn.lock().await;
sqlx::query(
"insert into memberships(user_id, room_id, status)
values (?, ?, 1);",
)
.bind(player_id)
.bind(room_id)
.execute(&mut *executor)
.await?;
Ok(())
}
pub async fn remove_room_member(&self, room_id: u32, player_id: u32) -> Result<()> {
let mut executor = self.conn.lock().await;
sqlx::query(
"delete from memberships
where user_id = ? and room_id = ?;",
)
.bind(player_id)
.bind(room_id)
.execute(&mut *executor)
.await?;
Ok(())
}
pub async fn set_room_topic(&mut self, id: u32, topic: &str) -> Result<()> {
let mut executor = self.conn.lock().await;
sqlx::query(
"update rooms
set topic = ?
where id = ?;",
)
.bind(topic)
.bind(id)
.fetch_optional(&mut *executor)
.await?;
Ok(())
}
}

View File

@ -1,30 +0,0 @@
use anyhow::Result;
use crate::repo::Storage;
use crate::room::RoomId;
impl Storage {
pub async fn retrieve_user_id_by_name(&self, name: &str) -> Result<Option<u32>> {
let mut executor = self.conn.lock().await;
let res: Option<(u32,)> = sqlx::query_as("select u.id from users u where u.name = ?;")
.bind(name)
.fetch_optional(&mut *executor)
.await?;
Ok(res.map(|(id,)| id))
}
pub async fn get_rooms_of_a_user(&self, user_id: u32) -> Result<Vec<RoomId>> {
let mut executor = self.conn.lock().await;
let res: Vec<(String,)> = sqlx::query_as(
"select r.name
from memberships m inner join rooms r on m.room_id = r.id
where m.user_id = ?;",
)
.bind(user_id)
.fetch_all(&mut *executor)
.await?;
res.into_iter().map(|(room_id,)| RoomId::from(room_id)).collect()
}
}

View File

@ -1,8 +1,6 @@
//! Domain of rooms — chats with multiple participants. //! Domain of rooms — chats with multiple participants.
use std::collections::HashSet;
use std::{collections::HashMap, hash::Hash, sync::Arc}; use std::{collections::HashMap, hash::Hash, sync::Arc};
use chrono::{DateTime, Utc};
use prometheus::{IntGauge, Registry as MetricRegistry}; use prometheus::{IntGauge, Registry as MetricRegistry};
use serde::Serialize; use serde::Serialize;
use tokio::sync::RwLock as AsyncRwLock; use tokio::sync::RwLock as AsyncRwLock;
@ -50,9 +48,27 @@ impl RoomRegistry {
pub async fn get_or_create_room(&mut self, room_id: RoomId) -> Result<RoomHandle> { pub async fn get_or_create_room(&mut self, room_id: RoomId) -> Result<RoomHandle> {
let mut inner = self.0.write().await; let mut inner = self.0.write().await;
if let Some(room_handle) = inner.get_or_load_room(&room_id).await? { if let Some(room_handle) = inner.rooms.get(&room_id) {
// room was already loaded into memory
log::debug!("Room {} was loaded already", &room_id.0);
Ok(room_handle.clone()) Ok(room_handle.clone())
} else if let Some(stored_room) = inner.storage.retrieve_room_by_name(&*room_id.0).await? {
// room exists, but was not loaded
log::debug!("Loading room {}...", &room_id.0);
let room = Room {
storage_id: stored_room.id,
room_id: room_id.clone(),
subscriptions: HashMap::new(), // TODO figure out how to populate subscriptions
topic: stored_room.topic.into(),
message_count: stored_room.message_count,
storage: inner.storage.clone(),
};
let room_handle = RoomHandle(Arc::new(AsyncRwLock::new(room)));
inner.rooms.insert(room_id, room_handle.clone());
inner.metric_active_rooms.inc();
Ok(room_handle)
} else { } else {
// room does not exist, create it and load
log::debug!("Creating room {}...", &room_id.0); log::debug!("Creating room {}...", &room_id.0);
let topic = "New room"; let topic = "New room";
let id = inner.storage.create_new_room(&*room_id.0, &*topic).await?; let id = inner.storage.create_new_room(&*room_id.0, &*topic).await?;
@ -60,7 +76,6 @@ impl RoomRegistry {
storage_id: id, storage_id: id,
room_id: room_id.clone(), room_id: room_id.clone(),
subscriptions: HashMap::new(), subscriptions: HashMap::new(),
members: HashSet::new(),
topic: topic.into(), topic: topic.into(),
message_count: 0, message_count: 0,
storage: inner.storage.clone(), storage: inner.storage.clone(),
@ -73,8 +88,9 @@ impl RoomRegistry {
} }
pub async fn get_room(&self, room_id: &RoomId) -> Option<RoomHandle> { pub async fn get_room(&self, room_id: &RoomId) -> Option<RoomHandle> {
let mut inner = self.0.write().await; let inner = self.0.read().await;
inner.get_or_load_room(room_id).await.unwrap() let res = inner.rooms.get(room_id);
res.map(|r| r.clone())
} }
pub async fn get_all_rooms(&self) -> Vec<RoomInfo> { pub async fn get_all_rooms(&self) -> Vec<RoomInfo> {
@ -97,66 +113,17 @@ struct RoomRegistryInner {
storage: Storage, storage: Storage,
} }
impl RoomRegistryInner {
async fn get_or_load_room(&mut self, room_id: &RoomId) -> Result<Option<RoomHandle>> {
if let Some(room_handle) = self.rooms.get(room_id) {
log::debug!("Room {} was loaded already", &room_id.0);
Ok(Some(room_handle.clone()))
} else if let Some(stored_room) = self.storage.retrieve_room_by_name(&*room_id.0).await? {
log::debug!("Loading room {}...", &room_id.0);
let room = Room {
storage_id: stored_room.id,
room_id: room_id.clone(),
subscriptions: HashMap::new(),
members: HashSet::new(), // TODO load members from storage
topic: stored_room.topic.into(),
message_count: stored_room.message_count,
storage: self.storage.clone(),
};
let room_handle = RoomHandle(Arc::new(AsyncRwLock::new(room)));
self.rooms.insert(room_id.clone(), room_handle.clone());
self.metric_active_rooms.inc();
Ok(Some(room_handle))
} else {
tracing::debug!("Room {} does not exist", &room_id.0);
Ok(None)
}
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct RoomHandle(Arc<AsyncRwLock<Room>>); pub struct RoomHandle(Arc<AsyncRwLock<Room>>);
impl RoomHandle { impl RoomHandle {
pub async fn subscribe(&self, player_id: &PlayerId, player_handle: PlayerHandle) { pub async fn subscribe(&self, player_id: PlayerId, player_handle: PlayerHandle) {
let mut lock = self.0.write().await; let mut lock = self.0.write().await;
tracing::info!("Adding a subscriber to a room"); lock.add_subscriber(player_id, player_handle).await;
lock.subscriptions.insert(player_id.clone(), player_handle);
}
pub async fn add_member(&self, player_id: &PlayerId, player_storage_id: u32) {
let mut lock = self.0.write().await;
tracing::info!("Adding a new member to a room");
let room_storage_id = lock.storage_id;
lock.storage.add_room_member(room_storage_id, player_storage_id).await.unwrap();
lock.members.insert(player_id.clone());
let update = Updates::RoomJoined {
room_id: lock.room_id.clone(),
new_member_id: player_id.clone(),
};
lock.broadcast_update(update, player_id).await;
} }
pub async fn unsubscribe(&self, player_id: &PlayerId) { pub async fn unsubscribe(&self, player_id: &PlayerId) {
let mut lock = self.0.write().await; let mut lock = self.0.write().await;
lock.subscriptions.remove(player_id); lock.subscriptions.remove(player_id);
}
pub async fn remove_member(&self, player_id: &PlayerId, player_storage_id: u32) {
let mut lock = self.0.write().await;
tracing::info!("Removing a member from a room");
let room_storage_id = lock.storage_id;
lock.storage.remove_room_member(room_storage_id, player_storage_id).await.unwrap();
lock.members.remove(player_id);
let update = Updates::RoomLeft { let update = Updates::RoomLeft {
room_id: lock.room_id.clone(), room_id: lock.room_id.clone(),
former_member_id: player_id.clone(), former_member_id: player_id.clone(),
@ -164,9 +131,9 @@ impl RoomHandle {
lock.broadcast_update(update, player_id).await; lock.broadcast_update(update, player_id).await;
} }
pub async fn send_message(&self, player_id: &PlayerId, body: Str, created_at: DateTime<Utc>) { pub async fn send_message(&self, player_id: PlayerId, body: Str) {
let mut lock = self.0.write().await; let mut lock = self.0.write().await;
let res = lock.send_message(player_id, body, created_at).await; let res = lock.send_message(player_id, body).await;
if let Err(err) = res { if let Err(err) = res {
log::warn!("Failed to send message: {err:?}"); log::warn!("Failed to send message: {err:?}");
} }
@ -181,16 +148,14 @@ impl RoomHandle {
} }
} }
pub async fn set_topic(&self, changer_id: &PlayerId, new_topic: Str) { pub async fn set_topic(&mut self, changer_id: PlayerId, new_topic: Str) {
let mut lock = self.0.write().await; let mut lock = self.0.write().await;
let storage_id = lock.storage_id;
lock.topic = new_topic.clone(); lock.topic = new_topic.clone();
lock.storage.set_room_topic(storage_id, &new_topic).await.unwrap();
let update = Updates::RoomTopicChanged { let update = Updates::RoomTopicChanged {
room_id: lock.room_id.clone(), room_id: lock.room_id.clone(),
new_topic: new_topic.clone(), new_topic: new_topic.clone(),
}; };
lock.broadcast_update(update, changer_id).await; lock.broadcast_update(update, &changer_id).await;
} }
} }
@ -201,33 +166,32 @@ struct Room {
room_id: RoomId, room_id: RoomId,
/// Player actors on the local node which are subscribed to this room's updates. /// Player actors on the local node which are subscribed to this room's updates.
subscriptions: HashMap<PlayerId, PlayerHandle>, subscriptions: HashMap<PlayerId, PlayerHandle>,
/// Members of the room.
members: HashSet<PlayerId>,
/// The total number of messages. Used to calculate the id of the new message. /// The total number of messages. Used to calculate the id of the new message.
message_count: u32, message_count: u32,
topic: Str, topic: Str,
storage: Storage, storage: Storage,
} }
impl Room { impl Room {
async fn send_message(&mut self, author_id: &PlayerId, body: Str, created_at: DateTime<Utc>) -> Result<()> { async fn add_subscriber(&mut self, player_id: PlayerId, player_handle: PlayerHandle) {
tracing::info!("Adding a subscriber to room");
self.subscriptions.insert(player_id.clone(), player_handle);
let update = Updates::RoomJoined {
room_id: self.room_id.clone(),
new_member_id: player_id.clone(),
};
self.broadcast_update(update, &player_id).await;
}
async fn send_message(&mut self, author_id: PlayerId, body: Str) -> Result<()> {
tracing::info!("Adding a message to room"); tracing::info!("Adding a message to room");
self.storage self.storage.insert_message(self.storage_id, self.message_count, &body, &*author_id.as_inner()).await?;
.insert_message(
self.storage_id,
self.message_count,
&body,
&*author_id.as_inner(),
&created_at,
)
.await?;
self.message_count += 1; self.message_count += 1;
let update = Updates::NewMessage { let update = Updates::NewMessage {
room_id: self.room_id.clone(), room_id: self.room_id.clone(),
author_id: author_id.clone(), author_id: author_id.clone(),
body, body,
created_at,
}; };
self.broadcast_update(update, author_id).await; self.broadcast_update(update, &author_id).await;
Ok(()) Ok(())
} }

View File

@ -12,7 +12,6 @@ tokio.workspace = true
prometheus.workspace = true prometheus.workspace = true
futures-util.workspace = true futures-util.workspace = true
nonempty.workspace = true nonempty.workspace = true
chrono.workspace = true
bitflags = "2.4.1" bitflags = "2.4.1"
proto-irc = { path = "../proto-irc" } proto-irc = { path = "../proto-irc" }
sasl = { path = "../sasl" } sasl = { path = "../sasl" }

View File

@ -1,10 +1,9 @@
use bitflags::bitflags; use bitflags::bitflags;
bitflags! { bitflags! {
#[derive(Debug, Clone, Copy)] #[derive(Debug)]
pub struct Capabilities: u32 { pub struct Capabilities: u32 {
const None = 0; const None = 0;
const Sasl = 1 << 0; const Sasl = 1 << 0;
const ServerTime = 1 << 1;
} }
} }

View File

@ -2,7 +2,6 @@ use std::collections::HashMap;
use std::net::SocketAddr; use std::net::SocketAddr;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use chrono::SecondsFormat;
use futures_util::future::join_all; use futures_util::future::join_all;
use nonempty::nonempty; use nonempty::nonempty;
use nonempty::NonEmpty; use nonempty::NonEmpty;
@ -24,7 +23,7 @@ use proto_irc::client::{client_message, ClientMessage};
use proto_irc::server::CapSubBody; use proto_irc::server::CapSubBody;
use proto_irc::server::{AwayStatus, ServerMessage, ServerMessageBody}; use proto_irc::server::{AwayStatus, ServerMessage, ServerMessageBody};
use proto_irc::user::PrefixedNick; use proto_irc::user::PrefixedNick;
use proto_irc::{Chan, Recipient, Tag}; use proto_irc::{Chan, Recipient};
use sasl::AuthBody; use sasl::AuthBody;
mod cap; mod cap;
@ -49,7 +48,6 @@ struct RegisteredUser {
*/ */
username: Str, username: Str,
realname: Str, realname: Str,
enabled_capabilities: Capabilities,
} }
async fn handle_socket( async fn handle_socket(
@ -88,266 +86,6 @@ async fn handle_socket(
Ok(()) Ok(())
} }
struct RegistrationState {
/// The last received `NICK` message.
future_nickname: Option<Str>,
/// The last received `USER` message.
future_username: Option<(Str, Str)>,
enabled_capabilities: Capabilities,
/// `CAP LS` or `CAP REQ` was received, but not `CAP END`.
cap_negotiation_in_progress: bool,
/// The last received `PASS` message.
pass: Option<Str>,
authentication_started: bool,
validated_user: Option<Str>,
}
impl RegistrationState {
fn new() -> RegistrationState {
RegistrationState {
future_nickname: None,
future_username: None,
enabled_capabilities: Capabilities::None,
cap_negotiation_in_progress: false,
pass: None,
authentication_started: false,
validated_user: None,
}
}
/// Handle an incoming message from the client during the registration process.
///
/// Returns `Some` if the user is fully registered, `None` if the registration is still in progress.
async fn handle_msg(
&mut self,
msg: ClientMessage,
writer: &mut BufWriter<WriteHalf<'_>>,
storage: &mut Storage,
config: &ServerConfig,
) -> Result<Option<RegisteredUser>> {
match msg {
ClientMessage::Pass { password } => {
self.pass = Some(password);
Ok(None)
}
ClientMessage::Capability { subcommand } => match subcommand {
CapabilitySubcommand::List { code: _ } => {
self.cap_negotiation_in_progress = true;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone().into()),
body: ServerMessageBody::Cap {
target: self.future_nickname.clone().unwrap_or_else(|| "*".into()),
subcmd: CapSubBody::Ls("sasl=PLAIN server-time".into()),
},
}
.write_async(writer)
.await?;
writer.flush().await?;
Ok(None)
}
CapabilitySubcommand::Req(caps) => {
self.cap_negotiation_in_progress = true;
let mut acked = vec![];
let mut naked = vec![];
for cap in caps {
if &*cap.name == "sasl" {
if cap.to_disable {
self.enabled_capabilities &= !Capabilities::Sasl;
} else {
self.enabled_capabilities |= Capabilities::Sasl;
}
acked.push(cap);
} else if &*cap.name == "server-time" {
if cap.to_disable {
self.enabled_capabilities &= !Capabilities::ServerTime;
} else {
self.enabled_capabilities |= Capabilities::ServerTime;
}
acked.push(cap);
} else {
naked.push(cap);
}
}
let mut ack_body = String::new();
if let Some((first, tail)) = acked.split_first() {
if first.to_disable {
ack_body.push('-');
}
ack_body += &*first.name;
for cap in tail {
ack_body.push(' ');
if cap.to_disable {
ack_body.push('-');
}
ack_body += &*cap.name;
}
}
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone().into()),
body: ServerMessageBody::Cap {
target: self.future_nickname.clone().unwrap_or_else(|| "*".into()),
subcmd: CapSubBody::Ack(ack_body.into()),
},
}
.write_async(writer)
.await?;
writer.flush().await?;
Ok(None)
}
CapabilitySubcommand::End => {
let Some((ref username, ref realname)) = self.future_username else {
self.cap_negotiation_in_progress = false;
return Ok(None);
};
let Some(nickname) = self.future_nickname.clone() else {
self.cap_negotiation_in_progress = false;
return Ok(None);
};
let username = username.clone();
let realname = realname.clone();
let candidate_user = RegisteredUser {
nickname: nickname.clone(),
username,
realname,
enabled_capabilities: self.enabled_capabilities,
};
self.finalize_auth(candidate_user, writer, storage, config).await
}
},
ClientMessage::Nick { nickname } => {
if self.cap_negotiation_in_progress {
self.future_nickname = Some(nickname);
Ok(None)
} else if let Some((username, realname)) = &self.future_username.clone() {
let candidate_user = RegisteredUser {
nickname: nickname.clone(),
username: username.clone(),
realname: realname.clone(),
enabled_capabilities: self.enabled_capabilities,
};
self.finalize_auth(candidate_user, writer, storage, config).await
} else {
self.future_nickname = Some(nickname);
Ok(None)
}
}
ClientMessage::User { username, realname } => {
if self.cap_negotiation_in_progress {
self.future_username = Some((username, realname));
Ok(None)
} else if let Some(nickname) = self.future_nickname.clone() {
let candidate_user = RegisteredUser {
nickname: nickname.clone(),
username,
realname,
enabled_capabilities: self.enabled_capabilities,
};
self.finalize_auth(candidate_user, writer, storage, config).await
} else {
self.future_username = Some((username, realname));
Ok(None)
}
}
ClientMessage::Authenticate(body) => {
if !self.authentication_started {
tracing::debug!("Received authentication request");
if &*body == "PLAIN" {
tracing::debug!("Authentication request with method PLAIN");
self.authentication_started = true;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone().into()),
body: ServerMessageBody::Authenticate("+".into()),
}
.write_async(writer)
.await?;
writer.flush().await?;
Ok(None)
} else {
let target = self.future_nickname.clone().unwrap_or_else(|| "*".into());
sasl_fail_message(config.server_name.clone(), target, "Unsupported mechanism".into())
.write_async(writer)
.await?;
writer.flush().await?;
Ok(None)
}
} else {
let body = AuthBody::from_str(body.as_bytes())?;
if let Err(e) = auth_user(storage, &body.login, &body.password).await {
tracing::warn!("Authentication failed: {:?}", e);
let target = self.future_nickname.clone().unwrap_or_else(|| "*".into());
sasl_fail_message(config.server_name.clone(), target, "Bad credentials".into())
.write_async(writer)
.await?;
writer.flush().await?;
Ok(None)
} else {
let login: Str = body.login.into();
self.validated_user = Some(login.clone());
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone().into()),
body: ServerMessageBody::N900LoggedIn {
nick: login.clone(),
address: login.clone(),
account: login.clone(),
message: format!("You are now logged in as {}", login).into(),
},
}
.write_async(writer)
.await?;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone().into()),
body: ServerMessageBody::N903SaslSuccess {
nick: login.clone(),
message: "SASL authentication successful".into(),
},
}
.write_async(writer)
.await?;
writer.flush().await?;
Ok(None)
}
}
// TODO handle abortion of authentication
}
_ => Ok(None),
}
}
async fn finalize_auth(
&mut self,
candidate_user: RegisteredUser,
writer: &mut BufWriter<WriteHalf<'_>>,
storage: &mut Storage,
config: &ServerConfig,
) -> Result<Option<RegisteredUser>> {
if self.enabled_capabilities.contains(Capabilities::Sasl)
&& self.validated_user.as_ref() == Some(&candidate_user.nickname)
{
Ok(Some(candidate_user))
} else {
let Some(candidate_password) = &self.pass else {
sasl_fail_message(
config.server_name.clone(),
candidate_user.nickname.clone(),
"User credentials was not provided".into(),
)
.write_async(writer)
.await?;
writer.flush().await?;
return Ok(None);
};
auth_user(storage, &*candidate_user.nickname, &*candidate_password).await?;
Ok(Some(candidate_user))
}
}
}
async fn handle_registration<'a>( async fn handle_registration<'a>(
reader: &mut BufReader<ReadHalf<'a>>, reader: &mut BufReader<ReadHalf<'a>>,
writer: &mut BufWriter<WriteHalf<'a>>, writer: &mut BufWriter<WriteHalf<'a>>,
@ -356,7 +94,14 @@ async fn handle_registration<'a>(
) -> Result<RegisteredUser> { ) -> Result<RegisteredUser> {
let mut buffer = vec![]; let mut buffer = vec![];
let mut state = RegistrationState::new(); let mut future_nickname: Option<Str> = None;
let mut future_username: Option<(Str, Str)> = None;
let mut enabled_capabilities = Capabilities::None;
let mut cap_negotiation_in_progress = false; // if true, expect `CAP END` to complete registration
let mut pass: Option<Str> = None;
let mut authentication_started = false;
let mut validated_user = None;
let user = loop { let user = loop {
let res = read_irc_message(reader, &mut buffer).await; let res = read_irc_message(reader, &mut buffer).await;
@ -387,8 +132,218 @@ async fn handle_registration<'a>(
} }
}; };
tracing::debug!("Incoming IRC message: {msg:?}"); tracing::debug!("Incoming IRC message: {msg:?}");
if let Some(user) = state.handle_msg(msg, writer, storage, config).await? { match msg {
break Ok(user); ClientMessage::Pass { password } => {
pass = Some(password);
}
ClientMessage::Capability { subcommand } => match subcommand {
CapabilitySubcommand::List { code: _ } => {
cap_negotiation_in_progress = true;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone().into()),
body: ServerMessageBody::Cap {
target: future_nickname.clone().unwrap_or_else(|| "*".into()),
subcmd: CapSubBody::Ls("sasl=PLAIN".into()),
},
}
.write_async(writer)
.await?;
writer.flush().await?;
}
CapabilitySubcommand::Req(caps) => {
cap_negotiation_in_progress = true;
let mut acked = vec![];
let mut naked = vec![];
for cap in caps {
if &*cap.name == "sasl" {
if cap.to_disable {
enabled_capabilities &= !Capabilities::Sasl;
} else {
enabled_capabilities |= Capabilities::Sasl;
}
acked.push(cap);
} else {
naked.push(cap);
}
}
let mut ack_body = String::new();
for cap in acked {
if cap.to_disable {
ack_body.push('-');
}
ack_body += &*cap.name;
}
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone().into()),
body: ServerMessageBody::Cap {
target: future_nickname.clone().unwrap_or_else(|| "*".into()),
subcmd: CapSubBody::Ack(ack_body.into()),
},
}
.write_async(writer)
.await?;
writer.flush().await?;
}
CapabilitySubcommand::End => {
let Some((ref username, ref realname)) = future_username else {
todo!();
};
let Some(nickname) = future_nickname.clone() else {
todo!();
};
let username = username.clone();
let realname = realname.clone();
let candidate_user = RegisteredUser {
nickname: nickname.clone(),
username,
realname,
};
if enabled_capabilities.contains(Capabilities::Sasl)
&& validated_user.as_ref() == Some(&candidate_user.nickname)
{
break Ok(candidate_user);
} else {
let Some(candidate_password) = pass else {
sasl_fail_message(
config.server_name.clone(),
nickname.clone(),
"User credentials was not provided".into(),
)
.write_async(writer)
.await?;
writer.flush().await?;
continue;
};
auth_user(storage, &*candidate_user.nickname, &*candidate_password).await?;
break Ok(candidate_user);
}
}
},
ClientMessage::Nick { nickname } => {
if cap_negotiation_in_progress {
future_nickname = Some(nickname);
} else if let Some((username, realname)) = future_username.clone() {
let candidate_user = RegisteredUser {
nickname: nickname.clone(),
username,
realname,
};
let Some(candidate_password) = pass else {
sasl_fail_message(
config.server_name.clone(),
nickname.clone(),
"User credentials was not provided".into(),
)
.write_async(writer)
.await?;
writer.flush().await?;
continue;
};
auth_user(storage, &*candidate_user.nickname, &*candidate_password).await?;
break Ok(candidate_user);
} else {
future_nickname = Some(nickname);
}
}
ClientMessage::User { username, realname } => {
if cap_negotiation_in_progress {
future_username = Some((username, realname));
} else if let Some(nickname) = future_nickname.clone() {
let candidate_user = RegisteredUser {
nickname: nickname.clone(),
username,
realname,
};
let Some(candidate_password) = pass else {
sasl_fail_message(
config.server_name.clone(),
nickname.clone(),
"User credentials was not provided".into(),
)
.write_async(writer)
.await?;
writer.flush().await?;
continue;
};
auth_user(storage, &*candidate_user.nickname, &*candidate_password).await?;
break Ok(candidate_user);
} else {
future_username = Some((username, realname));
}
}
ClientMessage::Authenticate(body) => {
if !authentication_started {
tracing::debug!("Received authentication request");
if &*body == "PLAIN" {
tracing::debug!("Authentication request with method PLAIN");
authentication_started = true;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone().into()),
body: ServerMessageBody::Authenticate("+".into()),
}
.write_async(writer)
.await?;
writer.flush().await?;
} else {
if let Some(nickname) = future_nickname.clone() {
sasl_fail_message(
config.server_name.clone(),
nickname.clone(),
"Unsupported mechanism".into(),
)
.write_async(writer)
.await?;
writer.flush().await?;
} else {
break Err(anyhow::Error::msg("Wrong authentication sequence"));
}
}
} else {
let body = AuthBody::from_str(body.as_bytes())?;
if let Err(e) = auth_user(storage, &body.login, &body.password).await {
tracing::warn!("Authentication failed: {:?}", e);
if let Some(nickname) = future_nickname.clone() {
sasl_fail_message(config.server_name.clone(), nickname.clone(), "Bad credentials".into())
.write_async(writer)
.await?;
writer.flush().await?;
} else {
}
} else {
let login: Str = body.login.into();
validated_user = Some(login.clone());
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone().into()),
body: ServerMessageBody::N900LoggedIn {
nick: login.clone(),
address: login.clone(),
account: login.clone(),
message: format!("You are now logged in as {}", login).into(),
},
}
.write_async(writer)
.await?;
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone().into()),
body: ServerMessageBody::N903SaslSuccess {
nick: login.clone(),
message: "SASL authentication successful".into(),
},
}
.write_async(writer)
.await?;
writer.flush().await?;
}
}
// TODO handle abortion of authentication
}
_ => {}
} }
buffer.clear(); buffer.clear();
}?; }?;
@ -437,7 +392,7 @@ async fn handle_registered_socket<'a>(
log::info!("Handling registered user: {user:?}"); log::info!("Handling registered user: {user:?}");
let player_id = PlayerId::from(user.nickname.clone())?; let player_id = PlayerId::from(user.nickname.clone())?;
let mut connection = players.connect_to_player(&player_id).await; let mut connection = players.connect_to_player(player_id.clone()).await;
let text: Str = format!("Welcome to {} Server", &config.server_name).into(); let text: Str = format!("Welcome to {} Server", &config.server_name).into();
ServerMessage { ServerMessage {
@ -606,18 +561,9 @@ async fn handle_update(
author_id, author_id,
room_id, room_id,
body, body,
created_at,
} => { } => {
let mut tags = vec![];
if user.enabled_capabilities.contains(Capabilities::ServerTime) {
let tag = Tag {
key: "time".into(),
value: Some(created_at.to_rfc3339_opts(SecondsFormat::Millis, true).into()),
};
tags.push(tag);
}
ServerMessage { ServerMessage {
tags, tags: vec![],
sender: Some(author_id.as_inner().clone()), sender: Some(author_id.as_inner().clone()),
body: ServerMessageBody::PrivateMessage { body: ServerMessageBody::PrivateMessage {
target: Recipient::Chan(Chan::Global(room_id.as_inner().clone())), target: Recipient::Chan(Chan::Global(room_id.as_inner().clone())),

View File

@ -1,20 +1,17 @@
use std::io::ErrorKind; use std::io::ErrorKind;
use std::net::SocketAddr;
use std::time::Duration; use std::time::Duration;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use chrono::SecondsFormat;
use prometheus::Registry as MetricsRegistry; use prometheus::Registry as MetricsRegistry;
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader}; use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::tcp::{ReadHalf, WriteHalf}; use tokio::net::tcp::{ReadHalf, WriteHalf};
use tokio::net::TcpStream; use tokio::net::TcpStream;
use lavina_core::player::{JoinResult, PlayerId, SendMessageResult};
use lavina_core::repo::{Storage, StorageConfig}; use lavina_core::repo::{Storage, StorageConfig};
use lavina_core::room::RoomId;
use lavina_core::{player::PlayerRegistry, room::RoomRegistry}; use lavina_core::{player::PlayerRegistry, room::RoomRegistry};
use projection_irc::APP_VERSION; use projection_irc::APP_VERSION;
use projection_irc::{launch, read_irc_message, RunningServer, ServerConfig}; use projection_irc::{launch, read_irc_message, RunningServer, ServerConfig};
struct TestScope<'a> { struct TestScope<'a> {
reader: BufReader<ReadHalf<'a>>, reader: BufReader<ReadHalf<'a>>,
writer: WriteHalf<'a>, writer: WriteHalf<'a>,
@ -92,11 +89,6 @@ impl<'a> TestScope<'a> {
Err(_) => Ok(()), Err(_) => Ok(()),
} }
} }
async fn expect_cap_ls(&mut self) -> Result<()> {
self.expect(":testserver CAP * LS :sasl=PLAIN server-time").await?;
Ok(())
}
} }
struct TestServer { struct TestServer {
@ -119,36 +111,7 @@ impl TestServer {
}) })
.await?; .await?;
let rooms = RoomRegistry::new(&mut metrics, storage.clone()).unwrap(); let rooms = RoomRegistry::new(&mut metrics, storage.clone()).unwrap();
let players = PlayerRegistry::empty(rooms.clone(), storage.clone(), &mut metrics).unwrap(); let players = PlayerRegistry::empty(rooms.clone(), &mut metrics).unwrap();
let server = launch(config, players.clone(), rooms.clone(), metrics.clone(), storage.clone()).await.unwrap();
Ok(TestServer {
metrics,
storage,
rooms,
players,
server,
})
}
async fn reboot(mut self) -> Result<TestServer> {
let config = ServerConfig {
listen_on: "127.0.0.1:0".parse().unwrap(),
server_name: "testserver".into(),
};
let TestServer {
mut metrics,
mut storage,
rooms,
mut players,
server,
} = self;
server.terminate().await?;
players.shutdown_all().await.unwrap();
drop(players);
drop(rooms);
let mut metrics = MetricsRegistry::new();
let rooms = RoomRegistry::new(&mut metrics, storage.clone()).unwrap();
let players = PlayerRegistry::empty(rooms.clone(), storage.clone(), &mut metrics).unwrap();
let server = launch(config, players.clone(), rooms.clone(), metrics.clone(), storage.clone()).await.unwrap(); let server = launch(config, players.clone(), rooms.clone(), metrics.clone(), storage.clone()).await.unwrap();
Ok(TestServer { Ok(TestServer {
metrics, metrics,
@ -189,76 +152,6 @@ async fn scenario_basic() -> Result<()> {
Ok(()) Ok(())
} }
#[tokio::test]
async fn scenario_join_and_reboot() -> Result<()> {
let mut server = TestServer::start().await?;
// test scenario
server.storage.create_user("tester").await?;
server.storage.set_password("tester", "password").await?;
let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream);
// Open a connection and join a channel
s.send("PASS password").await?;
s.send("NICK tester").await?;
s.send("USER UserName 0 * :Real Name").await?;
s.expect_server_introduction("tester").await?;
s.expect_nothing().await?;
s.send("JOIN #test").await?;
s.expect(":tester JOIN #test").await?;
s.expect(":testserver 332 tester #test :New room").await?;
s.expect(":testserver 353 tester = #test :tester").await?;
s.expect(":testserver 366 tester #test :End of /NAMES list").await?;
s.send("PRIVMSG #test :Hello").await?;
s.send("QUIT :Leaving").await?;
s.expect(":testserver ERROR :Leaving the server").await?;
s.expect_eof().await?;
stream.shutdown().await?;
// Open a new connection and expect to be force-joined to the channel
let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream);
async fn test(s: &mut TestScope<'_>) -> Result<()> {
s.send("PASS password").await?;
s.send("NICK tester").await?;
s.send("USER UserName 0 * :Real Name").await?;
s.expect_server_introduction("tester").await?;
s.expect(":tester JOIN #test").await?;
s.expect(":testserver 332 tester #test :New room").await?;
s.expect(":testserver 353 tester = #test :tester").await?;
s.expect(":testserver 366 tester #test :End of /NAMES list").await?;
s.send("QUIT :Leaving").await?;
s.expect(":testserver ERROR :Leaving the server").await?;
s.expect_eof().await?;
Ok(())
}
test(&mut s).await?;
stream.shutdown().await?;
// Reboot the server
let server = server.reboot().await?;
// Open a new connection and expect to be force-joined to the channel
let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream);
test(&mut s).await?;
stream.shutdown().await?;
// wrap up
server.server.terminate().await?;
Ok(())
}
#[tokio::test] #[tokio::test]
async fn scenario_force_join_msg() -> Result<()> { async fn scenario_force_join_msg() -> Result<()> {
let mut server = TestServer::start().await?; let mut server = TestServer::start().await?;
@ -404,7 +297,7 @@ async fn scenario_cap_full_negotiation() -> Result<()> {
s.send("CAP LS 302").await?; s.send("CAP LS 302").await?;
s.send("NICK tester").await?; s.send("NICK tester").await?;
s.send("USER UserName 0 * :Real Name").await?; s.send("USER UserName 0 * :Real Name").await?;
s.expect_cap_ls().await?; s.expect(":testserver CAP * LS :sasl=PLAIN").await?;
s.send("CAP REQ :sasl").await?; s.send("CAP REQ :sasl").await?;
s.expect(":testserver CAP tester ACK :sasl").await?; s.expect(":testserver CAP tester ACK :sasl").await?;
s.send("AUTHENTICATE PLAIN").await?; s.send("AUTHENTICATE PLAIN").await?;
@ -429,45 +322,6 @@ async fn scenario_cap_full_negotiation() -> Result<()> {
Ok(()) Ok(())
} }
#[tokio::test]
async fn scenario_cap_full_negotiation_nick_last() -> Result<()> {
let mut server = TestServer::start().await?;
// test scenario
server.storage.create_user("tester").await?;
server.storage.set_password("tester", "password").await?;
let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream);
s.send("CAP LS 302").await?;
s.expect_cap_ls().await?;
s.send("CAP REQ :sasl").await?;
s.expect(":testserver CAP * ACK :sasl").await?;
s.send("AUTHENTICATE PLAIN").await?;
s.expect(":testserver AUTHENTICATE +").await?;
s.send("AUTHENTICATE dGVzdGVyAHRlc3RlcgBwYXNzd29yZA==").await?; // base64-encoded 'tester\x00tester\x00password'
s.expect(":testserver 900 tester tester tester :You are now logged in as tester").await?;
s.expect(":testserver 903 tester :SASL authentication successful").await?;
s.send("CAP END").await?;
s.send("USER UserName 0 * :Real Name").await?;
s.send("NICK tester").await?;
s.expect_server_introduction("tester").await?;
s.expect_nothing().await?;
s.send("QUIT :Leaving").await?;
s.expect(":testserver ERROR :Leaving the server").await?;
s.expect_eof().await?;
stream.shutdown().await?;
// wrap up
server.server.terminate().await?;
Ok(())
}
#[tokio::test] #[tokio::test]
async fn scenario_cap_short_negotiation() -> Result<()> { async fn scenario_cap_short_negotiation() -> Result<()> {
let mut server = TestServer::start().await?; let mut server = TestServer::start().await?;
@ -521,7 +375,7 @@ async fn scenario_cap_sasl_fail() -> Result<()> {
s.send("CAP LS 302").await?; s.send("CAP LS 302").await?;
s.send("NICK tester").await?; s.send("NICK tester").await?;
s.send("USER UserName 0 * :Real Name").await?; s.send("USER UserName 0 * :Real Name").await?;
s.expect_cap_ls().await?; s.expect(":testserver CAP * LS :sasl=PLAIN").await?;
s.send("CAP REQ :sasl").await?; s.send("CAP REQ :sasl").await?;
s.expect(":testserver CAP tester ACK :sasl").await?; s.expect(":testserver CAP tester ACK :sasl").await?;
s.send("AUTHENTICATE SHA256").await?; s.send("AUTHENTICATE SHA256").await?;
@ -553,6 +407,7 @@ async fn scenario_cap_sasl_fail() -> Result<()> {
#[tokio::test] #[tokio::test]
async fn terminate_socket_scenario() -> Result<()> { async fn terminate_socket_scenario() -> Result<()> {
let mut server = TestServer::start().await?; let mut server = TestServer::start().await?;
let address: SocketAddr = ("127.0.0.1:0".parse().unwrap());
// test scenario // test scenario
@ -574,66 +429,3 @@ async fn terminate_socket_scenario() -> Result<()> {
Ok(()) Ok(())
} }
#[tokio::test]
async fn server_time_capability() -> Result<()> {
let mut server = TestServer::start().await?;
// test scenario
server.storage.create_user("tester").await?;
server.storage.set_password("tester", "password").await?;
let mut stream = TcpStream::connect(server.server.addr).await?;
let mut s = TestScope::new(&mut stream);
s.send("CAP LS 302").await?;
s.send("NICK tester").await?;
s.send("USER UserName 0 * :Real Name").await?;
s.expect(":testserver CAP * LS :sasl=PLAIN server-time").await?;
s.send("CAP REQ :sasl server-time").await?;
s.expect(":testserver CAP tester ACK :sasl server-time").await?;
s.send("AUTHENTICATE PLAIN").await?;
s.expect(":testserver AUTHENTICATE +").await?;
s.send("AUTHENTICATE dGVzdGVyAHRlc3RlcgBwYXNzd29yZA==").await?; // base64-encoded 'tester\x00tester\x00password'
s.expect(":testserver 900 tester tester tester :You are now logged in as tester").await?;
s.expect(":testserver 903 tester :SASL authentication successful").await?;
s.send("CAP END").await?;
s.expect_server_introduction("tester").await?;
s.expect_nothing().await?;
s.send("JOIN #test").await?;
s.expect(":tester JOIN #test").await?;
s.expect(":testserver 332 tester #test :New room").await?;
s.expect(":testserver 353 tester = #test :tester").await?;
s.expect(":testserver 366 tester #test :End of /NAMES list").await?;
server.storage.create_user("some_guy").await?;
let mut conn = server.players.connect_to_player(&PlayerId::from("some_guy").unwrap()).await;
let res = conn.join_room(RoomId::from("test").unwrap()).await?;
let JoinResult::Success(_) = res else {
panic!("Failed to join room");
};
s.expect(":some_guy JOIN #test").await?;
let SendMessageResult::Success(res) = conn.send_message(RoomId::from("test").unwrap(), "Hello".into()).await?
else {
panic!("Failed to send message");
};
s.expect(&format!(
"@time={} :some_guy PRIVMSG #test :Hello",
res.to_rfc3339_opts(SecondsFormat::Millis, true)
))
.await?;
s.send("QUIT :Leaving").await?;
s.expect(":testserver ERROR :Leaving the server").await?;
s.expect_eof().await?;
stream.shutdown().await?;
// wrap up
server.server.terminate().await?;
Ok(())
}

View File

@ -2,8 +2,8 @@
use quick_xml::events::Event; use quick_xml::events::Event;
use lavina_core::room::{RoomId, RoomRegistry}; use lavina_core::room::RoomRegistry;
use proto_xmpp::bind::{BindResponse, Jid, Name, Server}; use proto_xmpp::bind::{BindResponse, Jid, Name, Resource, Server};
use proto_xmpp::client::{Iq, IqError, IqErrorType, IqType}; use proto_xmpp::client::{Iq, IqError, IqErrorType, IqType};
use proto_xmpp::disco::{Feature, Identity, InfoQuery, Item, ItemQuery}; use proto_xmpp::disco::{Feature, Identity, InfoQuery, Item, ItemQuery};
use proto_xmpp::roster::RosterQuery; use proto_xmpp::roster::RosterQuery;
@ -17,7 +17,7 @@ use proto_xmpp::xml::ToXml;
impl<'a> XmppConnection<'a> { impl<'a> XmppConnection<'a> {
pub async fn handle_iq(&self, output: &mut Vec<Event<'static>>, iq: Iq<IqClientBody>) { pub async fn handle_iq(&self, output: &mut Vec<Event<'static>>, iq: Iq<IqClientBody>) {
match iq.body { match iq.body {
IqClientBody::Bind(_) => { IqClientBody::Bind(b) => {
let req = Iq { let req = Iq {
from: None, from: None,
id: iq.id, id: iq.id,
@ -52,32 +52,18 @@ impl<'a> XmppConnection<'a> {
req.serialize(output); req.serialize(output);
} }
IqClientBody::DiscoInfo(info) => { IqClientBody::DiscoInfo(info) => {
let response = self.disco_info(iq.to.as_ref(), &info).await; let response = self.disco_info(iq.to.as_deref(), &info);
match response { let req = Iq {
Ok(response) => { from: iq.to,
let req = Iq { id: iq.id,
from: iq.to, to: None,
id: iq.id, r#type: IqType::Result,
to: None, body: response,
r#type: IqType::Result, };
body: response, req.serialize(output);
};
req.serialize(output);
}
Err(response) => {
let req = Iq {
from: iq.to,
id: iq.id,
to: None,
r#type: IqType::Error,
body: response,
};
req.serialize(output);
}
}
} }
IqClientBody::DiscoItem(item) => { IqClientBody::DiscoItem(item) => {
let response = self.disco_items(iq.to.as_ref(), &item, self.rooms).await; let response = self.disco_items(iq.to.as_deref(), &item, self.rooms).await;
let req = Iq { let req = Iq {
from: iq.to, from: iq.to,
id: iq.id, id: iq.id,
@ -102,16 +88,12 @@ impl<'a> XmppConnection<'a> {
} }
} }
async fn disco_info(&self, to: Option<&Jid>, req: &InfoQuery) -> Result<InfoQuery, IqError> { fn disco_info(&self, to: Option<&str>, req: &InfoQuery) -> InfoQuery {
let identity; let identity;
let feature; let feature;
match to { match to {
Some(Jid { Some(r) if r == &*self.hostname => {
name: None,
server,
resource: None,
}) if server.0 == self.hostname => {
identity = vec![Identity { identity = vec![Identity {
category: "server".into(), category: "server".into(),
name: None, name: None,
@ -124,11 +106,7 @@ impl<'a> XmppConnection<'a> {
Feature::new("presence"), Feature::new("presence"),
] ]
} }
Some(Jid { Some(r) if r == &*self.hostname_rooms => {
name: None,
server,
resource: None,
}) if server.0 == self.hostname_rooms => {
identity = vec![Identity { identity = vec![Identity {
category: "conference".into(), category: "conference".into(),
name: Some("Chat rooms".into()), name: Some("Chat rooms".into()),
@ -140,53 +118,21 @@ impl<'a> XmppConnection<'a> {
Feature::new("http://jabber.org/protocol/muc"), Feature::new("http://jabber.org/protocol/muc"),
] ]
} }
Some(Jid {
name: Some(room_name),
server,
resource: None,
}) if server.0 == self.hostname_rooms => {
let room_id = RoomId::from(room_name.0.clone()).unwrap();
let Some(_) = self.rooms.get_room(&room_id).await else {
// TODO should return item-not-found
// example:
// <error type="cancel">
// <item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
// <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" xml:lang="en">Conference room does not exist</text>
// </error>
return Err(IqError {
r#type: IqErrorType::Cancel,
});
};
identity = vec![Identity {
category: "conference".into(),
name: Some(room_id.into_inner().to_string()),
r#type: "text".into(),
}];
feature = vec![
Feature::new("http://jabber.org/protocol/disco#info"),
Feature::new("http://jabber.org/protocol/disco#items"),
Feature::new("http://jabber.org/protocol/muc"),
]
}
_ => { _ => {
identity = vec![]; identity = vec![];
feature = vec![]; feature = vec![];
} }
}; };
Ok(InfoQuery { InfoQuery {
node: None, node: None,
identity, identity,
feature, feature,
}) }
} }
async fn disco_items(&self, to: Option<&Jid>, req: &ItemQuery, rooms: &RoomRegistry) -> ItemQuery { async fn disco_items(&self, to: Option<&str>, req: &ItemQuery, rooms: &RoomRegistry) -> ItemQuery {
let item = match to { let item = match to {
Some(Jid { Some(r) if r == &*self.hostname => {
name: None,
server,
resource: None,
}) if server.0 == self.hostname => {
vec![Item { vec![Item {
jid: Jid { jid: Jid {
name: None, name: None,
@ -197,11 +143,7 @@ impl<'a> XmppConnection<'a> {
node: None, node: None,
}] }]
} }
Some(Jid { Some(r) if r == &*self.hostname_rooms => {
name: None,
server,
resource: None,
}) if server.0 == self.hostname_rooms => {
let room_list = rooms.get_all_rooms().await; let room_list = rooms.get_all_rooms().await;
room_list room_list
.into_iter() .into_iter()

View File

@ -207,7 +207,7 @@ async fn handle_socket(
authenticated = socket_auth(&mut xml_reader, &mut xml_writer, &mut reader_buf, &mut storage, &hostname) => { authenticated = socket_auth(&mut xml_reader, &mut xml_writer, &mut reader_buf, &mut storage, &hostname) => {
match authenticated { match authenticated {
Ok(authenticated) => { Ok(authenticated) => {
let mut connection = players.connect_to_player(&authenticated.player_id).await; let mut connection = players.connect_to_player(authenticated.player_id.clone()).await;
socket_final( socket_final(
&mut xml_reader, &mut xml_reader,
&mut xml_writer, &mut xml_writer,

View File

@ -4,7 +4,7 @@ use quick_xml::events::Event;
use lavina_core::prelude::*; use lavina_core::prelude::*;
use lavina_core::room::RoomId; use lavina_core::room::RoomId;
use proto_xmpp::bind::{Jid, Name, Server}; use proto_xmpp::bind::{Jid, Server};
use proto_xmpp::client::Presence; use proto_xmpp::client::Presence;
use proto_xmpp::xml::{Ignore, ToXml}; use proto_xmpp::xml::{Ignore, ToXml};
@ -12,59 +12,42 @@ use crate::XmppConnection;
impl<'a> XmppConnection<'a> { impl<'a> XmppConnection<'a> {
pub async fn handle_presence(&mut self, output: &mut Vec<Event<'static>>, p: Presence<Ignore>) -> Result<()> { pub async fn handle_presence(&mut self, output: &mut Vec<Event<'static>>, p: Presence<Ignore>) -> Result<()> {
match p.to { let response = if p.to.is_none() {
None => { Presence::<()> {
self.self_presence(output).await; to: Some(Jid {
name: Some(self.user.xmpp_name.clone()),
server: Server(self.hostname.clone()),
resource: Some(self.user.xmpp_resource.clone()),
}),
from: Some(Jid {
name: Some(self.user.xmpp_name.clone()),
server: Server(self.hostname.clone()),
resource: Some(self.user.xmpp_resource.clone()),
}),
..Default::default()
} }
Some(Jid { } else if let Some(Jid {
name: Some(name), name: Some(name),
server, server,
// resources in MUCs are members' personas not implemented (yet?) resource: Some(resource),
resource: Some(_), }) = p.to
}) if server.0 == self.hostname_rooms => { {
self.muc_presence(name, output).await?; let a = self.user_handle.join_room(RoomId::from(name.0.clone())?).await?;
Presence::<()> {
to: Some(Jid {
name: Some(self.user.xmpp_name.clone()),
server: Server(self.hostname.clone()),
resource: Some(self.user.xmpp_resource.clone()),
}),
from: Some(Jid {
name: Some(name.clone()),
server: Server(self.hostname_rooms.clone()),
resource: Some(self.user.xmpp_muc_name.clone()),
}),
..Default::default()
} }
_ => { } else {
// TODO other presence cases Presence::<()>::default()
let response = Presence::<()>::default();
response.serialize(output);
}
}
Ok(())
}
async fn self_presence(&mut self, output: &mut Vec<Event<'static>>) {
let response = Presence::<()> {
to: Some(Jid {
name: Some(self.user.xmpp_name.clone()),
server: Server(self.hostname.clone()),
resource: Some(self.user.xmpp_resource.clone()),
}),
from: Some(Jid {
name: Some(self.user.xmpp_name.clone()),
server: Server(self.hostname.clone()),
resource: Some(self.user.xmpp_resource.clone()),
}),
..Default::default()
};
response.serialize(output);
}
async fn muc_presence(&mut self, name: Name, output: &mut Vec<Event<'static>>) -> Result<()> {
let a = self.user_handle.join_room(RoomId::from(name.0.clone())?).await?;
// TODO handle bans
let response = Presence::<()> {
to: Some(Jid {
name: Some(self.user.xmpp_name.clone()),
server: Server(self.hostname.clone()),
resource: Some(self.user.xmpp_resource.clone()),
}),
from: Some(Jid {
name: Some(name.clone()),
server: Server(self.hostname_rooms.clone()),
resource: Some(self.user.xmpp_muc_name.clone()),
}),
..Default::default()
}; };
response.serialize(output); response.serialize(output);
Ok(()) Ok(())

View File

@ -17,7 +17,6 @@ impl<'a> XmppConnection<'a> {
room_id, room_id,
author_id, author_id,
body, body,
created_at: _,
} => { } => {
Message::<()> { Message::<()> {
to: Some(Jid { to: Some(Jid {

View File

@ -143,7 +143,7 @@ impl TestServer {
}) })
.await?; .await?;
let rooms = RoomRegistry::new(&mut metrics, storage.clone()).unwrap(); let rooms = RoomRegistry::new(&mut metrics, storage.clone()).unwrap();
let players = PlayerRegistry::empty(rooms.clone(), storage.clone(), &mut metrics).unwrap(); let players = PlayerRegistry::empty(rooms.clone(), &mut metrics).unwrap();
let server = launch(config, players.clone(), rooms.clone(), metrics.clone(), storage.clone()).await.unwrap(); let server = launch(config, players.clone(), rooms.clone(), metrics.clone(), storage.clone()).await.unwrap();
Ok(TestServer { Ok(TestServer {
metrics, metrics,

View File

@ -18,19 +18,8 @@ use tokio::io::{AsyncWrite, AsyncWriteExt};
/// Single message tag value. /// Single message tag value.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Tag { pub struct Tag {
pub key: Str, key: Str,
pub value: Option<Str>, value: Option<u8>,
}
impl Tag {
pub async fn write_async(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
writer.write_all(self.key.as_bytes()).await?;
if let Some(value) = &self.value {
writer.write_all(b"=").await?;
writer.write_all(value.as_bytes()).await?;
}
Ok(())
}
} }
fn receiver(input: &str) -> IResult<&str, &str> { fn receiver(input: &str) -> IResult<&str, &str> {

View File

@ -19,13 +19,6 @@ pub struct ServerMessage {
impl ServerMessage { impl ServerMessage {
pub async fn write_async(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> { pub async fn write_async(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
if !self.tags.is_empty() {
for tag in &self.tags {
writer.write_all(b"@").await?;
tag.write_async(writer).await?;
writer.write_all(b" ").await?;
}
}
match &self.sender { match &self.sender {
Some(ref sender) => { Some(ref sender) => {
writer.write_all(b":").await?; writer.write_all(b":").await?;

View File

@ -48,7 +48,7 @@ impl Jid {
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
lazy_static! { lazy_static! {
static ref RE: Regex = Regex::new(r"^(([a-zA-Z0-9]+)@)?([^@/]+)(/([a-zA-Z0-9\-]+))?$").unwrap(); static ref RE: Regex = Regex::new(r"^(([a-zA-Z]+)@)?([a-zA-Z.]+)(/([a-zA-Z\-]+))?$").unwrap();
} }
let m = RE.captures(i).ok_or(anyhow!("Incorrectly format jid: {i}"))?; let m = RE.captures(i).ok_or(anyhow!("Incorrectly format jid: {i}"))?;

View File

@ -295,9 +295,9 @@ impl ToXml for IqError {
#[derive(PartialEq, Eq, Debug)] #[derive(PartialEq, Eq, Debug)]
pub struct Iq<T> { pub struct Iq<T> {
pub from: Option<Jid>, pub from: Option<String>,
pub id: String, pub id: String,
pub to: Option<Jid>, pub to: Option<String>,
pub r#type: IqType, pub r#type: IqType,
pub body: T, pub body: T,
} }
@ -323,9 +323,9 @@ enum IqParserInner<T: FromXml> {
Final(IqParserState<T>), Final(IqParserState<T>),
} }
struct IqParserState<T> { struct IqParserState<T> {
pub from: Option<Jid>, pub from: Option<String>,
pub id: Option<String>, pub id: Option<String>,
pub to: Option<Jid>, pub to: Option<String>,
pub r#type: Option<IqType>, pub r#type: Option<IqType>,
pub body: Option<T>, pub body: Option<T>,
} }
@ -348,15 +348,13 @@ impl<T: FromXml> Parser for IqParser<T> {
let attr = fail_fast!(attr); let attr = fail_fast!(attr);
if attr.key.0 == b"from" { if attr.key.0 == b"from" {
let value = fail_fast!(std::str::from_utf8(&*attr.value)); let value = fail_fast!(std::str::from_utf8(&*attr.value));
let value = fail_fast!(Jid::from_string(value)); state.from = Some(value.to_string())
state.from = Some(value)
} else if attr.key.0 == b"id" { } else if attr.key.0 == b"id" {
let value = fail_fast!(std::str::from_utf8(&*attr.value)); let value = fail_fast!(std::str::from_utf8(&*attr.value));
state.id = Some(value.to_string()) state.id = Some(value.to_string())
} else if attr.key.0 == b"to" { } else if attr.key.0 == b"to" {
let value = fail_fast!(std::str::from_utf8(&*attr.value)); let value = fail_fast!(std::str::from_utf8(&*attr.value));
let value = fail_fast!(Jid::from_string(value)); state.to = Some(value.to_string())
state.to = Some(value)
} else if attr.key.0 == b"type" { } else if attr.key.0 == b"type" {
let value = fail_fast!(IqType::from_str(&*attr.value)); let value = fail_fast!(IqType::from_str(&*attr.value));
state.r#type = Some(value); state.r#type = Some(value);
@ -433,17 +431,15 @@ impl<T: ToXml> ToXml for Iq<T> {
let mut start = BytesStart::new(start); let mut start = BytesStart::new(start);
let mut attrs = vec![]; let mut attrs = vec![];
if let Some(ref from) = self.from { if let Some(ref from) = self.from {
let value = from.to_string().into_bytes();
attrs.push(Attribute { attrs.push(Attribute {
key: QName(b"from"), key: QName(b"from"),
value: value.into(), value: from.as_bytes().into(),
}); });
}; };
if let Some(ref to) = self.to { if let Some(ref to) = self.to {
let value = to.to_string().into_bytes();
attrs.push(Attribute { attrs.push(Attribute {
key: QName(b"to"), key: QName(b"to"),
value: value.into(), value: to.as_bytes().into(),
}); });
} }
attrs.push(Attribute { attrs.push(Attribute {

View File

@ -1,30 +1,47 @@
use quick_xml::events::{BytesStart, Event}; use quick_xml::events::{BytesStart, Event};
use crate::xml::*; use crate::xml::*;
use anyhow::{anyhow, Result}; use anyhow::{anyhow as ffail, Result};
use quick_xml::name::ResolveResult;
pub const XMLNS: &'static str = "jabber:iq:roster"; pub const XMLNS: &'static str = "jabber:iq:roster";
#[derive(PartialEq, Eq, Debug)] #[derive(PartialEq, Eq, Debug)]
pub struct RosterQuery; pub struct RosterQuery;
pub struct QueryParser(QueryParserInner);
enum QueryParserInner {
Initial,
InQuery,
}
impl Parser for QueryParser {
type Output = Result<RosterQuery>;
fn consume<'a>(
self: Self,
namespace: quick_xml::name::ResolveResult,
event: &quick_xml::events::Event<'a>,
) -> Continuation<Self, Self::Output> {
match self.0 {
QueryParserInner::Initial => match event {
Event::Start(_) => Continuation::Continue(QueryParser(QueryParserInner::InQuery)),
Event::Empty(_) => Continuation::Final(Ok(RosterQuery)),
_ => Continuation::Final(Err(ffail!("Unexpected XML event: {event:?}"))),
},
QueryParserInner::InQuery => match event {
Event::End(_) => Continuation::Final(Ok(RosterQuery)),
_ => Continuation::Final(Err(ffail!("Unexpected XML event: {event:?}"))),
},
}
}
}
impl FromXml for RosterQuery { impl FromXml for RosterQuery {
type P = impl Parser<Output = Result<Self>>; type P = QueryParser;
fn parse() -> Self::P { fn parse() -> Self::P {
|(mut namespace, mut event): (ResolveResult<'static>, &'static Event<'static>)| -> Result<Self> { QueryParser(QueryParserInner::Initial)
match event {
Event::Start(_) => (),
Event::Empty(_) => return Ok(RosterQuery),
_ => return Err(anyhow!("Unexpected XML event: {event:?}")),
}
(namespace, event) = yield;
match event {
Event::End(_) => return Ok(RosterQuery),
_ => return Err(anyhow!("Unexpected XML event: {event:?}")),
}
}
} }
} }

View File

@ -2,29 +2,46 @@ use quick_xml::events::{BytesStart, Event};
use crate::xml::*; use crate::xml::*;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use quick_xml::name::ResolveResult;
pub const XMLNS: &'static str = "urn:ietf:params:xml:ns:xmpp-session"; pub const XMLNS: &'static str = "urn:ietf:params:xml:ns:xmpp-session";
#[derive(PartialEq, Eq, Debug)] #[derive(PartialEq, Eq, Debug)]
pub struct Session; pub struct Session;
pub struct SessionParser(SessionParserInner);
enum SessionParserInner {
Initial,
InSession,
}
impl Parser for SessionParser {
type Output = Result<Session>;
fn consume<'a>(
self: Self,
namespace: quick_xml::name::ResolveResult,
event: &quick_xml::events::Event<'a>,
) -> Continuation<Self, Self::Output> {
match self.0 {
SessionParserInner::Initial => match event {
Event::Start(_) => Continuation::Continue(SessionParser(SessionParserInner::InSession)),
Event::Empty(_) => Continuation::Final(Ok(Session)),
_ => Continuation::Final(Err(anyhow!("Unexpected XML event: {event:?}"))),
},
SessionParserInner::InSession => match event {
Event::End(_) => Continuation::Final(Ok(Session)),
_ => Continuation::Final(Err(anyhow!("Unexpected XML event: {event:?}"))),
},
}
}
}
impl FromXml for Session { impl FromXml for Session {
type P = impl Parser<Output = Result<Self>>; type P = SessionParser;
fn parse() -> Self::P { fn parse() -> Self::P {
|(mut namespace, mut event): (ResolveResult<'static>, &'static Event<'static>)| -> Result<Self> { SessionParser(SessionParserInner::Initial)
match event {
Event::Start(_) => (),
Event::Empty(_) => return Ok(Session),
_ => return Err(anyhow!("Unexpected XML event: {event:?}")),
}
(namespace, event) = yield;
match event {
Event::End(_) => return Ok(Session),
_ => return Err(anyhow!("Unexpected XML event: {event:?}")),
}
}
} }
} }

View File

@ -1,35 +1,49 @@
use super::*; use super::*;
use derive_more::From;
#[derive(Default, Debug, PartialEq, Eq)] #[derive(Default, Debug, PartialEq, Eq)]
pub struct Ignore; pub struct Ignore;
#[derive(From)]
pub struct IgnoreParser(IgnoreParserInner);
enum IgnoreParserInner {
Initial,
InTag { name: Vec<u8>, depth: u8 },
}
impl Parser for IgnoreParser {
type Output = Result<Ignore>;
fn consume<'a>(self: Self, _: ResolveResult, event: &Event<'a>) -> Continuation<Self, Self::Output> {
match self.0 {
IgnoreParserInner::Initial => match event {
Event::Start(bytes) => {
let name = bytes.name().0.to_owned();
Continuation::Continue(IgnoreParserInner::InTag { name, depth: 0 }.into())
}
Event::Empty(_) => Continuation::Final(Ok(Ignore)),
_ => Continuation::Final(Ok(Ignore)),
},
IgnoreParserInner::InTag { name, depth } => match event {
Event::End(bytes) if name == bytes.name().0 => {
if depth == 0 {
Continuation::Final(Ok(Ignore))
} else {
Continuation::Continue(IgnoreParserInner::InTag { name, depth: depth - 1 }.into())
}
}
_ => Continuation::Continue(IgnoreParserInner::InTag { name, depth }.into()),
},
}
}
}
impl FromXml for Ignore { impl FromXml for Ignore {
type P = impl Parser<Output = Result<Self>>; type P = IgnoreParser;
fn parse() -> Self::P { fn parse() -> Self::P {
|(mut namespace, mut event): (ResolveResult<'static>, &'static Event<'static>)| -> Result<Self> { IgnoreParserInner::Initial.into()
let mut depth = match event {
Event::Start(bytes) => 0,
Event::Empty(_) => return Ok(Ignore),
_ => return Ok(Ignore),
};
loop {
(namespace, event) = yield;
match event {
Event::End(_) => {
if depth == 0 {
return Ok(Ignore);
} else {
depth -= 1;
}
}
Event::Start(_) => {
depth += 1;
}
_ => (),
}
}
}
} }
} }

View File

@ -1 +1 @@
nightly-2024-04-19 nightly-2024-03-20

View File

@ -52,7 +52,7 @@ async fn main() -> Result<()> {
let mut metrics = MetricsRegistry::new(); let mut metrics = MetricsRegistry::new();
let storage = Storage::open(storage_config).await?; let storage = Storage::open(storage_config).await?;
let rooms = RoomRegistry::new(&mut metrics, storage.clone())?; let rooms = RoomRegistry::new(&mut metrics, storage.clone())?;
let mut players = PlayerRegistry::empty(rooms.clone(), storage.clone(), &mut metrics)?; let mut players = PlayerRegistry::empty(rooms.clone(), &mut metrics)?;
let telemetry_terminator = http::launch(telemetry_config, metrics.clone(), rooms.clone(), storage.clone()).await?; let telemetry_terminator = http::launch(telemetry_config, metrics.clone(), rooms.clone(), storage.clone()).await?;
let irc = projection_irc::launch( let irc = projection_irc::launch(
irc_config, irc_config,