mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 986ab7d051 | |||
| 6767ba826f | |||
| 7207398a9e | |||
| 1a7bda209b | |||
| 7e1950b3d2 | |||
| b507898c62 | |||
| f4af67575e | |||
| 6adb99397e | |||
| 8ce83a8a14 | |||
| 052c4dfa21 | |||
| a43dee1728 | |||
| 763d9b3de8 | |||
| 1e6d95583c | |||
| 8a254a33cc | |||
| c97dd54766 | |||
| 8ddb7c70c0 | |||
| cb9786466b | |||
| 18d2662b01 | |||
| 558262dd1f | |||
| d311b87579 | |||
| 8702f55cf5 | |||
| d4481b07ac | |||
| 92351df925 | |||
| 47e2733ea1 | |||
| 6637e4c6a7 | |||
| 35e441452f | |||
| 66bbb655bf | |||
| 81b202ce51 | |||
| 4657844d46 | |||
| 9016cd11a6 | |||
| dd70094719 | |||
| fcd49b7ab3 | |||
| 470c9b52dd | |||
| 0d8cafc329 | |||
| 2f9956ddca | |||
| 21a97cdd0b | |||
| e986cd4536 | |||
| 526d862296 | |||
| fbeb5bf186 | |||
| a336f2df44 | |||
| 19b78ec73e | |||
| 27ff2d9363 | |||
| 50fa8c3abf | |||
| 18c4be869f | |||
| fc00b96d8b | |||
| fa4156d8a6 | |||
| 23638cd714 | |||
| 9f1a483e76 | |||
| 688ef727e5 | |||
| 3de026160e | |||
| 9fe761513d | |||
| abf1e1195a | |||
| d9537e9b55 | |||
| 0d1de70d8f | |||
| 4aa03a71eb | |||
| f847918575 | |||
| 7569a0545b |
Generated
+63
-223
@@ -72,15 +72,6 @@ dependencies = [
|
|||||||
"alloc-no-stdlib",
|
"alloc-no-stdlib",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "android_system_properties"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotate-snippets"
|
name = "annotate-snippets"
|
||||||
version = "0.12.12"
|
version = "0.12.12"
|
||||||
@@ -454,13 +445,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-extra"
|
name = "axum-extra"
|
||||||
version = "0.10.3"
|
version = "0.12.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96"
|
checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"headers",
|
"headers",
|
||||||
"http",
|
"http",
|
||||||
@@ -468,8 +460,6 @@ dependencies = [
|
|||||||
"http-body-util",
|
"http-body-util",
|
||||||
"mime",
|
"mime",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustversion",
|
|
||||||
"serde_core",
|
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -615,15 +605,6 @@ dependencies = [
|
|||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "block2"
|
|
||||||
version = "0.6.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
|
||||||
dependencies = [
|
|
||||||
"objc2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blurhash"
|
name = "blurhash"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
@@ -813,13 +794,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clang-sys"
|
name = "clang-sys"
|
||||||
version = "1.8.1"
|
version = "1.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"glob",
|
"glob",
|
||||||
"libc",
|
"libc",
|
||||||
"libloading",
|
"libloading 0.7.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1025,7 +1006,7 @@ dependencies = [
|
|||||||
"ipaddress",
|
"ipaddress",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"libc",
|
"libc",
|
||||||
"libloading",
|
"libloading 0.9.0",
|
||||||
"lock_api",
|
"lock_api",
|
||||||
"log",
|
"log",
|
||||||
"maplit",
|
"maplit",
|
||||||
@@ -1240,7 +1221,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "continuwuity-admin-api"
|
name = "continuwuity-admin-api"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=df6fa9a394b259c9cd3420e2b52a47847510ed86#df6fa9a394b259c9cd3420e2b52a47847510ed86"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1596,16 +1577,6 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dispatch2"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"objc2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -1629,7 +1600,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "draupnir-antispam"
|
name = "draupnir-antispam"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=df6fa9a394b259c9cd3420e2b52a47847510ed86#df6fa9a394b259c9cd3420e2b52a47847510ed86"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -2831,9 +2802,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.182"
|
version = "0.2.180"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libfuzzer-sys"
|
name = "libfuzzer-sys"
|
||||||
@@ -2847,9 +2818,19 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libloading"
|
name = "libloading"
|
||||||
version = "0.8.9"
|
version = "0.7.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
|
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libloading"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
@@ -3021,7 +3002,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "meowlnir-antispam"
|
name = "meowlnir-antispam"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=df6fa9a394b259c9cd3420e2b52a47847510ed86#df6fa9a394b259c9cd3420e2b52a47847510ed86"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -3122,9 +3103,9 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.30.1"
|
version = "0.31.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
@@ -3278,165 +3259,6 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "objc2"
|
|
||||||
version = "0.6.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05"
|
|
||||||
dependencies = [
|
|
||||||
"objc2-encode",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "objc2-cloud-kit"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"objc2",
|
|
||||||
"objc2-foundation",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "objc2-core-data"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
|
||||||
dependencies = [
|
|
||||||
"objc2",
|
|
||||||
"objc2-foundation",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "objc2-core-foundation"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"dispatch2",
|
|
||||||
"objc2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "objc2-core-graphics"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"dispatch2",
|
|
||||||
"objc2",
|
|
||||||
"objc2-core-foundation",
|
|
||||||
"objc2-io-surface",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "objc2-core-image"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
|
|
||||||
dependencies = [
|
|
||||||
"objc2",
|
|
||||||
"objc2-foundation",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "objc2-core-location"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009"
|
|
||||||
dependencies = [
|
|
||||||
"objc2",
|
|
||||||
"objc2-foundation",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "objc2-core-text"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"objc2",
|
|
||||||
"objc2-core-foundation",
|
|
||||||
"objc2-core-graphics",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "objc2-encode"
|
|
||||||
version = "4.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "objc2-foundation"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"block2",
|
|
||||||
"libc",
|
|
||||||
"objc2",
|
|
||||||
"objc2-core-foundation",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "objc2-io-surface"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"objc2",
|
|
||||||
"objc2-core-foundation",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "objc2-quartz-core"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"objc2",
|
|
||||||
"objc2-core-foundation",
|
|
||||||
"objc2-foundation",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "objc2-ui-kit"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"block2",
|
|
||||||
"objc2",
|
|
||||||
"objc2-cloud-kit",
|
|
||||||
"objc2-core-data",
|
|
||||||
"objc2-core-foundation",
|
|
||||||
"objc2-core-graphics",
|
|
||||||
"objc2-core-image",
|
|
||||||
"objc2-core-location",
|
|
||||||
"objc2-core-text",
|
|
||||||
"objc2-foundation",
|
|
||||||
"objc2-quartz-core",
|
|
||||||
"objc2-user-notifications",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "objc2-user-notifications"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"
|
|
||||||
dependencies = [
|
|
||||||
"objc2",
|
|
||||||
"objc2-foundation",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.37.3"
|
version = "0.37.3"
|
||||||
@@ -3549,18 +3371,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "os_info"
|
name = "os_info"
|
||||||
version = "3.14.0"
|
version = "3.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224"
|
checksum = "d0e1ac5fde8d43c34139135df8ea9ee9465394b2d8d20f032d38998f64afffc3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android_system_properties",
|
|
||||||
"log",
|
"log",
|
||||||
"nix",
|
"plist",
|
||||||
"objc2",
|
|
||||||
"objc2-foundation",
|
|
||||||
"objc2-ui-kit",
|
|
||||||
"serde",
|
"serde",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3742,6 +3560,19 @@ version = "0.3.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "plist"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"indexmap",
|
||||||
|
"quick-xml",
|
||||||
|
"serde",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "png"
|
name = "png"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
@@ -3925,6 +3756,15 @@ 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 = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.38.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quinn"
|
name = "quinn"
|
||||||
version = "0.11.9"
|
version = "0.11.9"
|
||||||
@@ -4254,7 +4094,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma"
|
name = "ruma"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=df6fa9a394b259c9cd3420e2b52a47847510ed86#df6fa9a394b259c9cd3420e2b52a47847510ed86"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"assign",
|
"assign",
|
||||||
"continuwuity-admin-api",
|
"continuwuity-admin-api",
|
||||||
@@ -4277,7 +4117,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-appservice-api"
|
name = "ruma-appservice-api"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=df6fa9a394b259c9cd3420e2b52a47847510ed86#df6fa9a394b259c9cd3420e2b52a47847510ed86"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
@@ -4289,7 +4129,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-client-api"
|
name = "ruma-client-api"
|
||||||
version = "0.18.0"
|
version = "0.18.0"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=df6fa9a394b259c9cd3420e2b52a47847510ed86#df6fa9a394b259c9cd3420e2b52a47847510ed86"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"as_variant",
|
"as_variant",
|
||||||
"assign",
|
"assign",
|
||||||
@@ -4312,7 +4152,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-common"
|
name = "ruma-common"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=df6fa9a394b259c9cd3420e2b52a47847510ed86#df6fa9a394b259c9cd3420e2b52a47847510ed86"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"as_variant",
|
"as_variant",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -4344,7 +4184,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-events"
|
name = "ruma-events"
|
||||||
version = "0.28.1"
|
version = "0.28.1"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=df6fa9a394b259c9cd3420e2b52a47847510ed86#df6fa9a394b259c9cd3420e2b52a47847510ed86"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"as_variant",
|
"as_variant",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
@@ -4369,7 +4209,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-federation-api"
|
name = "ruma-federation-api"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=df6fa9a394b259c9cd3420e2b52a47847510ed86#df6fa9a394b259c9cd3420e2b52a47847510ed86"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"headers",
|
"headers",
|
||||||
@@ -4391,7 +4231,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-identifiers-validation"
|
name = "ruma-identifiers-validation"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=df6fa9a394b259c9cd3420e2b52a47847510ed86#df6fa9a394b259c9cd3420e2b52a47847510ed86"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
@@ -4400,7 +4240,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-identity-service-api"
|
name = "ruma-identity-service-api"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=df6fa9a394b259c9cd3420e2b52a47847510ed86#df6fa9a394b259c9cd3420e2b52a47847510ed86"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
@@ -4410,7 +4250,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-macros"
|
name = "ruma-macros"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=df6fa9a394b259c9cd3420e2b52a47847510ed86#df6fa9a394b259c9cd3420e2b52a47847510ed86"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
@@ -4425,7 +4265,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-push-gateway-api"
|
name = "ruma-push-gateway-api"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=df6fa9a394b259c9cd3420e2b52a47847510ed86#df6fa9a394b259c9cd3420e2b52a47847510ed86"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
@@ -4437,7 +4277,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-signatures"
|
name = "ruma-signatures"
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=df6fa9a394b259c9cd3420e2b52a47847510ed86#df6fa9a394b259c9cd3420e2b52a47847510ed86"
|
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
|
|||||||
+6
-4
@@ -97,7 +97,7 @@ features = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies.axum-extra]
|
[workspace.dependencies.axum-extra]
|
||||||
version = "0.10.1"
|
version = "0.12.0"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["typed-header", "tracing"]
|
features = ["typed-header", "tracing"]
|
||||||
|
|
||||||
@@ -253,7 +253,7 @@ features = [
|
|||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|
||||||
[workspace.dependencies.libloading]
|
[workspace.dependencies.libloading]
|
||||||
version = "0.8.6"
|
version = "0.9.0"
|
||||||
|
|
||||||
# Validating urls in config, was already a transitive dependency
|
# Validating urls in config, was already a transitive dependency
|
||||||
[workspace.dependencies.url]
|
[workspace.dependencies.url]
|
||||||
@@ -343,7 +343,7 @@ version = "0.1.2"
|
|||||||
[workspace.dependencies.ruma]
|
[workspace.dependencies.ruma]
|
||||||
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
|
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
|
||||||
#branch = "conduwuit-changes"
|
#branch = "conduwuit-changes"
|
||||||
rev = "e087ff15888156942ca2ffe6097d1b4c3fd27628"
|
rev = "bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||||
features = [
|
features = [
|
||||||
"compat",
|
"compat",
|
||||||
"rand",
|
"rand",
|
||||||
@@ -363,6 +363,7 @@ features = [
|
|||||||
"unstable-msc2870",
|
"unstable-msc2870",
|
||||||
"unstable-msc3026",
|
"unstable-msc3026",
|
||||||
"unstable-msc3061",
|
"unstable-msc3061",
|
||||||
|
"unstable-msc3814",
|
||||||
"unstable-msc3245",
|
"unstable-msc3245",
|
||||||
"unstable-msc3266",
|
"unstable-msc3266",
|
||||||
"unstable-msc3381", # polls
|
"unstable-msc3381", # polls
|
||||||
@@ -381,6 +382,7 @@ features = [
|
|||||||
"unstable-pdu",
|
"unstable-pdu",
|
||||||
"unstable-msc4155",
|
"unstable-msc4155",
|
||||||
"unstable-msc4143", # livekit well_known response
|
"unstable-msc4143", # livekit well_known response
|
||||||
|
"unstable-msc4284"
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies.rust-rocksdb]
|
[workspace.dependencies.rust-rocksdb]
|
||||||
@@ -472,7 +474,7 @@ features = ["use_std"]
|
|||||||
version = "0.5"
|
version = "0.5"
|
||||||
|
|
||||||
[workspace.dependencies.nix]
|
[workspace.dependencies.nix]
|
||||||
version = "0.30.1"
|
version = "0.31.0"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["resource"]
|
features = ["resource"]
|
||||||
|
|
||||||
|
|||||||
@@ -57,10 +57,15 @@ Continuwuity aims to:
|
|||||||
|
|
||||||
### Can I try it out?
|
### Can I try it out?
|
||||||
|
|
||||||
Check out the [documentation](https://continuwuity.org) for installation instructions, or join one of these vetted public homeservers running Continuwuity to get a feel for things!
|
Check out the [documentation](https://continuwuity.org) for installation instructions.
|
||||||
|
|
||||||
- https://continuwuity.rocks -- A public demo server operated by the Continuwuity Team.
|
If you want to try it out as a user, we have some partnered homeservers you can use:
|
||||||
- https://federated.nexus -- Federated Nexus is a community resource hosting multiple FOSS (especially federated) services, including Matrix and Forgejo.
|
* You can head over to [https://federated.nexus](https://federated.nexus/) in your browser.
|
||||||
|
* Hit the `Apply to Join` button. Once your request has been accepted, you will receive an email with your username and password.
|
||||||
|
* Head over to [https://app.federated.nexus](https://app.federated.nexus/) and you can sign in there, or use any other matrix chat client you wish elsewhere.
|
||||||
|
* Your username for matrix will be in the form of `@username:federated.nexus`, however you can simply use the `username` part to log in. Your password is your password.
|
||||||
|
|
||||||
|
* There's also [https://continuwuity.rocks/](https://continuwuity.rocks/). You can register a new account using Cinny via [this convenient link](https://app.cinny.in/register/continuwuity.rocks), or you can use Element or another matrix client *that supports registration*.
|
||||||
|
|
||||||
### What are we working on?
|
### What are we working on?
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Improved the concurrency handling of federation transactions, vastly improving performance and reliability by more accurately handling inbound transactions and reducing the amount of repeated wasted work. Contributed by @nex and @Jade.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Added MSC3202 Device masquerading (not all of MSC3202). This should fix issues with enabling MSC4190 for some Mautrix bridges. Contributed by @Jade
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Added MSC3814 Dehydrated Devices - you can now decrypt messages sent while all devices were logged out.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Removed the `allow_public_room_directory_without_auth` config option. Contributed by @0xnim.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Implement MSC4143 MatrixRTC transport discovery endpoint. Move RTC foci configuration from `[global.well_known]` to a new `[global.matrix_rtc]` section with a `foci` field. Contributed by @0xnim
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Fixed sliding sync v5 list ranges always starting from 0, causing extra rooms to be unnecessarily processed and returned. Contributed by @0xnim
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
BREAKING: Added an entrypoint to the Docker image. This means you no longer need to specify the binary when running a command using the image. Contributed by @Jade
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Improved URL preview fetching with a more compatible user agent for sites like YouTube Music. Added `!admin media delete-url-preview <url>` command to clear cached URL previews that were stuck and broken.
|
||||||
@@ -9,7 +9,6 @@ address = "0.0.0.0"
|
|||||||
allow_device_name_federation = true
|
allow_device_name_federation = true
|
||||||
allow_guest_registration = true
|
allow_guest_registration = true
|
||||||
allow_public_room_directory_over_federation = true
|
allow_public_room_directory_over_federation = true
|
||||||
allow_public_room_directory_without_auth = true
|
|
||||||
allow_registration = true
|
allow_registration = true
|
||||||
database_path = "/database"
|
database_path = "/database"
|
||||||
log = "trace,h2=debug,hyper=debug"
|
log = "trace,h2=debug,hyper=debug"
|
||||||
|
|||||||
+43
-14
@@ -290,6 +290,25 @@
|
|||||||
#
|
#
|
||||||
#max_fetch_prev_events = 192
|
#max_fetch_prev_events = 192
|
||||||
|
|
||||||
|
# How many incoming federation transactions the server is willing to be
|
||||||
|
# processing at any given time before it becomes overloaded and starts
|
||||||
|
# rejecting further transactions until some slots become available.
|
||||||
|
#
|
||||||
|
# Setting this value too low or too high may result in unstable
|
||||||
|
# federation, and setting it too high may cause runaway resource usage.
|
||||||
|
#
|
||||||
|
#max_concurrent_inbound_transactions = 150
|
||||||
|
|
||||||
|
# Maximum age (in seconds) for cached federation transaction responses.
|
||||||
|
# Entries older than this will be removed during cleanup.
|
||||||
|
#
|
||||||
|
#transaction_id_cache_max_age_secs = 7200 (2 hours)
|
||||||
|
|
||||||
|
# Maximum number of cached federation transaction responses.
|
||||||
|
# When the cache exceeds this limit, older entries will be removed.
|
||||||
|
#
|
||||||
|
#transaction_id_cache_max_entries = 8192
|
||||||
|
|
||||||
# Default/base connection timeout (seconds). This is used only by URL
|
# Default/base connection timeout (seconds). This is used only by URL
|
||||||
# previews and update/news endpoint checks.
|
# previews and update/news endpoint checks.
|
||||||
#
|
#
|
||||||
@@ -527,12 +546,6 @@
|
|||||||
#
|
#
|
||||||
#allow_public_room_directory_over_federation = false
|
#allow_public_room_directory_over_federation = false
|
||||||
|
|
||||||
# Set this to true to allow your server's public room directory to be
|
|
||||||
# queried without client authentication (access token) through the Client
|
|
||||||
# APIs. Set this to false to protect against /publicRooms spiders.
|
|
||||||
#
|
|
||||||
#allow_public_room_directory_without_auth = false
|
|
||||||
|
|
||||||
# Allow guests/unauthenticated users to access TURN credentials.
|
# Allow guests/unauthenticated users to access TURN credentials.
|
||||||
#
|
#
|
||||||
# This is the equivalent of Synapse's `turn_allow_guests` config option.
|
# This is the equivalent of Synapse's `turn_allow_guests` config option.
|
||||||
@@ -1325,7 +1338,7 @@
|
|||||||
# sender user's server name, inbound federation X-Matrix origin, and
|
# sender user's server name, inbound federation X-Matrix origin, and
|
||||||
# outbound federation handler.
|
# outbound federation handler.
|
||||||
#
|
#
|
||||||
# You can set this to ["*"] to block all servers by default, and then
|
# You can set this to [".*"] to block all servers by default, and then
|
||||||
# use `allowed_remote_server_names` to allow only specific servers.
|
# use `allowed_remote_server_names` to allow only specific servers.
|
||||||
#
|
#
|
||||||
# example: ["badserver\\.tld$", "badphrase", "19dollarfortnitecards"]
|
# example: ["badserver\\.tld$", "badphrase", "19dollarfortnitecards"]
|
||||||
@@ -1831,14 +1844,13 @@
|
|||||||
#
|
#
|
||||||
#support_mxid =
|
#support_mxid =
|
||||||
|
|
||||||
# A list of MatrixRTC foci URLs which will be served as part of the
|
# **DEPRECATED**: Use `[global.matrix_rtc].foci` instead.
|
||||||
# MSC4143 client endpoint at /.well-known/matrix/client. If you're
|
|
||||||
# setting up livekit, you'd want something like:
|
|
||||||
# rtc_focus_server_urls = [
|
|
||||||
# { type = "livekit", livekit_service_url = "https://livekit.example.com" },
|
|
||||||
# ]
|
|
||||||
#
|
#
|
||||||
# To disable, set this to be an empty vector (`[]`).
|
# A list of MatrixRTC foci URLs which will be served as part of the
|
||||||
|
# MSC4143 client endpoint at /.well-known/matrix/client.
|
||||||
|
#
|
||||||
|
# This option is deprecated and will be removed in a future release.
|
||||||
|
# Please migrate to the new `[global.matrix_rtc]` config section.
|
||||||
#
|
#
|
||||||
#rtc_focus_server_urls = []
|
#rtc_focus_server_urls = []
|
||||||
|
|
||||||
@@ -1860,6 +1872,23 @@
|
|||||||
#
|
#
|
||||||
#blurhash_max_raw_size = 33554432
|
#blurhash_max_raw_size = 33554432
|
||||||
|
|
||||||
|
[global.matrix_rtc]
|
||||||
|
|
||||||
|
# A list of MatrixRTC foci (transports) which will be served via the
|
||||||
|
# MSC4143 RTC transports endpoint at
|
||||||
|
# `/_matrix/client/v1/rtc/transports`. If you're setting up livekit,
|
||||||
|
# you'd want something like:
|
||||||
|
# ```toml
|
||||||
|
# [global.matrix_rtc]
|
||||||
|
# foci = [
|
||||||
|
# { type = "livekit", livekit_service_url = "https://livekit.example.com" },
|
||||||
|
# ]
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# To disable, set this to an empty list (`[]`).
|
||||||
|
#
|
||||||
|
#foci = []
|
||||||
|
|
||||||
[global.ldap]
|
[global.ldap]
|
||||||
|
|
||||||
# Whether to enable LDAP login.
|
# Whether to enable LDAP login.
|
||||||
|
|||||||
+9
-3
@@ -52,7 +52,7 @@ ENV BINSTALL_VERSION=1.17.5
|
|||||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||||
ENV CARGO_SBOM_VERSION=0.9.1
|
ENV CARGO_SBOM_VERSION=0.9.1
|
||||||
# renovate: datasource=crate depName=lddtree
|
# renovate: datasource=crate depName=lddtree
|
||||||
ENV LDDTREE_VERSION=0.4.0
|
ENV LDDTREE_VERSION=0.5.0
|
||||||
# renovate: datasource=crate depName=timelord-cli
|
# renovate: datasource=crate depName=timelord-cli
|
||||||
ENV TIMELORD_VERSION=3.0.1
|
ENV TIMELORD_VERSION=3.0.1
|
||||||
|
|
||||||
@@ -180,6 +180,11 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
|||||||
export RUSTFLAGS="${RUSTFLAGS}"
|
export RUSTFLAGS="${RUSTFLAGS}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
RUST_PROFILE_DIR="${RUST_PROFILE}"
|
||||||
|
if [[ "${RUST_PROFILE}" == "dev" ]]; then
|
||||||
|
RUST_PROFILE_DIR="debug"
|
||||||
|
fi
|
||||||
|
|
||||||
TARGET_DIR=($(cargo metadata --no-deps --format-version 1 | \
|
TARGET_DIR=($(cargo metadata --no-deps --format-version 1 | \
|
||||||
jq -r ".target_directory"))
|
jq -r ".target_directory"))
|
||||||
mkdir /out/sbin
|
mkdir /out/sbin
|
||||||
@@ -191,8 +196,8 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
|||||||
jq -r ".packages[] | select(.name == \"$PACKAGE\") | .targets[] | select( .kind | map(. == \"bin\") | any ) | .name"))
|
jq -r ".packages[] | select(.name == \"$PACKAGE\") | .targets[] | select( .kind | map(. == \"bin\") | any ) | .name"))
|
||||||
for BINARY in "${BINARIES[@]}"; do
|
for BINARY in "${BINARIES[@]}"; do
|
||||||
echo $BINARY
|
echo $BINARY
|
||||||
xx-verify $TARGET_DIR/$(xx-cargo --print-target-triple)/${RUST_PROFILE}/$BINARY
|
xx-verify $TARGET_DIR/$(xx-cargo --print-target-triple)/${RUST_PROFILE_DIR}/$BINARY
|
||||||
cp $TARGET_DIR/$(xx-cargo --print-target-triple)/${RUST_PROFILE}/$BINARY /out/sbin/$BINARY
|
cp $TARGET_DIR/$(xx-cargo --print-target-triple)/${RUST_PROFILE_DIR}/$BINARY /out/sbin/$BINARY
|
||||||
done
|
done
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
@@ -276,4 +281,5 @@ ENV LD_LIBRARY_PATH=/usr/lib
|
|||||||
# Continuwuity default port
|
# Continuwuity default port
|
||||||
EXPOSE 8008
|
EXPOSE 8008
|
||||||
|
|
||||||
|
ENTRYPOINT [ "/sbin/conduwuit" ]
|
||||||
CMD ["/sbin/conduwuit"]
|
CMD ["/sbin/conduwuit"]
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ ENV BINSTALL_VERSION=1.17.5
|
|||||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||||
ENV CARGO_SBOM_VERSION=0.9.1
|
ENV CARGO_SBOM_VERSION=0.9.1
|
||||||
# renovate: datasource=crate depName=lddtree
|
# renovate: datasource=crate depName=lddtree
|
||||||
ENV LDDTREE_VERSION=0.4.0
|
ENV LDDTREE_VERSION=0.5.0
|
||||||
|
|
||||||
# Install unpackaged tools
|
# Install unpackaged tools
|
||||||
RUN <<EOF
|
RUN <<EOF
|
||||||
|
|||||||
+4
-32
@@ -78,47 +78,19 @@ You will need to allow ports `7881/tcp` and `50100:50200/udp` through your firew
|
|||||||
|
|
||||||
### 3. Telling clients where to find LiveKit
|
### 3. Telling clients where to find LiveKit
|
||||||
|
|
||||||
To tell clients where to find LiveKit, you need to add the address of your `lk-jwt-service` to your client .well-known file. To do so, in the config section `global.well-known`, add (or modify) the option `rtc_focus_server_urls`.
|
To tell clients where to find LiveKit, you need to add the address of your `lk-jwt-service` to the `[global.matrix_rtc]` config section using the `foci` option.
|
||||||
|
|
||||||
The variable should be a list of servers serving as MatrixRTC endpoints to serve in the well-known file to the client.
|
The variable should be a list of servers serving as MatrixRTC endpoints. Clients discover these via the `/_matrix/client/v1/rtc/transports` endpoint (MSC4143).
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
rtc_focus_server_urls = [
|
[global.matrix_rtc]
|
||||||
|
foci = [
|
||||||
{ type = "livekit", livekit_service_url = "https://livekit.example.com" },
|
{ type = "livekit", livekit_service_url = "https://livekit.example.com" },
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
Remember to replace the URL with the address you are deploying your instance of lk-jwt-service to.
|
Remember to replace the URL with the address you are deploying your instance of lk-jwt-service to.
|
||||||
|
|
||||||
#### Serving .well-known manually
|
|
||||||
|
|
||||||
If you don't let Continuwuity serve your `.well-known` files, you need to add the following lines to your `.well-known/matrix/client` file, remembering to replace the URL with your own `lk-jwt-service` deployment:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"org.matrix.msc4143.rtc_foci": [
|
|
||||||
{
|
|
||||||
"type": "livekit",
|
|
||||||
"livekit_service_url": "https://livekit.example.com"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
The final file should look something like this:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"m.homeserver": {
|
|
||||||
"base_url":"https://matrix.example.com"
|
|
||||||
},
|
|
||||||
"org.matrix.msc4143.rtc_foci": [
|
|
||||||
{
|
|
||||||
"type": "livekit",
|
|
||||||
"livekit_service_url": "https://livekit.example.com"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Configure your Reverse Proxy
|
### 4. Configure your Reverse Proxy
|
||||||
|
|
||||||
Reverse proxies can be configured in many different ways - so we can't provide a step by step for this.
|
Reverse proxies can be configured in many different ways - so we can't provide a step by step for this.
|
||||||
|
|||||||
@@ -51,7 +51,13 @@ continuwuity aims to:
|
|||||||
|
|
||||||
Check out the [documentation](https://continuwuity.org) for installation instructions.
|
Check out the [documentation](https://continuwuity.org) for installation instructions.
|
||||||
|
|
||||||
There are currently no open registration continuwuity instances available.
|
If you want to try it out as a user, we have some partnered homeservers you can use:
|
||||||
|
* You can head over to [https://federated.nexus](https://federated.nexus/) in your browser.
|
||||||
|
* Hit the `Apply to Join` button. Once your request has been accepted, you will receive an email with your username and password.
|
||||||
|
* Head over to [https://app.federated.nexus](https://app.federated.nexus/) and you can sign in there, or use any other matrix chat client you wish elsewhere.
|
||||||
|
* Your username for matrix will be in the form of `@username:federated.nexus`, however you can simply use the `username` part to log in. Your password is your password.
|
||||||
|
|
||||||
|
* There's also [https://continuwuity.rocks/](https://continuwuity.rocks/). You can register a new account using Cinny via [this convenient link](https://app.cinny.in/register/continuwuity.rocks), or you can use Element or another matrix client *that supports registration*.
|
||||||
|
|
||||||
## What are we working on?
|
## What are we working on?
|
||||||
|
|
||||||
|
|||||||
@@ -36,3 +36,7 @@ Deletes all the local media from a local user on our server. This will always ig
|
|||||||
## `!admin media delete-all-from-server`
|
## `!admin media delete-all-from-server`
|
||||||
|
|
||||||
Deletes all remote media from the specified remote server. This will always ignore errors by default
|
Deletes all remote media from the specified remote server. This will always ignore errors by default
|
||||||
|
|
||||||
|
## `!admin media delete-url-preview`
|
||||||
|
|
||||||
|
Deletes a cached URL preview, forcing it to be re-fetched. Use --all to purge all cached URL previews
|
||||||
|
|||||||
@@ -77,7 +77,12 @@ rec {
|
|||||||
craneLib.buildDepsOnly (
|
craneLib.buildDepsOnly (
|
||||||
(commonAttrs commonAttrsArgs)
|
(commonAttrs commonAttrsArgs)
|
||||||
// {
|
// {
|
||||||
env = uwuenv.buildDepsOnlyEnv // (makeRocksDBEnv { inherit rocksdb; });
|
env = uwuenv.buildDepsOnlyEnv
|
||||||
|
// (makeRocksDBEnv { inherit rocksdb; })
|
||||||
|
// {
|
||||||
|
# required since we started using unstable reqwest apparently ... otherwise the all-features build will fail
|
||||||
|
RUSTFLAGS = "--cfg reqwest_unstable";
|
||||||
|
};
|
||||||
inherit (features) cargoExtraArgs;
|
inherit (features) cargoExtraArgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +107,13 @@ rec {
|
|||||||
'';
|
'';
|
||||||
cargoArtifacts = deps;
|
cargoArtifacts = deps;
|
||||||
doCheck = true;
|
doCheck = true;
|
||||||
env = uwuenv.buildPackageEnv // rocksdbEnv;
|
env =
|
||||||
|
uwuenv.buildPackageEnv
|
||||||
|
// rocksdbEnv
|
||||||
|
// {
|
||||||
|
# required since we started using unstable reqwest apparently ... otherwise the all-features build will fail
|
||||||
|
RUSTFLAGS = "--cfg reqwest_unstable";
|
||||||
|
};
|
||||||
passthru.env = uwuenv.buildPackageEnv // rocksdbEnv;
|
passthru.env = uwuenv.buildPackageEnv // rocksdbEnv;
|
||||||
meta.mainProgram = crateInfo.pname;
|
meta.mainProgram = crateInfo.pname;
|
||||||
inherit (features) cargoExtraArgs;
|
inherit (features) cargoExtraArgs;
|
||||||
|
|||||||
@@ -30,12 +30,15 @@ pub(super) async fn incoming_federation(&self) -> Result {
|
|||||||
.federation_handletime
|
.federation_handletime
|
||||||
.read();
|
.read();
|
||||||
|
|
||||||
let mut msg = format!("Handling {} incoming pdus:\n", map.len());
|
let mut msg = format!(
|
||||||
|
"Handling {} incoming PDUs across {} active transactions:\n",
|
||||||
|
map.len(),
|
||||||
|
self.services.transactions.txn_active_handle_count()
|
||||||
|
);
|
||||||
for (r, (e, i)) in map.iter() {
|
for (r, (e, i)) in map.iter() {
|
||||||
let elapsed = i.elapsed();
|
let elapsed = i.elapsed();
|
||||||
writeln!(msg, "{} {}: {}m{}s", r, e, elapsed.as_secs() / 60, elapsed.as_secs() % 60)?;
|
writeln!(msg, "{} {}: {}m{}s", r, e, elapsed.as_secs() / 60, elapsed.as_secs() % 60)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
msg
|
msg
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ pub(super) async fn delete(
|
|||||||
.delete(&mxc.as_str().try_into()?)
|
.delete(&mxc.as_str().try_into()?)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
return Err!("Deleted the MXC from our database and on our filesystem.",);
|
return self
|
||||||
|
.write_str("Deleted the MXC from our database and on our filesystem.")
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(event_id) = event_id {
|
if let Some(event_id) = event_id {
|
||||||
@@ -388,3 +390,19 @@ pub(super) async fn get_remote_thumbnail(
|
|||||||
self.write_str(&format!("```\n{result:#?}\nreceived {len} bytes for file content.\n```"))
|
self.write_str(&format!("```\n{result:#?}\nreceived {len} bytes for file content.\n```"))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
pub(super) async fn delete_url_preview(&self, url: Option<String>, all: bool) -> Result {
|
||||||
|
if all {
|
||||||
|
self.services.media.clear_url_previews().await;
|
||||||
|
|
||||||
|
return self.write_str("Deleted all cached URL previews.").await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = url.expect("clap enforces url is required unless --all");
|
||||||
|
|
||||||
|
self.services.media.remove_url_preview(&url).await?;
|
||||||
|
|
||||||
|
self.write_str(&format!("Deleted cached URL preview for: {url}"))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|||||||
@@ -108,4 +108,16 @@ pub enum MediaCommand {
|
|||||||
#[arg(long, default_value("800"))]
|
#[arg(long, default_value("800"))]
|
||||||
height: u32,
|
height: u32,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Deletes a cached URL preview, forcing it to be re-fetched.
|
||||||
|
/// Use --all to purge all cached URL previews.
|
||||||
|
DeleteUrlPreview {
|
||||||
|
/// The URL to clear from the saved preview data
|
||||||
|
#[arg(required_unless_present = "all")]
|
||||||
|
url: Option<String>,
|
||||||
|
|
||||||
|
/// Purge all cached URL previews
|
||||||
|
#[arg(long, conflicts_with = "url")]
|
||||||
|
all: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ pub(super) async fn compact(
|
|||||||
let parallelism = parallelism.unwrap_or(1);
|
let parallelism = parallelism.unwrap_or(1);
|
||||||
let results = maps
|
let results = maps
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.try_stream()
|
.try_stream::<conduwuit::Error>()
|
||||||
.paralleln_and_then(runtime, parallelism, move |map| {
|
.paralleln_and_then(runtime, parallelism, move |map| {
|
||||||
map.compact_blocking(options.clone())?;
|
map.compact_blocking(options.clone())?;
|
||||||
Ok(map.name().to_owned())
|
Ok(map.name().to_owned())
|
||||||
|
|||||||
@@ -20,7 +20,17 @@ pub enum ResolverCommand {
|
|||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Flush a specific server from the resolver caches or everything
|
/// Flush a given server from the resolver caches or flush them completely
|
||||||
|
///
|
||||||
|
/// * Examples:
|
||||||
|
/// * Flush a specific server:
|
||||||
|
///
|
||||||
|
/// `!admin query resolver flush-cache matrix.example.com`
|
||||||
|
///
|
||||||
|
/// * Flush all resolver caches completely:
|
||||||
|
///
|
||||||
|
/// `!admin query resolver flush-cache --all`
|
||||||
|
#[command(verbatim_doc_comment)]
|
||||||
FlushCache {
|
FlushCache {
|
||||||
name: Option<OwnedServerName>,
|
name: Option<OwnedServerName>,
|
||||||
|
|
||||||
|
|||||||
@@ -252,6 +252,13 @@ pub(crate) async fn register_route(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't allow registration with user IDs that aren't local
|
||||||
|
if !services.globals.user_is_local(&user_id) {
|
||||||
|
return Err!(Request(InvalidUsername(
|
||||||
|
"Username {body_username} is not local to this server"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
user_id
|
user_id
|
||||||
},
|
},
|
||||||
| Err(e) => {
|
| Err(e) => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use ruma::{
|
|||||||
},
|
},
|
||||||
events::{
|
events::{
|
||||||
AnyGlobalAccountDataEventContent, AnyRoomAccountDataEventContent,
|
AnyGlobalAccountDataEventContent, AnyRoomAccountDataEventContent,
|
||||||
GlobalAccountDataEventType, RoomAccountDataEventType,
|
RoomAccountDataEventType,
|
||||||
},
|
},
|
||||||
serde::Raw,
|
serde::Raw,
|
||||||
};
|
};
|
||||||
@@ -126,12 +126,6 @@ async fn set_account_data(
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if event_type_s == GlobalAccountDataEventType::PushRules.to_cow_str() {
|
|
||||||
return Err!(Request(BadJson(
|
|
||||||
"This endpoint cannot be used for setting/configuring push rules."
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let data: serde_json::Value = serde_json::from_str(data.get())
|
let data: serde_json::Value = serde_json::from_str(data.get())
|
||||||
.map_err(|e| err!(Request(BadJson(warn!("Invalid JSON provided: {e}")))))?;
|
.map_err(|e| err!(Request(BadJson(warn!("Invalid JSON provided: {e}")))))?;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
use axum::extract::State;
|
||||||
|
use axum_client_ip::InsecureClientIp;
|
||||||
|
use conduwuit::{Err, Result, at};
|
||||||
|
use futures::StreamExt;
|
||||||
|
use ruma::api::client::dehydrated_device::{
|
||||||
|
delete_dehydrated_device::unstable as delete_dehydrated_device,
|
||||||
|
get_dehydrated_device::unstable as get_dehydrated_device, get_events::unstable as get_events,
|
||||||
|
put_dehydrated_device::unstable as put_dehydrated_device,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::Ruma;
|
||||||
|
|
||||||
|
const MAX_BATCH_EVENTS: usize = 50;
|
||||||
|
|
||||||
|
/// # `PUT /_matrix/client/../dehydrated_device`
|
||||||
|
///
|
||||||
|
/// Creates or overwrites the user's dehydrated device.
|
||||||
|
#[tracing::instrument(skip_all, fields(%client))]
|
||||||
|
pub(crate) async fn put_dehydrated_device_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
InsecureClientIp(client): InsecureClientIp,
|
||||||
|
body: Ruma<put_dehydrated_device::Request>,
|
||||||
|
) -> Result<put_dehydrated_device::Response> {
|
||||||
|
let sender_user = body
|
||||||
|
.sender_user
|
||||||
|
.as_deref()
|
||||||
|
.expect("AccessToken authentication required");
|
||||||
|
|
||||||
|
let device_id = body.body.device_id.clone();
|
||||||
|
|
||||||
|
services
|
||||||
|
.users
|
||||||
|
.set_dehydrated_device(sender_user, body.body)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(put_dehydrated_device::Response { device_id })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `DELETE /_matrix/client/../dehydrated_device`
|
||||||
|
///
|
||||||
|
/// Deletes the user's dehydrated device without replacement.
|
||||||
|
#[tracing::instrument(skip_all, fields(%client))]
|
||||||
|
pub(crate) async fn delete_dehydrated_device_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
InsecureClientIp(client): InsecureClientIp,
|
||||||
|
body: Ruma<delete_dehydrated_device::Request>,
|
||||||
|
) -> Result<delete_dehydrated_device::Response> {
|
||||||
|
let sender_user = body.sender_user();
|
||||||
|
|
||||||
|
let device_id = services.users.get_dehydrated_device_id(sender_user).await?;
|
||||||
|
|
||||||
|
services.users.remove_device(sender_user, &device_id).await;
|
||||||
|
|
||||||
|
Ok(delete_dehydrated_device::Response { device_id })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `GET /_matrix/client/../dehydrated_device`
|
||||||
|
///
|
||||||
|
/// Gets the user's dehydrated device
|
||||||
|
#[tracing::instrument(skip_all, fields(%client))]
|
||||||
|
pub(crate) async fn get_dehydrated_device_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
InsecureClientIp(client): InsecureClientIp,
|
||||||
|
body: Ruma<get_dehydrated_device::Request>,
|
||||||
|
) -> Result<get_dehydrated_device::Response> {
|
||||||
|
let sender_user = body.sender_user();
|
||||||
|
|
||||||
|
let device = services.users.get_dehydrated_device(sender_user).await?;
|
||||||
|
|
||||||
|
Ok(get_dehydrated_device::Response {
|
||||||
|
device_id: device.device_id,
|
||||||
|
device_data: device.device_data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `GET /_matrix/client/../dehydrated_device/{device_id}/events`
|
||||||
|
///
|
||||||
|
/// Paginates the events of the dehydrated device.
|
||||||
|
#[tracing::instrument(skip_all, fields(%client))]
|
||||||
|
pub(crate) async fn get_dehydrated_events_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
InsecureClientIp(client): InsecureClientIp,
|
||||||
|
body: Ruma<get_events::Request>,
|
||||||
|
) -> Result<get_events::Response> {
|
||||||
|
let sender_user = body.sender_user();
|
||||||
|
|
||||||
|
let device_id = &body.body.device_id;
|
||||||
|
let existing_id = services.users.get_dehydrated_device_id(sender_user).await;
|
||||||
|
|
||||||
|
if existing_id.as_ref().is_err()
|
||||||
|
|| existing_id
|
||||||
|
.as_ref()
|
||||||
|
.is_ok_and(|existing_id| existing_id != device_id)
|
||||||
|
{
|
||||||
|
return Err!(Request(Forbidden("Not the dehydrated device_id.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let since: Option<u64> = body
|
||||||
|
.body
|
||||||
|
.next_batch
|
||||||
|
.as_deref()
|
||||||
|
.map(str::parse)
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let mut next_batch: Option<u64> = None;
|
||||||
|
let events = services
|
||||||
|
.users
|
||||||
|
.get_to_device_events(sender_user, device_id, since, None)
|
||||||
|
.take(MAX_BATCH_EVENTS)
|
||||||
|
.inspect(|&(count, _)| {
|
||||||
|
next_batch.replace(count);
|
||||||
|
})
|
||||||
|
.map(at!(1))
|
||||||
|
.collect()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(get_events::Response {
|
||||||
|
events,
|
||||||
|
next_batch: next_batch.as_ref().map(ToString::to_string),
|
||||||
|
})
|
||||||
|
}
|
||||||
+25
-3
@@ -6,6 +6,7 @@ use conduwuit::{
|
|||||||
Err, Result, err,
|
Err, Result, err,
|
||||||
utils::{self, content_disposition::make_content_disposition, math::ruma_from_usize},
|
utils::{self, content_disposition::make_content_disposition, math::ruma_from_usize},
|
||||||
};
|
};
|
||||||
|
use conduwuit_core::error;
|
||||||
use conduwuit_service::{
|
use conduwuit_service::{
|
||||||
Services,
|
Services,
|
||||||
media::{CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN, Dim, FileMeta, MXC_LENGTH},
|
media::{CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN, Dim, FileMeta, MXC_LENGTH},
|
||||||
@@ -144,12 +145,22 @@ pub(crate) async fn get_content_route(
|
|||||||
server_name: &body.server_name,
|
server_name: &body.server_name,
|
||||||
media_id: &body.media_id,
|
media_id: &body.media_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
let FileMeta {
|
let FileMeta {
|
||||||
content,
|
content,
|
||||||
content_type,
|
content_type,
|
||||||
content_disposition,
|
content_disposition,
|
||||||
} = fetch_file(&services, &mxc, user, body.timeout_ms, None).await?;
|
} = match fetch_file(&services, &mxc, user, body.timeout_ms, None).await {
|
||||||
|
| Ok(meta) => meta,
|
||||||
|
| Err(conduwuit::Error::Io(e)) => match e.kind() {
|
||||||
|
| std::io::ErrorKind::NotFound => return Err!(Request(NotFound("Media not found."))),
|
||||||
|
| std::io::ErrorKind::PermissionDenied => {
|
||||||
|
error!("Permission denied when trying to read file: {e:?}");
|
||||||
|
return Err!(Request(Unknown("Unknown error when fetching file.")));
|
||||||
|
},
|
||||||
|
| _ => return Err!(Request(Unknown("Unknown error when fetching file."))),
|
||||||
|
},
|
||||||
|
| Err(_) => return Err!(Request(Unknown("Unknown error when fetching file."))),
|
||||||
|
};
|
||||||
|
|
||||||
Ok(get_content::v1::Response {
|
Ok(get_content::v1::Response {
|
||||||
file: content.expect("entire file contents"),
|
file: content.expect("entire file contents"),
|
||||||
@@ -185,7 +196,18 @@ pub(crate) async fn get_content_as_filename_route(
|
|||||||
content,
|
content,
|
||||||
content_type,
|
content_type,
|
||||||
content_disposition,
|
content_disposition,
|
||||||
} = fetch_file(&services, &mxc, user, body.timeout_ms, Some(&body.filename)).await?;
|
} = match fetch_file(&services, &mxc, user, body.timeout_ms, None).await {
|
||||||
|
| Ok(meta) => meta,
|
||||||
|
| Err(conduwuit::Error::Io(e)) => match e.kind() {
|
||||||
|
| std::io::ErrorKind::NotFound => return Err!(Request(NotFound("Media not found."))),
|
||||||
|
| std::io::ErrorKind::PermissionDenied => {
|
||||||
|
error!("Permission denied when trying to read file: {e:?}");
|
||||||
|
return Err!(Request(Unknown("Unknown error when fetching file.")));
|
||||||
|
},
|
||||||
|
| _ => return Err!(Request(Unknown("Unknown error when fetching file."))),
|
||||||
|
},
|
||||||
|
| Err(_) => return Err!(Request(Unknown("Unknown error when fetching file."))),
|
||||||
|
};
|
||||||
|
|
||||||
Ok(get_content_as_filename::v1::Response {
|
Ok(get_content_as_filename::v1::Response {
|
||||||
file: content.expect("entire file contents"),
|
file: content.expect("entire file contents"),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ pub(super) mod appservice;
|
|||||||
pub(super) mod backup;
|
pub(super) mod backup;
|
||||||
pub(super) mod capabilities;
|
pub(super) mod capabilities;
|
||||||
pub(super) mod context;
|
pub(super) mod context;
|
||||||
|
pub(super) mod dehydrated_device;
|
||||||
pub(super) mod device;
|
pub(super) mod device;
|
||||||
pub(super) mod directory;
|
pub(super) mod directory;
|
||||||
pub(super) mod filter;
|
pub(super) mod filter;
|
||||||
@@ -49,6 +50,7 @@ pub(super) use appservice::*;
|
|||||||
pub(super) use backup::*;
|
pub(super) use backup::*;
|
||||||
pub(super) use capabilities::*;
|
pub(super) use capabilities::*;
|
||||||
pub(super) use context::*;
|
pub(super) use context::*;
|
||||||
|
pub(super) use dehydrated_device::*;
|
||||||
pub(super) use device::*;
|
pub(super) use device::*;
|
||||||
pub(super) use directory::*;
|
pub(super) use directory::*;
|
||||||
pub(super) use filter::*;
|
pub(super) use filter::*;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use axum::extract::State;
|
|||||||
use axum_client_ip::InsecureClientIp;
|
use axum_client_ip::InsecureClientIp;
|
||||||
use conduwuit::{Err, Event, Result, debug_info, info, matrix::pdu::PduEvent, utils::ReadyExt};
|
use conduwuit::{Err, Event, Result, debug_info, info, matrix::pdu::PduEvent, utils::ReadyExt};
|
||||||
use conduwuit_service::Services;
|
use conduwuit_service::Services;
|
||||||
use rand::Rng;
|
|
||||||
use ruma::{
|
use ruma::{
|
||||||
EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
|
EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||||
api::client::{
|
api::client::{
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ pub(crate) async fn send_message_event_route(
|
|||||||
|
|
||||||
// Check if this is a new transaction id
|
// Check if this is a new transaction id
|
||||||
if let Ok(response) = services
|
if let Ok(response) = services
|
||||||
.transaction_ids
|
.transactions
|
||||||
.existing_txnid(sender_user, sender_device, &body.txn_id)
|
.get_client_txn(sender_user, sender_device, &body.txn_id)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
// The client might have sent a txnid of the /sendToDevice endpoint
|
// The client might have sent a txnid of the /sendToDevice endpoint
|
||||||
@@ -92,7 +92,7 @@ pub(crate) async fn send_message_event_route(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
services.transaction_ids.add_txnid(
|
services.transactions.add_client_txnid(
|
||||||
sender_user,
|
sender_user,
|
||||||
sender_device,
|
sender_device,
|
||||||
&body.txn_id,
|
&body.txn_id,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use std::{
|
|||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum_client_ip::InsecureClientIp;
|
use axum_client_ip::InsecureClientIp;
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Result, extract_variant,
|
Result, at, extract_variant,
|
||||||
utils::{
|
utils::{
|
||||||
ReadyExt, TryFutureExtExt,
|
ReadyExt, TryFutureExtExt,
|
||||||
stream::{BroadbandExt, Tools, WidebandExt},
|
stream::{BroadbandExt, Tools, WidebandExt},
|
||||||
@@ -385,6 +385,7 @@ pub(crate) async fn build_sync_events(
|
|||||||
last_sync_end_count,
|
last_sync_end_count,
|
||||||
Some(current_count),
|
Some(current_count),
|
||||||
)
|
)
|
||||||
|
.map(at!(1))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let device_one_time_keys_count = services
|
let device_one_time_keys_count = services
|
||||||
|
|||||||
@@ -336,7 +336,9 @@ where
|
|||||||
let ranges = list.ranges.clone();
|
let ranges = list.ranges.clone();
|
||||||
|
|
||||||
for mut range in ranges {
|
for mut range in ranges {
|
||||||
range.0 = uint!(0);
|
range.0 = range
|
||||||
|
.0
|
||||||
|
.min(UInt::try_from(active_rooms.len()).unwrap_or(UInt::MAX));
|
||||||
range.1 = range.1.checked_add(uint!(1)).unwrap_or(range.1);
|
range.1 = range.1.checked_add(uint!(1)).unwrap_or(range.1);
|
||||||
range.1 = range
|
range.1 = range
|
||||||
.1
|
.1
|
||||||
@@ -1027,6 +1029,7 @@ async fn collect_to_device(
|
|||||||
events: services
|
events: services
|
||||||
.users
|
.users
|
||||||
.get_to_device_events(sender_user, sender_device, None, Some(next_batch))
|
.get_to_device_events(sender_user, sender_device, None, Some(next_batch))
|
||||||
|
.map(at!(1))
|
||||||
.collect()
|
.collect()
|
||||||
.await,
|
.await,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ pub(crate) async fn send_event_to_device_route(
|
|||||||
|
|
||||||
// Check if this is a new transaction id
|
// Check if this is a new transaction id
|
||||||
if services
|
if services
|
||||||
.transaction_ids
|
.transactions
|
||||||
.existing_txnid(sender_user, sender_device, &body.txn_id)
|
.get_client_txn(sender_user, sender_device, &body.txn_id)
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
.is_ok()
|
||||||
{
|
{
|
||||||
@@ -104,8 +104,8 @@ pub(crate) async fn send_event_to_device_route(
|
|||||||
|
|
||||||
// Save transaction id with empty data
|
// Save transaction id with empty data
|
||||||
services
|
services
|
||||||
.transaction_ids
|
.transactions
|
||||||
.add_txnid(sender_user, sender_device, &body.txn_id, &[]);
|
.add_client_txnid(sender_user, sender_device, &body.txn_id, &[]);
|
||||||
|
|
||||||
Ok(send_event_to_device::v3::Response {})
|
Ok(send_event_to_device::v3::Response {})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ pub(crate) async fn get_supported_versions_route(
|
|||||||
("org.matrix.msc2836".to_owned(), true), /* threading/threads (https://github.com/matrix-org/matrix-spec-proposals/pull/2836) */
|
("org.matrix.msc2836".to_owned(), true), /* threading/threads (https://github.com/matrix-org/matrix-spec-proposals/pull/2836) */
|
||||||
("org.matrix.msc2946".to_owned(), true), /* spaces/hierarchy summaries (https://github.com/matrix-org/matrix-spec-proposals/pull/2946) */
|
("org.matrix.msc2946".to_owned(), true), /* spaces/hierarchy summaries (https://github.com/matrix-org/matrix-spec-proposals/pull/2946) */
|
||||||
("org.matrix.msc3026.busy_presence".to_owned(), true), /* busy presence status (https://github.com/matrix-org/matrix-spec-proposals/pull/3026) */
|
("org.matrix.msc3026.busy_presence".to_owned(), true), /* busy presence status (https://github.com/matrix-org/matrix-spec-proposals/pull/3026) */
|
||||||
|
("org.matrix.msc3814".to_owned(), true), /* dehydrated devices */
|
||||||
("org.matrix.msc3827".to_owned(), true), /* filtering of /publicRooms by room type (https://github.com/matrix-org/matrix-spec-proposals/pull/3827) */
|
("org.matrix.msc3827".to_owned(), true), /* filtering of /publicRooms by room type (https://github.com/matrix-org/matrix-spec-proposals/pull/3827) */
|
||||||
("org.matrix.msc3952_intentional_mentions".to_owned(), true), /* intentional mentions (https://github.com/matrix-org/matrix-spec-proposals/pull/3952) */
|
("org.matrix.msc3952_intentional_mentions".to_owned(), true), /* intentional mentions (https://github.com/matrix-org/matrix-spec-proposals/pull/3952) */
|
||||||
("org.matrix.msc3916.stable".to_owned(), true), /* authenticated media (https://github.com/matrix-org/matrix-spec-proposals/pull/3916) */
|
("org.matrix.msc3916.stable".to_owned(), true), /* authenticated media (https://github.com/matrix-org/matrix-spec-proposals/pull/3916) */
|
||||||
|
|||||||
@@ -27,10 +27,32 @@ pub(crate) async fn well_known_client(
|
|||||||
identity_server: None,
|
identity_server: None,
|
||||||
sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url }),
|
sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url }),
|
||||||
tile_server: None,
|
tile_server: None,
|
||||||
rtc_foci: services.config.well_known.rtc_focus_server_urls.clone(),
|
rtc_foci: services
|
||||||
|
.config
|
||||||
|
.matrix_rtc
|
||||||
|
.effective_foci(&services.config.well_known.rtc_focus_server_urls)
|
||||||
|
.to_vec(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// # `GET /_matrix/client/v1/rtc/transports`
|
||||||
|
/// # `GET /_matrix/client/unstable/org.matrix.msc4143/rtc/transports`
|
||||||
|
///
|
||||||
|
/// Returns the list of MatrixRTC foci (transports) configured for this
|
||||||
|
/// homeserver, implementing MSC4143.
|
||||||
|
pub(crate) async fn get_rtc_transports(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
_body: Ruma<ruma::api::client::discovery::get_rtc_transports::Request>,
|
||||||
|
) -> Result<ruma::api::client::discovery::get_rtc_transports::Response> {
|
||||||
|
Ok(ruma::api::client::discovery::get_rtc_transports::Response::new(
|
||||||
|
services
|
||||||
|
.config
|
||||||
|
.matrix_rtc
|
||||||
|
.effective_foci(&services.config.well_known.rtc_focus_server_urls)
|
||||||
|
.to_vec(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
/// # `GET /.well-known/matrix/support`
|
/// # `GET /.well-known/matrix/support`
|
||||||
///
|
///
|
||||||
/// Server support contact and support page of a homeserver's domain.
|
/// Server support contact and support page of a homeserver's domain.
|
||||||
|
|||||||
@@ -160,6 +160,10 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
|||||||
.ruma_route(&client::update_device_route)
|
.ruma_route(&client::update_device_route)
|
||||||
.ruma_route(&client::delete_device_route)
|
.ruma_route(&client::delete_device_route)
|
||||||
.ruma_route(&client::delete_devices_route)
|
.ruma_route(&client::delete_devices_route)
|
||||||
|
.ruma_route(&client::put_dehydrated_device_route)
|
||||||
|
.ruma_route(&client::delete_dehydrated_device_route)
|
||||||
|
.ruma_route(&client::get_dehydrated_device_route)
|
||||||
|
.ruma_route(&client::get_dehydrated_events_route)
|
||||||
.ruma_route(&client::get_tags_route)
|
.ruma_route(&client::get_tags_route)
|
||||||
.ruma_route(&client::update_tag_route)
|
.ruma_route(&client::update_tag_route)
|
||||||
.ruma_route(&client::delete_tag_route)
|
.ruma_route(&client::delete_tag_route)
|
||||||
@@ -184,6 +188,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
|||||||
.ruma_route(&client::put_suspended_status)
|
.ruma_route(&client::put_suspended_status)
|
||||||
.ruma_route(&client::well_known_support)
|
.ruma_route(&client::well_known_support)
|
||||||
.ruma_route(&client::well_known_client)
|
.ruma_route(&client::well_known_client)
|
||||||
|
.ruma_route(&client::get_rtc_transports)
|
||||||
.route("/_conduwuit/server_version", get(client::conduwuit_server_version))
|
.route("/_conduwuit/server_version", get(client::conduwuit_server_version))
|
||||||
.route("/_continuwuity/server_version", get(client::conduwuit_server_version))
|
.route("/_continuwuity/server_version", get(client::conduwuit_server_version))
|
||||||
.ruma_route(&client::room_initial_sync_route)
|
.ruma_route(&client::room_initial_sync_route)
|
||||||
|
|||||||
+37
-19
@@ -14,7 +14,8 @@ use futures::{
|
|||||||
pin_mut,
|
pin_mut,
|
||||||
};
|
};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
CanonicalJsonObject, CanonicalJsonValue, OwnedDeviceId, OwnedServerName, OwnedUserId, UserId,
|
CanonicalJsonObject, CanonicalJsonValue, DeviceId, OwnedDeviceId, OwnedServerName,
|
||||||
|
OwnedUserId, UserId,
|
||||||
api::{
|
api::{
|
||||||
AuthScheme, IncomingRequest, Metadata,
|
AuthScheme, IncomingRequest, Metadata,
|
||||||
client::{
|
client::{
|
||||||
@@ -66,23 +67,17 @@ pub(super) async fn auth(
|
|||||||
if metadata.authentication == AuthScheme::None {
|
if metadata.authentication == AuthScheme::None {
|
||||||
match metadata {
|
match metadata {
|
||||||
| &get_public_rooms::v3::Request::METADATA => {
|
| &get_public_rooms::v3::Request::METADATA => {
|
||||||
if !services
|
match token {
|
||||||
.server
|
| Token::Appservice(_) | Token::User(_) => {
|
||||||
.config
|
// we should have validated the token above
|
||||||
.allow_public_room_directory_without_auth
|
// already
|
||||||
{
|
},
|
||||||
match token {
|
| Token::None | Token::Invalid => {
|
||||||
| Token::Appservice(_) | Token::User(_) => {
|
return Err(Error::BadRequest(
|
||||||
// we should have validated the token above
|
ErrorKind::MissingToken,
|
||||||
// already
|
"Missing or invalid access token.",
|
||||||
},
|
));
|
||||||
| Token::None | Token::Invalid => {
|
},
|
||||||
return Err(Error::BadRequest(
|
|
||||||
ErrorKind::MissingToken,
|
|
||||||
"Missing or invalid access token.",
|
|
||||||
));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
| &get_profile::v3::Request::METADATA
|
| &get_profile::v3::Request::METADATA
|
||||||
@@ -234,10 +229,33 @@ async fn auth_appservice(
|
|||||||
return Err!(Request(Exclusive("User is not in namespace.")));
|
return Err!(Request(Exclusive("User is not in namespace.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MSC3202/MSC4190: Handle device_id masquerading for appservices.
|
||||||
|
// The device_id can be provided via `device_id` or
|
||||||
|
// `org.matrix.msc3202.device_id` query parameter.
|
||||||
|
let sender_device = if let Some(ref device_id_str) = request.query.device_id {
|
||||||
|
let device_id: &DeviceId = device_id_str.as_str().into();
|
||||||
|
|
||||||
|
// Verify the device exists for this user
|
||||||
|
if services
|
||||||
|
.users
|
||||||
|
.get_device_metadata(&user_id, device_id)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return Err!(Request(Forbidden(
|
||||||
|
"Device does not exist for user or appservice cannot masquerade as this device."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(device_id.to_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Auth {
|
Ok(Auth {
|
||||||
origin: None,
|
origin: None,
|
||||||
sender_user: Some(user_id),
|
sender_user: Some(user_id),
|
||||||
sender_device: None,
|
sender_device,
|
||||||
appservice_info: Some(*info),
|
appservice_info: Some(*info),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ use service::Services;
|
|||||||
pub(super) struct QueryParams {
|
pub(super) struct QueryParams {
|
||||||
pub(super) access_token: Option<String>,
|
pub(super) access_token: Option<String>,
|
||||||
pub(super) user_id: Option<String>,
|
pub(super) user_id: Option<String>,
|
||||||
|
/// Device ID for appservice device masquerading (MSC3202/MSC4190).
|
||||||
|
/// Can be provided as `device_id` or `org.matrix.msc3202.device_id`.
|
||||||
|
#[serde(alias = "org.matrix.msc3202.device_id")]
|
||||||
|
pub(super) device_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) struct Request {
|
pub(super) struct Request {
|
||||||
|
|||||||
+214
-64
@@ -1,27 +1,33 @@
|
|||||||
use std::{collections::BTreeMap, net::IpAddr, time::Instant};
|
use std::{
|
||||||
|
collections::{BTreeMap, HashMap, HashSet},
|
||||||
|
net::IpAddr,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum_client_ip::InsecureClientIp;
|
use axum_client_ip::InsecureClientIp;
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Err, Error, Result, debug, debug_warn, err, error,
|
Err, Error, Result, debug, debug_warn, err, error,
|
||||||
result::LogErr,
|
result::LogErr,
|
||||||
|
state_res::lexicographical_topological_sort,
|
||||||
trace,
|
trace,
|
||||||
utils::{
|
utils::{
|
||||||
IterStream, ReadyExt, millis_since_unix_epoch,
|
IterStream, ReadyExt, millis_since_unix_epoch,
|
||||||
stream::{BroadbandExt, TryBroadbandExt, automatic_width},
|
stream::{BroadbandExt, TryBroadbandExt, automatic_width},
|
||||||
},
|
},
|
||||||
warn,
|
|
||||||
};
|
};
|
||||||
use conduwuit_service::{
|
use conduwuit_service::{
|
||||||
Services,
|
Services,
|
||||||
sending::{EDU_LIMIT, PDU_LIMIT},
|
sending::{EDU_LIMIT, PDU_LIMIT},
|
||||||
};
|
};
|
||||||
use futures::{FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt};
|
use futures::{FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt};
|
||||||
|
use http::StatusCode;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
CanonicalJsonObject, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, ServerName, UserId,
|
CanonicalJsonObject, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId,
|
||||||
|
RoomId, ServerName, UserId,
|
||||||
api::{
|
api::{
|
||||||
client::error::ErrorKind,
|
client::error::{ErrorKind, ErrorKind::LimitExceeded},
|
||||||
federation::transactions::{
|
federation::transactions::{
|
||||||
edu::{
|
edu::{
|
||||||
DeviceListUpdateContent, DirectDeviceContent, Edu, PresenceContent,
|
DeviceListUpdateContent, DirectDeviceContent, Edu, PresenceContent,
|
||||||
@@ -32,9 +38,16 @@ use ruma::{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
events::receipt::{ReceiptEvent, ReceiptEventContent, ReceiptType},
|
events::receipt::{ReceiptEvent, ReceiptEventContent, ReceiptType},
|
||||||
|
int,
|
||||||
serde::Raw,
|
serde::Raw,
|
||||||
to_device::DeviceIdOrAllDevices,
|
to_device::DeviceIdOrAllDevices,
|
||||||
|
uint,
|
||||||
};
|
};
|
||||||
|
use service::transactions::{
|
||||||
|
FederationTxnState, TransactionError, TxnKey, WrappedTransactionResponse,
|
||||||
|
};
|
||||||
|
use tokio::sync::watch::{Receiver, Sender};
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
|
|
||||||
@@ -44,15 +57,6 @@ type Pdu = (OwnedRoomId, OwnedEventId, CanonicalJsonObject);
|
|||||||
/// # `PUT /_matrix/federation/v1/send/{txnId}`
|
/// # `PUT /_matrix/federation/v1/send/{txnId}`
|
||||||
///
|
///
|
||||||
/// Push EDUs and PDUs to this server.
|
/// Push EDUs and PDUs to this server.
|
||||||
#[tracing::instrument(
|
|
||||||
name = "txn",
|
|
||||||
level = "debug",
|
|
||||||
skip_all,
|
|
||||||
fields(
|
|
||||||
%client,
|
|
||||||
origin = body.origin().as_str()
|
|
||||||
),
|
|
||||||
)]
|
|
||||||
pub(crate) async fn send_transaction_message_route(
|
pub(crate) async fn send_transaction_message_route(
|
||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
InsecureClientIp(client): InsecureClientIp,
|
InsecureClientIp(client): InsecureClientIp,
|
||||||
@@ -76,16 +80,73 @@ pub(crate) async fn send_transaction_message_route(
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let txn_start_time = Instant::now();
|
let txn_key = (body.origin().to_owned(), body.transaction_id.clone());
|
||||||
trace!(
|
|
||||||
pdus = body.pdus.len(),
|
|
||||||
edus = body.edus.len(),
|
|
||||||
elapsed = ?txn_start_time.elapsed(),
|
|
||||||
id = %body.transaction_id,
|
|
||||||
origin = %body.origin(),
|
|
||||||
"Starting txn",
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// Atomically check cache, join active, or start new transaction
|
||||||
|
match services
|
||||||
|
.transactions
|
||||||
|
.get_or_start_federation_txn(txn_key.clone())?
|
||||||
|
{
|
||||||
|
| FederationTxnState::Cached(response) => {
|
||||||
|
// Already responded
|
||||||
|
Ok(response)
|
||||||
|
},
|
||||||
|
| FederationTxnState::Active(receiver) => {
|
||||||
|
// Another thread is processing
|
||||||
|
wait_for_result(receiver).await
|
||||||
|
},
|
||||||
|
| FederationTxnState::Started { receiver, sender } => {
|
||||||
|
// We're the first, spawn the processing task
|
||||||
|
services
|
||||||
|
.server
|
||||||
|
.runtime()
|
||||||
|
.spawn(process_inbound_transaction(services, body, client, txn_key, sender));
|
||||||
|
// and wait for it
|
||||||
|
wait_for_result(receiver).await
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_result(
|
||||||
|
mut recv: Receiver<WrappedTransactionResponse>,
|
||||||
|
) -> Result<send_transaction_message::v1::Response> {
|
||||||
|
if tokio::time::timeout(Duration::from_secs(50), recv.changed())
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
// Took too long, return 429 to encourage the sender to try again
|
||||||
|
return Err(Error::BadRequest(
|
||||||
|
LimitExceeded { retry_after: None },
|
||||||
|
"Transaction is being still being processed. Please try again later.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let value = recv.borrow_and_update();
|
||||||
|
match value.clone() {
|
||||||
|
| Some(Ok(response)) => Ok(response),
|
||||||
|
| Some(Err(err)) => Err(transaction_error_to_response(&err)),
|
||||||
|
| None => Err(Error::Request(
|
||||||
|
ErrorKind::Unknown,
|
||||||
|
"Transaction processing failed unexpectedly".into(),
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
id = ?body.transaction_id.as_str(),
|
||||||
|
origin = ?body.origin()
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn process_inbound_transaction(
|
||||||
|
services: crate::State,
|
||||||
|
body: Ruma<send_transaction_message::v1::Request>,
|
||||||
|
client: IpAddr,
|
||||||
|
txn_key: TxnKey,
|
||||||
|
sender: Sender<WrappedTransactionResponse>,
|
||||||
|
) {
|
||||||
|
let txn_start_time = Instant::now();
|
||||||
let pdus = body
|
let pdus = body
|
||||||
.pdus
|
.pdus
|
||||||
.iter()
|
.iter()
|
||||||
@@ -102,40 +163,79 @@ pub(crate) async fn send_transaction_message_route(
|
|||||||
.filter_map(Result::ok)
|
.filter_map(Result::ok)
|
||||||
.stream();
|
.stream();
|
||||||
|
|
||||||
let results = handle(&services, &client, body.origin(), txn_start_time, pdus, edus).await?;
|
debug!(pdus = body.pdus.len(), edus = body.edus.len(), "Processing transaction",);
|
||||||
|
let results = match handle(&services, &client, body.origin(), pdus, edus).await {
|
||||||
|
| Ok(results) => results,
|
||||||
|
| Err(err) => {
|
||||||
|
fail_federation_txn(services, &txn_key, &sender, err);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (id, result) in &results {
|
||||||
|
if let Err(e) = result {
|
||||||
|
if matches!(e, Error::BadRequest(ErrorKind::NotFound, _)) {
|
||||||
|
debug_warn!("Incoming PDU failed {id}: {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
pdus = body.pdus.len(),
|
pdus = body.pdus.len(),
|
||||||
edus = body.edus.len(),
|
edus = body.edus.len(),
|
||||||
elapsed = ?txn_start_time.elapsed(),
|
elapsed = ?txn_start_time.elapsed(),
|
||||||
id = %body.transaction_id,
|
"Finished processing transaction"
|
||||||
origin = %body.origin(),
|
|
||||||
"Finished txn",
|
|
||||||
);
|
);
|
||||||
for (id, result) in &results {
|
|
||||||
if let Err(e) = result {
|
|
||||||
if matches!(e, Error::BadRequest(ErrorKind::NotFound, _)) {
|
|
||||||
warn!("Incoming PDU failed {id}: {e:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(send_transaction_message::v1::Response {
|
let response = send_transaction_message::v1::Response {
|
||||||
pdus: results
|
pdus: results
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(e, r)| (e, r.map_err(error::sanitized_message)))
|
.map(|(e, r)| (e, r.map_err(error::sanitized_message)))
|
||||||
.collect(),
|
.collect(),
|
||||||
})
|
};
|
||||||
|
|
||||||
|
services
|
||||||
|
.transactions
|
||||||
|
.finish_federation_txn(txn_key, sender, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles a failed federation transaction by sending the error through
|
||||||
|
/// the channel and cleaning up the transaction state. This allows waiters to
|
||||||
|
/// receive an appropriate error response.
|
||||||
|
fn fail_federation_txn(
|
||||||
|
services: crate::State,
|
||||||
|
txn_key: &TxnKey,
|
||||||
|
sender: &Sender<WrappedTransactionResponse>,
|
||||||
|
err: TransactionError,
|
||||||
|
) {
|
||||||
|
debug!("Transaction failed: {err}");
|
||||||
|
|
||||||
|
// Remove from active state so the transaction can be retried
|
||||||
|
services.transactions.remove_federation_txn(txn_key);
|
||||||
|
|
||||||
|
// Send the error to any waiters
|
||||||
|
if let Err(e) = sender.send(Some(Err(err))) {
|
||||||
|
debug_warn!("Failed to send transaction error to receivers: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a TransactionError into an appropriate HTTP error response.
|
||||||
|
fn transaction_error_to_response(err: &TransactionError) -> Error {
|
||||||
|
match err {
|
||||||
|
| TransactionError::ShuttingDown => Error::Request(
|
||||||
|
ErrorKind::Unknown,
|
||||||
|
"Server is shutting down, please retry later".into(),
|
||||||
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
async fn handle(
|
async fn handle(
|
||||||
services: &Services,
|
services: &Services,
|
||||||
client: &IpAddr,
|
client: &IpAddr,
|
||||||
origin: &ServerName,
|
origin: &ServerName,
|
||||||
started: Instant,
|
|
||||||
pdus: impl Stream<Item = Pdu> + Send,
|
pdus: impl Stream<Item = Pdu> + Send,
|
||||||
edus: impl Stream<Item = Edu> + Send,
|
edus: impl Stream<Item = Edu> + Send,
|
||||||
) -> Result<ResolvedMap> {
|
) -> std::result::Result<ResolvedMap, TransactionError> {
|
||||||
// group pdus by room
|
// group pdus by room
|
||||||
let pdus = pdus
|
let pdus = pdus
|
||||||
.collect()
|
.collect()
|
||||||
@@ -152,7 +252,7 @@ async fn handle(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.try_stream()
|
.try_stream()
|
||||||
.broad_and_then(|(room_id, pdus): (_, Vec<_>)| {
|
.broad_and_then(|(room_id, pdus): (_, Vec<_>)| {
|
||||||
handle_room(services, client, origin, started, room_id, pdus.into_iter())
|
handle_room(services, client, origin, room_id, pdus.into_iter())
|
||||||
.map_ok(Vec::into_iter)
|
.map_ok(Vec::into_iter)
|
||||||
.map_ok(IterStream::try_stream)
|
.map_ok(IterStream::try_stream)
|
||||||
})
|
})
|
||||||
@@ -169,14 +269,51 @@ async fn handle(
|
|||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attempts to build a localised directed acyclic graph out of the given PDUs,
|
||||||
|
/// returning them in a topologically sorted order.
|
||||||
|
///
|
||||||
|
/// This is used to attempt to process PDUs in an order that respects their
|
||||||
|
/// dependencies, however it is ultimately the sender's responsibility to send
|
||||||
|
/// them in a processable order, so this is just a best effort attempt. It does
|
||||||
|
/// not account for power levels or other tie breaks.
|
||||||
|
async fn build_local_dag(
|
||||||
|
pdu_map: &HashMap<OwnedEventId, CanonicalJsonObject>,
|
||||||
|
) -> Result<Vec<OwnedEventId>> {
|
||||||
|
debug_assert!(pdu_map.len() >= 2, "needless call to build_local_dag with less than 2 PDUs");
|
||||||
|
let mut dag: HashMap<OwnedEventId, HashSet<OwnedEventId>> = HashMap::new();
|
||||||
|
|
||||||
|
for (event_id, value) in pdu_map {
|
||||||
|
let prev_events = value
|
||||||
|
.get("prev_events")
|
||||||
|
.expect("pdu must have prev_events")
|
||||||
|
.as_array()
|
||||||
|
.expect("prev_events must be an array")
|
||||||
|
.iter()
|
||||||
|
.map(|v| {
|
||||||
|
OwnedEventId::parse(v.as_str().expect("prev_events values must be strings"))
|
||||||
|
.expect("prev_events must be valid event IDs")
|
||||||
|
})
|
||||||
|
.collect::<HashSet<OwnedEventId>>();
|
||||||
|
|
||||||
|
dag.insert(event_id.clone(), prev_events);
|
||||||
|
}
|
||||||
|
lexicographical_topological_sort(&dag, &|_| async {
|
||||||
|
// Note: we don't bother fetching power levels because that would massively slow
|
||||||
|
// this function down. This is a best-effort attempt to order events correctly
|
||||||
|
// for processing, however ultimately that should be the sender's job.
|
||||||
|
Ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0))))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| err!("failed to resolve local graph: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_room(
|
async fn handle_room(
|
||||||
services: &Services,
|
services: &Services,
|
||||||
_client: &IpAddr,
|
_client: &IpAddr,
|
||||||
origin: &ServerName,
|
origin: &ServerName,
|
||||||
txn_start_time: Instant,
|
|
||||||
room_id: OwnedRoomId,
|
room_id: OwnedRoomId,
|
||||||
pdus: impl Iterator<Item = Pdu> + Send,
|
pdus: impl Iterator<Item = Pdu> + Send,
|
||||||
) -> Result<Vec<(OwnedEventId, Result)>> {
|
) -> std::result::Result<Vec<(OwnedEventId, Result)>, TransactionError> {
|
||||||
let _room_lock = services
|
let _room_lock = services
|
||||||
.rooms
|
.rooms
|
||||||
.event_handler
|
.event_handler
|
||||||
@@ -185,27 +322,40 @@ async fn handle_room(
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
let room_id = &room_id;
|
let room_id = &room_id;
|
||||||
pdus.try_stream()
|
let pdu_map: HashMap<OwnedEventId, CanonicalJsonObject> = pdus
|
||||||
.and_then(|(_, event_id, value)| async move {
|
.into_iter()
|
||||||
services.server.check_running()?;
|
.map(|(_, event_id, value)| (event_id, value))
|
||||||
let pdu_start_time = Instant::now();
|
.collect();
|
||||||
let result = services
|
// Try to sort PDUs by their dependencies, but fall back to arbitrary order on
|
||||||
.rooms
|
// failure (e.g., cycles). This is best-effort; proper ordering is the sender's
|
||||||
.event_handler
|
// responsibility.
|
||||||
.handle_incoming_pdu(origin, room_id, &event_id, value, true)
|
let sorted_event_ids = if pdu_map.len() >= 2 {
|
||||||
.await
|
build_local_dag(&pdu_map).await.unwrap_or_else(|e| {
|
||||||
.map(|_| ());
|
debug_warn!("Failed to build local DAG for room {room_id}: {e}");
|
||||||
|
pdu_map.keys().cloned().collect()
|
||||||
debug!(
|
|
||||||
pdu_elapsed = ?pdu_start_time.elapsed(),
|
|
||||||
txn_elapsed = ?txn_start_time.elapsed(),
|
|
||||||
"Finished PDU {event_id}",
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok((event_id, result))
|
|
||||||
})
|
})
|
||||||
.try_collect()
|
} else {
|
||||||
.await
|
pdu_map.keys().cloned().collect()
|
||||||
|
};
|
||||||
|
let mut results = Vec::with_capacity(sorted_event_ids.len());
|
||||||
|
for event_id in sorted_event_ids {
|
||||||
|
let value = pdu_map
|
||||||
|
.get(&event_id)
|
||||||
|
.expect("sorted event IDs must be from the original map")
|
||||||
|
.clone();
|
||||||
|
services
|
||||||
|
.server
|
||||||
|
.check_running()
|
||||||
|
.map_err(|_| TransactionError::ShuttingDown)?;
|
||||||
|
let result = services
|
||||||
|
.rooms
|
||||||
|
.event_handler
|
||||||
|
.handle_incoming_pdu(origin, room_id, &event_id, value, true)
|
||||||
|
.await
|
||||||
|
.map(|_| ());
|
||||||
|
results.push((event_id, result));
|
||||||
|
}
|
||||||
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_edu(services: &Services, client: &IpAddr, origin: &ServerName, edu: Edu) {
|
async fn handle_edu(services: &Services, client: &IpAddr, origin: &ServerName, edu: Edu) {
|
||||||
@@ -478,8 +628,8 @@ async fn handle_edu_direct_to_device(
|
|||||||
|
|
||||||
// Check if this is a new transaction id
|
// Check if this is a new transaction id
|
||||||
if services
|
if services
|
||||||
.transaction_ids
|
.transactions
|
||||||
.existing_txnid(sender, None, message_id)
|
.get_client_txn(sender, None, message_id)
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
.is_ok()
|
||||||
{
|
{
|
||||||
@@ -498,8 +648,8 @@ async fn handle_edu_direct_to_device(
|
|||||||
|
|
||||||
// Save transaction id with empty data
|
// Save transaction id with empty data
|
||||||
services
|
services
|
||||||
.transaction_ids
|
.transactions
|
||||||
.add_txnid(sender, None, message_id, &[]);
|
.add_client_txnid(sender, None, message_id, &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_edu_direct_to_device_user<Event: Send + Sync>(
|
async fn handle_edu_direct_to_device_user<Event: Send + Sync>(
|
||||||
|
|||||||
+83
-18
@@ -368,6 +368,31 @@ pub struct Config {
|
|||||||
#[serde(default = "default_max_fetch_prev_events")]
|
#[serde(default = "default_max_fetch_prev_events")]
|
||||||
pub max_fetch_prev_events: u16,
|
pub max_fetch_prev_events: u16,
|
||||||
|
|
||||||
|
/// How many incoming federation transactions the server is willing to be
|
||||||
|
/// processing at any given time before it becomes overloaded and starts
|
||||||
|
/// rejecting further transactions until some slots become available.
|
||||||
|
///
|
||||||
|
/// Setting this value too low or too high may result in unstable
|
||||||
|
/// federation, and setting it too high may cause runaway resource usage.
|
||||||
|
///
|
||||||
|
/// default: 150
|
||||||
|
#[serde(default = "default_max_concurrent_inbound_transactions")]
|
||||||
|
pub max_concurrent_inbound_transactions: usize,
|
||||||
|
|
||||||
|
/// Maximum age (in seconds) for cached federation transaction responses.
|
||||||
|
/// Entries older than this will be removed during cleanup.
|
||||||
|
///
|
||||||
|
/// default: 7200 (2 hours)
|
||||||
|
#[serde(default = "default_transaction_id_cache_max_age_secs")]
|
||||||
|
pub transaction_id_cache_max_age_secs: u64,
|
||||||
|
|
||||||
|
/// Maximum number of cached federation transaction responses.
|
||||||
|
/// When the cache exceeds this limit, older entries will be removed.
|
||||||
|
///
|
||||||
|
/// default: 8192
|
||||||
|
#[serde(default = "default_transaction_id_cache_max_entries")]
|
||||||
|
pub transaction_id_cache_max_entries: usize,
|
||||||
|
|
||||||
/// Default/base connection timeout (seconds). This is used only by URL
|
/// Default/base connection timeout (seconds). This is used only by URL
|
||||||
/// previews and update/news endpoint checks.
|
/// previews and update/news endpoint checks.
|
||||||
///
|
///
|
||||||
@@ -653,12 +678,6 @@ pub struct Config {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub allow_public_room_directory_over_federation: bool,
|
pub allow_public_room_directory_over_federation: bool,
|
||||||
|
|
||||||
/// Set this to true to allow your server's public room directory to be
|
|
||||||
/// queried without client authentication (access token) through the Client
|
|
||||||
/// APIs. Set this to false to protect against /publicRooms spiders.
|
|
||||||
#[serde(default)]
|
|
||||||
pub allow_public_room_directory_without_auth: bool,
|
|
||||||
|
|
||||||
/// Allow guests/unauthenticated users to access TURN credentials.
|
/// Allow guests/unauthenticated users to access TURN credentials.
|
||||||
///
|
///
|
||||||
/// This is the equivalent of Synapse's `turn_allow_guests` config option.
|
/// This is the equivalent of Synapse's `turn_allow_guests` config option.
|
||||||
@@ -1525,7 +1544,7 @@ pub struct Config {
|
|||||||
/// sender user's server name, inbound federation X-Matrix origin, and
|
/// sender user's server name, inbound federation X-Matrix origin, and
|
||||||
/// outbound federation handler.
|
/// outbound federation handler.
|
||||||
///
|
///
|
||||||
/// You can set this to ["*"] to block all servers by default, and then
|
/// You can set this to [".*"] to block all servers by default, and then
|
||||||
/// use `allowed_remote_server_names` to allow only specific servers.
|
/// use `allowed_remote_server_names` to allow only specific servers.
|
||||||
///
|
///
|
||||||
/// example: ["badserver\\.tld$", "badphrase", "19dollarfortnitecards"]
|
/// example: ["badserver\\.tld$", "badphrase", "19dollarfortnitecards"]
|
||||||
@@ -2061,6 +2080,12 @@ pub struct Config {
|
|||||||
/// display: nested
|
/// display: nested
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub blurhashing: BlurhashConfig,
|
pub blurhashing: BlurhashConfig,
|
||||||
|
|
||||||
|
/// Configuration for MatrixRTC (MSC4143) transport discovery.
|
||||||
|
/// display: nested
|
||||||
|
#[serde(default)]
|
||||||
|
pub matrix_rtc: MatrixRtcConfig,
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
#[allow(clippy::zero_sized_map_values)]
|
#[allow(clippy::zero_sized_map_values)]
|
||||||
// this is a catchall, the map shouldn't be zero at runtime
|
// this is a catchall, the map shouldn't be zero at runtime
|
||||||
@@ -2126,17 +2151,16 @@ pub struct WellKnownConfig {
|
|||||||
/// listed.
|
/// listed.
|
||||||
pub support_mxid: Option<OwnedUserId>,
|
pub support_mxid: Option<OwnedUserId>,
|
||||||
|
|
||||||
/// A list of MatrixRTC foci URLs which will be served as part of the
|
/// **DEPRECATED**: Use `[global.matrix_rtc].foci` instead.
|
||||||
/// MSC4143 client endpoint at /.well-known/matrix/client. If you're
|
|
||||||
/// setting up livekit, you'd want something like:
|
|
||||||
/// rtc_focus_server_urls = [
|
|
||||||
/// { type = "livekit", livekit_service_url = "https://livekit.example.com" },
|
|
||||||
/// ]
|
|
||||||
///
|
///
|
||||||
/// To disable, set this to be an empty vector (`[]`).
|
/// A list of MatrixRTC foci URLs which will be served as part of the
|
||||||
|
/// MSC4143 client endpoint at /.well-known/matrix/client.
|
||||||
|
///
|
||||||
|
/// This option is deprecated and will be removed in a future release.
|
||||||
|
/// Please migrate to the new `[global.matrix_rtc]` config section.
|
||||||
///
|
///
|
||||||
/// default: []
|
/// default: []
|
||||||
#[serde(default = "default_rtc_focus_urls")]
|
#[serde(default)]
|
||||||
pub rtc_focus_server_urls: Vec<RtcFocusInfo>,
|
pub rtc_focus_server_urls: Vec<RtcFocusInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2165,6 +2189,43 @@ pub struct BlurhashConfig {
|
|||||||
pub blurhash_max_raw_size: u64,
|
pub blurhash_max_raw_size: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Default)]
|
||||||
|
#[config_example_generator(filename = "conduwuit-example.toml", section = "global.matrix_rtc")]
|
||||||
|
pub struct MatrixRtcConfig {
|
||||||
|
/// A list of MatrixRTC foci (transports) which will be served via the
|
||||||
|
/// MSC4143 RTC transports endpoint at
|
||||||
|
/// `/_matrix/client/v1/rtc/transports`. If you're setting up livekit,
|
||||||
|
/// you'd want something like:
|
||||||
|
/// ```toml
|
||||||
|
/// [global.matrix_rtc]
|
||||||
|
/// foci = [
|
||||||
|
/// { type = "livekit", livekit_service_url = "https://livekit.example.com" },
|
||||||
|
/// ]
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// To disable, set this to an empty list (`[]`).
|
||||||
|
///
|
||||||
|
/// default: []
|
||||||
|
#[serde(default)]
|
||||||
|
pub foci: Vec<RtcFocusInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MatrixRtcConfig {
|
||||||
|
/// Returns the effective foci, falling back to the deprecated
|
||||||
|
/// `rtc_focus_server_urls` if the new config is empty.
|
||||||
|
#[must_use]
|
||||||
|
pub fn effective_foci<'a>(
|
||||||
|
&'a self,
|
||||||
|
deprecated_foci: &'a [RtcFocusInfo],
|
||||||
|
) -> &'a [RtcFocusInfo] {
|
||||||
|
if !self.foci.is_empty() {
|
||||||
|
&self.foci
|
||||||
|
} else {
|
||||||
|
deprecated_foci
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize)]
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
#[config_example_generator(filename = "conduwuit-example.toml", section = "global.ldap")]
|
#[config_example_generator(filename = "conduwuit-example.toml", section = "global.ldap")]
|
||||||
pub struct LdapConfig {
|
pub struct LdapConfig {
|
||||||
@@ -2358,6 +2419,7 @@ const DEPRECATED_KEYS: &[&str] = &[
|
|||||||
"well_known_support_email",
|
"well_known_support_email",
|
||||||
"well_known_support_mxid",
|
"well_known_support_mxid",
|
||||||
"registration_token_file",
|
"registration_token_file",
|
||||||
|
"well_known.rtc_focus_server_urls",
|
||||||
];
|
];
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@@ -2540,6 +2602,12 @@ fn default_pusher_idle_timeout() -> u64 { 15 }
|
|||||||
|
|
||||||
fn default_max_fetch_prev_events() -> u16 { 192_u16 }
|
fn default_max_fetch_prev_events() -> u16 { 192_u16 }
|
||||||
|
|
||||||
|
fn default_max_concurrent_inbound_transactions() -> usize { 150 }
|
||||||
|
|
||||||
|
fn default_transaction_id_cache_max_age_secs() -> u64 { 60 * 60 * 2 }
|
||||||
|
|
||||||
|
fn default_transaction_id_cache_max_entries() -> usize { 8192 }
|
||||||
|
|
||||||
fn default_tracing_flame_filter() -> String {
|
fn default_tracing_flame_filter() -> String {
|
||||||
cfg!(debug_assertions)
|
cfg!(debug_assertions)
|
||||||
.then_some("trace,h2=off")
|
.then_some("trace,h2=off")
|
||||||
@@ -2635,9 +2703,6 @@ fn default_rocksdb_stats_level() -> u8 { 1 }
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn default_default_room_version() -> RoomVersionId { RoomVersionId::V11 }
|
pub fn default_default_room_version() -> RoomVersionId { RoomVersionId::V11 }
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn default_rtc_focus_urls() -> Vec<RtcFocusInfo> { vec![] }
|
|
||||||
|
|
||||||
fn default_ip_range_denylist() -> Vec<String> {
|
fn default_ip_range_denylist() -> Vec<String> {
|
||||||
vec![
|
vec![
|
||||||
"127.0.0.0/8".to_owned(),
|
"127.0.0.0/8".to_owned(),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ static SEMANTIC: &str = env!("CARGO_PKG_VERSION");
|
|||||||
static VERSION: OnceLock<String> = OnceLock::new();
|
static VERSION: OnceLock<String> = OnceLock::new();
|
||||||
static VERSION_UA: OnceLock<String> = OnceLock::new();
|
static VERSION_UA: OnceLock<String> = OnceLock::new();
|
||||||
static USER_AGENT: OnceLock<String> = OnceLock::new();
|
static USER_AGENT: OnceLock<String> = OnceLock::new();
|
||||||
|
static USER_AGENT_MEDIA: OnceLock<String> = OnceLock::new();
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@@ -21,14 +22,22 @@ pub fn name() -> &'static str { BRANDING }
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn version() -> &'static str { VERSION.get_or_init(init_version) }
|
pub fn version() -> &'static str { VERSION.get_or_init(init_version) }
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn version_ua() -> &'static str { VERSION_UA.get_or_init(init_version_ua) }
|
pub fn version_ua() -> &'static str { VERSION_UA.get_or_init(init_version_ua) }
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn user_agent() -> &'static str { USER_AGENT.get_or_init(init_user_agent) }
|
pub fn user_agent() -> &'static str { USER_AGENT.get_or_init(init_user_agent) }
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn user_agent_media() -> &'static str { USER_AGENT_MEDIA.get_or_init(init_user_agent_media) }
|
||||||
|
|
||||||
fn init_user_agent() -> String { format!("{}/{} (bot; +{WEBSITE})", name(), version_ua()) }
|
fn init_user_agent() -> String { format!("{}/{} (bot; +{WEBSITE})", name(), version_ua()) }
|
||||||
|
|
||||||
|
fn init_user_agent_media() -> String {
|
||||||
|
format!("{}/{} (embedbot; facebookexternalhit/1.1; +{WEBSITE})", name(), version_ua())
|
||||||
|
}
|
||||||
|
|
||||||
fn init_version_ua() -> String {
|
fn init_version_ua() -> String {
|
||||||
conduwuit_build_metadata::version_tag()
|
conduwuit_build_metadata::version_tag()
|
||||||
.map_or_else(|| SEMANTIC.to_owned(), |extra| format!("{SEMANTIC}+{extra}"))
|
.map_or_else(|| SEMANTIC.to_owned(), |extra| format!("{SEMANTIC}+{extra}"))
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use arrayvec::ArrayString;
|
use arrayvec::ArrayString;
|
||||||
use rand::{Rng, RngExt, seq::SliceRandom};
|
use rand::{RngExt, seq::SliceRandom};
|
||||||
|
|
||||||
pub fn shuffle<T>(vec: &mut [T]) {
|
pub fn shuffle<T>(vec: &mut [T]) {
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand::rng();
|
||||||
|
|||||||
@@ -3,19 +3,17 @@ use futures::{
|
|||||||
stream::{Stream, TryStream},
|
stream::{Stream, TryStream},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{Error, Result};
|
|
||||||
|
|
||||||
pub trait IterStream<I: IntoIterator + Send> {
|
pub trait IterStream<I: IntoIterator + Send> {
|
||||||
/// Convert an Iterator into a Stream
|
/// Convert an Iterator into a Stream
|
||||||
fn stream(self) -> impl Stream<Item = <I as IntoIterator>::Item> + Send;
|
fn stream(self) -> impl Stream<Item = <I as IntoIterator>::Item> + Send;
|
||||||
|
|
||||||
/// Convert an Iterator into a TryStream
|
/// Convert an Iterator into a TryStream with a generic error type
|
||||||
fn try_stream(
|
fn try_stream<E>(
|
||||||
self,
|
self,
|
||||||
) -> impl TryStream<
|
) -> impl TryStream<
|
||||||
Ok = <I as IntoIterator>::Item,
|
Ok = <I as IntoIterator>::Item,
|
||||||
Error = Error,
|
Error = E,
|
||||||
Item = Result<<I as IntoIterator>::Item, Error>,
|
Item = Result<<I as IntoIterator>::Item, E>,
|
||||||
> + Send;
|
> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,12 +26,12 @@ where
|
|||||||
fn stream(self) -> impl Stream<Item = <I as IntoIterator>::Item> + Send { stream::iter(self) }
|
fn stream(self) -> impl Stream<Item = <I as IntoIterator>::Item> + Send { stream::iter(self) }
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn try_stream(
|
fn try_stream<E>(
|
||||||
self,
|
self,
|
||||||
) -> impl TryStream<
|
) -> impl TryStream<
|
||||||
Ok = <I as IntoIterator>::Item,
|
Ok = <I as IntoIterator>::Item,
|
||||||
Error = Error,
|
Error = E,
|
||||||
Item = Result<<I as IntoIterator>::Item, Error>,
|
Item = Result<<I as IntoIterator>::Item, E>,
|
||||||
> + Send {
|
> + Send {
|
||||||
self.stream().map(Ok)
|
self.stream().map(Ok)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
//! Synchronous combinator extensions to futures::TryStream
|
//! Synchronous combinator extensions to futures::TryStream
|
||||||
|
|
||||||
|
use std::result::Result;
|
||||||
|
|
||||||
use futures::{TryFuture, TryStream, TryStreamExt};
|
use futures::{TryFuture, TryStream, TryStreamExt};
|
||||||
|
|
||||||
use super::automatic_width;
|
use super::automatic_width;
|
||||||
use crate::Result;
|
|
||||||
|
|
||||||
/// Concurrency extensions to augment futures::TryStreamExt. broad_ combinators
|
/// Concurrency extensions to augment futures::TryStreamExt. broad_ combinators
|
||||||
/// produce out-of-order
|
/// produce out-of-order
|
||||||
|
|||||||
@@ -362,6 +362,10 @@ pub(super) static MAPS: &[Descriptor] = &[
|
|||||||
name: "userid_blurhash",
|
name: "userid_blurhash",
|
||||||
..descriptor::RANDOM_SMALL
|
..descriptor::RANDOM_SMALL
|
||||||
},
|
},
|
||||||
|
Descriptor {
|
||||||
|
name: "userid_dehydrateddevice",
|
||||||
|
..descriptor::RANDOM_SMALL
|
||||||
|
},
|
||||||
Descriptor {
|
Descriptor {
|
||||||
name: "userid_devicelistversion",
|
name: "userid_devicelistversion",
|
||||||
..descriptor::RANDOM_SMALL
|
..descriptor::RANDOM_SMALL
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ use std::{sync::Arc, time::Duration};
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use conduwuit::{Result, Server, debug, error, warn};
|
use conduwuit::{Result, Server, debug, error, warn};
|
||||||
use database::{Deserialized, Map};
|
use database::{Deserialized, Map};
|
||||||
use rand::Rng;
|
|
||||||
use ruma::events::{Mentions, room::message::RoomMessageEventContent};
|
use ruma::events::{Mentions, room::message::RoomMessageEventContent};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ impl crate::Service for Service {
|
|||||||
let url_preview_user_agent = config
|
let url_preview_user_agent = config
|
||||||
.url_preview_user_agent
|
.url_preview_user_agent
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| conduwuit::version::user_agent().to_owned());
|
.unwrap_or_else(|| conduwuit::version::user_agent_media().to_owned());
|
||||||
|
|
||||||
Ok(Arc::new(Self {
|
Ok(Arc::new(Self {
|
||||||
default: base(config)?
|
default: base(config)?
|
||||||
|
|||||||
@@ -170,6 +170,8 @@ impl Data {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) async fn clear_url_previews(&self) { self.url_previews.clear().await; }
|
||||||
|
|
||||||
pub(super) fn set_url_preview(
|
pub(super) fn set_url_preview(
|
||||||
&self,
|
&self,
|
||||||
url: &str,
|
url: &str,
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ pub async fn remove_url_preview(&self, url: &str) -> Result<()> {
|
|||||||
self.db.remove_url_preview(url)
|
self.db.remove_url_preview(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[implement(Service)]
|
||||||
|
pub async fn clear_url_previews(&self) { self.db.clear_url_previews().await; }
|
||||||
|
|
||||||
#[implement(Service)]
|
#[implement(Service)]
|
||||||
pub async fn set_url_preview(&self, url: &str, data: &UrlPreviewData) -> Result<()> {
|
pub async fn set_url_preview(&self, url: &str, data: &UrlPreviewData) -> Result<()> {
|
||||||
let now = SystemTime::now()
|
let now = SystemTime::now()
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ pub mod rooms;
|
|||||||
pub mod sending;
|
pub mod sending;
|
||||||
pub mod server_keys;
|
pub mod server_keys;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
pub mod transaction_ids;
|
pub mod transactions;
|
||||||
pub mod uiaa;
|
pub mod uiaa;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
|
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ async fn get_auth_chain_outer(
|
|||||||
|
|
||||||
let chunk_cache: Vec<_> = chunk
|
let chunk_cache: Vec<_> = chunk
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.try_stream()
|
.try_stream::<conduwuit::Error>()
|
||||||
.broad_and_then(|(shortid, event_id)| async move {
|
.broad_and_then(|(shortid, event_id)| async move {
|
||||||
if let Ok(cached) = self.get_cached_eventid_authchain(&[shortid]).await {
|
if let Ok(cached) = self.get_cached_eventid_authchain(&[shortid]).await {
|
||||||
return Ok(cached.to_vec());
|
return Ok(cached.to_vec());
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ where
|
|||||||
},
|
},
|
||||||
| hash_map::Entry::Occupied(_) => {
|
| hash_map::Entry::Occupied(_) => {
|
||||||
return Err!(Database(
|
return Err!(Database(
|
||||||
"State event's type and state_key combination exists multiple times.",
|
"State event's type and state_key combination exists multiple times: {}, {}",
|
||||||
|
pdu.kind(),
|
||||||
|
state_key
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,7 +162,9 @@ where
|
|||||||
},
|
},
|
||||||
| hash_map::Entry::Occupied(_) => {
|
| hash_map::Entry::Occupied(_) => {
|
||||||
return Err!(Request(InvalidParam(
|
return Err!(Request(InvalidParam(
|
||||||
"Auth event's type and state_key combination exists multiple times.",
|
"Auth event's type and state_key combination exists multiple times: {}, {}",
|
||||||
|
auth_event.kind,
|
||||||
|
auth_event.state_key().unwrap_or("")
|
||||||
)));
|
)));
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use std::{
|
|||||||
|
|
||||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||||
use conduwuit_core::{
|
use conduwuit_core::{
|
||||||
Error, Event, Result, debug, err, error,
|
Error, Event, Result, at, debug, err, error,
|
||||||
result::LogErr,
|
result::LogErr,
|
||||||
trace,
|
trace,
|
||||||
utils::{
|
utils::{
|
||||||
@@ -175,7 +175,7 @@ impl Service {
|
|||||||
if !new_events.is_empty() {
|
if !new_events.is_empty() {
|
||||||
self.db.mark_as_active(new_events.iter());
|
self.db.mark_as_active(new_events.iter());
|
||||||
|
|
||||||
let new_events_vec = new_events.into_iter().map(|(_, event)| event).collect();
|
let new_events_vec = new_events.into_iter().map(at!(1)).collect();
|
||||||
futures.push(self.send_events(dest.clone(), new_events_vec));
|
futures.push(self.send_events(dest.clone(), new_events_vec));
|
||||||
} else {
|
} else {
|
||||||
statuses.remove(dest);
|
statuses.remove(dest);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use crate::{
|
|||||||
media, moderation, presence, pusher, registration_tokens, resolver, rooms, sending,
|
media, moderation, presence, pusher, registration_tokens, resolver, rooms, sending,
|
||||||
server_keys,
|
server_keys,
|
||||||
service::{self, Args, Map, Service},
|
service::{self, Args, Map, Service},
|
||||||
sync, transaction_ids, uiaa, users,
|
sync, transactions, uiaa, users,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Services {
|
pub struct Services {
|
||||||
@@ -37,7 +37,7 @@ pub struct Services {
|
|||||||
pub sending: Arc<sending::Service>,
|
pub sending: Arc<sending::Service>,
|
||||||
pub server_keys: Arc<server_keys::Service>,
|
pub server_keys: Arc<server_keys::Service>,
|
||||||
pub sync: Arc<sync::Service>,
|
pub sync: Arc<sync::Service>,
|
||||||
pub transaction_ids: Arc<transaction_ids::Service>,
|
pub transactions: Arc<transactions::Service>,
|
||||||
pub uiaa: Arc<uiaa::Service>,
|
pub uiaa: Arc<uiaa::Service>,
|
||||||
pub users: Arc<users::Service>,
|
pub users: Arc<users::Service>,
|
||||||
pub moderation: Arc<moderation::Service>,
|
pub moderation: Arc<moderation::Service>,
|
||||||
@@ -110,7 +110,7 @@ impl Services {
|
|||||||
sending: build!(sending::Service),
|
sending: build!(sending::Service),
|
||||||
server_keys: build!(server_keys::Service),
|
server_keys: build!(server_keys::Service),
|
||||||
sync: build!(sync::Service),
|
sync: build!(sync::Service),
|
||||||
transaction_ids: build!(transaction_ids::Service),
|
transactions: build!(transactions::Service),
|
||||||
uiaa: build!(uiaa::Service),
|
uiaa: build!(uiaa::Service),
|
||||||
users: build!(users::Service),
|
users: build!(users::Service),
|
||||||
moderation: build!(moderation::Service),
|
moderation: build!(moderation::Service),
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use conduwuit::{Result, implement};
|
|
||||||
use database::{Handle, Map};
|
|
||||||
use ruma::{DeviceId, TransactionId, UserId};
|
|
||||||
|
|
||||||
pub struct Service {
|
|
||||||
db: Data,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Data {
|
|
||||||
userdevicetxnid_response: Arc<Map>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl crate::Service for Service {
|
|
||||||
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
|
||||||
Ok(Arc::new(Self {
|
|
||||||
db: Data {
|
|
||||||
userdevicetxnid_response: args.db["userdevicetxnid_response"].clone(),
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[implement(Service)]
|
|
||||||
pub fn add_txnid(
|
|
||||||
&self,
|
|
||||||
user_id: &UserId,
|
|
||||||
device_id: Option<&DeviceId>,
|
|
||||||
txn_id: &TransactionId,
|
|
||||||
data: &[u8],
|
|
||||||
) {
|
|
||||||
let mut key = user_id.as_bytes().to_vec();
|
|
||||||
key.push(0xFF);
|
|
||||||
key.extend_from_slice(device_id.map(DeviceId::as_bytes).unwrap_or_default());
|
|
||||||
key.push(0xFF);
|
|
||||||
key.extend_from_slice(txn_id.as_bytes());
|
|
||||||
|
|
||||||
self.db.userdevicetxnid_response.insert(&key, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's no entry, this is a new transaction
|
|
||||||
#[implement(Service)]
|
|
||||||
pub async fn existing_txnid(
|
|
||||||
&self,
|
|
||||||
user_id: &UserId,
|
|
||||||
device_id: Option<&DeviceId>,
|
|
||||||
txn_id: &TransactionId,
|
|
||||||
) -> Result<Handle<'_>> {
|
|
||||||
let key = (user_id, device_id, txn_id);
|
|
||||||
self.db.userdevicetxnid_response.qry(&key).await
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,326 @@
|
|||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fmt,
|
||||||
|
sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicU64, Ordering},
|
||||||
|
},
|
||||||
|
time::{Duration, SystemTime},
|
||||||
|
};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use conduwuit::{Error, Result, SyncRwLock, debug_warn, warn};
|
||||||
|
use database::{Handle, Map};
|
||||||
|
use ruma::{
|
||||||
|
DeviceId, OwnedServerName, OwnedTransactionId, TransactionId, UserId,
|
||||||
|
api::{
|
||||||
|
client::error::ErrorKind::LimitExceeded,
|
||||||
|
federation::transactions::send_transaction_message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use tokio::sync::watch::{Receiver, Sender};
|
||||||
|
|
||||||
|
use crate::{Dep, config};
|
||||||
|
|
||||||
|
pub type TxnKey = (OwnedServerName, OwnedTransactionId);
|
||||||
|
pub type WrappedTransactionResponse =
|
||||||
|
Option<Result<send_transaction_message::v1::Response, TransactionError>>;
|
||||||
|
|
||||||
|
/// Errors that can occur during federation transaction processing.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TransactionError {
|
||||||
|
/// Server is shutting down - the sender should retry the entire
|
||||||
|
/// transaction.
|
||||||
|
ShuttingDown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TransactionError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
| Self::ShuttingDown => write!(f, "Server is shutting down"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for TransactionError {}
|
||||||
|
|
||||||
|
/// Minimum interval between cache cleanup runs.
|
||||||
|
/// Exists to prevent thrashing when the cache is full of things that can't be
|
||||||
|
/// cleared
|
||||||
|
const CLEANUP_INTERVAL_SECS: u64 = 30;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct CachedTxnResponse {
|
||||||
|
pub response: send_transaction_message::v1::Response,
|
||||||
|
pub created: SystemTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal state for a federation transaction.
|
||||||
|
/// Either actively being processed or completed and cached.
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum TxnState {
|
||||||
|
/// Transaction is currently being processed.
|
||||||
|
Active(Receiver<WrappedTransactionResponse>),
|
||||||
|
|
||||||
|
/// Transaction completed and response is cached.
|
||||||
|
Cached(CachedTxnResponse),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of atomically checking or starting a federation transaction.
|
||||||
|
pub enum FederationTxnState {
|
||||||
|
/// Transaction already completed and cached
|
||||||
|
Cached(send_transaction_message::v1::Response),
|
||||||
|
|
||||||
|
/// Transaction is currently being processed by another request.
|
||||||
|
/// Wait on this receiver for the result.
|
||||||
|
Active(Receiver<WrappedTransactionResponse>),
|
||||||
|
|
||||||
|
/// This caller should process the transaction (first to request it).
|
||||||
|
Started {
|
||||||
|
receiver: Receiver<WrappedTransactionResponse>,
|
||||||
|
sender: Sender<WrappedTransactionResponse>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Service {
|
||||||
|
services: Services,
|
||||||
|
db: Data,
|
||||||
|
federation_txn_state: Arc<SyncRwLock<HashMap<TxnKey, TxnState>>>,
|
||||||
|
last_cleanup: AtomicU64,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Services {
|
||||||
|
config: Dep<config::Service>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Data {
|
||||||
|
userdevicetxnid_response: Arc<Map>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl crate::Service for Service {
|
||||||
|
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||||
|
Ok(Arc::new(Self {
|
||||||
|
services: Services {
|
||||||
|
config: args.depend::<config::Service>("config"),
|
||||||
|
},
|
||||||
|
db: Data {
|
||||||
|
userdevicetxnid_response: args.db["userdevicetxnid_response"].clone(),
|
||||||
|
},
|
||||||
|
federation_txn_state: Arc::new(SyncRwLock::new(HashMap::new())),
|
||||||
|
last_cleanup: AtomicU64::new(0),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn clear_cache(&self) {
|
||||||
|
let mut state = self.federation_txn_state.write();
|
||||||
|
// Only clear cached entries, preserve active transactions
|
||||||
|
state.retain(|_, v| matches!(v, TxnState::Active(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Service {
|
||||||
|
/// Returns the count of currently active (in-progress) transactions.
|
||||||
|
#[must_use]
|
||||||
|
pub fn txn_active_handle_count(&self) -> usize {
|
||||||
|
let state = self.federation_txn_state.read();
|
||||||
|
state
|
||||||
|
.values()
|
||||||
|
.filter(|v| matches!(v, TxnState::Active(_)))
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_client_txnid(
|
||||||
|
&self,
|
||||||
|
user_id: &UserId,
|
||||||
|
device_id: Option<&DeviceId>,
|
||||||
|
txn_id: &TransactionId,
|
||||||
|
data: &[u8],
|
||||||
|
) {
|
||||||
|
let mut key = user_id.as_bytes().to_vec();
|
||||||
|
key.push(0xFF);
|
||||||
|
key.extend_from_slice(device_id.map(DeviceId::as_bytes).unwrap_or_default());
|
||||||
|
key.push(0xFF);
|
||||||
|
key.extend_from_slice(txn_id.as_bytes());
|
||||||
|
|
||||||
|
self.db.userdevicetxnid_response.insert(&key, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_client_txn(
|
||||||
|
&self,
|
||||||
|
user_id: &UserId,
|
||||||
|
device_id: Option<&DeviceId>,
|
||||||
|
txn_id: &TransactionId,
|
||||||
|
) -> Result<Handle<'_>> {
|
||||||
|
let key = (user_id, device_id, txn_id);
|
||||||
|
self.db.userdevicetxnid_response.qry(&key).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Atomically gets a cached response, joins an active transaction, or
|
||||||
|
/// starts a new one.
|
||||||
|
pub fn get_or_start_federation_txn(&self, key: TxnKey) -> Result<FederationTxnState> {
|
||||||
|
// Only one upgradable lock can be held at a time, and there aren't any
|
||||||
|
// read-only locks, so no point being upgradable
|
||||||
|
let mut state = self.federation_txn_state.write();
|
||||||
|
|
||||||
|
// Check existing state for this key
|
||||||
|
if let Some(txn_state) = state.get(&key) {
|
||||||
|
return Ok(match txn_state {
|
||||||
|
| TxnState::Cached(cached) => FederationTxnState::Cached(cached.response.clone()),
|
||||||
|
| TxnState::Active(receiver) => FederationTxnState::Active(receiver.clone()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if another transaction from this origin is already running
|
||||||
|
let has_active_from_origin = state
|
||||||
|
.iter()
|
||||||
|
.any(|(k, v)| k.0 == key.0 && matches!(v, TxnState::Active(_)));
|
||||||
|
|
||||||
|
if has_active_from_origin {
|
||||||
|
debug_warn!(
|
||||||
|
origin = ?key.0,
|
||||||
|
"Got concurrent transaction request from an origin with an active transaction"
|
||||||
|
);
|
||||||
|
return Err(Error::BadRequest(
|
||||||
|
LimitExceeded { retry_after: None },
|
||||||
|
"Still processing another transaction from this origin",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_active_txns = self.services.config.max_concurrent_inbound_transactions;
|
||||||
|
|
||||||
|
// Check if we're at capacity
|
||||||
|
if state.len() >= max_active_txns
|
||||||
|
&& let active_count = state
|
||||||
|
.values()
|
||||||
|
.filter(|v| matches!(v, TxnState::Active(_)))
|
||||||
|
.count() && active_count >= max_active_txns
|
||||||
|
{
|
||||||
|
warn!(
|
||||||
|
active = active_count,
|
||||||
|
max = max_active_txns,
|
||||||
|
"Server is overloaded, dropping incoming transaction"
|
||||||
|
);
|
||||||
|
return Err(Error::BadRequest(
|
||||||
|
LimitExceeded { retry_after: None },
|
||||||
|
"Server is overloaded, try again later",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start new transaction
|
||||||
|
let (sender, receiver) = tokio::sync::watch::channel(None);
|
||||||
|
state.insert(key, TxnState::Active(receiver.clone()));
|
||||||
|
|
||||||
|
Ok(FederationTxnState::Started { receiver, sender })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finishes a transaction by transitioning it from active to cached state.
|
||||||
|
/// Additionally may trigger cleanup of old entries.
|
||||||
|
pub fn finish_federation_txn(
|
||||||
|
&self,
|
||||||
|
key: TxnKey,
|
||||||
|
sender: Sender<WrappedTransactionResponse>,
|
||||||
|
response: send_transaction_message::v1::Response,
|
||||||
|
) {
|
||||||
|
// Check if cleanup might be needed before acquiring the lock
|
||||||
|
let should_try_cleanup = self.should_try_cleanup();
|
||||||
|
|
||||||
|
let mut state = self.federation_txn_state.write();
|
||||||
|
|
||||||
|
// Explicitly set cached first so there is no gap where receivers get a closed
|
||||||
|
// channel
|
||||||
|
state.insert(
|
||||||
|
key,
|
||||||
|
TxnState::Cached(CachedTxnResponse {
|
||||||
|
response: response.clone(),
|
||||||
|
created: SystemTime::now(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(e) = sender.send(Some(Ok(response))) {
|
||||||
|
debug_warn!("Failed to send transaction response to waiting receivers: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicitly close
|
||||||
|
drop(sender);
|
||||||
|
|
||||||
|
// This task is dangling, we can try clean caches now
|
||||||
|
if should_try_cleanup {
|
||||||
|
self.cleanup_entries_locked(&mut state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_federation_txn(&self, key: &TxnKey) {
|
||||||
|
let mut state = self.federation_txn_state.write();
|
||||||
|
state.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if enough time has passed since the last cleanup to consider
|
||||||
|
/// running another. Updates the last cleanup time if returning true.
|
||||||
|
fn should_try_cleanup(&self) -> bool {
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.expect("SystemTime before UNIX_EPOCH")
|
||||||
|
.as_secs();
|
||||||
|
let last = self.last_cleanup.load(Ordering::Relaxed);
|
||||||
|
|
||||||
|
if now.saturating_sub(last) >= CLEANUP_INTERVAL_SECS {
|
||||||
|
// CAS: only update if no one else has updated it since we read
|
||||||
|
self.last_cleanup
|
||||||
|
.compare_exchange(last, now, Ordering::Relaxed, Ordering::Relaxed)
|
||||||
|
.is_ok()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cleans up cached entries based on age and count limits.
|
||||||
|
///
|
||||||
|
/// First removes all cached entries older than the configured max age.
|
||||||
|
/// Then, if the cache still exceeds the max entry count, removes the oldest
|
||||||
|
/// cached entries until the count is within limits.
|
||||||
|
///
|
||||||
|
/// Must be called with write lock held on the state map.
|
||||||
|
fn cleanup_entries_locked(&self, state: &mut HashMap<TxnKey, TxnState>) {
|
||||||
|
let max_age_secs = self.services.config.transaction_id_cache_max_age_secs;
|
||||||
|
let max_entries = self.services.config.transaction_id_cache_max_entries;
|
||||||
|
|
||||||
|
// First pass: remove all cached entries older than max age
|
||||||
|
let cutoff = SystemTime::now()
|
||||||
|
.checked_sub(Duration::from_secs(max_age_secs))
|
||||||
|
.unwrap_or(SystemTime::UNIX_EPOCH);
|
||||||
|
|
||||||
|
state.retain(|_, v| match v {
|
||||||
|
| TxnState::Active(_) => true, // Never remove active transactions
|
||||||
|
| TxnState::Cached(cached) => cached.created > cutoff,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Count cached entries
|
||||||
|
let cached_count = state
|
||||||
|
.values()
|
||||||
|
.filter(|v| matches!(v, TxnState::Cached(_)))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
// Second pass: if still over max entries, remove oldest cached entries
|
||||||
|
if cached_count > max_entries {
|
||||||
|
let excess = cached_count.saturating_sub(max_entries);
|
||||||
|
|
||||||
|
// Collect cached entries sorted by age (oldest first)
|
||||||
|
let mut cached_entries: Vec<_> = state
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(k, v)| match v {
|
||||||
|
| TxnState::Cached(cached) => Some((k.clone(), cached.created)),
|
||||||
|
| TxnState::Active(_) => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
cached_entries.sort_by(|a, b| a.1.cmp(&b.1));
|
||||||
|
|
||||||
|
// Remove the oldest cached entries to get under the limit
|
||||||
|
for (key, _) in cached_entries.into_iter().take(excess) {
|
||||||
|
state.remove(&key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
use conduwuit::{Err, Result, implement, trace};
|
||||||
|
use conduwuit_database::{Deserialized, Json};
|
||||||
|
use ruma::{
|
||||||
|
DeviceId, OwnedDeviceId, UserId,
|
||||||
|
api::client::dehydrated_device::{
|
||||||
|
DehydratedDeviceData, put_dehydrated_device::unstable::Request,
|
||||||
|
},
|
||||||
|
serde::Raw,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct DehydratedDevice {
|
||||||
|
/// Unique ID of the device.
|
||||||
|
pub device_id: OwnedDeviceId,
|
||||||
|
|
||||||
|
/// Contains serialized and encrypted private data.
|
||||||
|
pub device_data: Raw<DehydratedDeviceData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates or recreates the user's dehydrated device.
|
||||||
|
#[implement(super::Service)]
|
||||||
|
#[tracing::instrument(
|
||||||
|
level = "info",
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
%user_id,
|
||||||
|
device_id = %request.device_id,
|
||||||
|
display_name = ?request.initial_device_display_name,
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn set_dehydrated_device(&self, user_id: &UserId, request: Request) -> Result {
|
||||||
|
assert!(
|
||||||
|
self.exists(user_id).await,
|
||||||
|
"Tried to create dehydrated device for non-existent user"
|
||||||
|
);
|
||||||
|
|
||||||
|
let existing_id = self.get_dehydrated_device_id(user_id).await;
|
||||||
|
|
||||||
|
if existing_id.is_err()
|
||||||
|
&& self
|
||||||
|
.get_device_metadata(user_id, &request.device_id)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
return Err!("A hydrated device already exists with that ID.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(existing_id) = existing_id {
|
||||||
|
self.remove_device(user_id, &existing_id).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.create_device(
|
||||||
|
user_id,
|
||||||
|
&request.device_id,
|
||||||
|
"",
|
||||||
|
request.initial_device_display_name.clone(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
trace!(device_data = ?request.device_data);
|
||||||
|
self.db.userid_dehydrateddevice.raw_put(
|
||||||
|
user_id,
|
||||||
|
Json(&DehydratedDevice {
|
||||||
|
device_id: request.device_id.clone(),
|
||||||
|
device_data: request.device_data,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
trace!(device_keys = ?request.device_keys);
|
||||||
|
self.add_device_keys(user_id, &request.device_id, &request.device_keys)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
trace!(one_time_keys = ?request.one_time_keys);
|
||||||
|
for (one_time_key_key, one_time_key_value) in &request.one_time_keys {
|
||||||
|
self.add_one_time_key(user_id, &request.device_id, one_time_key_key, one_time_key_value)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a user's dehydrated device.
|
||||||
|
///
|
||||||
|
/// Calling this directly will remove the dehydrated data but leak the frontage
|
||||||
|
/// device. Thus this is called by the regular device interface such that the
|
||||||
|
/// dehydrated data will not leak instead.
|
||||||
|
///
|
||||||
|
/// If device_id is given, the user's dehydrated device must match or this is a
|
||||||
|
/// no-op, but an Err is still returned to indicate that. Otherwise returns the
|
||||||
|
/// removed dehydrated device_id.
|
||||||
|
#[implement(super::Service)]
|
||||||
|
#[tracing::instrument(
|
||||||
|
level = "debug",
|
||||||
|
skip_all,
|
||||||
|
fields(
|
||||||
|
%user_id,
|
||||||
|
device_id = ?maybe_device_id,
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub(super) async fn remove_dehydrated_device(
|
||||||
|
&self,
|
||||||
|
user_id: &UserId,
|
||||||
|
maybe_device_id: Option<&DeviceId>,
|
||||||
|
) -> Result<OwnedDeviceId> {
|
||||||
|
let Ok(device_id) = self.get_dehydrated_device_id(user_id).await else {
|
||||||
|
return Err!(Request(NotFound("No dehydrated device for this user.")));
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(maybe_device_id) = maybe_device_id {
|
||||||
|
if maybe_device_id != device_id {
|
||||||
|
return Err!(Request(NotFound("Not the user's dehydrated device.")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.db.userid_dehydrateddevice.remove(user_id);
|
||||||
|
|
||||||
|
Ok(device_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the device_id of the user's dehydrated device.
|
||||||
|
#[implement(super::Service)]
|
||||||
|
#[tracing::instrument(
|
||||||
|
level = "debug",
|
||||||
|
skip_all,
|
||||||
|
fields(%user_id)
|
||||||
|
)]
|
||||||
|
pub async fn get_dehydrated_device_id(&self, user_id: &UserId) -> Result<OwnedDeviceId> {
|
||||||
|
self.get_dehydrated_device(user_id)
|
||||||
|
.await
|
||||||
|
.map(|device| device.device_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the dehydrated device private data
|
||||||
|
#[implement(super::Service)]
|
||||||
|
#[tracing::instrument(
|
||||||
|
level = "debug",
|
||||||
|
skip_all,
|
||||||
|
fields(%user_id),
|
||||||
|
ret,
|
||||||
|
)]
|
||||||
|
pub async fn get_dehydrated_device(&self, user_id: &UserId) -> Result<DehydratedDevice> {
|
||||||
|
self.db
|
||||||
|
.userid_dehydrateddevice
|
||||||
|
.get(user_id)
|
||||||
|
.await
|
||||||
|
.deserialized()
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
pub(super) mod dehydrated_device;
|
||||||
|
|
||||||
#[cfg(feature = "ldap")]
|
#[cfg(feature = "ldap")]
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::{collections::BTreeMap, mem, net::IpAddr, sync::Arc};
|
use std::{collections::BTreeMap, mem, net::IpAddr, sync::Arc};
|
||||||
@@ -5,7 +7,7 @@ use std::{collections::BTreeMap, mem, net::IpAddr, sync::Arc};
|
|||||||
#[cfg(feature = "ldap")]
|
#[cfg(feature = "ldap")]
|
||||||
use conduwuit::result::LogErr;
|
use conduwuit::result::LogErr;
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Err, Error, Result, Server, at, debug_warn, err, is_equal_to, trace,
|
Err, Error, Result, Server, debug_warn, err, is_equal_to, trace,
|
||||||
utils::{self, ReadyExt, stream::TryIgnore, string::Unquoted},
|
utils::{self, ReadyExt, stream::TryIgnore, string::Unquoted},
|
||||||
};
|
};
|
||||||
#[cfg(feature = "ldap")]
|
#[cfg(feature = "ldap")]
|
||||||
@@ -70,6 +72,7 @@ struct Data {
|
|||||||
userfilterid_filter: Arc<Map>,
|
userfilterid_filter: Arc<Map>,
|
||||||
userid_avatarurl: Arc<Map>,
|
userid_avatarurl: Arc<Map>,
|
||||||
userid_blurhash: Arc<Map>,
|
userid_blurhash: Arc<Map>,
|
||||||
|
userid_dehydrateddevice: Arc<Map>,
|
||||||
userid_devicelistversion: Arc<Map>,
|
userid_devicelistversion: Arc<Map>,
|
||||||
userid_displayname: Arc<Map>,
|
userid_displayname: Arc<Map>,
|
||||||
userid_lastonetimekeyupdate: Arc<Map>,
|
userid_lastonetimekeyupdate: Arc<Map>,
|
||||||
@@ -110,6 +113,7 @@ impl crate::Service for Service {
|
|||||||
userfilterid_filter: args.db["userfilterid_filter"].clone(),
|
userfilterid_filter: args.db["userfilterid_filter"].clone(),
|
||||||
userid_avatarurl: args.db["userid_avatarurl"].clone(),
|
userid_avatarurl: args.db["userid_avatarurl"].clone(),
|
||||||
userid_blurhash: args.db["userid_blurhash"].clone(),
|
userid_blurhash: args.db["userid_blurhash"].clone(),
|
||||||
|
userid_dehydrateddevice: args.db["userid_dehydrateddevice"].clone(),
|
||||||
userid_devicelistversion: args.db["userid_devicelistversion"].clone(),
|
userid_devicelistversion: args.db["userid_devicelistversion"].clone(),
|
||||||
userid_displayname: args.db["userid_displayname"].clone(),
|
userid_displayname: args.db["userid_displayname"].clone(),
|
||||||
userid_lastonetimekeyupdate: args.db["userid_lastonetimekeyupdate"].clone(),
|
userid_lastonetimekeyupdate: args.db["userid_lastonetimekeyupdate"].clone(),
|
||||||
@@ -184,6 +188,12 @@ impl Service {
|
|||||||
password: Option<&str>,
|
password: Option<&str>,
|
||||||
origin: Option<&str>,
|
origin: Option<&str>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
if !self.services.globals.user_is_local(user_id)
|
||||||
|
&& (password.is_some() || origin.is_some())
|
||||||
|
{
|
||||||
|
return Err!("Cannot create a nonlocal user with a set password or origin");
|
||||||
|
}
|
||||||
|
|
||||||
self.db
|
self.db
|
||||||
.userid_origin
|
.userid_origin
|
||||||
.insert(user_id, origin.unwrap_or("password"));
|
.insert(user_id, origin.unwrap_or("password"));
|
||||||
@@ -474,6 +484,11 @@ impl Service {
|
|||||||
|
|
||||||
/// Removes a device from a user.
|
/// Removes a device from a user.
|
||||||
pub async fn remove_device(&self, user_id: &UserId, device_id: &DeviceId) {
|
pub async fn remove_device(&self, user_id: &UserId, device_id: &DeviceId) {
|
||||||
|
// Remove dehydrated device if this is the dehydrated device
|
||||||
|
let _: Result<_> = self
|
||||||
|
.remove_dehydrated_device(user_id, Some(device_id))
|
||||||
|
.await;
|
||||||
|
|
||||||
let userdeviceid = (user_id, device_id);
|
let userdeviceid = (user_id, device_id);
|
||||||
|
|
||||||
// Remove tokens
|
// Remove tokens
|
||||||
@@ -997,7 +1012,7 @@ impl Service {
|
|||||||
device_id: &'a DeviceId,
|
device_id: &'a DeviceId,
|
||||||
since: Option<u64>,
|
since: Option<u64>,
|
||||||
to: Option<u64>,
|
to: Option<u64>,
|
||||||
) -> impl Stream<Item = Raw<AnyToDeviceEvent>> + Send + 'a {
|
) -> impl Stream<Item = (u64, Raw<AnyToDeviceEvent>)> + Send + 'a {
|
||||||
type Key<'a> = (&'a UserId, &'a DeviceId, u64);
|
type Key<'a> = (&'a UserId, &'a DeviceId, u64);
|
||||||
|
|
||||||
let from = (user_id, device_id, since.map_or(0, |since| since.saturating_add(1)));
|
let from = (user_id, device_id, since.map_or(0, |since| since.saturating_add(1)));
|
||||||
@@ -1011,7 +1026,7 @@ impl Service {
|
|||||||
&& device_id == *device_id_
|
&& device_id == *device_id_
|
||||||
&& to.is_none_or(|to| *count <= to)
|
&& to.is_none_or(|to| *count <= to)
|
||||||
})
|
})
|
||||||
.map(at!(1))
|
.map(|((_, _, count), event)| (count, event))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn remove_to_device_events<Until>(
|
pub async fn remove_to_device_events<Until>(
|
||||||
|
|||||||
Reference in New Issue
Block a user